Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scripts): add use-preview-builds.ts helper script #16

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions scripts/generate-preview-build-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ Preview builds have been published. [See these instructions (from the \`core\` m
${JSON.stringify(packageMap, null, 2)}
\`\`\`

You can also use this helper script to automatically setup \`"resolutions"\` into your project. You
will need to create a file (\`previews.json\` here) and copy the JSON content from above.

\`\`\`console
$ ./scripts/use-preview-builds.ts <project-path> previews.json
\`\`\`

</details>
`;

Expand Down
256 changes: 256 additions & 0 deletions scripts/use-preview-builds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
#!yarn ts-node

import PackageJson from '@npmcli/package-json';
import execa from 'execa';
import fs from 'node:fs/promises';

// Previews object displayed by the CI when you ask for preview builds.
type Arguments = {
// Path to the project that will use the preview builds.
path: string;
// Previews object.
previews: Previews;
};

// Previews object displayed by the CI when you ask for preview builds.
type Previews = Record<string, string>;

// A `yarn why <pkg> --json` line entry.
type YarnWhyEntry = {
children: Record<
string,
{
descriptor: 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} <project-path> <previews-json>\n${
message ? `\nerror: ${message}\n` : ''
}`,
);
}
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

/**
* Checks if a path is a directory or not.
*
* @param path - Path to check.
* @returns True if path is a directory, false otherwise.
*/
async function isDir(path: string) {
return (await fs.stat(path)).isDirectory();
}

/**
* Checks if a path is a file or not.
*
* @param path - Path to check.
* @returns True if path is a file, false otherwise.
*/
async function isFile(path: string) {
// We check for a directory here, since the path could be a symlink or a "sort-of" file (when using the <(...) notation)
return !(await isDir(path));
}

/**
* Verify and read previews JSON file.
*
* @param path - Previews JSON file path.
* @returns Previews object.
* @throws If the previews JSON file cannot be read.
*/
async function verifyAndReadPreviewsJson(path: string) {
if (!(await isFile(path))) {
throw new UsageError(`${path}: is not a file`);
}
const fileContent = await fs.readFile(path);

// Not 100% type safe, but we assume the caller knows how to use the script
return JSON.parse(fileContent.toString()) as Previews;
}

/**
* Verify project path.
*
* @param path - Project path.
* @throws If the project path is not compatible.
*/
async function verifyProjectPath(path: string) {
if (!(await isDir(path))) {
throw new UsageError(`${path}: is not a directory`);
}

const pkgJsonPath = `${path}/package.json`;
if (!(await isFile(pkgJsonPath))) {
throw new UsageError(`${pkgJsonPath}: no such file`);
}
}

/**
* Parse and verifies that each arguments is well-formatted.
*
* @returns Parsed arguments as an `Arguments` object.
*/
async function parseAndVerifyArguments(): Promise<Arguments> {
if (process.argv.length !== 4) {
throw new UsageError('not enough arguments');
}
// 1: ts-node (bin.js), 2: This script, 3: Project path, 4: Previews JSON path
const [, , path, previewsJsonPath] = process.argv as [
string,
string,
string,
string,
];

await verifyProjectPath(path);
const previews = await verifyAndReadPreviewsJson(previewsJsonPath);

return { path, previews };
}

/**

Check failure on line 124 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Missing JSDoc @param "pkgName" declaration

Check failure on line 124 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Missing JSDoc @param "pkgName" declaration
* Compute the list of in-use versions for a given package.
*
* @param path - Project path.
* @param pkg - Package name.

Check failure on line 128 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Expected @param names to be "path, pkgName". Got "path, pkg"

Check failure on line 128 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Expected @param names to be "path, pkgName". Got "path, pkg"
* @returns The list of in-use versions for the given package.
*/
async function getPkgVersions(path: string, pkgName: string): Promise<string[]> {

Check failure on line 131 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Replace `path:·string,·pkgName:·string` with `⏎··path:·string,⏎··pkgName:·string,⏎`

Check failure on line 131 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Replace `path:·string,·pkgName:·string` with `⏎··path:·string,⏎··pkgName:·string,⏎`
const { stdout } = await execa('yarn', ['--cwd', path, 'why', pkgName, '--json']);

Check failure on line 132 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Replace `'--cwd',·path,·'why',·pkgName,·'--json'` with `⏎····'--cwd',⏎····path,⏎····'why',⏎····pkgName,⏎····'--json',⏎··`

Check failure on line 132 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Replace `'--cwd',·path,·'why',·pkgName,·'--json'` with `⏎····'--cwd',⏎····path,⏎····'why',⏎····pkgName,⏎····'--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
const entries = stdout
.split('\n')
.map((line) => JSON.parse(line) as YarnWhyEntry);

const versions: Set<string> = new Set();
for (const entry of entries) {
const { children } = entry;

for (const [key, value] of Object.entries(children)) {
const { descriptor } = value;

// We only keep the current package information and skip those "virtual" resolutions (which
// seems internal to yarn)
if (key.startsWith(pkgName) && !descriptor.includes('@virtual:')) {
versions.add(descriptor);
}
}
}

return Array.from(versions);
}

/**
* Gets the original package name from its preview package name.
*
* @param pkgPreviewName - Preview package name.
* @returns The original package name.
*/
function getPkgOriginalName(pkgPreviewName: string): string {
return pkgPreviewName.replace('@metamask-previews/', '@metamask/');
}

/**

Check failure on line 172 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Missing JSDoc @param "pkgPreviewVersion" declaration

Check failure on line 172 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Missing JSDoc @param "pkgPreviewVersion" declaration
* Gets the original package name from its preview package name.
*
* @param pkgPreviewName - Preview package name.

Check failure on line 175 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Expected @param names to be "pkgPreviewVersion". Got "pkgPreviewName"

Check failure on line 175 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Expected @param names to be "pkgPreviewVersion". Got "pkgPreviewName"
* @returns The original package name.
*/
function getPkgOriginalVersion(pkgPreviewVersion: string): string {
const match = /^(\d+\.\d+\.\d+)-.*$/.exec(pkgPreviewVersion);

Check failure on line 179 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Use the 'u' flag

Check failure on line 179 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Use the 'u' flag

if (!match) {
throw new Error(
`unable to extract original version from: "${pkgPreviewVersion}"`

Check failure on line 183 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Insert `,`

Check failure on line 183 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Insert `,`
);
}
return match[1]; // Get first group
}

/**
* Gets the preview package name from its original package name.
*
* @param pkgName - Package name.
* @param previews - Records of all preview packages that will be used to "infere" the preview package name.
* @returns The preview package name.
*/
function getPkgPreviewName(pkgName: string, previews: Previews): string {
const pkgPreviewName = pkgName.replace('@metamask/', '@metamask-previews/');

if (!(pkgPreviewName in previews)) {
throw new Error(
`unable to find package "${pkgPreviewName}" ("${pkgName}") in previews`,
);
}
// At this point, we know it's defined so we can safely force the type
const pkgPreviewVersion: string = previews[pkgPreviewName];

return `npm:${pkgPreviewName}@${pkgPreviewVersion}`;
}

/**
* Update the "resolutions" entry from a "package.json" file.
*
* @param path - Project path that will be used to find the "package.json" file.
* @param previews - Records of all preview packages.
*/
async function updateResolutions(path: string, previews: Previews) {
const pkgJson = await PackageJson.load(path);

const resolutions = {};
for (const [pkgPreviewName, pkgPreviewVersion] of Object.entries(previews)) {
const pkgName = getPkgOriginalName(pkgPreviewName);
const pkgVersion = getPkgOriginalVersion(pkgPreviewVersion);

console.log(`:: updating resolutions for "${pkgPreviewName}"`);

const pkgVersions = await getPkgVersions(path, pkgName);
for (const pkgVersion of pkgVersions) {

Check failure on line 227 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

'pkgVersion' is already declared in the upper scope on line 222 column 11

Check failure on line 227 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

'pkgVersion' is already declared in the upper scope on line 222 column 11
resolutions[pkgVersion] = getPkgPreviewName(pkgPreviewName, previews);
}

// Also adds the package version itself (useful for non-published packages)
resolutions[`${pkgName}@npm:${pkgVersion}`] = getPkgPreviewName(pkgPreviewName, previews);

Check failure on line 232 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Replace `pkgPreviewName,·previews` with `⏎······pkgPreviewName,⏎······previews,⏎····`

Check failure on line 232 in scripts/use-preview-builds.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Replace `pkgPreviewName,·previews` with `⏎······pkgPreviewName,⏎······previews,⏎····`
}
console.log(':: resolutions will be updated with:');
console.log(resolutions);

pkgJson.update({
resolutions: {
...pkgJson.content.resolutions,
...resolutions,
},
});
await pkgJson.save();
}

/**
* The entrypoint to this script.
*/
async function main() {
const { previews, path } = await parseAndVerifyArguments();

console.log(`:: will update project: ${path}`);
console.log(':: with previews: ', previews);

await updateResolutions(path, previews);
}
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading