import chalk from 'chalk'; import { minimatch } from 'minimatch'; import fs from 'node:fs'; import path from 'node:path'; import prettier from 'prettier'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { getPrettierConfig } from './utils/getPrettierConfig'; import { getWorkspacesList } from './utils/getWorkspacesList'; /** * Example usage: * yarn update-project-references --read-only 1 2 3 --test true * yarn update-project-references --ignore *\/firmware */ (async () => { const { argv } = yargs(hideBin(process.argv)) .array('read-only') .array('ignore') .boolean('test') as any; const readOnlyGlobs: string[] = argv.readOnly || []; const ignoreGlobs: string[] = argv.ignore || []; const isTesting = argv.test || false; const prettierConfig = await getPrettierConfig(); const serializeConfig = (config: any, stringifySpaces?: number) => { try { return prettier.format( JSON.stringify(config, null, stringifySpaces).replace(/\\\\/g, '/'), prettierConfig, ); } catch (error) { console.error(error); process.exit(1); } }; const parseTSConfigFile = (configPath: string) => { try { return fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath).toString()) : null; } catch { console.error(chalk.bold.red('Error while parsing file: '), configPath); process.exit(1); } }; const isDiffInConfig = async (actualConfig: any[] = [], expectedConfig: any[] = []) => (await serializeConfig(actualConfig)) !== (await serializeConfig(expectedConfig)); const workspaces = getWorkspacesList(); // NOTE: Workspace keys must be sorted due to file systems being a part of the equation... Object.keys(workspaces) .sort() .forEach(async workspaceName => { const workspace = workspaces[workspaceName]; if (workspace.location === '.') { // Skip root workspace return; } if (ignoreGlobs.some((path: string) => minimatch(workspace.location, path))) { return; } const workspacePath = path.resolve(process.cwd(), workspace.location); const workspaceConfigPath = path.resolve(workspacePath, 'tsconfig.json'); const workspaceLibConfigPath = path.resolve(workspacePath, 'tsconfig.lib.json'); const workspaceLibESMConfigPath = path.resolve(workspacePath, 'tsconfig.libESM.json'); const defaultWorkspaceConfig = { extends: path.relative(workspacePath, path.resolve(process.cwd(), 'tsconfig.json')), compilerOptions: { outDir: './libDev' }, include: ['.'], }; // parse tsconfig.json, which should exist, so if it doesn't, assign default config to have it created const workspaceConfig = parseTSConfigFile(workspaceConfigPath) ?? defaultWorkspaceConfig; // parse tsconfig.lib.json, which may not exist, and shall not be created const workspaceLibConfig = parseTSConfigFile(workspaceLibConfigPath); // parse tsconfig.libESM.json, which may not exist, and shall not be created const workspaceLibESMConfig = parseTSConfigFile(workspaceLibESMConfigPath); // actual references of the workspace from parsed package.json (assigned later) const nextWorkspaceReferences: Array<{ path: string }> = []; Object.values(workspace.workspaceDependencies).forEach(dependencyLocation => { const dependencyPath = path.resolve(process.cwd(), dependencyLocation); const relativeDependencyPath = path.relative(workspacePath, dependencyPath); if (relativeDependencyPath) { nextWorkspaceReferences.push({ path: relativeDependencyPath }); } else { console.warn( chalk.yellow( `${dependencyLocation} might be referencing itself in package.json#dependencies.`, ), ); } }); const expectedReferences = nextWorkspaceReferences; const expectedLibReferences = nextWorkspaceReferences.filter( // Don't include reference to schema-utils due to issues with the @sinclair/typebox library // When using a reference it results in incorrect imports ({ path }) => path !== '../schema-utils', ); if (isTesting) { const isConfigDiff = await isDiffInConfig( workspaceConfig.references, expectedReferences, ); const isConfigLibDiff = workspaceLibConfig !== null && (await isDiffInConfig(workspaceLibConfig.references, expectedLibReferences)); const isConfigLibESMDiff = workspaceLibESMConfig !== null && (await isDiffInConfig(workspaceLibESMConfig.references, expectedLibReferences)); if (isConfigDiff || isConfigLibDiff || isConfigLibESMDiff) { console.error( chalk.red( `TypeScript project references in ${workspace.location} are inconsistent with package.json#dependencies.`, ), chalk.red.bold(`Run "yarn update-project-references" to fix them.`), ); process.exit(1); } return; } if (readOnlyGlobs.some((path: string) => minimatch(workspace.location, path))) return; workspaceConfig.references = expectedReferences; fs.writeFileSync(workspaceConfigPath, await serializeConfig(workspaceConfig)); if (workspaceLibConfig !== null) { workspaceLibConfig.references = expectedLibReferences; fs.writeFileSync( workspaceLibConfigPath, await serializeConfig(workspaceLibConfig, 2), ); } if (workspaceLibESMConfig !== null) { workspaceLibESMConfig.references = expectedLibReferences; fs.writeFileSync( workspaceLibESMConfigPath, await serializeConfig(workspaceLibESMConfig, 2), ); } }); })();