@webpieces/dev-config 0.2.33 ā 0.2.35
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/architecture/lib/graph-loader.js +1 -1
- package/architecture/lib/graph-loader.js.map +1 -1
- package/architecture/lib/graph-loader.ts +1 -1
- package/executors/validate-versions/executor.d.ts +16 -0
- package/executors/validate-versions/executor.js +210 -0
- package/executors/validate-versions/executor.js.map +1 -0
- package/executors/validate-versions/executor.ts +255 -0
- package/executors/validate-versions/schema.json +8 -0
- package/executors.json +5 -0
- package/package.json +1 -1
|
@@ -57,7 +57,7 @@ function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
|
|
|
57
57
|
for (const key of sortedKeys) {
|
|
58
58
|
sortedGraph[key] = graph[key];
|
|
59
59
|
}
|
|
60
|
-
const content = JSON.stringify(sortedGraph, null,
|
|
60
|
+
const content = JSON.stringify(sortedGraph, null, 4) + '\n';
|
|
61
61
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-loader.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAkBH,4CAgBC;AASD,8BAsBC;AAKD,0CAMC;;AA1ED,+CAAyB;AACzB,mDAA6B;AAG7B;;GAEG;AACU,QAAA,kBAAkB,GAAG,gCAAgC,CAAC;AAEnE;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAC5B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IAErD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAC;IAChD,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,KAAK,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,SAAS,CACrB,KAAoB,EACpB,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEnC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,qCAAqC;IACrC,MAAM,WAAW,GAAkB,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IAC5D,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,SAAgB,eAAe,CAC3B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph } from './graph-sorter';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Load the blessed graph from disk\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n * @returns The blessed graph, or null if file doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): EnhancedGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n return JSON.parse(content) as EnhancedGraph;\n } catch (err: unknown) {\n throw new Error(`Failed to load graph from ${fullPath}: ${err}`);\n }\n}\n\n/**\n * Save the graph to disk\n *\n * @param graph - The graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = JSON.stringify(sortedGraph, null,
|
|
1
|
+
{"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-loader.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAkBH,4CAgBC;AASD,8BAsBC;AAKD,0CAMC;;AA1ED,+CAAyB;AACzB,mDAA6B;AAG7B;;GAEG;AACU,QAAA,kBAAkB,GAAG,gCAAgC,CAAC;AAEnE;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAC5B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IAErD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAC;IAChD,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,KAAK,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,SAAS,CACrB,KAAoB,EACpB,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEnC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,qCAAqC;IACrC,MAAM,WAAW,GAAkB,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IAC5D,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,SAAgB,eAAe,CAC3B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph } from './graph-sorter';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Load the blessed graph from disk\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n * @returns The blessed graph, or null if file doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): EnhancedGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n return JSON.parse(content) as EnhancedGraph;\n } catch (err: unknown) {\n throw new Error(`Failed to load graph from ${fullPath}: ${err}`);\n }\n}\n\n/**\n * Save the graph to disk\n *\n * @param graph - The graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = JSON.stringify(sortedGraph, null, 4) + '\\n';\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate Versions Executor
|
|
3
|
+
*
|
|
4
|
+
* Validates package.json versions and checks npm ci compatibility.
|
|
5
|
+
* This catches peer dependency conflicts that npm ci catches but npm install doesn't.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* nx run dev-config:validate-versions
|
|
9
|
+
*/
|
|
10
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
11
|
+
export interface ValidateVersionsOptions {
|
|
12
|
+
}
|
|
13
|
+
export interface ExecutorResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
}
|
|
16
|
+
export default function runExecutor(_options: ValidateVersionsOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validate Versions Executor
|
|
4
|
+
*
|
|
5
|
+
* Validates package.json versions and checks npm ci compatibility.
|
|
6
|
+
* This catches peer dependency conflicts that npm ci catches but npm install doesn't.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* nx run dev-config:validate-versions
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.default = runExecutor;
|
|
13
|
+
const tslib_1 = require("tslib");
|
|
14
|
+
const child_process_1 = require("child_process");
|
|
15
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
16
|
+
const path = tslib_1.__importStar(require("path"));
|
|
17
|
+
// Find all package.json files except node_modules, dist, .nx, .angular
|
|
18
|
+
function findPackageJsonFiles(dir, basePath = '') {
|
|
19
|
+
const files = [];
|
|
20
|
+
const items = fs.readdirSync(dir);
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
const fullPath = path.join(dir, item);
|
|
23
|
+
const relativePath = path.join(basePath, item);
|
|
24
|
+
// Skip these directories
|
|
25
|
+
if (['node_modules', 'dist', '.nx', '.angular', 'tmp', '.git'].includes(item)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Skip all hidden directories (starting with .)
|
|
29
|
+
if (item.startsWith('.')) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const stat = fs.statSync(fullPath);
|
|
33
|
+
if (stat.isDirectory()) {
|
|
34
|
+
files.push(...findPackageJsonFiles(fullPath, relativePath));
|
|
35
|
+
}
|
|
36
|
+
else if (item === 'package.json') {
|
|
37
|
+
files.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
// Check if a version string uses semver ranges
|
|
43
|
+
function hasSemverRange(version) {
|
|
44
|
+
// Allow workspace protocol
|
|
45
|
+
if (version.startsWith('workspace:')) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Allow file: protocol (for local packages)
|
|
49
|
+
if (version.startsWith('file:')) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// Check for common semver range patterns
|
|
53
|
+
const semverPatterns = [
|
|
54
|
+
/^\^/, // ^1.2.3
|
|
55
|
+
/^~/, // ~1.2.3
|
|
56
|
+
/^\+/, // +1.2.3
|
|
57
|
+
/^\*/, // *
|
|
58
|
+
/^>/, // >1.2.3
|
|
59
|
+
/^</, // <1.2.3
|
|
60
|
+
/^>=/, // >=1.2.3
|
|
61
|
+
/^<=/, // <=1.2.3
|
|
62
|
+
/\|\|/, // 1.2.3 || 2.x
|
|
63
|
+
/ - /, // 1.2.3 - 2.3.4
|
|
64
|
+
/^\d+\.x/, // 1.x, 1.2.x
|
|
65
|
+
/^latest$/, // latest
|
|
66
|
+
/^next$/, // next
|
|
67
|
+
];
|
|
68
|
+
return semverPatterns.some((pattern) => pattern.test(version));
|
|
69
|
+
}
|
|
70
|
+
// Validate a single package.json file for semver ranges
|
|
71
|
+
function validatePackageJson(filePath) {
|
|
72
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
75
|
+
const pkg = JSON.parse(content);
|
|
76
|
+
const errors = [];
|
|
77
|
+
// Check dependencies
|
|
78
|
+
if (pkg.dependencies) {
|
|
79
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
80
|
+
// Skip internal workspace packages
|
|
81
|
+
if (name.startsWith('@webpieces/')) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (hasSemverRange(version)) {
|
|
85
|
+
errors.push(`dependencies.${name}: "${version}" uses semver range (should be fixed version)`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Check devDependencies
|
|
90
|
+
if (pkg.devDependencies) {
|
|
91
|
+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
|
92
|
+
// Skip internal workspace packages
|
|
93
|
+
if (name.startsWith('@webpieces/')) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (hasSemverRange(version)) {
|
|
97
|
+
errors.push(`devDependencies.${name}: "${version}" uses semver range (should be fixed version)`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Check peerDependencies (these can have ranges for compatibility)
|
|
102
|
+
// We don't validate peerDependencies for semver ranges since they're meant to be flexible
|
|
103
|
+
return errors;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
//const error = toError(err);
|
|
107
|
+
return [`Failed to parse ${filePath}: ${err.message}`];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Check npm ci compatibility
|
|
111
|
+
function checkNpmCiCompatibility(workspaceRoot) {
|
|
112
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
113
|
+
try {
|
|
114
|
+
// Run npm install --package-lock-only to check for peer dependency conflicts
|
|
115
|
+
// This simulates what npm ci does without actually installing
|
|
116
|
+
// Use --ignore-scripts to prevent infinite recursion (avoid triggering preinstall hook)
|
|
117
|
+
console.log(' Running npm dependency resolution check (10s timeout)...');
|
|
118
|
+
(0, child_process_1.execSync)('npm install --package-lock-only --ignore-scripts 2>&1', {
|
|
119
|
+
cwd: workspaceRoot,
|
|
120
|
+
stdio: 'pipe',
|
|
121
|
+
encoding: 'utf-8',
|
|
122
|
+
timeout: 10000 // 10 second timeout
|
|
123
|
+
});
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
//const error = toError(err);
|
|
128
|
+
// Check if it's a timeout
|
|
129
|
+
if (err.killed) {
|
|
130
|
+
return ['npm dependency check timed out - this might indicate a hang or network issue'];
|
|
131
|
+
}
|
|
132
|
+
// Parse the error output to extract peer dependency conflicts
|
|
133
|
+
const output = err.stdout || err.stderr || err.message;
|
|
134
|
+
// Check if it's a peer dependency error (npm error, not npm warn)
|
|
135
|
+
if (output.includes('npm error') && (output.includes('ERESOLVE') || output.includes('peer'))) {
|
|
136
|
+
return [output];
|
|
137
|
+
}
|
|
138
|
+
// If it's just warnings, not errors, we're OK
|
|
139
|
+
if (output.includes('npm warn') && !output.includes('npm error')) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
// Some other error - return it
|
|
143
|
+
return [`npm dependency check failed: ${output}`];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Check semver ranges in all package.json files
|
|
147
|
+
function checkSemverRanges(workspaceRoot) {
|
|
148
|
+
console.log('\nš Checking for semver ranges (warnings only):');
|
|
149
|
+
const packageFiles = findPackageJsonFiles(workspaceRoot);
|
|
150
|
+
let semverWarnings = 0;
|
|
151
|
+
for (const filePath of packageFiles) {
|
|
152
|
+
const relativePath = path.relative(workspaceRoot, filePath);
|
|
153
|
+
const errors = validatePackageJson(filePath);
|
|
154
|
+
if (errors.length > 0) {
|
|
155
|
+
console.log(` ā ļø ${relativePath}:`);
|
|
156
|
+
for (const error of errors) {
|
|
157
|
+
console.log(` ${error}`);
|
|
158
|
+
}
|
|
159
|
+
semverWarnings += errors.length;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log(` ā
${relativePath}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (semverWarnings > 0) {
|
|
166
|
+
console.log(`\n ā ļø Note: ${semverWarnings} semver ranges found (consider using fixed versions for reproducibility)`);
|
|
167
|
+
}
|
|
168
|
+
return { warnings: semverWarnings };
|
|
169
|
+
}
|
|
170
|
+
async function runExecutor(_options, context) {
|
|
171
|
+
console.log('\nš Validating Package Versions and npm ci Compatibility\n');
|
|
172
|
+
const workspaceRoot = context.root;
|
|
173
|
+
// Step 1: Check npm ci compatibility
|
|
174
|
+
console.log('š Checking npm ci compatibility (peer dependencies):');
|
|
175
|
+
const npmCiErrors = checkNpmCiCompatibility(workspaceRoot);
|
|
176
|
+
if (npmCiErrors.length > 0) {
|
|
177
|
+
console.log(' ā npm ci compatibility check failed:');
|
|
178
|
+
console.log(' This means "npm ci" will fail in CI even though "npm install" works locally.\n');
|
|
179
|
+
for (const error of npmCiErrors) {
|
|
180
|
+
const errorLines = error.split('\n').slice(0, 30);
|
|
181
|
+
for (const line of errorLines) {
|
|
182
|
+
console.log(` ${line}`);
|
|
183
|
+
}
|
|
184
|
+
if (error.split('\n').length > 30) {
|
|
185
|
+
console.log(' ... (truncated)');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
console.log(' ā
npm ci compatibility check passed');
|
|
192
|
+
}
|
|
193
|
+
// Step 2: Check for semver ranges
|
|
194
|
+
const { warnings: semverWarnings } = checkSemverRanges(workspaceRoot);
|
|
195
|
+
const packageFiles = findPackageJsonFiles(workspaceRoot);
|
|
196
|
+
// Summary
|
|
197
|
+
console.log(`\nš Summary:`);
|
|
198
|
+
console.log(` npm ci compatibility: ${npmCiErrors.length === 0 ? 'ā
' : 'ā'}`);
|
|
199
|
+
console.log(` Files checked: ${packageFiles.length}`);
|
|
200
|
+
console.log(` Semver warnings: ${semverWarnings}`);
|
|
201
|
+
console.log(` Errors: ${npmCiErrors.length}`);
|
|
202
|
+
if (npmCiErrors.length > 0) {
|
|
203
|
+
console.log('\nā VALIDATION FAILED!');
|
|
204
|
+
console.log(' Fix peer dependency conflicts to avoid CI failures.\n');
|
|
205
|
+
return { success: false };
|
|
206
|
+
}
|
|
207
|
+
console.log('\nā
VALIDATION PASSED!');
|
|
208
|
+
return { success: true };
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/executors/validate-versions/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Versions Executor\n *\n * Validates package.json versions and checks npm ci compatibility.\n * This catches peer dependency conflicts that npm ci catches but npm install doesn't.\n *\n * Usage:\n * nx run dev-config:validate-versions\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nexport interface ValidateVersionsOptions {\n // No options needed\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n// Find all package.json files except node_modules, dist, .nx, .angular\nfunction findPackageJsonFiles(dir: string, basePath = ''): string[] {\n const files: string[] = [];\n const items = fs.readdirSync(dir);\n\n for (const item of items) {\n const fullPath = path.join(dir, item);\n const relativePath = path.join(basePath, item);\n\n // Skip these directories\n if (\n ['node_modules', 'dist', '.nx', '.angular', 'tmp', '.git'].includes(\n item,\n )\n ) {\n continue;\n }\n\n // Skip all hidden directories (starting with .)\n if (item.startsWith('.')) {\n continue;\n }\n\n const stat = fs.statSync(fullPath);\n if (stat.isDirectory()) {\n files.push(...findPackageJsonFiles(fullPath, relativePath));\n } else if (item === 'package.json') {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\n// Check if a version string uses semver ranges\nfunction hasSemverRange(version: string): boolean {\n // Allow workspace protocol\n if (version.startsWith('workspace:')) {\n return false;\n }\n\n // Allow file: protocol (for local packages)\n if (version.startsWith('file:')) {\n return false;\n }\n\n // Check for common semver range patterns\n const semverPatterns = [\n /^\\^/, // ^1.2.3\n /^~/, // ~1.2.3\n /^\\+/, // +1.2.3\n /^\\*/, // *\n /^>/, // >1.2.3\n /^</, // <1.2.3\n /^>=/, // >=1.2.3\n /^<=/, // <=1.2.3\n /\\|\\|/, // 1.2.3 || 2.x\n / - /, // 1.2.3 - 2.3.4\n /^\\d+\\.x/, // 1.x, 1.2.x\n /^latest$/, // latest\n /^next$/, // next\n ];\n\n return semverPatterns.some((pattern) => pattern.test(version));\n}\n\n// Validate a single package.json file for semver ranges\nfunction validatePackageJson(filePath: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf-8');\n const pkg = JSON.parse(content);\n const errors: string[] = [];\n\n // Check dependencies\n if (pkg.dependencies) {\n for (const [name, version] of Object.entries(pkg.dependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) {\n continue;\n }\n\n if (hasSemverRange(version as string)) {\n errors.push(\n `dependencies.${name}: \"${version}\" uses semver range (should be fixed version)`,\n );\n }\n }\n }\n\n // Check devDependencies\n if (pkg.devDependencies) {\n for (const [name, version] of Object.entries(pkg.devDependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) {\n continue;\n }\n\n if (hasSemverRange(version as string)) {\n errors.push(\n `devDependencies.${name}: \"${version}\" uses semver range (should be fixed version)`,\n );\n }\n }\n }\n\n // Check peerDependencies (these can have ranges for compatibility)\n // We don't validate peerDependencies for semver ranges since they're meant to be flexible\n\n return errors;\n } catch (err: any) {\n //const error = toError(err);\n return [`Failed to parse ${filePath}: ${err.message}`];\n }\n}\n\n// Check npm ci compatibility\nfunction checkNpmCiCompatibility(workspaceRoot: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Run npm install --package-lock-only to check for peer dependency conflicts\n // This simulates what npm ci does without actually installing\n // Use --ignore-scripts to prevent infinite recursion (avoid triggering preinstall hook)\n console.log(' Running npm dependency resolution check (10s timeout)...');\n execSync('npm install --package-lock-only --ignore-scripts 2>&1', {\n cwd: workspaceRoot,\n stdio: 'pipe',\n encoding: 'utf-8',\n timeout: 10000 // 10 second timeout\n });\n return [];\n } catch (err: any) {\n //const error = toError(err);\n // Check if it's a timeout\n if (err.killed) {\n return ['npm dependency check timed out - this might indicate a hang or network issue'];\n }\n\n // Parse the error output to extract peer dependency conflicts\n const output = err.stdout || err.stderr || err.message;\n\n // Check if it's a peer dependency error (npm error, not npm warn)\n if (output.includes('npm error') && (output.includes('ERESOLVE') || output.includes('peer'))) {\n return [output];\n }\n\n // If it's just warnings, not errors, we're OK\n if (output.includes('npm warn') && !output.includes('npm error')) {\n return [];\n }\n\n // Some other error - return it\n return [`npm dependency check failed: ${output}`];\n }\n}\n\n// Check semver ranges in all package.json files\nfunction checkSemverRanges(workspaceRoot: string): { warnings: number } {\n console.log('\\nš Checking for semver ranges (warnings only):');\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n let semverWarnings = 0;\n\n for (const filePath of packageFiles) {\n const relativePath = path.relative(workspaceRoot, filePath);\n const errors = validatePackageJson(filePath);\n\n if (errors.length > 0) {\n console.log(` ā ļø ${relativePath}:`);\n for (const error of errors) {\n console.log(` ${error}`);\n }\n semverWarnings += errors.length;\n } else {\n console.log(` ā
${relativePath}`);\n }\n }\n\n if (semverWarnings > 0) {\n console.log(`\\n ā ļø Note: ${semverWarnings} semver ranges found (consider using fixed versions for reproducibility)`);\n }\n\n return { warnings: semverWarnings };\n}\n\nexport default async function runExecutor(\n _options: ValidateVersionsOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n console.log('\\nš Validating Package Versions and npm ci Compatibility\\n');\n\n const workspaceRoot = context.root;\n\n // Step 1: Check npm ci compatibility\n console.log('š Checking npm ci compatibility (peer dependencies):');\n const npmCiErrors = checkNpmCiCompatibility(workspaceRoot);\n if (npmCiErrors.length > 0) {\n console.log(' ā npm ci compatibility check failed:');\n console.log(' This means \"npm ci\" will fail in CI even though \"npm install\" works locally.\\n');\n for (const error of npmCiErrors) {\n const errorLines = error.split('\\n').slice(0, 30);\n for (const line of errorLines) {\n console.log(` ${line}`);\n }\n if (error.split('\\n').length > 30) {\n console.log(' ... (truncated)');\n }\n }\n console.log('');\n } else {\n console.log(' ā
npm ci compatibility check passed');\n }\n\n // Step 2: Check for semver ranges\n const { warnings: semverWarnings } = checkSemverRanges(workspaceRoot);\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n\n // Summary\n console.log(`\\nš Summary:`);\n console.log(` npm ci compatibility: ${npmCiErrors.length === 0 ? 'ā
' : 'ā'}`);\n console.log(` Files checked: ${packageFiles.length}`);\n console.log(` Semver warnings: ${semverWarnings}`);\n console.log(` Errors: ${npmCiErrors.length}`);\n\n if (npmCiErrors.length > 0) {\n console.log('\\nā VALIDATION FAILED!');\n console.log(' Fix peer dependency conflicts to avoid CI failures.\\n');\n return { success: false };\n }\n\n console.log('\\nā
VALIDATION PASSED!');\n return { success: true };\n}\n"]}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate Versions Executor
|
|
3
|
+
*
|
|
4
|
+
* Validates package.json versions and checks npm ci compatibility.
|
|
5
|
+
* This catches peer dependency conflicts that npm ci catches but npm install doesn't.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* nx run dev-config:validate-versions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
export interface ValidateVersionsOptions {
|
|
17
|
+
// No options needed
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ExecutorResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Find all package.json files except node_modules, dist, .nx, .angular
|
|
25
|
+
function findPackageJsonFiles(dir: string, basePath = ''): string[] {
|
|
26
|
+
const files: string[] = [];
|
|
27
|
+
const items = fs.readdirSync(dir);
|
|
28
|
+
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
const fullPath = path.join(dir, item);
|
|
31
|
+
const relativePath = path.join(basePath, item);
|
|
32
|
+
|
|
33
|
+
// Skip these directories
|
|
34
|
+
if (
|
|
35
|
+
['node_modules', 'dist', '.nx', '.angular', 'tmp', '.git'].includes(
|
|
36
|
+
item,
|
|
37
|
+
)
|
|
38
|
+
) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Skip all hidden directories (starting with .)
|
|
43
|
+
if (item.startsWith('.')) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stat = fs.statSync(fullPath);
|
|
48
|
+
if (stat.isDirectory()) {
|
|
49
|
+
files.push(...findPackageJsonFiles(fullPath, relativePath));
|
|
50
|
+
} else if (item === 'package.json') {
|
|
51
|
+
files.push(fullPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if a version string uses semver ranges
|
|
59
|
+
function hasSemverRange(version: string): boolean {
|
|
60
|
+
// Allow workspace protocol
|
|
61
|
+
if (version.startsWith('workspace:')) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Allow file: protocol (for local packages)
|
|
66
|
+
if (version.startsWith('file:')) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for common semver range patterns
|
|
71
|
+
const semverPatterns = [
|
|
72
|
+
/^\^/, // ^1.2.3
|
|
73
|
+
/^~/, // ~1.2.3
|
|
74
|
+
/^\+/, // +1.2.3
|
|
75
|
+
/^\*/, // *
|
|
76
|
+
/^>/, // >1.2.3
|
|
77
|
+
/^</, // <1.2.3
|
|
78
|
+
/^>=/, // >=1.2.3
|
|
79
|
+
/^<=/, // <=1.2.3
|
|
80
|
+
/\|\|/, // 1.2.3 || 2.x
|
|
81
|
+
/ - /, // 1.2.3 - 2.3.4
|
|
82
|
+
/^\d+\.x/, // 1.x, 1.2.x
|
|
83
|
+
/^latest$/, // latest
|
|
84
|
+
/^next$/, // next
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
return semverPatterns.some((pattern) => pattern.test(version));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate a single package.json file for semver ranges
|
|
91
|
+
function validatePackageJson(filePath: string): string[] {
|
|
92
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
93
|
+
try {
|
|
94
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
95
|
+
const pkg = JSON.parse(content);
|
|
96
|
+
const errors: string[] = [];
|
|
97
|
+
|
|
98
|
+
// Check dependencies
|
|
99
|
+
if (pkg.dependencies) {
|
|
100
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
101
|
+
// Skip internal workspace packages
|
|
102
|
+
if (name.startsWith('@webpieces/')) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (hasSemverRange(version as string)) {
|
|
107
|
+
errors.push(
|
|
108
|
+
`dependencies.${name}: "${version}" uses semver range (should be fixed version)`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check devDependencies
|
|
115
|
+
if (pkg.devDependencies) {
|
|
116
|
+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
|
117
|
+
// Skip internal workspace packages
|
|
118
|
+
if (name.startsWith('@webpieces/')) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (hasSemverRange(version as string)) {
|
|
123
|
+
errors.push(
|
|
124
|
+
`devDependencies.${name}: "${version}" uses semver range (should be fixed version)`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check peerDependencies (these can have ranges for compatibility)
|
|
131
|
+
// We don't validate peerDependencies for semver ranges since they're meant to be flexible
|
|
132
|
+
|
|
133
|
+
return errors;
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
//const error = toError(err);
|
|
136
|
+
return [`Failed to parse ${filePath}: ${err.message}`];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check npm ci compatibility
|
|
141
|
+
function checkNpmCiCompatibility(workspaceRoot: string): string[] {
|
|
142
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
143
|
+
try {
|
|
144
|
+
// Run npm install --package-lock-only to check for peer dependency conflicts
|
|
145
|
+
// This simulates what npm ci does without actually installing
|
|
146
|
+
// Use --ignore-scripts to prevent infinite recursion (avoid triggering preinstall hook)
|
|
147
|
+
console.log(' Running npm dependency resolution check (10s timeout)...');
|
|
148
|
+
execSync('npm install --package-lock-only --ignore-scripts 2>&1', {
|
|
149
|
+
cwd: workspaceRoot,
|
|
150
|
+
stdio: 'pipe',
|
|
151
|
+
encoding: 'utf-8',
|
|
152
|
+
timeout: 10000 // 10 second timeout
|
|
153
|
+
});
|
|
154
|
+
return [];
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
//const error = toError(err);
|
|
157
|
+
// Check if it's a timeout
|
|
158
|
+
if (err.killed) {
|
|
159
|
+
return ['npm dependency check timed out - this might indicate a hang or network issue'];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Parse the error output to extract peer dependency conflicts
|
|
163
|
+
const output = err.stdout || err.stderr || err.message;
|
|
164
|
+
|
|
165
|
+
// Check if it's a peer dependency error (npm error, not npm warn)
|
|
166
|
+
if (output.includes('npm error') && (output.includes('ERESOLVE') || output.includes('peer'))) {
|
|
167
|
+
return [output];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// If it's just warnings, not errors, we're OK
|
|
171
|
+
if (output.includes('npm warn') && !output.includes('npm error')) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Some other error - return it
|
|
176
|
+
return [`npm dependency check failed: ${output}`];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check semver ranges in all package.json files
|
|
181
|
+
function checkSemverRanges(workspaceRoot: string): { warnings: number } {
|
|
182
|
+
console.log('\nš Checking for semver ranges (warnings only):');
|
|
183
|
+
const packageFiles = findPackageJsonFiles(workspaceRoot);
|
|
184
|
+
let semverWarnings = 0;
|
|
185
|
+
|
|
186
|
+
for (const filePath of packageFiles) {
|
|
187
|
+
const relativePath = path.relative(workspaceRoot, filePath);
|
|
188
|
+
const errors = validatePackageJson(filePath);
|
|
189
|
+
|
|
190
|
+
if (errors.length > 0) {
|
|
191
|
+
console.log(` ā ļø ${relativePath}:`);
|
|
192
|
+
for (const error of errors) {
|
|
193
|
+
console.log(` ${error}`);
|
|
194
|
+
}
|
|
195
|
+
semverWarnings += errors.length;
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` ā
${relativePath}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (semverWarnings > 0) {
|
|
202
|
+
console.log(`\n ā ļø Note: ${semverWarnings} semver ranges found (consider using fixed versions for reproducibility)`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { warnings: semverWarnings };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default async function runExecutor(
|
|
209
|
+
_options: ValidateVersionsOptions,
|
|
210
|
+
context: ExecutorContext
|
|
211
|
+
): Promise<ExecutorResult> {
|
|
212
|
+
console.log('\nš Validating Package Versions and npm ci Compatibility\n');
|
|
213
|
+
|
|
214
|
+
const workspaceRoot = context.root;
|
|
215
|
+
|
|
216
|
+
// Step 1: Check npm ci compatibility
|
|
217
|
+
console.log('š Checking npm ci compatibility (peer dependencies):');
|
|
218
|
+
const npmCiErrors = checkNpmCiCompatibility(workspaceRoot);
|
|
219
|
+
if (npmCiErrors.length > 0) {
|
|
220
|
+
console.log(' ā npm ci compatibility check failed:');
|
|
221
|
+
console.log(' This means "npm ci" will fail in CI even though "npm install" works locally.\n');
|
|
222
|
+
for (const error of npmCiErrors) {
|
|
223
|
+
const errorLines = error.split('\n').slice(0, 30);
|
|
224
|
+
for (const line of errorLines) {
|
|
225
|
+
console.log(` ${line}`);
|
|
226
|
+
}
|
|
227
|
+
if (error.split('\n').length > 30) {
|
|
228
|
+
console.log(' ... (truncated)');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
console.log('');
|
|
232
|
+
} else {
|
|
233
|
+
console.log(' ā
npm ci compatibility check passed');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Step 2: Check for semver ranges
|
|
237
|
+
const { warnings: semverWarnings } = checkSemverRanges(workspaceRoot);
|
|
238
|
+
const packageFiles = findPackageJsonFiles(workspaceRoot);
|
|
239
|
+
|
|
240
|
+
// Summary
|
|
241
|
+
console.log(`\nš Summary:`);
|
|
242
|
+
console.log(` npm ci compatibility: ${npmCiErrors.length === 0 ? 'ā
' : 'ā'}`);
|
|
243
|
+
console.log(` Files checked: ${packageFiles.length}`);
|
|
244
|
+
console.log(` Semver warnings: ${semverWarnings}`);
|
|
245
|
+
console.log(` Errors: ${npmCiErrors.length}`);
|
|
246
|
+
|
|
247
|
+
if (npmCiErrors.length > 0) {
|
|
248
|
+
console.log('\nā VALIDATION FAILED!');
|
|
249
|
+
console.log(' Fix peer dependency conflicts to avoid CI failures.\n');
|
|
250
|
+
return { success: false };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log('\nā
VALIDATION PASSED!');
|
|
254
|
+
return { success: true };
|
|
255
|
+
}
|
package/executors.json
CHANGED
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"implementation": "./executors/validate-eslint-sync/executor",
|
|
35
35
|
"schema": "./executors/validate-eslint-sync/schema.json",
|
|
36
36
|
"description": "Validate that workspace and template eslint.webpieces.config.mjs files have identical rules"
|
|
37
|
+
},
|
|
38
|
+
"validate-versions": {
|
|
39
|
+
"implementation": "./executors/validate-versions/executor",
|
|
40
|
+
"schema": "./executors/validate-versions/schema.json",
|
|
41
|
+
"description": "Validate package.json versions and npm ci compatibility"
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
}
|