feat(core): add multi hash fn (#29935)

Adds function to compute multiple glob hashes in native code at the same time, greatly speeding up certain plugin performance.
This commit is contained in:
Craigory Coppola 2025-02-13 14:21:54 -05:00 committed by GitHub
parent 2ebdd2e5a2
commit c2e89f87b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 550 additions and 243 deletions

View File

@ -10,12 +10,12 @@
#### Parameters #### Parameters
| Name | Type | | Name | Type |
| :------------ | :------------------------------------------------------------------------- | | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createNodes` | [`CreateNodesFunction`](../../devkit/documents/CreateNodesFunction)\<`T`\> | | `createNodes` | (`projectConfigurationFile`: `string`, `options`: `T`, `context`: [`CreateNodesContext`](../../devkit/documents/CreateNodesContext), `idx`: `number`) => [`CreateNodesResult`](../../devkit/documents/CreateNodesResult) \| `Promise`\<[`CreateNodesResult`](../../devkit/documents/CreateNodesResult)\> |
| `configFiles` | readonly `string`[] | | `configFiles` | readonly `string`[] |
| `options` | `T` | | `options` | `T` |
| `context` | [`CreateNodesContextV2`](../../devkit/documents/CreateNodesContextV2) | | `context` | [`CreateNodesContextV2`](../../devkit/documents/CreateNodesContextV2) |
#### Returns #### Returns

View File

@ -1,6 +1,12 @@
# Function: isDaemonEnabled # Function: isDaemonEnabled
**isDaemonEnabled**(): `boolean` **isDaemonEnabled**(`nxJson?`): `boolean`
#### Parameters
| Name | Type |
| :------- | :----------------------------------------------------------------------------------------- |
| `nxJson` | [`NxJsonConfiguration`](../../devkit/documents/NxJsonConfiguration)\<`string`[] \| `"*"`\> |
#### Returns #### Returns

View File

@ -5,7 +5,11 @@ import {
hashArray, hashArray,
} from 'nx/src/devkit-exports'; } from 'nx/src/devkit-exports';
import { hashObject, hashWithWorkspaceContext } from 'nx/src/devkit-internals'; import {
hashMultiGlobWithWorkspaceContext,
hashObject,
hashWithWorkspaceContext,
} from 'nx/src/devkit-internals';
export async function calculateHashForCreateNodes( export async function calculateHashForCreateNodes(
projectRoot: string, projectRoot: string,
@ -21,3 +25,28 @@ export async function calculateHashForCreateNodes(
hashObject(options), hashObject(options),
]); ]);
} }
export async function calculateHashesForCreateNodes(
projectRoots: string[],
options: object,
context: CreateNodesContext | CreateNodesContextV2,
additionalGlobs: string[][] = []
): Promise<string[]> {
if (
additionalGlobs.length &&
additionalGlobs.length !== projectRoots.length
) {
throw new Error(
'If additionalGlobs is provided, it must be the same length as projectRoots'
);
}
return hashMultiGlobWithWorkspaceContext(
context.workspaceRoot,
projectRoots.map((projectRoot, idx) => [
join(projectRoot, '**/*'),
...(additionalGlobs.length ? additionalGlobs[idx] : []),
])
).then((hashes) => {
return hashes.map((hash) => hashArray([hash, hashObject(options)]));
});
}

View File

@ -11,7 +11,10 @@ import {
TargetConfiguration, TargetConfiguration,
writeJsonFile, writeJsonFile,
} from '@nx/devkit'; } from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import {
calculateHashesForCreateNodes,
calculateHashForCreateNodes,
} from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { basename, dirname, join, normalize, sep } from 'node:path/posix'; import { basename, dirname, join, normalize, sep } from 'node:path/posix';
import { hashObject } from 'nx/src/hasher/file-hasher'; import { hashObject } from 'nx/src/hasher/file-hasher';
@ -186,10 +189,10 @@ const internalCreateNodesV2 = async (
configFilePath: string, configFilePath: string,
options: EslintPluginOptions, options: EslintPluginOptions,
context: CreateNodesContextV2, context: CreateNodesContextV2,
eslintConfigFiles: string[],
projectRootsByEslintRoots: Map<string, string[]>, projectRootsByEslintRoots: Map<string, string[]>,
lintableFilesPerProjectRoot: Map<string, string[]>, lintableFilesPerProjectRoot: Map<string, string[]>,
projectsCache: Record<string, CreateNodesResult['projects']> projectsCache: Record<string, CreateNodesResult['projects']>,
hashByRoot: Map<string, string>
): Promise<CreateNodesResult> => { ): Promise<CreateNodesResult> => {
const configDir = dirname(configFilePath); const configDir = dirname(configFilePath);
@ -201,19 +204,7 @@ const internalCreateNodesV2 = async (
const projects: CreateNodesResult['projects'] = {}; const projects: CreateNodesResult['projects'] = {};
await Promise.all( await Promise.all(
projectRootsByEslintRoots.get(configDir).map(async (projectRoot) => { projectRootsByEslintRoots.get(configDir).map(async (projectRoot) => {
const parentConfigs = eslintConfigFiles.filter((eslintConfig) => const hash = hashByRoot.get(projectRoot);
isSubDir(projectRoot, dirname(eslintConfig))
);
const hash = await calculateHashForCreateNodes(
projectRoot,
options,
{
configFiles: eslintConfigFiles,
nxJsonConfiguration: context.nxJsonConfiguration,
workspaceRoot: context.workspaceRoot,
},
[...parentConfigs, join(projectRoot, '.eslintignore')]
);
if (projectsCache[hash]) { if (projectsCache[hash]) {
// We can reuse the projects in the cache. // We can reuse the projects in the cache.
@ -280,6 +271,20 @@ export const createNodesV2: CreateNodesV2<EslintPluginOptions> = [
options, options,
context context
); );
const hashes = await calculateHashesForCreateNodes(
projectRoots,
options,
context,
projectRoots.map((root) => {
const parentConfigs = eslintConfigFiles.filter((eslintConfig) =>
isSubDir(root, dirname(eslintConfig))
);
return [...parentConfigs, join(root, '.eslintignore')];
})
);
const hashByRoot = new Map<string, string>(
projectRoots.map((r, i) => [r, hashes[i]])
);
try { try {
return await createNodesFromFiles( return await createNodesFromFiles(
(configFile, options, context) => (configFile, options, context) =>
@ -287,10 +292,10 @@ export const createNodesV2: CreateNodesV2<EslintPluginOptions> = [
configFile, configFile,
options, options,
context, context,
eslintConfigFiles,
projectRootsByEslintRoots, projectRootsByEslintRoots,
lintableFilesPerProjectRoot, lintableFilesPerProjectRoot,
targetsCache targetsCache,
hashByRoot
), ),
eslintConfigFiles, eslintConfigFiles,
options, options,

View File

@ -1,6 +1,7 @@
import { import {
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
CreateNodesContextV2,
createNodesFromFiles, createNodesFromFiles,
CreateNodesV2, CreateNodesV2,
getPackageManagerCommand, getPackageManagerCommand,
@ -13,7 +14,10 @@ import {
TargetConfiguration, TargetConfiguration,
writeJsonFile, writeJsonFile,
} from '@nx/devkit'; } from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import {
calculateHashesForCreateNodes,
calculateHashForCreateNodes,
} from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { import {
clearRequireCache, clearRequireCache,
loadConfigFile, loadConfigFile,
@ -72,17 +76,70 @@ export const createNodesV2: CreateNodesV2<JestPluginOptions> = [
// Cache jest preset(s) to avoid penalties of module load times. Most of jest configs will use the same preset. // Cache jest preset(s) to avoid penalties of module load times. Most of jest configs will use the same preset.
const presetCache: Record<string, unknown> = {}; const presetCache: Record<string, unknown> = {};
const packageManagerWorkspacesGlob = combineGlobPatterns(
getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot)
);
options = normalizeOptions(options);
const { roots: projectRoots, configFiles: validConfigFiles } =
configFiles.reduce(
(acc, configFile) => {
const potentialRoot = dirname(configFile);
if (
checkIfConfigFileShouldBeProject(
configFile,
potentialRoot,
packageManagerWorkspacesGlob,
context
)
) {
acc.roots.push(potentialRoot);
acc.configFiles.push(configFile);
}
return acc;
},
{
roots: [],
configFiles: [],
} as {
roots: string[];
configFiles: string[];
}
);
const hashes = await calculateHashesForCreateNodes(
projectRoots,
options,
context
);
try { try {
return await createNodesFromFiles( return await createNodesFromFiles(
(configFile, options, context) => async (configFilePath, options, context, idx) => {
createNodesInternal( const projectRoot = projectRoots[idx];
configFile, const hash = hashes[idx];
targetsCache[hash] ??= await buildJestTargets(
configFilePath,
projectRoot,
options, options,
context, context,
targetsCache,
presetCache presetCache
), );
configFiles,
const { targets, metadata } = targetsCache[hash];
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
},
validConfigFiles,
options, options,
context context
); );
@ -98,35 +155,63 @@ export const createNodesV2: CreateNodesV2<JestPluginOptions> = [
*/ */
export const createNodes: CreateNodes<JestPluginOptions> = [ export const createNodes: CreateNodes<JestPluginOptions> = [
jestConfigGlob, jestConfigGlob,
(...args) => { async (configFilePath, options, context) => {
logger.warn( logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
); );
return createNodesInternal(...args, {}, {}); const projectRoot = dirname(configFilePath);
const packageManagerWorkspacesGlob = combineGlobPatterns(
getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot)
);
if (
!checkIfConfigFileShouldBeProject(
configFilePath,
projectRoot,
packageManagerWorkspacesGlob,
context
)
) {
return {};
}
options = normalizeOptions(options);
const { targets, metadata } = await buildJestTargets(
configFilePath,
projectRoot,
options,
context,
{}
);
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
}, },
]; ];
async function createNodesInternal( function checkIfConfigFileShouldBeProject(
configFilePath: string, configFilePath: string,
options: JestPluginOptions, projectRoot: string,
context: CreateNodesContext, packageManagerWorkspacesGlob: string,
targetsCache: Record<string, JestTargets>, context: CreateNodesContext | CreateNodesContextV2
presetCache: Record<string, unknown> ): boolean {
) {
const projectRoot = dirname(configFilePath);
const packageManagerWorkspacesGlob = combineGlobPatterns(
getGlobPatternsFromPackageManagerWorkspaces(context.workspaceRoot)
);
// Do not create a project if package.json and project.json isn't there. // Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if ( if (
!siblingFiles.includes('package.json') && !siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json') !siblingFiles.includes('project.json')
) { ) {
return {}; return false;
} else if ( } else if (
!siblingFiles.includes('project.json') && !siblingFiles.includes('project.json') &&
siblingFiles.includes('package.json') siblingFiles.includes('package.json')
@ -136,7 +221,7 @@ async function createNodesInternal(
const isPackageJsonProject = minimatch(path, packageManagerWorkspacesGlob); const isPackageJsonProject = minimatch(path, packageManagerWorkspacesGlob);
if (!isPackageJsonProject) { if (!isPackageJsonProject) {
return {}; return false;
} }
} }
@ -148,31 +233,9 @@ async function createNodesInternal(
// The `getJestProjectsAsync` function uses the project graph, which leads to a // The `getJestProjectsAsync` function uses the project graph, which leads to a
// circular dependency. We can skip this since it's no intended to be used for // circular dependency. We can skip this since it's no intended to be used for
// an Nx project. // an Nx project.
return {}; return false;
} }
return true;
options = normalizeOptions(options);
const hash = await calculateHashForCreateNodes(projectRoot, options, context);
targetsCache[hash] ??= await buildJestTargets(
configFilePath,
projectRoot,
options,
context,
presetCache
);
const { targets, metadata } = targetsCache[hash];
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
} }
async function buildJestTargets( async function buildJestTargets(

View File

@ -2,15 +2,16 @@ import { existsSync } from 'fs';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import type { ChangelogRenderOptions } from '../../release/changelog-renderer'; import type { ChangelogRenderOptions } from '../../release/changelog-renderer';
import { readJsonFile } from '../utils/fileutils'; import type { PackageManager } from '../utils/package-manager';
import { PackageManager } from '../utils/package-manager'; import type {
import { workspaceRoot } from '../utils/workspace-root';
import {
InputDefinition, InputDefinition,
TargetConfiguration, TargetConfiguration,
TargetDependencyConfig, TargetDependencyConfig,
} from './workspace-json-project-json'; } from './workspace-json-project-json';
import { readJsonFile } from '../utils/fileutils';
import { workspaceRoot } from '../utils/workspace-root';
export type ImplicitDependencyEntry<T = '*' | string[]> = { export type ImplicitDependencyEntry<T = '*' | string[]> = {
[key: string]: T | ImplicitJsonSubsetDependency<T>; [key: string]: T | ImplicitJsonSubsetDependency<T>;
}; };

View File

@ -16,7 +16,6 @@ import { getFullOsSocketPath, killSocketOrPath } from '../socket-utils';
import { import {
DAEMON_DIR_FOR_CURRENT_WORKSPACE, DAEMON_DIR_FOR_CURRENT_WORKSPACE,
DAEMON_OUTPUT_LOG_FILE, DAEMON_OUTPUT_LOG_FILE,
isDaemonDisabled,
removeSocketDir, removeSocketDir,
} from '../tmp-dir'; } from '../tmp-dir';
import { FileData, ProjectGraph } from '../../config/project-graph'; import { FileData, ProjectGraph } from '../../config/project-graph';
@ -50,7 +49,12 @@ import {
GET_FILES_IN_DIRECTORY, GET_FILES_IN_DIRECTORY,
HandleGetFilesInDirectoryMessage, HandleGetFilesInDirectoryMessage,
} from '../message-types/get-files-in-directory'; } from '../message-types/get-files-in-directory';
import { HASH_GLOB, HandleHashGlobMessage } from '../message-types/hash-glob'; import {
HASH_GLOB,
HASH_MULTI_GLOB,
HandleHashGlobMessage,
HandleHashMultiGlobMessage,
} from '../message-types/hash-glob';
import { import {
GET_ESTIMATED_TASK_TIMINGS, GET_ESTIMATED_TASK_TIMINGS,
GET_FLAKY_TASKS, GET_FLAKY_TASKS,
@ -91,6 +95,7 @@ import {
POST_TASKS_EXECUTION, POST_TASKS_EXECUTION,
PRE_TASKS_EXECUTION, PRE_TASKS_EXECUTION,
} from '../message-types/run-tasks-execution-hooks'; } from '../message-types/run-tasks-execution-hooks';
import { isDaemonEnabled } from './enabled';
const DAEMON_ENV_SETTINGS = { const DAEMON_ENV_SETTINGS = {
NX_PROJECT_GLOB_CACHE: 'false', NX_PROJECT_GLOB_CACHE: 'false',
@ -136,48 +141,7 @@ export class DaemonClient {
private _err: FileHandle = null; private _err: FileHandle = null;
enabled() { enabled() {
if (this._enabled === undefined) { return isDaemonEnabled(this.nxJson);
const useDaemonProcessOption = this.nxJson?.useDaemonProcess;
const env = process.env.NX_DAEMON;
// env takes precedence
// option=true,env=false => no daemon
// option=false,env=undefined => no daemon
// option=false,env=false => no daemon
// option=undefined,env=undefined => daemon
// option=true,env=true => daemon
// option=false,env=true => daemon
// CI=true,env=undefined => no daemon
// CI=true,env=false => no daemon
// CI=true,env=true => daemon
// docker=true,env=undefined => no daemon
// docker=true,env=false => no daemon
// docker=true,env=true => daemon
// WASM => no daemon because file watching does not work
if (
((isCI() || isDocker()) && env !== 'true') ||
isDaemonDisabled() ||
nxJsonIsNotPresent() ||
(useDaemonProcessOption === undefined && env === 'false') ||
(useDaemonProcessOption === true && env === 'false') ||
(useDaemonProcessOption === false && env === undefined) ||
(useDaemonProcessOption === false && env === 'false')
) {
this._enabled = false;
} else if (IS_WASM) {
output.warn({
title:
'The Nx Daemon is unsupported in WebAssembly environments. Some things may be slower than or not function as expected.',
});
this._enabled = false;
} else {
this._enabled = true;
}
}
return this._enabled;
} }
reset() { reset() {
@ -385,6 +349,14 @@ export class DaemonClient {
return this.sendToDaemonViaQueue(message); return this.sendToDaemonViaQueue(message);
} }
hashMultiGlob(globGroups: string[][]): Promise<string[]> {
const message: HandleHashMultiGlobMessage = {
type: HASH_MULTI_GLOB,
globGroups: globGroups,
};
return this.sendToDaemonViaQueue(message);
}
getFlakyTasks(hashes: string[]): Promise<string[]> { getFlakyTasks(hashes: string[]): Promise<string[]> {
const message: HandleGetFlakyTasks = { const message: HandleGetFlakyTasks = {
type: GET_FLAKY_TASKS, type: GET_FLAKY_TASKS,
@ -705,27 +677,6 @@ export class DaemonClient {
export const daemonClient = new DaemonClient(); export const daemonClient = new DaemonClient();
export function isDaemonEnabled() {
return daemonClient.enabled();
}
function isDocker() {
try {
statSync('/.dockerenv');
return true;
} catch {
try {
return readFileSync('/proc/self/cgroup', 'utf8')?.includes('docker');
} catch {}
return false;
}
}
function nxJsonIsNotPresent() {
return !hasNxJson(workspaceRoot);
}
function daemonProcessException(message: string) { function daemonProcessException(message: string) {
try { try {
let log = readFileSync(DAEMON_OUTPUT_LOG_FILE).toString().split('\n'); let log = readFileSync(DAEMON_OUTPUT_LOG_FILE).toString().split('\n');

View File

@ -0,0 +1,79 @@
import {
hasNxJson,
readNxJson,
type NxJsonConfiguration,
} from '../../config/nx-json';
import { readFileSync, statSync } from 'node:fs';
import { isCI } from '../../utils/is-ci';
import { workspaceRoot } from '../../utils/workspace-root';
import { isDaemonDisabled } from '../tmp-dir';
let _enabled: boolean | undefined;
export function isDaemonEnabled(nxJson: NxJsonConfiguration = readNxJson()) {
if (_enabled === undefined) {
const useDaemonProcessOption = nxJson?.useDaemonProcess;
const env = process.env.NX_DAEMON;
// env takes precedence
// option=true,env=false => no daemon
// option=false,env=undefined => no daemon
// option=false,env=false => no daemon
// option=undefined,env=undefined => daemon
// option=true,env=true => daemon
// option=false,env=true => daemon
// CI=true,env=undefined => no daemon
// CI=true,env=false => no daemon
// CI=true,env=true => daemon
// docker=true,env=undefined => no daemon
// docker=true,env=false => no daemon
// docker=true,env=true => daemon
// WASM => no daemon because file watching does not work
if (
((isCI() || isDocker()) && env !== 'true') ||
isDaemonDisabled() ||
nxJsonIsNotPresent() ||
(useDaemonProcessOption === undefined && env === 'false') ||
(useDaemonProcessOption === true && env === 'false') ||
(useDaemonProcessOption === false && env === undefined) ||
(useDaemonProcessOption === false && env === 'false')
) {
_enabled = false;
} else if (
(require('../../native') as typeof import('../../native')).IS_WASM
) {
(
require('../../utils/output') as typeof import('../../utils/output')
).output.warn({
title:
'The Nx Daemon is unsupported in WebAssembly environments. Some things may be slower than or not function as expected.',
});
_enabled = false;
} else {
_enabled = true;
}
}
return _enabled;
}
function isDocker() {
try {
statSync('/.dockerenv');
return true;
} catch {
try {
return readFileSync('/proc/self/cgroup', 'utf8')?.includes('docker');
} catch {}
return false;
}
}
function nxJsonIsNotPresent() {
return !hasNxJson(workspaceRoot);
}

View File

@ -16,3 +16,21 @@ export function isHandleHashGlobMessage(
message['type'] === HASH_GLOB message['type'] === HASH_GLOB
); );
} }
export const HASH_MULTI_GLOB = 'HASH_MULTI_GLOB' as const;
export type HandleHashMultiGlobMessage = {
type: typeof HASH_MULTI_GLOB;
globGroups: string[][];
};
export function isHandleHashMultiGlobMessage(
message: unknown
): message is HandleHashMultiGlobMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === HASH_MULTI_GLOB
);
}

View File

@ -1,5 +1,8 @@
import { workspaceRoot } from '../../utils/workspace-root'; import { workspaceRoot } from '../../utils/workspace-root';
import { hashWithWorkspaceContext } from '../../utils/workspace-context'; import {
hashMultiGlobWithWorkspaceContext,
hashWithWorkspaceContext,
} from '../../utils/workspace-context';
import { HandlerResult } from './server'; import { HandlerResult } from './server';
export async function handleHashGlob( export async function handleHashGlob(
@ -12,3 +15,13 @@ export async function handleHashGlob(
description: 'handleHashGlob', description: 'handleHashGlob',
}; };
} }
export async function handleHashMultiGlob(
globs: string[][]
): Promise<HandlerResult> {
const files = await hashMultiGlobWithWorkspaceContext(workspaceRoot, globs);
return {
response: JSON.stringify(files),
description: 'handleHashMultiGlob',
};
}

View File

@ -77,8 +77,12 @@ import {
isHandleGetFilesInDirectoryMessage, isHandleGetFilesInDirectoryMessage,
} from '../message-types/get-files-in-directory'; } from '../message-types/get-files-in-directory';
import { handleGetFilesInDirectory } from './handle-get-files-in-directory'; import { handleGetFilesInDirectory } from './handle-get-files-in-directory';
import { HASH_GLOB, isHandleHashGlobMessage } from '../message-types/hash-glob'; import {
import { handleHashGlob } from './handle-hash-glob'; HASH_GLOB,
isHandleHashGlobMessage,
isHandleHashMultiGlobMessage,
} from '../message-types/hash-glob';
import { handleHashGlob, handleHashMultiGlob } from './handle-hash-glob';
import { import {
GET_ESTIMATED_TASK_TIMINGS, GET_ESTIMATED_TASK_TIMINGS,
GET_FLAKY_TASKS, GET_FLAKY_TASKS,
@ -264,6 +268,10 @@ async function handleMessage(socket, data: string) {
await handleResult(socket, HASH_GLOB, () => await handleResult(socket, HASH_GLOB, () =>
handleHashGlob(payload.globs, payload.exclude) handleHashGlob(payload.globs, payload.exclude)
); );
} else if (isHandleHashMultiGlobMessage(payload)) {
await handleResult(socket, HASH_GLOB, () =>
handleHashMultiGlob(payload.globGroups)
);
} else if (isHandleGetFlakyTasksMessage(payload)) { } else if (isHandleGetFlakyTasksMessage(payload)) {
await handleResult(socket, GET_FLAKY_TASKS, () => await handleResult(socket, GET_FLAKY_TASKS, () =>
handleGetFlakyTasks(payload.hashes) handleGetFlakyTasks(payload.hashes)

View File

@ -260,4 +260,4 @@ export { cacheDir } from './utils/cache-directory';
*/ */
export { createProjectFileMapUsingProjectGraph } from './project-graph/file-map-utils'; export { createProjectFileMapUsingProjectGraph } from './project-graph/file-map-utils';
export { isDaemonEnabled } from './daemon/client/client'; export { isDaemonEnabled } from './daemon/client/enabled';

View File

@ -20,7 +20,10 @@ export { stripIndent } from './utils/logger';
export { readModulePackageJson } from './utils/package-json'; export { readModulePackageJson } from './utils/package-json';
export { splitByColons } from './utils/split-target'; export { splitByColons } from './utils/split-target';
export { hashObject } from './hasher/file-hasher'; export { hashObject } from './hasher/file-hasher';
export { hashWithWorkspaceContext } from './utils/workspace-context'; export {
hashWithWorkspaceContext,
hashMultiGlobWithWorkspaceContext,
} from './utils/workspace-context';
export { export {
createProjectRootMappingsFromProjectConfigurations, createProjectRootMappingsFromProjectConfigurations,
findProjectForPath, findProjectForPath,

View File

@ -100,6 +100,7 @@ export declare class WorkspaceContext {
* as the input globs. * as the input globs.
*/ */
multiGlob(globs: Array<string>, exclude?: Array<string> | undefined | null): Array<Array<string>> multiGlob(globs: Array<string>, exclude?: Array<string> | undefined | null): Array<Array<string>>
hashFilesMatchingGlobs(globGroups: Array<Array<string>>): Array<string>
hashFilesMatchingGlob(globs: Array<string>, exclude?: Array<string> | undefined | null): string hashFilesMatchingGlob(globs: Array<string>, exclude?: Array<string> | undefined | null): string
incrementalUpdate(updatedFiles: Array<string>, deletedFiles: Array<string>): Record<string, string> incrementalUpdate(updatedFiles: Array<string>, deletedFiles: Array<string>): Record<string, string>
updateProjectFiles(projectRootMappings: ProjectRootMappings, projectFiles: ExternalObject<ProjectFiles>, globalFiles: ExternalObject<Array<FileData>>, updatedFiles: Record<string, string>, deletedFiles: Array<string>): UpdatedWorkspaceFiles updateProjectFiles(projectRootMappings: ProjectRootMappings, projectFiles: ExternalObject<ProjectFiles>, globalFiles: ExternalObject<Array<FileData>>, updatedFiles: Record<string, string>, deletedFiles: Array<string>): UpdatedWorkspaceFiles

View File

@ -253,6 +253,29 @@ impl WorkspaceContext {
.collect() .collect()
} }
#[napi]
pub fn hash_files_matching_globs(
&self,
glob_groups: Vec<Vec<String>>,
) -> napi::Result<Vec<String>> {
let files = &self.all_file_data();
let hashes = glob_groups
.into_iter()
.map(|globs| {
let globbed_files =
config_files::glob_files(files, globs, None)?.collect::<Vec<_>>();
let mut hasher = xxh3::Xxh3::new();
for file in globbed_files {
hasher.update(file.file.as_bytes());
hasher.update(file.hash.as_bytes());
}
Ok(hasher.digest().to_string())
})
.collect::<napi::Result<Vec<_>>>()?;
Ok(hashes)
}
#[napi] #[napi]
pub fn hash_files_matching_glob( pub fn hash_files_matching_glob(
&self, &self,

View File

@ -1,3 +1,7 @@
import { performance } from 'node:perf_hooks';
performance.mark(`plugin worker ${process.pid} code loading -- start`);
import { consumeMessage, isPluginWorkerMessage } from './messaging'; import { consumeMessage, isPluginWorkerMessage } from './messaging';
import { createSerializableError } from '../../../utils/serializable-error'; import { createSerializableError } from '../../../utils/serializable-error';
import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket'; import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket';
@ -10,6 +14,13 @@ if (process.env.NX_PERF_LOGGING === 'true') {
require('../../../utils/perf-logging'); require('../../../utils/perf-logging');
} }
performance.mark(`plugin worker ${process.pid} code loading -- end`);
performance.measure(
`plugin worker ${process.pid} code loading`,
`plugin worker ${process.pid} code loading -- start`,
`plugin worker ${process.pid} code loading -- end`
);
global.NX_GRAPH_CREATION = true; global.NX_GRAPH_CREATION = true;
global.NX_PLUGIN_WORKER = true; global.NX_PLUGIN_WORKER = true;
let connected = false; let connected = false;

View File

@ -1,5 +1,5 @@
import type { ProjectGraph } from '../../config/project-graph'; import type { ProjectGraph } from '../../config/project-graph';
import type { PluginConfiguration } from '../../config/nx-json'; import { readNxJson, type PluginConfiguration } from '../../config/nx-json';
import { import {
AggregateCreateNodesError, AggregateCreateNodesError,
isAggregateCreateNodesError, isAggregateCreateNodesError,
@ -17,7 +17,7 @@ import type {
} from './public-api'; } from './public-api';
import { createNodesFromFiles } from './utils'; import { createNodesFromFiles } from './utils';
import { isIsolationEnabled } from './isolation/enabled'; import { isIsolationEnabled } from './isolation/enabled';
import { isDaemonEnabled } from '../../daemon/client/client'; import { isDaemonEnabled } from '../../daemon/client/enabled';
export class LoadedNxPlugin { export class LoadedNxPlugin {
index?: number; index?: number;
@ -123,7 +123,10 @@ export class LoadedNxPlugin {
this.preTasksExecution = async (context: PreTasksExecutionContext) => { this.preTasksExecution = async (context: PreTasksExecutionContext) => {
const updates = {}; const updates = {};
let originalEnv = process.env; let originalEnv = process.env;
if (isIsolationEnabled() || isDaemonEnabled()) { if (
isIsolationEnabled() ||
isDaemonEnabled(context.nxJsonConfiguration)
) {
process.env = new Proxy<NodeJS.ProcessEnv>(originalEnv, { process.env = new Proxy<NodeJS.ProcessEnv>(originalEnv, {
set: (target, key: string, value) => { set: (target, key: string, value) => {
target[key] = value; target[key] = value;

View File

@ -4,12 +4,13 @@ import type {
} from './public-api'; } from './public-api';
import { getPlugins } from './get-plugins'; import { getPlugins } from './get-plugins';
import { isOnDaemon } from '../../daemon/is-on-daemon'; import { isOnDaemon } from '../../daemon/is-on-daemon';
import { daemonClient, isDaemonEnabled } from '../../daemon/client/client'; import { daemonClient } from '../../daemon/client/client';
import { isDaemonEnabled } from '../../daemon/client/enabled';
export async function runPreTasksExecution( export async function runPreTasksExecution(
pluginContext: PreTasksExecutionContext pluginContext: PreTasksExecutionContext
) { ) {
if (isOnDaemon() || !isDaemonEnabled()) { if (isOnDaemon() || !isDaemonEnabled(pluginContext.nxJsonConfiguration)) {
performance.mark(`preTasksExecution:start`); performance.mark(`preTasksExecution:start`);
const plugins = await getPlugins(pluginContext.workspaceRoot); const plugins = await getPlugins(pluginContext.workspaceRoot);
const envs = await Promise.all( const envs = await Promise.all(
@ -30,7 +31,7 @@ export async function runPreTasksExecution(
}) })
); );
if (!isDaemonEnabled()) { if (!isDaemonEnabled(pluginContext.nxJsonConfiguration)) {
applyProcessEnvs(envs); applyProcessEnvs(envs);
} }
performance.mark(`preTasksExecution:end`); performance.mark(`preTasksExecution:end`);
@ -57,7 +58,7 @@ function applyProcessEnvs(envs: NodeJS.ProcessEnv[]) {
export async function runPostTasksExecution( export async function runPostTasksExecution(
context: PostTasksExecutionContext context: PostTasksExecutionContext
) { ) {
if (isOnDaemon() || !isDaemonEnabled()) { if (isOnDaemon() || !isDaemonEnabled(context.nxJsonConfiguration)) {
performance.mark(`postTasksExecution:start`); performance.mark(`postTasksExecution:start`);
const plugins = await getPlugins(); const plugins = await getPlugins();
await Promise.all( await Promise.all(

View File

@ -1,11 +1,16 @@
import { import {
CreateNodesContext,
CreateNodesContextV2, CreateNodesContextV2,
CreateNodesFunction,
CreateNodesResult, CreateNodesResult,
} from './public-api'; } from './public-api';
import { AggregateCreateNodesError } from '../error-types'; import { AggregateCreateNodesError } from '../error-types';
export async function createNodesFromFiles<T = unknown>( export async function createNodesFromFiles<T = unknown>(
createNodes: CreateNodesFunction<T>, createNodes: (
projectConfigurationFile: string,
options: T | undefined,
context: CreateNodesContext,
idx: number
) => CreateNodesResult | Promise<CreateNodesResult>,
configFiles: readonly string[], configFiles: readonly string[],
options: T, options: T,
context: CreateNodesContextV2 context: CreateNodesContextV2
@ -14,12 +19,17 @@ export async function createNodesFromFiles<T = unknown>(
const errors: Array<[file: string, error: Error]> = []; const errors: Array<[file: string, error: Error]> = [];
await Promise.all( await Promise.all(
configFiles.map(async (file) => { configFiles.map(async (file, idx) => {
try { try {
const value = await createNodes(file, options, { const value = await createNodes(
...context, file,
configFiles, options,
}); {
...context,
configFiles,
},
idx
);
if (value) { if (value) {
results.push([file, value] as const); results.push([file, value] as const);
} }

View File

@ -86,6 +86,17 @@ export async function hashWithWorkspaceContext(
return daemonClient.hashGlob(globs, exclude); return daemonClient.hashGlob(globs, exclude);
} }
export async function hashMultiGlobWithWorkspaceContext(
workspaceRoot: string,
globGroups: string[][]
) {
if (isOnDaemon() || !daemonClient.enabled()) {
ensureContextAvailable(workspaceRoot);
return workspaceContext.hashFilesMatchingGlobs(globGroups);
}
return daemonClient.hashMultiGlob(globGroups);
}
export async function updateContextWithChangedFiles( export async function updateContextWithChangedFiles(
workspaceRoot: string, workspaceRoot: string,
createdFiles: string[], createdFiles: string[],

View File

@ -1,6 +1,7 @@
import { exec } from 'child_process'; import { exec } from 'child_process';
import type { Compiler } from '@rspack/core'; import type { Compiler } from '@rspack/core';
import { daemonClient, isDaemonEnabled } from 'nx/src/daemon/client/client'; import { daemonClient } from 'nx/src/daemon/client/client';
import { isDaemonEnabled } from 'nx/src/daemon/client/enabled';
import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch'; import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch';
import { output } from 'nx/src/utils/output'; import { output } from 'nx/src/utils/output';

View File

@ -2,6 +2,7 @@ import {
CreateDependencies, CreateDependencies,
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
CreateNodesContextV2,
createNodesFromFiles, createNodesFromFiles,
CreateNodesV2, CreateNodesV2,
detectPackageManager, detectPackageManager,
@ -16,7 +17,10 @@ import {
import { dirname, isAbsolute, join, relative } from 'path'; import { dirname, isAbsolute, join, relative } from 'path';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { existsSync, readdirSync } from 'fs'; import { existsSync, readdirSync } from 'fs';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import {
calculateHashesForCreateNodes,
calculateHashForCreateNodes,
} from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { getLockFileName } from '@nx/js'; import { getLockFileName } from '@nx/js';
import { loadViteDynamicImport } from '../utils/executor-utils'; import { loadViteDynamicImport } from '../utils/executor-utils';
@ -42,7 +46,10 @@ export interface VitePluginOptions {
buildDepsTargetName?: string; buildDepsTargetName?: string;
} }
type ViteTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>; type ViteTargets = Pick<
ProjectConfiguration,
'targets' | 'metadata' | 'projectType'
>;
function readTargetsCache(cachePath: string): Record<string, ViteTargets> { function readTargetsCache(cachePath: string): Record<string, ViteTargets> {
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' && existsSync(cachePath) return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' && existsSync(cachePath)
@ -67,20 +74,88 @@ export const createNodesV2: CreateNodesV2<VitePluginOptions> = [
viteVitestConfigGlob, viteVitestConfigGlob,
async (configFilePaths, options, context) => { async (configFilePaths, options, context) => {
const optionsHash = hashObject(options); const optionsHash = hashObject(options);
const normalizedOptions = normalizeOptions(options);
const cachePath = join(workspaceDataDirectory, `vite-${optionsHash}.hash`); const cachePath = join(workspaceDataDirectory, `vite-${optionsHash}.hash`);
const targetsCache = readTargetsCache(cachePath); const targetsCache = readTargetsCache(cachePath);
const isUsingTsSolutionSetup = _isUsingTsSolutionSetup(); const isUsingTsSolutionSetup = _isUsingTsSolutionSetup();
const { roots: projectRoots, configFiles: validConfigFiles } =
configFilePaths.reduce(
(acc, configFile) => {
const potentialRoot = dirname(configFile);
if (checkIfConfigFileShouldBeProject(potentialRoot, context)) {
acc.roots.push(potentialRoot);
acc.configFiles.push(configFile);
}
return acc;
},
{
roots: [],
configFiles: [],
} as {
roots: string[];
configFiles: string[];
}
);
const lockfile = getLockFileName(
detectPackageManager(context.workspaceRoot)
);
const hashes = await calculateHashesForCreateNodes(
projectRoots,
{ ...normalizedOptions, isUsingTsSolutionSetup },
context,
projectRoots.map((r) => [lockfile])
);
try { try {
return await createNodesFromFiles( return await createNodesFromFiles(
(configFile, options, context) => async (configFile, _, context, idx) => {
createNodesInternal( const projectRoot = dirname(configFile);
configFile, // Do not create a project if package.json and project.json isn't there.
options, const siblingFiles = readdirSync(
context, join(context.workspaceRoot, projectRoot)
targetsCache, );
isUsingTsSolutionSetup
), const tsConfigFiles =
configFilePaths, siblingFiles.filter((p) =>
minimatch(p, 'tsconfig*{.json,.*.json}')
) ?? [];
// results from vitest.config.js will be different from results of vite.config.js
// but the hash will be the same because it is based on the files under the project root.
// Adding the config file path to the hash ensures that the final hash value is different
// for different config files.
const hash = hashes[idx] + configFile;
const { projectType, metadata, targets } = (targetsCache[hash] ??=
await buildViteTargets(
configFile,
projectRoot,
normalizedOptions,
tsConfigFiles,
isUsingTsSolutionSetup,
context
));
const project: ProjectConfiguration = {
root: projectRoot,
targets,
metadata,
};
// If project is buildable, then the project type.
// If it is not buildable, then leave it to other plugins/project.json to set the project type.
if (project.targets[normalizedOptions.buildTargetName]) {
project.projectType = projectType;
}
return {
projects: {
[projectRoot]: project,
},
};
},
validConfigFiles,
options, options,
context context
); );
@ -96,78 +171,52 @@ export const createNodes: CreateNodes<VitePluginOptions> = [
logger.warn( logger.warn(
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
); );
return createNodesInternal( const projectRoot = dirname(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 {};
}
const tsConfigFiles =
siblingFiles.filter((p) => minimatch(p, 'tsconfig*{.json,.*.json}')) ??
[];
const normalizedOptions = normalizeOptions(options);
const isUsingTsSolutionSetup = _isUsingTsSolutionSetup();
const { projectType, metadata, targets } = await buildViteTargets(
configFilePath, configFilePath,
options, projectRoot,
context, normalizedOptions,
{}, tsConfigFiles,
_isUsingTsSolutionSetup() isUsingTsSolutionSetup,
context
); );
const project: ProjectConfiguration = {
root: projectRoot,
targets,
metadata,
};
// If project is buildable, then the project type.
// If it is not buildable, then leave it to other plugins/project.json to set the project type.
if (project.targets[normalizedOptions.buildTargetName]) {
project.projectType = projectType;
}
return {
projects: {
[projectRoot]: project,
},
};
}, },
]; ];
async function createNodesInternal(
configFilePath: string,
options: VitePluginOptions,
context: CreateNodesContext,
targetsCache: Record<string, ViteTargets>,
isUsingTsSolutionSetup: boolean
) {
const projectRoot = dirname(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 {};
}
const tsConfigFiles =
siblingFiles.filter((p) => minimatch(p, 'tsconfig*{.json,.*.json}')) ?? [];
const normalizedOptions = normalizeOptions(options);
// We do not want to alter how the hash is calculated, so appending the config file path to the hash
// to prevent vite/vitest files overwriting the target cache created by the other
const hash =
(await calculateHashForCreateNodes(
projectRoot,
{ ...normalizedOptions, isUsingTsSolutionSetup },
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
)) + configFilePath;
const { isLibrary, ...viteTargets } = await buildViteTargets(
configFilePath,
projectRoot,
normalizedOptions,
tsConfigFiles,
isUsingTsSolutionSetup,
context
);
targetsCache[hash] ??= viteTargets;
const { targets, metadata } = targetsCache[hash];
const project: ProjectConfiguration = {
root: projectRoot,
targets,
metadata,
};
// If project is buildable, then the project type.
// If it is not buildable, then leave it to other plugins/project.json to set the project type.
if (project.targets[normalizedOptions.buildTargetName]) {
project.projectType = isLibrary ? 'library' : 'application';
}
return {
projects: {
[projectRoot]: project,
},
};
}
async function buildViteTargets( async function buildViteTargets(
configFilePath: string, configFilePath: string,
projectRoot: string, projectRoot: string,
@ -175,7 +224,7 @@ async function buildViteTargets(
tsConfigFiles: string[], tsConfigFiles: string[],
isUsingTsSolutionSetup: boolean, isUsingTsSolutionSetup: boolean,
context: CreateNodesContext context: CreateNodesContext
): Promise<ViteTargets & { isLibrary: boolean }> { ): Promise<ViteTargets> {
const absoluteConfigFilePath = joinPathFragments( const absoluteConfigFilePath = joinPathFragments(
context.workspaceRoot, context.workspaceRoot,
configFilePath configFilePath
@ -304,7 +353,11 @@ async function buildViteTargets(
); );
const metadata = {}; const metadata = {};
return { targets, metadata, isLibrary: Boolean(viteBuildConfig.build?.lib) }; return {
targets,
metadata,
projectType: viteBuildConfig.build?.lib ? 'library' : 'application',
};
} }
async function buildTarget( async function buildTarget(
@ -538,3 +591,19 @@ function normalizeOptions(options: VitePluginOptions): VitePluginOptions {
options.typecheckTargetName ??= 'typecheck'; options.typecheckTargetName ??= 'typecheck';
return options; return options;
} }
function checkIfConfigFileShouldBeProject(
projectRoot: string,
context: CreateNodesContext | CreateNodesContextV2
): boolean {
// 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 false;
}
return true;
}

View File

@ -1,8 +1,9 @@
import { exec } from 'child_process'; import { exec } from 'child_process';
import type { Compiler } from 'webpack'; import type { Compiler } from 'webpack';
import { daemonClient, isDaemonEnabled } from 'nx/src/daemon/client/client'; import { daemonClient } from 'nx/src/daemon/client/client';
import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch'; import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch';
import { output } from 'nx/src/utils/output'; import { output } from 'nx/src/utils/output';
import { isDaemonEnabled } from 'nx/src/daemon/client/enabled';
type PluginOptions = { type PluginOptions = {
skipInitialBuild?: boolean; skipInitialBuild?: boolean;