UNPKG

@nx/js

Version:

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

857 lines (856 loc) • 41.4 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, }); } /** * @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: {}, isExternalProjectReference: {} }; initializeTsConfigCache(configFilePaths, context.workspaceRoot); const normalizedOptions = normalizePluginOptions(options); const { configFilePaths: validConfigFilePaths, hashes, projectRoots, } = await resolveValidConfigFilesAndHashes(configFilePaths, 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}`; targetsCache[cacheKey] ??= buildTscTargets((0, node_path_1.join)(context.workspaceRoot, configFilePath), projectRoot, options, context); const { targets } = targetsCache[cacheKey]; return { projects: { [projectRoot]: { projectType: 'library', targets, }, }, }; }, validConfigFilePaths, normalizedOptions, context); } finally { writeToCache(targetsCachePath, targetsCache); writeTsConfigCache(toRelativePaths(tsConfigCacheData, context.workspaceRoot)); } }, ]; exports.createNodes = [ tsConfigGlob, async (configFilePath, options, context) => { devkit_1.logger.warn('`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'); const projectRoot = (0, node_path_1.dirname)(configFilePath); if (!checkIfConfigFileShouldBeProject(configFilePath, projectRoot, context)) { return {}; } const normalizedOptions = normalizePluginOptions(options); cache = { fileHashes: {}, rawFiles: {}, isExternalProjectReference: {} }; initializeTsConfigCache([configFilePath], context.workspaceRoot); const { targets } = buildTscTargets((0, node_path_1.join)(context.workspaceRoot, configFilePath), projectRoot, normalizedOptions, context); writeTsConfigCache(toRelativePaths(tsConfigCacheData, context.workspaceRoot)); return { projects: { [projectRoot]: { projectType: 'library', targets, }, }, }; }, ]; async function resolveValidConfigFilesAndHashes(configFilePaths, 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); if (!checkIfConfigFileShouldBeProject(configFilePath, projectRoot, context)) { continue; } projectRoots.push(projectRoot); validConfigFilePaths.push(configFilePath); hashes.push(await getConfigFileHash(configFilePath, context.workspaceRoot, projectRoot, 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, projectRoot, 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, projectRoot); const externalProjectReferences = resolveShallowExternalProjectReferences(tsConfig, workspaceRoot, projectRoot); let packageJson = null; try { packageJson = (0, devkit_1.readJsonFile)((0, node_path_1.join)(workspaceRoot, projectRoot, '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)] : []), // change this to bust the cache when making changes that would yield // different results for the same hash (0, file_hasher_1.hashObject)({ bust: 3 }), ]); } function checkIfConfigFileShouldBeProject(configFilePath, projectRoot, context) { // Do not create a project for the workspace root tsconfig files. if (projectRoot === '.') { return false; } // Do not create a project if package.json and project.json isn't there. const siblingFiles = (0, node_fs_1.readdirSync)((0, node_path_1.join)(context.workspaceRoot, projectRoot)); if (!siblingFiles.includes('package.json') && !siblingFiles.includes('project.json')) { return false; } // Do not create a project if it's not a tsconfig.json and there is no tsconfig.json in the same directory if ((0, node_path_1.basename)(configFilePath) !== '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(configFilePath, projectRoot, options, context) { const targets = {}; const namedInputs = (0, get_named_inputs_1.getNamedInputs)(projectRoot, context); const tsConfig = retrieveTsConfigFromCache(configFilePath, context.workspaceRoot); let internalProjectReferences; // Typecheck target if ((0, node_path_1.basename)(configFilePath) === 'tsconfig.json' && options.typecheck && tsConfig.raw?.['nx']?.addTypecheckTarget !== false) { internalProjectReferences = resolveInternalProjectReferences(tsConfig, context.workspaceRoot, projectRoot); const externalProjectReferences = resolveShallowExternalProjectReferences(tsConfig, context.workspaceRoot, projectRoot); const targetName = options.typecheck.targetName; if (!targets[targetName]) { let command = `tsc --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)(projectRoot, options.build.configName); if (context.configFiles.some((f) => f === buildConfigPath) && (0, util_1.isValidPackageJsonBuildConfig)(retrieveTsConfigFromCache(buildConfigPath, context.workspaceRoot), context.workspaceRoot, projectRoot)) { dependsOn.unshift(options.build.targetName); } } targets[targetName] = { dependsOn, command, options: { cwd: projectRoot }, cache: true, inputs: getInputs(namedInputs, configFilePath, tsConfig, internalProjectReferences, context.workspaceRoot, projectRoot), outputs: getOutputs(configFilePath, tsConfig, internalProjectReferences, context.workspaceRoot, projectRoot, /* emitDeclarationOnly */ true), syncGenerators: ['@nx/js:typescript-sync'], metadata: { technologies: ['typescript'], description: 'Runs type-checking for the project.', help: { command: `${pmc.exec} tsc --build --help`, example: { args: ['--force'], }, }, }, }; } } // Build target if (options.build && (0, node_path_1.basename)(configFilePath) === options.build.configName && (0, util_1.isValidPackageJsonBuildConfig)(tsConfig, context.workspaceRoot, projectRoot)) { internalProjectReferences ??= resolveInternalProjectReferences(tsConfig, context.workspaceRoot, projectRoot); const targetName = options.build.targetName; targets[targetName] = { dependsOn: [`^${targetName}`], command: `tsc --build ${options.build.configName}${options.verboseOutput ? ' --verbose' : ''}`, options: { cwd: projectRoot }, cache: true, inputs: getInputs(namedInputs, configFilePath, tsConfig, internalProjectReferences, context.workspaceRoot, projectRoot), outputs: getOutputs(configFilePath, tsConfig, internalProjectReferences, context.workspaceRoot, projectRoot, // 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} tsc --build --help`, example: { args: ['--force'], }, }, }, }; (0, util_1.addBuildAndWatchDepsTargets)(context.workspaceRoot, projectRoot, targets, { buildDepsTargetName: options.build.buildDepsName, watchDepsTargetName: options.build.watchDepsName, }, pmc); } return { targets }; } function getInputs(namedInputs, configFilePath, tsConfig, internalProjectReferences, workspaceRoot, projectRoot) { const configFiles = new Set(); 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 = [ [configFilePath, tsConfig], ...Object.entries(internalProjectReferences), ]; const absoluteProjectRoot = (0, node_path_1.join)(workspaceRoot, projectRoot); 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; projectTsConfigFiles.forEach(([configPath, config]) => { configFiles.add(configPath); const offset = (0, node_path_1.relative)(absoluteProjectRoot, (0, node_path_1.dirname)(configPath)); (config.raw?.include ?? []).forEach((p) => { const normalized = normalizeInput((0, node_path_1.join)(offset, substituteConfigDir(p)), config); normalized.forEach((input) => includePaths.add(input)); }); if (config.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); config.raw.exclude.forEach((e) => { const excludePath = substituteConfigDir(e); if (!otherFilesInclude.some((includePath) => picomatch(normalize(excludePath))(normalize(includePath)) || picomatch(normalize(includePath))(normalize(excludePath)))) { excludePaths.add(excludePath); } }); } }); const inputs = []; if (includePaths.size) { if ((0, node_fs_1.existsSync)((0, node_path_1.join)(workspaceRoot, projectRoot, 'package.json'))) { inputs.push('{projectRoot}/package.json'); } inputs.push(...Array.from(configFiles).map((p) => pathToInputOrOutput(p, workspaceRoot, projectRoot)), ...Array.from(includePaths).map((p) => pathToInputOrOutput((0, devkit_1.joinPathFragments)(projectRoot, p), workspaceRoot, projectRoot))); } 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)(projectRoot, p), workspaceRoot, projectRoot)}`)); } if (hasExternalProjectReferences(configFilePath, tsConfig, workspaceRoot, projectRoot)) { // 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(configFilePath, tsConfig, internalProjectReferences, workspaceRoot, projectRoot, 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((config) => { if (config.options.outFile) { const outFileName = (0, node_path_1.basename)(config.options.outFile, '.js'); const outFileDir = (0, node_path_1.dirname)(config.options.outFile); outputs.add(pathToInputOrOutput(config.options.outFile, workspaceRoot, projectRoot)); // 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, projectRoot)); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(outDir, `${outFileName}.d.ts`), workspaceRoot, projectRoot)); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(outDir, `${outFileName}.d.ts.map`), workspaceRoot, projectRoot)); // https://www.typescriptlang.org/tsconfig#tsBuildInfoFile outputs.add(tsConfig.options.tsBuildInfoFile ? pathToInputOrOutput(tsConfig.options.tsBuildInfoFile, workspaceRoot, projectRoot) : pathToInputOrOutput((0, devkit_1.joinPathFragments)(outDir, `${outFileName}.tsbuildinfo`), workspaceRoot, projectRoot)); } else if (config.options.outDir) { if (emitDeclarationOnly) { outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(config.options.outDir, '**/*.d.ts'), workspaceRoot, projectRoot)); if (tsConfig.options.declarationMap) { outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(config.options.outDir, '**/*.d.ts.map'), workspaceRoot, projectRoot)); } } else { outputs.add(pathToInputOrOutput(config.options.outDir, workspaceRoot, projectRoot)); } if (config.options.tsBuildInfoFile) { if (emitDeclarationOnly || !(0, node_path_1.normalize)(config.options.tsBuildInfoFile).startsWith(`${(0, node_path_1.normalize)(config.options.outDir)}${node_path_1.sep}`)) { // https://www.typescriptlang.org/tsconfig#tsBuildInfoFile outputs.add(pathToInputOrOutput(config.options.tsBuildInfoFile, workspaceRoot, projectRoot)); } } else if (config.options.rootDir && config.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)(config.options.rootDir, (0, node_path_1.join)(workspaceRoot, projectRoot)); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(config.options.outDir, relativeRootDir, `*.tsbuildinfo`), workspaceRoot, projectRoot)); } else if (emitDeclarationOnly) { // https://www.typescriptlang.org/tsconfig#tsBuildInfoFile const name = (0, node_path_1.basename)(configFilePath, '.json'); outputs.add(pathToInputOrOutput((0, devkit_1.joinPathFragments)(config.options.outDir, `${name}.tsbuildinfo`), workspaceRoot, projectRoot)); } } else if (config.raw?.include?.length || config.raw?.files?.length || (!config.raw?.include && !config.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 = (0, node_path_1.basename)(configFilePath, '.json'); outputs.add(tsConfig.options.tsBuildInfoFile ? pathToInputOrOutput(tsConfig.options.tsBuildInfoFile, workspaceRoot, projectRoot) : (0, devkit_1.joinPathFragments)('{projectRoot}', `${name}.tsbuildinfo`)); } }); return Array.from(outputs); } function pathToInputOrOutput(path, workspaceRoot, projectRoot) { const fullProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot); 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, projectRoot, projectReferences = {}) { if (!tsConfig.projectReferences?.length) { return {}; } for (const ref of tsConfig.projectReferences) { let refConfigPath = ref.path; if (projectReferences[refConfigPath]) { // Already resolved continue; } if (!(0, node_fs_1.existsSync)(refConfigPath)) { // the referenced tsconfig doesn't exist, ignore it continue; } if (isExternalProjectReference(refConfigPath, workspaceRoot, projectRoot)) { continue; } if (!refConfigPath.endsWith('.json')) { refConfigPath = (0, node_path_1.join)(refConfigPath, 'tsconfig.json'); } projectReferences[refConfigPath] = retrieveTsConfigFromCache(refConfigPath, workspaceRoot); resolveInternalProjectReferences(projectReferences[refConfigPath], workspaceRoot, projectRoot, projectReferences); } return projectReferences; } function resolveShallowExternalProjectReferences(tsConfig, workspaceRoot, projectRoot, projectReferences = {}) { if (!tsConfig.projectReferences?.length) { return projectReferences; } for (const ref of tsConfig.projectReferences) { let refConfigPath = ref.path; if (projectReferences[refConfigPath]) { // Already resolved continue; } if (!(0, node_fs_1.existsSync)(refConfigPath)) { // the referenced tsconfig doesn't exist, ignore it continue; } if (isExternalProjectReference(refConfigPath, workspaceRoot, projectRoot)) { if (!refConfigPath.endsWith('.json')) { refConfigPath = (0, node_path_1.join)(refConfigPath, 'tsconfig.json'); } projectReferences[refConfigPath] = retrieveTsConfigFromCache(refConfigPath, workspaceRoot); } } return projectReferences; } function hasExternalProjectReferences(tsConfigPath, tsConfig, workspaceRoot, projectRoot, 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)) { // Already seen continue; } if (!(0, node_fs_1.existsSync)(refConfigPath)) { // the referenced tsconfig doesn't exist, ignore it continue; } if (isExternalProjectReference(refConfigPath, workspaceRoot, projectRoot)) { return true; } if (!refConfigPath.endsWith('.json')) { refConfigPath = (0, node_path_1.join)(refConfigPath, 'tsconfig.json'); } const refTsConfig = retrieveTsConfigFromCache(refConfigPath, workspaceRoot); const result = hasExternalProjectReferences(refConfigPath, refTsConfig, workspaceRoot, projectRoot, seen); if (result) { return true; } } return false; } function isExternalProjectReference(refTsConfigPath, workspaceRoot, projectRoot) { const relativePath = posixRelative(workspaceRoot, refTsConfigPath); if (cache.isExternalProjectReference[relativePath] !== undefined) { return cache.isExternalProjectReference[relativePath]; } const absoluteProjectRoot = (0, node_path_1.join)(workspaceRoot, projectRoot); let currentPath = getTsConfigDirName(refTsConfigPath); if ((0, node_path_1.relative)(absoluteProjectRoot, currentPath).startsWith('..')) { // it's outside of the project root, so it's an external project reference cache.isExternalProjectReference[relativePath] = true; return true; } while (currentPath !== absoluteProjectRoot) { 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'))) { // it's inside a nested project root, so it's and external project reference cache.isExternalProjectReference[relativePath] = true; return true; } currentPath = (0, node_path_1.dirname)(currentPath); } // it's inside the project root, so it's an internal project reference cache.isExternalProjectReference[relativePath] = false; return false; } function getTsConfigDirName(tsConfigPath) { return (0, node_fs_1.statSync)(tsConfigPath).isFile() ? (0, node_path_1.dirname)(tsConfigPath) : (0, node_path_1.normalize)(tsConfigPath); } 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) { const hashes = []; if (!extendedConfigFiles.length) { return ''; } 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)); } } return hashes.join('|'); } 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 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', }; // 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', }; } return { 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)); }