@webpieces/nx-webpieces-rules 0.4.464 → 0.4.466
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/package.json +6 -6
- package/src/executors/validate-packagejson/executor.d.ts +7 -0
- package/src/executors/validate-packagejson/executor.js +7 -3
- package/src/executors/validate-packagejson/executor.js.map +1 -1
- package/src/executors/validate-packagejson/schema.json +9 -2
- package/src/lib/dep-usage-scanner.d.ts +54 -0
- package/src/lib/dep-usage-scanner.js +199 -0
- package/src/lib/dep-usage-scanner.js.map +1 -0
- package/src/lib/graph-metadata.js +26 -10
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-sorter.d.ts +9 -0
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/package-validator.d.ts +157 -21
- package/src/lib/package-validator.js +377 -153
- package/src/lib/package-validator.js.map +1 -1
- package/src/lib/runtime-graph.d.ts +21 -9
- package/src/lib/runtime-graph.js +66 -22
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/service-name-resolver.d.ts +31 -0
- package/src/lib/service-name-resolver.js +74 -5
- package/src/lib/service-name-resolver.js.map +1 -1
|
@@ -2,181 +2,405 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Package Validator
|
|
4
4
|
*
|
|
5
|
-
* Validates that package.json dependencies match the
|
|
6
|
-
*
|
|
5
|
+
* Validates that package.json dependencies match the architecture graph nx derived
|
|
6
|
+
* from the source, and — critically — that each dependency is declared in the RIGHT
|
|
7
|
+
* SECTION of package.json:
|
|
8
|
+
*
|
|
9
|
+
* - reached from production source → `dependencies` (or `peerDependencies`)
|
|
10
|
+
* - reached ONLY from test/dev files → `devDependencies`
|
|
11
|
+
*
|
|
12
|
+
* WHY the section matters: production images are built with
|
|
13
|
+
* `pnpm --filter=<svc> deploy --prod`, which installs exactly the `dependencies`
|
|
14
|
+
* closure. A test-support package parked in `dependencies` therefore ships test
|
|
15
|
+
* machinery (auth-bypass hooks, canned credentials, fake datastores) into the
|
|
16
|
+
* production container. Before this validator understood `devDependencies`, moving
|
|
17
|
+
* such a package to its correct home FAILED the build — the tool enforced the
|
|
18
|
+
* insecure layout. Now `devDependencies` is the required home for test-only deps,
|
|
19
|
+
* and listing one in `dependencies` is itself a violation.
|
|
7
20
|
*/
|
|
8
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.PackageValidator = exports.DeclaredDeps = exports.ValidationResult = exports.ProjectValidationResult = exports.PackageValidatorOptions = void 0;
|
|
9
23
|
exports.validatePackageJsonDependencies = validatePackageJsonDependencies;
|
|
10
24
|
const tslib_1 = require("tslib");
|
|
11
25
|
const fs = tslib_1.__importStar(require("fs"));
|
|
12
26
|
const path = tslib_1.__importStar(require("path"));
|
|
13
27
|
const devkit_1 = require("@nx/devkit");
|
|
28
|
+
const dep_usage_scanner_1 = require("./dep-usage-scanner");
|
|
29
|
+
const toError_1 = require("../toError");
|
|
14
30
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
31
|
+
* Options for {@link validatePackageJsonDependencies}. Data-only (CLAUDE.md: data
|
|
32
|
+
* structures are classes, never anonymous object literals).
|
|
17
33
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
24
|
-
try {
|
|
25
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
26
|
-
const deps = [];
|
|
27
|
-
// Collect ALL dependencies from package.json
|
|
28
|
-
for (const depType of ['dependencies', 'peerDependencies']) {
|
|
29
|
-
const depObj = packageJson[depType] || {};
|
|
30
|
-
for (const depName of Object.keys(depObj)) {
|
|
31
|
-
if (!deps.includes(depName)) {
|
|
32
|
-
deps.push(depName);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return deps.sort();
|
|
34
|
+
class PackageValidatorOptions {
|
|
35
|
+
testOnlyDepMode;
|
|
36
|
+
constructor(testOnlyDepMode = 'error') {
|
|
37
|
+
this.testOnlyDepMode = testOnlyDepMode;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
}
|
|
40
|
+
exports.PackageValidatorOptions = PackageValidatorOptions;
|
|
41
|
+
/**
|
|
42
|
+
* Validation result for a single project
|
|
43
|
+
*/
|
|
44
|
+
class ProjectValidationResult {
|
|
45
|
+
project;
|
|
46
|
+
valid;
|
|
47
|
+
missingInPackageJson;
|
|
48
|
+
extraInPackageJson;
|
|
49
|
+
/** Graph deps that are test-only but declared in `dependencies` (production closure). */
|
|
50
|
+
testOnlyInProdDependencies;
|
|
51
|
+
constructor(project, valid, missingInPackageJson, extraInPackageJson, testOnlyInProdDependencies) {
|
|
52
|
+
this.project = project;
|
|
53
|
+
this.valid = valid;
|
|
54
|
+
this.missingInPackageJson = missingInPackageJson;
|
|
55
|
+
this.extraInPackageJson = extraInPackageJson;
|
|
56
|
+
this.testOnlyInProdDependencies = testOnlyInProdDependencies;
|
|
43
57
|
}
|
|
44
58
|
}
|
|
59
|
+
exports.ProjectValidationResult = ProjectValidationResult;
|
|
45
60
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
61
|
+
* Overall validation result
|
|
62
|
+
*
|
|
63
|
+
* `errors` fail the build. Every error's fix is either ADDITIVE ("add it to package.json")
|
|
64
|
+
* or a MOVE between sections ("it belongs in devDependencies") — never "delete a
|
|
65
|
+
* dependency", so no error can push a user toward removing a runtime-required package.
|
|
66
|
+
*
|
|
67
|
+
* `warnings` never fail the build. Workspace deps in package.json that the architecture
|
|
68
|
+
* graph can't reach are reported here, NOT as errors: a transitively-reachable or even
|
|
69
|
+
* unreachable entry can still be a real runtime dependency (e.g. a peerDependency or a
|
|
70
|
+
* generated client that nx's import analysis doesn't traverse). Erroring on these is the
|
|
71
|
+
* "runtime-validity trap" that previously forced a bad package.json edit — so we only warn.
|
|
72
|
+
*/
|
|
73
|
+
class ValidationResult {
|
|
74
|
+
valid;
|
|
75
|
+
errors;
|
|
76
|
+
warnings;
|
|
77
|
+
projectResults;
|
|
78
|
+
constructor(valid, errors, warnings, projectResults) {
|
|
79
|
+
this.valid = valid;
|
|
80
|
+
this.errors = errors;
|
|
81
|
+
this.warnings = warnings;
|
|
82
|
+
this.projectResults = projectResults;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.ValidationResult = ValidationResult;
|
|
86
|
+
/**
|
|
87
|
+
* The three package.json sections that matter, kept apart so the validator can say
|
|
88
|
+
* WHICH one a package belongs in (the old code merged them and lost that information —
|
|
89
|
+
* and never even read devDependencies).
|
|
48
90
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
class DeclaredDeps {
|
|
92
|
+
dependencies;
|
|
93
|
+
devDependencies;
|
|
94
|
+
peerDependencies;
|
|
95
|
+
constructor(dependencies, devDependencies, peerDependencies) {
|
|
96
|
+
this.dependencies = dependencies;
|
|
97
|
+
this.devDependencies = devDependencies;
|
|
98
|
+
this.peerDependencies = peerDependencies;
|
|
99
|
+
}
|
|
100
|
+
/** Every declared package name, deduped and sorted (all three sections). */
|
|
101
|
+
all() {
|
|
102
|
+
const merged = new Set();
|
|
103
|
+
for (const name of this.dependencies)
|
|
104
|
+
merged.add(name);
|
|
105
|
+
for (const name of this.devDependencies)
|
|
106
|
+
merged.add(name);
|
|
107
|
+
for (const name of this.peerDependencies)
|
|
108
|
+
merged.add(name);
|
|
109
|
+
return Array.from(merged).sort();
|
|
110
|
+
}
|
|
111
|
+
/** Declared somewhere that survives `pnpm deploy --prod`. */
|
|
112
|
+
isProductionDeclared(packageName) {
|
|
113
|
+
return (this.dependencies.includes(packageName) ||
|
|
114
|
+
this.peerDependencies.includes(packageName));
|
|
115
|
+
}
|
|
116
|
+
isDevDeclared(packageName) {
|
|
117
|
+
return this.devDependencies.includes(packageName);
|
|
118
|
+
}
|
|
119
|
+
isDeclared(packageName) {
|
|
120
|
+
return this.isProductionDeclared(packageName) || this.isDevDeclared(packageName);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.DeclaredDeps = DeclaredDeps;
|
|
124
|
+
/**
|
|
125
|
+
* Per-project classification of graph deps against what package.json declares.
|
|
126
|
+
*/
|
|
127
|
+
class DepClassification {
|
|
128
|
+
/** Production-reached deps absent from `dependencies`/`peerDependencies`. */
|
|
129
|
+
missingInPackageJson = [];
|
|
130
|
+
/** Production-reached deps declared ONLY in `devDependencies` (runtime would break). */
|
|
131
|
+
prodDepsOnlyInDev = [];
|
|
132
|
+
/** Test-only deps declared in no section at all. */
|
|
133
|
+
missingTestOnlyDeps = [];
|
|
134
|
+
/** Test-only deps sitting in `dependencies` — i.e. shipped to production. */
|
|
135
|
+
testOnlyInProdDependencies = [];
|
|
136
|
+
/** Non-workspace (third-party) package.json entries — informational only. */
|
|
137
|
+
extraInPackageJson = [];
|
|
138
|
+
/** Workspace entries the graph cannot reach at all — warn-only drift. */
|
|
139
|
+
extraWorkspaceDeps = [];
|
|
140
|
+
}
|
|
141
|
+
class SingleProjectValidation {
|
|
142
|
+
result;
|
|
143
|
+
errors;
|
|
144
|
+
warnings;
|
|
145
|
+
constructor(result, errors, warnings) {
|
|
146
|
+
this.result = result;
|
|
147
|
+
this.errors = errors;
|
|
148
|
+
this.warnings = warnings;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The per-workspace lookups a single-project validation needs, passed as one object
|
|
153
|
+
* so method signatures stay readable.
|
|
154
|
+
*/
|
|
155
|
+
class ValidationContext {
|
|
156
|
+
graph;
|
|
157
|
+
projectToPackage;
|
|
158
|
+
packageToProject;
|
|
159
|
+
options;
|
|
160
|
+
constructor(graph, projectToPackage, packageToProject, options) {
|
|
161
|
+
this.graph = graph;
|
|
162
|
+
this.projectToPackage = projectToPackage;
|
|
163
|
+
this.packageToProject = packageToProject;
|
|
164
|
+
this.options = options;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
class PackageValidator {
|
|
168
|
+
scanner = new dep_usage_scanner_1.DepUsageScanner();
|
|
169
|
+
/**
|
|
170
|
+
* Read the three dependency sections of a project's package.json.
|
|
171
|
+
* Returns null when there is no package.json (apps often have none) so the caller
|
|
172
|
+
* can skip the project entirely.
|
|
173
|
+
*/
|
|
174
|
+
readDeclaredDeps(workspaceRoot, projectRoot) {
|
|
175
|
+
const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');
|
|
176
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
180
|
+
try {
|
|
181
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
182
|
+
return new DeclaredDeps(this.namesOf(packageJson, 'dependencies'), this.namesOf(packageJson, 'devDependencies'), this.namesOf(packageJson, 'peerDependencies'));
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const error = (0, toError_1.toError)(err);
|
|
186
|
+
console.warn(`Could not read package.json at ${packageJsonPath}: ${error.message}`);
|
|
187
|
+
return new DeclaredDeps([], [], []);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// webpieces-disable no-any-unknown -- parsed JSON is inherently untyped
|
|
191
|
+
namesOf(packageJson, section) {
|
|
192
|
+
const depObj = packageJson[section] || {};
|
|
193
|
+
return Object.keys(depObj).sort();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build map of project names to their package names
|
|
197
|
+
* e.g., "core-util" → "@webpieces/core-util"
|
|
198
|
+
*/
|
|
199
|
+
buildProjectToPackageMap(workspaceRoot,
|
|
200
|
+
// webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed
|
|
201
|
+
projectsConfig) {
|
|
202
|
+
const map = new Map();
|
|
203
|
+
// webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped
|
|
204
|
+
for (const configEntry of Object.entries(projectsConfig.projects)) {
|
|
205
|
+
const projectName = configEntry[0];
|
|
206
|
+
const packageJsonPath = path.join(workspaceRoot, configEntry[1].root, 'package.json');
|
|
207
|
+
if (!fs.existsSync(packageJsonPath))
|
|
208
|
+
continue;
|
|
209
|
+
const packageName = this.readPackageName(packageJsonPath);
|
|
210
|
+
if (packageName !== null)
|
|
211
|
+
map.set(projectName, packageName);
|
|
212
|
+
}
|
|
213
|
+
return map;
|
|
214
|
+
}
|
|
215
|
+
readPackageName(packageJsonPath) {
|
|
216
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
217
|
+
try {
|
|
218
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
219
|
+
return packageJson.name || null;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const error = (0, toError_1.toError)(err);
|
|
223
|
+
console.warn(`Could not parse ${packageJsonPath}: ${error.message}`);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Compute the transitive closure of a project's dependencies in the graph.
|
|
229
|
+
* Example: server → [core-meta, http-server]; the closure includes http-server and
|
|
230
|
+
* everything http-server reaches.
|
|
231
|
+
*
|
|
232
|
+
* Used to allow package.json entries for transitive deps (a legitimate pattern:
|
|
233
|
+
* npm install brings the whole dependency tree, so a consumer may list any reachable
|
|
234
|
+
* package directly).
|
|
235
|
+
*/
|
|
236
|
+
computeTransitiveClosure(projectName, graph) {
|
|
237
|
+
const closure = new Set();
|
|
238
|
+
const stack = [projectName];
|
|
239
|
+
while (stack.length > 0) {
|
|
240
|
+
const current = stack.pop();
|
|
241
|
+
const entry = graph[current];
|
|
242
|
+
if (!entry)
|
|
243
|
+
continue;
|
|
244
|
+
for (const dep of entry.dependsOn) {
|
|
245
|
+
if (!closure.has(dep)) {
|
|
246
|
+
closure.add(dep);
|
|
247
|
+
stack.push(dep);
|
|
62
248
|
}
|
|
63
249
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
250
|
+
}
|
|
251
|
+
return closure;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Split a project's graph deps into "declared correctly", "missing", and "declared in
|
|
255
|
+
* the wrong section", using the import scan to decide which section each dep belongs in.
|
|
256
|
+
*/
|
|
257
|
+
classifyDeps(declared, usage, entry, transitiveClosure, context) {
|
|
258
|
+
const classification = new DepClassification();
|
|
259
|
+
for (const depProjectName of entry.dependsOn) {
|
|
260
|
+
const depPackageName = context.projectToPackage.get(depProjectName) || depProjectName;
|
|
261
|
+
this.classifyOneDep(depProjectName, depPackageName, declared, usage, classification);
|
|
262
|
+
}
|
|
263
|
+
// Workspace extras are OK if reachable via transitive closure (matches the ESLint
|
|
264
|
+
// enforce-architecture rule which also allows transitive imports). Only flag extras
|
|
265
|
+
// that are NOT reachable at all — real graph drift.
|
|
266
|
+
for (const dep of declared.all()) {
|
|
267
|
+
const depProjectName = context.packageToProject.get(dep);
|
|
268
|
+
if (depProjectName === undefined) {
|
|
269
|
+
classification.extraInPackageJson.push(dep);
|
|
270
|
+
continue;
|
|
68
271
|
}
|
|
272
|
+
if (entry.dependsOn.includes(depProjectName))
|
|
273
|
+
continue;
|
|
274
|
+
if (transitiveClosure.has(depProjectName))
|
|
275
|
+
continue;
|
|
276
|
+
classification.extraWorkspaceDeps.push(dep);
|
|
69
277
|
}
|
|
278
|
+
return classification;
|
|
70
279
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const closure = new Set();
|
|
85
|
-
const stack = [projectName];
|
|
86
|
-
while (stack.length > 0) {
|
|
87
|
-
const current = stack.pop();
|
|
88
|
-
const entry = graph[current];
|
|
89
|
-
if (!entry)
|
|
90
|
-
continue;
|
|
91
|
-
for (const dep of entry.dependsOn) {
|
|
92
|
-
if (!closure.has(dep)) {
|
|
93
|
-
closure.add(dep);
|
|
94
|
-
stack.push(dep);
|
|
280
|
+
/**
|
|
281
|
+
* The heart of the fix: which SECTION does this dep belong in?
|
|
282
|
+
*
|
|
283
|
+
* A dep is test-only when the scan saw it imported by test/dev files and by NO
|
|
284
|
+
* production file. Anything else — including a dep we never saw imported at all (it
|
|
285
|
+
* may be loaded reflectively at runtime) — is treated as production, so this can
|
|
286
|
+
* never push a runtime-required package out of `dependencies`.
|
|
287
|
+
*/
|
|
288
|
+
classifyOneDep(depProjectName, depPackageName, declared, usage, classification) {
|
|
289
|
+
if (usage.isTestOnly(depPackageName)) {
|
|
290
|
+
if (!declared.isDeclared(depPackageName)) {
|
|
291
|
+
classification.missingTestOnlyDeps.push(depProjectName);
|
|
292
|
+
return;
|
|
95
293
|
}
|
|
294
|
+
if (declared.dependencies.includes(depPackageName)) {
|
|
295
|
+
classification.testOnlyInProdDependencies.push(depProjectName);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (declared.isProductionDeclared(depPackageName))
|
|
300
|
+
return;
|
|
301
|
+
if (declared.isDevDeclared(depPackageName)) {
|
|
302
|
+
classification.prodDepsOnlyInDev.push(depProjectName);
|
|
303
|
+
return;
|
|
96
304
|
}
|
|
305
|
+
classification.missingInPackageJson.push(depProjectName);
|
|
306
|
+
}
|
|
307
|
+
validateSingleProject(projectName, entry, projectRoot, declared, usage, context) {
|
|
308
|
+
const transitiveClosure = this.computeTransitiveClosure(projectName, context.graph);
|
|
309
|
+
const classification = this.classifyDeps(declared, usage, entry, transitiveClosure, context);
|
|
310
|
+
const where = `Project ${projectName} (${projectRoot}/package.json)`;
|
|
311
|
+
const errors = this.buildErrors(where, classification, context.options);
|
|
312
|
+
const warnings = this.buildWarnings(where, projectName, classification, context);
|
|
313
|
+
const missing = classification.missingInPackageJson.concat(classification.missingTestOnlyDeps);
|
|
314
|
+
const result = new ProjectValidationResult(projectName, errors.length === 0, missing, classification.extraInPackageJson, classification.testOnlyInProdDependencies);
|
|
315
|
+
return new SingleProjectValidation(result, errors, warnings);
|
|
316
|
+
}
|
|
317
|
+
buildErrors(where, classification, options) {
|
|
318
|
+
const errors = [];
|
|
319
|
+
if (classification.missingInPackageJson.length > 0) {
|
|
320
|
+
errors.push(`${where} is missing dependencies: ${classification.missingInPackageJson.join(', ')}\n` +
|
|
321
|
+
` These are imported by PRODUCTION source files.\n` +
|
|
322
|
+
` Fix: Add them to package.json "dependencies"`);
|
|
323
|
+
}
|
|
324
|
+
if (classification.missingTestOnlyDeps.length > 0) {
|
|
325
|
+
errors.push(`${where} is missing dependencies: ${classification.missingTestOnlyDeps.join(', ')}\n` +
|
|
326
|
+
` These are imported ONLY by test/dev files (*.spec.ts, __tests__/, test configs).\n` +
|
|
327
|
+
` Fix: Add them to package.json "devDependencies" — NOT "dependencies", which would ship them to production`);
|
|
328
|
+
}
|
|
329
|
+
if (classification.prodDepsOnlyInDev.length > 0) {
|
|
330
|
+
errors.push(`${where} declares production imports only in devDependencies: ${classification.prodDepsOnlyInDev.join(', ')}\n` +
|
|
331
|
+
` Production source files import them, so \`pnpm deploy --prod\` would omit them and the runtime would break.\n` +
|
|
332
|
+
` Fix: Move them to package.json "dependencies"`);
|
|
333
|
+
}
|
|
334
|
+
const testOnlyInProd = classification.testOnlyInProdDependencies;
|
|
335
|
+
if (options.testOnlyDepMode === 'error' && testOnlyInProd.length > 0) {
|
|
336
|
+
errors.push(this.testOnlyInProdMessage(where, testOnlyInProd));
|
|
337
|
+
}
|
|
338
|
+
return errors;
|
|
339
|
+
}
|
|
340
|
+
buildWarnings(where, projectName, classification, context) {
|
|
341
|
+
const warnings = [];
|
|
342
|
+
const testOnlyInProd = classification.testOnlyInProdDependencies;
|
|
343
|
+
if (context.options.testOnlyDepMode === 'warn' && testOnlyInProd.length > 0) {
|
|
344
|
+
warnings.push(this.testOnlyInProdMessage(where, testOnlyInProd));
|
|
345
|
+
}
|
|
346
|
+
// Unreachable workspace extras are WARN-ONLY: they may be real runtime deps that nx's
|
|
347
|
+
// import analysis can't see (peerDependency / generated client). Never error — that is
|
|
348
|
+
// the runtime-validity trap. We surface them so genuine drift is still visible.
|
|
349
|
+
for (const extraPkg of classification.extraWorkspaceDeps) {
|
|
350
|
+
const extraProject = context.packageToProject.get(extraPkg);
|
|
351
|
+
warnings.push(`${where} has "${extraPkg}" but the architecture graph has no path ${projectName} → ${extraProject}.\n` +
|
|
352
|
+
` This is allowed (it may be a runtime-only/peer dependency). If it is genuinely unused, you may remove it.`);
|
|
353
|
+
}
|
|
354
|
+
return warnings;
|
|
355
|
+
}
|
|
356
|
+
testOnlyInProdMessage(where, deps) {
|
|
357
|
+
return (`${where} lists test-only packages in "dependencies": ${deps.join(', ')}\n` +
|
|
358
|
+
` No production source file imports them — only test/dev files do — yet \`pnpm deploy --prod\`\n` +
|
|
359
|
+
` installs the "dependencies" closure, so this ships test machinery (fakes, auth-bypass hooks,\n` +
|
|
360
|
+
` canned credentials) into the production image.\n` +
|
|
361
|
+
` Fix: Move them to package.json "devDependencies"`);
|
|
362
|
+
}
|
|
363
|
+
async validate(graph, workspaceRoot, options) {
|
|
364
|
+
const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
|
|
365
|
+
const projectsConfig = (0, devkit_1.readProjectsConfigurationFromProjectGraph)(projectGraph);
|
|
366
|
+
const projectToPackage = this.buildProjectToPackageMap(workspaceRoot, projectsConfig);
|
|
367
|
+
const packageToProject = new Map();
|
|
368
|
+
for (const pair of projectToPackage.entries()) {
|
|
369
|
+
packageToProject.set(pair[1], pair[0]);
|
|
370
|
+
}
|
|
371
|
+
const context = new ValidationContext(graph, projectToPackage, packageToProject, options);
|
|
372
|
+
const errors = [];
|
|
373
|
+
const warnings = [];
|
|
374
|
+
const projectResults = [];
|
|
375
|
+
for (const graphPair of Object.entries(graph)) {
|
|
376
|
+
const projectName = graphPair[0];
|
|
377
|
+
const entry = graphPair[1];
|
|
378
|
+
const projectConfig = projectsConfig.projects[projectName];
|
|
379
|
+
if (!projectConfig)
|
|
380
|
+
continue;
|
|
381
|
+
const declared = this.readDeclaredDeps(workspaceRoot, projectConfig.root);
|
|
382
|
+
if (declared === null)
|
|
383
|
+
continue;
|
|
384
|
+
const usage = this.scanner.scan(path.join(workspaceRoot, projectConfig.root));
|
|
385
|
+
const validation = this.validateSingleProject(projectName, entry, projectConfig.root, declared, usage, context);
|
|
386
|
+
projectResults.push(validation.result);
|
|
387
|
+
errors.push(...validation.errors);
|
|
388
|
+
warnings.push(...validation.warnings);
|
|
389
|
+
}
|
|
390
|
+
return new ValidationResult(errors.length === 0, errors, warnings, projectResults);
|
|
97
391
|
}
|
|
98
|
-
return closure;
|
|
99
|
-
}
|
|
100
|
-
function classifyDeps(packageJsonDeps, entry, transitiveClosure, projectToPackage, packageToProject) {
|
|
101
|
-
const missingInPackageJson = [];
|
|
102
|
-
for (const depProjectName of entry.dependsOn) {
|
|
103
|
-
const depPackageName = projectToPackage.get(depProjectName) || depProjectName;
|
|
104
|
-
if (!packageJsonDeps.includes(depPackageName)) {
|
|
105
|
-
missingInPackageJson.push(depProjectName);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Workspace extras are OK if reachable via transitive closure (matches the
|
|
109
|
-
// ESLint enforce-architecture rule which also allows transitive imports).
|
|
110
|
-
// Only flag extras that are NOT reachable at all — real graph drift.
|
|
111
|
-
const extraInPackageJson = [];
|
|
112
|
-
const extraWorkspaceDeps = [];
|
|
113
|
-
for (const dep of packageJsonDeps) {
|
|
114
|
-
const depProjectName = packageToProject.get(dep);
|
|
115
|
-
if (depProjectName === undefined) {
|
|
116
|
-
extraInPackageJson.push(dep);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (entry.dependsOn.includes(depProjectName))
|
|
120
|
-
continue;
|
|
121
|
-
if (transitiveClosure.has(depProjectName))
|
|
122
|
-
continue;
|
|
123
|
-
extraWorkspaceDeps.push(dep);
|
|
124
|
-
}
|
|
125
|
-
return { missingInPackageJson, extraInPackageJson, extraWorkspaceDeps };
|
|
126
|
-
}
|
|
127
|
-
function validateSingleProject(projectName, entry, projectRoot, packageJsonDeps, graph, projectToPackage, packageToProject) {
|
|
128
|
-
const transitiveClosure = computeTransitiveClosure(projectName, graph);
|
|
129
|
-
const classification = classifyDeps(packageJsonDeps, entry, transitiveClosure, projectToPackage, packageToProject);
|
|
130
|
-
const errors = [];
|
|
131
|
-
if (classification.missingInPackageJson.length > 0) {
|
|
132
|
-
errors.push(`Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${classification.missingInPackageJson.join(', ')}\n` +
|
|
133
|
-
` Fix: Add these to package.json dependencies`);
|
|
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 = [];
|
|
139
|
-
for (const extraPkg of classification.extraWorkspaceDeps) {
|
|
140
|
-
const extraProject = packageToProject.get(extraPkg);
|
|
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.`);
|
|
143
|
-
}
|
|
144
|
-
// extraWorkspaceDeps do NOT affect validity — only missing (additive-fix) errors do.
|
|
145
|
-
const valid = classification.missingInPackageJson.length === 0;
|
|
146
|
-
return {
|
|
147
|
-
result: {
|
|
148
|
-
project: projectName,
|
|
149
|
-
valid,
|
|
150
|
-
missingInPackageJson: classification.missingInPackageJson,
|
|
151
|
-
extraInPackageJson: classification.extraInPackageJson,
|
|
152
|
-
},
|
|
153
|
-
errors,
|
|
154
|
-
warnings,
|
|
155
|
-
};
|
|
156
392
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const projectConfig = projectsConfig.projects[projectName];
|
|
170
|
-
if (!projectConfig)
|
|
171
|
-
continue;
|
|
172
|
-
const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectConfig.root);
|
|
173
|
-
if (packageJsonDeps === null)
|
|
174
|
-
continue;
|
|
175
|
-
const validation = validateSingleProject(projectName, entry, projectConfig.root, packageJsonDeps, graph, projectToPackage, packageToProject);
|
|
176
|
-
projectResults.push(validation.result);
|
|
177
|
-
errors.push(...validation.errors);
|
|
178
|
-
warnings.push(...validation.warnings);
|
|
179
|
-
}
|
|
180
|
-
return { valid: errors.length === 0, errors, warnings, projectResults };
|
|
393
|
+
exports.PackageValidator = PackageValidator;
|
|
394
|
+
/**
|
|
395
|
+
* Validate that package.json dependencies cover the dependency graph AND that each dep
|
|
396
|
+
* is declared in the correct section (dependencies vs devDependencies).
|
|
397
|
+
*
|
|
398
|
+
* @param graph - Enhanced graph with project dependencies (uses project names)
|
|
399
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
400
|
+
* @param options - Strictness of the "test-only dep in the production closure" check
|
|
401
|
+
*/
|
|
402
|
+
// webpieces-disable no-function-outside-class -- stable module entry point imported by the executor; it only delegates to PackageValidator
|
|
403
|
+
async function validatePackageJsonDependencies(graph, workspaceRoot, options = new PackageValidatorOptions()) {
|
|
404
|
+
return new PackageValidator().validate(graph, workspaceRoot, options);
|
|
181
405
|
}
|
|
182
406
|
//# sourceMappingURL=package-validator.js.map
|