From 672cc7b0302770756121f68d2f217dda9883ea2a Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 11 Sep 2024 18:30:28 +0200 Subject: [PATCH] build(preview): rewrite prepare-preview-buidlds in ts + support workspace packages --- package.json | 3 +- scripts/prepare-preview-builds.jq | 9 -- scripts/prepare-preview-builds.sh | 39 ------ scripts/prepare-preview-builds.ts | 225 ++++++++++++++++++++++++++++++ yarn.lock | 1 + 5 files changed, 228 insertions(+), 49 deletions(-) delete mode 100644 scripts/prepare-preview-builds.jq delete mode 100755 scripts/prepare-preview-builds.sh create mode 100755 scripts/prepare-preview-builds.ts diff --git a/package.json b/package.json index 69bc62e8..4a681675 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lint:eslint": "eslint . --cache --ext js,cjs,mjs,ts", "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies:fix", "lint:misc": "prettier '**/*.json' '**/*.md' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path .gitignore", - "prepare:preview": "./scripts/prepare-preview-builds.sh", + "prepare:preview": "ts-node scripts/prepare-preview-builds.ts", "prepare:preview:local": "yarn prepare:preview @metamask-previews $(git rev-parse --short HEAD)", "publish:preview": "yarn workspaces foreach --all --no-private --parallel --verbose run publish:preview", "readme:update": "ts-node scripts/update-readme-content.ts", @@ -48,6 +48,7 @@ "@metamask/eslint-config-jest": "^12.0.0", "@metamask/eslint-config-nodejs": "^12.0.0", "@metamask/eslint-config-typescript": "^12.0.0", + "@npmcli/package-json": "^5.0.0", "@types/jest": "^28.1.6", "@types/node": "^16", "@typescript-eslint/eslint-plugin": "^5.43.0", diff --git a/scripts/prepare-preview-builds.jq b/scripts/prepare-preview-builds.jq deleted file mode 100644 index a523e49f..00000000 --- a/scripts/prepare-preview-builds.jq +++ /dev/null @@ -1,9 +0,0 @@ -# The name is overwritten, causing the package to get published under a -# different NPM scope than non-preview builds. -.name |= sub("@metamask/"; "\($npm_scope)/") | - -# The prerelease version is overwritten, preserving the non-prerelease portion -# of the version. Technically we'd want to bump the non-prerelease portion as -# well if we wanted this to be SemVer-compliant, but it was simpler not to. -# This is just for testing, it doesn't need to strictly follow SemVer. -.version |= split("-")[0] + "-preview-\($hash)" diff --git a/scripts/prepare-preview-builds.sh b/scripts/prepare-preview-builds.sh deleted file mode 100755 index 886fabaf..00000000 --- a/scripts/prepare-preview-builds.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# This script prepares a package to be published as a preview build -# to GitHub Packages. - -if [[ $# -eq 0 ]]; then - echo "Missing commit hash." - exit 1 -fi - -# We don't want to assume that preview builds will be published alongside -# "production" versions. There are security- and aesthetic-based advantages to -# keeping them separate. -npm_scope="$1" - -# We use the short commit hash as the prerelease version. This ensures each -# preview build is unique and can be linked to a specific commit. -shorthash="$2" - -prepare-preview-manifest() { - local manifest_file="$1" - - # jq does not support in-place modification of files, so a temporary file is - # used to store the result of the operation. The original file is then - # overwritten with the temporary file. - jq --raw-output --arg npm_scope "$npm_scope" --arg hash "$shorthash" --from-file scripts/prepare-preview-builds.jq "$manifest_file" > temp.json - mv temp.json "$manifest_file" -} - -echo "Preparing manifests..." -while IFS=$'\t' read -r location name; do - echo "- $name" - prepare-preview-manifest "$location/package.json" -done < <(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map(select(.location != ".")) | map([.location, .name]) | map(@tsv) | .[]') - -echo "Installing dependencies..." -yarn install --no-immutable diff --git a/scripts/prepare-preview-builds.ts b/scripts/prepare-preview-builds.ts new file mode 100755 index 00000000..c4ef5885 --- /dev/null +++ b/scripts/prepare-preview-builds.ts @@ -0,0 +1,225 @@ +#!yarn ts-node + +import PackageJson from '@npmcli/package-json'; +// import { spawn } from 'node:child_process/promises'; +import { spawn } from 'child_process'; +import execa from 'execa'; + +// Previews object displayed by the CI when you ask for preview builds. +type Arguments = { + // NPM scope (@metamask-previews) + npmScope: string; + // Commit ID + commitId: string; +}; + +type WorkspacePackage = { + location: string; + name: string; +}; + +type WorkspacePreviewPackage = WorkspacePackage & { + previewName: string; + previewVersion: string; + version: string; +}; + +class UsageError extends Error { + constructor(message: string) { + // 1 because `ts-node` is being used as a launcher, so argv[0] is ts-node "bin.js" + const bin: string = process.argv[1]; + + super( + `usage: ${bin} \n${ + message ? `\nerror: ${message}\n` : '' + }`, + ); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); + +/** + * Parse and verifies that each arguments is well-formatted. + * + * @returns Parsed arguments as an `Arguments` object. + */ +async function parseAndVerifyArguments(): Promise { + if (process.argv.length !== 4) { + throw new UsageError('not enough arguments'); + } + // 1: ts-node (bin.js), 2: This script, 3: NPM scope, 4: Commit ID + const [, , npmScope, commitId] = process.argv as [ + string, + string, + string, + string, + ]; + + return { npmScope, commitId }; +} + +/** + * Gets a preview package from a workspace package. + * + * @param pkg - Workspace package. + * @param npmScope - NPM scope used for the preview. + * @param commitId - Commit ID used in the preview version. + * @returns The preview package. + */ +async function getPkgPreview( + pkg: WorkspacePackage, + npmScope: string, + commitId: string, +): Promise { + const pkgJson = await PackageJson.load(pkg.location); + + // Assuming we always have a version in our package.json + const pkgVersion: string = pkgJson.content.version; + + return { + ...pkg, + version: pkgVersion, + previewName: pkg.name.replace('@metamask/', `${npmScope}/`), + previewVersion: `${pkgVersion}-${commitId}`, + }; +} + +/** + * Gets all workspace packages. + * + * @returns The list of workspace packages. + */ +async function getWorkspacePackages(): Promise { + const { stdout } = await execa('yarn', [ + 'workspaces', + 'list', + '--no-private', + '--json', + ]); + + // Stops early, to avoid having JSON parsing error on empty lines + if (stdout.trim() === '') { + return []; + } + + // Each `yarn why --json` lines is a JSON object, so parse it and "type" it + return stdout.split('\n').map((line) => JSON.parse(line) as WorkspacePackage); +} + +/** + * Gets all workspace packages as preview packages. + * + * @param npmScope - NPM scope used for the preview. + * @param commitId - Commit ID used in the preview version. + * @returns The list of preview packages. + */ +async function getWorkspacePreviewPackages( + npmScope: string, + commitId: string, +): Promise { + const pkgs = await getWorkspacePackages(); + return await Promise.all( + pkgs.map(async (pkg) => await getPkgPreview(pkg, npmScope, commitId)), + ); +} + +/** + * Updates all workspace packages with their preview name and version. + * + * @param previewPkgs - Preview package list. + */ +async function updateWorkspacePackagesWithPreviewInfo( + previewPkgs: WorkspacePreviewPackage[], +): Promise { + for (const pkg of previewPkgs) { + const pkgJson = await PackageJson.load(pkg.location); + + // Update package.json with preview ingo + pkgJson.update({ + name: pkg.previewName, + version: pkg.previewVersion, + }); + + // Update dependencies that refers to a workspace package. We pin the current version + // of that package instead, and `yarn` will resolve this using the global resolutions + // (see `updateWorkspaceResolutions`) + for (const depKey of ['dependencies', 'devDependencies']) { + const deps = pkgJson.content[depKey]; + + for (const { name, version } of previewPkgs) { + // Only consider dependenc that refers to a local workspace package + if (name in deps && deps[name] === 'workspace:^') { + // Override this dependency with a "fixed" version, + // `yarn` will resolve this using the global resolutions being injected by + // `updateWorkspaceResolutions` + deps[name] = version; + } + } + + // Finally override the dependencies + pkgJson.update({ + [depKey]: deps, + }); + } + + await pkgJson.save(); + } +} + +/** + * Updates workspace resolutions with preview packages. + * + * @param previewPkgs - Preview package list. + */ +async function updateWorkspaceResolutions( + previewPkgs: WorkspacePreviewPackage[], +): Promise { + const workspacePkgJson = await PackageJson.load('.'); + + // Compute resolutions to map currently versionned packages to their preview + // counterpart + const resolutions = {}; + for (const pkg of previewPkgs) { + resolutions[`${pkg.name}@${pkg.version}`] = `workspace:${pkg.location}`; + } + + // Update workspace resolutions to use preview packages + workspacePkgJson.update({ + resolutions: { + ...workspacePkgJson.content.resolutions, + // This comes after so we can override any "conflicting" resolutions (that would share + // the same name) + ...resolutions, + }, + }); + + await workspacePkgJson.save(); +} + +/** + * Yarn install. + */ +function yarnInstall() { + spawn('yarn', ['install', '--no-immutable'], { stdio: 'inherit' }); +} + +/** + * The entrypoint to this script. + */ +async function main() { + const { npmScope, commitId } = await parseAndVerifyArguments(); + const previewPkgs = await getWorkspacePreviewPackages(npmScope, commitId); + + console.log(':: preparing manifests...'); + await updateWorkspacePackagesWithPreviewInfo(previewPkgs); + + console.log(':: updating global resolutions...'); + await updateWorkspaceResolutions(previewPkgs); + + console.log(':: installing dependencies...'); + yarnInstall(); +} diff --git a/yarn.lock b/yarn.lock index cc566458..d6326265 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1683,6 +1683,7 @@ __metadata: "@metamask/eslint-config-jest": "npm:^12.0.0" "@metamask/eslint-config-nodejs": "npm:^12.0.0" "@metamask/eslint-config-typescript": "npm:^12.0.0" + "@npmcli/package-json": "npm:^5.0.0" "@types/jest": "npm:^28.1.6" "@types/node": "npm:^16" "@typescript-eslint/eslint-plugin": "npm:^5.43.0"