@webpieces/nx-webpieces-rules 0.0.1

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.
Files changed (61) hide show
  1. package/LICENSE +373 -0
  2. package/executors.json +124 -0
  3. package/package.json +36 -0
  4. package/src/executor-result.ts +7 -0
  5. package/src/executors/generate/executor.ts +61 -0
  6. package/src/executors/generate/schema.json +14 -0
  7. package/src/executors/help/executor.ts +63 -0
  8. package/src/executors/help/schema.json +7 -0
  9. package/src/executors/validate-architecture-unchanged/executor.ts +253 -0
  10. package/src/executors/validate-architecture-unchanged/schema.json +14 -0
  11. package/src/executors/validate-catch-error-pattern/executor.ts +11 -0
  12. package/src/executors/validate-catch-error-pattern/schema.json +24 -0
  13. package/src/executors/validate-code/executor.ts +11 -0
  14. package/src/executors/validate-code/schema.json +287 -0
  15. package/src/executors/validate-dtos/executor.ts +11 -0
  16. package/src/executors/validate-dtos/schema.json +33 -0
  17. package/src/executors/validate-eslint-sync/executor.ts +87 -0
  18. package/src/executors/validate-eslint-sync/schema.json +7 -0
  19. package/src/executors/validate-modified-files/executor.ts +11 -0
  20. package/src/executors/validate-modified-files/schema.json +25 -0
  21. package/src/executors/validate-modified-methods/executor.ts +11 -0
  22. package/src/executors/validate-modified-methods/schema.json +25 -0
  23. package/src/executors/validate-new-methods/executor.ts +11 -0
  24. package/src/executors/validate-new-methods/schema.json +25 -0
  25. package/src/executors/validate-no-any-unknown/executor.ts +11 -0
  26. package/src/executors/validate-no-any-unknown/schema.json +24 -0
  27. package/src/executors/validate-no-architecture-cycles/executor.ts +63 -0
  28. package/src/executors/validate-no-architecture-cycles/schema.json +8 -0
  29. package/src/executors/validate-no-destructure/executor.ts +11 -0
  30. package/src/executors/validate-no-destructure/schema.json +24 -0
  31. package/src/executors/validate-no-direct-api-resolver/executor.ts +11 -0
  32. package/src/executors/validate-no-direct-api-resolver/schema.json +29 -0
  33. package/src/executors/validate-no-implicit-any/executor.ts +11 -0
  34. package/src/executors/validate-no-implicit-any/schema.json +24 -0
  35. package/src/executors/validate-no-inline-types/executor.ts +11 -0
  36. package/src/executors/validate-no-inline-types/schema.json +24 -0
  37. package/src/executors/validate-no-skiplevel-deps/executor.ts +274 -0
  38. package/src/executors/validate-no-skiplevel-deps/schema.json +8 -0
  39. package/src/executors/validate-no-unmanaged-exceptions/executor.ts +11 -0
  40. package/src/executors/validate-no-unmanaged-exceptions/schema.json +24 -0
  41. package/src/executors/validate-packagejson/executor.ts +76 -0
  42. package/src/executors/validate-packagejson/schema.json +8 -0
  43. package/src/executors/validate-prisma-converters/executor.ts +11 -0
  44. package/src/executors/validate-prisma-converters/schema.json +38 -0
  45. package/src/executors/validate-return-types/executor.ts +11 -0
  46. package/src/executors/validate-return-types/schema.json +24 -0
  47. package/src/executors/validate-ts-in-src/executor.ts +283 -0
  48. package/src/executors/validate-ts-in-src/schema.json +25 -0
  49. package/src/executors/validate-versions-locked/executor.ts +376 -0
  50. package/src/executors/validate-versions-locked/schema.json +8 -0
  51. package/src/executors/visualize/executor.ts +65 -0
  52. package/src/executors/visualize/schema.json +14 -0
  53. package/src/index.ts +9 -0
  54. package/src/lib/graph-comparator.ts +154 -0
  55. package/src/lib/graph-generator.ts +97 -0
  56. package/src/lib/graph-loader.ts +119 -0
  57. package/src/lib/graph-sorter.ts +137 -0
  58. package/src/lib/graph-visualizer.ts +253 -0
  59. package/src/lib/package-validator.ts +184 -0
  60. package/src/plugin.ts +666 -0
  61. package/src/toError.ts +36 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,666 @@
