@venizia/dev-configs 0.4.3 → 0.4.5

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 (86) hide show
  1. package/LICENSE.md +2 -5
  2. package/README.md +15 -2
  3. package/dist/checks/catalog.d.ts +39 -0
  4. package/dist/checks/catalog.d.ts.map +1 -0
  5. package/dist/checks/catalog.js +94 -0
  6. package/dist/checks/catalog.js.map +1 -0
  7. package/dist/checks/index.d.ts +15 -0
  8. package/dist/checks/index.d.ts.map +1 -0
  9. package/dist/checks/index.js +31 -0
  10. package/dist/checks/index.js.map +1 -0
  11. package/dist/checks/module-cycles.d.ts +22 -0
  12. package/dist/checks/module-cycles.d.ts.map +1 -0
  13. package/dist/checks/module-cycles.js +149 -0
  14. package/dist/checks/module-cycles.js.map +1 -0
  15. package/dist/checks/purity/manifest.d.ts +94 -0
  16. package/dist/checks/purity/manifest.d.ts.map +1 -0
  17. package/dist/checks/purity/manifest.js +177 -0
  18. package/dist/checks/purity/manifest.js.map +1 -0
  19. package/dist/checks/purity/probe.d.ts +68 -0
  20. package/dist/checks/purity/probe.d.ts.map +1 -0
  21. package/dist/checks/purity/probe.js +295 -0
  22. package/dist/checks/purity/probe.js.map +1 -0
  23. package/dist/checks/purity/run.d.ts +47 -0
  24. package/dist/checks/purity/run.d.ts.map +1 -0
  25. package/dist/checks/purity/run.js +121 -0
  26. package/dist/checks/purity/run.js.map +1 -0
  27. package/dist/checks/split-report.d.ts +19 -0
  28. package/dist/checks/split-report.d.ts.map +1 -0
  29. package/dist/checks/split-report.js +123 -0
  30. package/dist/checks/split-report.js.map +1 -0
  31. package/dist/checks/test-controls.d.ts +46 -0
  32. package/dist/checks/test-controls.d.ts.map +1 -0
  33. package/dist/checks/test-controls.js +78 -0
  34. package/dist/checks/test-controls.js.map +1 -0
  35. package/dist/checks/test-dead-code.d.ts +18 -0
  36. package/dist/checks/test-dead-code.d.ts.map +1 -0
  37. package/dist/checks/test-dead-code.js +50 -0
  38. package/dist/checks/test-dead-code.js.map +1 -0
  39. package/dist/checks/test-mutate.d.ts +58 -0
  40. package/dist/checks/test-mutate.d.ts.map +1 -0
  41. package/dist/checks/test-mutate.js +165 -0
  42. package/dist/checks/test-mutate.js.map +1 -0
  43. package/dist/checks/test-weight.d.ts +64 -0
  44. package/dist/checks/test-weight.d.ts.map +1 -0
  45. package/dist/checks/test-weight.js +124 -0
  46. package/dist/checks/test-weight.js.map +1 -0
  47. package/dist/cli/check.d.ts +14 -0
  48. package/dist/cli/check.d.ts.map +1 -0
  49. package/dist/cli/check.js +319 -0
  50. package/dist/cli/check.js.map +1 -0
  51. package/dist/cli/codemod/const-classes.d.ts +80 -0
  52. package/dist/cli/codemod/const-classes.d.ts.map +1 -0
  53. package/dist/cli/codemod/const-classes.js +603 -0
  54. package/dist/cli/codemod/const-classes.js.map +1 -0
  55. package/dist/cli/codemod/drop-redundant-casts.d.ts +112 -0
  56. package/dist/cli/codemod/drop-redundant-casts.d.ts.map +1 -0
  57. package/dist/cli/codemod/drop-redundant-casts.js +780 -0
  58. package/dist/cli/codemod/drop-redundant-casts.js.map +1 -0
  59. package/dist/cli/codemod/index.d.ts +2 -1
  60. package/dist/cli/codemod/index.d.ts.map +1 -1
  61. package/dist/cli/codemod/index.js +25 -7
  62. package/dist/cli/codemod/index.js.map +1 -1
  63. package/dist/cli/codemod/model-settings.d.ts.map +1 -1
  64. package/dist/cli/codemod/model-settings.js +39 -2
  65. package/dist/cli/codemod/model-settings.js.map +1 -1
  66. package/dist/cli/index.js +9 -1
  67. package/dist/cli/index.js.map +1 -1
  68. package/dist/testing/index.d.ts +2 -0
  69. package/dist/testing/index.d.ts.map +1 -1
  70. package/dist/testing/index.js +2 -0
  71. package/dist/testing/index.js.map +1 -1
  72. package/dist/testing/repo-tmp.d.ts +12 -0
  73. package/dist/testing/repo-tmp.d.ts.map +1 -0
  74. package/dist/testing/repo-tmp.js +67 -0
  75. package/dist/testing/repo-tmp.js.map +1 -0
  76. package/dist/testing/spawn.helper.d.ts +13 -0
  77. package/dist/testing/spawn.helper.d.ts.map +1 -0
  78. package/dist/testing/spawn.helper.js +22 -0
  79. package/dist/testing/spawn.helper.js.map +1 -0
  80. package/dist/testing/tmpdir-preload.d.ts +2 -0
  81. package/dist/testing/tmpdir-preload.d.ts.map +1 -0
  82. package/dist/testing/tmpdir-preload.js +11 -0
  83. package/dist/testing/tmpdir-preload.js.map +1 -0
  84. package/package.json +9 -1
  85. package/src/tsconfig/tsconfig.base.json +5 -2
  86. package/src/tsconfig/tsconfig.common.json +2 -2
