wrec 0.29.1 → 0.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{wrec-DdxbuCnt.js → wrec-DEac2MyH.js} +206 -184
- package/dist/wrec-ssr.d.ts +3 -0
- package/dist/wrec-ssr.es.js +1 -1
- package/dist/wrec.d.ts +3 -0
- package/dist/wrec.es.js +1 -1
- package/package.json +2 -2
- package/scripts/lint.js +7 -1
- package/scripts/used-by.js +282 -203
package/scripts/used-by.js
CHANGED
|
@@ -1,50 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// This script inspects a given Wrec component source file and
|
|
3
|
+
// determines the proper values for property config `usedBy` properties.
|
|
4
|
+
// Each value is a list of methods that use the property
|
|
5
|
+
// or a single method name.
|
|
6
|
+
// It uses the TypeScript compiler to parse the file,
|
|
7
|
+
// discover which expressions call which methods,
|
|
8
|
+
// trace property usage through those call chains, and
|
|
9
|
+
// output or update the `usedBy` properties`.
|
|
10
|
+
//
|
|
11
|
+
// To run this, enter `npx wrec-usedby [--dry] [file-path]`
|
|
12
|
+
// If no file-path is specified, the script runs on
|
|
13
|
+
// all .js and .ts files in and below the current directory.
|
|
14
|
+
//
|
|
15
|
+
// Include the --dry flag for a dry run where `usedBy` values are output,
|
|
16
|
+
// but no files are modified.
|
|
2
17
|
|
|
3
18
|
import fs from 'node:fs';
|
|
4
19
|
import path from 'node:path';
|
|
20
|
+
import {fileURLToPath} from 'node:url';
|
|
5
21
|
import ts from 'typescript';
|
|
6
22
|
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (args.includes('--check')) {
|
|
18
|
-
console.error('Use --dry instead of --check.');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (inputPaths.length !== 1) {
|
|
23
|
-
console.error(
|
|
24
|
-
'Specify a single source file, e.g. npx wrec-usedby src/examples/radio-group.js'
|
|
23
|
+
function buildConfigText(sourceFile, member, methodNames, quote) {
|
|
24
|
+
const {text} = sourceFile;
|
|
25
|
+
const configObject = member.initializer;
|
|
26
|
+
const existingMembers = configObject.properties.filter(
|
|
27
|
+
property =>
|
|
28
|
+
!(
|
|
29
|
+
ts.isPropertyAssignment(property) &&
|
|
30
|
+
getNameText(property.name) === 'usedBy'
|
|
31
|
+
)
|
|
25
32
|
);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (const target of targets) {
|
|
33
|
-
if (!fs.existsSync(target)) {
|
|
34
|
-
console.error(`File not found: ${path.relative(cwd, target)}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
33
|
+
const existingTexts = existingMembers.map(property =>
|
|
34
|
+
text.slice(property.getStart(sourceFile), property.end).trim()
|
|
35
|
+
);
|
|
36
|
+
if (methodNames.length > 0)
|
|
37
|
+
existingTexts.push(createUsedByProperty(methodNames, quote));
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const original = text.slice(
|
|
40
|
+
configObject.getStart(sourceFile),
|
|
41
|
+
configObject.end
|
|
42
|
+
);
|
|
43
|
+
const multiline = original.includes('\n');
|
|
44
|
+
if (!multiline) {
|
|
45
|
+
const openMatch = original.match(/^\{(\s*)/);
|
|
46
|
+
const closeMatch = original.match(/(\s*)\}$/);
|
|
47
|
+
const openSpacing = openMatch ? openMatch[1] : ' ';
|
|
48
|
+
const closeSpacing = closeMatch ? closeMatch[1] : ' ';
|
|
49
|
+
return `{${openSpacing}${existingTexts.join(', ')}${closeSpacing}}`;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
// Preserve the surrounding formatting style so the update feels like a
|
|
53
|
+
// minimal edit instead of reformatting the whole object.
|
|
54
|
+
const memberIndent = getIndent(text, member.getStart(sourceFile));
|
|
55
|
+
const firstExisting = existingMembers[0];
|
|
56
|
+
const innerIndent = firstExisting
|
|
57
|
+
? getIndent(text, firstExisting.getStart(sourceFile))
|
|
58
|
+
: memberIndent + ' ';
|
|
59
|
+
return `{\n${existingTexts.map(part => `${innerIndent}${part}`).join(',\n')}\n${memberIndent}}`;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
function collectFiles(startPath, files = []) {
|
|
@@ -52,9 +64,7 @@ function collectFiles(startPath, files = []) {
|
|
|
52
64
|
|
|
53
65
|
const stat = fs.statSync(startPath);
|
|
54
66
|
if (stat.isFile()) {
|
|
55
|
-
if (
|
|
56
|
-
files.push(startPath);
|
|
57
|
-
}
|
|
67
|
+
if (isSupportedSourceFile(startPath)) files.push(startPath);
|
|
58
68
|
return files;
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -63,12 +73,7 @@ function collectFiles(startPath, files = []) {
|
|
|
63
73
|
if (entry.isDirectory()) {
|
|
64
74
|
if (entry.name === 'node_modules' || entry.name === 'dist') continue;
|
|
65
75
|
collectFiles(fullPath, files);
|
|
66
|
-
} else if (
|
|
67
|
-
entry.isFile() &&
|
|
68
|
-
/\.(js|ts)$/.test(entry.name) &&
|
|
69
|
-
!entry.name.endsWith('.d.ts') &&
|
|
70
|
-
!entry.name.includes('.test.')
|
|
71
|
-
) {
|
|
76
|
+
} else if (entry.isFile() && isSupportedSourceFile(entry.name, true)) {
|
|
72
77
|
files.push(fullPath);
|
|
73
78
|
}
|
|
74
79
|
}
|
|
@@ -76,63 +81,11 @@ function collectFiles(startPath, files = []) {
|
|
|
76
81
|
return files;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
ts.isIdentifier(name) ||
|
|
84
|
-
ts.isStringLiteral(name) ||
|
|
85
|
-
ts.isPrivateIdentifier(name)
|
|
86
|
-
) {
|
|
87
|
-
return name.text;
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function hasStaticModifier(node) {
|
|
93
|
-
return Boolean(
|
|
94
|
-
node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword)
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function getWrecImportInfo(sourceFile) {
|
|
99
|
-
const names = new Set(['Wrec']);
|
|
100
|
-
let quote = "'";
|
|
101
|
-
|
|
102
|
-
for (const statement of sourceFile.statements) {
|
|
103
|
-
if (
|
|
104
|
-
!ts.isImportDeclaration(statement) ||
|
|
105
|
-
!statement.importClause ||
|
|
106
|
-
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
107
|
-
) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const moduleName = statement.moduleSpecifier.text;
|
|
112
|
-
const isWrecModule =
|
|
113
|
-
moduleName === 'wrec' ||
|
|
114
|
-
moduleName === 'wrec/ssr' ||
|
|
115
|
-
moduleName.endsWith('/wrec') ||
|
|
116
|
-
moduleName.endsWith('/wrec-ssr');
|
|
117
|
-
if (!isWrecModule) continue;
|
|
118
|
-
|
|
119
|
-
const namedBindings = statement.importClause.namedBindings;
|
|
120
|
-
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
121
|
-
|
|
122
|
-
for (const element of namedBindings.elements) {
|
|
123
|
-
const importedName = element.propertyName?.text ?? element.name.text;
|
|
124
|
-
if (importedName === 'Wrec') {
|
|
125
|
-
names.add(element.name.text);
|
|
126
|
-
|
|
127
|
-
const moduleText = statement.moduleSpecifier.getText(sourceFile);
|
|
128
|
-
if (moduleText.startsWith('"') || moduleText.startsWith("'")) {
|
|
129
|
-
quote = moduleText[0];
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
84
|
+
function createUsedByProperty(methodNames, quote) {
|
|
85
|
+
if (methodNames.length === 1) {
|
|
86
|
+
return `usedBy: ${quote}${methodNames[0]}${quote}`;
|
|
133
87
|
}
|
|
134
|
-
|
|
135
|
-
return {names, quote};
|
|
88
|
+
return `usedBy: [${methodNames.map(name => `${quote}${name}${quote}`).join(', ')}]`;
|
|
136
89
|
}
|
|
137
90
|
|
|
138
91
|
function extendsWrec(node, wrecNames) {
|
|
@@ -150,51 +103,6 @@ function extendsWrec(node, wrecNames) {
|
|
|
150
103
|
);
|
|
151
104
|
}
|
|
152
105
|
|
|
153
|
-
function getTemplateCalledMethods(classNode) {
|
|
154
|
-
const methodNames = new Set();
|
|
155
|
-
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
156
|
-
|
|
157
|
-
function visit(node) {
|
|
158
|
-
if (
|
|
159
|
-
ts.isPropertyAccessExpression(node) &&
|
|
160
|
-
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
161
|
-
ts.isCallExpression(node.parent) &&
|
|
162
|
-
node.parent.expression === node
|
|
163
|
-
) {
|
|
164
|
-
methodNames.add(node.name.text);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
ts.forEachChild(node, visit);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function addTemplateTextMethods(template) {
|
|
171
|
-
const text = template.getText();
|
|
172
|
-
for (const match of text.matchAll(CALL_RE)) {
|
|
173
|
-
methodNames.add(match[1]);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
for (const member of classNode.members) {
|
|
178
|
-
if (
|
|
179
|
-
ts.isPropertyDeclaration(member) &&
|
|
180
|
-
hasStaticModifier(member) &&
|
|
181
|
-
getNameText(member.name) === 'html' &&
|
|
182
|
-
member.initializer
|
|
183
|
-
) {
|
|
184
|
-
if (
|
|
185
|
-
ts.isTaggedTemplateExpression(member.initializer) &&
|
|
186
|
-
ts.isIdentifier(member.initializer.tag) &&
|
|
187
|
-
member.initializer.tag.text === 'html'
|
|
188
|
-
) {
|
|
189
|
-
addTemplateTextMethods(member.initializer.template);
|
|
190
|
-
}
|
|
191
|
-
ts.forEachChild(member.initializer, visit);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return methodNames;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
106
|
function getComputedCalledMethods(classNode) {
|
|
199
107
|
const methodNames = new Set();
|
|
200
108
|
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
@@ -238,6 +146,12 @@ function getComputedCalledMethods(classNode) {
|
|
|
238
146
|
return methodNames;
|
|
239
147
|
}
|
|
240
148
|
|
|
149
|
+
function getIndent(text, pos) {
|
|
150
|
+
const lineStart = text.lastIndexOf('\n', pos - 1) + 1;
|
|
151
|
+
const match = /^[ \t]*/.exec(text.slice(lineStart));
|
|
152
|
+
return match ? match[0] : '';
|
|
153
|
+
}
|
|
154
|
+
|
|
241
155
|
function getMethodUsages(classNode, propertyNames) {
|
|
242
156
|
const methodInfo = new Map();
|
|
243
157
|
for (const member of classNode.members) {
|
|
@@ -283,6 +197,9 @@ function getMethodUsages(classNode, propertyNames) {
|
|
|
283
197
|
}
|
|
284
198
|
|
|
285
199
|
function visit(child) {
|
|
200
|
+
// Record both direct property reads like `this.foo` and method calls
|
|
201
|
+
// like `this.renderFoo()` so we can later propagate property usage
|
|
202
|
+
// through method-to-method call chains.
|
|
286
203
|
if (
|
|
287
204
|
ts.isPropertyAccessExpression(child) &&
|
|
288
205
|
child.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
@@ -354,6 +271,9 @@ function getMethodUsages(classNode, propertyNames) {
|
|
|
354
271
|
const memo = new Map();
|
|
355
272
|
|
|
356
273
|
function getTransitiveProps(methodName, seen = new Set()) {
|
|
274
|
+
// Starting from methods that are reachable from the template/computed
|
|
275
|
+
// properties, walk through nested method calls and accumulate every
|
|
276
|
+
// component property touched along the way.
|
|
357
277
|
if (memo.has(methodName)) return memo.get(methodName);
|
|
358
278
|
if (seen.has(methodName)) return new Set();
|
|
359
279
|
|
|
@@ -395,55 +315,129 @@ function getMethodUsages(classNode, propertyNames) {
|
|
|
395
315
|
return propToMethods;
|
|
396
316
|
}
|
|
397
317
|
|
|
398
|
-
function
|
|
399
|
-
if (
|
|
400
|
-
|
|
318
|
+
function getNameText(name) {
|
|
319
|
+
if (
|
|
320
|
+
ts.isIdentifier(name) ||
|
|
321
|
+
ts.isStringLiteral(name) ||
|
|
322
|
+
ts.isPrivateIdentifier(name)
|
|
323
|
+
) {
|
|
324
|
+
return name.text;
|
|
401
325
|
}
|
|
402
|
-
return
|
|
326
|
+
return null;
|
|
403
327
|
}
|
|
404
328
|
|
|
405
|
-
function
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
|
|
329
|
+
function getTemplateCalledMethods(classNode) {
|
|
330
|
+
const methodNames = new Set();
|
|
331
|
+
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
332
|
+
|
|
333
|
+
function visit(node) {
|
|
334
|
+
if (
|
|
335
|
+
ts.isPropertyAccessExpression(node) &&
|
|
336
|
+
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
337
|
+
ts.isCallExpression(node.parent) &&
|
|
338
|
+
node.parent.expression === node
|
|
339
|
+
) {
|
|
340
|
+
methodNames.add(node.name.text);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
ts.forEachChild(node, visit);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function addTemplateTextMethods(template) {
|
|
347
|
+
// Template expressions can hide method calls inside raw template text,
|
|
348
|
+
// so use a regex in addition to AST traversal to catch those names.
|
|
349
|
+
const text = template.getText();
|
|
350
|
+
for (const match of text.matchAll(CALL_RE)) {
|
|
351
|
+
methodNames.add(match[1]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const member of classNode.members) {
|
|
356
|
+
if (
|
|
357
|
+
ts.isPropertyDeclaration(member) &&
|
|
358
|
+
hasStaticModifier(member) &&
|
|
359
|
+
getNameText(member.name) === 'html' &&
|
|
360
|
+
member.initializer
|
|
361
|
+
) {
|
|
362
|
+
if (
|
|
363
|
+
ts.isTaggedTemplateExpression(member.initializer) &&
|
|
364
|
+
ts.isIdentifier(member.initializer.tag) &&
|
|
365
|
+
member.initializer.tag.text === 'html'
|
|
366
|
+
) {
|
|
367
|
+
addTemplateTextMethods(member.initializer.template);
|
|
368
|
+
}
|
|
369
|
+
ts.forEachChild(member.initializer, visit);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return methodNames;
|
|
409
374
|
}
|
|
410
375
|
|
|
411
|
-
function
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
const existingMembers = configObject.properties.filter(
|
|
415
|
-
property =>
|
|
416
|
-
!(
|
|
417
|
-
ts.isPropertyAssignment(property) &&
|
|
418
|
-
getNameText(property.name) === 'usedBy'
|
|
419
|
-
)
|
|
420
|
-
);
|
|
421
|
-
const existingTexts = existingMembers.map(property =>
|
|
422
|
-
text.slice(property.getStart(sourceFile), property.end).trim()
|
|
423
|
-
);
|
|
424
|
-
if (methodNames.length > 0)
|
|
425
|
-
existingTexts.push(createUsedByProperty(methodNames, quote));
|
|
376
|
+
function getWrecImportInfo(sourceFile) {
|
|
377
|
+
const names = new Set(['Wrec']);
|
|
378
|
+
let quote = "'";
|
|
426
379
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
380
|
+
// Support aliased imports such as `import {Wrec as Base} from 'wrec'`
|
|
381
|
+
// so subclass detection still works and generated text matches the
|
|
382
|
+
// file's existing quote style.
|
|
383
|
+
for (const statement of sourceFile.statements) {
|
|
384
|
+
if (
|
|
385
|
+
!ts.isImportDeclaration(statement) ||
|
|
386
|
+
!statement.importClause ||
|
|
387
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
388
|
+
) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const moduleName = statement.moduleSpecifier.text;
|
|
393
|
+
const isWrecModule =
|
|
394
|
+
moduleName === 'wrec' ||
|
|
395
|
+
moduleName === 'wrec/ssr' ||
|
|
396
|
+
moduleName.endsWith('/wrec') ||
|
|
397
|
+
moduleName.endsWith('/wrec-ssr');
|
|
398
|
+
if (!isWrecModule) continue;
|
|
399
|
+
|
|
400
|
+
const namedBindings = statement.importClause.namedBindings;
|
|
401
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
402
|
+
|
|
403
|
+
for (const element of namedBindings.elements) {
|
|
404
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
405
|
+
if (importedName === 'Wrec') {
|
|
406
|
+
names.add(element.name.text);
|
|
407
|
+
|
|
408
|
+
const moduleText = statement.moduleSpecifier.getText(sourceFile);
|
|
409
|
+
if (moduleText.startsWith('"') || moduleText.startsWith("'")) {
|
|
410
|
+
quote = moduleText[0];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {names, quote};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function hasStaticModifier(node) {
|
|
420
|
+
return Boolean(
|
|
421
|
+
node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword)
|
|
430
422
|
);
|
|
431
|
-
|
|
432
|
-
if (!multiline) return `{ ${existingTexts.join(', ')} }`;
|
|
423
|
+
}
|
|
433
424
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
425
|
+
function isSupportedSourceFile(filePath, excludeTests = false) {
|
|
426
|
+
return (
|
|
427
|
+
/\.(js|ts)$/.test(filePath) &&
|
|
428
|
+
!/\.d\.ts$/.test(filePath) &&
|
|
429
|
+
(!excludeTests || !filePath.includes('.test.'))
|
|
430
|
+
);
|
|
440
431
|
}
|
|
441
432
|
|
|
442
433
|
function transformSourceFile(sourceFile) {
|
|
443
434
|
const edits = [];
|
|
444
435
|
const {names: wrecNames, quote} = getWrecImportInfo(sourceFile);
|
|
436
|
+
const suggestions = [];
|
|
445
437
|
let foundWrecSubclass = false;
|
|
446
438
|
|
|
439
|
+
// Each matching class contributes text replacements for the specific
|
|
440
|
+
// property config objects that need `usedBy` added, updated, or removed.
|
|
447
441
|
for (const node of sourceFile.statements) {
|
|
448
442
|
if (!ts.isClassDeclaration(node) || !extendsWrec(node, wrecNames)) continue;
|
|
449
443
|
foundWrecSubclass = true;
|
|
@@ -492,6 +486,15 @@ function transformSourceFile(sourceFile) {
|
|
|
492
486
|
);
|
|
493
487
|
const hadUsedBy =
|
|
494
488
|
existingMembers.length !== configObject.properties.length;
|
|
489
|
+
if (methodNames.length > 0 || hadUsedBy) {
|
|
490
|
+
suggestions.push({
|
|
491
|
+
propName,
|
|
492
|
+
suggestion:
|
|
493
|
+
methodNames.length > 0
|
|
494
|
+
? createUsedByProperty(methodNames, quote)
|
|
495
|
+
: 'remove usedBy'
|
|
496
|
+
});
|
|
497
|
+
}
|
|
495
498
|
const needsUsedBy = methodNames.length > 0;
|
|
496
499
|
if (!hadUsedBy && !needsUsedBy) continue;
|
|
497
500
|
|
|
@@ -517,6 +520,7 @@ function transformSourceFile(sourceFile) {
|
|
|
517
520
|
changed: false,
|
|
518
521
|
edits: [],
|
|
519
522
|
foundWrecSubclass: false,
|
|
523
|
+
suggestions,
|
|
520
524
|
text: sourceFile.text
|
|
521
525
|
};
|
|
522
526
|
}
|
|
@@ -526,6 +530,7 @@ function transformSourceFile(sourceFile) {
|
|
|
526
530
|
changed: false,
|
|
527
531
|
edits: [],
|
|
528
532
|
foundWrecSubclass: true,
|
|
533
|
+
suggestions,
|
|
529
534
|
text: sourceFile.text
|
|
530
535
|
};
|
|
531
536
|
}
|
|
@@ -537,49 +542,123 @@ function transformSourceFile(sourceFile) {
|
|
|
537
542
|
nextSource.slice(0, edit.start) + edit.text + nextSource.slice(edit.end);
|
|
538
543
|
}
|
|
539
544
|
|
|
540
|
-
return {
|
|
545
|
+
return {
|
|
546
|
+
changed: true,
|
|
547
|
+
edits,
|
|
548
|
+
foundWrecSubclass: true,
|
|
549
|
+
suggestions,
|
|
550
|
+
text: nextSource
|
|
551
|
+
};
|
|
541
552
|
}
|
|
542
553
|
|
|
543
|
-
|
|
554
|
+
function validateTargetFile(target, cwd = process.cwd()) {
|
|
555
|
+
if (!fs.existsSync(target)) {
|
|
556
|
+
throw new Error(`File not found: ${path.relative(cwd, target)}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const stat = fs.statSync(target);
|
|
560
|
+
if (!stat.isFile()) {
|
|
561
|
+
throw new Error(`Not a file: ${path.relative(cwd, target)}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!/\.(js|ts)$/.test(target) || /\.d\.ts$/.test(target)) {
|
|
565
|
+
throw new Error(`Unsupported file type: ${path.relative(cwd, target)}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
544
568
|
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
569
|
+
export function updateUsedBySource(filePath, text) {
|
|
570
|
+
const scriptKind = filePath.endsWith('.ts')
|
|
571
|
+
? ts.ScriptKind.TS
|
|
572
|
+
: ts.ScriptKind.JS;
|
|
548
573
|
const sourceFile = ts.createSourceFile(
|
|
549
|
-
|
|
574
|
+
filePath,
|
|
550
575
|
text,
|
|
551
576
|
ts.ScriptTarget.Latest,
|
|
552
577
|
true,
|
|
553
578
|
scriptKind
|
|
554
579
|
);
|
|
555
580
|
|
|
581
|
+
return transformSourceFile(sourceFile);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function updateUsedByFile(filePath, options = {}) {
|
|
585
|
+
const {dry = false, quiet = false} = options;
|
|
586
|
+
const cwd = process.cwd();
|
|
587
|
+
const resolved = path.resolve(cwd, filePath);
|
|
588
|
+
validateTargetFile(resolved, cwd);
|
|
589
|
+
|
|
590
|
+
const text = fs.readFileSync(resolved, 'utf8');
|
|
556
591
|
const {
|
|
557
592
|
changed,
|
|
558
|
-
edits,
|
|
559
593
|
foundWrecSubclass,
|
|
594
|
+
suggestions,
|
|
560
595
|
text: nextText
|
|
561
|
-
} =
|
|
596
|
+
} = updateUsedBySource(resolved, text);
|
|
562
597
|
if (!foundWrecSubclass) {
|
|
563
|
-
|
|
564
|
-
|
|
598
|
+
throw new Error('No class extending Wrec was found.');
|
|
599
|
+
}
|
|
600
|
+
if (dry) {
|
|
601
|
+
return {changed, foundWrecSubclass, suggestions, text: nextText};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (changed) {
|
|
605
|
+
// Otherwise, apply the rewritten source text back to disk.
|
|
606
|
+
fs.writeFileSync(resolved, nextText);
|
|
607
|
+
}
|
|
608
|
+
return {changed, foundWrecSubclass, suggestions: [], text: nextText, quiet};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function fail(message) {
|
|
612
|
+
console.error(message);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function main() {
|
|
617
|
+
const args = process.argv.slice(2);
|
|
618
|
+
const dry = args.includes('--dry');
|
|
619
|
+
const quiet = args.includes('--quiet');
|
|
620
|
+
const inputPaths = args.filter(arg => !arg.startsWith('--'));
|
|
621
|
+
|
|
622
|
+
if (args.includes('--check')) {
|
|
623
|
+
throw new Error('Use --dry instead of --check.');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (inputPaths.length !== 1) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
'Specify a single source file, e.g. npx wrec-usedby src/examples/radio-group.js'
|
|
629
|
+
);
|
|
565
630
|
}
|
|
566
|
-
if (!changed) continue;
|
|
567
631
|
|
|
568
|
-
|
|
632
|
+
const result = updateUsedByFile(inputPaths[0], {dry, quiet});
|
|
569
633
|
if (dry) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
console.log(`${
|
|
634
|
+
// In dry mode, report the inferred change and exit non-zero when at
|
|
635
|
+
// least one update would be needed so the script can be used in checks.
|
|
636
|
+
for (const {propName, suggestion} of result.suggestions) {
|
|
637
|
+
console.log(`${propName} - ${suggestion}`);
|
|
574
638
|
}
|
|
575
|
-
|
|
576
|
-
|
|
639
|
+
if (!result.changed) return;
|
|
640
|
+
process.exit(1);
|
|
577
641
|
}
|
|
578
|
-
|
|
642
|
+
|
|
643
|
+
if (result.changed) console.log('updated');
|
|
579
644
|
}
|
|
580
645
|
|
|
581
|
-
|
|
646
|
+
const isCliEntry = (() => {
|
|
647
|
+
if (!process.argv[1]) return false;
|
|
648
|
+
try {
|
|
649
|
+
return (
|
|
650
|
+
fs.realpathSync(process.argv[1]) ===
|
|
651
|
+
fs.realpathSync(fileURLToPath(import.meta.url))
|
|
652
|
+
);
|
|
653
|
+
} catch {
|
|
654
|
+
return path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
655
|
+
}
|
|
656
|
+
})();
|
|
582
657
|
|
|
583
|
-
if (
|
|
584
|
-
|
|
658
|
+
if (isCliEntry) {
|
|
659
|
+
try {
|
|
660
|
+
main();
|
|
661
|
+
} catch (error) {
|
|
662
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
663
|
+
}
|
|
585
664
|
}
|