@webpieces/nx-webpieces-rules 0.3.128 ā 0.3.130
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/executors.json +5 -0
- package/package.json +5 -5
- package/src/executors/generate/executor.js +4 -4
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.js +3 -3
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-no-skiplevel-deps/executor.d.ts +12 -6
- package/src/executors/validate-no-skiplevel-deps/executor.js +16 -113
- package/src/executors/validate-no-skiplevel-deps/executor.js.map +1 -1
- package/src/executors/validate-nx-wiring/executor.d.ts +34 -0
- package/src/executors/validate-nx-wiring/executor.js +202 -0
- package/src/executors/validate-nx-wiring/executor.js.map +1 -0
- package/src/executors/validate-nx-wiring/schema.json +19 -0
- package/src/executors/validate-packagejson/executor.js +14 -8
- package/src/executors/validate-packagejson/executor.js.map +1 -1
- package/src/lib/graph-generator.d.ts +27 -8
- package/src/lib/graph-generator.js +48 -53
- package/src/lib/graph-generator.js.map +1 -1
- package/src/lib/package-validator.d.ts +10 -0
- package/src/lib/package-validator.js +12 -5
- package/src/lib/package-validator.js.map +1 -1
- package/src/lib/transitive-reduction.d.ts +25 -0
- package/src/lib/transitive-reduction.js +56 -0
- package/src/lib/transitive-reduction.js.map +1 -0
- package/src/plugin.d.ts +1 -0
- package/src/plugin.js +21 -1
- package/src/plugin.js.map +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"title": "Validate Nx Wiring Executor",
|
|
4
|
+
"description": "Validates that the webpieces validators are wired into the build via nx.json targetDefaults dependsOn, so a build cannot pass while running zero validators.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"requiredDeps": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": { "type": "string" },
|
|
10
|
+
"description": "Target names that each compile executor's dependsOn must include. Defaults: architecture:validate-complete, validate-no-file-import-cycles."
|
|
11
|
+
},
|
|
12
|
+
"compileExecutors": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": { "type": "string" },
|
|
15
|
+
"description": "Compile executors to require wiring on, but only when actually used in the project graph. Defaults: @nx/js:tsc, @angular/build:application."
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"required": []
|
|
19
|
+
}
|
|
@@ -19,17 +19,24 @@ async function runExecutor(options, context) {
|
|
|
19
19
|
console.log('\nš¦ Validating Package.json Dependencies\n');
|
|
20
20
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
21
21
|
try {
|
|
22
|
-
// Step 1:
|
|
23
|
-
console.log(
|
|
24
|
-
const
|
|
22
|
+
// Step 1: Build the full graph from nx, then transitively reduce it (the view)
|
|
23
|
+
console.log("š Generating dependency graph from nx's project graph...");
|
|
24
|
+
const reducedGraph = await (0, graph_generator_1.generateReducedGraph)();
|
|
25
25
|
// Step 2: Topological sort (to get enhanced graph with levels)
|
|
26
26
|
console.log('š Computing topological layers...');
|
|
27
|
-
const enhancedGraph = (0, graph_sorter_1.sortGraphTopologically)(
|
|
27
|
+
const enhancedGraph = (0, graph_sorter_1.sortGraphTopologically)(reducedGraph);
|
|
28
28
|
// Step 3: Validate package.json dependencies match
|
|
29
|
-
console.log('š¦ Validating package.json dependencies match
|
|
29
|
+
console.log('š¦ Validating package.json dependencies match the architecture graph...');
|
|
30
30
|
const packageValidation = await (0, package_validator_1.validatePackageJsonDependencies)(enhancedGraph, workspaceRoot);
|
|
31
|
+
// Warnings never fail the build (e.g. runtime-only / peer deps).
|
|
32
|
+
if (packageValidation.warnings.length > 0) {
|
|
33
|
+
console.warn('\nā ļø Package.json notices (non-fatal):');
|
|
34
|
+
for (const warning of packageValidation.warnings) {
|
|
35
|
+
console.warn(` ${warning}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
31
38
|
if (!packageValidation.valid) {
|
|
32
|
-
console.error('ā Package.json validation failed!');
|
|
39
|
+
console.error('\nā Package.json validation failed!');
|
|
33
40
|
console.error('\nErrors:');
|
|
34
41
|
for (const error of packageValidation.errors) {
|
|
35
42
|
console.error(` ${error}`);
|
|
@@ -37,10 +44,9 @@ async function runExecutor(options, context) {
|
|
|
37
44
|
console.error('\nTo fix:');
|
|
38
45
|
console.error(' 1. Review the missing dependencies above');
|
|
39
46
|
console.error(' 2. Add the missing dependencies to the respective package.json files');
|
|
40
|
-
console.error(' 3. Ensure dependencies in package.json match build.dependsOn in project.json');
|
|
41
47
|
return { success: false };
|
|
42
48
|
}
|
|
43
|
-
console.log('ā
Package.json dependencies
|
|
49
|
+
console.log('ā
Package.json dependencies cover the architecture graph');
|
|
44
50
|
// Print summary
|
|
45
51
|
const validProjects = packageValidation.projectResults.filter(r => r.valid).length;
|
|
46
52
|
const totalProjects = packageValidation.projectResults.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-packagejson/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAgBH,
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-packagejson/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAgBH,8BA0DC;AAvED,+DAAiE;AACjE,yDAAgE;AAChE,mEAA8E;AAC9E,2CAAwC;AAUzB,KAAK,UAAU,WAAW,CACrC,OAAmC,EACnC,OAAwB;IAExB,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;IAE3D,8DAA8D;IAC9D,IAAI,CAAC;QACD,+EAA+E;QAC/E,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;QACzE,MAAM,YAAY,GAAG,MAAM,IAAA,sCAAoB,GAAE,CAAC;QAElD,+DAA+D;QAC/D,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QAClD,MAAM,aAAa,GAAG,IAAA,qCAAsB,EAAC,YAAY,CAAC,CAAC;QAE3D,mDAAmD;QACnD,OAAO,CAAC,GAAG,CAAC,yEAAyE,CAAC,CAAC;QACvF,MAAM,iBAAiB,GAAG,MAAM,IAAA,mDAA+B,EAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QAE9F,iEAAiE;QACjE,IAAI,iBAAiB,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;YACxD,KAAK,MAAM,OAAO,IAAI,iBAAiB,CAAC,QAAQ,EAAE,CAAC;gBAC/C,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;YACjC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;YACrD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,iBAAiB,CAAC,MAAM,EAAE,CAAC;gBAC3C,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;YAChC,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAC5D,OAAO,CAAC,KAAK,CAAC,wEAAwE,CAAC,CAAC;YACxF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,0DAA0D,CAAC,CAAC;QAExE,gBAAgB;QAChB,MAAM,aAAa,GAAG,iBAAiB,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;QACnF,MAAM,aAAa,GAAG,iBAAiB,CAAC,cAAc,CAAC,MAAM,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,0BAA0B,aAAa,EAAE,CAAC,CAAC;QACvD,OAAO,CAAC,GAAG,CAAC,aAAa,aAAa,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,eAAe,aAAa,GAAG,aAAa,EAAE,CAAC,CAAC;QAE5D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAClE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Validate Package.json Executor\n *\n * Validates that package.json dependencies match project.json build dependencies.\n * This ensures the two sources of truth don't drift apart.\n *\n * Usage:\n * nx run architecture:validate-packagejson\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { validatePackageJsonDependencies } from '../../lib/package-validator';\nimport { toError } from '../../toError';\n\nexport interface ValidatePackageJsonOptions {\n // No options needed for now\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nexport default async function runExecutor(\n options: ValidatePackageJsonOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n\n console.log('\\nš¦ Validating Package.json Dependencies\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Build the full graph from nx, then transitively reduce it (the view)\n console.log(\"š Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 2: Topological sort (to get enhanced graph with levels)\n console.log('š Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n\n // Step 3: Validate package.json dependencies match\n console.log('š¦ Validating package.json dependencies match the architecture graph...');\n const packageValidation = await validatePackageJsonDependencies(enhancedGraph, workspaceRoot);\n\n // Warnings never fail the build (e.g. runtime-only / peer deps).\n if (packageValidation.warnings.length > 0) {\n console.warn('\\nā ļø Package.json notices (non-fatal):');\n for (const warning of packageValidation.warnings) {\n console.warn(` ${warning}`);\n }\n }\n\n if (!packageValidation.valid) {\n console.error('\\nā Package.json validation failed!');\n console.error('\\nErrors:');\n for (const error of packageValidation.errors) {\n console.error(` ${error}`);\n }\n console.error('\\nTo fix:');\n console.error(' 1. Review the missing dependencies above');\n console.error(' 2. Add the missing dependencies to the respective package.json files');\n return { success: false };\n }\n\n console.log('ā
Package.json dependencies cover the architecture graph');\n\n // Print summary\n const validProjects = packageValidation.projectResults.filter(r => r.valid).length;\n const totalProjects = packageValidation.projectResults.length;\n console.log(`\\nš Validation Summary:`);\n console.log(` Projects validated: ${totalProjects}`);\n console.log(` Valid: ${validProjects}`);\n console.log(` Invalid: ${totalProjects - validProjects}`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('ā Package.json validation failed:', error.message);\n return { success: false };\n }\n}\n"]}
|
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Graph Generator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Builds the workspace dependency graph from nx's OWN project graph
|
|
5
|
+
* (createProjectGraphAsync). nx already derives those edges from BOTH source
|
|
6
|
+
* imports AND package.json workspace deps, so there is no hand-maintained edge
|
|
7
|
+
* list and no separate import scan ā we consume what nx already computed.
|
|
8
|
+
*
|
|
9
|
+
* - generateGraph() ā the FULL graph (every workspace edge nx knows).
|
|
10
|
+
* This is what build order follows via `^build`.
|
|
11
|
+
* - generateReducedGraph() ā transitive reduction of the full graph: the minimal
|
|
12
|
+
* edge set with identical reachability, used as the
|
|
13
|
+
* architecture VIEW written to dependencies.json.
|
|
6
14
|
*/
|
|
7
15
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
16
|
+
* Build the full dependency graph from nx's project graph.
|
|
17
|
+
*
|
|
18
|
+
* nx's `projectGraph.nodes` are the workspace projects; `projectGraph.dependencies`
|
|
19
|
+
* holds every edge nx inferred (imports + package.json). We keep only edges whose
|
|
20
|
+
* target is another workspace project (dropping `npm:` externals) and drop excluded
|
|
21
|
+
* projects.
|
|
22
|
+
*
|
|
23
|
+
* Returns: { projectName: [workspaceDependencyNames] } (deps sorted, deduped)
|
|
10
24
|
*/
|
|
11
25
|
export declare function generateRawGraph(): Promise<Record<string, string[]>>;
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
27
|
+
* The full workspace dependency graph (every edge nx knows).
|
|
14
28
|
*/
|
|
15
|
-
export declare function
|
|
29
|
+
export declare function generateGraph(): Promise<Record<string, string[]>>;
|
|
16
30
|
/**
|
|
17
|
-
*
|
|
31
|
+
* The transitively-reduced view of the full graph. This is the canonical
|
|
32
|
+
* "architecture graph" written to and validated against dependencies.json.
|
|
33
|
+
*
|
|
34
|
+
* Reduction is undefined on cycles; callers that care about cycles
|
|
35
|
+
* (validate-no-architecture-cycles) run on the FULL graph and throw first. For the
|
|
36
|
+
* view executors, the downstream topological sort also throws on any cycle.
|
|
18
37
|
*/
|
|
19
|
-
export declare function
|
|
38
|
+
export declare function generateReducedGraph(): Promise<Record<string, string[]>>;
|
|
@@ -2,83 +2,78 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Graph Generator
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Builds the workspace dependency graph from nx's OWN project graph
|
|
6
|
+
* (createProjectGraphAsync). nx already derives those edges from BOTH source
|
|
7
|
+
* imports AND package.json workspace deps, so there is no hand-maintained edge
|
|
8
|
+
* list and no separate import scan ā we consume what nx already computed.
|
|
9
|
+
*
|
|
10
|
+
* - generateGraph() ā the FULL graph (every workspace edge nx knows).
|
|
11
|
+
* This is what build order follows via `^build`.
|
|
12
|
+
* - generateReducedGraph() ā transitive reduction of the full graph: the minimal
|
|
13
|
+
* edge set with identical reachability, used as the
|
|
14
|
+
* architecture VIEW written to dependencies.json.
|
|
7
15
|
*/
|
|
8
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
17
|
exports.generateRawGraph = generateRawGraph;
|
|
10
|
-
exports.transformGraph = transformGraph;
|
|
11
18
|
exports.generateGraph = generateGraph;
|
|
19
|
+
exports.generateReducedGraph = generateReducedGraph;
|
|
12
20
|
const devkit_1 = require("@nx/devkit");
|
|
21
|
+
const transitive_reduction_1 = require("./transitive-reduction");
|
|
13
22
|
/**
|
|
14
23
|
* Projects to exclude from graph validation (tools, configs, etc.)
|
|
15
24
|
*/
|
|
16
25
|
const EXCLUDED_PROJECTS = new Set(['architecture']);
|
|
17
26
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (typeof dep === 'string') {
|
|
27
|
-
// Format: "project-name:build" or just "build" (for self)
|
|
28
|
-
const match = dep.match(/^([^:]+):build$/);
|
|
29
|
-
if (match) {
|
|
30
|
-
deps.push(match[1]);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
// 2. Also read from implicitDependencies
|
|
36
|
-
if (projectConfig.implicitDependencies && Array.isArray(projectConfig.implicitDependencies)) {
|
|
37
|
-
for (const dep of projectConfig.implicitDependencies) {
|
|
38
|
-
if (typeof dep === 'string' && !deps.includes(dep)) {
|
|
39
|
-
deps.push(dep);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return deps.sort();
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Generate raw dependency graph from project.json files
|
|
47
|
-
* Returns: { projectName: [dependencyNames] }
|
|
27
|
+
* Build the full dependency graph from nx's project graph.
|
|
28
|
+
*
|
|
29
|
+
* nx's `projectGraph.nodes` are the workspace projects; `projectGraph.dependencies`
|
|
30
|
+
* holds every edge nx inferred (imports + package.json). We keep only edges whose
|
|
31
|
+
* target is another workspace project (dropping `npm:` externals) and drop excluded
|
|
32
|
+
* projects.
|
|
33
|
+
*
|
|
34
|
+
* Returns: { projectName: [workspaceDependencyNames] } (deps sorted, deduped)
|
|
48
35
|
*/
|
|
49
36
|
async function generateRawGraph() {
|
|
50
37
|
const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
|
|
51
|
-
const
|
|
38
|
+
const workspaceProjects = new Set(Object.keys(projectGraph.nodes));
|
|
52
39
|
const rawDeps = {};
|
|
53
|
-
for (const
|
|
54
|
-
// Skip excluded projects (tools, plugins)
|
|
40
|
+
for (const projectName of workspaceProjects) {
|
|
55
41
|
if (EXCLUDED_PROJECTS.has(projectName)) {
|
|
56
42
|
continue;
|
|
57
43
|
}
|
|
58
|
-
|
|
59
|
-
const deps =
|
|
60
|
-
|
|
44
|
+
const edges = projectGraph.dependencies[projectName] ?? [];
|
|
45
|
+
const deps = new Set();
|
|
46
|
+
for (const edge of edges) {
|
|
47
|
+
const target = edge.target;
|
|
48
|
+
// Keep only workspaceāworkspace edges; skip self and excluded projects.
|
|
49
|
+
if (target === projectName)
|
|
50
|
+
continue;
|
|
51
|
+
if (!workspaceProjects.has(target))
|
|
52
|
+
continue; // drops npm: externals
|
|
53
|
+
if (EXCLUDED_PROJECTS.has(target))
|
|
54
|
+
continue;
|
|
55
|
+
deps.add(target);
|
|
56
|
+
}
|
|
57
|
+
rawDeps[projectName] = Array.from(deps).sort();
|
|
61
58
|
}
|
|
62
59
|
return rawDeps;
|
|
63
60
|
}
|
|
64
61
|
/**
|
|
65
|
-
*
|
|
62
|
+
* The full workspace dependency graph (every edge nx knows).
|
|
66
63
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
for (const [projectName, deps] of Object.entries(rawGraph)) {
|
|
70
|
-
// Use project names as-is - don't force @webpieces scope on client projects
|
|
71
|
-
const transformedName = projectName;
|
|
72
|
-
const transformedDeps = deps.sort();
|
|
73
|
-
result[transformedName] = transformedDeps;
|
|
74
|
-
}
|
|
75
|
-
return result;
|
|
64
|
+
async function generateGraph() {
|
|
65
|
+
return generateRawGraph();
|
|
76
66
|
}
|
|
77
67
|
/**
|
|
78
|
-
*
|
|
68
|
+
* The transitively-reduced view of the full graph. This is the canonical
|
|
69
|
+
* "architecture graph" written to and validated against dependencies.json.
|
|
70
|
+
*
|
|
71
|
+
* Reduction is undefined on cycles; callers that care about cycles
|
|
72
|
+
* (validate-no-architecture-cycles) run on the FULL graph and throw first. For the
|
|
73
|
+
* view executors, the downstream topological sort also throws on any cycle.
|
|
79
74
|
*/
|
|
80
|
-
async function
|
|
81
|
-
const
|
|
82
|
-
return
|
|
75
|
+
async function generateReducedGraph() {
|
|
76
|
+
const full = await generateGraph();
|
|
77
|
+
return (0, transitive_reduction_1.transitiveReduction)(full);
|
|
83
78
|
}
|
|
84
79
|
//# sourceMappingURL=graph-generator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-generator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-generator.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"graph-generator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-generator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AAoBH,4CA0BC;AAKD,sCAEC;AAUD,oDAGC;AAhED,uCAAqD;AACrD,iEAA6D;AAE7D;;GAEG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAS,CAAC,cAAc,CAAC,CAAC,CAAC;AAE5D;;;;;;;;;GASG;AACI,KAAK,UAAU,gBAAgB;IAClC,MAAM,YAAY,GAAG,MAAM,IAAA,gCAAuB,GAAE,CAAC;IACrD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IAEnE,MAAM,OAAO,GAA6B,EAAE,CAAC;IAE7C,KAAK,MAAM,WAAW,IAAI,iBAAiB,EAAE,CAAC;QAC1C,IAAI,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YACrC,SAAS;QACb,CAAC;QAED,MAAM,KAAK,GAAG,YAAY,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC3D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YAC3B,wEAAwE;YACxE,IAAI,MAAM,KAAK,WAAW;gBAAE,SAAS;YACrC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,SAAS,CAAC,uBAAuB;YACrE,IAAI,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,SAAS;YAC5C,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QAED,OAAO,CAAC,WAAW,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACnD,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,aAAa;IAC/B,OAAO,gBAAgB,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,oBAAoB;IACtC,MAAM,IAAI,GAAG,MAAM,aAAa,EAAE,CAAC;IACnC,OAAO,IAAA,0CAAmB,EAAC,IAAI,CAAC,CAAC;AACrC,CAAC","sourcesContent":["/**\n * Graph Generator\n *\n * Builds the workspace dependency graph from nx's OWN project graph\n * (createProjectGraphAsync). nx already derives those edges from BOTH source\n * imports AND package.json workspace deps, so there is no hand-maintained edge\n * list and no separate import scan ā we consume what nx already computed.\n *\n * - generateGraph() ā the FULL graph (every workspace edge nx knows).\n * This is what build order follows via `^build`.\n * - generateReducedGraph() ā transitive reduction of the full graph: the minimal\n * edge set with identical reachability, used as the\n * architecture VIEW written to dependencies.json.\n */\n\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport { transitiveReduction } from './transitive-reduction';\n\n/**\n * Projects to exclude from graph validation (tools, configs, etc.)\n */\nconst EXCLUDED_PROJECTS = new Set<string>(['architecture']);\n\n/**\n * Build the full dependency graph from nx's project graph.\n *\n * nx's `projectGraph.nodes` are the workspace projects; `projectGraph.dependencies`\n * holds every edge nx inferred (imports + package.json). We keep only edges whose\n * target is another workspace project (dropping `npm:` externals) and drop excluded\n * projects.\n *\n * Returns: { projectName: [workspaceDependencyNames] } (deps sorted, deduped)\n */\nexport async function generateRawGraph(): Promise<Record<string, string[]>> {\n const projectGraph = await createProjectGraphAsync();\n const workspaceProjects = new Set(Object.keys(projectGraph.nodes));\n\n const rawDeps: Record<string, string[]> = {};\n\n for (const projectName of workspaceProjects) {\n if (EXCLUDED_PROJECTS.has(projectName)) {\n continue;\n }\n\n const edges = projectGraph.dependencies[projectName] ?? [];\n const deps = new Set<string>();\n for (const edge of edges) {\n const target = edge.target;\n // Keep only workspaceāworkspace edges; skip self and excluded projects.\n if (target === projectName) continue;\n if (!workspaceProjects.has(target)) continue; // drops npm: externals\n if (EXCLUDED_PROJECTS.has(target)) continue;\n deps.add(target);\n }\n\n rawDeps[projectName] = Array.from(deps).sort();\n }\n\n return rawDeps;\n}\n\n/**\n * The full workspace dependency graph (every edge nx knows).\n */\nexport async function generateGraph(): Promise<Record<string, string[]>> {\n return generateRawGraph();\n}\n\n/**\n * The transitively-reduced view of the full graph. This is the canonical\n * \"architecture graph\" written to and validated against dependencies.json.\n *\n * Reduction is undefined on cycles; callers that care about cycles\n * (validate-no-architecture-cycles) run on the FULL graph and throw first. For the\n * view executors, the downstream topological sort also throws on any cycle.\n */\nexport async function generateReducedGraph(): Promise<Record<string, string[]>> {\n const full = await generateGraph();\n return transitiveReduction(full);\n}\n"]}
|
|
@@ -15,10 +15,20 @@ export interface ProjectValidationResult {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Overall validation result
|
|
18
|
+
*
|
|
19
|
+
* `errors` fail the build; their only fix is ADDITIVE ("add to package.json"), so they can
|
|
20
|
+
* never push a user toward removing a runtime-required dependency.
|
|
21
|
+
*
|
|
22
|
+
* `warnings` never fail the build. Workspace deps in package.json that the architecture graph
|
|
23
|
+
* can't reach are reported here, NOT as errors: a transitively-reachable or even unreachable
|
|
24
|
+
* entry can still be a real runtime dependency (e.g. a peerDependency or a generated client
|
|
25
|
+
* that nx's import analysis doesn't traverse). Erroring on these is the "runtime-validity trap"
|
|
26
|
+
* that previously forced a bad package.json edit ā so we only warn.
|
|
18
27
|
*/
|
|
19
28
|
export interface ValidationResult {
|
|
20
29
|
valid: boolean;
|
|
21
30
|
errors: string[];
|
|
31
|
+
warnings: string[];
|
|
22
32
|
projectResults: ProjectValidationResult[];
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
@@ -132,13 +132,17 @@ function validateSingleProject(projectName, entry, projectRoot, packageJsonDeps,
|
|
|
132
132
|
errors.push(`Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${classification.missingInPackageJson.join(', ')}\n` +
|
|
133
133
|
` Fix: Add these to package.json dependencies`);
|
|
134
134
|
}
|
|
135
|
+
// Unreachable workspace extras are WARN-ONLY: they may be real runtime deps that nx's
|
|
136
|
+
// import analysis can't see (peerDependency / generated client). Never error ā that is
|
|
137
|
+
// the runtime-validity trap. We surface them so genuine drift is still visible.
|
|
138
|
+
const warnings = [];
|
|
135
139
|
for (const extraPkg of classification.extraWorkspaceDeps) {
|
|
136
140
|
const extraProject = packageToProject.get(extraPkg);
|
|
137
|
-
|
|
138
|
-
`
|
|
141
|
+
warnings.push(`Project ${projectName} (${projectRoot}/package.json) has "${extraPkg}" but the architecture graph has no path ${projectName} ā ${extraProject}.\n` +
|
|
142
|
+
` This is allowed (it may be a runtime-only/peer dependency). If it is genuinely unused, you may remove it.`);
|
|
139
143
|
}
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
// extraWorkspaceDeps do NOT affect validity ā only missing (additive-fix) errors do.
|
|
145
|
+
const valid = classification.missingInPackageJson.length === 0;
|
|
142
146
|
return {
|
|
143
147
|
result: {
|
|
144
148
|
project: projectName,
|
|
@@ -147,6 +151,7 @@ function validateSingleProject(projectName, entry, projectRoot, packageJsonDeps,
|
|
|
147
151
|
extraInPackageJson: classification.extraInPackageJson,
|
|
148
152
|
},
|
|
149
153
|
errors,
|
|
154
|
+
warnings,
|
|
150
155
|
};
|
|
151
156
|
}
|
|
152
157
|
async function validatePackageJsonDependencies(graph, workspaceRoot) {
|
|
@@ -158,6 +163,7 @@ async function validatePackageJsonDependencies(graph, workspaceRoot) {
|
|
|
158
163
|
packageToProject.set(pkgName, projName);
|
|
159
164
|
}
|
|
160
165
|
const errors = [];
|
|
166
|
+
const warnings = [];
|
|
161
167
|
const projectResults = [];
|
|
162
168
|
for (const [projectName, entry] of Object.entries(graph)) {
|
|
163
169
|
const projectConfig = projectsConfig.projects[projectName];
|
|
@@ -169,7 +175,8 @@ async function validatePackageJsonDependencies(graph, workspaceRoot) {
|
|
|
169
175
|
const validation = validateSingleProject(projectName, entry, projectConfig.root, packageJsonDeps, graph, projectToPackage, packageToProject);
|
|
170
176
|
projectResults.push(validation.result);
|
|
171
177
|
errors.push(...validation.errors);
|
|
178
|
+
warnings.push(...validation.warnings);
|
|
172
179
|
}
|
|
173
|
-
return { valid: errors.length === 0, errors, projectResults };
|
|
180
|
+
return { valid: errors.length === 0, errors, warnings, projectResults };
|
|
174
181
|
}
|
|
175
182
|
//# sourceMappingURL=package-validator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-validator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/package-validator.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AA0OH,0EAqCC;;AA7QD,+CAAyB;AACzB,mDAA6B;AAC7B,uCAGoB;AAsBpB;;;GAGG;AACH,SAAS,mBAAmB,CAAC,aAAqB,EAAE,WAAmB;IACnE,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;IAE9E,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC,CAAC,qDAAqD;IACtE,CAAC;IAED,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC;QAC1E,MAAM,IAAI,GAAa,EAAE,CAAC;QAE1B,6CAA6C;QAC7C,KAAK,MAAM,OAAO,IAAI,CAAC,cAAc,EAAE,kBAAkB,CAAC,EAAE,CAAC;YACzD,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAC1C,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC1B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,CAAC;YACL,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC;QACT,OAAO,CAAC,IAAI,CAAC,kCAAkC,eAAe,EAAE,CAAC,CAAC;QAClE,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,wBAAwB,CAC7B,aAAqB;AACrB,sGAAsG;AACtG,cAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEtC,oFAAoF;IACpF,KAAK,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAM,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/E,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QAC9E,IAAI,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YACjC,8DAA8D;YAC9D,IAAI,CAAC;gBACD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1E,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC;oBACnB,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;gBAC3C,CAAC;YACL,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,6BAA6B;gBAC7B,KAAK,GAAG,CAAC;gBACT,sBAAsB;YAC1B,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC;AACf,CAAC;AAwBD;;;;;;;;;GASG;AACH,SAAS,wBAAwB,CAC7B,WAAmB,EACnB,KAAiC;IAEjC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,KAAK,GAAG,CAAC,WAAW,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACpB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAOD,SAAS,YAAY,CACjB,eAAyB,EACzB,KAAiB,EACjB,iBAA8B,EAC9B,gBAAqC,EACrC,gBAAqC;IAErC,MAAM,oBAAoB,GAAa,EAAE,CAAC;IAC1C,KAAK,MAAM,cAAc,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAC3C,MAAM,cAAc,GAAG,gBAAgB,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,cAAc,CAAC;QAC9E,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YAC5C,oBAAoB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC9C,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,0EAA0E;IAC1E,qEAAqE;IACrE,MAAM,kBAAkB,GAAa,EAAE,CAAC;IACxC,MAAM,kBAAkB,GAAa,EAAE,CAAC;IACxC,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAChC,MAAM,cAAc,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;YAC/B,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC7B,SAAS;QACb,CAAC;QACD,IAAI,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,SAAS;QACvD,IAAI,iBAAiB,CAAC,GAAG,CAAC,cAAc,CAAC;YAAE,SAAS;QACpD,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,CAAC;AAC5E,CAAC;AAED,SAAS,qBAAqB,CAC1B,WAAmB,EACnB,KAAiB,EACjB,WAAmB,EACnB,eAAyB,EACzB,KAAiC,EACjC,gBAAqC,EACrC,gBAAqC;IAErC,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACvE,MAAM,cAAc,GAAG,YAAY,CAC/B,eAAe,EACf,KAAK,EACL,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,CACnB,CAAC;IAEF,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,cAAc,CAAC,oBAAoB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,CAAC,IAAI,CACP,WAAW,WAAW,KAAK,WAAW,2CAA2C,cAAc,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC/H,+CAA+C,CACtD,CAAC;IACN,CAAC;IACD,KAAK,MAAM,QAAQ,IAAI,cAAc,CAAC,kBAAkB,EAAE,CAAC;QACvD,MAAM,YAAY,GAAG,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpD,MAAM,CAAC,IAAI,CACP,WAAW,WAAW,KAAK,WAAW,uBAAuB,QAAQ,oEAAoE,WAAW,MAAM,YAAY,6BAA6B;YAC/L,sBAAsB,YAAY,mGAAmG,QAAQ,mCAAmC,CACvL,CAAC;IACN,CAAC;IAED,MAAM,KAAK,GACP,cAAc,CAAC,oBAAoB,CAAC,MAAM,KAAK,CAAC;QAChD,cAAc,CAAC,kBAAkB,CAAC,MAAM,KAAK,CAAC,CAAC;IACnD,OAAO;QACH,MAAM,EAAE;YACJ,OAAO,EAAE,WAAW;YACpB,KAAK;YACL,oBAAoB,EAAE,cAAc,CAAC,oBAAoB;YACzD,kBAAkB,EAAE,cAAc,CAAC,kBAAkB;SACxD;QACD,MAAM;KACT,CAAC;AACN,CAAC;AAEM,KAAK,UAAU,+BAA+B,CACjD,KAAiC,EACjC,aAAqB;IAErB,MAAM,YAAY,GAAG,MAAM,IAAA,gCAAuB,GAAE,CAAC;IACrD,MAAM,cAAc,GAAG,IAAA,kDAAyC,EAAC,YAAY,CAAC,CAAC;IAE/E,MAAM,gBAAgB,GAAG,wBAAwB,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IACjF,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACnD,KAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,gBAAgB,CAAC,OAAO,EAAE,EAAE,CAAC;QAC3D,gBAAgB,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,cAAc,GAA8B,EAAE,CAAC;IAErD,KAAK,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,MAAM,aAAa,GAAG,cAAc,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC3D,IAAI,CAAC,aAAa;YAAE,SAAS;QAE7B,MAAM,eAAe,GAAG,mBAAmB,CAAC,aAAa,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;QAC/E,IAAI,eAAe,KAAK,IAAI;YAAE,SAAS;QAEvC,MAAM,UAAU,GAAG,qBAAqB,CACpC,WAAW,EACX,KAAK,EACL,aAAa,CAAC,IAAI,EAClB,eAAe,EACf,KAAK,EACL,gBAAgB,EAChB,gBAAgB,CACnB,CAAC;QACF,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;AAClE,CAAC","sourcesContent":["/**\n * Package Validator\n *\n * Validates that package.json dependencies match the project.json build.dependsOn\n * This ensures the two sources of truth don't drift apart.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n createProjectGraphAsync,\n readProjectsConfigurationFromProjectGraph,\n} from '@nx/devkit';\nimport { toError } from '../toError';\n\n/**\n * Validation result for a single project\n */\nexport interface ProjectValidationResult {\n project: string;\n valid: boolean;\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n}\n\n/**\n * Overall validation result\n */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n projectResults: ProjectValidationResult[];\n}\n\n/**\n * Read package.json dependencies for a project\n * Returns null if package.json doesn't exist (apps often don't have one)\n */\nfunction readPackageJsonDeps(workspaceRoot: string, projectRoot: string): string[] | null {\n const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');\n\n if (!fs.existsSync(packageJsonPath)) {\n return null; // No package.json - skip validation for this project\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const deps: string[] = [];\n\n // Collect ALL dependencies from package.json\n for (const depType of ['dependencies', 'peerDependencies']) {\n const depObj = packageJson[depType] || {};\n for (const depName of Object.keys(depObj)) {\n if (!deps.includes(depName)) {\n deps.push(depName);\n }\n }\n }\n\n return deps.sort();\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n console.warn(`Could not read package.json at ${packageJsonPath}`);\n return [];\n }\n}\n\n/**\n * Build map of project names to their package names\n * e.g., \"core-util\" ā \"@webpieces/core-util\"\n */\nfunction buildProjectToPackageMap(\n workspaceRoot: string,\n // webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed\n projectsConfig: any\n): Map<string, string> {\n const map = new Map<string, string>();\n\n // webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped\n for (const [projectName, config] of Object.entries<any>(projectsConfig.projects)) {\n const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');\n if (fs.existsSync(packageJsonPath)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n if (packageJson.name) {\n map.set(projectName, packageJson.name);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n // Ignore parse errors\n }\n }\n }\n\n return map;\n}\n\n/**\n * Validate that package.json dependencies match the dependency graph\n *\n * For each project in the graph:\n * - Check that all graph dependencies exist in package.json\n * - Maps project names to package names for accurate comparison\n *\n * @param graph - Enhanced graph with project dependencies (uses project names)\n * @param workspaceRoot - Absolute path to workspace root\n * @returns Validation result with errors if any\n */\ninterface GraphEntry {\n level: number;\n dependsOn: string[];\n}\n\ninterface DepClassification {\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n extraWorkspaceDeps: string[];\n}\n\n/**\n * Compute the transitive closure of a project's dependencies in the graph.\n * Example: server ā [core-meta, http-server]; transitive closure includes\n * http-server and everything http-server reaches (http-routing, http-filters,\n * core-context, core-util, http-api).\n *\n * Used to allow package.json entries for transitive deps (a legitimate pattern:\n * npm install brings the whole dependency tree, so a consumer may list any\n * reachable package directly).\n */\nfunction computeTransitiveClosure(\n projectName: string,\n graph: Record<string, GraphEntry>\n): Set<string> {\n const closure = new Set<string>();\n const stack = [projectName];\n while (stack.length > 0) {\n const current = stack.pop()!;\n const entry = graph[current];\n if (!entry) continue;\n for (const dep of entry.dependsOn) {\n if (!closure.has(dep)) {\n closure.add(dep);\n stack.push(dep);\n }\n }\n }\n return closure;\n}\n\ninterface SingleProjectValidation {\n result: ProjectValidationResult;\n errors: string[];\n}\n\nfunction classifyDeps(\n packageJsonDeps: string[],\n entry: GraphEntry,\n transitiveClosure: Set<string>,\n projectToPackage: Map<string, string>,\n packageToProject: Map<string, string>\n): DepClassification {\n const missingInPackageJson: string[] = [];\n for (const depProjectName of entry.dependsOn) {\n const depPackageName = projectToPackage.get(depProjectName) || depProjectName;\n if (!packageJsonDeps.includes(depPackageName)) {\n missingInPackageJson.push(depProjectName);\n }\n }\n\n // Workspace extras are OK if reachable via transitive closure (matches the\n // ESLint enforce-architecture rule which also allows transitive imports).\n // Only flag extras that are NOT reachable at all ā real graph drift.\n const extraInPackageJson: string[] = [];\n const extraWorkspaceDeps: string[] = [];\n for (const dep of packageJsonDeps) {\n const depProjectName = packageToProject.get(dep);\n if (depProjectName === undefined) {\n extraInPackageJson.push(dep);\n continue;\n }\n if (entry.dependsOn.includes(depProjectName)) continue;\n if (transitiveClosure.has(depProjectName)) continue;\n extraWorkspaceDeps.push(dep);\n }\n\n return { missingInPackageJson, extraInPackageJson, extraWorkspaceDeps };\n}\n\nfunction validateSingleProject(\n projectName: string,\n entry: GraphEntry,\n projectRoot: string,\n packageJsonDeps: string[],\n graph: Record<string, GraphEntry>,\n projectToPackage: Map<string, string>,\n packageToProject: Map<string, string>\n): SingleProjectValidation {\n const transitiveClosure = computeTransitiveClosure(projectName, graph);\n const classification = classifyDeps(\n packageJsonDeps,\n entry,\n transitiveClosure,\n projectToPackage,\n packageToProject\n );\n\n const errors: string[] = [];\n if (classification.missingInPackageJson.length > 0) {\n errors.push(\n `Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${classification.missingInPackageJson.join(', ')}\\n` +\n ` Fix: Add these to package.json dependencies`\n );\n }\n for (const extraPkg of classification.extraWorkspaceDeps) {\n const extraProject = packageToProject.get(extraPkg);\n errors.push(\n `Project ${projectName} (${projectRoot}/package.json) has \"${extraPkg}\" in package.json but architecture/dependencies.json has no path ${projectName} ā ${extraProject} (not even transitively).\\n` +\n ` Fix: Either add \"${extraProject}:build\" to project.json:build.dependsOn (then run \\`nx run architecture:generate\\`), or remove \"${extraPkg}\" from package.json dependencies.`\n );\n }\n\n const valid =\n classification.missingInPackageJson.length === 0 &&\n classification.extraWorkspaceDeps.length === 0;\n return {\n result: {\n project: projectName,\n valid,\n missingInPackageJson: classification.missingInPackageJson,\n extraInPackageJson: classification.extraInPackageJson,\n },\n errors,\n };\n}\n\nexport async function validatePackageJsonDependencies(\n graph: Record<string, GraphEntry>,\n workspaceRoot: string\n): Promise<ValidationResult> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);\n const packageToProject = new Map<string, string>();\n for (const [projName, pkgName] of projectToPackage.entries()) {\n packageToProject.set(pkgName, projName);\n }\n\n const errors: string[] = [];\n const projectResults: ProjectValidationResult[] = [];\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const projectConfig = projectsConfig.projects[projectName];\n if (!projectConfig) continue;\n\n const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectConfig.root);\n if (packageJsonDeps === null) continue;\n\n const validation = validateSingleProject(\n projectName,\n entry,\n projectConfig.root,\n packageJsonDeps,\n graph,\n projectToPackage,\n packageToProject\n );\n projectResults.push(validation.result);\n errors.push(...validation.errors);\n }\n\n return { valid: errors.length === 0, errors, projectResults };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"package-validator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/package-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Package Validator\n *\n * Validates that package.json dependencies match the project.json build.dependsOn\n * This ensures the two sources of truth don't drift apart.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n createProjectGraphAsync,\n readProjectsConfigurationFromProjectGraph,\n} from '@nx/devkit';\nimport { toError } from '../toError';\n\n/**\n * Validation result for a single project\n */\nexport interface ProjectValidationResult {\n project: string;\n valid: boolean;\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n}\n\n/**\n * Overall validation result\n *\n * `errors` fail the build; their only fix is ADDITIVE (\"add to package.json\"), so they can\n * never push a user toward removing a runtime-required dependency.\n *\n * `warnings` never fail the build. Workspace deps in package.json that the architecture graph\n * can't reach are reported here, NOT as errors: a transitively-reachable or even unreachable\n * entry can still be a real runtime dependency (e.g. a peerDependency or a generated client\n * that nx's import analysis doesn't traverse). Erroring on these is the \"runtime-validity trap\"\n * that previously forced a bad package.json edit ā so we only warn.\n */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n projectResults: ProjectValidationResult[];\n}\n\n/**\n * Read package.json dependencies for a project\n * Returns null if package.json doesn't exist (apps often don't have one)\n */\nfunction readPackageJsonDeps(workspaceRoot: string, projectRoot: string): string[] | null {\n const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');\n\n if (!fs.existsSync(packageJsonPath)) {\n return null; // No package.json - skip validation for this project\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const deps: string[] = [];\n\n // Collect ALL dependencies from package.json\n for (const depType of ['dependencies', 'peerDependencies']) {\n const depObj = packageJson[depType] || {};\n for (const depName of Object.keys(depObj)) {\n if (!deps.includes(depName)) {\n deps.push(depName);\n }\n }\n }\n\n return deps.sort();\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n console.warn(`Could not read package.json at ${packageJsonPath}`);\n return [];\n }\n}\n\n/**\n * Build map of project names to their package names\n * e.g., \"core-util\" ā \"@webpieces/core-util\"\n */\nfunction buildProjectToPackageMap(\n workspaceRoot: string,\n // webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed\n projectsConfig: any\n): Map<string, string> {\n const map = new Map<string, string>();\n\n // webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped\n for (const [projectName, config] of Object.entries<any>(projectsConfig.projects)) {\n const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');\n if (fs.existsSync(packageJsonPath)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n if (packageJson.name) {\n map.set(projectName, packageJson.name);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n // Ignore parse errors\n }\n }\n }\n\n return map;\n}\n\n/**\n * Validate that package.json dependencies match the dependency graph\n *\n * For each project in the graph:\n * - Check that all graph dependencies exist in package.json\n * - Maps project names to package names for accurate comparison\n *\n * @param graph - Enhanced graph with project dependencies (uses project names)\n * @param workspaceRoot - Absolute path to workspace root\n * @returns Validation result with errors if any\n */\ninterface GraphEntry {\n level: number;\n dependsOn: string[];\n}\n\ninterface DepClassification {\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n extraWorkspaceDeps: string[];\n}\n\n/**\n * Compute the transitive closure of a project's dependencies in the graph.\n * Example: server ā [core-meta, http-server]; transitive closure includes\n * http-server and everything http-server reaches (http-routing, http-filters,\n * core-context, core-util, http-api).\n *\n * Used to allow package.json entries for transitive deps (a legitimate pattern:\n * npm install brings the whole dependency tree, so a consumer may list any\n * reachable package directly).\n */\nfunction computeTransitiveClosure(\n projectName: string,\n graph: Record<string, GraphEntry>\n): Set<string> {\n const closure = new Set<string>();\n const stack = [projectName];\n while (stack.length > 0) {\n const current = stack.pop()!;\n const entry = graph[current];\n if (!entry) continue;\n for (const dep of entry.dependsOn) {\n if (!closure.has(dep)) {\n closure.add(dep);\n stack.push(dep);\n }\n }\n }\n return closure;\n}\n\ninterface SingleProjectValidation {\n result: ProjectValidationResult;\n errors: string[];\n warnings: string[];\n}\n\nfunction classifyDeps(\n packageJsonDeps: string[],\n entry: GraphEntry,\n transitiveClosure: Set<string>,\n projectToPackage: Map<string, string>,\n packageToProject: Map<string, string>\n): DepClassification {\n const missingInPackageJson: string[] = [];\n for (const depProjectName of entry.dependsOn) {\n const depPackageName = projectToPackage.get(depProjectName) || depProjectName;\n if (!packageJsonDeps.includes(depPackageName)) {\n missingInPackageJson.push(depProjectName);\n }\n }\n\n // Workspace extras are OK if reachable via transitive closure (matches the\n // ESLint enforce-architecture rule which also allows transitive imports).\n // Only flag extras that are NOT reachable at all ā real graph drift.\n const extraInPackageJson: string[] = [];\n const extraWorkspaceDeps: string[] = [];\n for (const dep of packageJsonDeps) {\n const depProjectName = packageToProject.get(dep);\n if (depProjectName === undefined) {\n extraInPackageJson.push(dep);\n continue;\n }\n if (entry.dependsOn.includes(depProjectName)) continue;\n if (transitiveClosure.has(depProjectName)) continue;\n extraWorkspaceDeps.push(dep);\n }\n\n return { missingInPackageJson, extraInPackageJson, extraWorkspaceDeps };\n}\n\nfunction validateSingleProject(\n projectName: string,\n entry: GraphEntry,\n projectRoot: string,\n packageJsonDeps: string[],\n graph: Record<string, GraphEntry>,\n projectToPackage: Map<string, string>,\n packageToProject: Map<string, string>\n): SingleProjectValidation {\n const transitiveClosure = computeTransitiveClosure(projectName, graph);\n const classification = classifyDeps(\n packageJsonDeps,\n entry,\n transitiveClosure,\n projectToPackage,\n packageToProject\n );\n\n const errors: string[] = [];\n if (classification.missingInPackageJson.length > 0) {\n errors.push(\n `Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${classification.missingInPackageJson.join(', ')}\\n` +\n ` Fix: Add these to package.json dependencies`\n );\n }\n\n // Unreachable workspace extras are WARN-ONLY: they may be real runtime deps that nx's\n // import analysis can't see (peerDependency / generated client). Never error ā that is\n // the runtime-validity trap. We surface them so genuine drift is still visible.\n const warnings: string[] = [];\n for (const extraPkg of classification.extraWorkspaceDeps) {\n const extraProject = packageToProject.get(extraPkg);\n warnings.push(\n `Project ${projectName} (${projectRoot}/package.json) has \"${extraPkg}\" but the architecture graph has no path ${projectName} ā ${extraProject}.\\n` +\n ` This is allowed (it may be a runtime-only/peer dependency). If it is genuinely unused, you may remove it.`\n );\n }\n\n // extraWorkspaceDeps do NOT affect validity ā only missing (additive-fix) errors do.\n const valid = classification.missingInPackageJson.length === 0;\n return {\n result: {\n project: projectName,\n valid,\n missingInPackageJson: classification.missingInPackageJson,\n extraInPackageJson: classification.extraInPackageJson,\n },\n errors,\n warnings,\n };\n}\n\nexport async function validatePackageJsonDependencies(\n graph: Record<string, GraphEntry>,\n workspaceRoot: string\n): Promise<ValidationResult> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);\n const packageToProject = new Map<string, string>();\n for (const [projName, pkgName] of projectToPackage.entries()) {\n packageToProject.set(pkgName, projName);\n }\n\n const errors: string[] = [];\n const warnings: string[] = [];\n const projectResults: ProjectValidationResult[] = [];\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const projectConfig = projectsConfig.projects[projectName];\n if (!projectConfig) continue;\n\n const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectConfig.root);\n if (packageJsonDeps === null) continue;\n\n const validation = validateSingleProject(\n projectName,\n entry,\n projectConfig.root,\n packageJsonDeps,\n graph,\n projectToPackage,\n packageToProject\n );\n projectResults.push(validation.result);\n errors.push(...validation.errors);\n warnings.push(...validation.warnings);\n }\n\n return { valid: errors.length === 0, errors, warnings, projectResults };\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transitive Reduction
|
|
3
|
+
*
|
|
4
|
+
* Computes the transitive reduction of a DAG: the minimal set of edges that
|
|
5
|
+
* preserves the exact same reachability (transitive closure) as the input graph.
|
|
6
|
+
*
|
|
7
|
+
* For a DAG the transitive reduction is unique. An edge u ā v is redundant when v
|
|
8
|
+
* is reachable from u through some OTHER direct child w of u (w ā v). Removing all
|
|
9
|
+
* redundant edges yields the reduced graph.
|
|
10
|
+
*
|
|
11
|
+
* This is purely a VIEW transformation for architecture/dependencies.json ā it must
|
|
12
|
+
* never feed back into package.json or build order. Build order continues to follow
|
|
13
|
+
* nx's full project graph (via `^build`); reduction preserves reachability, so any
|
|
14
|
+
* topological order valid for the full graph is also valid for the reduced graph.
|
|
15
|
+
*
|
|
16
|
+
* The input MUST be acyclic. Reduction is undefined on cycles; callers run the
|
|
17
|
+
* cycle-detecting topological sort (graph-sorter) which throws on cycles first.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Compute the transitive reduction of a DAG.
|
|
21
|
+
*
|
|
22
|
+
* @param graph - Full DAG as { project: [directChildren] }
|
|
23
|
+
* @returns Reduced graph { project: [minimalDirectChildren] } (children sorted)
|
|
24
|
+
*/
|
|
25
|
+
export declare function transitiveReduction(graph: Record<string, string[]>): Record<string, string[]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transitive Reduction
|
|
4
|
+
*
|
|
5
|
+
* Computes the transitive reduction of a DAG: the minimal set of edges that
|
|
6
|
+
* preserves the exact same reachability (transitive closure) as the input graph.
|
|
7
|
+
*
|
|
8
|
+
* For a DAG the transitive reduction is unique. An edge u ā v is redundant when v
|
|
9
|
+
* is reachable from u through some OTHER direct child w of u (w ā v). Removing all
|
|
10
|
+
* redundant edges yields the reduced graph.
|
|
11
|
+
*
|
|
12
|
+
* This is purely a VIEW transformation for architecture/dependencies.json ā it must
|
|
13
|
+
* never feed back into package.json or build order. Build order continues to follow
|
|
14
|
+
* nx's full project graph (via `^build`); reduction preserves reachability, so any
|
|
15
|
+
* topological order valid for the full graph is also valid for the reduced graph.
|
|
16
|
+
*
|
|
17
|
+
* The input MUST be acyclic. Reduction is undefined on cycles; callers run the
|
|
18
|
+
* cycle-detecting topological sort (graph-sorter) which throws on cycles first.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.transitiveReduction = transitiveReduction;
|
|
22
|
+
/**
|
|
23
|
+
* Compute the transitive reduction of a DAG.
|
|
24
|
+
*
|
|
25
|
+
* @param graph - Full DAG as { project: [directChildren] }
|
|
26
|
+
* @returns Reduced graph { project: [minimalDirectChildren] } (children sorted)
|
|
27
|
+
*/
|
|
28
|
+
function transitiveReduction(graph) {
|
|
29
|
+
// Memoized reachability (transitive closure) per node.
|
|
30
|
+
const closure = new Map();
|
|
31
|
+
function reach(node) {
|
|
32
|
+
const cached = closure.get(node);
|
|
33
|
+
if (cached)
|
|
34
|
+
return cached;
|
|
35
|
+
const acc = new Set();
|
|
36
|
+
// Set before recursion: safe for a DAG, and guards against runaway recursion
|
|
37
|
+
// if the input is unexpectedly cyclic (closure stays finite).
|
|
38
|
+
closure.set(node, acc);
|
|
39
|
+
for (const child of graph[node] ?? []) {
|
|
40
|
+
acc.add(child);
|
|
41
|
+
for (const reachable of reach(child)) {
|
|
42
|
+
acc.add(reachable);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return acc;
|
|
46
|
+
}
|
|
47
|
+
const reduced = {};
|
|
48
|
+
for (const u of Object.keys(graph)) {
|
|
49
|
+
const children = graph[u] ?? [];
|
|
50
|
+
// Keep u ā v only if NO sibling w (w ā v) already reaches v.
|
|
51
|
+
const kept = children.filter((v) => !children.some((w) => w !== v && reach(w).has(v)));
|
|
52
|
+
reduced[u] = kept.sort();
|
|
53
|
+
}
|
|
54
|
+
return reduced;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=transitive-reduction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transitive-reduction.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/transitive-reduction.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;AAQH,kDAiCC;AAvCD;;;;;GAKG;AACH,SAAgB,mBAAmB,CAC/B,KAA+B;IAE/B,uDAAuD;IACvD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE/C,SAAS,KAAK,CAAC,IAAY;QACvB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAC9B,6EAA6E;QAC7E,8DAA8D;QAC9D,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACvB,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACpC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACf,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACvB,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAA6B,EAAE,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAChC,6DAA6D;QAC7D,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CACxB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAC3D,CAAC;QACF,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC","sourcesContent":["/**\n * Transitive Reduction\n *\n * Computes the transitive reduction of a DAG: the minimal set of edges that\n * preserves the exact same reachability (transitive closure) as the input graph.\n *\n * For a DAG the transitive reduction is unique. An edge u ā v is redundant when v\n * is reachable from u through some OTHER direct child w of u (w ā v). Removing all\n * redundant edges yields the reduced graph.\n *\n * This is purely a VIEW transformation for architecture/dependencies.json ā it must\n * never feed back into package.json or build order. Build order continues to follow\n * nx's full project graph (via `^build`); reduction preserves reachability, so any\n * topological order valid for the full graph is also valid for the reduced graph.\n *\n * The input MUST be acyclic. Reduction is undefined on cycles; callers run the\n * cycle-detecting topological sort (graph-sorter) which throws on cycles first.\n */\n\n/**\n * Compute the transitive reduction of a DAG.\n *\n * @param graph - Full DAG as { project: [directChildren] }\n * @returns Reduced graph { project: [minimalDirectChildren] } (children sorted)\n */\nexport function transitiveReduction(\n graph: Record<string, string[]>\n): Record<string, string[]> {\n // Memoized reachability (transitive closure) per node.\n const closure = new Map<string, Set<string>>();\n\n function reach(node: string): Set<string> {\n const cached = closure.get(node);\n if (cached) return cached;\n\n const acc = new Set<string>();\n // Set before recursion: safe for a DAG, and guards against runaway recursion\n // if the input is unexpectedly cyclic (closure stays finite).\n closure.set(node, acc);\n for (const child of graph[node] ?? []) {\n acc.add(child);\n for (const reachable of reach(child)) {\n acc.add(reachable);\n }\n }\n return acc;\n }\n\n const reduced: Record<string, string[]> = {};\n for (const u of Object.keys(graph)) {\n const children = graph[u] ?? [];\n // Keep u ā v only if NO sibling w (w ā v) already reaches v.\n const kept = children.filter(\n (v) => !children.some((w) => w !== v && reach(w).has(v))\n );\n reduced[u] = kept.sort();\n }\n return reduced;\n}\n"]}
|
package/src/plugin.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface ValidationOptions {
|
|
|
37
37
|
validateModifiedFiles?: boolean;
|
|
38
38
|
validateVersionsLocked?: boolean;
|
|
39
39
|
validateTsInSrc?: boolean;
|
|
40
|
+
validateNxWiring?: boolean;
|
|
40
41
|
newMethodsMaxLines?: number;
|
|
41
42
|
modifiedAndNewMethodsMaxLines?: number;
|
|
42
43
|
modifiedFilesMaxLines?: number;
|
package/src/plugin.js
CHANGED
|
@@ -32,7 +32,10 @@ const DEFAULT_OPTIONS = {
|
|
|
32
32
|
graphPath: 'architecture/dependencies.json',
|
|
33
33
|
validations: {
|
|
34
34
|
noCycles: true,
|
|
35
|
-
|
|
35
|
+
// Retired: the architecture graph is now auto-reduced in `generate`, so the
|
|
36
|
+
// committed graph can never contain a skip-level edge. Defaults off; the
|
|
37
|
+
// executor is a no-op kept for one release. See validate-no-skiplevel-deps.
|
|
38
|
+
noSkipLevelDeps: false,
|
|
36
39
|
architectureUnchanged: true,
|
|
37
40
|
validatePackageJson: true,
|
|
38
41
|
validateNewMethods: true,
|
|
@@ -40,6 +43,7 @@ const DEFAULT_OPTIONS = {
|
|
|
40
43
|
validateModifiedFiles: true,
|
|
41
44
|
validateVersionsLocked: true,
|
|
42
45
|
validateTsInSrc: true,
|
|
46
|
+
validateNxWiring: true,
|
|
43
47
|
newMethodsMaxLines: 30,
|
|
44
48
|
modifiedAndNewMethodsMaxLines: 80,
|
|
45
49
|
modifiedFilesMaxLines: 900,
|
|
@@ -184,6 +188,8 @@ function buildValidationTargetsList(validations) {
|
|
|
184
188
|
targets.push('validate-versions-locked');
|
|
185
189
|
if (validations.validateTsInSrc)
|
|
186
190
|
targets.push('validate-ts-in-src');
|
|
191
|
+
if (validations.validateNxWiring)
|
|
192
|
+
targets.push('validate-nx-wiring');
|
|
187
193
|
return targets;
|
|
188
194
|
}
|
|
189
195
|
/**
|
|
@@ -228,6 +234,9 @@ function createWorkspaceTargetsWithoutPrefix(opts) {
|
|
|
228
234
|
if (validations.validateTsInSrc) {
|
|
229
235
|
targets['validate-ts-in-src'] = createValidateTsInSrcTarget();
|
|
230
236
|
}
|
|
237
|
+
if (validations.validateNxWiring) {
|
|
238
|
+
targets['validate-nx-wiring'] = createValidateNxWiringTarget();
|
|
239
|
+
}
|
|
231
240
|
// Add validate-complete target that runs all enabled validations
|
|
232
241
|
const validationTargets = buildValidationTargetsList(validations);
|
|
233
242
|
if (validationTargets.length > 0) {
|
|
@@ -428,6 +437,17 @@ function createValidateTsInSrcTarget() {
|
|
|
428
437
|
},
|
|
429
438
|
};
|
|
430
439
|
}
|
|
440
|
+
function createValidateNxWiringTarget() {
|
|
441
|
+
return {
|
|
442
|
+
executor: '@webpieces/nx-webpieces-rules:validate-nx-wiring',
|
|
443
|
+
cache: false, // Cheap; depends on nx.json + project graph, not worth caching
|
|
444
|
+
inputs: ['{workspaceRoot}/nx.json', '{workspaceRoot}/webpieces.config.json'],
|
|
445
|
+
metadata: {
|
|
446
|
+
technologies: ['nx'],
|
|
447
|
+
description: 'Validate the webpieces validators are wired into the build via nx.json dependsOn',
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
431
451
|
function createValidateCompleteTarget(validationTargets) {
|
|
432
452
|
return {
|
|
433
453
|
executor: 'nx:noop',
|