fix(misc): ensure plugins are not creating workspace context while creating nodes (#26253)

This commit is contained in:
Craigory Coppola 2024-05-31 18:54:56 -04:00 committed by GitHub
parent a308e1dc6b
commit 6f223005b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 718 additions and 197 deletions

View File

@ -129,6 +129,7 @@ It only uses language primitives and immutable objects
- [getProjects](../../devkit/documents/getProjects) - [getProjects](../../devkit/documents/getProjects)
- [getWorkspaceLayout](../../devkit/documents/getWorkspaceLayout) - [getWorkspaceLayout](../../devkit/documents/getWorkspaceLayout)
- [glob](../../devkit/documents/glob) - [glob](../../devkit/documents/glob)
- [globAsync](../../devkit/documents/globAsync)
- [hashArray](../../devkit/documents/hashArray) - [hashArray](../../devkit/documents/hashArray)
- [installPackagesTask](../../devkit/documents/installPackagesTask) - [installPackagesTask](../../devkit/documents/installPackagesTask)
- [isWorkspacesEnabled](../../devkit/documents/isWorkspacesEnabled) - [isWorkspacesEnabled](../../devkit/documents/isWorkspacesEnabled)

View File

@ -18,3 +18,7 @@ Paths should be unix-style with forward slashes.
`string`[] `string`[]
Normalized paths in the workspace that match the provided glob patterns. Normalized paths in the workspace that match the provided glob patterns.
**`Deprecated`**
Use [globAsync](../../devkit/documents/globAsync) instead.

View File

@ -0,0 +1,20 @@
# Function: globAsync
**globAsync**(`tree`, `patterns`): `Promise`\<`string`[]\>
Performs a tree-aware glob search on the files in a workspace. Able to find newly
created files and hides deleted files before the updates are committed to disk.
Paths should be unix-style with forward slashes.
#### Parameters
| Name | Type | Description |
| :--------- | :------------------------------------ | :---------------------- |
| `tree` | [`Tree`](../../devkit/documents/Tree) | The file system tree |
| `patterns` | `string`[] | A list of glob patterns |
#### Returns
`Promise`\<`string`[]\>
Normalized paths in the workspace that match the provided glob patterns.

View File

@ -129,6 +129,7 @@ It only uses language primitives and immutable objects
- [getProjects](../../devkit/documents/getProjects) - [getProjects](../../devkit/documents/getProjects)
- [getWorkspaceLayout](../../devkit/documents/getWorkspaceLayout) - [getWorkspaceLayout](../../devkit/documents/getWorkspaceLayout)
- [glob](../../devkit/documents/glob) - [glob](../../devkit/documents/glob)
- [globAsync](../../devkit/documents/globAsync)
- [hashArray](../../devkit/documents/hashArray) - [hashArray](../../devkit/documents/hashArray)
- [installPackagesTask](../../devkit/documents/installPackagesTask) - [installPackagesTask](../../devkit/documents/installPackagesTask)
- [isWorkspacesEnabled](../../devkit/documents/isWorkspacesEnabled) - [isWorkspacesEnabled](../../devkit/documents/isWorkspacesEnabled)

View File

@ -12,4 +12,5 @@ module.exports = {
coverageReporters: ['html'], coverageReporters: ['html'],
maxWorkers: 1, maxWorkers: 1,
testEnvironment: 'node', testEnvironment: 'node',
setupFiles: ['../../scripts/unit-test-setup.js'],
}; };

View File

@ -75,6 +75,11 @@ describe('Cypress builder', () => {
configuration, configuration,
}; };
}; };
(devkit as any).logger = {
warn: jest.fn(),
log: jest.fn(),
info: jest.fn(),
};
cypressRun = jest cypressRun = jest
.spyOn(Cypress, 'run') .spyOn(Cypress, 'run')
.mockReturnValue(Promise.resolve({})); .mockReturnValue(Promise.resolve({}));

View File

