diff --git a/package-lock.json b/package-lock.json index 7253858378..d8ff36242c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13419,6 +13419,14 @@ "node": ">= 0.8" } }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "engines": { + "node": ">=4" + } + }, "node_modules/deprecated-react-native-prop-types": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-5.0.0.tgz", @@ -34401,6 +34409,7 @@ "@octokit/auth-app": "^4.0.9", "async-retry": "1.3.3", "conventional-changelog-angular": "5.0.13", + "dependency-graph": "1.0.0", "npm-registry-fetch": "14.0.5", "octokit": "2.0.14", "semver": "7.5.2", @@ -34411,7 +34420,8 @@ "git-publish-all": "git-publish-all.mjs", "github-publish": "create-github-release.mjs", "is-cli-release": "is-cli-release.mjs", - "npm-publish": "npm-publish-package.mjs" + "npm-publish": "npm-publish-package.mjs", + "reify": "reify.mjs" }, "devDependencies": { "@types/conventional-changelog-writer": "^4.0.2", diff --git a/package.json b/package.json index a9abfc5748..36907031fc 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "release:github": "npx -p=@coveord/release github-publish", "nx:graph": "nx graph --file=topology.json", "release:phase0": "npx -p=@coveord/release git-lock", - "release:phase1": "nx run-many --target=release:phase1 --all --parallel=false --output-style=stream", + "release:phase1": "nx run-many --target=release:phase1 --all --parallel=false --output-style=stream && npx -p=@coveord/release reify", "release:phase2": "npx -p=@coveord/release is-cli-release" }, "workspaces": [ diff --git a/utils/release/npm-publish-package.mjs b/utils/release/npm-publish-package.mjs index 930f67f2a3..96995cad99 100755 --- a/utils/release/npm-publish-package.mjs +++ b/utils/release/npm-publish-package.mjs @@ -21,7 +21,6 @@ import {fileURLToPath} from 'node:url'; import retry from 'async-retry'; import {inc, compareBuild, gt, SemVer} from 'semver'; import {json as fetchNpm} from 'npm-registry-fetch'; -import Arborist from '@npmcli/arborist'; /** * Check if the package json in the provided folder has changed since the last commit @@ -169,7 +168,6 @@ async function updateWorkspaceDependent(version) { dependentPackageJsonPath, JSON.stringify(dependentPackageJson) ); - await updateLockfileEntries(dependencyPackageJson.name); } } @@ -191,22 +189,6 @@ function updateDependency(packageJson, dependency, version) { } } -/** - * Check the dependency tree from the lockfile and make sure all entries - * of `entryName` satisfies the package.json files entries. - * @param {string} entryName the package to update across the tree - */ -async function updateLockfileEntries(entryName) { - const arb = new Arborist({savePrefix: '', path: rootFolder}); - await arb.loadVirtual(); - await arb.buildIdealTree({ - update: { - names: [entryName], - }, - }); - await arb.reify(); -} - function isPrivatePackage() { const packageJson = JSON.parse( readFileSync('package.json', {encoding: 'utf-8'}) diff --git a/utils/release/package.json b/utils/release/package.json index 5f1a51e9d0..520d360e7d 100644 --- a/utils/release/package.json +++ b/utils/release/package.json @@ -9,6 +9,7 @@ "@octokit/auth-app": "^4.0.9", "async-retry": "1.3.3", "conventional-changelog-angular": "5.0.13", + "dependency-graph": "1.0.0", "npm-registry-fetch": "14.0.5", "octokit": "2.0.14", "semver": "7.5.2", @@ -23,7 +24,8 @@ "npm-publish": "./npm-publish-package.mjs", "git-publish-all": "./git-publish-all.mjs", "github-publish": "./create-github-release.mjs", - "is-cli-release": "./is-cli-release.mjs" + "is-cli-release": "./is-cli-release.mjs", + "reify": "./reify.mjs" }, "scripts": { "test": "tsc" diff --git a/utils/release/reify.mjs b/utils/release/reify.mjs new file mode 100644 index 0000000000..c3e131d35d --- /dev/null +++ b/utils/release/reify.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +import Arborist from '@npmcli/arborist'; +import {DepGraph} from 'dependency-graph'; +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; +const REPO_FS_ROOT = resolve( + dirname(fileURLToPath(import.meta.url)), + '..', + '..' +); + +if (!process.env.INIT_CWD) { + throw new Error('Should be called using npm run-script'); +} +process.chdir(process.env.INIT_CWD); + +/** + * Current strategy: reify each package (along with their dependants transitively) in topological (parents->dependants) order. + * + * Other (untested) strategies that may work: + * * Strategy A: reify each package individually in topological (parents->dependants) order. + * * Strategy B: reify all packages at once. + */ + +/** + * @typedef {Map & {workspace: boolean, to: Arborist.Link}>} EdgeMap + */ + +/** + * @param {Arborist.Node} rootNode + */ +function buildDependencyGraph(rootNode) { + const graph = /** @type {DepGraph} */ (new DepGraph()); + /** + * @param {Arborist.Node} node + */ + function getWorkspaceDependencies(node) { + const edgesOut = + node.edgesOut instanceof Map + ? Array.from(node.edgesOut.values()) + : node.edgesOut; + const workspaces = edgesOut.filter( + (edge) => + edge.to.package.name && rootNode.workspaces?.has(edge.to.package.name) + ); + return workspaces.map((edge) => + edge.to instanceof Arborist.Link ? edge.to.target : edge.to + ); + } + + /** + * @param {Arborist.Node} node + */ + function addWorkspaceDependencies(node) { + const dependencies = getWorkspaceDependencies(node); + for (const dependency of dependencies) { + if (!node.package.name || !dependency.package.name) { + throw 'Workspaces must all have a name.'; + } + graph.addDependency(node.package.name, dependency.package.name); + addWorkspaceDependencies(dependency); + } + } + + const workspaces = getWorkspaceDependencies(rootNode); + for (const workspace of workspaces) { + if (!workspace.package.name) { + throw 'Workspaces must all have a name.'; + } + graph.addNode(workspace.package.name, workspace); + } + for (const workspace of workspaces) { + addWorkspaceDependencies(workspace); + } + return graph; +} + +async function initArborist() { + const registry = process.env.npm_config_registry; + const arb = new Arborist({ + savePrefix: '', + path: REPO_FS_ROOT, + registry, + }); + console.log('Loading virtual tree.'); + await arb.loadVirtual(); + return arb; +} + +const graph = buildDependencyGraph( + /** @type {Arborist.Node} */ ((await initArborist()).virtualTree) +); +for (const packageToUpdate of graph.overallOrder()) { + const names = [ + packageToUpdate, + ...graph.dependantsOf(packageToUpdate), + ].flatMap((packageName) => [ + graph.getNodeData(packageName).name, + packageName, + ]); + console.log('Updating package-lock for', names); + const arb = await initArborist(); + console.log('Building ideal tree'); + await arb.buildIdealTree({ + update: { + names, + }, + }); + console.log('Applying ideal tree.'); + await arb.reify(); +}