uppy
Version:
Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:
241 lines (206 loc) • 6.97 kB
JavaScript
// Upload Uppy releases to tlcdn.com (CDN). Copyright (c) 2018, Transloadit Ltd.
//
// This file:
//
// - Assumes EDGLY_KEY and EDGLY_SECRET are available (e.g. set via Travis secrets)
// - Assumes a fully built uppy is in root dir (unless a specific tag was specified, then it's fetched from npm)
// - Collects dist/ files that would be in an npm package release, and uploads to
// eg. https://releases.transloadit.com/uppy/v1.0.1/uppy.css
// - Uses local package by default, if [version] argument was specified, takes package from npm
//
// Run as:
//
// yarn uploadcdn <package-name> [version]
//
// To override an existing release (DANGER!)
//
// yarn uploadcdn <package-name> [version] -- --force
//
// Authors:
//
// - Kevin van Zonneveld <kevin@transloadit.com>
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { finished, pipeline } from 'node:stream/promises'
import { fileURLToPath } from 'node:url'
import {
ListObjectsV2Command,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3'
import AdmZip from 'adm-zip'
import concat from 'concat-stream'
import mime from 'mime-types'
import packlist from 'npm-packlist'
import pacote from 'pacote'
import tar from 'tar'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
const AWS_REGION = 'us-east-1'
const AWS_BUCKET = 'releases.transloadit.com'
/**
* Get remote dist/ files by fetching the tarball for the given version
from npm and filtering it down to package/dist/ files.
*
* @param {string} packageName eg. @uppy/robodog
* @param {string} version eg. 1.8.0
* @returns a Map<string, Buffer>, filename → content
*/
async function getRemoteDistFiles(packageName, version) {
const pkg = `${packageName}@${version}`
console.log('Using npm package', pkg)
const files = new Map()
const tarball = await pacote.tarball.stream(pkg, (stream) =>
pipeline(stream, new tar.Parse()),
)
tarball.on('entry', (readEntry) => {
if (readEntry.path.startsWith('package/dist/')) {
readEntry
.pipe(
concat((buf) => {
files.set(readEntry.path.replace(/^package\/dist\//, ''), buf)
}),
)
.on('error', (err) => {
tarball.emit('error', err)
})
} else {
readEntry.resume()
}
})
await finished(tarball)
return files
}
/**
* Get local dist/ files by asking npm-packlist what files would be added
* to an npm package during publish, and filtering those down to just dist/ files.
*
* @param {string} packageName Name of package, eg. @uppy/locales
* @returns a Map<string, Buffer>, filename → content
*/
async function getLocalDistFiles(packageName) {
// Base file path of the package, eg. ./packages/@uppy/locales or ./packages/uppy
const packagePath = packageName.startsWith('@uppy/')
? path.join(__dirname, '..', packageName)
: path.join(__dirname)
console.log('Making local package from', packagePath)
const prefix = 'dist'
const files = (await packlist({ path: packagePath })).flatMap((f) => {
const prefixSlash = `${prefix}/`
if (f.startsWith(prefixSlash)) {
const name = f.split(prefixSlash)[1]
if (name.length > 0) {
return [name]
}
}
return []
})
const entries = await Promise.all(
files.map(async (f) => [
f,
await readFile(path.join(packagePath, prefix, f)),
]),
)
return new Map(entries)
}
async function main(packageName, version) {
if (!packageName) {
console.error('usage: upload-to-cdn <packagename> [version]')
console.error('Must provide a package name')
process.exit(1)
}
if (!process.env.EDGLY_KEY || !process.env.EDGLY_SECRET) {
console.error('Missing EDGLY_KEY or EDGLY_SECRET env variables, bailing')
process.exit(1)
}
// version should only be a positional arg and semver string
// this deals with usage like `npm run uploadcdn uppy -- --force`
// where we force push a local build
let resolvedVersion = version?.startsWith('-') ? undefined : version
const s3Client = new S3Client({
credentials: {
accessKeyId: process.env.EDGLY_KEY,
secretAccessKey: process.env.EDGLY_SECRET,
},
region: AWS_REGION,
})
const remote = !!resolvedVersion
console.log('Using', remote ? 'Remote' : 'Local', 'build')
if (!remote) {
// Updated path logic: now running from packages/uppy, need to reference the package directly
const packagePath = packageName.startsWith('@uppy/')
? path.join(__dirname, '..', packageName)
: path.join(__dirname)
const packageJsonContent = await readFile(
path.join(packagePath, 'package.json'),
'utf8',
)
resolvedVersion = JSON.parse(packageJsonContent).version
}
// Warn if uploading a local build not from CI:
// - If we're on CI, this should be a release commit.
// - If we're local, normally we should upload a released version, not a local build.
if (!remote && !process.env.CI) {
console.log(
'Warning, writing a local build to the CDN, this is usually not what you want. Sleeping 3s. Press CTRL+C!',
)
await delay(3000)
}
// uppy → releases/uppy/
// @uppy/robodog → releases/uppy/robodog/
// @uppy/locales → releases/uppy/locales/
const dirName = packageName.startsWith('@uppy/')
? packageName.replace(/^@/, '')
: 'uppy'
const s3Dir = path.posix.join(dirName, `v${resolvedVersion}`)
const { Contents: existing } = await s3Client.send(
new ListObjectsV2Command({
Bucket: AWS_BUCKET,
Prefix: s3Dir,
}),
)
if (existing?.length > 0) {
if (process.argv.includes('--force')) {
console.warn(
`WARN Release files for ${dirName} v${resolvedVersion} already exist, overwriting...`,
)
} else {
console.error(
`Release files for ${dirName} v${resolvedVersion} already exist, exiting...`,
)
process.exit(1)
}
}
const files = remote
? await getRemoteDistFiles(packageName, resolvedVersion)
: await getLocalDistFiles(packageName)
if (packageName === 'uppy') {
console.log('Creating downloadable zip archive')
const zip = new AdmZip()
for (const [filename, buffer] of files.entries()) {
zip.addFile(filename, buffer)
}
files.set(`uppy-v${resolvedVersion}.zip`, zip.toBuffer())
}
if (files.size === 0) console.warn('No files to upload')
for (const [filename, buffer] of files.entries()) {
const key = path.posix.join(s3Dir, filename)
console.log(`pushing s3://${AWS_BUCKET}/${key}`)
await s3Client.send(
new PutObjectCommand({
Bucket: AWS_BUCKET,
Key: key,
ContentType: mime.lookup(filename),
Body: buffer,
}),
)
}
}
main(...process.argv.slice(2)).catch((err) => {
console.error(err)
process.exit(1)
})