@ -19,12 +19,13 @@ import { getLockFileName } from '@nx/js';
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 { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
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 { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { NX_PLUGIN_OPTIONS } from '../utils/constants'; import { NX_PLUGIN_OPTIONS } from '../utils/constants';
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
import { hashObject } from 'nx/src/devkit-internals'; import { hashObject } from 'nx/src/devkit-internals';
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
export interface CypressPluginOptions { export interface CypressPluginOptions {
ciTargetName?: string; ciTargetName?: string;
@ -98,9 +99,12 @@ async function createNodesInternal(
return {}; return {};
} }
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= await buildCypressTargets( targetsCache[hash] ??= await buildCypressTargets(
configFilePath, configFilePath,
@ -237,7 +241,7 @@ async function buildCypressTargets(
: Array.isArray(cypressConfig.e2e.excludeSpecPattern) : Array.isArray(cypressConfig.e2e.excludeSpecPattern)
? cypressConfig.e2e.excludeSpecPattern.map((p) => join(projectRoot, p)) ? cypressConfig.e2e.excludeSpecPattern.map((p) => join(projectRoot, p))
: [join(projectRoot, cypressConfig.e2e.excludeSpecPattern)]; : [join(projectRoot, cypressConfig.e2e.excludeSpecPattern)];
const specFiles = globWithWorkspaceContext( const specFiles = await globWithWorkspaceContext(
context.workspaceRoot, context.workspaceRoot,
specPatterns, specPatterns,
excludeSpecPatterns excludeSpecPatterns

View File

@ -42,7 +42,7 @@ export const createDependencies: CreateDependencies = () => {
export const createNodes: CreateNodes<DetoxPluginOptions> = [ export const createNodes: CreateNodes<DetoxPluginOptions> = [
'**/{detox.config,.detoxrc}.{json,js}', '**/{detox.config,.detoxrc}.{json,js}',
(configFilePath, options, context) => { async (configFilePath, options, context) => {
options = normalizeOptions(options); options = normalizeOptions(options);
const projectRoot = dirname(configFilePath); const projectRoot = dirname(configFilePath);
@ -52,9 +52,12 @@ export const createNodes: CreateNodes<DetoxPluginOptions> = [
return {}; return {};
} }
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= buildDetoxTargets(projectRoot, options, context); targetsCache[hash] ??= buildDetoxTargets(projectRoot, options, context);

View File

@ -3,14 +3,14 @@ import { CreateNodesContext, hashArray } from 'nx/src/devkit-exports';
import { hashObject, hashWithWorkspaceContext } from 'nx/src/devkit-internals'; import { hashObject, hashWithWorkspaceContext } from 'nx/src/devkit-internals';
export function calculateHashForCreateNodes( export async function calculateHashForCreateNodes(
projectRoot: string, projectRoot: string,
options: object, options: object,
context: CreateNodesContext, context: CreateNodesContext,
additionalGlobs: string[] = [] additionalGlobs: string[] = []
): string { ): Promise<string> {
return hashArray([ return hashArray([
hashWithWorkspaceContext(context.workspaceRoot, [ await hashWithWorkspaceContext(context.workspaceRoot, [
join(projectRoot, '**/*'), join(projectRoot, '**/*'),
...additionalGlobs, ...additionalGlobs,
]), ]),

View File

@ -3,6 +3,8 @@ import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
import { WORKSPACE_PLUGIN_DIR } from '../../constants'; import { WORKSPACE_PLUGIN_DIR } from '../../constants';
import update from './rename-workspace-rules'; import update from './rename-workspace-rules';
import 'nx/src/internal-testing-utils/mock-project-graph';
const rule1Name = 'test-rule'; const rule1Name = 'test-rule';
const rule2Name = 'my-rule'; const rule2Name = 'my-rule';

View File

@ -3,6 +3,7 @@ import 'nx/src/internal-testing-utils/mock-project-graph';
import { NxJsonConfiguration, readJson, Tree, updateJson } from '@nx/devkit'; import { NxJsonConfiguration, readJson, Tree, updateJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { LinterInitOptions, lintInitGenerator } from './init'; import { LinterInitOptions, lintInitGenerator } from './init';
import { setWorkspaceRoot } from 'nx/src/utils/workspace-root';
describe('@nx/eslint:init', () => { describe('@nx/eslint:init', () => {
let tree: Tree; let tree: Tree;
@ -10,6 +11,7 @@ describe('@nx/eslint:init', () => {
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
setWorkspaceRoot(tree.root);
options = { options = {
addPlugin: true, addPlugin: true,
}; };

View File

@ -51,7 +51,7 @@ export const createNodes: CreateNodes<EslintPluginOptions> = [
} }
} }
const projectFiles = globWithWorkspaceContext( const projectFiles = await globWithWorkspaceContext(
context.workspaceRoot, context.workspaceRoot,
[ [
'project.json', 'project.json',
@ -77,7 +77,7 @@ export const createNodes: CreateNodes<EslintPluginOptions> = [
const nestedProjectRootPatterns = excludePatterns.slice(index + 1); const nestedProjectRootPatterns = excludePatterns.slice(index + 1);
// Ignore project roots where the project does not contain any lintable files // Ignore project roots where the project does not contain any lintable files
const lintableFiles = globWithWorkspaceContext( const lintableFiles = await globWithWorkspaceContext(
context.workspaceRoot, context.workspaceRoot,
[join(childProjectRoot, `**/*.{${options.extensions.join(',')}}`)], [join(childProjectRoot, `**/*.{${options.extensions.join(',')}}`)],
// exclude nested eslint roots and nested project roots // exclude nested eslint roots and nested project roots

View File

@ -72,9 +72,12 @@ export const createNodes: CreateNodes<ExpoPluginOptions> = [
return {}; return {};
} }
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= buildExpoTargets(projectRoot, options, context); targetsCache[hash] ??= buildExpoTargets(projectRoot, options, context);

View File

@ -72,7 +72,7 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
); );
const targetsCache = readTargetsCache(cachePath); const targetsCache = readTargetsCache(cachePath);
populateGradleReport(context.workspaceRoot); await populateGradleReport(context.workspaceRoot);
const gradleReport = getCurrentGradleReport(); const gradleReport = getCurrentGradleReport();
try { try {
@ -93,14 +93,14 @@ export const makeCreateNodes =
gradleReport: GradleReport, gradleReport: GradleReport,
targetsCache: GradleTargets targetsCache: GradleTargets
): CreateNodesFunction => ): CreateNodesFunction =>
( async (
gradleFilePath, gradleFilePath,
options: GradlePluginOptions | undefined, options: GradlePluginOptions | undefined,
context: CreateNodesContext context: CreateNodesContext
) => { ) => {
const projectRoot = dirname(gradleFilePath); const projectRoot = dirname(gradleFilePath);
const hash = calculateHashForCreateNodes( const hash = await calculateHashForCreateNodes(
projectRoot, projectRoot,
options ?? {}, options ?? {},
context context
@ -128,14 +128,14 @@ export const makeCreateNodes =
*/ */
export const createNodes: CreateNodes<GradlePluginOptions> = [ export const createNodes: CreateNodes<GradlePluginOptions> = [
gradleConfigGlob, gradleConfigGlob,
(configFile, options, context) => { async (configFile, 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.'
); );
populateGradleReport(context.workspaceRoot); await populateGradleReport(context.workspaceRoot);
const gradleReport = getCurrentGradleReport(); const gradleReport = getCurrentGradleReport();
const internalCreateNodes = makeCreateNodes(gradleReport, {}); const internalCreateNodes = makeCreateNodes(gradleReport, {});
return internalCreateNodes(configFile, options, context); return await internalCreateNodes(configFile, options, context);
}, },
]; ];

View File

@ -36,8 +36,10 @@ export function getCurrentGradleReport() {
return gradleReportCache; return gradleReportCache;
} }
export function populateGradleReport(workspaceRoot: string): void { export async function populateGradleReport(
const gradleConfigHash = hashWithWorkspaceContext(workspaceRoot, [ workspaceRoot: string
): Promise<void> {
const gradleConfigHash = await hashWithWorkspaceContext(workspaceRoot, [
gradleConfigGlob, gradleConfigGlob,
]); ]);
if (gradleReportCache && gradleConfigHash === gradleCurrentConfigHash) { if (gradleReportCache && gradleConfigHash === gradleCurrentConfigHash) {

View File

@ -127,7 +127,7 @@ async function createNodesInternal(
options = normalizeOptions(options); options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context); const hash = await calculateHashForCreateNodes(projectRoot, options, context);
targetsCache[hash] ??= await buildJestTargets( targetsCache[hash] ??= await buildJestTargets(
configFilePath, configFilePath,
projectRoot, projectRoot,

View File

@ -76,7 +76,7 @@ export const PLUGIN_NAME = '@nx/js/typescript';
export const createNodes: CreateNodes<TscPluginOptions> = [ export const createNodes: CreateNodes<TscPluginOptions> = [
'**/tsconfig*.json', '**/tsconfig*.json',
(configFilePath, options, context) => { async (configFilePath, options, context) => {
const pluginOptions = normalizePluginOptions(options); const pluginOptions = normalizePluginOptions(options);
const projectRoot = dirname(configFilePath); const projectRoot = dirname(configFilePath);
const fullConfigPath = joinPathFragments( const fullConfigPath = joinPathFragments(
@ -101,7 +101,7 @@ export const createNodes: CreateNodes<TscPluginOptions> = [
return {}; return {};
} }
const nodeHash = calculateHashForCreateNodes( const nodeHash = await calculateHashForCreateNodes(
projectRoot, projectRoot,
pluginOptions, pluginOptions,
context, context,

View File

@ -7,4 +7,5 @@ export default {
globals: {}, globals: {},
displayName: 'nest', displayName: 'nest',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/test-setup.ts'],
}; };

View File

@ -0,0 +1,12 @@
// If a test uses a util from devkit, but that util
// lives in the Nx package and creates the project graph,
// we need to mock the resolved value inside the Nx package
jest
.spyOn(
require('nx/src/project-graph/project-graph'),
'createProjectGraphAsync'
)
.mockResolvedValue({
nodes: {},
dependencies: {},
});

View File

@ -11,7 +11,8 @@
"**/*.test.ts", "**/*.test.ts",
"**/*_spec.ts", "**/*_spec.ts",
"**/*_test.ts", "**/*_test.ts",
"jest.config.ts" "jest.config.ts",
"test-setup.ts"
], ],
"include": ["**/*.ts"] "include": ["**/*.ts"]
} }

View File

@ -17,6 +17,7 @@
"**/*.spec.jsx", "**/*.spec.jsx",
"**/*.test.jsx", "**/*.test.jsx",
"**/*.d.ts", "**/*.d.ts",
"jest.config.ts" "jest.config.ts",
"test-setup.ts"
] ]
} }

View File

@ -63,9 +63,12 @@ export const createNodes: CreateNodes<NextPluginOptions> = [
} }
options = normalizeOptions(options); options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= await buildNextTargets( targetsCache[hash] ??= await buildNextTargets(
configFilePath, configFilePath,

View File

@ -62,9 +62,12 @@ export const createNodes: CreateNodes<NuxtPluginOptions> = [
options = normalizeOptions(options); options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= await buildNuxtTargets( targetsCache[hash] ??= await buildNuxtTargets(
configFilePath, configFilePath,
projectRoot, projectRoot,

View File

@ -5,6 +5,14 @@ import {
import { createTreeWithEmptyWorkspace } from '../generators/testing-utils/create-tree-with-empty-workspace'; import { createTreeWithEmptyWorkspace } from '../generators/testing-utils/create-tree-with-empty-workspace';
import { addProjectConfiguration } from '../generators/utils/project-configuration'; import { addProjectConfiguration } from '../generators/utils/project-configuration';
jest.mock('../project-graph/project-graph', () => ({
...jest.requireActual('../project-graph/project-graph'),
createProjectGraphAsync: () => ({
nodes: {},
externalNodes: {},
}),
}));
describe('ngcli-adapter', () => { describe('ngcli-adapter', () => {
it('arrayBufferToString should support large buffers', () => { it('arrayBufferToString should support large buffers', () => {
const largeString = 'a'.repeat(1000000); const largeString = 'a'.repeat(1000000);

View File

@ -290,7 +290,7 @@ async function addBundler(options: NormalizedOptions) {
options.isStandalone, options.isStandalone,
options.appIsJs options.appIsJs
); );
renameJsToJsx(options.reactAppName, options.isStandalone); await renameJsToJsx(options.reactAppName, options.isStandalone);
} else { } else {
output.log({ title: '🧑‍🔧 Setting up craco + Webpack' }); output.log({ title: '🧑‍🔧 Setting up craco + Webpack' });
const { addCracoCommandsToPackageScripts } = await import( const { addCracoCommandsToPackageScripts } = await import(

View File

@ -3,8 +3,8 @@ import { globWithWorkspaceContext } from '../../../../utils/workspace-context';
import { fileExists } from '../../../../utils/fileutils'; import { fileExists } from '../../../../utils/fileutils';
// Vite cannot process JSX like <div> or <Header> unless the file is named .jsx or .tsx // Vite cannot process JSX like <div> or <Header> unless the file is named .jsx or .tsx
export function renameJsToJsx(appName: string, isStandalone: boolean) { export async function renameJsToJsx(appName: string, isStandalone: boolean) {
const files = globWithWorkspaceContext(process.cwd(), [ const files = await globWithWorkspaceContext(process.cwd(), [
isStandalone ? 'src/**/*.js' : `apps/${appName}/src/**/*.js`, isStandalone ? 'src/**/*.js' : `apps/${appName}/src/**/*.js`,
]); ]);

View File

@ -168,7 +168,7 @@ async function detectPlugins(): Promise<{
updatePackageScripts: boolean; updatePackageScripts: boolean;
}> { }> {
let files = ['package.json'].concat( let files = ['package.json'].concat(
globWithWorkspaceContext(process.cwd(), ['**/*/package.json']) await globWithWorkspaceContext(process.cwd(), ['**/*/package.json'])
); );
const detectedPlugins = new Set<string>(); const detectedPlugins = new Set<string>();

View File

@ -42,7 +42,7 @@ describe('Workspaces', () => {
readNxJson(fs.tempDir).plugins, readNxJson(fs.tempDir).plugins,
fs.tempDir fs.tempDir
); );
const res = retrieveProjectConfigurations( const res = await retrieveProjectConfigurations(
plugins, plugins,
fs.tempDir, fs.tempDir,
readNxJson(fs.tempDir) readNxJson(fs.tempDir)

View File

@ -4,7 +4,7 @@ import { readFileSync, statSync } from 'fs';
import { FileHandle, open } from 'fs/promises'; import { FileHandle, open } from 'fs/promises';
import { ensureDirSync, ensureFileSync } from 'fs-extra'; import { ensureDirSync, ensureFileSync } from 'fs-extra';
import { connect } from 'net'; import { connect } from 'net';
import { join } from 'path'; import { extname, join } from 'path';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { getFullOsSocketPath, killSocketOrPath } from '../socket-utils'; import { getFullOsSocketPath, killSocketOrPath } from '../socket-utils';
@ -30,6 +30,21 @@ import {
ProjectGraphError, ProjectGraphError,
} from '../../project-graph/error-types'; } from '../../project-graph/error-types';
import { loadRootEnvFiles } from '../../utils/dotenv'; import { loadRootEnvFiles } from '../../utils/dotenv';
import { HandleGlobMessage } from '../message-types/glob';
import {
GET_NX_WORKSPACE_FILES,
HandleNxWorkspaceFilesMessage,
} from '../message-types/get-nx-workspace-files';
import {
GET_CONTEXT_FILE_DATA,
HandleContextFileDataMessage,
} from '../message-types/get-context-file-data';
import {
GET_FILES_IN_DIRECTORY,
HandleGetFilesInDirectoryMessage,
} from '../message-types/get-files-in-directory';
import { HASH_GLOB, HandleHashGlobMessage } from '../message-types/hash-glob';
import { NxWorkspaceFiles } from '../../native';
const DAEMON_ENV_SETTINGS = { const DAEMON_ENV_SETTINGS = {
NX_PROJECT_GLOB_CACHE: 'false', NX_PROJECT_GLOB_CACHE: 'false',
@ -256,6 +271,49 @@ export class DaemonClient {
}); });
} }
glob(globs: string[], exclude?: string[]): Promise<string[]> {
const message: HandleGlobMessage = {
type: 'GLOB',
globs,
exclude,
};
return this.sendToDaemonViaQueue(message);
}
getWorkspaceContextFileData(): Promise<FileData[]> {
const message: HandleContextFileDataMessage = {
type: GET_CONTEXT_FILE_DATA,
};
return this.sendToDaemonViaQueue(message);
}
getWorkspaceFiles(
projectRootMap: Record<string, string>
): Promise<NxWorkspaceFiles> {
const message: HandleNxWorkspaceFilesMessage = {
type: GET_NX_WORKSPACE_FILES,
projectRootMap,
};
return this.sendToDaemonViaQueue(message);
}
getFilesInDirectory(dir: string): Promise<string[]> {
const message: HandleGetFilesInDirectoryMessage = {
type: GET_FILES_IN_DIRECTORY,
dir,
};
return this.sendToDaemonViaQueue(message);
}
hashGlob(globs: string[], exclude?: string[]): Promise<string> {
const message: HandleHashGlobMessage = {
type: HASH_GLOB,
globs,
exclude,
};
return this.sendToDaemonViaQueue(message);
}
async isServerAvailable(): Promise<boolean> { async isServerAvailable(): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
try { try {
@ -414,14 +472,17 @@ export class DaemonClient {
const backgroundProcess = spawn( const backgroundProcess = spawn(
process.execPath, process.execPath,
[join(__dirname, '../server/start.js')], [join(__dirname, `../server/start.js`)],
{ {
cwd: workspaceRoot, cwd: workspaceRoot,
stdio: ['ignore', this._out.fd, this._err.fd], stdio: ['ignore', this._out.fd, this._err.fd],
detached: true, detached: true,
windowsHide: true, windowsHide: true,
shell: false, shell: false,
env: { ...process.env, ...DAEMON_ENV_SETTINGS }, env: {
...process.env,
...DAEMON_ENV_SETTINGS,
},
} }
); );
backgroundProcess.unref(); backgroundProcess.unref();

View File

@ -0,0 +1,3 @@
export function isOnDaemon() {
return !!global.NX_DAEMON;
}

View File

@ -0,0 +1,16 @@
export const GET_CONTEXT_FILE_DATA = 'GET_CONTEXT_FILE_DATA' as const;
export type HandleContextFileDataMessage = {
type: typeof GET_CONTEXT_FILE_DATA;
};
export function isHandleContextFileDataMessage(
message: unknown
): message is HandleContextFileDataMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === GET_CONTEXT_FILE_DATA
);
}

View File

@ -0,0 +1,17 @@
export const GET_FILES_IN_DIRECTORY = 'GET_FILES_IN_DIRECTORY' as const;
export type HandleGetFilesInDirectoryMessage = {
type: typeof GET_FILES_IN_DIRECTORY;
dir: string;
};
export function isHandleGetFilesInDirectoryMessage(
message: unknown
): message is HandleGetFilesInDirectoryMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === GET_FILES_IN_DIRECTORY
);
}

View File

@ -0,0 +1,17 @@
export const GET_NX_WORKSPACE_FILES = 'GET_NX_WORKSPACE_FILES' as const;
export type HandleNxWorkspaceFilesMessage = {
type: typeof GET_NX_WORKSPACE_FILES;
projectRootMap: Record<string, string>;
};
export function isHandleNxWorkspaceFilesMessage(
message: unknown
): message is HandleNxWorkspaceFilesMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === GET_NX_WORKSPACE_FILES
);
}

View File

@ -0,0 +1,18 @@
export const GLOB = 'GLOB' as const;
export type HandleGlobMessage = {
type: typeof GLOB;
globs: string[];
exclude?: string[];
};
export function isHandleGlobMessage(
message: unknown
): message is HandleGlobMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === GLOB
);
}

View File

@ -0,0 +1,18 @@
export const HASH_GLOB = 'HASH_GLOB' as const;
export type HandleHashGlobMessage = {
type: typeof HASH_GLOB;
globs: string[];
exclude?: string[];
};
export function isHandleHashGlobMessage(
message: unknown
): message is HandleHashGlobMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === HASH_GLOB
);
}

View File

@ -0,0 +1,18 @@
export const GLOB = 'GLOB' as const;
export type HandleUpdateContextMessage = {
type: typeof GLOB;
updatedFiles: string[];
deletedFiles: string[];
};
export function isHandleUpdateContextMessage(
message: unknown
): message is HandleUpdateContextMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === GLOB
);
}

View File

@ -0,0 +1,11 @@
import { getAllFileDataInContext } from '../../utils/workspace-context';
import { workspaceRoot } from '../../utils/workspace-root';
import { HandlerResult } from './server';
export async function handleContextFileData(): Promise<HandlerResult> {
const files = await getAllFileDataInContext(workspaceRoot);
return {
response: JSON.stringify(files),
description: 'handleContextFileData',
};
}

View File

@ -0,0 +1,13 @@
import { getFilesInDirectoryUsingContext } from '../../utils/workspace-context';
import { workspaceRoot } from '../../utils/workspace-root';
import { HandlerResult } from './server';
export async function handleGetFilesInDirectory(
dir: string
): Promise<HandlerResult> {
const files = await getFilesInDirectoryUsingContext(workspaceRoot, dir);
return {
response: JSON.stringify(files),
description: 'handleNxWorkspaceFiles',
};
}

View File

@ -0,0 +1,14 @@
import { workspaceRoot } from '../../utils/workspace-root';
import { globWithWorkspaceContext } from '../../utils/workspace-context';
import { HandlerResult } from './server';
export async function handleGlob(
globs: string[],
exclude?: string[]
): Promise<HandlerResult> {
const files = await globWithWorkspaceContext(workspaceRoot, globs, exclude);
return {
response: JSON.stringify(files),
description: 'handleGlob',
};
}

View File

@ -0,0 +1,14 @@
import { workspaceRoot } from '../../utils/workspace-root';
import { hashWithWorkspaceContext } from '../../utils/workspace-context';
import { HandlerResult } from './server';
export async function handleHashGlob(
globs: string[],
exclude?: string[]
): Promise<HandlerResult> {
const files = await hashWithWorkspaceContext(workspaceRoot, globs, exclude);
return {
response: JSON.stringify(files),
description: 'handleHashGlob',
};
}

View File

@ -0,0 +1,16 @@
import { getNxWorkspaceFilesFromContext } from '../../utils/workspace-context';
import { workspaceRoot } from '../../utils/workspace-root';
import { HandlerResult } from './server';
export async function handleNxWorkspaceFiles(
projectRootMap: Record<string, string>
): Promise<HandlerResult> {
const files = await getNxWorkspaceFilesFromContext(
workspaceRoot,
projectRootMap
);
return {
response: JSON.stringify(files),
description: 'handleNxWorkspaceFiles',
};
}

View File

@ -1,10 +0,0 @@
import { getAllFileDataInContext } from '../../utils/workspace-context';
import { workspaceRoot } from '../../utils/workspace-root';
export async function handleRequestFileData() {
const response = JSON.stringify(getAllFileDataInContext(workspaceRoot));
return {
response,
description: 'handleRequestFileData',
};
}

View File

@ -26,7 +26,6 @@ import {
handleRecordOutputsHash, handleRecordOutputsHash,
} from './handle-outputs-tracking'; } from './handle-outputs-tracking';
import { handleProcessInBackground } from './handle-process-in-background'; import { handleProcessInBackground } from './handle-process-in-background';
import { handleRequestFileData } from './handle-request-file-data';
import { handleRequestProjectGraph } from './handle-request-project-graph'; import { handleRequestProjectGraph } from './handle-request-project-graph';
import { handleRequestShutdown } from './handle-request-shutdown'; import { handleRequestShutdown } from './handle-request-shutdown';
import { serverLogger } from './logger'; import { serverLogger } from './logger';
@ -52,11 +51,32 @@ import {
watchOutputFiles, watchOutputFiles,
watchWorkspace, watchWorkspace,
} from './watcher'; } from './watcher';
import { handleGlob } from './handle-glob';
import { GLOB, isHandleGlobMessage } from '../message-types/glob';
import {
GET_NX_WORKSPACE_FILES,
isHandleNxWorkspaceFilesMessage,
} from '../message-types/get-nx-workspace-files';
import { handleNxWorkspaceFiles } from './handle-nx-workspace-files';
import {
GET_CONTEXT_FILE_DATA,
isHandleContextFileDataMessage,
} from '../message-types/get-context-file-data';
import { handleContextFileData } from './handle-context-file-data';
import {
GET_FILES_IN_DIRECTORY,
isHandleGetFilesInDirectoryMessage,
} from '../message-types/get-files-in-directory';
import { handleGetFilesInDirectory } from './handle-get-files-in-directory';
import { HASH_GLOB, isHandleHashGlobMessage } from '../message-types/hash-glob';
import { handleHashGlob } from './handle-hash-glob';
let performanceObserver: PerformanceObserver | undefined; let performanceObserver: PerformanceObserver | undefined;
let workspaceWatcherError: Error | undefined; let workspaceWatcherError: Error | undefined;
let outputsWatcherError: Error | undefined; let outputsWatcherError: Error | undefined;
global.NX_DAEMON = true;
export type HandlerResult = { export type HandlerResult = {
description: string; description: string;
error?: any; error?: any;
@ -111,11 +131,12 @@ async function handleMessage(socket, data: string) {
); );
} }
if (daemonIsOutdated()) { const outdated = daemonIsOutdated();
if (outdated) {
await respondWithErrorAndExit( await respondWithErrorAndExit(
socket, socket,
`Lock files changed`, `Daemon outdated`,
new Error('LOCK-FILES-CHANGED') new Error(outdated)
); );
} }
@ -143,10 +164,6 @@ async function handleMessage(socket, data: string) {
); );
} else if (payload.type === 'HASH_TASKS') { } else if (payload.type === 'HASH_TASKS') {
await handleResult(socket, 'HASH_TASKS', () => handleHashTasks(payload)); await handleResult(socket, 'HASH_TASKS', () => handleHashTasks(payload));
} else if (payload.type === 'REQUEST_FILE_DATA') {
await handleResult(socket, 'REQUEST_FILE_DATA', () =>
handleRequestFileData()
);
} else if (payload.type === 'PROCESS_IN_BACKGROUND') { } else if (payload.type === 'PROCESS_IN_BACKGROUND') {
await handleResult(socket, 'PROCESS_IN_BACKGROUND', () => await handleResult(socket, 'PROCESS_IN_BACKGROUND', () =>
handleProcessInBackground(payload) handleProcessInBackground(payload)
@ -165,6 +182,26 @@ async function handleMessage(socket, data: string) {
); );
} else if (payload.type === 'REGISTER_FILE_WATCHER') { } else if (payload.type === 'REGISTER_FILE_WATCHER') {
registeredFileWatcherSockets.push({ socket, config: payload.config }); registeredFileWatcherSockets.push({ socket, config: payload.config });
} else if (isHandleGlobMessage(payload)) {
await handleResult(socket, GLOB, () =>
handleGlob(payload.globs, payload.exclude)
);
} else if (isHandleNxWorkspaceFilesMessage(payload)) {
await handleResult(socket, GET_NX_WORKSPACE_FILES, () =>
handleNxWorkspaceFiles(payload.projectRootMap)
);
} else if (isHandleGetFilesInDirectoryMessage(payload)) {
await handleResult(socket, GET_FILES_IN_DIRECTORY, () =>
handleGetFilesInDirectory(payload.dir)
);
} else if (isHandleContextFileDataMessage(payload)) {
await handleResult(socket, GET_CONTEXT_FILE_DATA, () =>
handleContextFileData()
);
} else if (isHandleHashGlobMessage(payload)) {
await handleResult(socket, HASH_GLOB, () =>
handleHashGlob(payload.globs, payload.exclude)
);
} else { } else {
await respondWithErrorAndExit( await respondWithErrorAndExit(
socket, socket,
@ -233,8 +270,13 @@ function registerProcessTerminationListeners() {
let existingLockHash: string | undefined; let existingLockHash: string | undefined;
function daemonIsOutdated(): boolean { function daemonIsOutdated(): string | null {
return nxVersionChanged() || lockFileHashChanged(); if (nxVersionChanged()) {
return 'NX_VERSION_CHANGED';
} else if (lockFileHashChanged()) {
return 'LOCK_FILES_CHANGED';
}
return null;
} }
function nxVersionChanged(): boolean { function nxVersionChanged(): boolean {
@ -291,10 +333,11 @@ const handleWorkspaceChanges: FileWatcherCallback = async (
try { try {
resetInactivityTimeout(handleInactivityTimeout); resetInactivityTimeout(handleInactivityTimeout);
if (daemonIsOutdated()) { const outdatedReason = daemonIsOutdated();
if (outdatedReason) {
await handleServerProcessTermination({ await handleServerProcessTermination({
server, server,
reason: 'Lock file changed', reason: outdatedReason,
}); });
return; return;
} }

View File

@ -132,7 +132,7 @@ export {
/** /**
* @category Generators * @category Generators
*/ */
export { glob } from './generators/utils/glob'; export { glob, globAsync } from './generators/utils/glob';
/** /**
* @category Generators * @category Generators

View File

@ -1,7 +1,28 @@
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { Tree } from '../tree'; import { Tree } from '../tree';
import { combineGlobPatterns } from '../../utils/globs'; import { combineGlobPatterns } from '../../utils/globs';
import { globWithWorkspaceContext } from '../../utils/workspace-context'; import {
globWithWorkspaceContext,
globWithWorkspaceContextSync,
} from '../../utils/workspace-context';
/**
* Performs a tree-aware glob search on the files in a workspace. Able to find newly
* created files and hides deleted files before the updates are committed to disk.
* Paths should be unix-style with forward slashes.
*
* @param tree The file system tree
* @param patterns A list of glob patterns
* @returns Normalized paths in the workspace that match the provided glob patterns.
* @deprecated Use {@link globAsync} instead.
*/
export function glob(tree: Tree, patterns: string[]): string[] {
return combineGlobResultsWithTree(
tree,
patterns,
globWithWorkspaceContextSync(tree.root, patterns)
);
}
/** /**
* Performs a tree-aware glob search on the files in a workspace. Able to find newly * Performs a tree-aware glob search on the files in a workspace. Able to find newly
@ -12,8 +33,23 @@ import { globWithWorkspaceContext } from '../../utils/workspace-context';
* @param patterns A list of glob patterns * @param patterns A list of glob patterns
* @returns Normalized paths in the workspace that match the provided glob patterns. * @returns Normalized paths in the workspace that match the provided glob patterns.
*/ */
export function glob(tree: Tree, patterns: string[]): string[] { export async function globAsync(
const matches = new Set(globWithWorkspaceContext(tree.root, patterns)); tree: Tree,
patterns: string[]
): Promise<string[]> {
return combineGlobResultsWithTree(
tree,
patterns,
await globWithWorkspaceContext(tree.root, patterns)
);
}
function combineGlobResultsWithTree(
tree: Tree,
patterns: string[],
results: string[]
) {
const matches = new Set(results);
const combinedGlob = combineGlobPatterns(patterns); const combinedGlob = combineGlobPatterns(patterns);
const matcher = minimatch.makeRe(combinedGlob); const matcher = minimatch.makeRe(combinedGlob);

View File

@ -4,12 +4,8 @@ import { basename, join, relative } from 'path';
import { import {
buildProjectConfigurationFromPackageJson, buildProjectConfigurationFromPackageJson,
getGlobPatternsFromPackageManagerWorkspaces, getGlobPatternsFromPackageManagerWorkspaces,
createNodes as packageJsonWorkspacesCreateNodes,
} from '../../plugins/package-json-workspaces'; } from '../../plugins/package-json-workspaces';
import { import { buildProjectFromProjectJson } from '../../plugins/project-json/build-nodes/project-json';
buildProjectFromProjectJson,
ProjectJsonProjectsPlugin,
} from '../../plugins/project-json/build-nodes/project-json';
import { renamePropertyWithStableKeys } from '../../adapter/angular-json'; import { renamePropertyWithStableKeys } from '../../adapter/angular-json';
import { import {
ProjectConfiguration, ProjectConfiguration,
@ -19,8 +15,7 @@ import {
mergeProjectConfigurationIntoRootMap, mergeProjectConfigurationIntoRootMap,
readProjectConfigurationsFromRootMap, readProjectConfigurationsFromRootMap,
} from '../../project-graph/utils/project-configuration-utils'; } from '../../project-graph/utils/project-configuration-utils';
import { configurationGlobs } from '../../project-graph/utils/retrieve-workspace-files'; import { globWithWorkspaceContextSync } from '../../utils/workspace-context';
import { globWithWorkspaceContext } from '../../utils/workspace-context';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { PackageJson } from '../../utils/package-json'; import { PackageJson } from '../../utils/package-json';
import { joinPathFragments, normalizePath } from '../../utils/path'; import { joinPathFragments, normalizePath } from '../../utils/path';
@ -28,7 +23,6 @@ import { readJson, writeJson } from './json';
import { readNxJson } from './nx-json'; import { readNxJson } from './nx-json';
import type { Tree } from '../tree'; import type { Tree } from '../tree';
import { NxPlugin } from '../../project-graph/plugins';
export { readNxJson, updateNxJson } from './nx-json'; export { readNxJson, updateNxJson } from './nx-json';
@ -200,7 +194,7 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
readJson(tree, p, { expectComments: true }) readJson(tree, p, { expectComments: true })
), ),
]; ];
const globbedFiles = globWithWorkspaceContext(tree.root, patterns); const globbedFiles = globWithWorkspaceContextSync(tree.root, patterns);
const createdFiles = findCreatedProjectFiles(tree, patterns); const createdFiles = findCreatedProjectFiles(tree, patterns);
const deletedFiles = findDeletedProjectFiles(tree, patterns); const deletedFiles = findDeletedProjectFiles(tree, patterns);
const projectFiles = [...globbedFiles, ...createdFiles].filter( const projectFiles = [...globbedFiles, ...createdFiles].filter(

View File

@ -89,7 +89,7 @@ describe('explicit package json dependencies', () => {
const fileMap = createFileMap( const fileMap = createFileMap(
projectsConfigurations as any, projectsConfigurations as any,
getAllFileDataInContext(tempFs.tempDir) await getAllFileDataInContext(tempFs.tempDir)
).fileMap; ).fileMap;
const builder = new ProjectGraphBuilder(undefined, fileMap.projectFileMap); const builder = new ProjectGraphBuilder(undefined, fileMap.projectFileMap);

View File

@ -39,12 +39,7 @@ export async function createFileMapUsingProjectGraph(
): Promise<WorkspaceFileMap> { ): Promise<WorkspaceFileMap> {
const configs = readProjectsConfigurationFromProjectGraph(graph); const configs = readProjectsConfigurationFromProjectGraph(graph);
let files: FileData[]; let files: FileData[] = await getAllFileDataInContext(workspaceRoot);
if (daemonClient.enabled()) {
files = await daemonClient.getAllFileData();
} else {
files = getAllFileDataInContext(workspaceRoot);
}
return createFileMap(configs, files); return createFileMap(configs, files);
} }

View File

@ -1,6 +1,6 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { extname, join, relative, sep } from 'path'; import { basename, extname, join, relative, sep } from 'path';
import { readNxJson } from '../config/configuration'; import { readNxJson } from '../config/configuration';
import { FileData } from '../config/project-graph'; import { FileData } from '../config/project-graph';
import { import {
@ -17,16 +17,18 @@ import {
} from './project-graph'; } from './project-graph';
import { toOldFormat } from '../adapter/angular-json'; import { toOldFormat } from '../adapter/angular-json';
import { getIgnoreObject } from '../utils/ignore'; import { getIgnoreObject } from '../utils/ignore';
import { retrieveProjectConfigurationPaths } from './utils/retrieve-workspace-files';
import { import {
mergeProjectConfigurationIntoRootMap, mergeProjectConfigurationIntoRootMap,
readProjectConfigurationsFromRootMap, readProjectConfigurationsFromRootMap,
} from './utils/project-configuration-utils'; } from './utils/project-configuration-utils';
import { NxJsonConfiguration } from '../config/nx-json'; import {
import { getDefaultPluginsSync } from '../utils/nx-plugin.deprecated'; buildProjectConfigurationFromPackageJson,
import { minimatch } from 'minimatch'; getGlobPatternsFromPackageManagerWorkspaces,
import { CreateNodesResult } from '../devkit-exports'; } from '../plugins/package-json-workspaces';
import { PackageJsonProjectsNextToProjectJsonPlugin } from '../plugins/project-json/build-nodes/package-json-next-to-project-json'; import { globWithWorkspaceContextSync } from '../utils/workspace-context';
import { buildProjectFromProjectJson } from '../plugins/project-json/build-nodes/project-json';
import { PackageJson } from '../utils/package-json';
import { NxJsonConfiguration } from '../devkit-exports';
export interface Change { export interface Change {
type: string; type: string;
@ -151,7 +153,7 @@ export function readWorkspaceConfig(opts: {
} catch { } catch {
configuration = { configuration = {
version: 2, version: 2,
projects: getProjectsSyncNoInference(root, nxJson).projects, projects: getProjectsSync(root, nxJson),
}; };
} }
if (opts.format === 'angularCli') { if (opts.format === 'angularCli') {
@ -179,50 +181,59 @@ export { FileData };
/** /**
* TODO(v20): Remove this function. * TODO(v20): Remove this function.
*/ */
function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) { function getProjectsSync(
const allConfigFiles = retrieveProjectConfigurationPaths( root: string,
root, nxJson: NxJsonConfiguration
getDefaultPluginsSync(root) ): {
); [name: string]: ProjectConfiguration;
const plugins = [ } {
PackageJsonProjectsNextToProjectJsonPlugin, /**
...getDefaultPluginsSync(root), * We can't update projects that come from plugins anyways, so we are going
* to ignore them for now. Plugins should add their own add/create/update methods
* if they would like to use devkit to update inferred projects.
*/
const patterns = [
'**/project.json',
'project.json',
...getGlobPatternsFromPackageManagerWorkspaces(root, readJsonFile),
]; ];
const projectFiles = globWithWorkspaceContextSync(root, patterns);
const projectRootMap: Record<string, ProjectConfiguration> = {}; const rootMap: Record<string, ProjectConfiguration> = {};
for (const projectFile of projectFiles) {
// We iterate over plugins first - this ensures that plugins specified first take precedence. if (basename(projectFile) === 'project.json') {
for (const plugin of plugins) { const json = readJsonFile(projectFile);
const [pattern, createNodes] = plugin.createNodes ?? []; const config = buildProjectFromProjectJson(json, projectFile);
if (!pattern) { mergeProjectConfigurationIntoRootMap(
continue; rootMap,
} config,
const matchingConfigFiles = allConfigFiles.filter((file) => undefined,
minimatch(file, pattern, { dot: true }) undefined,
); true
for (const file of matchingConfigFiles) { );
if (minimatch(file, pattern, { dot: true })) { } else if (basename(projectFile) === 'package.json') {
let r = createNodes( const packageJson = readJsonFile<PackageJson>(projectFile);
file, const config = buildProjectConfigurationFromPackageJson(
{}, packageJson,
projectFile,
nxJson
);
if (!rootMap[config.root]) {
mergeProjectConfigurationIntoRootMap(
rootMap,
// Inferred targets, tags, etc don't show up when running generators
// This is to help avoid running into issues when trying to update the workspace
{ {
nxJsonConfiguration: nxJson, name: config.name,
workspaceRoot: root, root: config.root,
configFiles: matchingConfigFiles, },
} undefined,
) as CreateNodesResult; undefined,
for (const node in r.projects) { true
const project = { );
root: node,
...r.projects[node],
};
mergeProjectConfigurationIntoRootMap(projectRootMap, project);
}
} }
} }
} }
return { return readProjectConfigurationsFromRootMap(rootMap);
projects: readProjectConfigurationsFromRootMap(projectRootMap),
};
} }

View File

@ -28,17 +28,23 @@ export function loadRemoteNxPlugin(
// but its typescript. // but its typescript.
const isWorkerTypescript = path.extname(__filename) === '.ts'; const isWorkerTypescript = path.extname(__filename) === '.ts';
const workerPath = path.join(__dirname, 'plugin-worker'); const workerPath = path.join(__dirname, 'plugin-worker');
const env: Record<string, string> = {
...process.env,
...(isWorkerTypescript
? {
// Ensures that the worker uses the same tsconfig as the main process
TS_NODE_PROJECT: path.join(
__dirname,
'../../../../tsconfig.lib.json'
),
}
: {}),
};
const worker = fork(workerPath, [], { const worker = fork(workerPath, [], {
stdio: ['ignore', 'inherit', 'inherit', 'ipc'], stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
env: { env,
...process.env,
...(isWorkerTypescript
? {
// Ensures that the worker uses the same tsconfig as the main process
TS_NODE_PROJECT: path.join(__dirname, '../../../tsconfig.lib.json'),
}
: {}),
},
execArgv: [ execArgv: [
...process.execArgv, ...process.execArgv,
// If the worker is typescript, we need to register ts-node // If the worker is typescript, we need to register ts-node

View File

@ -25,7 +25,7 @@ describe('retrieveProjectConfigurationPaths', () => {
}) })
); );
const configPaths = retrieveProjectConfigurationPaths(fs.tempDir, [ const configPaths = await retrieveProjectConfigurationPaths(fs.tempDir, [
{ {
createNodes: [ createNodes: [
'{project.json,**/project.json}', '{project.json,**/project.json}',

View File

@ -38,7 +38,7 @@ export async function retrieveWorkspaceFiles(
performance.mark('get-workspace-files:start'); performance.mark('get-workspace-files:start');
const { projectFileMap, globalFiles, externalReferences } = const { projectFileMap, globalFiles, externalReferences } =
getNxWorkspaceFilesFromContext(workspaceRoot, projectRootMap); await getNxWorkspaceFilesFromContext(workspaceRoot, projectRootMap);
performance.mark('get-workspace-files:end'); performance.mark('get-workspace-files:end');
performance.measure( performance.measure(
'get-workspace-files', 'get-workspace-files',
@ -60,13 +60,16 @@ export async function retrieveWorkspaceFiles(
* Walk through the workspace and return `ProjectConfigurations`. Only use this if the projectFileMap is not needed. * Walk through the workspace and return `ProjectConfigurations`. Only use this if the projectFileMap is not needed.
*/ */
export function retrieveProjectConfigurations( export async function retrieveProjectConfigurations(
plugins: LoadedNxPlugin[], plugins: LoadedNxPlugin[],
workspaceRoot: string, workspaceRoot: string,
nxJson: NxJsonConfiguration nxJson: NxJsonConfiguration
): Promise<ConfigurationResult> { ): Promise<ConfigurationResult> {
const globPatterns = configurationGlobs(plugins); const globPatterns = configurationGlobs(plugins);
const workspaceFiles = globWithWorkspaceContext(workspaceRoot, globPatterns); const workspaceFiles = await globWithWorkspaceContext(
workspaceRoot,
globPatterns
);
return createProjectConfigurations( return createProjectConfigurations(
workspaceRoot, workspaceRoot,
@ -98,7 +101,11 @@ export async function retrieveProjectConfigurationsWithAngularProjects(
workspaceRoot workspaceRoot
); );
const res = retrieveProjectConfigurations(plugins, workspaceRoot, nxJson); const res = await retrieveProjectConfigurations(
plugins,
workspaceRoot,
nxJson
);
cleanup(); cleanup();
return res; return res;
} }
@ -106,7 +113,7 @@ export async function retrieveProjectConfigurationsWithAngularProjects(
export function retrieveProjectConfigurationPaths( export function retrieveProjectConfigurationPaths(
root: string, root: string,
plugins: Array<{ createNodes?: readonly [string, ...unknown[]] } & unknown> plugins: Array<{ createNodes?: readonly [string, ...unknown[]] } & unknown>
): string[] { ): Promise<string[]> {
const projectGlobPatterns = configurationGlobs(plugins); const projectGlobPatterns = configurationGlobs(plugins);
return globWithWorkspaceContext(root, projectGlobPatterns); return globWithWorkspaceContext(root, projectGlobPatterns);
} }
@ -122,7 +129,10 @@ export async function retrieveProjectConfigurationsWithoutPluginInference(
): Promise<Record<string, ProjectConfiguration>> { ): Promise<Record<string, ProjectConfiguration>> {
const nxJson = readNxJson(root); const nxJson = readNxJson(root);
const [plugins, cleanup] = await loadNxPlugins([]); // only load default plugins const [plugins, cleanup] = await loadNxPlugins([]); // only load default plugins
const projectGlobPatterns = retrieveProjectConfigurationPaths(root, plugins); const projectGlobPatterns = await retrieveProjectConfigurationPaths(
root,
plugins
);
const cacheKey = root + ',' + projectGlobPatterns.join(','); const cacheKey = root + ',' + projectGlobPatterns.join(',');
if (projectsWithoutPluginCache.has(cacheKey)) { if (projectsWithoutPluginCache.has(cacheKey)) {
@ -130,7 +140,7 @@ export async function retrieveProjectConfigurationsWithoutPluginInference(
} }
const projectFiles = const projectFiles =
globWithWorkspaceContext(root, projectGlobPatterns) ?? []; (await globWithWorkspaceContext(root, projectGlobPatterns)) ?? [];
const { projects } = await createProjectConfigurations( const { projects } = await createProjectConfigurations(
root, root,
nxJson, nxJson,

View File

@ -1,12 +1,7 @@
import { FileData } from '../config/project-graph'; import { FileData } from '../config/project-graph';
import { daemonClient } from '../daemon/client/client';
import { getAllFileDataInContext } from './workspace-context'; import { getAllFileDataInContext } from './workspace-context';
import { workspaceRoot } from './workspace-root'; import { workspaceRoot } from './workspace-root';
export function allFileData(): Promise<FileData[]> { export function allFileData(): Promise<FileData[]> {
if (daemonClient.enabled()) { return getAllFileDataInContext(workspaceRoot);
return daemonClient.getAllFileData();
} else {
return Promise.resolve(getAllFileDataInContext(workspaceRoot));
}
} }

View File

@ -1,6 +1,8 @@
import type { NxWorkspaceFilesExternals, WorkspaceContext } from '../native'; import type { NxWorkspaceFilesExternals, WorkspaceContext } from '../native';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { cacheDirectoryForWorkspace } from './cache-directory'; import { cacheDirectoryForWorkspace } from './cache-directory';
import { isOnDaemon } from '../daemon/is-on-daemon';
import { daemonClient } from '../daemon/client/client';
let workspaceContext: WorkspaceContext | undefined; let workspaceContext: WorkspaceContext | undefined;
@ -20,15 +22,25 @@ export function setupWorkspaceContext(workspaceRoot: string) {
); );
} }
export function getNxWorkspaceFilesFromContext( export async function getNxWorkspaceFilesFromContext(
workspaceRoot: string, workspaceRoot: string,
projectRootMap: Record<string, string> projectRootMap: Record<string, string>
) { ) {
ensureContextAvailable(workspaceRoot); if (isOnDaemon() || !daemonClient.enabled()) {
return workspaceContext.getWorkspaceFiles(projectRootMap); ensureContextAvailable(workspaceRoot);
return workspaceContext.getWorkspaceFiles(projectRootMap);
}
return daemonClient.getWorkspaceFiles(projectRootMap);
} }
export function globWithWorkspaceContext( /**
* Sync method to get files matching globs from workspace context.
* NOTE: This method will create the workspace context if it doesn't exist.
* It should only be used within Nx internal in code paths that **must** be sync.
* If used in an isolated plugin thread this will cause the workspace context
* to be recreated which is slow.
*/
export function globWithWorkspaceContextSync(
workspaceRoot: string, workspaceRoot: string,
globs: string[], globs: string[],
exclude?: string[] exclude?: string[]
@ -37,13 +49,29 @@ export function globWithWorkspaceContext(
return workspaceContext.glob(globs, exclude); return workspaceContext.glob(globs, exclude);
} }
export function hashWithWorkspaceContext( export async function globWithWorkspaceContext(
workspaceRoot: string, workspaceRoot: string,
globs: string[], globs: string[],
exclude?: string[] exclude?: string[]
) { ) {
ensureContextAvailable(workspaceRoot); if (isOnDaemon() || !daemonClient.enabled()) {
return workspaceContext.hashFilesMatchingGlob(globs, exclude); ensureContextAvailable(workspaceRoot);
return workspaceContext.glob(globs, exclude);
} else {
return daemonClient.glob(globs, exclude);
}
}
export async function hashWithWorkspaceContext(
workspaceRoot: string,
globs: string[],
exclude?: string[]
) {
if (isOnDaemon() || !daemonClient.enabled()) {
ensureContextAvailable(workspaceRoot);
return workspaceContext.hashFilesMatchingGlob(globs, exclude);
}
return daemonClient.hashGlob(globs, exclude);
} }
export function updateFilesInContext( export function updateFilesInContext(
@ -53,17 +81,23 @@ export function updateFilesInContext(
return workspaceContext?.incrementalUpdate(updatedFiles, deletedFiles); return workspaceContext?.incrementalUpdate(updatedFiles, deletedFiles);
} }
export function getAllFileDataInContext(workspaceRoot: string) { export async function getAllFileDataInContext(workspaceRoot: string) {
ensureContextAvailable(workspaceRoot); if (isOnDaemon() || !daemonClient.enabled()) {
return workspaceContext.allFileData(); ensureContextAvailable(workspaceRoot);
return workspaceContext.allFileData();
}
return daemonClient.getWorkspaceContextFileData();
} }
export function getFilesInDirectoryUsingContext( export async function getFilesInDirectoryUsingContext(
workspaceRoot: string, workspaceRoot: string,
dir: string dir: string
) { ) {
ensureContextAvailable(workspaceRoot); if (isOnDaemon() || !daemonClient.enabled()) {
return workspaceContext.getFilesInDirectory(dir); ensureContextAvailable(workspaceRoot);
return workspaceContext.getFilesInDirectory(dir);
}
return daemonClient.getFilesInDirectory(dir);
} }
export function updateProjectFiles( export function updateProjectFiles(

View File

@ -108,9 +108,12 @@ async function createNodesInternal(
const normalizedOptions = normalizeOptions(options); const normalizedOptions = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= await buildPlaywrightTargets( targetsCache[hash] ??= await buildPlaywrightTargets(
configFilePath, configFilePath,
@ -199,7 +202,7 @@ async function buildPlaywrightTargets(
playwrightConfig.testMatch ??= '**/*.@(spec|test).?(c|m)[jt]s?(x)'; playwrightConfig.testMatch ??= '**/*.@(spec|test).?(c|m)[jt]s?(x)';
const dependsOn: TargetConfiguration['dependsOn'] = []; const dependsOn: TargetConfiguration['dependsOn'] = [];
forEachTestFile( await forEachTestFile(
(testFile) => { (testFile) => {
const relativeSpecFilePath = normalizePath( const relativeSpecFilePath = normalizePath(
relative(projectRoot, testFile) relative(projectRoot, testFile)
@ -246,7 +249,7 @@ async function buildPlaywrightTargets(
return { targets, metadata }; return { targets, metadata };
} }
function forEachTestFile( async function forEachTestFile(
cb: (path: string) => void, cb: (path: string) => void,
opts: { opts: {
context: CreateNodesContext; context: CreateNodesContext;
@ -254,7 +257,7 @@ function forEachTestFile(
config: PlaywrightTestConfig; config: PlaywrightTestConfig;
} }
) { ) {
const files = getFilesInDirectoryUsingContext( const files = await getFilesInDirectoryUsingContext(
opts.context.workspaceRoot, opts.context.workspaceRoot,
opts.path opts.path
); );

View File

@ -70,9 +70,12 @@ export const createNodes: CreateNodes<ReactNativePluginOptions> = [
return {}; return {};
} }
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= buildReactNativeTargets( targetsCache[hash] ??= buildReactNativeTargets(
projectRoot, projectRoot,

View File

@ -66,9 +66,12 @@ export const createNodes: CreateNodes<RemixPluginOptions> = [
options = normalizeOptions(options); options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= await buildRemixTargets( targetsCache[hash] ??= await buildRemixTargets(
configFilePath, configFilePath,
projectRoot, projectRoot,

View File

@ -6,10 +6,39 @@ import {
describe('createWatchPaths', () => { describe('createWatchPaths', () => {
it('should list root paths of dependencies relative to project root', async () => { it('should list root paths of dependencies relative to project root', async () => {
const testDir = joinPathFragments(workspaceRoot, 'e2e/remix'); // This test is written based on the Nx repo's project graph.
jest
.spyOn(require('@nx/devkit'), 'createProjectGraphAsync')
.mockResolvedValue({
nodes: {
parent: {
type: 'app',
name: 'parent',
data: { root: 'apps/parent' },
},
lib: {
type: 'lib',
name: 'lib',
data: { root: 'packages/lib' },
},
example: {
type: 'app',
name: 'example',
data: { root: 'examples/example' },
},
},
dependencies: {
parent: [
{ type: 'static', source: 'parent', target: 'lib' },
{ type: 'static', source: 'parent', target: 'example' },
],
example: [{ type: 'static', source: 'example', target: 'lib' }],
},
});
const testDir = joinPathFragments(workspaceRoot, 'apps/parent');
const paths = await createWatchPaths(testDir); const paths = await createWatchPaths(testDir);
expect(paths).toEqual(['../../packages', '../../graph', '../../e2e/utils']); expect(paths).toEqual(['../../packages', '../../examples']);
}); });
}); });

View File

@ -63,9 +63,12 @@ export const createNodes: CreateNodes<RollupPluginOptions> = [
options = normalizeOptions(options); options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
targetsCache[hash] ??= await buildRollupTarget( targetsCache[hash] ??= await buildRollupTarget(
configFilePath, configFilePath,

View File

@ -72,9 +72,12 @@ export const createNodes: CreateNodes<StorybookPluginOptions> = [
} }
options = normalizeOptions(options); options = normalizeOptions(options);
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
const projectName = buildProjectName(projectRoot, context.workspaceRoot); const projectName = buildProjectName(projectRoot, context.workspaceRoot);

View File

@ -44,9 +44,10 @@ describe('@nx/vite/plugin', () => {
production: ['!{projectRoot}/**/*.spec.ts'], production: ['!{projectRoot}/**/*.spec.ts'],
}, },
}, },
workspaceRoot: '', workspaceRoot: tempFs.tempDir,
}; };
tempFs.createFileSync('index.html', ''); tempFs.createFileSync('index.html', '');
tempFs.createFileSync('package.json', '');
}); });
afterEach(() => { afterEach(() => {

View File

@ -6,7 +6,6 @@ import {
joinPathFragments, joinPathFragments,
readJsonFile, readJsonFile,
TargetConfiguration, TargetConfiguration,
workspaceRoot,
writeJsonFile, writeJsonFile,
} from '@nx/devkit'; } from '@nx/devkit';
import { dirname, isAbsolute, join, relative } from 'path'; import { dirname, isAbsolute, join, relative } from 'path';
@ -118,7 +117,8 @@ async function buildViteTargets(
const { buildOutputs, testOutputs, hasTest, isBuildable } = getOutputs( const { buildOutputs, testOutputs, hasTest, isBuildable } = getOutputs(
viteConfig, viteConfig,
projectRoot projectRoot,
context.workspaceRoot
); );
const namedInputs = getNamedInputs(projectRoot, context); const namedInputs = getNamedInputs(projectRoot, context);
@ -244,7 +244,8 @@ function serveStaticTarget(options: VitePluginOptions) {
function getOutputs( function getOutputs(
viteConfig: Record<string, any> | undefined, viteConfig: Record<string, any> | undefined,
projectRoot: string projectRoot: string,
workspaceRoot: string
): { ): {
buildOutputs: string[]; buildOutputs: string[];
testOutputs: string[]; testOutputs: string[];
@ -256,6 +257,7 @@ function getOutputs(
const buildOutputPath = normalizeOutputPath( const buildOutputPath = normalizeOutputPath(
build?.outDir, build?.outDir,
projectRoot, projectRoot,
workspaceRoot,
'dist' 'dist'
); );
@ -267,6 +269,7 @@ function getOutputs(
const reportsDirectoryPath = normalizeOutputPath( const reportsDirectoryPath = normalizeOutputPath(
test?.coverage?.reportsDirectory, test?.coverage?.reportsDirectory,
projectRoot, projectRoot,
workspaceRoot,
'coverage' 'coverage'
); );
@ -281,6 +284,7 @@ function getOutputs(
function normalizeOutputPath( function normalizeOutputPath(
outputPath: string | undefined, outputPath: string | undefined,
projectRoot: string, projectRoot: string,
workspaceRoot: string,
path: 'coverage' | 'dist' path: 'coverage' | 'dist'
): string | undefined { ): string | undefined {
if (!outputPath) { if (!outputPath) {

View File

@ -69,9 +69,12 @@ export const createNodes: CreateNodes<WebpackPluginOptions> = [
return {}; return {};
} }
const hash = calculateHashForCreateNodes(projectRoot, options, context, [ const hash = await calculateHashForCreateNodes(
getLockFileName(detectPackageManager(context.workspaceRoot)), projectRoot,
]); options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
);
const targets = targetsCache[hash] const targets = targetsCache[hash]
? targetsCache[hash] ? targetsCache[hash]
: await createWebpackTargets( : await createWebpackTargets(

View File

@ -0,0 +1,34 @@
module.exports = () => {
/**
* When the daemon is enabled during unit tests,
* and the daemon is already running, the daemon-client.ts
* code will be used, but it will hit the already running
* daemon which is from the installed version of Nx.
*
* In the vast majority of cases, this is fine. However,
* if a new message type has been added to the daemon in
* the source code, and isn't yet in the installed version,
* any test that hits that codepath will fail. This is because
* the installed version of the daemon doesn't know how to
* handle the new message type.
*
* To prevent this, we disable the daemon during unit tests.
*/
process.env.NX_DAEMON = 'false';
/**
* When `createProjectGraphAsync` is called during tests,
* if its not mocked, it will return the Nx repo's project
* graph. We don't want any unit tests to depend on the structure
* of the Nx repo, so we mock it to return an empty project graph.
*/
jest.doMock('@nx/devkit', () => ({
...jest.requireActual('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return {
nodes: {},
dependencies: {},
};
}),
}));
};

View File

@ -1,5 +1,13 @@
const nxPreset = require('@nx/jest/preset').default;
/* eslint-disable */ /* eslint-disable */
export default { export default {
...nxPreset,
testTimeout: 35000,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
coverageReporters: ['html'],
maxWorkers: 1,
testEnvironment: 'node',
displayName: 'typedoc-theme', displayName: 'typedoc-theme',
globals: {}, globals: {},
@ -14,5 +22,5 @@ export default {
resolver: '../scripts/patched-jest-resolver.js', resolver: '../scripts/patched-jest-resolver.js',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../coverage/typedoc-theme', coverageDirectory: '../coverage/typedoc-theme',
preset: '../jest.preset.js', setupFiles: [],
}; };