diff --git a/scripts/__tests__/integration/utils.js b/scripts/__tests__/integration/utils.js index cc4a6a6159f..452c5dedbc8 100644 --- a/scripts/__tests__/integration/utils.js +++ b/scripts/__tests__/integration/utils.js @@ -63,31 +63,24 @@ exports.expectSuccessfulExec = expectSuccessfulExec; * @property {Record} packageJson */ -/** @param {ctx} ExampleContext */ +/** + * @param {ctx} ExampleContext + * @returns {Promise>} The installed monorepo dependency map + */ async function buildExample({packageJson, exampleDir}) { - const lexicalPackages = new Map( - packagesManager.getPublicPackages().map((pkg) => [pkg.getNpmName(), pkg]), - ); let hasPlaywright = false; - const installDeps = [ - 'dependencies', - 'devDependencies', - 'peerDependencies', - ].flatMap((depType) => { - const deps = packageJson[depType] || {}; - hasPlaywright ||= '@playwright/test' in deps; - return Object.keys(deps).flatMap((k) => { - const pkg = lexicalPackages.get(k); - return pkg - ? [ - path.resolve( - 'npm', - `${pkg.getDirectoryName()}-${pkg.packageJson.version}.tgz`, - ), - ] - : []; - }); - }); + const depsMap = packagesManager.computedMonorepoDependencyMap( + ['dependencies', 'devDependencies', 'peerDependencies'].flatMap( + (depType) => { + const deps = packageJson[depType] || {}; + hasPlaywright ||= '@playwright/test' in deps; + return Object.keys(deps); + }, + ), + ); + const installDeps = [...depsMap.values()].map((pkg) => + path.resolve('npm', `${pkg.getDirectoryName()}-${monorepoVersion}.tgz`), + ); if (installDeps.length === 0) { throw new Error(`No lexical dependencies detected: ${exampleDir}`); } @@ -105,6 +98,7 @@ async function buildExample({packageJson, exampleDir}) { await exec('npx playwright install'); } }); + return depsMap; } /** @@ -119,19 +113,30 @@ function describeExample(packageJsonPath, bodyFun = undefined) { /** @type {ExampleContext} */ const ctx = {exampleDir, packageJson, packageJsonPath}; describe(exampleDir, () => { - beforeAll(async () => buildExample(ctx), LONG_TIMEOUT); + /** @type {PackageMetadata[]} */ + const deps = []; + beforeAll(async () => { + deps.push(...(await buildExample(ctx)).values()); + }, LONG_TIMEOUT); test('install & build succeeded', () => { expect(true).toBe(true); }); test(`installed lexical ${monorepoVersion}`, () => { - expect( - fs.existsSync(path.join(exampleDir, 'node_modules', 'lexical')), - ).toBe(true); - expect( - fs.readJsonSync( - path.join(exampleDir, 'node_modules', 'lexical', 'package.json'), - ), - ).toMatchObject({version: monorepoVersion}); + const packageNames = deps.map((pkg) => pkg.getNpmName()); + expect(packageNames).toContain('lexical'); + for (const pkg of deps) { + const installedPath = path.join( + exampleDir, + 'node_modules', + pkg.getNpmName(), + ); + expect({[installedPath]: fs.existsSync(installedPath)}).toEqual({ + [installedPath]: true, + }); + expect( + fs.readJsonSync(path.join(installedPath, 'package.json')), + ).toMatchObject({name: pkg.getNpmName(), version: monorepoVersion}); + } }); if (packageJson.scripts.test) { test( diff --git a/scripts/shared/packagesManager.js b/scripts/shared/packagesManager.js index 6042a6e0970..d70a23ead0a 100644 --- a/scripts/shared/packagesManager.js +++ b/scripts/shared/packagesManager.js @@ -27,6 +27,10 @@ function packageSort(a, b) { class PackagesManager { /** @type {Array} */ packages; + /** @type {Map} */ + packagesByNpmName = new Map(); + /** @type {Map} */ + packagesByDirectoryName = new Map(); /** * @param {Array} packagePaths @@ -35,6 +39,10 @@ class PackagesManager { this.packages = packagePaths .map((packagePath) => new PackageMetadata(packagePath)) .sort(packageSort); + for (const pkg of this.packages) { + this.packagesByNpmName.set(pkg.getNpmName(), pkg); + this.packagesByDirectoryName.set(pkg.getDirectoryName(), pkg); + } } /** @@ -43,9 +51,7 @@ class PackagesManager { * @returns {PackageMetadata} */ getPackageByNpmName(name) { - const pkg = this.packages.find( - (candidate) => candidate.getNpmName() === name, - ); + const pkg = this.packagesByNpmName.get(name); if (!pkg) { throw new Error(`Missing package with npm name '${name}'`); } @@ -58,9 +64,7 @@ class PackagesManager { * @returns {PackageMetadata} */ getPackageByDirectoryName(name) { - const pkg = this.packages.find( - (candidate) => candidate.getDirectoryName() === name, - ); + const pkg = this.packagesByDirectoryName.get(name); if (!pkg) { throw new Error(`Missing package with directory name '${name}'`); } @@ -85,6 +89,38 @@ class PackagesManager { getPublicPackages() { return this.packages.filter((pkg) => !pkg.isPrivate()); } + + /** + * Given an array of npm dependencies (may include non-Lexical names), + * return all required transitive monorepo dependencies to have those + * packages (in a topologically ordered Map). + * + * @param {Array} npmDependencies + * @returns {Map} + */ + computedMonorepoDependencyMap(npmDependencies) { + /** @type {Map} */ + const depsMap = new Map(); + const visited = new Set(); + /** @param {string[]} deps */ + const traverse = (deps) => { + for (const dep of deps) { + if (visited.has(dep)) { + continue; + } + visited.add(dep); + if (!depsMap.has(dep)) { + const pkg = this.packagesByNpmName.get(dep); + if (pkg) { + traverse(Object.keys(pkg.packageJson.dependencies || {})); + depsMap.set(dep, pkg); + } + } + } + }; + traverse(npmDependencies); + return depsMap; + } } exports.packagesManager = new PackagesManager(