@webpieces/dev-config 0.2.94 → 0.2.97

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 (183) hide show
  1. package/config/eslint/base.mjs +1 -1
  2. package/executors.json +6 -91
  3. package/package.json +6 -19
  4. package/{executors → src/executors}/help/executor.d.ts +4 -2
  5. package/src/executors/help/executor.js.map +1 -0
  6. package/{executors → src/executors}/validate-eslint-sync/executor.d.ts +3 -2
  7. package/src/executors/validate-eslint-sync/executor.js.map +1 -0
  8. package/{executors → src/executors}/validate-versions-locked/executor.js +5 -3
  9. package/src/executors/validate-versions-locked/executor.js.map +1 -0
  10. package/src/generators/init/generator.js.map +1 -1
  11. package/src/index.d.ts +1 -1
  12. package/src/index.js +1 -1
  13. package/src/index.js.map +1 -1
  14. package/src/plugin.d.ts +86 -0
  15. package/{plugin.js → src/plugin.js} +31 -15
  16. package/src/plugin.js.map +1 -0
  17. package/src/toError.d.ts +5 -0
  18. package/src/toError.js +37 -0
  19. package/src/toError.js.map +1 -0
  20. package/templates/eslint.webpieces.config.mjs +1 -1
  21. package/templates/webpieces.exceptions.md +15 -15
  22. package/architecture/executors/diff-utils.d.ts +0 -24
  23. package/architecture/executors/diff-utils.js +0 -119
  24. package/architecture/executors/diff-utils.js.map +0 -1
  25. package/architecture/executors/diff-utils.ts +0 -127
  26. package/architecture/executors/generate/executor.d.ts +0 -16
  27. package/architecture/executors/generate/executor.js +0 -44
  28. package/architecture/executors/generate/executor.js.map +0 -1
  29. package/architecture/executors/generate/executor.ts +0 -59
  30. package/architecture/executors/generate/schema.json +0 -14
  31. package/architecture/executors/validate-architecture-unchanged/executor.d.ts +0 -17
  32. package/architecture/executors/validate-architecture-unchanged/executor.js +0 -229
  33. package/architecture/executors/validate-architecture-unchanged/executor.js.map +0 -1
  34. package/architecture/executors/validate-architecture-unchanged/executor.ts +0 -251
  35. package/architecture/executors/validate-architecture-unchanged/schema.json +0 -14
  36. package/architecture/executors/validate-code/executor.d.ts +0 -78
  37. package/architecture/executors/validate-code/executor.js +0 -243
  38. package/architecture/executors/validate-code/executor.js.map +0 -1
  39. package/architecture/executors/validate-code/executor.ts +0 -406
  40. package/architecture/executors/validate-code/schema.json +0 -227
  41. package/architecture/executors/validate-dtos/executor.d.ts +0 -42
  42. package/architecture/executors/validate-dtos/executor.js +0 -561
  43. package/architecture/executors/validate-dtos/executor.js.map +0 -1
  44. package/architecture/executors/validate-dtos/executor.ts +0 -689
  45. package/architecture/executors/validate-dtos/schema.json +0 -33
  46. package/architecture/executors/validate-modified-files/executor.d.ts +0 -25
  47. package/architecture/executors/validate-modified-files/executor.js +0 -501
  48. package/architecture/executors/validate-modified-files/executor.js.map +0 -1
  49. package/architecture/executors/validate-modified-files/executor.ts +0 -571
  50. package/architecture/executors/validate-modified-files/schema.json +0 -25
  51. package/architecture/executors/validate-modified-methods/executor.d.ts +0 -31
  52. package/architecture/executors/validate-modified-methods/executor.js +0 -694
  53. package/architecture/executors/validate-modified-methods/executor.js.map +0 -1
  54. package/architecture/executors/validate-modified-methods/executor.ts +0 -797
  55. package/architecture/executors/validate-modified-methods/schema.json +0 -25
  56. package/architecture/executors/validate-new-methods/executor.d.ts +0 -28
  57. package/architecture/executors/validate-new-methods/executor.js +0 -513
  58. package/architecture/executors/validate-new-methods/executor.js.map +0 -1
  59. package/architecture/executors/validate-new-methods/executor.ts +0 -584
  60. package/architecture/executors/validate-new-methods/schema.json +0 -25
  61. package/architecture/executors/validate-no-any-unknown/executor.d.ts +0 -42
  62. package/architecture/executors/validate-no-any-unknown/executor.js +0 -462
  63. package/architecture/executors/validate-no-any-unknown/executor.js.map +0 -1
  64. package/architecture/executors/validate-no-any-unknown/executor.ts +0 -540
  65. package/architecture/executors/validate-no-any-unknown/schema.json +0 -24
  66. package/architecture/executors/validate-no-architecture-cycles/executor.d.ts +0 -16
  67. package/architecture/executors/validate-no-architecture-cycles/executor.js +0 -48
  68. package/architecture/executors/validate-no-architecture-cycles/executor.js.map +0 -1
  69. package/architecture/executors/validate-no-architecture-cycles/executor.ts +0 -60
  70. package/architecture/executors/validate-no-architecture-cycles/schema.json +0 -8
  71. package/architecture/executors/validate-no-destructure/executor.d.ts +0 -52
  72. package/architecture/executors/validate-no-destructure/executor.js +0 -491
  73. package/architecture/executors/validate-no-destructure/executor.js.map +0 -1
  74. package/architecture/executors/validate-no-destructure/executor.ts +0 -578
  75. package/architecture/executors/validate-no-destructure/schema.json +0 -24
  76. package/architecture/executors/validate-no-direct-api-resolver/executor.d.ts +0 -47
  77. package/architecture/executors/validate-no-direct-api-resolver/executor.js +0 -566
  78. package/architecture/executors/validate-no-direct-api-resolver/executor.js.map +0 -1
  79. package/architecture/executors/validate-no-direct-api-resolver/executor.ts +0 -666
  80. package/architecture/executors/validate-no-direct-api-resolver/schema.json +0 -29
  81. package/architecture/executors/validate-no-inline-types/executor.d.ts +0 -91
  82. package/architecture/executors/validate-no-inline-types/executor.js +0 -669
  83. package/architecture/executors/validate-no-inline-types/executor.js.map +0 -1
  84. package/architecture/executors/validate-no-inline-types/executor.ts +0 -775
  85. package/architecture/executors/validate-no-inline-types/schema.json +0 -24
  86. package/architecture/executors/validate-no-skiplevel-deps/executor.d.ts +0 -19
  87. package/architecture/executors/validate-no-skiplevel-deps/executor.js +0 -227
  88. package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +0 -1
  89. package/architecture/executors/validate-no-skiplevel-deps/executor.ts +0 -267
  90. package/architecture/executors/validate-no-skiplevel-deps/schema.json +0 -8
  91. package/architecture/executors/validate-packagejson/executor.d.ts +0 -16
  92. package/architecture/executors/validate-packagejson/executor.js +0 -57
  93. package/architecture/executors/validate-packagejson/executor.js.map +0 -1
  94. package/architecture/executors/validate-packagejson/executor.ts +0 -74
  95. package/architecture/executors/validate-packagejson/schema.json +0 -8
  96. package/architecture/executors/validate-prisma-converters/executor.d.ts +0 -60
  97. package/architecture/executors/validate-prisma-converters/executor.js +0 -634
  98. package/architecture/executors/validate-prisma-converters/executor.js.map +0 -1
  99. package/architecture/executors/validate-prisma-converters/executor.ts +0 -822
  100. package/architecture/executors/validate-prisma-converters/schema.json +0 -38
  101. package/architecture/executors/validate-return-types/executor.d.ts +0 -29
  102. package/architecture/executors/validate-return-types/executor.js +0 -439
  103. package/architecture/executors/validate-return-types/executor.js.map +0 -1
  104. package/architecture/executors/validate-return-types/executor.ts +0 -524
  105. package/architecture/executors/validate-return-types/schema.json +0 -24
  106. package/architecture/executors/visualize/executor.d.ts +0 -17
  107. package/architecture/executors/visualize/executor.js +0 -49
  108. package/architecture/executors/visualize/executor.js.map +0 -1
  109. package/architecture/executors/visualize/executor.ts +0 -63
  110. package/architecture/executors/visualize/schema.json +0 -14
  111. package/architecture/index.d.ts +0 -19
  112. package/architecture/index.js +0 -23
  113. package/architecture/index.js.map +0 -1
  114. package/architecture/index.ts +0 -20
  115. package/architecture/lib/graph-comparator.d.ts +0 -39
  116. package/architecture/lib/graph-comparator.js +0 -100
  117. package/architecture/lib/graph-comparator.js.map +0 -1
  118. package/architecture/lib/graph-comparator.ts +0 -141
  119. package/architecture/lib/graph-generator.d.ts +0 -19
  120. package/architecture/lib/graph-generator.js +0 -84
  121. package/architecture/lib/graph-generator.js.map +0 -1
  122. package/architecture/lib/graph-generator.ts +0 -97
  123. package/architecture/lib/graph-loader.d.ts +0 -31
  124. package/architecture/lib/graph-loader.js +0 -98
  125. package/architecture/lib/graph-loader.js.map +0 -1
  126. package/architecture/lib/graph-loader.ts +0 -116
  127. package/architecture/lib/graph-sorter.d.ts +0 -37
  128. package/architecture/lib/graph-sorter.js +0 -110
  129. package/architecture/lib/graph-sorter.js.map +0 -1
  130. package/architecture/lib/graph-sorter.ts +0 -137
  131. package/architecture/lib/graph-visualizer.d.ts +0 -29
  132. package/architecture/lib/graph-visualizer.js +0 -217
  133. package/architecture/lib/graph-visualizer.js.map +0 -1
  134. package/architecture/lib/graph-visualizer.ts +0 -231
  135. package/architecture/lib/package-validator.d.ts +0 -38
  136. package/architecture/lib/package-validator.js +0 -126
  137. package/architecture/lib/package-validator.js.map +0 -1
  138. package/architecture/lib/package-validator.ts +0 -170
  139. package/eslint-plugin/__tests__/catch-error-pattern.test.ts +0 -359
  140. package/eslint-plugin/__tests__/max-file-lines.test.ts +0 -207
  141. package/eslint-plugin/__tests__/max-method-lines.test.ts +0 -258
  142. package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +0 -359
  143. package/eslint-plugin/index.d.ts +0 -23
  144. package/eslint-plugin/index.js +0 -30
  145. package/eslint-plugin/index.js.map +0 -1
  146. package/eslint-plugin/index.ts +0 -29
  147. package/eslint-plugin/rules/catch-error-pattern.d.ts +0 -11
  148. package/eslint-plugin/rules/catch-error-pattern.js +0 -196
  149. package/eslint-plugin/rules/catch-error-pattern.js.map +0 -1
  150. package/eslint-plugin/rules/catch-error-pattern.ts +0 -281
  151. package/eslint-plugin/rules/enforce-architecture.d.ts +0 -15
  152. package/eslint-plugin/rules/enforce-architecture.js +0 -476
  153. package/eslint-plugin/rules/enforce-architecture.js.map +0 -1
  154. package/eslint-plugin/rules/enforce-architecture.ts +0 -543
  155. package/eslint-plugin/rules/max-file-lines.d.ts +0 -12
  156. package/eslint-plugin/rules/max-file-lines.js +0 -257
  157. package/eslint-plugin/rules/max-file-lines.js.map +0 -1
  158. package/eslint-plugin/rules/max-file-lines.ts +0 -272
  159. package/eslint-plugin/rules/max-method-lines.d.ts +0 -12
  160. package/eslint-plugin/rules/max-method-lines.js +0 -240
  161. package/eslint-plugin/rules/max-method-lines.js.map +0 -1
  162. package/eslint-plugin/rules/max-method-lines.ts +0 -287
  163. package/eslint-plugin/rules/no-unmanaged-exceptions.d.ts +0 -22
  164. package/eslint-plugin/rules/no-unmanaged-exceptions.js +0 -160
  165. package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +0 -1
  166. package/eslint-plugin/rules/no-unmanaged-exceptions.ts +0 -179
  167. package/executors/help/executor.js.map +0 -1
  168. package/executors/help/executor.ts +0 -61
  169. package/executors/validate-eslint-sync/executor.js.map +0 -1
  170. package/executors/validate-eslint-sync/executor.ts +0 -83
  171. package/executors/validate-versions-locked/executor.js.map +0 -1
  172. package/executors/validate-versions-locked/executor.ts +0 -367
  173. package/plugin/README.md +0 -243
  174. package/plugin/index.d.ts +0 -4
  175. package/plugin/index.js +0 -8
  176. package/plugin/index.js.map +0 -1
  177. package/plugin/index.ts +0 -4
  178. /package/{executors → src/executors}/help/executor.js +0 -0
  179. /package/{executors → src/executors}/help/schema.json +0 -0
  180. /package/{executors → src/executors}/validate-eslint-sync/executor.js +0 -0
  181. /package/{executors → src/executors}/validate-eslint-sync/schema.json +0 -0
  182. /package/{executors → src/executors}/validate-versions-locked/executor.d.ts +0 -0
  183. /package/{executors → src/executors}/validate-versions-locked/schema.json +0 -0
