UNPKG

@nx/js

Version:

The JS plugin for Nx contains executors and generators that provide the best experience for developing JavaScript and TypeScript projects.

907 lines (906 loc) • 43.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodes = exports.createNodesV2 = exports.PLUGIN_NAME = exports.createDependencies = void 0; const devkit_1 = require("@nx/devkit"); const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const posix = require("node:path/posix"); const file_hasher_1 = require("nx/src/hasher/file-hasher"); const picomatch = require("picomatch"); // eslint-disable-next-line @typescript-eslint/no-restricted-imports const lock_file_1 = require("nx/src/plugins/js/lock-file/lock-file"); const cache_directory_1 = require("nx/src/utils/cache-directory"); const util_1 = require("./util"); let ts; const pmc = (0, devkit_1.getPackageManagerCommand)(); const TSCONFIG_CACHE_VERSION = 1; const TS_CONFIG_CACHE_PATH = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, 'tsconfig-files.hash'); let tsConfigCacheData; let cache; function readFromCache(cachePath) { try { return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' ? (0, devkit_1.readJsonFile)(cachePath) : {}; } catch { return {}; } } function readTsConfigCacheData() { const cache = readFromCache(TS_CONFIG_CACHE_PATH); if (cache.version !== TSCONFIG_CACHE_VERSION) { return {}; } return cache.data; } function writeToCache(cachePath, data) { const maxAttempts = 5; for (let attempt = 0; attempt < maxAttempts; attempt++) { const unique = (Math.random().toString(16) + '00000000').slice(2, 10); const tempPath = `${cachePath}.${process.pid}.${unique}.tmp`; try { (0, devkit_1.writeJsonFile)(tempPath, data, { spaces: 0 }); (0, node_fs_1.renameSync)(tempPath, cachePath); return; } catch { try { (0, node_fs_1.unlinkSync)(tempPath); } catch { } } } } function writeTsConfigCache(data) { writeToCache(TS_CONFIG_CACHE_PATH, { version: TSCONFIG_CACHE_VERSION, data, }); } function createConfigContext(configFilePath, workspaceRoot, projectContext) { const absolutePath = (0, node_path_1.join)(workspaceRoot, configFilePath); return { originalPath: configFilePath, absolutePath, relativePath: posix.relative(workspaceRoot, absolutePath), basename: (0, node_path_1.basename)(configFilePath), basenameNoExt: (0, node_path_1.basename)(configFilePath, '.json'), dirname: (0, node_path_1.dirname)(absolutePath), project: projectContext, }; } function getConfigContext(configPath, workspaceRoot) { const absolutePath = configPath.startsWith('/') || configPath.startsWith(workspaceRoot) ? (0, node_path_1.normalize)(configPath) : (0, node_path_1.join)(workspaceRoot, configPath); let context = cache.configContexts.get(absolutePath); if (context) { return context; } const relativePath = (0, node_path_1.relative)(workspaceRoot, absolutePath); const projectRoot = (0, node_path_1.dirname)(relativePath); if (!cache.projectContexts.has(projectRoot)) { cache.projectContexts.set(projectRoot, { root: projectRoot, normalized: posix.normalize(projectRoot), absolute: (0, node_path_1.join)(workspaceRoot, projectRoot), }); } const newContext = createConfigContext(relativePath, workspaceRoot, cache.projectContexts.get(projectRoot)); cache.configContexts.set(absolutePath, newContext); return newContext; } /** * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'. */ const createDependencies = () => { return []; }; exports.createDependencies = createDependencies; exports.PLUGIN_NAME = '@nx/js/typescript'; const tsConfigGlob = '**/tsconfig*.json'; exports.createNodesV2 = [ tsConfigGlob, async (configFilePaths, options, context) => { const optionsHash = (0, file_hasher_1.hashObject)(options); const targetsCachePath = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, `tsc-${optionsHash}.hash`); const targetsCache = readFromCache(targetsCachePath); cache = { fileHashes: {}, rawFiles: {}, picomatchMatchers: {}, extendedFilesHashes: new Map(), configOwners: new Map(), projectContexts: new Map(), configContexts: new Map(), }; initializeTsConfigCache(configFilePaths, context.workspaceRoot); const normalizedOptions = normalizePluginOptions(options); const { configFilePaths: validConfigFilePaths, hashes, projectRoots, } = await resolveValidConfigFilesAndHashes(configFilePaths, normalizedOptions, optionsHash, context); try { return await (0, devkit_1.createNodesFromFiles)((configFilePath, options, context, idx) => { const projectRoot = projectRoots[idx]; const hash = hashes[idx]; const cacheKey = `${hash}_${configFilePath}`; const absolutePath = (0, node_path_1.join)(context.workspaceRoot, configFilePath); const configContext = getConfigContext(absolutePath, context.workspaceRoot); targetsCache[cacheKey] ??= buildTscTargets(configContext, options, context, validConfigFilePaths); const { targets } = targetsCache[cacheKey]; return { projects: { [projectRoot]: { targets, }, }, }; }, validConfigFilePaths, normalizedOptions, context); } finally { writeToCache(targetsCachePath, targetsCache); writeTsConfigCache(toRelativePaths(tsConfigCacheData, context.workspaceRoot)); // Release memory after plugin invocation cache = null; } }, ]; exports.createNodes = exports.createNodesV2; async function resolveValidConfigFilesAndHashes(configFilePaths, options, optionsHash, context) { const lockFileHash = (0, file_hasher_1.hashFile)((0, node_path_1.join)(context.workspaceRoot, (0, lock_file_1.getLockFileName)((0, devkit_1.detectPackageManager)(context.workspaceRoot)))) ?? ''; const validConfigFilePaths = []; const hashes = []; const projectRoots = []; for await (const configFilePath of configFilePaths) { const projectRoot = (0, node_path_1.dirname)(configFilePath); const absolutePath = (0, node_path_1.join)(context.workspaceRoot, configFilePath); const configContext = getConfigContext(absolutePath, context.workspaceRoot); // Skip configs that can't produce any targets based on plugin options const isTypecheckConfig = configContext.basename === 'tsconfig.json'; const isBuildConfig = options.build && configContext.basename === options.build.configName; if (!isTypecheckConfig && !isBuildConfig) { continue; } if (!checkIfConfigFileShouldBeProject(configContext)) { continue; } projectRoots.push(projectRoot); validConfigFilePaths.push(configFilePath); hashes.push(await getConfigFileHash(configFilePath, context.workspaceRoot, configContext.project, optionsHash, lockFileHash)); } return { configFilePaths: validConfigFilePaths, hashes, projectRoots }; } /** * The cache key is composed by: * - hashes of the content of the relevant files that can affect what's inferred by the plugin: * - current config file * - config files extended by the current config file (recursively up to the root config file) * - referenced config files that are internal to the owning Nx project of the current config file, * or is a shallow external reference of the owning Nx project * - lock file * - project's package.json * - hash of the plugin options * - current config file path */ async function getConfigFileHash(configFilePath, workspaceRoot, project, optionsHash, lockFileHash) { const fullConfigPath = (0, node_path_1.join)(workspaceRoot, configFilePath); const tsConfig = retrieveTsConfigFromCache(fullConfigPath, workspaceRoot); const extendedConfigFiles = getExtendedConfigFiles(tsConfig, workspaceRoot); const internalReferencedFiles = resolveInternalProjectReferences(tsConfig, workspaceRoot, project); const externalProjectReferences = resolveShallowExternalProjectReferences(tsConfig, workspaceRoot, project); let packageJson = null; try { packageJson = (0, devkit_1.readJsonFile)((0, node_path_1.join)(workspaceRoot, project.root, 'package.json')); } catch { } return (0, file_hasher_1.hashArray)([ ...[ fullConfigPath, ...extendedConfigFiles.files.sort(), ...Object.keys(internalReferencedFiles).sort(), ...Object.keys(externalProjectReferences).sort(), ].map((file) => getFileHash(file, workspaceRoot)), ...extendedConfigFiles.packages.sort(), lockFileHash, optionsHash, ...(packageJson ? [(0, file_hasher_1.hashObject)(packageJson)] : []), ]); } function checkIfConfigFileShouldBeProject(config) { // Do not create a project for the workspace root tsconfig files. if (config.project.root === '.') { return false; } // Do not create a project if package.json and project.json isn't there. const siblingFiles = (0, node_fs_1.readdirSync)(config.project.absolute); if (!siblingFiles.includes('package.json') && !siblingFiles.includes('project.json')) { return false; } cache.configOwners.set(config.relativePath, config.project.normalized); // Do not create a project if it's not a tsconfig.json and there is no tsconfig.json in the same directory if (config.basename !== 'tsconfig.json' && !siblingFiles.includes('tsconfig.json')) { return false; } // Do not create project for Next.js projects since they are not compatible with // project references and typecheck will fail. if (siblingFiles.includes('next.config.js') || siblingFiles.includes('next.config.cjs') || siblingFiles.includes('next.config.mjs') || siblingFiles.includes('next.config.ts')) { return false; } return true; } function buildTscTargets(config, options, context, configFiles) { const targets = {}; const namedInputs = (0, get_named_inputs_1.getNamedInputs)(config.project.root, context); const tsConfig = retrieveTsConfigFromCache(config.absolutePath, context.workspaceRoot); let internalProjectReferences; if (config.basename === 'tsconfig.json' && options.typecheck && tsConfig.raw?.['nx']?.addTypecheckTarget !== false) { internalProjectReferences = resolveInternalProjectReferences(tsConfig, context.workspaceRoot, config.project); const externalProjectReferences = resolveShallowExternalProjectReferences(tsConfig, context.workspaceRoot, config.project); const targetName = options.typecheck.targetName; const compiler = options.compiler; if (!targets[targetName]) { let command = `${compiler} --build --emitDeclarationOnly${options.verboseOutput ? ' --verbose' : ''}`; if (tsConfig.options.noEmit || Object.values(internalProjectReferences).some((ref) => ref.options.noEmit) || Object.values(externalProjectReferences).some((ref) => ref.options.noEmit)) { // `tsc --build` does not work with `noEmit: true` command = `echo "The 'typecheck' target is disabled because one or more project references set 'noEmit: true' in their tsconfig. Remove this property to resolve this issue."`; } const dependsOn = [`^${targetName}`]; if (options.build && targets[options.build.targetName]) { // we already processed and have a build target dependsOn.unshift(options.build.targetName); } else if (options.build) { // check if the project will have a build target const buildConfigPath = (0, devkit_1.joinPathFragments)(config.project.root, options.build.configName); if (configFiles.some((f) => f === buildConfigPath) && (options.build.skipBuildCheck || (0, util_1.isValidPackageJsonBuildConfig)(retrieveTsConfigFromCache(buildConfigPath, context.workspaceRoot), context.workspaceRoot, config.project.root))) { dependsOn.unshift(options.build.targetName); } } targets[targetName] = { dependsOn, command, options: { cwd: config.project.root }, cache: true, inputs: getInputs(namedInputs, config, tsConfig, internalProjectReferences, context.workspaceRoot), outputs: getOutputs(config, tsConfig, internalProjectReferences, context.workspaceRoot, /* emitDeclarationOnly */ true), syncGenerators: ['@nx/js:typescript-sync'], metadata: { technologies: ['typescript'], description: 'Runs type-checking for the project.', help: { command: `${pmc.exec} ${compiler} --build --help`, example: { args: ['--force'], }, }, }, }; } } // Build target if (options.build && config.basename === options.build.configName && (options.build.skipBuildCheck || (0, util_1.isValidPackageJsonBuildConfig)(tsConfig, context.workspaceRoot, config.project.root))) { internalProjectReferences ??= resolveInternalProjectReferences(tsConfig, context.workspaceRoot, config.project); const targetName = options.build.targetName; const compiler = options.compiler; targets[targetName] = { dependsOn: [`^${targetName}`], command: `${compiler} --build ${options.build.configName}${options.verboseOutput ? ' --verbose' : ''}`, options: { cwd: config.project.root }, cache: true, inputs: getInputs(namedInputs, config, tsConfig, internalProjectReferences, context.workspaceRoot), outputs: getOutputs(config, tsConfig, internalProjectReferences, context.workspaceRoot, // should be false for build target, but providing it just in case is set to true tsConfig.options.emitDeclarationOnly), syncGenerators: ['@nx/js:typescript-sync'], metadata: { technologies: ['typescript'], description: 'Builds the project with `tsc`.', help: { command: `${pmc.exec} ${compiler} --build --help`, example: { args: ['--force'], }, }, }, }; (0, util_1.addBuildAndWatchDepsTargets)(context.workspaceRoot, config.project.root, targets, { buildDepsTargetName: options.build.buildDepsName, watchDepsTargetName: options.build.watchDepsName, }, pmc); } return { targets }; } function getInputs(namedInputs, config, tsConfig, internalProjectReferences, workspaceRoot) { const configFiles = new Set(); // TODO(leo): temporary disable external dependencies until we support hashing // glob patterns from external dependencies // const externalDependencies = ['typescript']; const extendedConfigFiles = getExtendedConfigFiles(tsConfig, workspaceRoot); extendedConfigFiles.files.forEach((configPath) => { configFiles.add(configPath); }); // externalDependencies.push(...extendedConfigFiles.packages); const includePaths = new Set(); const excludePaths = new Set(); const projectTsConfigFiles = [ [config.originalPath, tsConfig], ...Object.entries(internalProjectReferences), ]; const absoluteProjectRoot = config.project.absolute; if (!ts) { ts = require('typescript'); } // https://github.com/microsoft/TypeScript/blob/19b777260b26aac5707b1efd34202054164d4a9d/src/compiler/utilities.ts#L9869 const supportedTSExtensions = [ ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Dts, ts.Extension.Cts, ts.Extension.Dcts, ts.Extension.Mts, ts.Extension.Dmts, ]; // https://github.com/microsoft/TypeScript/blob/19b777260b26aac5707b1efd34202054164d4a9d/src/compiler/utilities.ts#L9878 const allSupportedExtensions = [ ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Dts, ts.Extension.Js, ts.Extension.Jsx, ts.Extension.Cts, ts.Extension.Dcts, ts.Extension.Cjs, ts.Extension.Mts, ts.Extension.Dmts, ts.Extension.Mjs, ]; const normalizeInput = (input, config) => { const extensions = config.options.allowJs ? [...allSupportedExtensions] : [...supportedTSExtensions]; if (config.options.resolveJsonModule) { extensions.push(ts.Extension.Json); } const segments = input.split('/'); // An "includes" path "foo" is implicitly a glob "foo/**/*" if its last // segment has no extension, and does not contain any glob characters // itself. // https://github.com/microsoft/TypeScript/blob/19b777260b26aac5707b1efd34202054164d4a9d/src/compiler/utilities.ts#L9577-L9585 if (!/[.*?]/.test(segments.at(-1))) { return extensions.map((ext) => `${segments.join('/')}/**/*${ext}`); } return [input]; }; const configDirTemplate = '${configDir}'; const substituteConfigDir = (p) => p.startsWith(configDirTemplate) ? p.replace(configDirTemplate, './') : p; // Helper function to get or create cached picomatch matchers const getOrCreateMatcher = (pattern) => { if (!cache.picomatchMatchers[pattern]) { cache.picomatchMatchers[pattern] = picomatch(pattern); } return cache.picomatchMatchers[pattern]; }; projectTsConfigFiles.forEach(([configPath, tsconfig]) => { configFiles.add(configPath); const offset = (0, node_path_1.relative)(absoluteProjectRoot, (0, node_path_1.dirname)(configPath)); (tsconfig.raw?.include ?? []).forEach((p) => { const normalized = normalizeInput((0, node_path_1.join)(offset, substituteConfigDir(p)), tsconfig); normalized.forEach((input) => includePaths.add(input)); }); if (tsconfig.raw?.exclude) { /** * We need to filter out the exclude paths that are already included in * other tsconfig files. If they are not included in other tsconfig files, * they still correctly apply to the current file and we should keep them. */ const otherFilesInclude = []; projectTsConfigFiles.forEach(([path, c]) => { if (path !== configPath) { otherFilesInclude.push(...(c.raw?.include ?? []).map(substituteConfigDir)); } }); const normalize = (p) => (p.startsWith('./') ? p.slice(2) : p); tsconfig.raw.exclude.forEach((e) => { const excludePath = substituteConfigDir(e); const normalizedExclude = normalize(excludePath); const excludeMatcher = getOrCreateMatcher(normalizedExclude); if (!otherFilesInclude.some((includePath) => { const normalizedInclude = normalize(includePath); const includeMatcher = getOrCreateMatcher(normalizedInclude); return (excludeMatcher(normalizedInclude) || includeMatcher(normalizedExclude)); })) { excludePaths.add(excludePath); } }); } }); const inputs = []; if (includePaths.size) { if ((0, node_fs_1.existsSync)((0, node_path_1.join)(workspaceRoot, config.project.root, 'package.json'))) { inputs.push('{projectRoot}/package.json'); } inputs.push(...Array.from(configFiles).map((p) => pathToInputOrOutput(p, workspaceRoot, config.project)), ...Array.from(includePaths).map((p) => pathToInputOrOutput((0, devkit_1.joinPathFragments)(config.project.root, p), workspaceRoot, config.project))); } else { // If we couldn't identify any include paths, we default to the default // named inputs. inputs.push('production' in namedInputs ? 'production' : 'default'); } if (excludePaths.size) { inputs.push(...Array.from(excludePaths).map((p) => `!${pathToInputOrOutput((0, devkit_1.joinPathFragments)(config.project.root, p), workspaceRoot, config.project)}`)); } if (hasExternalProjectReferences(config.originalPath, tsConfig, workspaceRoot, config.project)) { // Importing modules from a referenced project will load its output declaration files (d.ts) // https://www.typescriptlang.org/docs/handbook/project-references.html#what-is-a-project-reference inputs.push({ dependentTasksOutputFiles: '**/*.d.ts' }); } else { inputs.push('production' in namedInputs ? '^production' : '^default'); } // inputs.push({ externalDependencies }); return inputs; } function getOutputs(config, tsConfig, internalProjectReferences, workspaceRoot, emitDeclarationOnly) { const outputs = new Set(); // We could have more surgical outputs based on the tsconfig options, but the // user could override them through the command line and that wouldn't be // reflected in the outputs. So, we just include everything that could be // produced by the tsc command. [tsConfig, ...Object.values(internalProjectReferences)].forEach((tsconfig) => { if (tsconfig.options.outFile) { const outFileName = (0, node_path_1.basename)(tsconfig.options.outFile, '.js'); const outFileDir = (0, node_path_1.dirname)(tsconfig.options.outFile); outputs.add(pathToInputOrOutput(tsconfig.options.outFile, workspaceRoot, config.project)); // outFile is not be used with .cjs, .mjs, .jsx, so the list is simpler const outDir = (0, node_path_1.relative)(workspaceRoot, outFileDir); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(outDir, `${outFileName}.js.map`), workspaceRoot, config.project)); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(outDir, `${outFileName}.d.ts`), workspaceRoot, config.project)); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(outDir, `${outFileName}.d.ts.map`), workspaceRoot, config.project)); // https://www.typescriptlang.org/tsconfig#tsBuildInfoFile outputs.add(tsConfig.options.tsBuildInfoFile ? pathToInputOrOutput(tsConfig.options.tsBuildInfoFile, workspaceRoot, config.project) : pathToInputOrOutput((0, devkit_1.joinPathFragments)(outDir, `${outFileName}.tsbuildinfo`), workspaceRoot, config.project)); } else if (tsconfig.options.outDir) { if (emitDeclarationOnly) { outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(tsconfig.options.outDir, '**/*.d.ts'), workspaceRoot, config.project)); if (tsConfig.options.declarationMap) { outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(tsconfig.options.outDir, '**/*.d.ts.map'), workspaceRoot, config.project)); } } else { outputs.add(pathToInputOrOutput(tsconfig.options.outDir, workspaceRoot, config.project)); } if (tsconfig.options.tsBuildInfoFile) { if (emitDeclarationOnly || !(0, node_path_1.normalize)(tsconfig.options.tsBuildInfoFile).startsWith(`${(0, node_path_1.normalize)(tsconfig.options.outDir)}${node_path_1.sep}`)) { // https://www.typescriptlang.org/tsconfig#tsBuildInfoFile outputs.add(pathToInputOrOutput(tsconfig.options.tsBuildInfoFile, workspaceRoot, config.project)); } } else if (tsconfig.options.rootDir && tsconfig.options.rootDir !== '.') { // If rootDir is set, then the tsbuildinfo file will be outside the outDir so we need to add it. const relativeRootDir = (0, node_path_1.relative)(tsconfig.options.rootDir, (0, node_path_1.join)(workspaceRoot, config.project.root)); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(tsconfig.options.outDir, relativeRootDir, `*.tsbuildinfo`), workspaceRoot, config.project)); } else if (emitDeclarationOnly) { // https://www.typescriptlang.org/tsconfig#tsBuildInfoFile const name = config.basenameNoExt; outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(tsconfig.options.outDir, `${name}.tsbuildinfo`), workspaceRoot, config.project)); } } else if (tsconfig.raw?.include?.length || tsconfig.raw?.files?.length || (!tsconfig.raw?.include && !tsconfig.raw?.files)) { // tsc produce files in place when no outDir or outFile is set outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.js')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.cjs')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.mjs')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.jsx')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.js.map')); // should also include .cjs and .mjs data outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.jsx.map')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.d.ts')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.d.cts')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.d.mts')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.d.ts.map')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.d.cts.map')); outputs.add((0, devkit_1.joinPathFragments)('{projectRoot}', '**/*.d.mts.map')); // https://www.typescriptlang.org/tsconfig#tsBuildInfoFile const name = config.basenameNoExt; outputs.add(tsConfig.options.tsBuildInfoFile ? pathToInputOrOutput(tsConfig.options.tsBuildInfoFile, workspaceRoot, config.project) : (0, devkit_1.joinPathFragments)('{projectRoot}', `${name}.tsbuildinfo`)); } }); return Array.from(outputs); } function pathToInputOrOutput(path, workspaceRoot, project) { const fullProjectRoot = project.absolute; const fullPath = (0, node_path_1.resolve)(workspaceRoot, path); const pathRelativeToProjectRoot = (0, devkit_1.normalizePath)((0, node_path_1.relative)(fullProjectRoot, fullPath)); if (pathRelativeToProjectRoot.startsWith('..')) { return (0, devkit_1.joinPathFragments)('{workspaceRoot}', (0, node_path_1.relative)(workspaceRoot, fullPath)); } return (0, devkit_1.joinPathFragments)('{projectRoot}', pathRelativeToProjectRoot); } function getExtendedConfigFiles(tsConfig, workspaceRoot, extendedConfigFiles = new Set(), extendedExternalPackages = new Set()) { for (const extendedConfigFile of tsConfig.extendedConfigFiles) { if (extendedConfigFile.externalPackage) { extendedExternalPackages.add(extendedConfigFile.externalPackage); } else if (extendedConfigFile.filePath) { extendedConfigFiles.add(extendedConfigFile.filePath); getExtendedConfigFiles(retrieveTsConfigFromCache(extendedConfigFile.filePath, workspaceRoot), workspaceRoot, extendedConfigFiles, extendedExternalPackages); } } return { files: Array.from(extendedConfigFiles), packages: Array.from(extendedExternalPackages), }; } function resolveInternalProjectReferences(tsConfig, workspaceRoot, project, projectReferences = {}) { if (!tsConfig.projectReferences?.length) { return {}; } for (const ref of tsConfig.projectReferences) { let refConfigPath = ref.path; if (projectReferences[refConfigPath]) { continue; } if (!(0, node_fs_1.existsSync)(refConfigPath)) { continue; } if (!refConfigPath.endsWith('.json')) { refConfigPath = (0, node_path_1.join)(refConfigPath, 'tsconfig.json'); } const refContext = getConfigContext(refConfigPath, workspaceRoot); if (isExternalProjectReference(refContext, project, workspaceRoot)) { continue; } projectReferences[refConfigPath] = retrieveTsConfigFromCache(refConfigPath, workspaceRoot); resolveInternalProjectReferences(projectReferences[refConfigPath], workspaceRoot, project, projectReferences); } return projectReferences; } function resolveShallowExternalProjectReferences(tsConfig, workspaceRoot, project, projectReferences = {}) { if (!tsConfig.projectReferences?.length) { return projectReferences; } for (const ref of tsConfig.projectReferences) { let refConfigPath = ref.path; if (projectReferences[refConfigPath]) { continue; } if (!(0, node_fs_1.existsSync)(refConfigPath)) { continue; } if (!refConfigPath.endsWith('.json')) { refConfigPath = (0, node_path_1.join)(refConfigPath, 'tsconfig.json'); } const refContext = getConfigContext(refConfigPath, workspaceRoot); if (isExternalProjectReference(refContext, project, workspaceRoot)) { projectReferences[refConfigPath] = retrieveTsConfigFromCache(refConfigPath, workspaceRoot); } } return projectReferences; } function hasExternalProjectReferences(tsConfigPath, tsConfig, workspaceRoot, project, seen = new Set()) { if (!tsConfig.projectReferences?.length) { return false; } seen.add(tsConfigPath); for (const ref of tsConfig.projectReferences) { let refConfigPath = ref.path; if (seen.has(refConfigPath)) { continue; } if (!(0, node_fs_1.existsSync)(refConfigPath)) { continue; } if (!refConfigPath.endsWith('.json')) { refConfigPath = (0, node_path_1.join)(refConfigPath, 'tsconfig.json'); } const refContext = getConfigContext(refConfigPath, workspaceRoot); if (isExternalProjectReference(refContext, project, workspaceRoot)) { return true; } const refTsConfig = retrieveTsConfigFromCache(refConfigPath, workspaceRoot); const result = hasExternalProjectReferences(refConfigPath, refTsConfig, workspaceRoot, project, seen); if (result) { return true; } } return false; } function isExternalProjectReference(refConfig, project, workspaceRoot) { const owner = cache.configOwners.get(refConfig.relativePath); if (owner !== undefined) { return owner !== project.normalized; } let currentPath = refConfig.dirname; if ((0, node_path_1.relative)(project.absolute, currentPath).startsWith('..')) { return true; } while (currentPath !== project.absolute) { if ((0, node_fs_1.existsSync)((0, node_path_1.join)(currentPath, 'package.json')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(currentPath, 'project.json'))) { const owner = posixRelative(workspaceRoot, currentPath); cache.configOwners.set(refConfig.relativePath, owner); return owner !== project.normalized; } currentPath = (0, node_path_1.dirname)(currentPath); } return false; } function retrieveTsConfigFromCache(tsConfigPath, workspaceRoot) { const relativePath = posixRelative(workspaceRoot, tsConfigPath); // we don't need to check the hash if it's in the cache, because we've already // checked it when we initially populated the cache return tsConfigCacheData[relativePath] ? tsConfigCacheData[relativePath].data : readTsConfigAndCache(tsConfigPath, workspaceRoot); } function initializeTsConfigCache(configFilePaths, workspaceRoot) { tsConfigCacheData = toAbsolutePaths(readTsConfigCacheData(), workspaceRoot); // ensure hashes are checked and the cache is invalidated and populated as needed for (const configFilePath of configFilePaths) { const fullConfigPath = (0, node_path_1.join)(workspaceRoot, configFilePath); readTsConfigAndCache(fullConfigPath, workspaceRoot); } } function readTsConfigAndCache(tsConfigPath, workspaceRoot) { const relativePath = posixRelative(workspaceRoot, tsConfigPath); const hash = getFileHash(tsConfigPath, workspaceRoot); let extendedFilesHash; if (tsConfigCacheData[relativePath] && tsConfigCacheData[relativePath].hash === hash) { extendedFilesHash = getExtendedFilesHash(tsConfigCacheData[relativePath].data.extendedConfigFiles, workspaceRoot); if (tsConfigCacheData[relativePath].extendedFilesHash === extendedFilesHash) { return tsConfigCacheData[relativePath].data; } } const tsConfig = readTsConfig(tsConfigPath, workspaceRoot); const extendedConfigFiles = []; if (tsConfig.raw?.extends) { const extendsArray = typeof tsConfig.raw.extends === 'string' ? [tsConfig.raw.extends] : tsConfig.raw.extends; for (const extendsPath of extendsArray) { const extendedConfigFile = resolveExtendedTsConfigPath(extendsPath, (0, node_path_1.dirname)(tsConfigPath)); if (extendedConfigFile) { extendedConfigFiles.push(extendedConfigFile); } } } extendedFilesHash ??= getExtendedFilesHash(extendedConfigFiles, workspaceRoot); tsConfigCacheData[relativePath] = { data: { options: tsConfig.options, projectReferences: tsConfig.projectReferences, raw: tsConfig.raw, extendedConfigFiles, }, hash, extendedFilesHash, }; return tsConfigCacheData[relativePath].data; } function getExtendedFilesHash(extendedConfigFiles, workspaceRoot) { if (!extendedConfigFiles.length) { return ''; } // Create a stable cache key from sorted file paths const cacheKey = extendedConfigFiles .map((f) => f.filePath || f.externalPackage) .filter(Boolean) .sort() .join('|'); if (cache.extendedFilesHashes.has(cacheKey)) { return cache.extendedFilesHashes.get(cacheKey); } const hashes = []; for (const extendedConfigFile of extendedConfigFiles) { if (extendedConfigFile.externalPackage) { hashes.push(extendedConfigFile.externalPackage); } else if (extendedConfigFile.filePath) { hashes.push(getFileHash(extendedConfigFile.filePath, workspaceRoot)); hashes.push(getExtendedFilesHash(readTsConfigAndCache(extendedConfigFile.filePath, workspaceRoot) .extendedConfigFiles, workspaceRoot)); } } const hash = hashes.join('|'); cache.extendedFilesHashes.set(cacheKey, hash); return hash; } function readTsConfig(tsConfigPath, workspaceRoot) { if (!ts) { ts = require('typescript'); } const tsSys = { ...ts.sys, readFile: (path) => readFile(path, workspaceRoot), readDirectory: () => [], }; const readResult = ts.readConfigFile(tsConfigPath, tsSys.readFile); // read with a custom host that won't read directories which is only used // to identify the filenames included in the program, which we won't use return ts.parseJsonConfigFileContent(readResult.config, tsSys, (0, node_path_1.dirname)(tsConfigPath)); } function normalizePluginOptions(pluginOptions = {}) { const defaultCompiler = 'tsc'; const compiler = pluginOptions.compiler ?? defaultCompiler; const defaultTypecheckTargetName = 'typecheck'; let typecheck = { targetName: defaultTypecheckTargetName, }; if (pluginOptions.typecheck === false) { typecheck = false; } else if (pluginOptions.typecheck && typeof pluginOptions.typecheck !== 'boolean') { typecheck = { targetName: pluginOptions.typecheck.targetName ?? defaultTypecheckTargetName, }; } const defaultBuildTargetName = 'build'; const defaultBuildConfigName = 'tsconfig.lib.json'; let build = { targetName: defaultBuildTargetName, configName: defaultBuildConfigName, buildDepsName: 'build-deps', watchDepsName: 'watch-deps', skipBuildCheck: false, }; // Build target is not enabled by default if (!pluginOptions.build) { build = false; } else if (pluginOptions.build && typeof pluginOptions.build !== 'boolean') { build = { targetName: pluginOptions.build.targetName ?? defaultBuildTargetName, configName: pluginOptions.build.configName ?? defaultBuildConfigName, buildDepsName: pluginOptions.build.buildDepsName ?? 'build-deps', watchDepsName: pluginOptions.build.watchDepsName ?? 'watch-deps', skipBuildCheck: pluginOptions.build.skipBuildCheck ?? false, }; } return { compiler, typecheck, build, verboseOutput: pluginOptions.verboseOutput ?? false, }; } function resolveExtendedTsConfigPath(tsConfigPath, directory) { try { const resolvedPath = require.resolve(tsConfigPath, { paths: directory ? [directory] : undefined, }); if (tsConfigPath.startsWith('.') || !resolvedPath.includes('/node_modules/')) { return { filePath: resolvedPath }; } // parse the package from the tsconfig path const packageName = tsConfigPath.startsWith('@') ? tsConfigPath.split('/').slice(0, 2).join('/') : tsConfigPath.split('/')[0]; return { filePath: resolvedPath, externalPackage: packageName }; } catch { return null; } } function getFileHash(filePath, workspaceRoot) { const relativePath = posixRelative(workspaceRoot, filePath); if (!cache.fileHashes[relativePath]) { const content = readFile(filePath, workspaceRoot); cache.fileHashes[relativePath] = (0, file_hasher_1.hashArray)([content]); } return cache.fileHashes[relativePath]; } function readFile(filePath, workspaceRoot) { const relativePath = posixRelative(workspaceRoot, filePath); if (!cache.rawFiles[relativePath]) { const content = (0, node_fs_1.readFileSync)(filePath, 'utf8'); cache.rawFiles[relativePath] = content; } return cache.rawFiles[relativePath]; } function toAbsolutePaths(cache, workspaceRoot) { const updatedCache = {}; for (const [key, { data, extendedFilesHash, hash }] of Object.entries(cache)) { updatedCache[key] = { data: { options: { noEmit: data.options.noEmit }, raw: { nx: { addTypecheckTarget: data.raw?.['nx']?.addTypecheckTarget }, }, extendedConfigFiles: data.extendedConfigFiles, }, extendedFilesHash, hash, }; if (data.options.rootDir) { updatedCache[key].data.options.rootDir = (0, node_path_1.join)(workspaceRoot, data.options.rootDir); } if (data.options.outDir) { updatedCache[key].data.options.outDir = (0, node_path_1.join)(workspaceRoot, data.options.outDir); } if (data.options.outFile) { updatedCache[key].data.options.outFile = (0, node_path_1.join)(workspaceRoot, data.options.outFile); } if (data.options.tsBuildInfoFile) { updatedCache[key].data.options.tsBuildInfoFile = (0, node_path_1.join)(workspaceRoot, data.options.tsBuildInfoFile); } if (data.extendedConfigFiles.length) { updatedCache[key].data.extendedConfigFiles.forEach((file) => { file.filePath = (0, node_path_1.join)(workspaceRoot, file.filePath); }); } if (data.projectReferences) { updatedCache[key].data.projectReferences = data.projectReferences.map((ref) => ({ ...ref, path: (0, node_path_1.join)(workspaceRoot, ref.path) })); } } return updatedCache; } function toRelativePaths(cache, workspaceRoot) { const updatedCache = {}; for (const [key, { data, extendedFilesHash, hash }] of Object.entries(cache)) { updatedCache[key] = { data: { options: { noEmit: data.options.noEmit }, raw: { nx: { addTypecheckTarget: data.raw?.['nx']?.addTypecheckTarget }, }, extendedConfigFiles: data.extendedConfigFiles, }, extendedFilesHash, hash, }; if (data.options.rootDir) { updatedCache[key].data.options.rootDir = posixRelative(workspaceRoot, data.options.rootDir); } if (data.options.outDir) { updatedCache[key].data.options.outDir = posixRelative(workspaceRoot, data.options.outDir); } if (data.options.outFile) { updatedCache[key].data.options.outFile = posixRelative(workspaceRoot, data.options.outFile); } if (data.options.tsBuildInfoFile) { updatedCache[key].data.options.tsBuildInfoFile = posixRelative(workspaceRoot, data.options.tsBuildInfoFile); } if (data.extendedConfigFiles.length) { updatedCache[key].data.extendedConfigFiles.forEach((file) => { file.filePath = posixRelative(workspaceRoot, file.filePath); }); } if (data.projectReferences) { updatedCache[key].data.projectReferences = data.projectReferences.map((ref) => ({ ...ref, path: posixRelative(workspaceRoot, ref.path), })); } } return updatedCache; } function posixRelative(workspaceRoot, path) { return posix.normalize((0, node_path_1.relative)(workspaceRoot, path)); }