feat(js): add createNodesV2 for typescript plugin (#26788)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

There is no implementation for the `CreateNodesV2` API.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

There should be an implementation for the `CreateNodesV2` API.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
<!-- Fixes NXC-790 -->
<!-- Fixes NXC-791 -->

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2024-07-10 10:21:58 +02:00 committed by GitHub
parent 2b7b5231fb
commit e31b1689c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 121 additions and 76 deletions

View File

@ -1,8 +1,8 @@
import { type CreateNodesContext } from '@nx/devkit'; import { type CreateNodesContext } from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils';
import { minimatch } from 'minimatch'; 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 { setupWorkspaceContext } from 'nx/src/utils/workspace-context';
import { PLUGIN_NAME, createNodesV2, type TscPluginOptions } from './plugin';
describe(`Plugin: ${PLUGIN_NAME}`, () => { describe(`Plugin: ${PLUGIN_NAME}`, () => {
let context: CreateNodesContext; let context: CreateNodesContext;
@ -2170,7 +2170,7 @@ async function applyFilesToTempFsAndContext(
await tempFs.createFiles(fileSys); await tempFs.createFiles(fileSys);
// @ts-expect-error update otherwise readonly property for testing // @ts-expect-error update otherwise readonly property for testing
context.configFiles = Object.keys(fileSys).filter((file) => context.configFiles = Object.keys(fileSys).filter((file) =>
minimatch(file, createNodes[0], { dot: true }) minimatch(file, createNodesV2[0], { dot: true })
); );
setupWorkspaceContext(tempFs.tempDir); setupWorkspaceContext(tempFs.tempDir);
} }
@ -2181,15 +2181,19 @@ async function invokeCreateNodesOnMatchingFiles(
) { ) {
const aggregateProjects: Record<string, any> = {}; const aggregateProjects: Record<string, any> = {};
for (const file of context.configFiles) { for (const file of context.configFiles) {
const nodes = await createNodes[1](file, pluginOptions, context); const results = await createNodesV2[1]([file], pluginOptions, context);
for (const [projectName, project] of Object.entries(nodes.projects ?? {})) { for (const [, nodes] of results) {
if (aggregateProjects[projectName]) { for (const [projectName, project] of Object.entries(
aggregateProjects[projectName].targets = { nodes.projects ?? {}
...aggregateProjects[projectName].targets, )) {
...project.targets, if (aggregateProjects[projectName]) {
}; aggregateProjects[projectName].targets = {
} else { ...aggregateProjects[projectName].targets,
aggregateProjects[projectName] = project; ...project.targets,
};
} else {
aggregateProjects[projectName] = project;
}
} }
} }
} }

View File

@ -1,20 +1,26 @@
import { import {
createNodesFromFiles,
detectPackageManager, detectPackageManager,
joinPathFragments, joinPathFragments,
logger,
normalizePath, normalizePath,
readJsonFile, readJsonFile,
writeJsonFile, writeJsonFile,
type CreateDependencies, type CreateDependencies,
type CreateNodes, type CreateNodes,
type CreateNodesContext, type CreateNodesContext,
type CreateNodesResult,
type CreateNodesV2,
type NxJsonConfiguration, type NxJsonConfiguration,
type ProjectConfiguration,
type TargetConfiguration, type TargetConfiguration,
} from '@nx/devkit'; } from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { minimatch } from 'minimatch';
import { existsSync, readdirSync, statSync } from 'node:fs'; import { existsSync, readdirSync, statSync } from 'node:fs';
import { basename, dirname, join, relative } from 'node:path'; 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 // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { getLockFileName } from 'nx/src/plugins/js/lock-file/lock-file'; import { getLockFileName } from 'nx/src/plugins/js/lock-file/lock-file';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
@ -49,85 +55,119 @@ interface NormalizedPluginOptions {
}; };
} }
const cachePath = join(workspaceDataDirectory, 'tsc.hash'); type TscProjectResult = Pick<ProjectConfiguration, 'targets'>;
const targetsCache = readTargetsCache();
function readTargetsCache(): Record< function readTargetsCache(cachePath: string): Record<string, TscProjectResult> {
string,
Record<string, TargetConfiguration<unknown>>
> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; return existsSync(cachePath) ? readJsonFile(cachePath) : {};
} }
function writeTargetsToCache() { function writeTargetsToCache(
const oldCache = readTargetsCache(); cachePath: string,
writeJsonFile(cachePath, { results?: Record<string, TscProjectResult>
...oldCache, ) {
...targetsCache, writeJsonFile(cachePath, results);
});
} }
/**
* @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'.
*/
export const createDependencies: CreateDependencies = () => { export const createDependencies: CreateDependencies = () => {
writeTargetsToCache();
return []; return [];
}; };
export const PLUGIN_NAME = '@nx/js/typescript'; export const PLUGIN_NAME = '@nx/js/typescript';
export const createNodes: CreateNodes<TscPluginOptions> = [ const tsConfigGlob = '**/tsconfig*.json';
'**/tsconfig*.json',
async (configFilePath, options, context) => {
const pluginOptions = normalizePluginOptions(options);
const projectRoot = dirname(configFilePath);
const fullConfigPath = joinPathFragments(
context.workspaceRoot,
configFilePath
);
// Do not create a project if package.json and project.json isn't there. export const createNodesV2: CreateNodesV2<TscPluginOptions> = [
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); tsConfigGlob,
if ( async (configFilePaths, options, context) => {
!siblingFiles.includes('package.json') && const optionsHash = hashObject(options);
!siblingFiles.includes('project.json') const cachePath = join(workspaceDataDirectory, `tsc-${optionsHash}.hash`);
) { const targetsCache = readTargetsCache(cachePath);
return {}; 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<TscPluginOptions> = [
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<string, TscProjectResult>
): Promise<CreateNodesResult> {
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( function buildTscTargets(
configFilePath: string, configFilePath: string,
projectRoot: string, projectRoot: string,
@ -220,7 +260,7 @@ function buildTscTargets(
}; };
} }
return targets; return { targets };
} }
function getInputs( function getInputs(

View File

@ -1,4 +1,5 @@
export { export {
createDependencies, createDependencies,
createNodes, createNodes,
createNodesV2,
} from './src/plugins/typescript/plugin'; } from './src/plugins/typescript/plugin';