@@ -1,822 +0,0 @@
1
- /**
2
- * Validate Prisma Converters Executor
3
- *
4
- * Validates that Prisma converter methods follow a scalable pattern:
5
- * methods returning XxxDto (where XxxDbo exists in schema.prisma) must
6
- * accept that exact XxxDbo as the first parameter. This keeps single-table
7
- * converters clean and forces join converters to compose them.
8
- *
9
- * ============================================================================
10
- * RULES
11
- * ============================================================================
12
- *
13
- * 1. First param must be exact Dbo:
14
- * If method returns XxxDto and XxxDbo exists in schema.prisma,
15
- * the first parameter must be of type XxxDbo.
16
- *
17
- * 2. Extra params must be booleans:
18
- * Additional parameters beyond the Dbo are allowed but must be boolean
19
- * (used for filtering payloads / security info).
20
- *
21
- * 3. No async converters:
22
- * Methods returning Promise<XxxDto> are flagged — converters should be
23
- * pure data mapping, no async work.
24
- *
25
- * 4. No standalone functions:
26
- * Standalone functions in converter files are flagged — must be class
27
- * methods so the converter class can be injected (dependency tree tracing).
28
- *
29
- * 5. Dto creation outside converters directory:
30
- * Files outside the configured convertersPath that create `new XxxDto(...)`
31
- * where XxxDbo exists in schema.prisma are flagged — Dto instances tied to
32
- * a Dbo must only be created via a converter class.
33
- *
34
- * ============================================================================
35
- * SKIP CONDITIONS
36
- * ============================================================================
37
- * - Methods with @deprecated decorator or JSDoc tag
38
- * - Lines with: // webpieces-disable prisma-converter -- [reason]
39
- *
40
- * ============================================================================
41
- * MODES
42
- * ============================================================================
43
- * - OFF: Skip validation entirely
44
- * - NEW_AND_MODIFIED_METHODS: Validate new/modified methods in converters + changed lines in non-converters
45
- * - MODIFIED_FILES: Validate all methods in modified files
46
- */
47
-
48
- import type { ExecutorContext } from '@nx/devkit';
49
- import { execSync } from 'child_process';
50
- import * as fs from 'fs';
51
- import * as path from 'path';
52
- import * as ts from 'typescript';
53
- import { getFileDiff, getChangedLineNumbers, findNewMethodSignaturesInDiff, isNewOrModified } from '../diff-utils';
54
-
55
- export type PrismaConverterMode = 'OFF' | 'NEW_AND_MODIFIED_METHODS' | 'MODIFIED_FILES';
56
-
57
- export interface ValidatePrismaConvertersOptions {
58
- mode?: PrismaConverterMode;
59
- disableAllowed?: boolean;
60
- schemaPath?: string;
61
- convertersPaths?: string[];
62
- enforcePaths?: string[];
63
- ignoreModifiedUntilEpoch?: number;
64
- }
65
-
66
- export interface ExecutorResult {
67
- success: boolean;
68
- }
69
-
70
- interface PrismaConverterViolation {
71
- file: string;
72
- line: number;
73
- message: string;
74
- }
75
-
76
- interface UnwrapResult {
77
- inner: string;
78
- isAsync: boolean;
79
- }
80
-
81
- interface FileContext {
82
- filePath: string;
83
- fileLines: string[];
84
- sourceFile: ts.SourceFile;
85
- prismaModels: Set<string>;
86
- disableAllowed: boolean;
87
- }
88
-
89
- /**
90
- * Auto-detect the base branch by finding the merge-base with origin/main.
91
- */
92
- function detectBase(workspaceRoot: string): string | null {
93
- try {
94
- const mergeBase = execSync('git merge-base HEAD origin/main', {
95
- cwd: workspaceRoot,
96
- encoding: 'utf-8',
97
- stdio: ['pipe', 'pipe', 'pipe'],
98
- }).trim();
99
-
100
- if (mergeBase) {
101
- return mergeBase;
102
- }
103
- } catch {
104
- try {
105
- const mergeBase = execSync('git merge-base HEAD main', {
106
- cwd: workspaceRoot,
107
- encoding: 'utf-8',
108
- stdio: ['pipe', 'pipe', 'pipe'],
109
- }).trim();
110
-
111
- if (mergeBase) {
112
- return mergeBase;
113
- }
114
- } catch {
115
- // Ignore
116
- }
117
- }
118
- return null;
119
- }
120
-
121
- /**
122
- * Get changed TypeScript files between base and head (or working tree if head not specified).
123
- */
124
- // webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
125
- function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head?: string): string[] {
126
- try {
127
- const diffTarget = head ? `${base} ${head}` : base;
128
- const output = execSync(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
129
- cwd: workspaceRoot,
130
- encoding: 'utf-8',
131
- });
132
- const changedFiles = output
133
- .trim()
134
- .split('\n')
135
- .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
136
-
137
- if (!head) {
138
- try {
139
- const untrackedOutput = execSync(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
140
- cwd: workspaceRoot,
141
- encoding: 'utf-8',
142
- });
143
- const untrackedFiles = untrackedOutput
144
- .trim()
145
- .split('\n')
146
- .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
147
- const allFiles = new Set([...changedFiles, ...untrackedFiles]);
148
- return Array.from(allFiles);
149
- } catch {
150
- return changedFiles;
151
- }
152
- }
153
-
154
- return changedFiles;
155
- } catch {
156
- return [];
157
- }
158
- }
159
-
160
- /**
161
- * Parse schema.prisma to extract all model names into a Set.
162
- */
163
- function parsePrismaModels(schemaPath: string): Set<string> {
164
- const models = new Set<string>();
165
-
166
- if (!fs.existsSync(schemaPath)) {
167
- return models;
168
- }
169
-
170
- const content = fs.readFileSync(schemaPath, 'utf-8');
171
- const regex = /^model\s+(\w+)\s*\{/gm;
172
- let match: RegExpExecArray | null;
173
-
174
- while ((match = regex.exec(content)) !== null) {
175
- models.add(match[1]);
176
- }
177
-
178
- return models;
179
- }
180
-
181
- /**
182
- * Derive the expected Dbo name from a return type ending in Dto.
183
- * "XxxDto" -> "XxxDbo". Returns null if name doesn't end with Dto.
184
- */
185
- function deriveExpectedDboName(returnType: string): string | null {
186
- if (!returnType.endsWith('Dto')) return null;
187
- return returnType.slice(0, -3) + 'Dbo';
188
- }
189
-
190
- /**
191
- * Check if a line has a webpieces-disable comment for prisma-converter.
192
- */
193
- function hasDisableComment(lines: string[], lineNumber: number): boolean {
194
- const startCheck = Math.max(0, lineNumber - 5);
195
- for (let i = lineNumber - 2; i >= startCheck; i--) {
196
- const line = lines[i]?.trim() ?? '';
197
- if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {
198
- break;
199
- }
200
- if (line.includes('webpieces-disable') && line.includes('prisma-converter')) {
201
- return true;
202
- }
203
- }
204
- return false;
205
- }
206
-
207
- /**
208
- * Check if a method/function node has a @deprecated decorator.
209
- */
210
- function hasDeprecatedDecorator(node: ts.MethodDeclaration | ts.FunctionDeclaration): boolean {
211
- const modifiers = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
212
- if (!modifiers) return false;
213
-
214
- for (const decorator of modifiers) {
215
- const expr = decorator.expression;
216
- // @deprecated or @deprecated()
217
- if (ts.isIdentifier(expr) && expr.text === 'deprecated') return true;
218
- if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === 'deprecated') {
219
- return true;
220
- }
221
- }
222
- return false;
223
- }
224
-
225
- /**
226
- * Check if a node has @deprecated in its JSDoc comments.
227
- */
228
- function hasDeprecatedJsDoc(node: ts.Node): boolean {
229
- const jsDocs = ts.getJSDocTags(node);
230
- for (const tag of jsDocs) {
231
- if (tag.tagName.text === 'deprecated') return true;
232
- }
233
- return false;
234
- }
235
-
236
- /**
237
- * Check if a method is deprecated via decorator or JSDoc.
238
- */
239
- function isDeprecated(node: ts.MethodDeclaration | ts.FunctionDeclaration): boolean {
240
- return hasDeprecatedDecorator(node) || hasDeprecatedJsDoc(node);
241
- }
242
-
243
- /**
244
- * Extract the text of a type node, stripping whitespace.
245
- */
246
- function getTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): string {
247
- return typeNode.getText(sourceFile).trim();
248
- }
249
-
250
- /**
251
- * Unwrap Promise<T> to get T. Returns the inner type text if wrapped, otherwise returns as-is.
252
- */
253
- function unwrapPromise(typeText: string): UnwrapResult {
254
- const promiseMatch = typeText.match(/^Promise\s*<\s*(.+)\s*>$/);
255
- if (promiseMatch) {
256
- return { inner: promiseMatch[1].trim(), isAsync: true };
257
- }
258
- return { inner: typeText, isAsync: false };
259
- }
260
-
261
- /**
262
- * Check a standalone function declaration in a converter file and return a violation if applicable.
263
- */
264
- function checkStandaloneFunction(
265
- node: ts.FunctionDeclaration,
266
- ctx: FileContext
267
- ): PrismaConverterViolation | null {
268
- if (!node.name) return null;
269
-
270
- const startPos = node.getStart(ctx.sourceFile);
271
- const pos = ctx.sourceFile.getLineAndCharacterOfPosition(startPos);
272
- const line = pos.line + 1;
273
-
274
- if ((ctx.disableAllowed && hasDisableComment(ctx.fileLines, line)) || isDeprecated(node)) return null;
275
-
276
- return {
277
- file: ctx.filePath,
278
- line,
279
- message: `Standalone function "${node.name.text}" found in converter file. ` +
280
- 'Move to a converter class so it can be injected via DI.',
281
- };
282
- }
283
-
284
- /**
285
- * Validate the parameters of a converter method that returns a Dto with a matching Dbo.
286
- */
287
- function checkMethodParams(
288
- node: ts.MethodDeclaration,
289
- innerType: string,
290
- expectedDbo: string,
291
- ctx: FileContext,
292
- line: number
293
- ): PrismaConverterViolation[] {
294
- const violations: PrismaConverterViolation[] = [];
295
- const params = node.parameters;
296
-
297
- if (params.length === 0) {
298
- violations.push({
299
- file: ctx.filePath,
300
- line,
301
- message: `Method returns "${innerType}" but has no parameters. ` +
302
- `First parameter must be of type "${expectedDbo}".`,
303
- });
304
- return violations;
305
- }
306
-
307
- const firstParam = params[0];
308
- if (firstParam.type) {
309
- const firstParamType = getTypeText(firstParam.type, ctx.sourceFile);
310
- if (firstParamType !== expectedDbo) {
311
- violations.push({
312
- file: ctx.filePath,
313
- line,
314
- message: `Method returns "${innerType}" but first parameter is "${firstParamType}". ` +
315
- `First parameter must be of type "${expectedDbo}".`,
316
- });
317
- }
318
- }
319
-
320
- for (let i = 1; i < params.length; i++) {
321
- const param = params[i];
322
- if (param.type) {
323
- const paramType = getTypeText(param.type, ctx.sourceFile);
324
- if (paramType !== 'boolean') {
325
- const paramName = param.name.getText(ctx.sourceFile);
326
- violations.push({
327
- file: ctx.filePath,
328
- line,
329
- message: `Extra parameter "${paramName}" has type "${paramType}" but must be "boolean". ` +
330
- 'Additional converter parameters are only for boolean flags (payload filtering / security).',
331
- });
332
- }
333
- }
334
- }
335
-
336
- return violations;
337
- }
338
-
339
- /**
340
- * Check a class method declaration for converter pattern violations.
341
- */
342
- function checkConverterMethod(
343
- node: ts.MethodDeclaration,
344
- ctx: FileContext
345
- ): PrismaConverterViolation[] {
346
- if (!node.name || !node.type) return [];
347
-
348
- const startPos = node.getStart(ctx.sourceFile);
349
- const pos = ctx.sourceFile.getLineAndCharacterOfPosition(startPos);
350
- const line = pos.line + 1;
351
-
352
- if ((ctx.disableAllowed && hasDisableComment(ctx.fileLines, line)) || isDeprecated(node)) return [];
353
-
354
- const returnTypeText = getTypeText(node.type, ctx.sourceFile);
355
- const { inner: innerType, isAsync } = unwrapPromise(returnTypeText);
356
- const expectedDbo = deriveExpectedDboName(innerType);
357
-
358
- if (!expectedDbo || !ctx.prismaModels.has(expectedDbo)) return [];
359
-
360
- if (isAsync) {
361
- return [{
362
- file: ctx.filePath,
363
- line,
364
- message: `Async converter method returning "Promise<${innerType}>" found. ` +
365
- 'Converters should be pure data mapping with no async work. Remove async/Promise.',
366
- }];
367
- }
368
-
369
- return checkMethodParams(node, innerType, expectedDbo, ctx, line);
370
- }
371
-
372
- /**
373
- * Find converter method violations in a single file.
374
- * Checks class methods for proper Dbo parameter patterns and flags standalone functions.
375
- */
376
- function findConverterViolationsInFile(
377
- filePath: string,
378
- workspaceRoot: string,
379
- prismaModels: Set<string>,
380
- disableAllowed: boolean
381
- ): PrismaConverterViolation[] {
382
- const fullPath = path.join(workspaceRoot, filePath);
383
- if (!fs.existsSync(fullPath)) return [];
384
-
385
- const content = fs.readFileSync(fullPath, 'utf-8');
386
- const ctx: FileContext = {
387
- filePath,
388
- fileLines: content.split('\n'),
389
- sourceFile: ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true),
390
- prismaModels,
391
- disableAllowed,
392
- };
393
-
394
- const violations: PrismaConverterViolation[] = [];
395
-
396
- function visitNode(node: ts.Node): void {
397
- if (ts.isFunctionDeclaration(node)) {
398
- const violation = checkStandaloneFunction(node, ctx);
399
- if (violation) violations.push(violation);
400
- }
401
-
402
- if (ts.isMethodDeclaration(node)) {
403
- violations.push(...checkConverterMethod(node, ctx));
404
- }
405
-
406
- ts.forEachChild(node, visitNode);
407
- }
408
-
409
- visitNode(ctx.sourceFile);
410
- return violations;
411
- }
412
-
413
- /**
414
- * Find violations in non-converter files: creating `new XxxDto(...)` where XxxDbo exists in prisma.
415
- * These Dto instances must only be created inside converter classes.
416
- */
417
- // webpieces-disable max-lines-new-methods -- AST traversal for new-expression detection with prisma model matching
418
- function findDtoCreationOutsideConverters(
419
- filePath: string,
420
- workspaceRoot: string,
421
- prismaModels: Set<string>,
422
- convertersPaths: string[],
423
- disableAllowed: boolean
424
- ): PrismaConverterViolation[] {
425
- const fullPath = path.join(workspaceRoot, filePath);
426
- if (!fs.existsSync(fullPath)) return [];
427
-
428
- const content = fs.readFileSync(fullPath, 'utf-8');
429
- const fileLines = content.split('\n');
430
- const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
431
-
432
- const violations: PrismaConverterViolation[] = [];
433
-
434
- function visitNode(node: ts.Node): void {
435
- // Detect `new XxxDto(...)` expressions
436
- if (ts.isNewExpression(node) && ts.isIdentifier(node.expression)) {
437
- const className = node.expression.text;
438
- const expectedDbo = deriveExpectedDboName(className);
439
-
440
- if (expectedDbo && prismaModels.has(expectedDbo)) {
441
- const startPos = node.getStart(sourceFile);
442
- const pos = sourceFile.getLineAndCharacterOfPosition(startPos);
443
- const line = pos.line + 1;
444
-
445
- if (!disableAllowed || !hasDisableComment(fileLines, line)) {
446
- const dirs = convertersPaths.map((p) => `"${p}"`).join(', ');
447
- violations.push({
448
- file: filePath,
449
- line,
450
- message: `"${className}" can only be created from its Dbo using a converter in one of these directories: ${dirs}. ` +
451
- 'Move this Dto construction into a converter class method.',
452
- });
453
- }
454
- }
455
- }
456
-
457
- ts.forEachChild(node, visitNode);
458
- }
459
-
460
- visitNode(sourceFile);
461
- return violations;
462
- }
463
-
464
- /**
465
- * Find converter violations only for new/modified methods (NEW_AND_MODIFIED_METHODS mode).
466
- * For converter files: only check methods/functions that are new or have changed lines in their range.
467
- */
468
- // webpieces-disable max-lines-new-methods -- AST traversal with method boundary filtering for new/modified detection
469
- function findConverterViolationsForModifiedMethods(
470
- filePath: string,
471
- workspaceRoot: string,
472
- prismaModels: Set<string>,
473
- disableAllowed: boolean,
474
- changedLines: Set<number>,
475
- newMethodNames: Set<string>
476
- ): PrismaConverterViolation[] {
477
- const fullPath = path.join(workspaceRoot, filePath);
478
- if (!fs.existsSync(fullPath)) return [];
479
-
480
- const content = fs.readFileSync(fullPath, 'utf-8');
481
- const ctx: FileContext = {
482
- filePath,
483
- fileLines: content.split('\n'),
484
- sourceFile: ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true),
485
- prismaModels,
486
- disableAllowed,
487
- };
488
-
489
- const violations: PrismaConverterViolation[] = [];
490
-
491
- function visitNode(node: ts.Node): void {
492
- if (ts.isFunctionDeclaration(node) && node.name) {
493
- const start = ctx.sourceFile.getLineAndCharacterOfPosition(node.getStart(ctx.sourceFile));
494
- const end = ctx.sourceFile.getLineAndCharacterOfPosition(node.getEnd());
495
- if (isNewOrModified(node.name.text, start.line + 1, end.line + 1, changedLines, newMethodNames)) {
496
- const violation = checkStandaloneFunction(node, ctx);
497
- if (violation) violations.push(violation);
498
- }
499
- }
500
-
501
- if (ts.isMethodDeclaration(node) && node.name) {
502
- const start = ctx.sourceFile.getLineAndCharacterOfPosition(node.getStart(ctx.sourceFile));
503
- const end = ctx.sourceFile.getLineAndCharacterOfPosition(node.getEnd());
504
- const methodName = node.name.getText(ctx.sourceFile);
505
- if (isNewOrModified(methodName, start.line + 1, end.line + 1, changedLines, newMethodNames)) {
506
- violations.push(...checkConverterMethod(node, ctx));
507
- }
508
- }
509
-
510
- ts.forEachChild(node, visitNode);
511
- }
512
-
513
- visitNode(ctx.sourceFile);
514
- return violations;
515
- }
516
-
517
- /**
518
- * Find Dto creation violations only on changed lines (NEW_AND_MODIFIED_METHODS mode).
519
- * For non-converter files: only flag `new XxxDto(...)` on changed lines in the diff.
520
- */
521
- // webpieces-disable max-lines-new-methods -- AST traversal for new-expression detection with changed-line filtering
522
- function findDtoCreationOnChangedLines(
523
- filePath: string,
524
- workspaceRoot: string,
525
- prismaModels: Set<string>,
526
- convertersPaths: string[],
527
- disableAllowed: boolean,
528
- changedLines: Set<number>
529
- ): PrismaConverterViolation[] {
530
- const fullPath = path.join(workspaceRoot, filePath);
531
- if (!fs.existsSync(fullPath)) return [];
532
-
533
- const content = fs.readFileSync(fullPath, 'utf-8');
534
- const fileLines = content.split('\n');
535
- const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
536
-
537
- const violations: PrismaConverterViolation[] = [];
538
-
539
- function visitNode(node: ts.Node): void {
540
- if (ts.isNewExpression(node) && ts.isIdentifier(node.expression)) {
541
- const className = node.expression.text;
542
- const expectedDbo = deriveExpectedDboName(className);
543
-
544
- if (expectedDbo && prismaModels.has(expectedDbo)) {
545
- const startPos = node.getStart(sourceFile);
546
- const pos = sourceFile.getLineAndCharacterOfPosition(startPos);
547
- const line = pos.line + 1;
548
-
549
- if (changedLines.has(line) && (!disableAllowed || !hasDisableComment(fileLines, line))) {
550
- const dirs = convertersPaths.map((p) => `"${p}"`).join(', ');
551
- violations.push({
552
- file: filePath,
553
- line,
554
- message: `"${className}" can only be created from its Dbo using a converter in one of these directories: ${dirs}. ` +
555
- 'Move this Dto construction into a converter class method.',
556
- });
557
- }
558
- }
559
- }
560
-
561
- ts.forEachChild(node, visitNode);
562
- }
563
-
564
- visitNode(sourceFile);
565
- return violations;
566
- }
567
-
568
- /**
569
- * Collect violations for NEW_AND_MODIFIED_METHODS mode.
570
- * Converter files: method-level — only check new/modified methods.
571
- * Non-converter files: line-level — only flag new XxxDto() on changed lines.
572
- */
573
- // webpieces-disable max-lines-new-methods -- File classification and diff-based violation collection
574
- function collectViolationsForModifiedMethodAndCode(
575
- changedFiles: string[],
576
- convertersPaths: string[],
577
- workspaceRoot: string,
578
- prismaModels: Set<string>,
579
- disableAllowed: boolean,
580
- base: string,
581
- head: string | undefined
582
- ): PrismaConverterViolation[] {
583
- const converterFiles = changedFiles.filter((f) =>
584
- convertersPaths.some((cp) => f.startsWith(cp))
585
- );
586
- const nonConverterFiles = changedFiles.filter((f) =>
587
- !convertersPaths.some((cp) => f.startsWith(cp))
588
- );
589
-
590
- const allViolations: PrismaConverterViolation[] = [];
591
-
592
- if (converterFiles.length > 0) {
593
- console.log(`📂 Checking ${converterFiles.length} converter file(s) (new/modified methods only)...`);
594
- for (const file of converterFiles) {
595
- const diff = getFileDiff(workspaceRoot, file, base, head);
596
- const changedLines = getChangedLineNumbers(diff);
597
- const newMethodNames = findNewMethodSignaturesInDiff(diff);
598
- if (changedLines.size === 0 && newMethodNames.size === 0) continue;
599
- allViolations.push(...findConverterViolationsForModifiedMethods(
600
- file, workspaceRoot, prismaModels, disableAllowed, changedLines, newMethodNames
601
- ));
602
- }
603
- }
604
-
605
- if (nonConverterFiles.length > 0) {
606
- console.log(`📂 Checking ${nonConverterFiles.length} non-converter file(s) for Dto creation (changed lines only)...`);
607
- for (const file of nonConverterFiles) {
608
- const diff = getFileDiff(workspaceRoot, file, base, head);
609
- const changedLines = getChangedLineNumbers(diff);
610
- if (changedLines.size === 0) continue;
611
- allViolations.push(...findDtoCreationOnChangedLines(
612
- file, workspaceRoot, prismaModels, convertersPaths, disableAllowed, changedLines
613
- ));
614
- }
615
- }
616
-
617
- return allViolations;
618
- }
619
-
620
- /**
621
- * Report violations to console.
622
- */
623
- function reportViolations(violations: PrismaConverterViolation[], mode: PrismaConverterMode): void {
624
- console.error('');
625
- console.error('❌ Prisma converter violations found!');
626
- console.error('');
627
- console.error('📚 Converter methods returning XxxDto (where XxxDbo exists in schema.prisma)');
628
- console.error(' must accept XxxDbo as the first parameter. This keeps single-table');
629
- console.error(' converters clean and forces join converters to compose them.');
630
- console.error('');
631
- console.error(' GOOD: convertUserDbo(userDbo: UserDbo): UserDto { }');
632
- console.error(' GOOD: convertVersionDbo(version: VersionDbo, partial?: boolean): VersionDto { }');
633
- console.error(' GOOD: convertToJoinDto(item: SomeJoinType): CourseJoinDto { } // no matching JoinDbo');
634
- console.error('');
635
- console.error(' BAD: async convertUser(dbo: UserDbo): Promise<UserDto> { } // no async');
636
- console.error(' BAD: convertCourse(course: CourseWithMeta): CourseDto { } // wrong first param');
637
- console.error(' BAD: convertUser(dbo: UserDbo, name: string): UserDto { } // extra non-boolean');
638
- console.error(' BAD: export function convertSession(s: SessionDbo): SessionDto // standalone function');
639
- console.error('');
640
-
641
- for (const v of violations) {
642
- console.error(` ❌ ${v.file}:${v.line}`);
643
- console.error(` ${v.message}`);
644
- }
645
- console.error('');
646
-
647
- console.error(' Escape hatch (use sparingly):');
648
- console.error(' // webpieces-disable prisma-converter -- [your reason]');
649
- console.error('');
650
- console.error(` Current mode: ${mode}`);
651
- console.error('');
652
- }
653
-
654
- /**
655
- * Resolve git base ref from env vars or auto-detection.
656
- */
657
- function resolveBase(workspaceRoot: string): string | undefined {
658
- const envBase = process.env['NX_BASE'];
659
- if (envBase) return envBase;
660
- return detectBase(workspaceRoot) ?? undefined;
661
- }
662
-
663
- /**
664
- * Collect all violations from converter and non-converter files.
665
- */
666
- function collectAllViolations(
667
- changedFiles: string[],
668
- convertersPaths: string[],
669
- workspaceRoot: string,
670
- prismaModels: Set<string>,
671
- disableAllowed: boolean
672
- ): PrismaConverterViolation[] {
673
- const converterFiles = changedFiles.filter((f) =>
674
- convertersPaths.some((cp) => f.startsWith(cp))
675
- );
676
- const nonConverterFiles = changedFiles.filter((f) =>
677
- !convertersPaths.some((cp) => f.startsWith(cp))
678
- );
679
-
680
- const allViolations: PrismaConverterViolation[] = [];
681
-
682
- if (converterFiles.length > 0) {
683
- console.log(`📂 Checking ${converterFiles.length} converter file(s)...`);
684
- for (const file of converterFiles) {
685
- allViolations.push(...findConverterViolationsInFile(file, workspaceRoot, prismaModels, disableAllowed));
686
- }
687
- }
688
-
689
- if (nonConverterFiles.length > 0) {
690
- console.log(`📂 Checking ${nonConverterFiles.length} non-converter file(s) for Dto creation...`);
691
- for (const file of nonConverterFiles) {
692
- allViolations.push(...findDtoCreationOutsideConverters(file, workspaceRoot, prismaModels, convertersPaths, disableAllowed));
693
- }
694
- }
695
-
696
- return allViolations;
697
- }
698
-
699
- /**
700
- * Run validation after early-exit checks have passed.
701
- */
702
- function validateChangedFiles(
703
- workspaceRoot: string,
704
- schemaPath: string,
705
- convertersPaths: string[],
706
- enforcePaths: string[],
707
- base: string,
708
- mode: PrismaConverterMode,
709
- disableAllowed: boolean
710
- ): ExecutorResult {
711
- const head = process.env['NX_HEAD'];
712
-
713
- console.log(` Base: ${base}`);
714
- console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
715
- console.log('');
716
-
717
- const fullSchemaPath = path.join(workspaceRoot, schemaPath);
718
- const prismaModels = parsePrismaModels(fullSchemaPath);
719
-
720
- if (prismaModels.size === 0) {
721
- console.log('⏭️ No models found in schema.prisma');
722
- console.log('');
723
- return { success: true };
724
- }
725
-
726
- console.log(` Found ${prismaModels.size} model(s) in schema.prisma`);
727
-
728
- let changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
729
- if (enforcePaths.length > 0) {
730
- changedFiles = changedFiles.filter((f) =>
731
- enforcePaths.some((ep) => f.startsWith(ep))
732
- );
733
- }
734
-
735
- if (changedFiles.length === 0) {
736
- console.log('✅ No TypeScript files changed');
737
- return { success: true };
738
- }
739
-
740
- let allViolations: PrismaConverterViolation[];
741
-
742
- if (mode === 'NEW_AND_MODIFIED_METHODS') {
743
- allViolations = collectViolationsForModifiedMethodAndCode(
744
- changedFiles, convertersPaths, workspaceRoot, prismaModels, disableAllowed, base, head
745
- );
746
- } else {
747
- allViolations = collectAllViolations(changedFiles, convertersPaths, workspaceRoot, prismaModels, disableAllowed);
748
- }
749
-
750
- if (allViolations.length === 0) {
751
- console.log('✅ All converter patterns are valid');
752
- return { success: true };
753
- }
754
-
755
- reportViolations(allViolations, mode);
756
- return { success: false };
757
- }
758
-
759
- /**
760
- * Resolve mode considering ignoreModifiedUntilEpoch override.
761
- * When active, downgrades to OFF. When expired, logs a warning.
762
- */
763
- function resolvePrismaConverterMode(
764
- normalMode: PrismaConverterMode,
765
- epoch: number | undefined
766
- ): PrismaConverterMode {
767
- if (epoch === undefined || normalMode === 'OFF') {
768
- return normalMode;
769
- }
770
- const nowSeconds = Date.now() / 1000;
771
- if (nowSeconds < epoch) {
772
- const expiresDate = new Date(epoch * 1000).toISOString().split('T')[0];
773
- console.log(`\n⏭️ Skipping prisma-converter validation (ignoreModifiedUntilEpoch active, expires: ${expiresDate})`);
774
- console.log('');
775
- return 'OFF';
776
- }
777
- return normalMode;
778
- }
779
-
780
- export default async function runExecutor(
781
- options: ValidatePrismaConvertersOptions,
782
- context: ExecutorContext
783
- ): Promise<ExecutorResult> {
784
- const workspaceRoot = context.root;
785
- const mode = resolvePrismaConverterMode(options.mode ?? 'OFF', options.ignoreModifiedUntilEpoch);
786
-
787
- if (mode === 'OFF') {
788
- console.log('\n⏭️ Skipping prisma-converter validation (mode: OFF)');
789
- console.log('');
790
- return { success: true };
791
- }
792
-
793
- const schemaPath = options.schemaPath;
794
- const convertersPaths = options.convertersPaths ?? [];
795
- const enforcePaths = options.enforcePaths ?? [];
796
-
797
- if (!schemaPath || convertersPaths.length === 0) {
798
- const reason = !schemaPath ? 'no schemaPath configured' : 'no convertersPaths configured';
799
- console.log(`\n⏭️ Skipping prisma-converter validation (${reason})`);
800
- console.log('');
801
- return { success: true };
802
- }
803
-
804
- console.log('\n📏 Validating Prisma Converters\n');
805
- console.log(` Mode: ${mode}`);
806
- console.log(` Schema: ${schemaPath}`);
807
- console.log(` Converter paths: ${convertersPaths.join(', ')}`);
808
- if (enforcePaths.length > 0) {
809
- console.log(` Enforce paths: ${enforcePaths.join(', ')}`);
810
- }
811
-
812
- const base = resolveBase(workspaceRoot);
813
-
814
- if (!base) {
815
- console.log('\n⏭️ Skipping prisma-converter validation (could not detect base branch)');
816
- console.log('');
817
- return { success: true };
818
- }
819
-
820
- const disableAllowed = options.disableAllowed ?? true;
821
- return validateChangedFiles(workspaceRoot, schemaPath, convertersPaths, enforcePaths, base, mode, disableAllowed);
822
- }