index.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/env node
  2. // Upload Uppy releases to tlcdn.com (CDN). Copyright (c) 2018, Transloadit Ltd.
  3. //
  4. // This file:
  5. //
  6. // - Assumes EDGLY_KEY and EDGLY_SECRET are available (e.g. set via Travis secrets)
  7. // - Assumes a fully built uppy is in root dir (unless a specific tag was specified, then it's fetched from npm)
  8. // - Collects dist/ files that would be in an npm package release, and uploads to
  9. // eg. https://releases.transloadit.com/uppy/v1.0.1/uppy.css
  10. // - Uses local package by default, if [version] argument was specified, takes package from npm
  11. //
  12. // Run as:
  13. //
  14. // yarn uploadcdn <package-name> [version]
  15. //
  16. // To override an existing release (DANGER!)
  17. //
  18. // yarn uploadcdn <package-name> [version] -- --force
  19. //
  20. // Authors:
  21. //
  22. // - Kevin van Zonneveld <kevin@transloadit.com>
  23. const path = require('node:path')
  24. const { pipeline, finished } = require('node:stream/promises')
  25. const { readFile } = require('node:fs/promises')
  26. const { S3Client, ListObjectsV2Command, PutObjectCommand } = require('@aws-sdk/client-s3');
  27. const packlist = require('npm-packlist')
  28. const tar = require('tar')
  29. const pacote = require('pacote')
  30. const concat = require('concat-stream')
  31. const mime = require('mime-types')
  32. const AdmZip = require('adm-zip')
  33. function delay (ms) {
  34. return new Promise(resolve => setTimeout(resolve, ms))
  35. }
  36. const AWS_REGION = 'us-east-1'
  37. const AWS_BUCKET = 'releases.transloadit.com'
  38. /**
  39. * Get remote dist/ files by fetching the tarball for the given version
  40. from npm and filtering it down to package/dist/ files.
  41. *
  42. * @param {string} packageName eg. @uppy/robodog
  43. * @param {string} version eg. 1.8.0
  44. * @returns a Map<string, Buffer>, filename → content
  45. */
  46. async function getRemoteDistFiles (packageName, version) {
  47. const files = new Map()
  48. const tarball = await pacote.tarball.stream(`${packageName}@${version}`, stream => pipeline(stream, new tar.Parse()))
  49. tarball.on('entry', (readEntry) => {
  50. if (readEntry.path.startsWith('package/dist/')) {
  51. readEntry
  52. .pipe(concat((buf) => {
  53. files.set(readEntry.path.replace(/^package\/dist\//, ''), buf)
  54. }))
  55. .on('error', (err) => {
  56. tarball.emit('error', err)
  57. })
  58. } else {
  59. readEntry.resume()
  60. }
  61. })
  62. await finished(tarball)
  63. return files
  64. }
  65. /**
  66. * Get local dist/ files by asking npm-packlist what files would be added
  67. * to an npm package during publish, and filtering those down to just dist/ files.
  68. *
  69. * @param {string} packagePath Base file path of the package, eg. ./packages/@uppy/locales
  70. * @returns a Map<string, Buffer>, filename → content
  71. */
  72. async function getLocalDistFiles (packagePath) {
  73. const files = (await packlist({ path: packagePath }))
  74. .filter(f => f.startsWith('dist/'))
  75. .map(f => f.replace(/^dist\//, ''))
  76. const entries = await Promise.all(
  77. files.map(async (f) => [
  78. f,
  79. await readFile(path.join(packagePath, 'dist', f)),
  80. ]),
  81. )
  82. return new Map(entries)
  83. }
  84. async function main (packageName, version) {
  85. if (!packageName) {
  86. console.error('usage: upload-to-cdn <packagename> [version]')
  87. console.error('Must provide a package name')
  88. process.exit(1)
  89. }
  90. if (!process.env.EDGLY_KEY || !process.env.EDGLY_SECRET) {
  91. console.error('Missing EDGLY_KEY or EDGLY_SECRET env variables, bailing')
  92. process.exit(1)
  93. }
  94. // version should only be a positional arg and semver string
  95. // this deals with usage like `npm run uploadcdn uppy -- --force`
  96. // where we force push a local build
  97. if (version?.startsWith('-')) version = undefined // eslint-disable-line no-param-reassign
  98. const s3Client = new S3Client({
  99. credentials: {
  100. accessKeyId: process.env.EDGLY_KEY,
  101. secretAccessKey: process.env.EDGLY_SECRET,
  102. },
  103. region: AWS_REGION,
  104. })
  105. const remote = !!version
  106. if (!remote) {
  107. // eslint-disable-next-line import/no-dynamic-require, global-require, no-param-reassign
  108. version = require(`../../packages/${packageName}/package.json`).version
  109. }
  110. // Warn if uploading a local build not from CI:
  111. // - If we're on CI, this should be a release commit.
  112. // - If we're local, normally we should upload a released version, not a local build.
  113. if (!remote && !process.env.CI) {
  114. console.log('Warning, writing a local build to the CDN, this is usually not what you want. Sleeping 3s. Press CTRL+C!')
  115. await delay(3000)
  116. }
  117. const packagePath = remote
  118. ? `${packageName}@${version}`
  119. : path.join(__dirname, '..', '..', 'packages', packageName)
  120. // uppy → releases/uppy/
  121. // @uppy/robodog → releases/uppy/robodog/
  122. // @uppy/locales → releases/uppy/locales/
  123. const dirName = packageName.startsWith('@uppy/')
  124. ? packageName.replace(/^@/, '')
  125. : 'uppy'
  126. const outputPath = path.posix.join(dirName, `v${version}`)
  127. const { Contents: existing } = await s3Client.send(new ListObjectsV2Command({
  128. Bucket: AWS_BUCKET,
  129. Prefix: outputPath,
  130. }))
  131. if (existing?.length > 0) {
  132. if (process.argv.includes('--force')) {
  133. console.warn(`WARN Release files for ${dirName} v${version} already exist, overwriting...`)
  134. } else {
  135. console.error(`Release files for ${dirName} v${version} already exist, exiting...`)
  136. process.exit(1)
  137. }
  138. }
  139. const files = remote
  140. ? await getRemoteDistFiles(packageName, version)
  141. : await getLocalDistFiles(packagePath)
  142. if (packageName === 'uppy') {
  143. // Create downloadable zip archive
  144. const zip = new AdmZip()
  145. for (const [filename, buffer] of files.entries()) {
  146. zip.addFile(filename, buffer)
  147. }
  148. files.set(`uppy-v${version}.zip`, zip.toBuffer())
  149. }
  150. for (const [filename, buffer] of files.entries()) {
  151. const key = path.posix.join(outputPath, filename)
  152. console.log(`pushing s3://${AWS_BUCKET}/${key}`)
  153. await s3Client.send(new PutObjectCommand({
  154. Bucket: AWS_BUCKET,
  155. Key: key,
  156. ContentType: mime.lookup(filename),
  157. Body: buffer,
  158. }))
  159. }
  160. }
  161. main(...process.argv.slice(2)).catch((err) => {
  162. console.error(err)
  163. process.exit(1)
  164. })