diff --git a/packages/js/src/plugins/typescript/plugin.spec.ts b/packages/js/src/plugins/typescript/plugin.spec.ts index 105eca1773..eada80a698 100644 --- a/packages/js/src/plugins/typescript/plugin.spec.ts +++ b/packages/js/src/plugins/typescript/plugin.spec.ts @@ -1,8 +1,8 @@ import { type CreateNodesContext } from '@nx/devkit'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; import { minimatch } from 'minimatch'; -import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; -import { PLUGIN_NAME, TscPluginOptions, createNodes } from './plugin'; import { setupWorkspaceContext } from 'nx/src/utils/workspace-context'; +import { PLUGIN_NAME, createNodesV2, type TscPluginOptions } from './plugin'; describe(`Plugin: ${PLUGIN_NAME}`, () => { let context: CreateNodesContext; @@ -2170,7 +2170,7 @@ async function applyFilesToTempFsAndContext( await tempFs.createFiles(fileSys); // @ts-expect-error update otherwise readonly property for testing context.configFiles = Object.keys(fileSys).filter((file) => - minimatch(file, createNodes[0], { dot: true }) + minimatch(file, createNodesV2[0], { dot: true }) ); setupWorkspaceContext(tempFs.tempDir); } @@ -2181,15 +2181,19 @@ async function invokeCreateNodesOnMatchingFiles( ) { const aggregateProjects: Record = {}; for (const file of context.configFiles) { - const nodes = await createNodes[1](file, pluginOptions, context); - for (const [projectName, project] of Object.entries(nodes.projects ?? {})) { - if (aggregateProjects[projectName]) { - aggregateProjects[projectName].targets = { - ...aggregateProjects[projectName].targets, - ...project.targets, - }; - } else { - aggregateProjects[projectName] = project; + const results = await createNodesV2[1]([file], pluginOptions, context); + for (const [, nodes] of results) { + for (const [projectName, project] of Object.entries( + nodes.projects ?? {} + )) { + if (aggregateProjects[projectName]) { + aggregateProjects[projectName].targets = { + ...aggregateProjects[projectName].targets, + ...project.targets, + }; + } else { + aggregateProjects[projectName] = project; + } } } } diff --git a/packages/js/src/plugins/typescript/plugin.ts b/packages/js/src/plugins/typescript/plugin.ts index 2a34f4395d..19ee31893b 100644 --- a/packages/js/src/plugins/typescript/plugin.ts +++ b/packages/js/src/plugins/typescript/plugin.ts @@ -1,20 +1,26 @@ import { + createNodesFromFiles, detectPackageManager, joinPathFragments, + logger, normalizePath, readJsonFile, writeJsonFile, type CreateDependencies, type CreateNodes, type CreateNodesContext, + type CreateNodesResult, + type CreateNodesV2, type NxJsonConfiguration, + type ProjectConfiguration, type TargetConfiguration, } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; +import { minimatch } from 'minimatch'; import { existsSync, readdirSync, statSync } from 'node:fs'; import { basename, dirname, join, relative } from 'node:path'; -import { minimatch } from 'minimatch'; +import { hashObject } from 'nx/src/hasher/file-hasher'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { getLockFileName } from 'nx/src/plugins/js/lock-file/lock-file'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; @@ -49,85 +55,119 @@ interface NormalizedPluginOptions { }; } -const cachePath = join(workspaceDataDirectory, 'tsc.hash'); -const targetsCache = readTargetsCache(); +type TscProjectResult = Pick; -function readTargetsCache(): Record< - string, - Record> -> { +function readTargetsCache(cachePath: string): Record { return existsSync(cachePath) ? readJsonFile(cachePath) : {}; } -function writeTargetsToCache() { - const oldCache = readTargetsCache(); - writeJsonFile(cachePath, { - ...oldCache, - ...targetsCache, - }); +function writeTargetsToCache( + cachePath: string, + results?: Record +) { + writeJsonFile(cachePath, results); } +/** + * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'. + */ export const createDependencies: CreateDependencies = () => { - writeTargetsToCache(); return []; }; export const PLUGIN_NAME = '@nx/js/typescript'; -export const createNodes: CreateNodes = [ - '**/tsconfig*.json', - async (configFilePath, options, context) => { - const pluginOptions = normalizePluginOptions(options); - const projectRoot = dirname(configFilePath); - const fullConfigPath = joinPathFragments( - context.workspaceRoot, - configFilePath - ); +const tsConfigGlob = '**/tsconfig*.json'; - // Do not create a project if package.json and project.json isn't there. - const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); - if ( - !siblingFiles.includes('package.json') && - !siblingFiles.includes('project.json') - ) { - return {}; +export const createNodesV2: CreateNodesV2 = [ + tsConfigGlob, + async (configFilePaths, options, context) => { + const optionsHash = hashObject(options); + const cachePath = join(workspaceDataDirectory, `tsc-${optionsHash}.hash`); + const targetsCache = readTargetsCache(cachePath); + const normalizedOptions = normalizePluginOptions(options); + try { + return await createNodesFromFiles( + (configFile, options, context) => + createNodesInternal(configFile, options, context, targetsCache), + configFilePaths, + normalizedOptions, + context + ); + } finally { + writeTargetsToCache(cachePath, targetsCache); } - - // Do not create a project if it's not a tsconfig.json and there is no tsconfig.json in the same directory - if ( - basename(configFilePath) !== 'tsconfig.json' && - !siblingFiles.includes('tsconfig.json') - ) { - return {}; - } - - const nodeHash = await calculateHashForCreateNodes( - projectRoot, - pluginOptions, - context, - [getLockFileName(detectPackageManager(context.workspaceRoot))] - ); - // The hash is calculated at the node/project level, so we add the config file path to avoid conflicts when caching - const cacheKey = `${nodeHash}_${configFilePath}`; - - targetsCache[cacheKey] ??= buildTscTargets( - fullConfigPath, - projectRoot, - pluginOptions, - context - ); - - return { - projects: { - [projectRoot]: { - projectType: 'library', - targets: targetsCache[cacheKey], - }, - }, - }; }, ]; +export const createNodes: CreateNodes = [ + tsConfigGlob, + async (configFilePath, options, context) => { + logger.warn( + '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' + ); + const normalizedOptions = normalizePluginOptions(options); + return createNodesInternal(configFilePath, normalizedOptions, context, {}); + }, +]; + +async function createNodesInternal( + configFilePath: string, + options: NormalizedPluginOptions, + context: CreateNodesContext, + targetsCache: Record +): Promise { + const projectRoot = dirname(configFilePath); + const fullConfigPath = joinPathFragments( + context.workspaceRoot, + configFilePath + ); + + // Do not create a project if package.json and project.json isn't there. + const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } + + // Do not create a project if it's not a tsconfig.json and there is no tsconfig.json in the same directory + if ( + basename(configFilePath) !== 'tsconfig.json' && + !siblingFiles.includes('tsconfig.json') + ) { + return {}; + } + + const nodeHash = await calculateHashForCreateNodes( + projectRoot, + options, + context, + [getLockFileName(detectPackageManager(context.workspaceRoot))] + ); + // The hash is calculated at the node/project level, so we add the config file path to avoid conflicts when caching + const cacheKey = `${nodeHash}_${configFilePath}`; + + targetsCache[cacheKey] ??= buildTscTargets( + fullConfigPath, + projectRoot, + options, + context + ); + + const { targets } = targetsCache[cacheKey]; + + return { + projects: { + [projectRoot]: { + projectType: 'library', + targets, + }, + }, + }; +} + function buildTscTargets( configFilePath: string, projectRoot: string, @@ -220,7 +260,7 @@ function buildTscTargets( }; } - return targets; + return { targets }; } function getInputs( diff --git a/packages/js/typescript.ts b/packages/js/typescript.ts index 1db5322d40..3880e90c3d 100644 --- a/packages/js/typescript.ts +++ b/packages/js/typescript.ts @@ -1,4 +1,5 @@ export { createDependencies, createNodes, + createNodesV2, } from './src/plugins/typescript/plugin';