feat(misc): add nx syncing mechanism and update the typescript-sync generator (#26793)

- Add the `nx sync` command to run sync generators and apply changes to
bring the workspace up to date according to their logic.
- Add the `nx sync:check` command to validate that the workspace is up
to date by running the sync generators without applying the changes. It
can be used on CI as a validation check.
- Update the task runner to run the sync generators (or obtain their
state from the daemon) and prompt the user whether to apply the changes,
if any
- This is only run if the `NX_ENABLE_SYNC_GENERATORS` environment
variable is set to `'true'`
  - Allow the user to configure a default value for the prompt
- Update the `@nx/js:typescript-sync` generator (keep tsconfig project
references in sync with the project graph) with misc fixes

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

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

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

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

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

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

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2024-08-12 16:44:04 +02:00 committed by GitHub
parent f208acde54
commit add5a675c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 2896 additions and 386 deletions

View File

@ -37,6 +37,7 @@ Nx.json configuration
- [plugins](../../devkit/documents/NxJsonConfiguration#plugins): PluginConfiguration[]
- [pluginsConfig](../../devkit/documents/NxJsonConfiguration#pluginsconfig): Record<string, Record<string, unknown>>
- [release](../../devkit/documents/NxJsonConfiguration#release): NxReleaseConfiguration
- [sync](../../devkit/documents/NxJsonConfiguration#sync): NxSyncConfiguration
- [targetDefaults](../../devkit/documents/NxJsonConfiguration#targetdefaults): TargetDefaults
- [tasksRunnerOptions](../../devkit/documents/NxJsonConfiguration#tasksrunneroptions): Object
- [useDaemonProcess](../../devkit/documents/NxJsonConfiguration#usedaemonprocess): boolean
@ -240,6 +241,14 @@ Configuration for `nx release` (versioning and publishing of applications and li
---
### sync
`Optional` **sync**: `NxSyncConfiguration`
Configuration for the `nx sync` command.
---
### targetDefaults
`Optional` **targetDefaults**: [`TargetDefaults`](../../devkit/documents/TargetDefaults)

View File

@ -23,6 +23,7 @@ Target's configuration
- [options](../../devkit/documents/TargetConfiguration#options): T
- [outputs](../../devkit/documents/TargetConfiguration#outputs): string[]
- [parallelism](../../devkit/documents/TargetConfiguration#parallelism): boolean
- [syncGenerators](../../devkit/documents/TargetConfiguration#syncgenerators): string[]
## Properties
@ -119,3 +120,12 @@ caching engine.
Whether this target can be run in parallel with other tasks
Default is true
---
### syncGenerators
`Optional` **syncGenerators**: `string`[]
List of generators to run before the target to ensure the workspace
is up to date.

View File

@ -36,6 +36,7 @@ use ProjectsConfigurations or NxJsonConfiguration
- [pluginsConfig](../../devkit/documents/Workspace#pluginsconfig): Record<string, Record<string, unknown>>
- [projects](../../devkit/documents/Workspace#projects): Record<string, ProjectConfiguration>
- [release](../../devkit/documents/Workspace#release): NxReleaseConfiguration
- [sync](../../devkit/documents/Workspace#sync): NxSyncConfiguration
- [targetDefaults](../../devkit/documents/Workspace#targetdefaults): TargetDefaults
- [tasksRunnerOptions](../../devkit/documents/Workspace#tasksrunneroptions): Object
- [useDaemonProcess](../../devkit/documents/Workspace#usedaemonprocess): boolean
@ -328,6 +329,18 @@ Configuration for `nx release` (versioning and publishing of applications and li
---
### sync
`Optional` **sync**: `NxSyncConfiguration`
Configuration for the `nx sync` command.
#### Inherited from
[NxJsonConfiguration](../../devkit/documents/NxJsonConfiguration).[sync](../../devkit/documents/NxJsonConfiguration#sync)
---
### targetDefaults
`Optional` **targetDefaults**: [`TargetDefaults`](../../devkit/documents/TargetDefaults)

View File

@ -7987,9 +7987,9 @@
"disableCollapsible": false
},
{
"id": "sync",
"path": "/nx-api/js/generators/sync",
"name": "sync",
"id": "typescript-sync",
"path": "/nx-api/js/generators/typescript-sync",
"name": "typescript-sync",
"children": [],
"isExternal": false,
"disableCollapsible": false

View File

@ -1283,13 +1283,13 @@
"path": "/nx-api/js/generators/setup-build",
"type": "generator"
},
"/nx-api/js/generators/sync": {
"/nx-api/js/generators/typescript-sync": {
"description": "Synchronize TypeScript project references based on the project graph",
"file": "generated/packages/js/generators/sync.json",
"file": "generated/packages/js/generators/typescript-sync.json",
"hidden": true,
"name": "sync",
"originalFilePath": "/packages/js/src/generators/sync/schema.json",
"path": "/nx-api/js/generators/sync",
"name": "typescript-sync",
"originalFilePath": "/packages/js/src/generators/typescript-sync/schema.json",
"path": "/nx-api/js/generators/typescript-sync",
"type": "generator"
}
},

View File

@ -1267,11 +1267,11 @@
},
{
"description": "Synchronize TypeScript project references based on the project graph",
"file": "generated/packages/js/generators/sync.json",
"file": "generated/packages/js/generators/typescript-sync.json",
"hidden": true,
"name": "sync",
"originalFilePath": "/packages/js/src/generators/sync/schema.json",
"path": "js/generators/sync",
"name": "typescript-sync",
"originalFilePath": "/packages/js/src/generators/typescript-sync/schema.json",
"path": "js/generators/typescript-sync",
"type": "generator"
}
],

View File

@ -1,6 +1,6 @@
{
"name": "sync",
"factory": "./src/generators/sync/sync#syncGenerator",
"name": "typescript-sync",
"factory": "./src/generators/typescript-sync/typescript-sync",
"schema": {
"$schema": "https://json-schema.org/schema",
"$id": "action",
@ -11,9 +11,10 @@
"presets": []
},
"description": "Synchronize TypeScript project references based on the project graph",
"alias": ["sync"],
"hidden": true,
"implementation": "/packages/js/src/generators/sync/sync#syncGenerator.ts",
"implementation": "/packages/js/src/generators/typescript-sync/typescript-sync.ts",
"aliases": [],
"path": "/packages/js/src/generators/sync/schema.json",
"path": "/packages/js/src/generators/typescript-sync/schema.json",
"type": "generator"
}

View File

@ -476,7 +476,7 @@
- [release-version](/nx-api/js/generators/release-version)
- [setup-verdaccio](/nx-api/js/generators/setup-verdaccio)
- [setup-build](/nx-api/js/generators/setup-build)
- [sync](/nx-api/js/generators/sync)
- [typescript-sync](/nx-api/js/generators/typescript-sync)
- [nest](/nx-api/nest)
- [documents](/nx-api/nest/documents)
- [Overview](/nx-api/nest/documents/overview)

View File

@ -42,10 +42,11 @@
"alias": ["build"],
"description": "setup-build generator"
},
"sync": {
"factory": "./src/generators/sync/sync#syncGenerator",
"schema": "./src/generators/sync/schema.json",
"typescript-sync": {
"factory": "./src/generators/typescript-sync/typescript-sync",
"schema": "./src/generators/typescript-sync/schema.json",
"description": "Synchronize TypeScript project references based on the project graph",
"alias": ["sync"],
"hidden": true
}
}

View File

@ -1,206 +0,0 @@
import { ProjectGraph, Tree, readJson, writeJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { syncGenerator } from './sync';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual('@nx/devkit'),
createProjectGraphAsync: jest.fn(() => Promise.resolve(projectGraph)),
}));
describe('syncGenerator()', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace();
projectGraph = {
nodes: {
a: {
name: 'a',
type: 'lib',
data: {
root: 'packages/a',
},
},
b: {
name: 'b',
type: 'lib',
data: {
root: 'packages/b',
},
},
},
dependencies: {
a: [],
b: [
{
type: 'static',
source: 'b',
target: 'a',
},
],
},
};
writeJson(tree, 'nx.json', {
// Wire up the @nx/js/typescript plugin with default options
plugins: ['@nx/js/typescript'],
});
// Root tsconfigs
writeJson(tree, 'tsconfig.json', {});
writeJson(tree, 'tsconfig.options.json', { compilerOptions: {} });
// Package A
writeJson(tree, 'packages/a/tsconfig.json', {});
writeJson(tree, 'packages/a/tsconfig.lib.json', {
compilerOptions: {
outDir: '../../dist/packages/a/dist',
},
});
writeJson(tree, 'packages/a/package.json', {
name: 'a',
version: '0.0.0',
});
// Package B (depends on A)
writeJson(tree, 'packages/b/tsconfig.json', {});
writeJson(tree, 'packages/b/tsconfig.lib.json', {});
writeJson(tree, 'packages/b/package.json', {
name: 'b',
version: '0.0.0',
dependencies: {
a: '0.0.0',
},
});
});
it('should error if the @nx/js/typescript plugin is not configured in nx.json', async () => {
const nxJson = readJson(tree, 'nx.json');
nxJson.plugins = nxJson.plugins.filter((p) => p !== '@nx/js/typescript');
writeJson(tree, 'nx.json', nxJson);
await expect(syncGenerator(tree, {})).rejects.toMatchInlineSnapshot(
`[Error: The @nx/js/typescript plugin must be added to the "plugins" array in nx.json before syncing tsconfigs]`
);
});
describe('root tsconfig.json', () => {
it('should sync project references to the root tsconfig.json', async () => {
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(
`undefined`
);
await syncGenerator(tree, {});
const rootTsconfig = readJson(tree, 'tsconfig.json');
expect(rootTsconfig.references).toMatchInlineSnapshot(`
[
{
"path": "./packages/a",
},
{
"path": "./packages/b",
},
]
`);
});
it('should respect existing project references in the root tsconfig.json', async () => {
writeJson(tree, 'tsconfig.json', {
// Swapped order and additional manual reference
references: [
{ path: './packages/b' },
{ path: 'packages/a' },
{ path: 'packages/c' },
],
});
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
[
{
"path": "./packages/b",
},
{
"path": "packages/a",
},
{
"path": "packages/c",
},
]
`);
await syncGenerator(tree, {});
const rootTsconfig = readJson(tree, 'tsconfig.json');
expect(rootTsconfig.references).toMatchInlineSnapshot(`
[
{
"path": "./packages/b",
},
{
"path": "packages/a",
},
{
"path": "packages/c",
},
]
`);
});
});
describe('project level tsconfig.json', () => {
it('should sync project references to project level tsconfig.json files where needed', async () => {
expect(
readJson(tree, 'packages/b/tsconfig.json').references
).toMatchInlineSnapshot(`undefined`);
await syncGenerator(tree, {});
expect(readJson(tree, 'packages/b/tsconfig.json').references)
.toMatchInlineSnapshot(`
[
{
"path": "../a",
},
]
`);
});
it('should respect existing project references in the project level tsconfig.json', async () => {
writeJson(tree, 'packages/b/tsconfig.json', {
// Swapped order and additional manual reference
references: [{ path: '../some/thing' }, { path: '../../another/one' }],
});
expect(readJson(tree, 'packages/b/tsconfig.json').references)
.toMatchInlineSnapshot(`
[
{
"path": "../some/thing",
},
{
"path": "../../another/one",
},
]
`);
await syncGenerator(tree, {});
const rootTsconfig = readJson(tree, 'packages/b/tsconfig.json');
// The dependency reference on "a" is added to the start of the array
expect(rootTsconfig.references).toMatchInlineSnapshot(`
[
{
"path": "../a",
},
{
"path": "../some/thing",
},
{
"path": "../../another/one",
},
]
`);
});
});
});

View File

@ -1,130 +0,0 @@
import {
Tree,
createProjectGraphAsync,
formatFiles,
joinPathFragments,
readJson,
readNxJson,
writeJson,
} from '@nx/devkit';
import { relative } from 'node:path';
import { PLUGIN_NAME, TscPluginOptions } from '../../plugins/typescript/plugin';
import { SyncSchema } from './schema';
interface Tsconfig {
references?: Array<{ path: string }>;
compilerOptions?: {
paths?: Record<string, string[]>;
rootDir?: string;
outDir?: string;
};
}
export async function syncGenerator(tree: Tree, options: SyncSchema) {
// Ensure that the plugin has been wired up in nx.json
const nxJson = readNxJson(tree);
let tscPluginConfig: { plugin: string; options?: TscPluginOptions } | string =
nxJson.plugins.find((p) => {
if (typeof p === 'string') {
return p === PLUGIN_NAME;
}
return p.plugin === PLUGIN_NAME;
});
if (!tscPluginConfig) {
throw new Error(
`The ${PLUGIN_NAME} plugin must be added to the "plugins" array in nx.json before syncing tsconfigs`
);
}
const projectGraph = await createProjectGraphAsync();
const firstPartyDeps = Object.entries(projectGraph.dependencies).filter(
([name, data]) => !name.startsWith('npm:') && data.length > 0
);
// Root tsconfig containing project references for the whole workspace
const rootTsconfigPath = 'tsconfig.json';
const rootTsconfig = readJson<Tsconfig>(tree, rootTsconfigPath);
const tsconfigProjectNodeValues = Object.values(projectGraph.nodes).filter(
(node) => {
const projectTsconfigPath = joinPathFragments(
node.data.root,
'tsconfig.json'
);
return tree.exists(projectTsconfigPath);
}
);
if (tsconfigProjectNodeValues.length > 0) {
// Sync the root tsconfig references from the project graph (do not destroy existing references)
rootTsconfig.references = rootTsconfig.references || [];
const referencesSet = new Set(
rootTsconfig.references.map((ref) => normalizeReferencePath(ref.path))
);
for (const node of tsconfigProjectNodeValues) {
const normalizedPath = normalizeReferencePath(node.data.root);
// Skip the root tsconfig itself
if (node.data.root !== '.' && !referencesSet.has(normalizedPath)) {
rootTsconfig.references.push({ path: `./${normalizedPath}` });
}
}
writeJson(tree, rootTsconfigPath, rootTsconfig);
}
for (const [name, data] of firstPartyDeps) {
// Get the source project nodes for the source and target
const sourceProjectNode = projectGraph.nodes[name];
// Find the relevant tsconfig files for the source project
const sourceProjectTsconfigPath = joinPathFragments(
sourceProjectNode.data.root,
'tsconfig.json'
);
if (!tree.exists(sourceProjectTsconfigPath)) {
console.warn(
`Skipping project "${name}" as there is no tsconfig.json file found in the project root "${sourceProjectNode.data.root}"`
);
continue;
}
const sourceTsconfig = readJson<Tsconfig>(tree, sourceProjectTsconfigPath);
for (const dep of data) {
// Get the target project node
const targetProjectNode = projectGraph.nodes[dep.target];
if (!targetProjectNode) {
// It's an external dependency
continue;
}
// Set defaults only in the case where we have at least one dependency so that we don't patch files when not necessary
sourceTsconfig.references = sourceTsconfig.references || [];
// Ensure the project reference for the target is set
const relativePathToTargetRoot = relative(
sourceProjectNode.data.root,
targetProjectNode.data.root
);
if (
!sourceTsconfig.references.some(
(ref) => ref.path === relativePathToTargetRoot
)
) {
// Make sure we unshift rather than push so that dependencies are built in the right order by TypeScript when it is run directly from the root of the workspace
sourceTsconfig.references.unshift({ path: relativePathToTargetRoot });
}
}
// Update the source tsconfig files
writeJson(tree, sourceProjectTsconfigPath, sourceTsconfig);
}
await formatFiles(tree);
}
// Normalize the paths to strip leading `./` and trailing `/tsconfig.json`
function normalizeReferencePath(path: string): string {
return path.replace(/\/tsconfig.json$/, '').replace(/^\.\//, '');
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,400 @@
import {
createProjectGraphAsync,
formatFiles,
joinPathFragments,
logger,
readJson,
readNxJson,
writeJson,
type ExpandedPluginConfiguration,
type ProjectGraph,
type ProjectGraphProjectNode,
type Tree,
} from '@nx/devkit';
import { dirname, normalize, relative } from 'node:path/posix';
import type { SyncGeneratorResult } from 'nx/src/utils/sync-generators';
import {
PLUGIN_NAME,
type TscPluginOptions,
} from '../../plugins/typescript/plugin';
interface Tsconfig {
references?: Array<{ path: string }>;
compilerOptions?: {
paths?: Record<string, string[]>;
rootDir?: string;
outDir?: string;
};
}
const COMMON_RUNTIME_TS_CONFIG_FILE_NAMES = [
'tsconfig.app.json',
'tsconfig.lib.json',
'tsconfig.build.json',
'tsconfig.cjs.json',
'tsconfig.esm.json',
'tsconfig.runtime.json',
];
export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
// Ensure that the plugin has been wired up in nx.json
const nxJson = readNxJson(tree);
const tscPluginConfig:
| string
| ExpandedPluginConfiguration<TscPluginOptions> = nxJson.plugins.find(
(p) => {
if (typeof p === 'string') {
return p === PLUGIN_NAME;
}
return p.plugin === PLUGIN_NAME;
}
);
if (!tscPluginConfig) {
throw new Error(
`The ${PLUGIN_NAME} plugin must be added to the "plugins" array in nx.json before syncing tsconfigs`
);
}
// Root tsconfig containing project references for the whole workspace
const rootTsconfigPath = 'tsconfig.json';
if (!tree.exists(rootTsconfigPath)) {
throw new Error(`A "tsconfig.json" file must exist in the workspace root.`);
}
const rootTsconfig = readJson<Tsconfig>(tree, rootTsconfigPath);
const projectGraph = await createProjectGraphAsync();
const projectRoots = new Set<string>();
const tsconfigProjectNodeValues = Object.values(projectGraph.nodes).filter(
(node) => {
projectRoots.add(node.data.root);
const projectTsconfigPath = joinPathFragments(
node.data.root,
'tsconfig.json'
);
return tree.exists(projectTsconfigPath);
}
);
// Track if any changes were made to the tsconfig files. We check the changes
// made by this generator to know if the TS config is out of sync with the
// project graph. Therefore, we don't format the files if there were no changes
// to avoid potential format-only changes that can lead to false positives.
let hasChanges = false;
if (tsconfigProjectNodeValues.length > 0) {
const referencesSet = new Set();
for (const ref of rootTsconfig.references ?? []) {
// reference path is relative to the tsconfig file
const resolvedRefPath = getTsConfigPathFromReferencePath(
tree,
rootTsconfigPath,
ref.path
);
if (tree.exists(resolvedRefPath)) {
// we only keep the references that still exist
referencesSet.add(normalizeReferencePath(ref.path));
} else {
hasChanges = true;
}
}
for (const node of tsconfigProjectNodeValues) {
const normalizedPath = normalizeReferencePath(node.data.root);
// Skip the root tsconfig itself
if (node.data.root !== '.' && !referencesSet.has(normalizedPath)) {
referencesSet.add(normalizedPath);
hasChanges = true;
}
}
if (hasChanges) {
rootTsconfig.references = Array.from(referencesSet).map((ref) => ({
path: `./${ref}`,
}));
writeJson(tree, rootTsconfigPath, rootTsconfig);
}
}
const runtimeTsConfigFileNames =
(nxJson.sync?.generatorOptions?.['@nx/js:typescript-sync']
?.runtimeTsConfigFileNames as string[]) ??
COMMON_RUNTIME_TS_CONFIG_FILE_NAMES;
const collectedDependencies = new Map<string, ProjectGraphProjectNode[]>();
for (const [name, data] of Object.entries(projectGraph.dependencies)) {
if (
!projectGraph.nodes[name] ||
projectGraph.nodes[name].data.root === '.' ||
!data.length
) {
continue;
}
// Get the source project nodes for the source and target
const sourceProjectNode = projectGraph.nodes[name];
// Find the relevant tsconfig file for the source project
const sourceProjectTsconfigPath = joinPathFragments(
sourceProjectNode.data.root,
'tsconfig.json'
);
if (!tree.exists(sourceProjectTsconfigPath)) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
logger.warn(
`Skipping project "${name}" as there is no tsconfig.json file found in the project root "${sourceProjectNode.data.root}".`
);
}
continue;
}
// Collect the dependencies of the source project
const dependencies = collectProjectDependencies(
tree,
name,
projectGraph,
collectedDependencies
);
if (!dependencies.length) {
continue;
}
for (const runtimeTsConfigFileName of runtimeTsConfigFileNames) {
const runtimeTsConfigPath = joinPathFragments(
sourceProjectNode.data.root,
runtimeTsConfigFileName
);
if (!tree.exists(runtimeTsConfigPath)) {
continue;
}
// Update project references for the runtime tsconfig
hasChanges =
updateTsConfigReferences(
tree,
runtimeTsConfigPath,
dependencies,
sourceProjectNode.data.root,
projectRoots,
runtimeTsConfigFileName,
runtimeTsConfigFileNames
) || hasChanges;
}
// Update project references for the tsconfig.json file
hasChanges =
updateTsConfigReferences(
tree,
sourceProjectTsconfigPath,
dependencies,
sourceProjectNode.data.root,
projectRoots
) || hasChanges;
}
if (hasChanges) {
await formatFiles(tree);
return {
outOfSyncMessage:
'Based on the workspace project graph, some TypeScript configuration files are missing project references to the projects they depend on.',
};
}
}
export default syncGenerator;
function updateTsConfigReferences(
tree: Tree,
tsConfigPath: string,
dependencies: ProjectGraphProjectNode[],
projectRoot: string,
projectRoots: Set<string>,
runtimeTsConfigFileName?: string,
possibleRuntimeTsConfigFileNames?: string[]
): boolean {
const tsConfig = readJson<Tsconfig>(tree, tsConfigPath);
// We have at least one dependency so we can safely set it to an empty array if not already set
const references = [];
const originalReferencesSet = new Set();
const newReferencesSet = new Set();
for (const ref of tsConfig.references ?? []) {
const normalizedPath = normalizeReferencePath(ref.path);
originalReferencesSet.add(normalizedPath);
// reference path is relative to the tsconfig file
const resolvedRefPath = getTsConfigPathFromReferencePath(
tree,
tsConfigPath,
ref.path
);
if (
isInternalProjectReference(
tree,
resolvedRefPath,
projectRoot,
projectRoots
)
) {
// we keep all internal references
references.push(ref);
newReferencesSet.add(normalizedPath);
}
}
let hasChanges = false;
for (const dep of dependencies) {
// Ensure the project reference for the target is set
let referencePath = dep.data.root;
if (runtimeTsConfigFileName) {
const runtimeTsConfigPath = joinPathFragments(
dep.data.root,
runtimeTsConfigFileName
);
if (tree.exists(runtimeTsConfigPath)) {
referencePath = runtimeTsConfigPath;
} else {
// Check for other possible runtime tsconfig file names
// TODO(leo): should we check if there are more than one runtime tsconfig files and throw an error?
for (const possibleRuntimeTsConfigFileName of possibleRuntimeTsConfigFileNames ??
[]) {
const possibleRuntimeTsConfigPath = joinPathFragments(
dep.data.root,
possibleRuntimeTsConfigFileName
);
if (tree.exists(possibleRuntimeTsConfigPath)) {
referencePath = possibleRuntimeTsConfigPath;
break;
}
}
}
}
const relativePathToTargetRoot = relative(projectRoot, referencePath);
if (!newReferencesSet.has(relativePathToTargetRoot)) {
newReferencesSet.add(relativePathToTargetRoot);
// Make sure we unshift rather than push so that dependencies are built in the right order by TypeScript when it is run directly from the root of the workspace
references.unshift({ path: relativePathToTargetRoot });
}
if (!originalReferencesSet.has(relativePathToTargetRoot)) {
hasChanges = true;
}
}
hasChanges ||= newReferencesSet.size !== originalReferencesSet.size;
if (hasChanges) {
tsConfig.references = references;
writeJson(tree, tsConfigPath, tsConfig);
}
return hasChanges;
}
// TODO(leo): follow up with the TypeScript team to confirm if we really need
// to reference transitive dependencies.
// Collect the dependencies of a project recursively sorted from root to leaf
function collectProjectDependencies(
tree: Tree,
projectName: string,
projectGraph: ProjectGraph,
collectedDependencies: Map<string, ProjectGraphProjectNode[]>
): ProjectGraphProjectNode[] {
if (collectedDependencies.has(projectName)) {
// We've already collected the dependencies for this project
return collectedDependencies.get(projectName);
}
collectedDependencies.set(projectName, []);
for (const dep of projectGraph.dependencies[projectName]) {
const targetProjectNode = projectGraph.nodes[dep.target];
if (!targetProjectNode) {
// It's an npm dependency
continue;
}
// Add the target project node to the list of dependencies for the current project
if (
!collectedDependencies
.get(projectName)
.some((d) => d.name === targetProjectNode.name)
) {
collectedDependencies.get(projectName).push(targetProjectNode);
}
if (process.env.NX_DISABLE_TS_SYNC_TRANSITIVE_DEPENDENCIES === 'true') {
continue;
}
// Recursively get the dependencies of the target project
const transitiveDependencies = collectProjectDependencies(
tree,
dep.target,
projectGraph,
collectedDependencies
);
for (const transitiveDep of transitiveDependencies) {
if (
!collectedDependencies
.get(projectName)
.some((d) => d.name === transitiveDep.name)
) {
collectedDependencies.get(projectName).push(transitiveDep);
}
}
}
return collectedDependencies.get(projectName);
}
// Normalize the paths to strip leading `./` and trailing `/tsconfig.json`
function normalizeReferencePath(path: string): string {
return normalize(path)
.replace(/\/tsconfig.json$/, '')
.replace(/^\.\//, '');
}
function isInternalProjectReference(
tree: Tree,
refTsConfigPath: string,
projectRoot: string,
projectRoots: Set<string>
): boolean {
let currentPath = getTsConfigDirName(tree, refTsConfigPath);
if (relative(projectRoot, currentPath).startsWith('..')) {
// it's outside of the project root, so it's an external project reference
return false;
}
while (currentPath !== projectRoot) {
if (projectRoots.has(currentPath)) {
// it's inside a nested project root, so it's and external project reference
return false;
}
currentPath = dirname(currentPath);
}
// it's inside the project root, so it's an internal project reference
return true;
}
function getTsConfigDirName(tree: Tree, tsConfigPath: string): string {
return tree.isFile(tsConfigPath)
? dirname(tsConfigPath)
: normalize(tsConfigPath);
}
function getTsConfigPathFromReferencePath(
tree: Tree,
ownerTsConfigPath: string,
referencePath: string
): string {
const resolvedRefPath = joinPathFragments(
dirname(ownerTsConfigPath),
referencePath
);
return tree.isFile(resolvedRefPath)
? resolvedRefPath
: joinPathFragments(resolvedRefPath, 'tsconfig.json');
}

View File

@ -81,6 +81,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{projectRoot}/**/*.d.mts.map",
"{projectRoot}/tsconfig.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -137,6 +140,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -175,6 +181,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -216,6 +225,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -300,6 +312,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -343,6 +358,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -388,6 +406,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -433,6 +454,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -486,6 +510,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -564,6 +591,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -590,6 +620,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib/nested-project",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -830,6 +863,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{projectRoot}/**/*.d.mts.map",
"{projectRoot}/tsconfig.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -880,6 +916,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{workspaceRoot}/dist/libs/my-lib/index.d.ts.map",
"{workspaceRoot}/dist/libs/my-lib/index.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -924,6 +963,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"outputs": [
"{workspaceRoot}/dist/libs/my-lib",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -979,6 +1021,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{projectRoot}/**/*.d.mts.map",
"{projectRoot}/tsconfig.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1067,6 +1112,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{workspaceRoot}/dist/out-tsc/libs/my-lib/specs",
"{workspaceRoot}/dist/out-tsc/libs/my-lib/cypress",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1094,6 +1142,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"outputs": [
"{workspaceRoot}/dist/out-tsc/libs/my-lib/nested-project",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1146,6 +1197,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{workspaceRoot}/dist/libs/my-lib/index.d.ts.map",
"{workspaceRoot}/dist/libs/my-lib/my-lib.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1203,6 +1257,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{projectRoot}/**/*.d.mts.map",
"{workspaceRoot}/dist/libs/my-lib/my-lib.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1351,6 +1408,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1396,6 +1456,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1445,6 +1508,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1494,6 +1560,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1544,6 +1613,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1602,6 +1674,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1665,6 +1740,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1902,6 +1980,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{projectRoot}/**/*.d.mts.map",
"{projectRoot}/tsconfig.lib.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -1957,6 +2038,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{workspaceRoot}/dist/libs/my-lib/index.d.ts.map",
"{workspaceRoot}/dist/libs/my-lib/index.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -2006,6 +2090,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"outputs": [
"{workspaceRoot}/dist/libs/my-lib",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -2066,6 +2153,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{projectRoot}/**/*.d.mts.map",
"{projectRoot}/tsconfig.lib.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -2127,6 +2217,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{workspaceRoot}/dist/libs/my-lib/lib.tsbuildinfo",
"{workspaceRoot}/dist/libs/my-lib/other",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -2184,6 +2277,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{workspaceRoot}/dist/libs/my-lib/index.d.ts.map",
"{workspaceRoot}/dist/libs/my-lib/my-lib.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
@ -2246,6 +2342,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"{projectRoot}/**/*.d.mts.map",
"{workspaceRoot}/dist/libs/my-lib/my-lib.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},

View File

@ -228,6 +228,7 @@ function buildTscTargets(
context.workspaceRoot,
projectRoot
),
syncGenerators: ['@nx/js:typescript-sync'],
};
}
}
@ -261,6 +262,7 @@ function buildTscTargets(
context.workspaceRoot,
projectRoot
),
syncGenerators: ['@nx/js:typescript-sync'],
};
}

View File

@ -246,6 +246,31 @@
"type": "string"
}
}
},
"sync": {
"type": "object",
"description": "Configuration for the `nx sync` command",
"properties": {
"globalGenerators": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of workspace-wide sync generators to be run (not attached to targets)"
},
"generatorOptions": {
"type": "object",
"description": "Options for the sync generators.",
"additionalProperties": {
"type": "object"
}
},
"applyChanges": {
"type": "boolean",
"description": "Whether to automatically apply sync generator changes when running tasks. If not set, the user will be prompted. If set to `true`, the user will not be prompted and the changes will be applied. If set to `false`, the user will not be prompted and the changes will not be applied."
}
},
"additionalProperties": false
}
},
"definitions": {

View File

@ -153,6 +153,13 @@
}
},
"additionalProperties": true
},
"syncGenerators": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of generators to run before the target to ensure the workspace is up to date"
}
}
}

View File

@ -79,6 +79,7 @@ export const allowedWorkspaceExtensions = [
'useDaemonProcess',
'useInferencePlugins',
'neverConnectToCloud',
'sync',
] as const;
if (!patched) {

View File

@ -40,6 +40,7 @@ import {
yargsPrintAffectedCommand,
yargsAffectedGraphCommand,
} from './deprecated/command-objects';
import { yargsSyncCheckCommand, yargsSyncCommand } from './sync/command-object';
// Ensure that the output takes up the available width of the terminal.
yargs.wrap(yargs.terminalWidth());
@ -86,6 +87,8 @@ export const commandsObject = yargs
.command(yargsRunCommand)
.command(yargsRunManyCommand)
.command(yargsShowCommand)
.command(yargsSyncCommand)
.command(yargsSyncCheckCommand)
.command(yargsViewLogsCommand)
.command(yargsWatchCommand)
.command(yargsNxInfixCommand)

View File

@ -0,0 +1,43 @@
import type { CommandModule } from 'yargs';
export interface SyncArgs {
verbose?: boolean;
}
export const yargsSyncCommand: CommandModule<
Record<string, unknown>,
SyncArgs
> = {
command: 'sync',
describe: false,
builder: (yargs) =>
yargs.option('verbose', {
type: 'boolean',
description:
'Prints additional information about the commands (e.g., stack traces)',
}),
handler: async (args) => {
process.exit(await import('./sync').then((m) => m.syncHandler(args)));
},
};
export const yargsSyncCheckCommand: CommandModule<
Record<string, unknown>,
SyncArgs
> = {
command: 'sync:check',
describe: false,
builder: (yargs) =>
yargs.option('verbose', {
type: 'boolean',
description:
'Prints additional information about the commands (e.g., stack traces)',
}),
handler: async (args) => {
process.exit(
await import('./sync').then((m) =>
m.syncHandler({ ...args, check: true })
)
);
},
};

View File

@ -0,0 +1,46 @@
import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params';
import {
collectAllRegisteredSyncGenerators,
flushSyncGeneratorChanges,
getSyncGeneratorChanges,
syncGeneratorResultsToMessageLines,
} from '../../utils/sync-generators';
import type { SyncArgs } from './command-object';
interface SyncOptions extends SyncArgs {
check?: boolean;
}
export function syncHandler(options: SyncOptions): Promise<number> {
if (options.verbose) {
process.env.NX_VERBOSE_LOGGING = 'true';
}
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
return handleErrors(isVerbose, async () => {
const projectGraph = await createProjectGraphAsync();
const syncGenerators = await collectAllRegisteredSyncGenerators(
projectGraph
);
const results = await getSyncGeneratorChanges(syncGenerators);
if (!results.length) {
return 0;
}
if (options.check) {
output.error({
title: `The workspace is out of sync`,
bodyLines: syncGeneratorResultsToMessageLines(results),
});
return 1;
}
await flushSyncGeneratorChanges(results);
return 0;
});
}

View File

@ -298,6 +298,28 @@ export interface NxReleaseConfiguration {
versionPlans?: boolean;
}
export interface NxSyncConfiguration {
/**
* List of workspace-wide sync generators to be run (not attached to targets).
*/
globalGenerators?: string[];
/**
* Options for the sync generators.
*/
generatorOptions?: {
[generatorName: string]: Record<string, unknown>;
};
/**
* Whether to automatically apply sync generator changes when running tasks.
* If not set, the user will be prompted.
* If set to `true`, the user will not be prompted and the changes will be applied.
* If set to `false`, the user will not be prompted and the changes will not be applied.
*/
applyChanges?: boolean;
}
/**
* Nx.json configuration
*
@ -456,6 +478,11 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
* Set this to false to disable connection to Nx Cloud
*/
neverConnectToCloud?: boolean;
/**
* Configuration for the `nx sync` command.
*/
sync?: NxSyncConfiguration;
}
export type PluginConfiguration = string | ExpandedPluginConfiguration;

View File

@ -236,4 +236,10 @@ export interface TargetConfiguration<T = any> {
* Default is true
*/
parallelism?: boolean;
/**
* List of generators to run before the target to ensure the workspace
* is up to date.
*/
syncGenerators?: string[];
}

View File

@ -49,6 +49,23 @@ import {
HandleWriteTaskRunsToHistoryMessage,
} from '../message-types/task-history';
import { FORCE_SHUTDOWN } from '../message-types/force-shutdown';
import {
GET_SYNC_GENERATOR_CHANGES,
type HandleGetSyncGeneratorChangesMessage,
} from '../message-types/get-sync-generator-changes';
import type { SyncGeneratorChangesResult } from '../../utils/sync-generators';
import {
GET_REGISTERED_SYNC_GENERATORS,
type HandleGetRegisteredSyncGeneratorsMessage,
} from '../message-types/get-registered-sync-generators';
import {
UPDATE_WORKSPACE_CONTEXT,
type HandleUpdateWorkspaceContextMessage,
} from '../message-types/update-workspace-context';
import {
FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK,
type HandleFlushSyncGeneratorChangesToDiskMessage,
} from '../message-types/flush-sync-generator-changes-to-disk';
const DAEMON_ENV_SETTINGS = {
NX_PROJECT_GLOB_CACHE: 'false',
@ -343,6 +360,45 @@ export class DaemonClient {
return this.sendMessageToDaemon(message);
}
getSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
const message: HandleGetSyncGeneratorChangesMessage = {
type: GET_SYNC_GENERATOR_CHANGES,
generators,
};
return this.sendToDaemonViaQueue(message);
}
flushSyncGeneratorChangesToDisk(generators: string[]): Promise<void> {
const message: HandleFlushSyncGeneratorChangesToDiskMessage = {
type: FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK,
generators,
};
return this.sendToDaemonViaQueue(message);
}
getRegisteredSyncGenerators(): Promise<string[]> {
const message: HandleGetRegisteredSyncGeneratorsMessage = {
type: GET_REGISTERED_SYNC_GENERATORS,
};
return this.sendToDaemonViaQueue(message);
}
updateWorkspaceContext(
createdFiles: string[],
updatedFiles: string[],
deletedFiles: string[]
): Promise<void> {
const message: HandleUpdateWorkspaceContextMessage = {
type: UPDATE_WORKSPACE_CONTEXT,
createdFiles,
updatedFiles,
deletedFiles,
};
return this.sendToDaemonViaQueue(message);
}
async isServerAvailable(): Promise<boolean> {
return new Promise((resolve) => {
try {

View File

@ -0,0 +1,18 @@
export const FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK =
'CLEAR_CACHED_SYNC_GENERATOR_CHANGES' as const;
export type HandleFlushSyncGeneratorChangesToDiskMessage = {
type: typeof FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK;
generators: string[];
};
export function isHandleFlushSyncGeneratorChangesToDiskMessage(
message: unknown
): message is HandleFlushSyncGeneratorChangesToDiskMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK
);
}

View File

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

View File

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

View File

@ -1,18 +0,0 @@
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,19 @@
export const UPDATE_WORKSPACE_CONTEXT = 'UPDATE_WORKSPACE_CONTEXT' as const;
export type HandleUpdateWorkspaceContextMessage = {
type: typeof UPDATE_WORKSPACE_CONTEXT;
createdFiles: string[];
updatedFiles: string[];
deletedFiles: string[];
};
export function isHandleUpdateWorkspaceContextMessage(
message: unknown
): message is HandleUpdateWorkspaceContextMessage {
return (
typeof message === 'object' &&
message !== null &&
'type' in message &&
message['type'] === UPDATE_WORKSPACE_CONTEXT
);
}

View File

@ -0,0 +1,13 @@
import type { HandlerResult } from './server';
import { flushSyncGeneratorChangesToDisk } from './sync-generators';
export async function handleFlushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<HandlerResult> {
await flushSyncGeneratorChangesToDisk(generators);
return {
response: '{}',
description: 'handleFlushSyncGeneratorChangesToDisk',
};
}

View File

@ -0,0 +1,11 @@
import type { HandlerResult } from './server';
import { getCachedRegisteredSyncGenerators } from './sync-generators';
export async function handleGetRegisteredSyncGenerators(): Promise<HandlerResult> {
const syncGenerators = await getCachedRegisteredSyncGenerators();
return {
response: JSON.stringify(syncGenerators),
description: 'handleGetSyncGeneratorChanges',
};
}

View File

@ -0,0 +1,20 @@
import type { HandlerResult } from './server';
import { getCachedSyncGeneratorChanges } from './sync-generators';
export async function handleGetSyncGeneratorChanges(
generators: string[]
): Promise<HandlerResult> {
const changes = await getCachedSyncGeneratorChanges(generators);
// strip out the content of the changes and any potential callback
const result = changes.map((change) => ({
generatorName: change.generatorName,
changes: change.changes.map((c) => ({ ...c, content: null })),
outOfSyncMessage: change.outOfSyncMessage,
}));
return {
response: JSON.stringify(result),
description: 'handleGetSyncGeneratorChanges',
};
}

View File

@ -0,0 +1,15 @@
import { addUpdatedAndDeletedFiles } from './project-graph-incremental-recomputation';
import type { HandlerResult } from './server';
export async function handleUpdateWorkspaceContext(
createdFiles: string[],
updatedFiles: string[],
deletedFiles: string[]
): Promise<HandlerResult> {
addUpdatedAndDeletedFiles(createdFiles, updatedFiles, deletedFiles);
return {
response: '{}',
description: 'handleUpdateContextFiles',
};
}

View File

@ -62,6 +62,9 @@ export let currentProjectGraph: ProjectGraph | undefined;
const collectedUpdatedFiles = new Set<string>();
const collectedDeletedFiles = new Set<string>();
const projectGraphRecomputationListeners = new Set<
(projectGraph: ProjectGraph) => void
>();
let storedWorkspaceConfigHash: string | undefined;
let waitPeriod = 100;
let scheduledTimeoutId;
@ -69,8 +72,10 @@ let knownExternalNodes: Record<string, ProjectGraphExternalNode> = {};
export async function getCachedSerializedProjectGraphPromise(): Promise<SerializedProjectGraph> {
try {
let wasScheduled = false;
// recomputing it now on demand. we can ignore the scheduled timeout
if (scheduledTimeoutId) {
wasScheduled = true;
clearTimeout(scheduledTimeoutId);
scheduledTimeoutId = undefined;
}
@ -88,7 +93,13 @@ export async function getCachedSerializedProjectGraphPromise(): Promise<Serializ
cachedSerializedProjectGraphPromise =
processFilesAndCreateAndSerializeProjectGraph(plugins);
}
return await cachedSerializedProjectGraphPromise;
const result = await cachedSerializedProjectGraphPromise;
if (wasScheduled) {
notifyProjectGraphRecomputationListeners(result.projectGraph);
}
return result;
} catch (e) {
return {
error: e,
@ -135,15 +146,23 @@ export function addUpdatedAndDeletedFiles(
cachedSerializedProjectGraphPromise =
processFilesAndCreateAndSerializeProjectGraph(await getPlugins());
await cachedSerializedProjectGraphPromise;
const { projectGraph } = await cachedSerializedProjectGraphPromise;
if (createdFiles.length > 0) {
notifyFileWatcherSockets(createdFiles, null, null);
}
notifyProjectGraphRecomputationListeners(projectGraph);
}, waitPeriod);
}
}
export function registerProjectGraphRecomputationListener(
listener: (projectGraph: ProjectGraph) => void
) {
projectGraphRecomputationListeners.add(listener);
}
function computeWorkspaceConfigHash(
projectsConfigurations: Record<string, ProjectConfiguration>
) {
@ -413,3 +432,9 @@ async function resetInternalStateIfNxDepsMissing() {
await resetInternalState();
}
}
function notifyProjectGraphRecomputationListeners(projectGraph: ProjectGraph) {
for (const listener of projectGraphRecomputationListeners) {
listener(projectGraph);
}
}

View File

@ -33,7 +33,10 @@ import {
disableOutputsTracking,
processFileChangesInOutputs,
} from './outputs-tracking';
import { addUpdatedAndDeletedFiles } from './project-graph-incremental-recomputation';
import {
addUpdatedAndDeletedFiles,
registerProjectGraphRecomputationListener,
} from './project-graph-incremental-recomputation';
import {
getOutputWatcherInstance,
getWatcherInstance,
@ -78,6 +81,27 @@ import { handleGetTaskHistoryForHashes } from './handle-get-task-history';
import { handleWriteTaskRunsToHistory } from './handle-write-task-runs-to-history';
import { isHandleForceShutdownMessage } from '../message-types/force-shutdown';
import { handleForceShutdown } from './handle-force-shutdown';
import {
GET_SYNC_GENERATOR_CHANGES,
isHandleGetSyncGeneratorChangesMessage,
} from '../message-types/get-sync-generator-changes';
import { handleGetSyncGeneratorChanges } from './handle-get-sync-generator-changes';
import { collectAndScheduleSyncGenerators } from './sync-generators';
import {
GET_REGISTERED_SYNC_GENERATORS,
isHandleGetRegisteredSyncGeneratorsMessage,
} from '../message-types/get-registered-sync-generators';
import { handleGetRegisteredSyncGenerators } from './handle-get-registered-sync-generators';
import {
UPDATE_WORKSPACE_CONTEXT,
isHandleUpdateWorkspaceContextMessage,
} from '../message-types/update-workspace-context';
import { handleUpdateWorkspaceContext } from './handle-update-workspace-context';
import {
FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK,
isHandleFlushSyncGeneratorChangesToDiskMessage,
} from '../message-types/flush-sync-generator-changes-to-disk';
import { handleFlushSyncGeneratorChangesToDisk } from './handle-flush-sync-generator-changes-to-disk';
let performanceObserver: PerformanceObserver | undefined;
let workspaceWatcherError: Error | undefined;
@ -225,6 +249,26 @@ async function handleMessage(socket, data: string) {
await handleResult(socket, 'FORCE_SHUTDOWN', () =>
handleForceShutdown(server)
);
} else if (isHandleGetSyncGeneratorChangesMessage(payload)) {
await handleResult(socket, GET_SYNC_GENERATOR_CHANGES, () =>
handleGetSyncGeneratorChanges(payload.generators)
);
} else if (isHandleFlushSyncGeneratorChangesToDiskMessage(payload)) {
await handleResult(socket, FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK, () =>
handleFlushSyncGeneratorChangesToDisk(payload.generators)
);
} else if (isHandleGetRegisteredSyncGeneratorsMessage(payload)) {
await handleResult(socket, GET_REGISTERED_SYNC_GENERATORS, () =>
handleGetRegisteredSyncGenerators()
);
} else if (isHandleUpdateWorkspaceContextMessage(payload)) {
await handleResult(socket, UPDATE_WORKSPACE_CONTEXT, () =>
handleUpdateWorkspaceContext(
payload.createdFiles,
payload.updatedFiles,
payload.deletedFiles
)
);
} else {
await respondWithErrorAndExit(
socket,
@ -482,6 +526,13 @@ export async function startServer(): Promise<Server> {
);
}
// listen for project graph recomputation events to collect and schedule sync generators
registerProjectGraphRecomputationListener(
collectAndScheduleSyncGenerators
);
// trigger an initial project graph recomputation
addUpdatedAndDeletedFiles([], [], []);
return resolve(server);
} catch (err) {
await handleWorkspaceChanges(err, []);

View File

@ -0,0 +1,273 @@
import { readNxJson } from '../../config/nx-json';
import type { ProjectGraph } from '../../config/project-graph';
import type { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { FsTree } from '../../generators/tree';
import { hashArray } from '../../hasher/file-hasher';
import { readProjectsConfigurationFromProjectGraph } from '../../project-graph/project-graph';
import {
collectRegisteredGlobalSyncGenerators,
collectRegisteredTaskSyncGenerators,
flushSyncGeneratorChanges,
runSyncGenerator,
type SyncGeneratorChangesResult,
} from '../../utils/sync-generators';
import { workspaceRoot } from '../../utils/workspace-root';
import { serverLogger } from './logger';
import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation';
const syncGeneratorsCacheResultPromises = new Map<
string,
Promise<SyncGeneratorChangesResult>
>();
let registeredTaskSyncGenerators = new Set<string>();
let registeredGlobalSyncGenerators = new Set<string>();
const scheduledGenerators = new Set<string>();
let waitPeriod = 100;
let registeredSyncGenerators: Set<string> | undefined;
let scheduledTimeoutId: NodeJS.Timeout | undefined;
let storedProjectGraphHash: string | undefined;
let storedNxJsonHash: string | undefined;
const log = (...messageParts: unknown[]) => {
serverLogger.log('[SYNC]:', ...messageParts);
};
// TODO(leo): check conflicts and reuse the Tree where possible
export async function getCachedSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
try {
log('get sync generators changes on demand', generators);
// this is invoked imperatively, so we clear any scheduled run
if (scheduledTimeoutId) {
log('clearing scheduled run');
clearTimeout(scheduledTimeoutId);
scheduledTimeoutId = undefined;
}
// reset the wait time
waitPeriod = 100;
let projects: Record<string, ProjectConfiguration> | null;
let errored = false;
const getProjectsConfigurations = async () => {
if (projects || errored) {
return projects;
}
const { projectGraph, error } =
await getCachedSerializedProjectGraphPromise();
projects = projectGraph
? readProjectsConfigurationFromProjectGraph(projectGraph).projects
: null;
errored = error !== undefined;
return projects;
};
return (
await Promise.all(
generators.map(async (generator) => {
if (
scheduledGenerators.has(generator) ||
!syncGeneratorsCacheResultPromises.has(generator)
) {
// it's scheduled to run (there are pending changes to process) or
// it's not scheduled and there's no cached result, so run it
const projects = await getProjectsConfigurations();
if (projects) {
log(generator, 'already scheduled or not cached, running it now');
runGenerator(generator, projects);
} else {
log(
generator,
'already scheduled or not cached, project graph errored'
);
/**
* This should never happen. This is invoked imperatively, and by
* the time it is invoked, the project graph would have already
* been requested. If it errored, it would have been reported and
* this wouldn't have been invoked. We handle it just in case.
*
* Since the project graph would be reported by the relevant
* handlers separately, we just ignore the error, don't cache
* any result and return an empty result, the next time this is
* invoked the process will repeat until it eventually recovers
* when the project graph is fixed.
*/
return Promise.resolve({ changes: [], generatorName: generator });
}
} else {
log(
generator,
'not scheduled and has cached result, returning cached result'
);
}
return syncGeneratorsCacheResultPromises.get(generator);
})
)
).flat();
} catch (e) {
console.error(e);
syncGeneratorsCacheResultPromises.clear();
return [];
}
}
export async function flushSyncGeneratorChangesToDisk(
generators: string[]
): Promise<void> {
log('flush sync generators changes', generators);
const results = await getCachedSyncGeneratorChanges(generators);
for (const generator of generators) {
syncGeneratorsCacheResultPromises.delete(generator);
}
await flushSyncGeneratorChanges(results);
}
export function collectAndScheduleSyncGenerators(
projectGraph: ProjectGraph
): void {
if (!projectGraph) {
// If the project graph is not available, we can't collect and schedule
// sync generators. The project graph error will be reported separately.
return;
}
log('collect registered sync generators');
collectAllRegisteredSyncGenerators(projectGraph);
// a change imply we need to re-run all the generators
// make sure to schedule all the collected generators
scheduledGenerators.clear();
for (const generator of registeredSyncGenerators) {
scheduledGenerators.add(generator);
}
log('scheduling:', [...scheduledGenerators]);
if (scheduledTimeoutId) {
// we have a scheduled run already, so we don't need to do anything
return;
}
scheduledTimeoutId = setTimeout(async () => {
scheduledTimeoutId = undefined;
if (waitPeriod < 4000) {
waitPeriod = waitPeriod * 2;
}
if (scheduledGenerators.size === 0) {
// no generators to run
return;
}
const { projects } =
readProjectsConfigurationFromProjectGraph(projectGraph);
for (const generator of scheduledGenerators) {
runGenerator(generator, projects);
}
await Promise.all(syncGeneratorsCacheResultPromises.values());
}, waitPeriod);
}
export async function getCachedRegisteredSyncGenerators(): Promise<string[]> {
log('get registered sync generators');
if (!registeredSyncGenerators) {
log('no registered sync generators, collecting them');
const { projectGraph } = await getCachedSerializedProjectGraphPromise();
collectAllRegisteredSyncGenerators(projectGraph);
} else {
log('registered sync generators already collected, returning them');
}
return [...registeredSyncGenerators];
}
function collectAllRegisteredSyncGenerators(projectGraph: ProjectGraph): void {
const projectGraphHash = hashProjectGraph(projectGraph);
if (storedProjectGraphHash !== projectGraphHash) {
storedProjectGraphHash = projectGraphHash;
registeredTaskSyncGenerators =
collectRegisteredTaskSyncGenerators(projectGraph);
} else {
log('project graph hash is the same, not collecting task sync generators');
}
const nxJson = readNxJson();
const nxJsonHash = hashArray(nxJson.sync?.globalGenerators?.sort() ?? []);
if (storedNxJsonHash !== nxJsonHash) {
storedNxJsonHash = nxJsonHash;
registeredGlobalSyncGenerators =
collectRegisteredGlobalSyncGenerators(nxJson);
} else {
log('nx.json hash is the same, not collecting global sync generators');
}
const generators = new Set([
...registeredTaskSyncGenerators,
...registeredGlobalSyncGenerators,
]);
if (!registeredSyncGenerators) {
registeredSyncGenerators = generators;
return;
}
for (const generator of registeredSyncGenerators) {
if (!generators.has(generator)) {
registeredSyncGenerators.delete(generator);
syncGeneratorsCacheResultPromises.delete(generator);
}
}
for (const generator of generators) {
if (!registeredSyncGenerators.has(generator)) {
registeredSyncGenerators.add(generator);
}
}
}
function runGenerator(
generator: string,
projects: Record<string, ProjectConfiguration>
): void {
log('running scheduled generator', generator);
// remove it from the scheduled set
scheduledGenerators.delete(generator);
const tree = new FsTree(
workspaceRoot,
false,
`running sync generator ${generator}`
);
// run the generator and cache the result
syncGeneratorsCacheResultPromises.set(
generator,
runSyncGenerator(tree, generator, projects).then((result) => {
log(generator, 'changes:', result.changes.map((c) => c.path).join(', '));
return result;
})
);
}
function hashProjectGraph(projectGraph: ProjectGraph): string {
const stringifiedProjects = Object.entries(projectGraph.nodes)
.sort(([projectNameA], [projectNameB]) =>
projectNameA.localeCompare(projectNameB)
)
.map(
([projectName, projectConfig]) =>
`${projectName}:${JSON.stringify(projectConfig)}`
);
return hashArray(stringifiedProjects);
}

View File

@ -1,3 +1,5 @@
import { prompt } from 'enquirer';
import * as ora from 'ora';
import { join } from 'path';
import {
NxJsonConfiguration,
@ -11,12 +13,18 @@ import { TargetDependencyConfig } from '../config/workspace-json-project-json';
import { daemonClient } from '../daemon/client/client';
import { createTaskHasher } from '../hasher/create-task-hasher';
import { hashTasksThatDoNotDependOnOutputsOfOtherTasks } from '../hasher/hash-task';
import { createProjectGraphAsync } from '../project-graph/project-graph';
import { NxArgs } from '../utils/command-line-utils';
import { isRelativePath } from '../utils/fileutils';
import { isCI } from '../utils/is-ci';
import { isNxCloudUsed } from '../utils/nx-cloud-utils';
import { output } from '../utils/output';
import { handleErrors } from '../utils/params';
import {
flushSyncGeneratorChanges,
getSyncGeneratorChanges,
syncGeneratorResultsToMessageLines,
} from '../utils/sync-generators';
import { workspaceRoot } from '../utils/workspace-root';
import { createTaskGraph } from './create-task-graph';
import { CompositeLifeCycle, LifeCycle } from './life-cycle';
@ -35,6 +43,7 @@ import {
} from './task-graph-utils';
import { TasksRunner, TaskStatus } from './tasks-runner';
import { shouldStreamOutput } from './utils';
import chalk = require('chalk');
async function getTerminalOutputLifeCycle(
initiatingProject: string,
@ -146,7 +155,7 @@ function createTaskGraphAndRunValidations(
export async function runCommand(
projectsToRun: ProjectGraphProjectNode[],
projectGraph: ProjectGraph,
currentProjectGraph: ProjectGraph,
{ nxJson }: { nxJson: NxJsonConfiguration },
nxArgs: NxArgs,
overrides: any,
@ -159,12 +168,14 @@ export async function runCommand(
async () => {
const projectNames = projectsToRun.map((t) => t.name);
const taskGraph = createTaskGraphAndRunValidations(
projectGraph,
extraTargetDependencies ?? {},
const { projectGraph, taskGraph } =
await ensureWorkspaceIsInSyncAndGetGraphs(
currentProjectGraph,
nxJson,
projectNames,
nxArgs,
overrides,
extraTargetDependencies,
extraOptions
);
const tasks = Object.values(taskGraph.tasks);
@ -198,6 +209,171 @@ export async function runCommand(
return status;
}
async function ensureWorkspaceIsInSyncAndGetGraphs(
projectGraph: ProjectGraph,
nxJson: NxJsonConfiguration,
projectNames: string[],
nxArgs: NxArgs,
overrides: any,
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>,
extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
): Promise<{
projectGraph: ProjectGraph;
taskGraph: TaskGraph;
}> {
let taskGraph = createTaskGraphAndRunValidations(
projectGraph,
extraTargetDependencies ?? {},
projectNames,
nxArgs,
overrides,
extraOptions
);
if (process.env.NX_ENABLE_SYNC_GENERATORS !== 'true') {
return { projectGraph, taskGraph };
}
// collect unique syncGenerators from the tasks
const uniqueSyncGenerators = new Set<string>();
for (const { target } of Object.values(taskGraph.tasks)) {
const { syncGenerators } =
projectGraph.nodes[target.project].data.targets[target.target];
if (!syncGenerators) {
continue;
}
for (const generator of syncGenerators) {
uniqueSyncGenerators.add(generator);
}
}
if (!uniqueSyncGenerators.size) {
// There are no sync generators registered in the tasks to run
return { projectGraph, taskGraph };
}
const syncGenerators = Array.from(uniqueSyncGenerators);
const results = await getSyncGeneratorChanges(syncGenerators);
if (!results.length) {
// There are no changes to sync, workspace is up to date
return { projectGraph, taskGraph };
}
const outOfSyncTitle = 'The workspace is out of sync';
const resultBodyLines = syncGeneratorResultsToMessageLines(results);
const fixMessage =
'You can manually run `nx sync` to update your workspace or you can set `sync.applyChanges` to `true` in your `nx.json` to apply the changes automatically when running tasks.';
const willErrorOnCiMessage = 'Please note that this will be an error on CI.';
if (isCI() || !process.stdout.isTTY) {
// If the user is running in CI or is running in a non-TTY environment we
// throw an error to stop the execution of the tasks.
throw new Error(
`${outOfSyncTitle}\n${resultBodyLines.join('\n')}\n${fixMessage}`
);
}
if (nxJson.sync?.applyChanges === false) {
// If the user has set `sync.applyChanges` to `false` in their `nx.json`
// we don't prompt the them and just log a warning informing them that
// the workspace is out of sync and they have it set to not apply changes
// automatically.
output.warn({
title: outOfSyncTitle,
bodyLines: [
...resultBodyLines,
'Your workspace is set to not apply changes automatically (`sync.applyChanges` is set to `false` in your `nx.json`).',
willErrorOnCiMessage,
fixMessage,
],
});
return { projectGraph, taskGraph };
}
output.warn({
title: outOfSyncTitle,
bodyLines: [
...resultBodyLines,
nxJson.sync?.applyChanges === true
? 'Proceeding to sync the changes automatically (`sync.applyChanges` is set to `true` in your `nx.json`).'
: willErrorOnCiMessage,
],
});
const applyChanges =
nxJson.sync?.applyChanges === true ||
(await promptForApplyingSyncGeneratorChanges());
if (applyChanges) {
const spinner = ora('Syncing the workspace...');
spinner.start();
// Flush sync generator changes to disk
await flushSyncGeneratorChanges(results);
// Re-create project graph and task graph
projectGraph = await createProjectGraphAsync();
taskGraph = createTaskGraphAndRunValidations(
projectGraph,
extraTargetDependencies ?? {},
projectNames,
nxArgs,
overrides,
extraOptions
);
if (nxJson.sync?.applyChanges === true) {
spinner.succeed(`The workspace was synced successfully!
Please make sure to commit the changes to your repository or this will error on CI.`);
} else {
// The user was prompted and we already logged a message about erroring on CI
// so here we just tell them to commit the changes.
spinner.succeed(`The workspace was synced successfully!
Please make sure to commit the changes to your repository.`);
}
} else {
output.warn({
title: 'Syncing the workspace was skipped',
bodyLines: [
'This could lead to unexpected results or errors when running tasks.',
fixMessage,
],
});
}
return { projectGraph, taskGraph };
}
async function promptForApplyingSyncGeneratorChanges(): Promise<boolean> {
const promptConfig = {
name: 'applyChanges',
type: 'select',
message:
'Would you like to sync the changes to get your worskpace up to date?',
choices: [
{
name: 'yes',
message: 'Yes, sync the changes and run the tasks',
},
{
name: 'no',
message: 'No, run the tasks without syncing the changes',
},
],
footer: () =>
chalk.dim(
'\nYou can skip this prompt by setting the `sync.applyChanges` option in your `nx.json`.'
),
};
return await prompt<{ applyChanges: 'yes' | 'no' }>([promptConfig]).then(
({ applyChanges }) => applyChanges === 'yes'
);
}
function setEnvVarsBasedOnArgs(nxArgs: NxArgs, loadDotEnvFiles: boolean) {
if (
nxArgs.outputStyle == 'stream' ||

View File

@ -0,0 +1,258 @@
import { performance } from 'perf_hooks';
import { parseGeneratorString } from '../command-line/generate/generate';
import { getGeneratorInformation } from '../command-line/generate/generator-utils';
import type { GeneratorCallback } from '../config/misc-interfaces';
import { readNxJson } from '../config/nx-json';
import type { ProjectGraph } from '../config/project-graph';
import type { ProjectConfiguration } from '../config/workspace-json-project-json';
import { daemonClient } from '../daemon/client/client';
import { isOnDaemon } from '../daemon/is-on-daemon';
import {
flushChanges,
FsTree,
type FileChange,
type Tree,
} from '../generators/tree';
import {
createProjectGraphAsync,
readProjectsConfigurationFromProjectGraph,
} from '../project-graph/project-graph';
import { updateContextWithChangedFiles } from './workspace-context';
import { workspaceRoot } from './workspace-root';
import chalk = require('chalk');
export type SyncGeneratorResult = void | {
callback?: GeneratorCallback;
outOfSyncMessage?: string;
};
export type SyncGenerator = (
tree: Tree
) => SyncGeneratorResult | Promise<SyncGeneratorResult>;
export type SyncGeneratorChangesResult = {
changes: FileChange[];
generatorName: string;
callback?: GeneratorCallback;
outOfSyncMessage?: string;
};
export async function getSyncGeneratorChanges(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
performance.mark('get-sync-generators-changes:start');
let results: SyncGeneratorChangesResult[];
if (!daemonClient.enabled()) {
results = await runSyncGenerators(generators);
} else {
results = await daemonClient.getSyncGeneratorChanges(generators);
}
performance.mark('get-sync-generators-changes:end');
performance.measure(
'get-sync-generators-changes',
'get-sync-generators-changes:start',
'get-sync-generators-changes:end'
);
return results.filter((r) => r.changes.length > 0);
}
export async function flushSyncGeneratorChanges(
results: SyncGeneratorChangesResult[]
): Promise<void> {
if (isOnDaemon() || !daemonClient.enabled()) {
await flushSyncGeneratorChangesToDisk(results);
} else {
await daemonClient.flushSyncGeneratorChangesToDisk(
results.map((r) => r.generatorName)
);
}
}
export async function collectAllRegisteredSyncGenerators(
projectGraph: ProjectGraph
): Promise<string[]> {
if (!daemonClient.enabled()) {
return [
...collectRegisteredTaskSyncGenerators(projectGraph),
...collectRegisteredGlobalSyncGenerators(),
];
}
return await daemonClient.getRegisteredSyncGenerators();
}
export async function runSyncGenerator(
tree: FsTree,
generatorSpecifier: string,
projects: Record<string, ProjectConfiguration>
): Promise<SyncGeneratorChangesResult> {
performance.mark(`run-sync-generator:${generatorSpecifier}:start`);
const { collection, generator } = parseGeneratorString(generatorSpecifier);
const { implementationFactory } = getGeneratorInformation(
collection,
generator,
workspaceRoot,
projects
);
const implementation = implementationFactory() as SyncGenerator;
const result = await implementation(tree);
let callback: GeneratorCallback | undefined;
let outOfSyncMessage: string | undefined;
if (result && typeof result === 'object') {
callback = result.callback;
outOfSyncMessage = result.outOfSyncMessage;
}
performance.mark(`run-sync-generator:${generatorSpecifier}:end`);
performance.measure(
`run-sync-generator:${generatorSpecifier}`,
`run-sync-generator:${generatorSpecifier}:start`,
`run-sync-generator:${generatorSpecifier}:end`
);
return {
changes: tree.listChanges(),
generatorName: generatorSpecifier,
callback,
outOfSyncMessage,
};
}
export function collectRegisteredTaskSyncGenerators(
projectGraph: ProjectGraph
): Set<string> {
const taskSyncGenerators = new Set<string>();
for (const {
data: { targets },
} of Object.values(projectGraph.nodes)) {
if (!targets) {
continue;
}
for (const target of Object.values(targets)) {
if (!target.syncGenerators) {
continue;
}
for (const generator of target.syncGenerators) {
taskSyncGenerators.add(generator);
}
}
}
return taskSyncGenerators;
}
export function collectRegisteredGlobalSyncGenerators(
nxJson = readNxJson()
): Set<string> {
const globalSyncGenerators = new Set<string>();
if (!nxJson.sync?.globalGenerators?.length) {
return globalSyncGenerators;
}
for (const generator of nxJson.sync.globalGenerators) {
globalSyncGenerators.add(generator);
}
return globalSyncGenerators;
}
export function syncGeneratorResultsToMessageLines(
results: SyncGeneratorChangesResult[]
): string[] {
const messageLines: string[] = [];
for (const result of results) {
messageLines.push(
`The ${chalk.bold(
result.generatorName
)} sync generator identified ${chalk.bold(result.changes.length)} file${
result.changes.length === 1 ? '' : 's'
} in the workspace that ${
result.changes.length === 1 ? 'is' : 'are'
} out of sync${result.outOfSyncMessage ? ':' : '.'}`
);
if (result.outOfSyncMessage) {
messageLines.push(result.outOfSyncMessage);
}
messageLines.push('');
}
return messageLines;
}
async function runSyncGenerators(
generators: string[]
): Promise<SyncGeneratorChangesResult[]> {
const tree = new FsTree(workspaceRoot, false, 'running sync generators');
const projectGraph = await createProjectGraphAsync();
const { projects } = readProjectsConfigurationFromProjectGraph(projectGraph);
const results: SyncGeneratorChangesResult[] = [];
for (const generator of generators) {
const result = await runSyncGenerator(tree, generator, projects);
results.push(result);
}
return results;
}
async function flushSyncGeneratorChangesToDisk(
results: SyncGeneratorChangesResult[]
): Promise<void> {
performance.mark('flush-sync-generator-changes-to-disk:start');
const { changes, createdFiles, updatedFiles, deletedFiles, callbacks } =
processSyncGeneratorResults(results);
// Write changes to disk
flushChanges(workspaceRoot, changes);
// Run the callbacks
if (callbacks.length) {
for (const callback of callbacks) {
await callback();
}
}
// Update the context files
await updateContextWithChangedFiles(createdFiles, updatedFiles, deletedFiles);
performance.mark('flush-sync-generator-changes-to-disk:end');
performance.measure(
'flush sync generator changes to disk',
'flush-sync-generator-changes-to-disk:start',
'flush-sync-generator-changes-to-disk:end'
);
}
function processSyncGeneratorResults(results: SyncGeneratorChangesResult[]) {
const changes: FileChange[] = [];
const createdFiles: string[] = [];
const updatedFiles: string[] = [];
const deletedFiles: string[] = [];
const callbacks: GeneratorCallback[] = [];
for (const result of results) {
if (result.callback) {
callbacks.push(result.callback);
}
for (const change of result.changes) {
changes.push(change);
if (change.type === 'CREATE') {
createdFiles.push(change.path);
} else if (change.type === 'UPDATE') {
updatedFiles.push(change.path);
} else if (change.type === 'DELETE') {
deletedFiles.push(change.path);
}
}
}
return { changes, createdFiles, updatedFiles, deletedFiles, callbacks };
}

View File

@ -74,6 +74,30 @@ export async function hashWithWorkspaceContext(
return daemonClient.hashGlob(globs, exclude);
}
export async function updateContextWithChangedFiles(
createdFiles: string[],
updatedFiles: string[],
deletedFiles: string[]
) {
if (!daemonClient.enabled()) {
updateFilesInContext([...createdFiles, ...updatedFiles], deletedFiles);
} else if (isOnDaemon()) {
// make sure to only import this when running on the daemon
const { addUpdatedAndDeletedFiles } = await import(
'../daemon/server/project-graph-incremental-recomputation'
);
// update files for the incremental graph recomputation on the daemon
addUpdatedAndDeletedFiles(createdFiles, updatedFiles, deletedFiles);
} else {
// daemon is enabled but we are not running on it, ask the daemon to update the context
await daemonClient.updateWorkspaceContext(
createdFiles,
updatedFiles,
deletedFiles
);
}
}
export function updateFilesInContext(
updatedFiles: string[],
deletedFiles: string[]