@webpieces/nx-webpieces-rules 0.4.465 → 0.4.467
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-architecture-unchanged/executor.js +6 -0
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-eslint-sync/executor.js +5 -0
- package/src/executors/validate-eslint-sync/executor.js.map +1 -1
- package/src/executors/validate-no-architecture-cycles/executor.d.ts +1 -1
- package/src/executors/validate-no-architecture-cycles/executor.js +7 -1
- package/src/executors/validate-no-architecture-cycles/executor.js.map +1 -1
- package/src/executors/validate-packagejson/executor.d.ts +7 -0
- package/src/executors/validate-packagejson/executor.js +51 -23
- package/src/executors/validate-packagejson/executor.js.map +1 -1
- package/src/executors/validate-packagejson/schema.json +9 -2
- package/src/executors/validate-versions-locked/executor.js +6 -1
- package/src/executors/validate-versions-locked/executor.js.map +1 -1
- 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/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/rule-gate.d.ts +33 -0
- package/src/lib/rule-gate.js +58 -0
- package/src/lib/rule-gate.js.map +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dep Usage Scanner
|
|
4
|
+
*
|
|
5
|
+
* Answers ONE question for a project: "is package X reached from production
|
|
6
|
+
* source, or ONLY from test/dev files?"
|
|
7
|
+
*
|
|
8
|
+
* WHY this exists: nx derives graph edges from ALL TypeScript sources, specs
|
|
9
|
+
* included. Without this scan, a package imported only by `*.spec.ts` looks
|
|
10
|
+
* identical to a package imported by a controller, so the validator forces it
|
|
11
|
+
* into `dependencies` — and `pnpm deploy --prod` then ships test machinery
|
|
12
|
+
* (auth-bypass hooks, canned credentials, fakes) into the production image.
|
|
13
|
+
* Splitting the scan by file kind lets `devDependencies` be the REQUIRED home
|
|
14
|
+
* for test-only packages.
|
|
15
|
+
*
|
|
16
|
+
* The scan is deliberately conservative: a package is "test-only" ONLY when it
|
|
17
|
+
* is imported by at least one test/dev file and by ZERO production files. Any
|
|
18
|
+
* doubt (no import found at all, e.g. a runtime-only/reflection dependency)
|
|
19
|
+
* resolves to "production", so this can never push a runtime-required package
|
|
20
|
+
* out of `dependencies`.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.DepUsageScanner = exports.DepUsage = void 0;
|
|
24
|
+
const tslib_1 = require("tslib");
|
|
25
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
26
|
+
const path = tslib_1.__importStar(require("path"));
|
|
27
|
+
const toError_1 = require("../toError");
|
|
28
|
+
/** Directories never worth scanning (build output, vendored code, VCS). */
|
|
29
|
+
const SKIP_DIRS = new Set([
|
|
30
|
+
'node_modules',
|
|
31
|
+
'dist',
|
|
32
|
+
'build',
|
|
33
|
+
'out-tsc',
|
|
34
|
+
'coverage',
|
|
35
|
+
'.nx',
|
|
36
|
+
'.git',
|
|
37
|
+
'tmp',
|
|
38
|
+
'.angular',
|
|
39
|
+
]);
|
|
40
|
+
/** Source extensions whose imports we understand. */
|
|
41
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
42
|
+
'.ts',
|
|
43
|
+
'.tsx',
|
|
44
|
+
'.mts',
|
|
45
|
+
'.cts',
|
|
46
|
+
'.js',
|
|
47
|
+
'.jsx',
|
|
48
|
+
'.mjs',
|
|
49
|
+
'.cjs',
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Directory names that make everything below them test/dev-only.
|
|
53
|
+
* Kept tight on purpose — a false "this is a test dir" would let a real
|
|
54
|
+
* production import be classified as test-only.
|
|
55
|
+
*/
|
|
56
|
+
const TEST_DIR_NAMES = new Set([
|
|
57
|
+
'__tests__',
|
|
58
|
+
'__mocks__',
|
|
59
|
+
'__fixtures__',
|
|
60
|
+
'test',
|
|
61
|
+
'tests',
|
|
62
|
+
'e2e',
|
|
63
|
+
'e2e-tests',
|
|
64
|
+
]);
|
|
65
|
+
/** `foo.spec.ts`, `foo.test.tsx`, `foo-e2e.spec.mts`, `foo.testkit.ts`, ... */
|
|
66
|
+
const TEST_FILE_RE = /[.-](spec|test|e2e|testkit|mock|mocks|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
67
|
+
/** Tooling config/bootstrap files: dev-time by definition. */
|
|
68
|
+
const DEV_CONFIG_RE = /^(vitest|vite|jest|playwright|cypress|karma|webpack|rollup|eslint|prettier)\.[\w.-]*config\.[cm]?[jt]s$/;
|
|
69
|
+
/** `jest.setup.ts`, `vitest.setup.ts`, `test-setup.ts`, ... */
|
|
70
|
+
const DEV_SETUP_RE = /^([\w-]*[.-])?(setup|test-setup)\.[cm]?[jt]s$/;
|
|
71
|
+
/** Bare-import extraction: `from 'x'`, `import 'x'`, `import('x')`, `require('x')`. */
|
|
72
|
+
const IMPORT_RE = /(?:\bfrom\s*|\bimport\s*|\brequire\s*\(\s*|\bimport\s*\(\s*)['"]([^'"]+)['"]/g;
|
|
73
|
+
/**
|
|
74
|
+
* Which packages a project reaches from production code vs. only from test/dev code.
|
|
75
|
+
* Data-only: no logic lives here (see CLAUDE.md — data structures are classes).
|
|
76
|
+
*/
|
|
77
|
+
class DepUsage {
|
|
78
|
+
prodPackages;
|
|
79
|
+
testPackages;
|
|
80
|
+
constructor(prodPackages, testPackages) {
|
|
81
|
+
this.prodPackages = prodPackages;
|
|
82
|
+
this.testPackages = testPackages;
|
|
83
|
+
}
|
|
84
|
+
/** True when the package is imported by tests and by no production file. */
|
|
85
|
+
isTestOnly(packageName) {
|
|
86
|
+
return this.testPackages.has(packageName) && !this.prodPackages.has(packageName);
|
|
87
|
+
}
|
|
88
|
+
/** True when we saw the package in NO file at all (kind is unknown → treat as prod). */
|
|
89
|
+
isUnseen(packageName) {
|
|
90
|
+
return !this.testPackages.has(packageName) && !this.prodPackages.has(packageName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.DepUsage = DepUsage;
|
|
94
|
+
class DepUsageScanner {
|
|
95
|
+
/**
|
|
96
|
+
* Walk a project directory and record every bare import specifier, bucketed
|
|
97
|
+
* by whether the importing file is production or test/dev.
|
|
98
|
+
*/
|
|
99
|
+
scan(absProjectDir) {
|
|
100
|
+
const usage = new DepUsage(new Set(), new Set());
|
|
101
|
+
if (!fs.existsSync(absProjectDir))
|
|
102
|
+
return usage;
|
|
103
|
+
this.walk(absProjectDir, absProjectDir, usage);
|
|
104
|
+
return usage;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Is this path a test/dev file? Path is relative to the project root and
|
|
108
|
+
* uses either separator.
|
|
109
|
+
*/
|
|
110
|
+
isDevFile(relPath) {
|
|
111
|
+
const normalized = relPath.split(path.sep).join('/');
|
|
112
|
+
const segments = normalized.split('/');
|
|
113
|
+
const fileName = segments[segments.length - 1];
|
|
114
|
+
for (const dir of segments.slice(0, -1)) {
|
|
115
|
+
if (TEST_DIR_NAMES.has(dir))
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (TEST_FILE_RE.test(fileName))
|
|
119
|
+
return true;
|
|
120
|
+
if (DEV_CONFIG_RE.test(fileName))
|
|
121
|
+
return true;
|
|
122
|
+
if (DEV_SETUP_RE.test(fileName))
|
|
123
|
+
return true;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The package a bare specifier belongs to, or null for relative/absolute
|
|
128
|
+
* paths and node: builtins. `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`.
|
|
129
|
+
*/
|
|
130
|
+
toPackageName(specifier) {
|
|
131
|
+
if (specifier.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
if (specifier.startsWith('.') || specifier.startsWith('/'))
|
|
134
|
+
return null;
|
|
135
|
+
if (specifier.startsWith('node:'))
|
|
136
|
+
return null;
|
|
137
|
+
const parts = specifier.split('/');
|
|
138
|
+
if (specifier.startsWith('@')) {
|
|
139
|
+
if (parts.length < 2)
|
|
140
|
+
return null;
|
|
141
|
+
return `${parts[0]}/${parts[1]}`;
|
|
142
|
+
}
|
|
143
|
+
return parts[0];
|
|
144
|
+
}
|
|
145
|
+
walk(absDir, projectRoot, usage) {
|
|
146
|
+
const entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const absPath = path.join(absDir, entry.name);
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
if (SKIP_DIRS.has(entry.name))
|
|
151
|
+
continue;
|
|
152
|
+
this.walk(absPath, projectRoot, usage);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!entry.isFile())
|
|
156
|
+
continue;
|
|
157
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(entry.name)))
|
|
158
|
+
continue;
|
|
159
|
+
this.scanFile(absPath, projectRoot, usage);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
scanFile(absPath, projectRoot, usage) {
|
|
163
|
+
const source = this.readFile(absPath);
|
|
164
|
+
if (source === null)
|
|
165
|
+
return;
|
|
166
|
+
const relPath = path.relative(projectRoot, absPath);
|
|
167
|
+
const bucket = this.isDevFile(relPath) ? usage.testPackages : usage.prodPackages;
|
|
168
|
+
for (const packageName of this.extractPackageNames(source)) {
|
|
169
|
+
bucket.add(packageName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
extractPackageNames(source) {
|
|
173
|
+
const names = [];
|
|
174
|
+
IMPORT_RE.lastIndex = 0;
|
|
175
|
+
let match = IMPORT_RE.exec(source);
|
|
176
|
+
while (match !== null) {
|
|
177
|
+
const packageName = this.toPackageName(match[1]);
|
|
178
|
+
if (packageName !== null)
|
|
179
|
+
names.push(packageName);
|
|
180
|
+
match = IMPORT_RE.exec(source);
|
|
181
|
+
}
|
|
182
|
+
return names;
|
|
183
|
+
}
|
|
184
|
+
readFile(absPath) {
|
|
185
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
186
|
+
try {
|
|
187
|
+
return fs.readFileSync(absPath, 'utf-8');
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const error = (0, toError_1.toError)(err);
|
|
191
|
+
// An unreadable file cannot change the classification of a package; skipping it
|
|
192
|
+
// only ever makes the scan MORE conservative (fewer test-only classifications).
|
|
193
|
+
console.warn(`Could not read ${absPath} while classifying deps: ${error.message}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.DepUsageScanner = DepUsageScanner;
|
|
199
|
+
//# sourceMappingURL=dep-usage-scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dep-usage-scanner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dep-usage-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dep Usage Scanner\n *\n * Answers ONE question for a project: \"is package X reached from production\n * source, or ONLY from test/dev files?\"\n *\n * WHY this exists: nx derives graph edges from ALL TypeScript sources, specs\n * included. Without this scan, a package imported only by `*.spec.ts` looks\n * identical to a package imported by a controller, so the validator forces it\n * into `dependencies` — and `pnpm deploy --prod` then ships test machinery\n * (auth-bypass hooks, canned credentials, fakes) into the production image.\n * Splitting the scan by file kind lets `devDependencies` be the REQUIRED home\n * for test-only packages.\n *\n * The scan is deliberately conservative: a package is \"test-only\" ONLY when it\n * is imported by at least one test/dev file and by ZERO production files. Any\n * doubt (no import found at all, e.g. a runtime-only/reflection dependency)\n * resolves to \"production\", so this can never push a runtime-required package\n * out of `dependencies`.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../toError';\n\n/** Directories never worth scanning (build output, vendored code, VCS). */\nconst SKIP_DIRS = new Set([\n 'node_modules',\n 'dist',\n 'build',\n 'out-tsc',\n 'coverage',\n '.nx',\n '.git',\n 'tmp',\n '.angular',\n]);\n\n/** Source extensions whose imports we understand. */\nconst SOURCE_EXTENSIONS = new Set([\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n]);\n\n/**\n * Directory names that make everything below them test/dev-only.\n * Kept tight on purpose — a false \"this is a test dir\" would let a real\n * production import be classified as test-only.\n */\nconst TEST_DIR_NAMES = new Set([\n '__tests__',\n '__mocks__',\n '__fixtures__',\n 'test',\n 'tests',\n 'e2e',\n 'e2e-tests',\n]);\n\n/** `foo.spec.ts`, `foo.test.tsx`, `foo-e2e.spec.mts`, `foo.testkit.ts`, ... */\nconst TEST_FILE_RE = /[.-](spec|test|e2e|testkit|mock|mocks|fixture|fixtures)\\.[cm]?[jt]sx?$/;\n\n/** Tooling config/bootstrap files: dev-time by definition. */\nconst DEV_CONFIG_RE =\n /^(vitest|vite|jest|playwright|cypress|karma|webpack|rollup|eslint|prettier)\\.[\\w.-]*config\\.[cm]?[jt]s$/;\n\n/** `jest.setup.ts`, `vitest.setup.ts`, `test-setup.ts`, ... */\nconst DEV_SETUP_RE = /^([\\w-]*[.-])?(setup|test-setup)\\.[cm]?[jt]s$/;\n\n/** Bare-import extraction: `from 'x'`, `import 'x'`, `import('x')`, `require('x')`. */\nconst IMPORT_RE =\n /(?:\\bfrom\\s*|\\bimport\\s*|\\brequire\\s*\\(\\s*|\\bimport\\s*\\(\\s*)['\"]([^'\"]+)['\"]/g;\n\n/**\n * Which packages a project reaches from production code vs. only from test/dev code.\n * Data-only: no logic lives here (see CLAUDE.md — data structures are classes).\n */\nexport class DepUsage {\n prodPackages: Set<string>;\n testPackages: Set<string>;\n\n constructor(prodPackages: Set<string>, testPackages: Set<string>) {\n this.prodPackages = prodPackages;\n this.testPackages = testPackages;\n }\n\n /** True when the package is imported by tests and by no production file. */\n isTestOnly(packageName: string): boolean {\n return this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n\n /** True when we saw the package in NO file at all (kind is unknown → treat as prod). */\n isUnseen(packageName: string): boolean {\n return !this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n}\n\nexport class DepUsageScanner {\n /**\n * Walk a project directory and record every bare import specifier, bucketed\n * by whether the importing file is production or test/dev.\n */\n scan(absProjectDir: string): DepUsage {\n const usage = new DepUsage(new Set<string>(), new Set<string>());\n if (!fs.existsSync(absProjectDir)) return usage;\n this.walk(absProjectDir, absProjectDir, usage);\n return usage;\n }\n\n /**\n * Is this path a test/dev file? Path is relative to the project root and\n * uses either separator.\n */\n isDevFile(relPath: string): boolean {\n const normalized = relPath.split(path.sep).join('/');\n const segments = normalized.split('/');\n const fileName = segments[segments.length - 1];\n\n for (const dir of segments.slice(0, -1)) {\n if (TEST_DIR_NAMES.has(dir)) return true;\n }\n if (TEST_FILE_RE.test(fileName)) return true;\n if (DEV_CONFIG_RE.test(fileName)) return true;\n if (DEV_SETUP_RE.test(fileName)) return true;\n return false;\n }\n\n /**\n * The package a bare specifier belongs to, or null for relative/absolute\n * paths and node: builtins. `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`.\n */\n toPackageName(specifier: string): string | null {\n if (specifier.length === 0) return null;\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null;\n if (specifier.startsWith('node:')) return null;\n const parts = specifier.split('/');\n if (specifier.startsWith('@')) {\n if (parts.length < 2) return null;\n return `${parts[0]}/${parts[1]}`;\n }\n return parts[0];\n }\n\n private walk(absDir: string, projectRoot: string, usage: DepUsage): void {\n const entries = fs.readdirSync(absDir, { withFileTypes: true });\n for (const entry of entries) {\n const absPath = path.join(absDir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n this.walk(absPath, projectRoot, usage);\n continue;\n }\n if (!entry.isFile()) continue;\n if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue;\n this.scanFile(absPath, projectRoot, usage);\n }\n }\n\n private scanFile(absPath: string, projectRoot: string, usage: DepUsage): void {\n const source = this.readFile(absPath);\n if (source === null) return;\n const relPath = path.relative(projectRoot, absPath);\n const bucket = this.isDevFile(relPath) ? usage.testPackages : usage.prodPackages;\n for (const packageName of this.extractPackageNames(source)) {\n bucket.add(packageName);\n }\n }\n\n private extractPackageNames(source: string): string[] {\n const names: string[] = [];\n IMPORT_RE.lastIndex = 0;\n let match = IMPORT_RE.exec(source);\n while (match !== null) {\n const packageName = this.toPackageName(match[1]);\n if (packageName !== null) names.push(packageName);\n match = IMPORT_RE.exec(source);\n }\n return names;\n }\n\n private readFile(absPath: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readFileSync(absPath, 'utf-8');\n } catch (err: unknown) {\n const error = toError(err);\n // An unreadable file cannot change the classification of a package; skipping it\n // only ever makes the scan MORE conservative (fewer test-only classifications).\n console.warn(`Could not read ${absPath} while classifying deps: ${error.message}`);\n return null;\n }\n }\n}\n"]}
|
|
@@ -1,50 +1,186 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package Validator
|
|
3
3
|
*
|
|
4
|
-
* Validates that package.json dependencies match the
|
|
5
|
-
*
|
|
4
|
+
* Validates that package.json dependencies match the architecture graph nx derived
|
|
5
|
+
* from the source, and — critically — that each dependency is declared in the RIGHT
|
|
6
|
+
* SECTION of package.json:
|
|
7
|
+
*
|
|
8
|
+
* - reached from production source → `dependencies` (or `peerDependencies`)
|
|
9
|
+
* - reached ONLY from test/dev files → `devDependencies`
|
|
10
|
+
*
|
|
11
|
+
* WHY the section matters: production images are built with
|
|
12
|
+
* `pnpm --filter=<svc> deploy --prod`, which installs exactly the `dependencies`
|
|
13
|
+
* closure. A test-support package parked in `dependencies` therefore ships test
|
|
14
|
+
* machinery (auth-bypass hooks, canned credentials, fake datastores) into the
|
|
15
|
+
* production container. Before this validator understood `devDependencies`, moving
|
|
16
|
+
* such a package to its correct home FAILED the build — the tool enforced the
|
|
17
|
+
* insecure layout. Now `devDependencies` is the required home for test-only deps,
|
|
18
|
+
* and listing one in `dependencies` is itself a violation.
|
|
6
19
|
*/
|
|
20
|
+
import { DepUsage } from './dep-usage-scanner';
|
|
21
|
+
/**
|
|
22
|
+
* How hard to push back when a test-only package sits in `dependencies`
|
|
23
|
+
* (i.e. inside the production deploy closure).
|
|
24
|
+
*
|
|
25
|
+
* - 'error' (default): fails the build. This is the guardrail the security bug asked for.
|
|
26
|
+
* - 'warn': reports it without failing — the migration setting for a repo that needs a
|
|
27
|
+
* few releases to clean up its package.json files.
|
|
28
|
+
* - 'off': skip the check entirely.
|
|
29
|
+
*
|
|
30
|
+
* Missing deps, and production imports declared only in `devDependencies`, ALWAYS error:
|
|
31
|
+
* their fix is unambiguous and the alternative is a broken runtime.
|
|
32
|
+
*/
|
|
33
|
+
export type TestOnlyDepMode = 'error' | 'warn' | 'off';
|
|
34
|
+
/**
|
|
35
|
+
* Options for {@link validatePackageJsonDependencies}. Data-only (CLAUDE.md: data
|
|
36
|
+
* structures are classes, never anonymous object literals).
|
|
37
|
+
*/
|
|
38
|
+
export declare class PackageValidatorOptions {
|
|
39
|
+
testOnlyDepMode: TestOnlyDepMode;
|
|
40
|
+
constructor(testOnlyDepMode?: TestOnlyDepMode);
|
|
41
|
+
}
|
|
7
42
|
/**
|
|
8
43
|
* Validation result for a single project
|
|
9
44
|
*/
|
|
10
|
-
export
|
|
45
|
+
export declare class ProjectValidationResult {
|
|
11
46
|
project: string;
|
|
12
47
|
valid: boolean;
|
|
13
48
|
missingInPackageJson: string[];
|
|
14
49
|
extraInPackageJson: string[];
|
|
50
|
+
/** Graph deps that are test-only but declared in `dependencies` (production closure). */
|
|
51
|
+
testOnlyInProdDependencies: string[];
|
|
52
|
+
constructor(project: string, valid: boolean, missingInPackageJson: string[], extraInPackageJson: string[], testOnlyInProdDependencies: string[]);
|
|
15
53
|
}
|
|
16
54
|
/**
|
|
17
55
|
* Overall validation result
|
|
18
56
|
*
|
|
19
|
-
* `errors` fail the build
|
|
20
|
-
*
|
|
57
|
+
* `errors` fail the build. Every error's fix is either ADDITIVE ("add it to package.json")
|
|
58
|
+
* or a MOVE between sections ("it belongs in devDependencies") — never "delete a
|
|
59
|
+
* dependency", so no error can push a user toward removing a runtime-required package.
|
|
21
60
|
*
|
|
22
|
-
* `warnings` never fail the build. Workspace deps in package.json that the architecture
|
|
23
|
-
* can't reach are reported here, NOT as errors: a transitively-reachable or even
|
|
24
|
-
* entry can still be a real runtime dependency (e.g. a peerDependency or a
|
|
25
|
-
* that nx's import analysis doesn't traverse). Erroring on these is the
|
|
26
|
-
* that previously forced a bad package.json edit — so we only warn.
|
|
61
|
+
* `warnings` never fail the build. Workspace deps in package.json that the architecture
|
|
62
|
+
* graph can't reach are reported here, NOT as errors: a transitively-reachable or even
|
|
63
|
+
* unreachable entry can still be a real runtime dependency (e.g. a peerDependency or a
|
|
64
|
+
* generated client that nx's import analysis doesn't traverse). Erroring on these is the
|
|
65
|
+
* "runtime-validity trap" that previously forced a bad package.json edit — so we only warn.
|
|
27
66
|
*/
|
|
28
|
-
export
|
|
67
|
+
export declare class ValidationResult {
|
|
29
68
|
valid: boolean;
|
|
30
69
|
errors: string[];
|
|
31
70
|
warnings: string[];
|
|
32
71
|
projectResults: ProjectValidationResult[];
|
|
72
|
+
constructor(valid: boolean, errors: string[], warnings: string[], projectResults: ProjectValidationResult[]);
|
|
33
73
|
}
|
|
34
74
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
* The three package.json sections that matter, kept apart so the validator can say
|
|
76
|
+
* WHICH one a package belongs in (the old code merged them and lost that information —
|
|
77
|
+
* and never even read devDependencies).
|
|
78
|
+
*/
|
|
79
|
+
export declare class DeclaredDeps {
|
|
80
|
+
dependencies: string[];
|
|
81
|
+
devDependencies: string[];
|
|
82
|
+
peerDependencies: string[];
|
|
83
|
+
constructor(dependencies: string[], devDependencies: string[], peerDependencies: string[]);
|
|
84
|
+
/** Every declared package name, deduped and sorted (all three sections). */
|
|
85
|
+
all(): string[];
|
|
86
|
+
/** Declared somewhere that survives `pnpm deploy --prod`. */
|
|
87
|
+
isProductionDeclared(packageName: string): boolean;
|
|
88
|
+
isDevDeclared(packageName: string): boolean;
|
|
89
|
+
isDeclared(packageName: string): boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Graph shape produced by graph-sorter (an input contract we never construct here).
|
|
44
93
|
*/
|
|
45
94
|
interface GraphEntry {
|
|
46
95
|
level: number;
|
|
47
96
|
dependsOn: string[];
|
|
48
97
|
}
|
|
49
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Per-project classification of graph deps against what package.json declares.
|
|
100
|
+
*/
|
|
101
|
+
declare class DepClassification {
|
|
102
|
+
/** Production-reached deps absent from `dependencies`/`peerDependencies`. */
|
|
103
|
+
missingInPackageJson: string[];
|
|
104
|
+
/** Production-reached deps declared ONLY in `devDependencies` (runtime would break). */
|
|
105
|
+
prodDepsOnlyInDev: string[];
|
|
106
|
+
/** Test-only deps declared in no section at all. */
|
|
107
|
+
missingTestOnlyDeps: string[];
|
|
108
|
+
/** Test-only deps sitting in `dependencies` — i.e. shipped to production. */
|
|
109
|
+
testOnlyInProdDependencies: string[];
|
|
110
|
+
/** Non-workspace (third-party) package.json entries — informational only. */
|
|
111
|
+
extraInPackageJson: string[];
|
|
112
|
+
/** Workspace entries the graph cannot reach at all — warn-only drift. */
|
|
113
|
+
extraWorkspaceDeps: string[];
|
|
114
|
+
}
|
|
115
|
+
declare class SingleProjectValidation {
|
|
116
|
+
result: ProjectValidationResult;
|
|
117
|
+
errors: string[];
|
|
118
|
+
warnings: string[];
|
|
119
|
+
constructor(result: ProjectValidationResult, errors: string[], warnings: string[]);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The per-workspace lookups a single-project validation needs, passed as one object
|
|
123
|
+
* so method signatures stay readable.
|
|
124
|
+
*/
|
|
125
|
+
declare class ValidationContext {
|
|
126
|
+
graph: Record<string, GraphEntry>;
|
|
127
|
+
projectToPackage: Map<string, string>;
|
|
128
|
+
packageToProject: Map<string, string>;
|
|
129
|
+
options: PackageValidatorOptions;
|
|
130
|
+
constructor(graph: Record<string, GraphEntry>, projectToPackage: Map<string, string>, packageToProject: Map<string, string>, options: PackageValidatorOptions);
|
|
131
|
+
}
|
|
132
|
+
export declare class PackageValidator {
|
|
133
|
+
private readonly scanner;
|
|
134
|
+
/**
|
|
135
|
+
* Read the three dependency sections of a project's package.json.
|
|
136
|
+
* Returns null when there is no package.json (apps often have none) so the caller
|
|
137
|
+
* can skip the project entirely.
|
|
138
|
+
*/
|
|
139
|
+
readDeclaredDeps(workspaceRoot: string, projectRoot: string): DeclaredDeps | null;
|
|
140
|
+
private namesOf;
|
|
141
|
+
/**
|
|
142
|
+
* Build map of project names to their package names
|
|
143
|
+
* e.g., "core-util" → "@webpieces/core-util"
|
|
144
|
+
*/
|
|
145
|
+
buildProjectToPackageMap(workspaceRoot: string, projectsConfig: any): Map<string, string>;
|
|
146
|
+
private readPackageName;
|
|
147
|
+
/**
|
|
148
|
+
* Compute the transitive closure of a project's dependencies in the graph.
|
|
149
|
+
* Example: server → [core-meta, http-server]; the closure includes http-server and
|
|
150
|
+
* everything http-server reaches.
|
|
151
|
+
*
|
|
152
|
+
* Used to allow package.json entries for transitive deps (a legitimate pattern:
|
|
153
|
+
* npm install brings the whole dependency tree, so a consumer may list any reachable
|
|
154
|
+
* package directly).
|
|
155
|
+
*/
|
|
156
|
+
computeTransitiveClosure(projectName: string, graph: Record<string, GraphEntry>): Set<string>;
|
|
157
|
+
/**
|
|
158
|
+
* Split a project's graph deps into "declared correctly", "missing", and "declared in
|
|
159
|
+
* the wrong section", using the import scan to decide which section each dep belongs in.
|
|
160
|
+
*/
|
|
161
|
+
classifyDeps(declared: DeclaredDeps, usage: DepUsage, entry: GraphEntry, transitiveClosure: Set<string>, context: ValidationContext): DepClassification;
|
|
162
|
+
/**
|
|
163
|
+
* The heart of the fix: which SECTION does this dep belong in?
|
|
164
|
+
*
|
|
165
|
+
* A dep is test-only when the scan saw it imported by test/dev files and by NO
|
|
166
|
+
* production file. Anything else — including a dep we never saw imported at all (it
|
|
167
|
+
* may be loaded reflectively at runtime) — is treated as production, so this can
|
|
168
|
+
* never push a runtime-required package out of `dependencies`.
|
|
169
|
+
*/
|
|
170
|
+
private classifyOneDep;
|
|
171
|
+
validateSingleProject(projectName: string, entry: GraphEntry, projectRoot: string, declared: DeclaredDeps, usage: DepUsage, context: ValidationContext): SingleProjectValidation;
|
|
172
|
+
private buildErrors;
|
|
173
|
+
private buildWarnings;
|
|
174
|
+
private testOnlyInProdMessage;
|
|
175
|
+
validate(graph: Record<string, GraphEntry>, workspaceRoot: string, options: PackageValidatorOptions): Promise<ValidationResult>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Validate that package.json dependencies cover the dependency graph AND that each dep
|
|
179
|
+
* is declared in the correct section (dependencies vs devDependencies).
|
|
180
|
+
*
|
|
181
|
+
* @param graph - Enhanced graph with project dependencies (uses project names)
|
|
182
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
183
|
+
* @param options - Strictness of the "test-only dep in the production closure" check
|
|
184
|
+
*/
|
|
185
|
+
export declare function validatePackageJsonDependencies(graph: Record<string, GraphEntry>, workspaceRoot: string, options?: PackageValidatorOptions): Promise<ValidationResult>;
|
|
50
186
|
export {};
|