|
| 1 | +import path from 'path'; |
| 2 | +import { brotliCompressSync } from 'zlib'; |
| 3 | +import { inspect } from 'util'; |
| 4 | +import { |
| 5 | + DOWNLOADS_BUCKET, |
| 6 | + DOWNLOADS_BUCKET_PUBLIC_HOST, |
| 7 | + ENTRYPOINT_FILENAME, |
| 8 | + MANIFEST_FILENAME, |
| 9 | + RELEASE_COMMIT, |
| 10 | + asyncPutObject, |
| 11 | + getObjectKey, |
| 12 | +} from './utils.mts'; |
| 13 | + |
| 14 | +const publicManifestUrl = new URL( |
| 15 | + getObjectKey(MANIFEST_FILENAME), |
| 16 | + DOWNLOADS_BUCKET_PUBLIC_HOST |
| 17 | +); |
| 18 | + |
| 19 | +const publicEntrypointUrl = new URL( |
| 20 | + getObjectKey(ENTRYPOINT_FILENAME), |
| 21 | + DOWNLOADS_BUCKET_PUBLIC_HOST |
| 22 | +); |
| 23 | + |
| 24 | +let assets: URL[]; |
| 25 | + |
| 26 | +function assertResponseIsOk(res: Response) { |
| 27 | + if (res.status !== 200) { |
| 28 | + throw new Error( |
| 29 | + `Request returned a non-OK response: ${res.status} ${res.statusText}` |
| 30 | + ); |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +try { |
| 35 | + const res = await fetch(publicManifestUrl); |
| 36 | + assertResponseIsOk(res); |
| 37 | + const manifest = await res.json(); |
| 38 | + |
| 39 | + if ( |
| 40 | + !( |
| 41 | + Array.isArray(manifest) && |
| 42 | + manifest.every((asset) => { |
| 43 | + return typeof asset === 'string'; |
| 44 | + }) |
| 45 | + ) |
| 46 | + ) { |
| 47 | + throw new Error( |
| 48 | + `Manifest schema is not matching: expected string[], got ${inspect( |
| 49 | + manifest |
| 50 | + )}` |
| 51 | + ); |
| 52 | + } |
| 53 | + |
| 54 | + assets = manifest.map((asset) => { |
| 55 | + return new URL(getObjectKey(asset), DOWNLOADS_BUCKET_PUBLIC_HOST); |
| 56 | + }); |
| 57 | + |
| 58 | + await Promise.all( |
| 59 | + assets.map(async (assetUrl) => { |
| 60 | + const res = await fetch(assetUrl, { method: 'HEAD' }); |
| 61 | + assertResponseIsOk(res); |
| 62 | + }) |
| 63 | + ); |
| 64 | +} catch (err) { |
| 65 | + throw new AggregateError( |
| 66 | + [err], |
| 67 | + `Aborting publish, failed to resolve manifest ${publicManifestUrl}` |
| 68 | + ); |
| 69 | +} |
| 70 | + |
| 71 | +const ALLOWED_PUBLISH_ENVIRONMENTS = ['dev', 'qa', 'stage', 'prod']; |
| 72 | + |
| 73 | +const PUBLISH_ENVIRONMENT = process.env.COMPASS_WEB_PUBLISH_ENVIRONMENT; |
| 74 | + |
| 75 | +if (!ALLOWED_PUBLISH_ENVIRONMENTS.includes(PUBLISH_ENVIRONMENT ?? '')) { |
| 76 | + throw new Error( |
| 77 | + `Unknown publish environment: expected ${inspect( |
| 78 | + ALLOWED_PUBLISH_ENVIRONMENTS |
| 79 | + )}, got ${inspect(PUBLISH_ENVIRONMENT)}` |
| 80 | + ); |
| 81 | +} |
| 82 | + |
| 83 | +function buildProxyEntrypointFile() { |
| 84 | + return ( |
| 85 | + assets |
| 86 | + .map((asset) => { |
| 87 | + return `import ${JSON.stringify(asset)};`; |
| 88 | + }) |
| 89 | + .concat( |
| 90 | + `export * from ${JSON.stringify(publicEntrypointUrl)};`, |
| 91 | + `/** Compass version: https://github.com/mongodb-js/compass/tree/${RELEASE_COMMIT} */` |
| 92 | + ) |
| 93 | + .join('\n') + '\n' |
| 94 | + ); |
| 95 | +} |
| 96 | + |
| 97 | +const fileKey = getObjectKey('index.mjs', PUBLISH_ENVIRONMENT); |
| 98 | +const fileContent = buildProxyEntrypointFile(); |
| 99 | +const compressedFileContent = brotliCompressSync(fileContent); |
| 100 | + |
| 101 | +console.log( |
| 102 | + 'Uploading entrypoint to s3://%s/%s ...', |
| 103 | + DOWNLOADS_BUCKET, |
| 104 | + fileKey |
| 105 | +); |
| 106 | + |
| 107 | +const ENTRYPOINT_CACHE_MAX_AGE = 1000 * 60 * 3; // 3mins |
| 108 | + |
| 109 | +const res = await asyncPutObject({ |
| 110 | + ACL: 'private', |
| 111 | + Bucket: DOWNLOADS_BUCKET, |
| 112 | + Key: fileKey, |
| 113 | + Body: compressedFileContent, |
| 114 | + ContentType: 'text/javascript', |
| 115 | + ContentEncoding: 'br', |
| 116 | + ContentLength: compressedFileContent.byteLength, |
| 117 | + // "Latest" entrypoint files can change quite often, so max-age is quite |
| 118 | + // short and browser should always revalidate on stale |
| 119 | + CacheControl: `public, max-age=${ENTRYPOINT_CACHE_MAX_AGE}, must-revalidate`, |
| 120 | +}); |
| 121 | + |
| 122 | +console.log( |
| 123 | + 'Successfully uploaded %s (ETag: %s)', |
| 124 | + path.basename(fileKey), |
| 125 | + res.ETag |
| 126 | +); |
0 commit comments