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:
parent
f208acde54
commit
add5a675c3
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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(/^\.\//, '');
|
||||
}
|
||||
1178
packages/js/src/generators/typescript-sync/typescript-sync.spec.ts
Normal file
1178
packages/js/src/generators/typescript-sync/typescript-sync.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
400
packages/js/src/generators/typescript-sync/typescript-sync.ts
Normal file
400
packages/js/src/generators/typescript-sync/typescript-sync.ts
Normal 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');
|
||||
}
|
||||
@ -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",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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'],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@ export const allowedWorkspaceExtensions = [
|
||||
'useDaemonProcess',
|
||||
'useInferencePlugins',
|
||||
'neverConnectToCloud',
|
||||
'sync',
|
||||
] as const;
|
||||
|
||||
if (!patched) {
|
||||
|
||||
@ -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)
|
||||
|
||||
43
packages/nx/src/command-line/sync/command-object.ts
Normal file
43
packages/nx/src/command-line/sync/command-object.ts
Normal 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 })
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
46
packages/nx/src/command-line/sync/sync.ts
Normal file
46
packages/nx/src/command-line/sync/sync.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, []);
|
||||
|
||||
273
packages/nx/src/daemon/server/sync-generators.ts
Normal file
273
packages/nx/src/daemon/server/sync-generators.ts
Normal 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);
|
||||
}
|
||||
@ -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' ||
|
||||
|
||||
258
packages/nx/src/utils/sync-generators.ts
Normal file
258
packages/nx/src/utils/sync-generators.ts
Normal 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 };
|
||||
}
|
||||
@ -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[]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user