@@ -0,0 +1,603 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConstClassesCommand = exports.ConstClasses = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const files_1 = require("./files");
7
+ const DEFAULT_IMPORT_FROM = '@venizia/ignis-helpers/common';
8
+ const TEST_FILE_PATTERN = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?tsx?$/;
9
+ /** Utility types whose type argument is a set of keys, not an enumerable value. */
10
+ const KEY_SET_TYPES = new Set(['Pick', 'Omit', 'Extract', 'Exclude', 'Record']);
11
+ /** A file that can hold a string-literal union: a quote beside a `|`. */
12
+ const UNION_TEXT = /\|\s*['"]|['"]\s*\|/;
13
+ const RESERVED_KEYS = new Set(['SCHEME_SET']);
14
+ /** The plural of a PascalCase word, for the class that holds its values. */
15
+ const pluralize = (word) => {
16
+ const alreadyPlural = /[^uis]s$/.test(word) || /ings$/.test(word);
17
+ if (alreadyPlural) {
18
+ return word;
19
+ }
20
+ if (/(s|x|z|ch|sh)$/.test(word)) {
21
+ return `${word}es`;
22
+ }
23
+ if (/[^aeiou]y$/.test(word)) {
24
+ return `${word.slice(0, -1)}ies`;
25
+ }
26
+ return `${word}s`;
27
+ };
28
+ /** `read-only`, `readOnly` and `string[]` become `READ_ONLY`, `READ_ONLY` and `STRING_ARRAY`; `undefined` when nothing is left. */
29
+ const toKey = (value) => {
30
+ const key = value
31
+ .replace(/\[\]/g, '_ARRAY_')
32
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
33
+ .replace(/[^A-Za-z0-9]+/g, '_')
34
+ .replace(/^_+|_+$/g, '')
35
+ .toUpperCase();
36
+ if (key === '') {
37
+ return undefined;
38
+ }
39
+ return /^[0-9]/.test(key) ? `_${key}` : key;
40
+ };
41
+ const pascal = (word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
42
+ /**
43
+ * Turns the string-literal unions of exported declarations into const classes: an exported type
44
+ * alias, and a property of an exported interface or object type. The values stay the same, so every
45
+ * existing literal still type-checks and no call site changes. A union it cannot name is reported.
46
+ */
47
+ class ConstClasses {
48
+ static convert(opts) {
49
+ const { typescript, fileName, source, overrides = {}, importFrom = DEFAULT_IMPORT_FROM } = opts;
50
+ const relativePath = opts.relativePath ?? fileName;
51
+ const sourceFile = typescript.createSourceFile(fileName, source, typescript.ScriptTarget.Latest, true);
52
+ const taken = ConstClasses.declaredNames({ typescript, sourceFile });
53
+ const proposals = [];
54
+ const untouched = [];
55
+ const edits = [];
56
+ const insertions = new Map();
57
+ const handled = new Set();
58
+ const report = (node, reason) => {
59
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
60
+ untouched.push({ line: line + 1, column: character + 1, reason });
61
+ };
62
+ const lineOf = (node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
63
+ /** Plans one conversion; `undefined` and a report when the names or the members do not work. */
64
+ const plan = (planOpts) => {
65
+ const { node, id, anchor } = planOpts;
66
+ const override = overrides[id] ?? {};
67
+ const union = ConstClasses.readUnion({ typescript, node });
68
+ if (override.skip) {
69
+ handled.add(node);
70
+ return;
71
+ }
72
+ const className = override.class ?? planOpts.className;
73
+ const typeName = planOpts.mayRenameType
74
+ ? (override.type ?? planOpts.typeName)
75
+ : planOpts.typeName;
76
+ const members = union.values.map((value) => ({
77
+ value,
78
+ key: override.members?.[value] ?? toKey(value) ?? '',
79
+ }));
80
+ const problem = ConstClasses.checkPlan({
81
+ className,
82
+ typeName,
83
+ members,
84
+ taken,
85
+ mayRenameType: planOpts.mayRenameType,
86
+ });
87
+ if (problem) {
88
+ report(node, `${problem}; left untouched (name it in the mapping file: ${id})`);
89
+ handled.add(node);
90
+ return;
91
+ }
92
+ taken.add(className);
93
+ if (planOpts.mayRenameType) {
94
+ taken.add(typeName);
95
+ }
96
+ handled.add(node);
97
+ proposals.push({
98
+ id,
99
+ line: lineOf(node),
100
+ className,
101
+ typeName,
102
+ members,
103
+ nullish: union.nullish,
104
+ });
105
+ const classText = ConstClasses.renderClass({ className, typeName, members });
106
+ const reference = `TConstValue<typeof ${className}>`;
107
+ const nullishText = union.nullish.map((member) => ` | ${member}`).join('');
108
+ if (planOpts.mayRenameType) {
109
+ const aliasText = `export type ${typeName} = ${reference};\n\n`;
110
+ const at = anchor.getStart(sourceFile, true);
111
+ insertions.set(at, [...(insertions.get(at) ?? []), `${classText}\n\n${aliasText}`]);
112
+ edits.push({
113
+ start: node.getStart(sourceFile),
114
+ end: node.end,
115
+ text: `${typeName}${nullishText}`,
116
+ });
117
+ return;
118
+ }
119
+ const at = anchor.getStart(sourceFile, true);
120
+ insertions.set(at, [...(insertions.get(at) ?? []), `${classText}\n\n`]);
121
+ edits.push({
122
+ start: node.getStart(sourceFile),
123
+ end: node.end,
124
+ text: `${reference}${nullishText}`,
125
+ });
126
+ };
127
+ for (const statement of sourceFile.statements) {
128
+ if (!ConstClasses.isExported({ typescript, node: statement })) {
129
+ continue;
130
+ }
131
+ if (typescript.isTypeAliasDeclaration(statement)) {
132
+ const union = typescript.isUnionTypeNode(statement.type) ? statement.type : undefined;
133
+ if (union && ConstClasses.isEnumerable({ typescript, node: union })) {
134
+ const base = statement.name.text.replace(/^T(?=[A-Z])/, '');
135
+ plan({
136
+ node: union,
137
+ id: `${relativePath}#${statement.name.text}`,
138
+ className: pluralize(base),
139
+ typeName: statement.name.text,
140
+ anchor: statement,
141
+ mayRenameType: false,
142
+ });
143
+ continue;
144
+ }
145
+ if (typescript.isTypeLiteralNode(statement.type)) {
146
+ ConstClasses.planMembers({
147
+ typescript,
148
+ container: statement,
149
+ members: statement.type.members,
150
+ relativePath,
151
+ plan,
152
+ });
153
+ }
154
+ }
155
+ if (typescript.isInterfaceDeclaration(statement)) {
156
+ ConstClasses.planMembers({
157
+ typescript,
158
+ container: statement,
159
+ members: statement.members,
160
+ relativePath,
161
+ plan,
162
+ });
163
+ }
164
+ ConstClasses.findStrays({ typescript, statement, handled, report });
165
+ }
166
+ if (proposals.length === 0) {
167
+ return { output: source, changed: false, proposals, untouched };
168
+ }
169
+ for (const [at, texts] of insertions) {
170
+ edits.push({ start: at, end: at, text: texts.join('') });
171
+ }
172
+ // Last, so that at the top of a file the import lands above the class inserted at the same place.
173
+ const importEdit = ConstClasses.planImport({ typescript, sourceFile, importFrom });
174
+ if (importEdit) {
175
+ edits.push(importEdit);
176
+ }
177
+ let output = source;
178
+ for (const edit of edits.sort((a, b) => b.start - a.start || b.end - a.end)) {
179
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
180
+ }
181
+ return { output, changed: output !== source, proposals, untouched };
182
+ }
183
+ static planMembers(opts) {
184
+ const { typescript, container, members, relativePath, plan } = opts;
185
+ const containerName = container.name.text;
186
+ const base = containerName.replace(/^[IT](?=[A-Z])/, '');
187
+ for (const member of members) {
188
+ if (!typescript.isPropertySignature(member) || !member.type) {
189
+ continue;
190
+ }
191
+ if (!typescript.isUnionTypeNode(member.type) ||
192
+ !ConstClasses.isEnumerable({ typescript, node: member.type })) {
193
+ continue;
194
+ }
195
+ const property = typescript.isIdentifier(member.name) ? member.name.text : undefined;
196
+ if (!property) {
197
+ continue;
198
+ }
199
+ plan({
200
+ node: member.type,
201
+ id: `${relativePath}#${containerName}.${property}`,
202
+ className: pluralize(`${base}${pascal(property)}`),
203
+ typeName: `T${base}${pascal(property)}`,
204
+ anchor: container,
205
+ mayRenameType: true,
206
+ });
207
+ }
208
+ }
209
+ /** Every top-level name the file declares or imports. */
210
+ static declaredNames(opts) {
211
+ const { typescript, sourceFile } = opts;
212
+ const names = new Set();
213
+ for (const statement of sourceFile.statements) {
214
+ if (typescript.isImportDeclaration(statement) && statement.importClause) {
215
+ const { name, namedBindings } = statement.importClause;
216
+ if (name) {
217
+ names.add(name.text);
218
+ }
219
+ if (namedBindings && typescript.isNamespaceImport(namedBindings)) {
220
+ names.add(namedBindings.name.text);
221
+ }
222
+ if (namedBindings && typescript.isNamedImports(namedBindings)) {
223
+ for (const element of namedBindings.elements) {
224
+ names.add(element.name.text);
225
+ }
226
+ }
227
+ continue;
228
+ }
229
+ if (typescript.isVariableStatement(statement)) {
230
+ for (const declaration of statement.declarationList.declarations) {
231
+ if (typescript.isIdentifier(declaration.name)) {
232
+ names.add(declaration.name.text);
233
+ }
234
+ }
235
+ continue;
236
+ }
237
+ if ((typescript.isClassDeclaration(statement) ||
238
+ typescript.isFunctionDeclaration(statement) ||
239
+ typescript.isInterfaceDeclaration(statement) ||
240
+ typescript.isTypeAliasDeclaration(statement) ||
241
+ typescript.isEnumDeclaration(statement) ||
242
+ typescript.isModuleDeclaration(statement)) &&
243
+ statement.name &&
244
+ typescript.isIdentifier(statement.name)) {
245
+ names.add(statement.name.text);
246
+ }
247
+ }
248
+ return names;
249
+ }
250
+ static isExported(opts) {
251
+ const { typescript, node } = opts;
252
+ const modifiers = typescript.canHaveModifiers(node) ? typescript.getModifiers(node) : undefined;
253
+ return (modifiers?.some((modifier) => modifier.kind === typescript.SyntaxKind.ExportKeyword) === true);
254
+ }
255
+ /** The string values of a union, and its `null` or `undefined` members; the union must be string literals (and nullish) only. */
256
+ static readUnion(opts) {
257
+ const { typescript, node } = opts;
258
+ const values = [];
259
+ const nullish = [];
260
+ for (const member of node.types) {
261
+ if (typescript.isLiteralTypeNode(member) && typescript.isStringLiteral(member.literal)) {
262
+ if (!values.includes(member.literal.text)) {
263
+ values.push(member.literal.text);
264
+ }
265
+ continue;
266
+ }
267
+ if (member.kind === typescript.SyntaxKind.NullKeyword) {
268
+ nullish.push('null');
269
+ }
270
+ else if (member.kind === typescript.SyntaxKind.UndefinedKeyword) {
271
+ nullish.push('undefined');
272
+ }
273
+ else if (typescript.isLiteralTypeNode(member) &&
274
+ member.literal.kind === typescript.SyntaxKind.NullKeyword) {
275
+ nullish.push('null');
276
+ }
277
+ }
278
+ return { values, nullish };
279
+ }
280
+ /** At least two string literals, and nothing else but `null` or `undefined`. */
281
+ static isEnumerable(opts) {
282
+ const { typescript, node } = opts;
283
+ let literals = 0;
284
+ for (const member of node.types) {
285
+ if (typescript.isLiteralTypeNode(member) && typescript.isStringLiteral(member.literal)) {
286
+ literals += 1;
287
+ continue;
288
+ }
289
+ const isNullish = member.kind === typescript.SyntaxKind.UndefinedKeyword ||
290
+ member.kind === typescript.SyntaxKind.NullKeyword ||
291
+ (typescript.isLiteralTypeNode(member) &&
292
+ member.literal.kind === typescript.SyntaxKind.NullKeyword);
293
+ if (!isNullish) {
294
+ return false;
295
+ }
296
+ }
297
+ return literals >= 2;
298
+ }
299
+ static checkPlan(opts) {
300
+ const { className, typeName, members, taken, mayRenameType } = opts;
301
+ if (taken.has(className)) {
302
+ return `the name ${className} is already used in this file`;
303
+ }
304
+ if (mayRenameType && taken.has(typeName)) {
305
+ return `the name ${typeName} is already used in this file`;
306
+ }
307
+ const keys = new Set();
308
+ for (const { value, key } of members) {
309
+ if (key === '') {
310
+ return `no member name can be made from ${JSON.stringify(value)}`;
311
+ }
312
+ if (RESERVED_KEYS.has(key)) {
313
+ return `${JSON.stringify(value)} would become the reserved member ${key}`;
314
+ }
315
+ if (keys.has(key)) {
316
+ return `two values give the member ${key}`;
317
+ }
318
+ keys.add(key);
319
+ }
320
+ return undefined;
321
+ }
322
+ static renderClass(opts) {
323
+ const { className, typeName, members } = opts;
324
+ const quote = (value) => `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
325
+ const lines = [
326
+ `/** The values of \`${typeName}\`. */`,
327
+ `export class ${className} {`,
328
+ ...members.map(({ value, key }) => ` static readonly ${key} = ${quote(value)};`),
329
+ '',
330
+ ` static readonly SCHEME_SET = new Set<string>([${members.map(({ key }) => `this.${key}`).join(', ')}]);`,
331
+ '',
332
+ ` static isValid(value: string): value is ${typeName} {`,
333
+ ' return this.SCHEME_SET.has(value);',
334
+ ' }',
335
+ '}',
336
+ ];
337
+ return lines.join('\n');
338
+ }
339
+ /** Reports every string-literal union of an exported declaration the codemod did not handle: it cannot name those. */
340
+ static findStrays(opts) {
341
+ const { typescript, statement, handled, report } = opts;
342
+ const walk = (node) => {
343
+ if (typescript.isVariableDeclaration(node) && node.type) {
344
+ // The annotation of an exported constant is not what the lint rule asks to name.
345
+ if (node.initializer) {
346
+ walk(node.initializer);
347
+ }
348
+ return;
349
+ }
350
+ if (typescript.isUnionTypeNode(node) && ConstClasses.isEnumerable({ typescript, node })) {
351
+ if (!handled.has(node) && !ConstClasses.isKeySet({ typescript, node })) {
352
+ report(node, 'a string-literal union in this position (a parameter, a return type, a generic or a nested type) has no name to give it; write the const class by hand');
353
+ }
354
+ return;
355
+ }
356
+ typescript.forEachChild(node, walk);
357
+ };
358
+ walk(statement);
359
+ }
360
+ static isKeySet(opts) {
361
+ const { typescript, node } = opts;
362
+ const parent = node.parent;
363
+ if (typescript.isMappedTypeNode(parent) || typescript.isTypeOperatorNode(parent)) {
364
+ return true;
365
+ }
366
+ if (typescript.isTypeParameterDeclaration(parent) &&
367
+ typescript.isMappedTypeNode(parent.parent)) {
368
+ return true;
369
+ }
370
+ if (parent &&
371
+ typescript.isTypeReferenceNode(parent) &&
372
+ typescript.isIdentifier(parent.typeName) &&
373
+ KEY_SET_TYPES.has(parent.typeName.text)) {
374
+ return true;
375
+ }
376
+ return false;
377
+ }
378
+ /** The edit that makes `TConstValue` available: nothing when the file has it, a specifier on an import from the same module, else a new import line. */
379
+ static planImport(opts) {
380
+ const { typescript, sourceFile, importFrom } = opts;
381
+ let lastImportEnd = 0;
382
+ let sameModule;
383
+ for (const statement of sourceFile.statements) {
384
+ if (!typescript.isImportDeclaration(statement)) {
385
+ continue;
386
+ }
387
+ lastImportEnd = statement.end;
388
+ const bindings = statement.importClause?.namedBindings;
389
+ if (bindings && typescript.isNamedImports(bindings)) {
390
+ if (bindings.elements.some((element) => element.name.text === 'TConstValue')) {
391
+ return undefined;
392
+ }
393
+ if (typescript.isStringLiteral(statement.moduleSpecifier) &&
394
+ statement.moduleSpecifier.text === importFrom) {
395
+ sameModule = statement;
396
+ }
397
+ }
398
+ }
399
+ const bindings = sameModule?.importClause?.namedBindings;
400
+ if (sameModule &&
401
+ bindings &&
402
+ typescript.isNamedImports(bindings) &&
403
+ bindings.elements.length > 0) {
404
+ const last = bindings.elements[bindings.elements.length - 1];
405
+ const typeOnly = sameModule.importClause?.isTypeOnly === true;
406
+ return {
407
+ start: last?.end ?? bindings.end,
408
+ end: last?.end ?? bindings.end,
409
+ text: typeOnly ? ', TConstValue' : ', type TConstValue',
410
+ };
411
+ }
412
+ const line = `import type { TConstValue } from '${importFrom}';`;
413
+ if (lastImportEnd > 0) {
414
+ return { start: lastImportEnd, end: lastImportEnd, text: `\n${line}` };
415
+ }
416
+ return { start: 0, end: 0, text: `${line}\n\n` };
417
+ }
418
+ }
419
+ exports.ConstClasses = ConstClasses;
420
+ const USAGE = `Usage: devz codemod const-classes [paths...] [--write | --check] [--map <file>] [--emit-map <file>] [--import-from <module>] [--include-tests]
421
+
422
+ Turns a string-literal union in an exported declaration into a const class (SCHEME_SET, isValid) plus
423
+ \`TConstValue<typeof X>\`, the shape the venizia/const-class-values lint rule asks for. It handles an
424
+ exported type alias and a property of an exported interface or object type. The values stay the same,
425
+ so every existing literal still type-checks and no call site changes.
426
+
427
+ It has to choose names: the class is the type name without its leading T, in the plural; a member is
428
+ the value in upper snake case. The dry run prints every proposal.
429
+
430
+ --write write the changed files (without it, nothing is written)
431
+ --check dry run that exits 1 while a union can be converted or is left untouched (for CI)
432
+ --map <file> a JSON file of overrides, keyed by the id the dry run prints
433
+ --emit-map <file> write the proposals as a mapping file to edit, then pass it with --map
434
+ --import-from <m> where TConstValue is imported from (default ${DEFAULT_IMPORT_FROM})
435
+ --include-tests also convert test files (skipped by default)
436
+ --help print this text and exit
437
+
438
+ A mapping entry: { "src/a.ts#TMode": { "class": "Modes", "members": { "read-only": "READ_ONLY" } } },
439
+ or { "skip": true }. A union in a parameter, a return type, a generic or a nested type is reported as
440
+ left untouched: it has no name, so write that class by hand. Key sets (Pick, Omit, Extract, Exclude,
441
+ Record, mapped types) are not enumerable values and are never touched.`;
442
+ /** A usage error: nothing was read or written. */
443
+ const USAGE_ERROR_CODE = 2;
444
+ class ConstClassFlags {
445
+ static { this.WRITE = '--write'; }
446
+ static { this.CHECK = '--check'; }
447
+ static { this.MAP = '--map'; }
448
+ static { this.EMIT_MAP = '--emit-map'; }
449
+ static { this.IMPORT_FROM = '--import-from'; }
450
+ static { this.INCLUDE_TESTS = '--include-tests'; }
451
+ static { this.HELP = '--help'; }
452
+ static { this.HELP_SHORT = '-h'; }
453
+ static { this.SCHEME_SET = new Set([
454
+ this.WRITE,
455
+ this.CHECK,
456
+ this.MAP,
457
+ this.EMIT_MAP,
458
+ this.IMPORT_FROM,
459
+ this.INCLUDE_TESTS,
460
+ this.HELP,
461
+ this.HELP_SHORT,
462
+ ]); }
463
+ /** The flags that take a value. */
464
+ static { this.WITH_VALUE = new Set([this.MAP, this.EMIT_MAP, this.IMPORT_FROM]); }
465
+ static isValid(value) {
466
+ return this.SCHEME_SET.has(value);
467
+ }
468
+ }
469
+ /** `devz codemod const-classes [paths...] [--write | --check]`. */
470
+ class ConstClassesCommand {
471
+ static { this.USAGE = USAGE; }
472
+ static async run(opts) {
473
+ const { root, args } = opts;
474
+ if (args.includes(ConstClassFlags.HELP) || args.includes(ConstClassFlags.HELP_SHORT)) {
475
+ console.log(USAGE);
476
+ return 0;
477
+ }
478
+ const flags = [];
479
+ const paths = [];
480
+ const values = new Map();
481
+ for (let index = 0; index < args.length; index += 1) {
482
+ const argument = args[index] ?? '';
483
+ const [name, inline] = argument.startsWith('--') ? argument.split(/=(.*)/s) : [argument];
484
+ if (name !== undefined && ConstClassFlags.WITH_VALUE.has(name)) {
485
+ const value = inline ?? args[index + 1];
486
+ if (inline === undefined) {
487
+ index += 1;
488
+ }
489
+ if (!value || value.startsWith('-')) {
490
+ console.error(`devz codemod: ${name} needs a value\n${USAGE}`);
491
+ return USAGE_ERROR_CODE;
492
+ }
493
+ values.set(name, value);
494
+ continue;
495
+ }
496
+ if (argument.startsWith('-')) {
497
+ flags.push(argument);
498
+ }
499
+ else {
500
+ paths.push(argument);
501
+ }
502
+ }
503
+ const unknown = flags.filter((flag) => !ConstClassFlags.isValid(flag));
504
+ if (unknown.length > 0) {
505
+ console.error(`devz codemod: unknown option ${unknown.map((flag) => `"${flag}"`).join(', ')}\n${USAGE}`);
506
+ return USAGE_ERROR_CODE;
507
+ }
508
+ const isWrite = flags.includes(ConstClassFlags.WRITE);
509
+ const isCheck = flags.includes(ConstClassFlags.CHECK);
510
+ const includeTests = flags.includes(ConstClassFlags.INCLUDE_TESTS);
511
+ if (isWrite && isCheck) {
512
+ console.error(`devz codemod: ${ConstClassFlags.WRITE} and ${ConstClassFlags.CHECK} contradict each other\n${USAGE}`);
513
+ return USAGE_ERROR_CODE;
514
+ }
515
+ let overrides = {};
516
+ const mapPath = values.get(ConstClassFlags.MAP);
517
+ if (mapPath) {
518
+ let parsed;
519
+ try {
520
+ parsed = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(root, mapPath), 'utf8'));
521
+ }
522
+ catch (error) {
523
+ console.error(`devz codemod: cannot read the mapping file ${mapPath}: ${error instanceof Error ? error.message : String(error)}`);
524
+ return USAGE_ERROR_CODE;
525
+ }
526
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
527
+ console.error(`devz codemod: cannot read the mapping file ${mapPath}: the file must hold one JSON object`);
528
+ return USAGE_ERROR_CODE;
529
+ }
530
+ overrides = Object.fromEntries(Object.entries(parsed));
531
+ }
532
+ let typescript;
533
+ try {
534
+ typescript = (await import('typescript')).default;
535
+ }
536
+ catch {
537
+ console.error('devz codemod: the "typescript" package is required to read your sources');
538
+ return 1;
539
+ }
540
+ const targets = (paths.length > 0 ? paths : ['.']).map((path) => (0, node_path_1.resolve)(root, path));
541
+ const importFrom = values.get(ConstClassFlags.IMPORT_FROM);
542
+ const emitted = {};
543
+ let changedFiles = 0;
544
+ let converted = 0;
545
+ let untouchedCount = 0;
546
+ for (const target of targets) {
547
+ const files = files_1.CodemodFiles.collect({ target });
548
+ if (files.length === 0 && paths.length > 0) {
549
+ console.warn(`devz codemod: no source files under "${(0, node_path_1.relative)(root, target) || target}" (git ignores the path, or it holds none): nothing was read`);
550
+ }
551
+ for (const file of files) {
552
+ const relativePath = (0, node_path_1.relative)(root, file).split('\\').join('/');
553
+ if (!includeTests && TEST_FILE_PATTERN.test(relativePath)) {
554
+ continue;
555
+ }
556
+ const source = (0, node_fs_1.readFileSync)(file, 'utf8');
557
+ if (!UNION_TEXT.test(source)) {
558
+ continue;
559
+ }
560
+ const result = ConstClasses.convert({
561
+ typescript,
562
+ fileName: file,
563
+ source,
564
+ relativePath,
565
+ overrides,
566
+ importFrom,
567
+ });
568
+ for (const item of result.untouched) {
569
+ untouchedCount += 1;
570
+ console.warn(`${relativePath}:${item.line}:${item.column} ${item.reason}`);
571
+ }
572
+ for (const proposal of result.proposals) {
573
+ converted += 1;
574
+ emitted[proposal.id] = {
575
+ class: proposal.className,
576
+ ...(proposal.id.split('#')[1]?.includes('.') ? { type: proposal.typeName } : {}),
577
+ members: Object.fromEntries(proposal.members.map(({ value, key }) => [value, key])),
578
+ };
579
+ console.log(`${isWrite ? 'converted' : 'would convert'} ${relativePath}:${proposal.line} ${proposal.typeName} -> class ${proposal.className} { ${proposal.members.map(({ key, value }) => `${key} = '${value}'`).join(', ')} }` +
580
+ ` [${proposal.id}]`);
581
+ }
582
+ if (result.changed) {
583
+ changedFiles += 1;
584
+ if (isWrite) {
585
+ (0, node_fs_1.writeFileSync)(file, result.output);
586
+ }
587
+ }
588
+ }
589
+ }
590
+ const emitPath = values.get(ConstClassFlags.EMIT_MAP);
591
+ if (emitPath) {
592
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(root, emitPath), `${JSON.stringify(emitted, null, 2)}\n`);
593
+ console.log(`Wrote ${Object.keys(emitted).length} proposal(s) to ${emitPath}; edit it, then pass --map ${emitPath}.`);
594
+ }
595
+ console.log(`devz codemod const-classes: ${converted} union(s) ${isWrite ? 'converted' : 'would be converted'} in ${changedFiles} file(s), ${untouchedCount} left untouched`);
596
+ if (!isWrite && changedFiles > 0) {
597
+ console.log(`Nothing was written. Run again with ${ConstClassFlags.WRITE} to apply (add --map to rename), then run your typecheck and lint.`);
598
+ }
599
+ return isCheck && (converted > 0 || untouchedCount > 0) ? 1 : 0;
600
+ }
601
+ }
602
+ exports.ConstClassesCommand = ConstClassesCommand;
603
+ //# sourceMappingURL=const-classes.js.map