diff --git a/packages/tools/src/commands/integrity/dependencies.ts b/packages/tools/src/commands/integrity/dependencies.ts index f609f1e92..c2d08fce0 100644 --- a/packages/tools/src/commands/integrity/dependencies.ts +++ b/packages/tools/src/commands/integrity/dependencies.ts @@ -11,6 +11,7 @@ import path from 'path'; type PackageJson = { dependencies?: Record; devDependencies?: Record; + optionalDependencies?: Record; }; const jsonCache: Map = new Map(); @@ -83,15 +84,36 @@ const getInternalDependencies = ( return { errors, dependencies }; }; +type DependencyType = 'dependencies' | 'optionalDependencies'; + +const mergeDependency = ( + dependencies: Map, + errors: string[], + workspaceName: string, + dependencyName: string, + version: string, +) => { + const recordedVersion = dependencies.get(dependencyName); + if (recordedVersion && recordedVersion !== version) { + errors.push( + `Dependency mismatch for ${dependencyName} in ${workspaceName}: ${recordedVersion} vs ${version}`, + ); + return; + } + + dependencies.set(dependencyName, version); +}; + // From a workspace name, returns its dependencies and their versions. const getDependencies = (workspaces: Workspace[], name: string) => { const errors: string[] = []; const dependencies: Map = new Map(); + const optionalDependencies: Map = new Map(); const workspace = workspaces.find((w) => w.name === name); if (!workspace) { errors.push(`Could not find workspace for ${name}.`); - return { errors, dependencies }; + return { errors, dependencies, optionalDependencies }; } const pkg: PackageJson = getPackageJson(workspace); @@ -100,7 +122,95 @@ const getDependencies = (workspaces: Workspace[], name: string) => { dependencies.set(dependencyName, version); } - return { errors, dependencies }; + for (const [dependencyName, version] of Object.entries(pkg.optionalDependencies || {})) { + optionalDependencies.set(dependencyName, version); + } + + return { errors, dependencies, optionalDependencies }; +}; + +const getExpectedDependencies = ( + workspaces: Workspace[], + bundler: Workspace, + internalDependencies: Set, + errors: string[], +) => { + const dependencies: Map = new Map(); + const optionalDependencies: Map = new Map(); + + // Look through the internal dependencies we're loading. + for (const internalDep of internalDependencies) { + const externalDependencies = getDependencies(workspaces, internalDep); + errors.push(...externalDependencies.errors); + + for (const [depName, depVersion] of externalDependencies.dependencies) { + mergeDependency(dependencies, errors, bundler.name, depName, depVersion); + } + + for (const [depName, depVersion] of externalDependencies.optionalDependencies) { + mergeDependency(optionalDependencies, errors, bundler.name, depName, depVersion); + } + } + + // Required dependencies win if a transitive workspace lists the same package + // as optional while another requires it. + for (const depName of dependencies.keys()) { + optionalDependencies.delete(depName); + } + + return { + dependencies: cleanDependencies(Object.fromEntries(dependencies), onlyExternalDependencies), + optionalDependencies: cleanDependencies( + Object.fromEntries(optionalDependencies), + onlyExternalDependencies, + ), + }; +}; + +const syncDependencyRecord = ( + pkg: PackageJson, + dependencyType: DependencyType, + currentDependencies: Record, + expectedDependencies: Record, +) => { + // First list all the dependencies we need to check. + const dependenciesToCheck = new Map([ + ...Object.entries(expectedDependencies), + ...Object.entries(currentDependencies), + ]); + + // Crawl through each list and identify the differences. + let dependenciesMatch = true; + let outputLog = `{`; + const newDependenciesToApply = { ...(pkg[dependencyType] || {}) }; + for (const [depName, depVersion] of dependenciesToCheck) { + if (!currentDependencies[depName]) { + // Missing dependency. + dependenciesMatch = false; + newDependenciesToApply[depName] = depVersion; + outputLog += green(`\n + "${depName}": "${depVersion}"`); + } else if (!expectedDependencies[depName]) { + // Extra dependency. + dependenciesMatch = false; + delete newDependenciesToApply[depName]; + outputLog += red(`\n - "${depName}": "${depVersion}"`); + } else if ( + currentDependencies[depName] !== depVersion || + expectedDependencies[depName] !== depVersion + ) { + // Mismatching versions. + dependenciesMatch = false; + newDependenciesToApply[depName] = expectedDependencies[depName]; + outputLog += red(`\n - "${depName}": "${currentDependencies[depName]}"`); + outputLog += green(`\n + "${depName}": "${expectedDependencies[depName]}"`); + } else { + // All good. + outputLog += dim(`\n "${depName}": "${depVersion}"`); + } + } + outputLog += '\n}'; + + return { dependenciesMatch, newDependenciesToApply, outputLog }; }; // Based on the internal dependencies, we need to verify that the declared dependencies are correct @@ -111,76 +221,50 @@ export const updateDependencies = async (workspaces: Workspace[], bundlers: Work console.log(` Verifying ${green('dependencies')} for ${green(bundler.name)}.`); const pkg = getPackageJson(bundler); const currentDependencies = cleanDependencies(pkg.dependencies, allDependencies); - const recordedDependencies: Record = {}; + const currentOptionalDependencies = cleanDependencies( + pkg.optionalDependencies, + allDependencies, + ); const internalDependencies = getInternalDependencies(workspaces, bundler); errors.push(...internalDependencies.errors); + const expected = getExpectedDependencies( + workspaces, + bundler, + internalDependencies.dependencies, + errors, + ); - // Look through the internal dependencies we're loading. - for (const internalDep of internalDependencies.dependencies) { - const externalDependencies = getDependencies(workspaces, internalDep); - errors.push(...externalDependencies.errors); - - for (const [depName, depVersion] of externalDependencies.dependencies) { - if (recordedDependencies[depName] && recordedDependencies[depName] !== depVersion) { - errors.push( - `Dependency mismatch for ${depName} in ${bundler.name}: ${recordedDependencies[depName]} vs ${depVersion}`, - ); - continue; - } - - recordedDependencies[depName] = depVersion; - } - } - - const expectedDependencies: Record = cleanDependencies( - recordedDependencies, - onlyExternalDependencies, + const dependenciesSync = syncDependencyRecord( + pkg, + 'dependencies', + currentDependencies, + expected.dependencies, + ); + const optionalDependenciesSync = syncDependencyRecord( + pkg, + 'optionalDependencies', + currentOptionalDependencies, + expected.optionalDependencies, ); - // First list all the dependencies we need to check. - const depdenciesToCheck = new Map([ - ...Object.entries(expectedDependencies), - ...Object.entries(currentDependencies), - ]); - - // Crawl through each list and identify the differences. - let dependenciesMatch = true; - let outputLog = `{`; - const newDependenciesToApply = { ...pkg.dependencies }; - for (const [depName, depVersion] of depdenciesToCheck) { - if (!currentDependencies[depName]) { - // Missing dependency. - dependenciesMatch = false; - newDependenciesToApply[depName] = depVersion; - outputLog += green(`\n + "${depName}": "${depVersion}"`); - } else if (!expectedDependencies[depName]) { - // Extra dependency. - dependenciesMatch = false; - delete newDependenciesToApply[depName]; - outputLog += red(`\n - "${depName}": "${depVersion}"`); - } else if ( - currentDependencies[depName] !== depVersion || - expectedDependencies[depName] !== depVersion - ) { - // Mismatching versions. - dependenciesMatch = false; - newDependenciesToApply[depName] = expectedDependencies[depName]; - outputLog += red(`\n - "${depName}": "${currentDependencies[depName]}"`); - outputLog += green(`\n + "${depName}": "${expectedDependencies[depName]}"`); - } else { - // All good. - outputLog += dim(`\n "${depName}": "${depVersion}"`); - } + if (!dependenciesSync.dependenciesMatch) { + // Log the error. + console.log( + ` Mismatch ${red('dependencies')} for ${red(bundler.name)}:\n${dependenciesSync.outputLog}`, + ); + // Fix the dependencies. + pkg.dependencies = dependenciesSync.newDependenciesToApply; + console.log(` Writing ${red('package.json')} of ${red(bundler.name)}.`); + outputJsonSync(path.resolve(ROOT, bundler.location, 'package.json'), pkg); } - outputLog += '\n}'; - if (!dependenciesMatch) { + if (!optionalDependenciesSync.dependenciesMatch) { // Log the error. console.log( - ` Mismatch ${red('dependencies')} for ${red(bundler.name)}:\n${outputLog}`, + ` Mismatch ${red('optionalDependencies')} for ${red(bundler.name)}:\n${optionalDependenciesSync.outputLog}`, ); // Fix the dependencies. - pkg.dependencies = newDependenciesToApply; + pkg.optionalDependencies = optionalDependenciesSync.newDependenciesToApply; console.log(` Writing ${red('package.json')} of ${red(bundler.name)}.`); outputJsonSync(path.resolve(ROOT, bundler.location, 'package.json'), pkg); }