1
+ /**
2
+ * Unified Nx Inference Plugin for @webpieces/nx-webpieces-rules
3
+ *
4
+ * This plugin automatically creates targets for:
5
+ * 1. Workspace-level architecture validation (generate, visualize, validate-*)
6
+ * 2. Per-project circular dependency checking
7
+ *
8
+ * Install with: nx add @webpieces/nx-webpieces-rules
9
+ *
10
+ * Usage:
11
+ * Add to nx.json plugins array:
12
+ * {
13
+ * "plugins": ["@webpieces/nx-webpieces-rules"]
14
+ * }
15
+ *
16
+ * Then all targets appear automatically without manual project.json configuration.
17
+ */
18
+
19
+ import { dirname, join } from 'path';
20
+ import { existsSync } from 'fs';
21
+ import type {
22
+ CreateNodesV2,
23
+ CreateNodesContextV2,
24
+ CreateNodesResultV2,
25
+ CreateNodesResult,
26
+ TargetConfiguration,
27
+ } from '@nx/devkit';
28
+
29
+ /**
30
+ * Circular dependency checking options
31
+ */
32
+ export interface CircularDepsOptions {
33
+ enabled?: boolean;
34
+ targetName?: string;
35
+ excludePatterns?: string[];
36
+ }
37
+
38
+ /**
39
+ * Validation options for architecture checks
40
+ */
41
+ export interface ValidationOptions {
42
+ noCycles?: boolean;
43
+ noSkipLevelDeps?: boolean;
44
+ architectureUnchanged?: boolean;
45
+ validatePackageJson?: boolean;
46
+ validateNewMethods?: boolean;
47
+ validateModifiedMethods?: boolean;
48
+ validateModifiedFiles?: boolean;
49
+ validateVersionsLocked?: boolean;
50
+ validateTsInSrc?: boolean;
51
+ newMethodsMaxLines?: number;
52
+ modifiedAndNewMethodsMaxLines?: number;
53
+ modifiedFilesMaxLines?: number;
54
+ /**
55
+ * Validation mode for method/file size limits:
56
+ * - STRICT: All limits enforced, disable comments ignored
57
+ * - NORMAL: Limits enforced, disable comments with dates work
58
+ * - OFF: Skip size validations entirely (for fast iteration)
59
+ */
60
+ validationMode?: 'STRICT' | 'NORMAL' | 'OFF';
61
+ }
62
+
63
+ /**
64
+ * Feature flags for workspace targets
65
+ */
66
+ export interface FeatureOptions {
67
+ generate?: boolean;
68
+ visualize?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Workspace-level configuration options
73
+ */
74
+ export interface WorkspaceOptions {
75
+ enabled?: boolean;
76
+ targetPrefix?: string;
77
+ graphPath?: string;
78
+ validations?: ValidationOptions;
79
+ features?: FeatureOptions;
80
+ }
81
+
82
+ /**
83
+ * Configuration for @webpieces/nx-webpieces-rules Nx plugin
84
+ */
85
+ export interface ArchitecturePluginOptions {
86
+ circularDeps?: CircularDepsOptions;
87
+ workspace?: WorkspaceOptions;
88
+ }
89
+
90
+ const DEFAULT_OPTIONS: Required<ArchitecturePluginOptions> = {
91
+ circularDeps: {
92
+ enabled: true,
93
+ targetName: 'validate-no-file-import-cycles',
94
+ excludePatterns: [],
95
+ },
96
+ workspace: {
97
+ enabled: true,
98
+ targetPrefix: 'arch:',
99
+ graphPath: 'architecture/dependencies.json',
100
+ validations: {
101
+ noCycles: true,
102
+ noSkipLevelDeps: true,
103
+ architectureUnchanged: true,
104
+ validatePackageJson: true,
105
+ validateNewMethods: true,
106
+ validateModifiedMethods: true,
107
+ validateModifiedFiles: true,
108
+ validateVersionsLocked: true,
109
+ validateTsInSrc: true,
110
+ newMethodsMaxLines: 30,
111
+ modifiedAndNewMethodsMaxLines: 80,
112
+ modifiedFilesMaxLines: 900,
113
+ validationMode: 'NORMAL',
114
+ },
115
+ features: {
116
+ generate: true,
117
+ visualize: true,
118
+ },
119
+ },
120
+ };
121
+
122
+ function normalizeOptions(
123
+ options: ArchitecturePluginOptions | undefined,
124
+ ): Required<ArchitecturePluginOptions> {
125
+ const circularDeps = {
126
+ ...DEFAULT_OPTIONS.circularDeps,
127
+ ...options?.circularDeps,
128
+ };
129
+
130
+ const workspace = {
131
+ ...DEFAULT_OPTIONS.workspace,
132
+ ...options?.workspace,
133
+ validations: {
134
+ ...DEFAULT_OPTIONS.workspace.validations,
135
+ ...options?.workspace?.validations,
136
+ },
137
+ features: {
138
+ ...DEFAULT_OPTIONS.workspace.features,
139
+ ...options?.workspace?.features,
140
+ },
141
+ };
142
+
143
+ return {
144
+ circularDeps,
145
+ workspace,
146
+ } as Required<ArchitecturePluginOptions>;
147
+ }
148
+
149
+ async function createNodesFunction(
150
+ projectFiles: readonly string[],
151
+ options: ArchitecturePluginOptions | undefined,
152
+ context: CreateNodesContextV2,
153
+ ): Promise<CreateNodesResultV2> {
154
+ const opts = normalizeOptions(options);
155
+ const results: CreateNodesResultV2 = [];
156
+
157
+ // Add workspace-level architecture targets
158
+ addArchitectureProject(results, projectFiles, opts, context);
159
+
160
+ // Add per-project targets (circular-deps, ci)
161
+ addPerProjectTargets(results, projectFiles, opts, context);
162
+
163
+ return results;
164
+ }
165
+
166
+ function addArchitectureProject(
167
+ results: CreateNodesResultV2,
168
+ projectFiles: readonly string[],
169
+ opts: Required<ArchitecturePluginOptions>,
170
+ context: CreateNodesContextV2,
171
+ ): void {
172
+ if (!opts.workspace.enabled) return;
173
+
174
+ const archDirPath = join(context.workspaceRoot, 'architecture');
175
+ if (!existsSync(archDirPath)) return;
176
+
177
+ const workspaceTargets = createWorkspaceTargetsWithoutPrefix(opts);
178
+ if (Object.keys(workspaceTargets).length === 0) return;
179
+
180
+ const result: CreateNodesResult = {
181
+ projects: {
182
+ architecture: {
183
+ name: 'architecture',
184
+ root: 'architecture',
185
+ targets: workspaceTargets,
186
+ },
187
+ },
188
+ };
189
+
190
+ const firstProjectFile = projectFiles[0];
191
+ if (firstProjectFile) {
192
+ results.push([firstProjectFile, result] as const);
193
+ }
194
+ }
195
+
196
+ function addPerProjectTargets(
197
+ results: CreateNodesResultV2,
198
+ projectFiles: readonly string[],
199
+ opts: Required<ArchitecturePluginOptions>,
200
+ context: CreateNodesContextV2,
201
+ ): void {
202
+ // Track processed project roots to avoid duplicates when both files exist
203
+ const processedRoots = new Set<string>();
204
+
205
+ for (const projectFile of projectFiles) {
206
+ const isProjectJson = projectFile.endsWith('project.json');
207
+ const isPackageJson = projectFile.endsWith('package.json');
208
+
209
+ if (!isProjectJson && !isPackageJson) continue;
210
+
211
+ const projectRoot = dirname(projectFile);
212
+
213
+ // Skip root (workspace manifest, not a project)
214
+ if (projectRoot === '.') continue;
215
+
216
+ // Skip if we've already processed this project root
217
+ if (processedRoots.has(projectRoot)) continue;
218
+
219
+ // For package.json, skip if project.json also exists in same directory
220
+ // (prefer project.json - it will be processed separately)
221
+ if (isPackageJson) {
222
+ const projectJsonPath = join(context.workspaceRoot, projectRoot, 'project.json');
223
+ if (existsSync(projectJsonPath)) continue;
224
+ }
225
+
226
+ processedRoots.add(projectRoot);
227
+
228
+ const targets: Record<string, TargetConfiguration> = {};
229
+
230
+ // Add circular-deps target ONLY for project.json projects
231
+ // (package.json-only projects may not have TypeScript source)
232
+ if (isProjectJson && opts.circularDeps.enabled) {
233
+ if (!isExcluded(projectRoot, opts.circularDeps.excludePatterns!)) {
234
+ const targetName = opts.circularDeps.targetName!;
235
+ targets[targetName] = createCircularDepsTarget(projectRoot, targetName);
236
+ }
237
+ }
238
+
239
+ // Add ci target to ALL projects (both project.json and package.json)
240
+ targets['ci'] = createCiTarget();
241
+
242
+ if (Object.keys(targets).length === 0) continue;
243
+
244
+ const result: CreateNodesResult = {
245
+ projects: {
246
+ [projectRoot]: {
247
+ targets,
248
+ },
249
+ },
250
+ };
251
+
252
+ results.push([projectFile, result] as const);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Nx V2 Inference Plugin
258
+ * Matches project.json and package.json files to create targets
259
+ */
260
+ export const createNodesV2: CreateNodesV2<ArchitecturePluginOptions> = [
261
+ // Pattern to match project.json and package.json files
262
+ '**/{project,package}.json',
263
+
264
+ // Inference function
265
+ createNodesFunction,
266
+ ];
267
+
268
+ /**
269
+ * Build list of enabled validation target names for validate-complete dependency chain
270
+ */
271
+ function buildValidationTargetsList(
272
+ validations: Required<ArchitecturePluginOptions>['workspace']['validations'],
273
+ ): string[] {
274
+ const targets: string[] = [];
275
+ if (validations!.noCycles) targets.push('validate-no-architecture-cycles');
276
+ if (validations!.architectureUnchanged) targets.push('validate-architecture-unchanged');
277
+ if (validations!.noSkipLevelDeps) targets.push('validate-no-skiplevel-deps');
278
+ if (validations!.validatePackageJson) targets.push('validate-packagejson');
279
+ // Use combined validate-code instead of 3 separate targets
280
+ if (
281
+ validations!.validateNewMethods ||
282
+ validations!.validateModifiedMethods ||
283
+ validations!.validateModifiedFiles
284
+ ) {
285
+ targets.push('validate-code');
286
+ }
287
+ if (validations!.validateVersionsLocked) targets.push('validate-versions-locked');
288
+ if (validations!.validateTsInSrc) targets.push('validate-ts-in-src');
289
+ return targets;
290
+ }
291
+
292
+ /**
293
+ * Create workspace-level architecture validation targets WITHOUT prefix
294
+ * Used for virtual 'architecture' project
295
+ */
296
+ function createWorkspaceTargetsWithoutPrefix(
297
+ opts: Required<ArchitecturePluginOptions>,
298
+ ): Record<string, TargetConfiguration> {
299
+ const targets: Record<string, TargetConfiguration> = {};
300
+ const graphPath = opts.workspace.graphPath!;
301
+ const validations = opts.workspace.validations!;
302
+
303
+ // Add help target (always available)
304
+ targets['help'] = createHelpTarget();
305
+
306
+ if (opts.workspace.features!.generate) {
307
+ targets['generate'] = createGenerateTarget(graphPath);
308
+ }
309
+ if (opts.workspace.features!.visualize) {
310
+ targets['visualize'] = createVisualizeTargetWithoutPrefix(graphPath);
311
+ }
312
+ if (validations.noCycles) {
313
+ targets['validate-no-architecture-cycles'] = createValidateNoCyclesTarget();
314
+ }
315
+ if (validations.architectureUnchanged) {
316
+ targets['validate-architecture-unchanged'] = createValidateUnchangedTarget(graphPath);
317
+ }
318
+ if (validations.noSkipLevelDeps) {
319
+ targets['validate-no-skiplevel-deps'] = createValidateNoSkipLevelTarget();
320
+ }
321
+ if (validations.validatePackageJson) {
322
+ targets['validate-packagejson'] = createValidatePackageJsonTarget();
323
+ }
324
+ // Use combined validate-code instead of 3 separate targets
325
+ // Options come from webpieces.config.json at the workspace root
326
+ // (loaded via @webpieces/rules-config; same source of truth as @webpieces/ai-hook-rules)
327
+ if (
328
+ validations.validateNewMethods ||
329
+ validations.validateModifiedMethods ||
330
+ validations.validateModifiedFiles
331
+ ) {
332
+ targets['validate-code'] = createValidateCodeTarget();
333
+ }
334
+ if (validations.validateVersionsLocked) {
335
+ targets['validate-versions-locked'] = createValidateVersionsLockedTarget();
336
+ }
337
+ if (validations.validateTsInSrc) {
338
+ targets['validate-ts-in-src'] = createValidateTsInSrcTarget();
339
+ }
340
+
341
+ // Add validate-complete target that runs all enabled validations
342
+ const validationTargets = buildValidationTargetsList(validations);
343
+ if (validationTargets.length > 0) {
344
+ targets['validate-complete'] = createValidateCompleteTarget(validationTargets);
345
+ }
346
+
347
+ return targets;
348
+ }
349
+
350
+ /**
351
+ * Create workspace-level architecture validation targets (DEPRECATED - keeping for backward compat)
352
+ * Used when root project.json exists (old style with '.' project)
353
+ */
354
+ function createWorkspaceTargets(
355
+ opts: Required<ArchitecturePluginOptions>,
356
+ ): Record<string, TargetConfiguration> {
357
+ const targets: Record<string, TargetConfiguration> = {};
358
+ const prefix = opts.workspace.targetPrefix!;
359
+ const graphPath = opts.workspace.graphPath!;
360
+
361
+ // Add help target (always available)
362
+ targets[`${prefix}help`] = createHelpTarget();
363
+
364
+ if (opts.workspace.features!.generate) {
365
+ targets[`${prefix}generate`] = createGenerateTarget(graphPath);
366
+ }
367
+
368
+ if (opts.workspace.features!.visualize) {
369
+ targets[`${prefix}visualize`] = createVisualizeTarget(prefix, graphPath);
370
+ }
371
+
372
+ if (opts.workspace.validations!.noCycles) {
373
+ targets[`${prefix}validate-no-architecture-cycles`] = createValidateNoCyclesTarget();
374
+ }
375
+
376
+ if (opts.workspace.validations!.architectureUnchanged) {
377
+ targets[`${prefix}validate-architecture-unchanged`] =
378
+ createValidateUnchangedTarget(graphPath);
379
+ }
380
+
381
+ if (opts.workspace.validations!.noSkipLevelDeps) {
382
+ targets[`${prefix}validate-no-skiplevel-deps`] = createValidateNoSkipLevelTarget();
383
+ }
384
+
385
+ if (opts.workspace.validations!.validatePackageJson) {
386
+ targets[`${prefix}validate-packagejson`] = createValidatePackageJsonTarget();
387
+ }
388
+
389
+ // Use combined validate-code instead of 3 separate targets
390
+ // Options come from webpieces.config.json at the workspace root
391
+ // (loaded via @webpieces/rules-config; same source of truth as @webpieces/ai-hook-rules)
392
+ if (
393
+ opts.workspace.validations!.validateNewMethods ||
394
+ opts.workspace.validations!.validateModifiedMethods ||
395
+ opts.workspace.validations!.validateModifiedFiles
396
+ ) {
397
+ targets[`${prefix}validate-code`] = createValidateCodeTarget();
398
+ }
399
+
400
+ return targets;
401
+ }
402
+
403
+ function createGenerateTarget(graphPath: string): TargetConfiguration {
404
+ return {
405
+ executor: '@webpieces/nx-webpieces-rules:generate',
406
+ cache: false,
407
+ outputs: ['{workspaceRoot}/architecture/dependencies.json'],
408
+ options: { graphPath },
409
+ metadata: {
410
+ technologies: ['nx'],
411
+ description: 'Generate the architecture dependency graph from project.json files',
412
+ },
413
+ };
414
+ }
415
+
416
+ function createVisualizeTargetWithoutPrefix(graphPath: string): TargetConfiguration {
417
+ return {
418
+ executor: '@webpieces/nx-webpieces-rules:visualize',
419
+ dependsOn: ['generate'],
420
+ options: { graphPath },
421
+ metadata: {
422
+ technologies: ['nx'],
423
+ description: 'Generate visual representations of the architecture graph',
424
+ },
425
+ };
426
+ }
427
+
428
+ function createVisualizeTarget(prefix: string, graphPath: string): TargetConfiguration {
429
+ return {
430
+ executor: '@webpieces/nx-webpieces-rules:visualize',
431
+ dependsOn: [`${prefix}generate`],
432
+ options: { graphPath },
433
+ metadata: {
434
+ technologies: ['nx'],
435
+ description: 'Generate visual representations of the architecture graph',
436
+ },
437
+ };
438
+ }
439
+
440
+ function createValidateNoCyclesTarget(): TargetConfiguration {
441
+ return {
442
+ executor: '@webpieces/nx-webpieces-rules:validate-no-architecture-cycles',
443
+ cache: true,
444
+ inputs: ['{workspaceRoot}/**/project.json', '{workspaceRoot}/architecture/dependencies.json'],
445
+ metadata: {
446
+ technologies: ['nx'],
447
+ description: 'Validate the architecture has no circular project dependencies',
448
+ },
449
+ };
450
+ }
451
+
452
+ function createValidateUnchangedTarget(graphPath: string): TargetConfiguration {
453
+ return {
454
+ executor: '@webpieces/nx-webpieces-rules:validate-architecture-unchanged',
455
+ cache: false,
456
+ inputs: ['default', '{workspaceRoot}/architecture/dependencies.json'],
457
+ options: { graphPath },
458
+ metadata: {
459
+ technologies: ['nx'],
460
+ description: 'Validate the architecture matches the saved blessed graph',
461
+ },
462
+ };
463
+ }
464
+
465
+ function createValidateNoSkipLevelTarget(): TargetConfiguration {
466
+ return {
467
+ executor: '@webpieces/nx-webpieces-rules:validate-no-skiplevel-deps',
468
+ cache: true,
469
+ inputs: ['{workspaceRoot}/**/project.json', '{workspaceRoot}/architecture/dependencies.json'],
470
+ metadata: {
471
+ technologies: ['nx'],
472
+ description: 'Validate no project has redundant transitive dependencies',
473
+ },
474
+ };
475
+ }
476
+
477
+ function createValidatePackageJsonTarget(): TargetConfiguration {
478
+ return {
479
+ executor: '@webpieces/nx-webpieces-rules:validate-packagejson',
480
+ cache: true,
481
+ inputs: ['{workspaceRoot}/**/project.json', '{workspaceRoot}/**/package.json'],
482
+ metadata: {
483
+ technologies: ['nx'],
484
+ description: 'Validate package.json dependencies match project.json build dependencies',
485
+ },
486
+ };
487
+ }
488
+
489
+ function createValidateNewMethodsTarget(
490
+ maxLines: number,
491
+ mode: 'STRICT' | 'NORMAL' | 'OFF',
492
+ ): TargetConfiguration {
493
+ return {
494
+ executor: '@webpieces/nx-webpieces-rules:validate-new-methods',
495
+ cache: false, // Don't cache - depends on git state
496
+ inputs: ['default'],
497
+ options: { max: maxLines, mode },
498
+ metadata: {
499
+ technologies: ['nx'],
500
+ description: `Validate new methods do not exceed ${maxLines} lines (only runs in affected mode)`,
501
+ },
502
+ };
503
+ }
504
+
505
+ function createValidateModifiedMethodsTarget(
506
+ maxLines: number,
507
+ mode: 'STRICT' | 'NORMAL' | 'OFF',
508
+ ): TargetConfiguration {
509
+ return {
510
+ executor: '@webpieces/nx-webpieces-rules:validate-modified-methods',
511
+ cache: false, // Don't cache - depends on git state
512
+ inputs: ['default'],
513
+ options: { max: maxLines, mode },
514
+ metadata: {
515
+ technologies: ['nx'],
516
+ description: `Validate new and modified methods do not exceed ${maxLines} lines (encourages gradual cleanup)`,
517
+ },
518
+ };
519
+ }
520
+
521
+ function createValidateModifiedFilesTarget(
522
+ maxLines: number,
523
+ mode: 'STRICT' | 'NORMAL' | 'OFF',
524
+ ): TargetConfiguration {
525
+ return {
526
+ executor: '@webpieces/nx-webpieces-rules:validate-modified-files',
527
+ cache: false, // Don't cache - depends on git state
528
+ inputs: ['default'],
529
+ options: { max: maxLines, mode },
530
+ metadata: {
531
+ technologies: ['nx'],
532
+ description: `Validate modified files do not exceed ${maxLines} lines (encourages keeping files small)`,
533
+ },
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Create combined validate-code target
539
+ * Options come from webpieces.config.json at the workspace root
540
+ * (loaded by the executor via @webpieces/rules-config — same source of truth as @webpieces/ai-hook-rules)
541
+ */
542
+ function createValidateCodeTarget(): TargetConfiguration {
543
+ return {
544
+ executor: '@webpieces/nx-webpieces-rules:validate-code',
545
+ cache: false, // Don't cache - depends on git state
546
+ inputs: ['default', '{workspaceRoot}/webpieces.config.json'],
547
+ // No options here - they come from webpieces.config.json at runtime
548
+ metadata: {
549
+ technologies: ['nx'],
550
+ description: 'Combined validation for new methods, modified methods, and file sizes',
551
+ },
552
+ };
553
+ }
554
+
555
+ function createValidateVersionsLockedTarget(): TargetConfiguration {
556
+ return {
557
+ executor: '@webpieces/nx-webpieces-rules:validate-versions-locked',
558
+ cache: true,
559
+ inputs: ['{workspaceRoot}/**/package.json'],
560
+ metadata: {
561
+ technologies: ['nx'],
562
+ description:
563
+ 'Validate package.json versions are locked (no semver ranges) and consistent across projects',
564
+ },
565
+ };
566
+ }
567
+
568
+ function createValidateTsInSrcTarget(): TargetConfiguration {
569
+ return {
570
+ executor: '@webpieces/nx-webpieces-rules:validate-ts-in-src',
571
+ cache: false,
572
+ inputs: ['default', '{workspaceRoot}/webpieces.config.json'],
573
+ metadata: {
574
+ technologies: ['nx'],
575
+ description: 'Validate all .ts files in projects are inside the src/ directory',
576
+ },
577
+ };
578
+ }
579
+
580
+ function createValidateCompleteTarget(validationTargets: string[]): TargetConfiguration {
581
+ return {
582
+ executor: 'nx:noop',
583
+ cache: true,
584
+ dependsOn: validationTargets,
585
+ metadata: {
586
+ technologies: ['nx'],
587
+ description: 'Run all architecture validations (cycles, unchanged, skip-level deps)',
588
+ },
589
+ };
590
+ }
591
+
592
+ /**
593
+ * Create per-project ci target - Gradle-style composite target
594
+ * Runs lint, build, and test in parallel
595
+ * (with test depending on build via targetDefaults)
596
+ *
597
+ * NOTE: Type checking is done by the build target (@nx/js:tsc) during compilation.
598
+ */
599
+ function createCiTarget(): TargetConfiguration {
600
+ return {
601
+ executor: 'nx:noop',
602
+ cache: true,
603
+ dependsOn: ['lint', 'build', 'test'],
604
+ metadata: {
605
+ technologies: ['nx'],
606
+ description: 'Run all CI checks: lint, build, and test (Gradle-style composite target)',
607
+ },
608
+ };
609
+ }
610
+
611
+ function createHelpTarget(): TargetConfiguration {
612
+ return {
613
+ executor: '@webpieces/nx-webpieces-rules:help',
614
+ cache: false, // Never cache - always show help output
615
+ metadata: {
616
+ technologies: ['nx'],
617
+ description: 'Display help for @webpieces/nx-webpieces-rules commands and targets',
618
+ },
619
+ };
620
+ }
621
+
622
+ /**
623
+ * Create per-project circular dependency checking target
624
+ * Runs on project root (.) to check ALL TypeScript files in the project
625
+ */
626
+ function createCircularDepsTarget(projectRoot: string, targetName: string): TargetConfiguration {
627
+ return {
628
+ executor: 'nx:run-commands',
629
+ cache: true,
630
+ inputs: ['default'],
631
+ outputs: [] as string[],
632
+ options: {
633
+ command: 'npx madge --circular --extensions ts,tsx .',
634
+ cwd: projectRoot,
635
+ },
636
+ metadata: {
637
+ technologies: ['madge'],
638
+ description: 'Check for circular dependencies using madge',
639
+ },
640
+ };
641
+ }
642
+
643
+ /**
644
+ * Check if a project should be excluded based on patterns
645
+ */
646
+ function isExcluded(projectRoot: string, excludePatterns: string[]): boolean {
647
+ if (excludePatterns.length === 0) {
648
+ return false;
649
+ }
650
+
651
+ // Simple glob matching (could be enhanced with minimatch if needed)
652
+ return excludePatterns.some((pattern) => {
653
+ // Convert glob pattern to regex
654
+ const regexPattern = pattern
655
+ .replace(/\*\*/g, '.*') // ** matches any path
656
+ .replace(/\*/g, '[^/]*'); // * matches any string except /
657
+
658
+ const regex = new RegExp(`^${regexPattern}$`);
659
+ return regex.test(projectRoot);
660
+ });
661
+ }
662
+
663
+ /**
664
+ * Export plugin as default for Nx
665
+ */
666
+ export default { createNodesV2 };
package/src/toError.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Lightweight duplicate of @webpieces/core-util toError for use in dev-config.
3
+ * dev-config is Level 0 and cannot depend on core-util (also Level 0).
4
+ */
5
+ // webpieces-disable no-any-unknown -- toError intentionally accepts unknown to safely convert any thrown value to Error
6
+ export function toError(err: unknown): Error {
7
+ if (err instanceof Error) {
8
+ return err;
9
+ }
10
+
11
+ if (err && typeof err === 'object') {
12
+ if ('message' in err) {
13
+ const error = new Error(String(err.message));
14
+
15
+ if ('stack' in err && typeof err.stack === 'string') {
16
+ error.stack = err.stack;
17
+ }
18
+ if ('name' in err && typeof err.name === 'string') {
19
+ error.name = err.name;
20
+ }
21
+ return error;
22
+ }
23
+
24
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- must handle circular references without recursion
25
+ try {
26
+ return new Error(`Non-Error object thrown: ${JSON.stringify(err)}`);
27
+ } catch (err: unknown) {
28
+ //const error = toError(err);
29
+ void err;
30
+ return new Error('Non-Error object thrown (unable to stringify)');
31
+ }
32
+ }
33
+
34
+ const message = err == null ? 'Null or undefined thrown' : String(err);
35
+ return new Error(message);
36
+ }