wrec 0.23.2 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/scripts/used-by.js +584 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "wrec",
|
|
3
3
|
"description": "a small library that greatly simplifies building web components",
|
|
4
4
|
"author": "R. Mark Volkmann",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.24.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -26,8 +26,12 @@
|
|
|
26
26
|
"import": "./dist/wrec-ssr.es.js"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"wrec-usedby": "./scripts/used-by.js"
|
|
31
|
+
},
|
|
29
32
|
"files": [
|
|
30
33
|
"dist",
|
|
34
|
+
"scripts/used-by.js",
|
|
31
35
|
"README.md"
|
|
32
36
|
],
|
|
33
37
|
"scripts": {
|
|
@@ -46,11 +50,11 @@
|
|
|
46
50
|
"get-port": "^7.1.0",
|
|
47
51
|
"node-html-parser": "^7.1.0",
|
|
48
52
|
"oxlint": "^1.51.0",
|
|
49
|
-
"typescript": "^5.9.3",
|
|
50
53
|
"vite": "^7.3.1",
|
|
51
54
|
"vite-plugin-dts": "^4.5.4"
|
|
52
55
|
},
|
|
53
56
|
"dependencies": {
|
|
57
|
+
"typescript": "^5.9.3",
|
|
54
58
|
"xss": "^1.0.15"
|
|
55
59
|
}
|
|
56
60
|
}
|
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const dry = args.includes('--dry');
|
|
9
|
+
const verbose = args.includes('--verbose');
|
|
10
|
+
const inputPaths = args.filter(arg => !arg.startsWith('--'));
|
|
11
|
+
|
|
12
|
+
if (args.includes('--write')) {
|
|
13
|
+
console.error('--write is no longer supported; writing is now the default.');
|
|
14
|
+
process.exit(1);
|
|
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'
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const targets = inputPaths.map(file => path.resolve(cwd, file));
|
|
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
|
+
}
|
|
37
|
+
|
|
38
|
+
const stat = fs.statSync(target);
|
|
39
|
+
if (!stat.isFile()) {
|
|
40
|
+
console.error(`Not a file: ${path.relative(cwd, target)}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!/\.(js|ts)$/.test(target) || /\.d\.ts$/.test(target)) {
|
|
45
|
+
console.error(
|
|
46
|
+
`Unsupported file type: ${path.relative(cwd, target)}`
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectFiles(startPath, files = []) {
|
|
53
|
+
if (!fs.existsSync(startPath)) return files;
|
|
54
|
+
|
|
55
|
+
const stat = fs.statSync(startPath);
|
|
56
|
+
if (stat.isFile()) {
|
|
57
|
+
if (/\.(js|ts)$/.test(startPath) && !/\.d\.ts$/.test(startPath)) {
|
|
58
|
+
files.push(startPath);
|
|
59
|
+
}
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const entry of fs.readdirSync(startPath, {withFileTypes: true})) {
|
|
64
|
+
const fullPath = path.join(startPath, entry.name);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
if (entry.name === 'node_modules' || entry.name === 'dist') continue;
|
|
67
|
+
collectFiles(fullPath, files);
|
|
68
|
+
} else if (
|
|
69
|
+
entry.isFile() &&
|
|
70
|
+
/\.(js|ts)$/.test(entry.name) &&
|
|
71
|
+
!entry.name.endsWith('.d.ts') &&
|
|
72
|
+
!entry.name.includes('.test.')
|
|
73
|
+
) {
|
|
74
|
+
files.push(fullPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const files = targets.flatMap(target => collectFiles(target));
|
|
82
|
+
|
|
83
|
+
function getNameText(name) {
|
|
84
|
+
if (
|
|
85
|
+
ts.isIdentifier(name) ||
|
|
86
|
+
ts.isStringLiteral(name) ||
|
|
87
|
+
ts.isPrivateIdentifier(name)
|
|
88
|
+
) {
|
|
89
|
+
return name.text;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasStaticModifier(node) {
|
|
95
|
+
return Boolean(
|
|
96
|
+
node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getWrecImportInfo(sourceFile) {
|
|
101
|
+
const names = new Set(['Wrec']);
|
|
102
|
+
let quote = "'";
|
|
103
|
+
|
|
104
|
+
for (const statement of sourceFile.statements) {
|
|
105
|
+
if (
|
|
106
|
+
!ts.isImportDeclaration(statement) ||
|
|
107
|
+
!statement.importClause ||
|
|
108
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
109
|
+
) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const moduleName = statement.moduleSpecifier.text;
|
|
114
|
+
const isWrecModule =
|
|
115
|
+
moduleName === 'wrec' ||
|
|
116
|
+
moduleName === 'wrec/ssr' ||
|
|
117
|
+
moduleName.endsWith('/wrec') ||
|
|
118
|
+
moduleName.endsWith('/wrec-ssr');
|
|
119
|
+
if (!isWrecModule) continue;
|
|
120
|
+
|
|
121
|
+
const namedBindings = statement.importClause.namedBindings;
|
|
122
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
123
|
+
|
|
124
|
+
for (const element of namedBindings.elements) {
|
|
125
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
126
|
+
if (importedName === 'Wrec') {
|
|
127
|
+
names.add(element.name.text);
|
|
128
|
+
|
|
129
|
+
const moduleText = statement.moduleSpecifier.getText(sourceFile);
|
|
130
|
+
if (moduleText.startsWith('"') || moduleText.startsWith("'")) {
|
|
131
|
+
quote = moduleText[0];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {names, quote};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function extendsWrec(node, wrecNames) {
|
|
141
|
+
return Boolean(
|
|
142
|
+
node.heritageClauses?.some(
|
|
143
|
+
clause =>
|
|
144
|
+
clause.token === ts.SyntaxKind.ExtendsKeyword &&
|
|
145
|
+
clause.types.some(
|
|
146
|
+
type =>
|
|
147
|
+
ts.isExpressionWithTypeArguments(type) &&
|
|
148
|
+
ts.isIdentifier(type.expression) &&
|
|
149
|
+
wrecNames.has(type.expression.text)
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getTemplateCalledMethods(classNode) {
|
|
156
|
+
const methodNames = new Set();
|
|
157
|
+
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
158
|
+
|
|
159
|
+
function visit(node) {
|
|
160
|
+
if (
|
|
161
|
+
ts.isPropertyAccessExpression(node) &&
|
|
162
|
+
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
163
|
+
ts.isCallExpression(node.parent) &&
|
|
164
|
+
node.parent.expression === node
|
|
165
|
+
) {
|
|
166
|
+
methodNames.add(node.name.text);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ts.forEachChild(node, visit);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function addTemplateTextMethods(template) {
|
|
173
|
+
const text = template.getText();
|
|
174
|
+
for (const match of text.matchAll(CALL_RE)) {
|
|
175
|
+
methodNames.add(match[1]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const member of classNode.members) {
|
|
180
|
+
if (
|
|
181
|
+
ts.isPropertyDeclaration(member) &&
|
|
182
|
+
hasStaticModifier(member) &&
|
|
183
|
+
getNameText(member.name) === 'html' &&
|
|
184
|
+
member.initializer
|
|
185
|
+
) {
|
|
186
|
+
if (
|
|
187
|
+
ts.isTaggedTemplateExpression(member.initializer) &&
|
|
188
|
+
ts.isIdentifier(member.initializer.tag) &&
|
|
189
|
+
member.initializer.tag.text === 'html'
|
|
190
|
+
) {
|
|
191
|
+
addTemplateTextMethods(member.initializer.template);
|
|
192
|
+
}
|
|
193
|
+
ts.forEachChild(member.initializer, visit);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return methodNames;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getComputedCalledMethods(classNode) {
|
|
201
|
+
const methodNames = new Set();
|
|
202
|
+
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
203
|
+
|
|
204
|
+
for (const member of classNode.members) {
|
|
205
|
+
if (
|
|
206
|
+
!ts.isPropertyDeclaration(member) ||
|
|
207
|
+
!hasStaticModifier(member) ||
|
|
208
|
+
getNameText(member.name) !== 'properties' ||
|
|
209
|
+
!member.initializer ||
|
|
210
|
+
!ts.isObjectLiteralExpression(member.initializer)
|
|
211
|
+
) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const property of member.initializer.properties) {
|
|
216
|
+
if (
|
|
217
|
+
!ts.isPropertyAssignment(property) ||
|
|
218
|
+
!ts.isObjectLiteralExpression(property.initializer)
|
|
219
|
+
) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const configProperty of property.initializer.properties) {
|
|
224
|
+
if (
|
|
225
|
+
!ts.isPropertyAssignment(configProperty) ||
|
|
226
|
+
getNameText(configProperty.name) !== 'computed'
|
|
227
|
+
) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!ts.isStringLiteralLike(configProperty.initializer)) continue;
|
|
232
|
+
const computed = configProperty.initializer.text;
|
|
233
|
+
for (const match of computed.matchAll(CALL_RE)) {
|
|
234
|
+
methodNames.add(match[1]);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return methodNames;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getMethodUsages(classNode, propertyNames) {
|
|
244
|
+
const methodInfo = new Map();
|
|
245
|
+
for (const member of classNode.members) {
|
|
246
|
+
if (hasStaticModifier(member)) continue;
|
|
247
|
+
|
|
248
|
+
if (
|
|
249
|
+
(ts.isMethodDeclaration(member) ||
|
|
250
|
+
ts.isGetAccessorDeclaration(member) ||
|
|
251
|
+
ts.isSetAccessorDeclaration(member)) &&
|
|
252
|
+
member.body
|
|
253
|
+
) {
|
|
254
|
+
const methodName = getNameText(member.name);
|
|
255
|
+
if (!methodName) continue;
|
|
256
|
+
|
|
257
|
+
const props = new Set();
|
|
258
|
+
const calledMethods = new Set();
|
|
259
|
+
const isPrivate = ts.isPrivateIdentifier(member.name);
|
|
260
|
+
|
|
261
|
+
function addBindingName(name) {
|
|
262
|
+
if (ts.isIdentifier(name)) {
|
|
263
|
+
props.add(name.text);
|
|
264
|
+
} else if (ts.isObjectBindingPattern(name)) {
|
|
265
|
+
addObjectBindingProps(name);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function addObjectBindingProps(bindingPattern) {
|
|
270
|
+
for (const element of bindingPattern.elements) {
|
|
271
|
+
if (element.dotDotDotToken) continue;
|
|
272
|
+
|
|
273
|
+
if (element.propertyName) {
|
|
274
|
+
const name = getNameText(element.propertyName);
|
|
275
|
+
if (name) props.add(name);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (ts.isIdentifier(element.name)) {
|
|
280
|
+
props.add(element.name.text);
|
|
281
|
+
} else if (ts.isObjectBindingPattern(element.name)) {
|
|
282
|
+
addObjectBindingProps(element.name);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function visit(child) {
|
|
288
|
+
if (
|
|
289
|
+
ts.isPropertyAccessExpression(child) &&
|
|
290
|
+
child.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
291
|
+
) {
|
|
292
|
+
const name = child.name.text;
|
|
293
|
+
props.add(name);
|
|
294
|
+
if (
|
|
295
|
+
ts.isCallExpression(child.parent) &&
|
|
296
|
+
child.parent.expression === child
|
|
297
|
+
) {
|
|
298
|
+
calledMethods.add(name);
|
|
299
|
+
}
|
|
300
|
+
} else if (
|
|
301
|
+
ts.isElementAccessExpression(child) &&
|
|
302
|
+
child.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
303
|
+
child.argumentExpression &&
|
|
304
|
+
ts.isStringLiteralLike(child.argumentExpression)
|
|
305
|
+
) {
|
|
306
|
+
const name = child.argumentExpression.text;
|
|
307
|
+
props.add(name);
|
|
308
|
+
if (
|
|
309
|
+
ts.isCallExpression(child.parent) &&
|
|
310
|
+
child.parent.expression === child
|
|
311
|
+
) {
|
|
312
|
+
calledMethods.add(name);
|
|
313
|
+
}
|
|
314
|
+
} else if (
|
|
315
|
+
ts.isVariableDeclaration(child) &&
|
|
316
|
+
child.initializer &&
|
|
317
|
+
child.initializer.kind === ts.SyntaxKind.ThisKeyword
|
|
318
|
+
) {
|
|
319
|
+
if (ts.isObjectBindingPattern(child.name)) {
|
|
320
|
+
addObjectBindingProps(child.name);
|
|
321
|
+
} else {
|
|
322
|
+
addBindingName(child.name);
|
|
323
|
+
}
|
|
324
|
+
} else if (
|
|
325
|
+
ts.isBinaryExpression(child) &&
|
|
326
|
+
child.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
327
|
+
child.right.kind === ts.SyntaxKind.ThisKeyword
|
|
328
|
+
) {
|
|
329
|
+
if (ts.isObjectLiteralExpression(child.left)) {
|
|
330
|
+
for (const property of child.left.properties) {
|
|
331
|
+
if (
|
|
332
|
+
ts.isShorthandPropertyAssignment(property) ||
|
|
333
|
+
ts.isPropertyAssignment(property)
|
|
334
|
+
) {
|
|
335
|
+
const name = getNameText(property.name);
|
|
336
|
+
if (name) props.add(name);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} else if (ts.isObjectBindingPattern(child.left)) {
|
|
340
|
+
addObjectBindingProps(child.left);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
ts.forEachChild(child, visit);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
ts.forEachChild(member.body, visit);
|
|
348
|
+
methodInfo.set(methodName, {calledMethods, isPrivate, props});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const entryMethods = new Set([
|
|
353
|
+
...getTemplateCalledMethods(classNode),
|
|
354
|
+
...getComputedCalledMethods(classNode)
|
|
355
|
+
]);
|
|
356
|
+
const memo = new Map();
|
|
357
|
+
|
|
358
|
+
function getTransitiveProps(methodName, seen = new Set()) {
|
|
359
|
+
if (memo.has(methodName)) return memo.get(methodName);
|
|
360
|
+
if (seen.has(methodName)) return new Set();
|
|
361
|
+
|
|
362
|
+
seen.add(methodName);
|
|
363
|
+
const info = methodInfo.get(methodName);
|
|
364
|
+
const props = new Set(info?.props ?? []);
|
|
365
|
+
|
|
366
|
+
if (info) {
|
|
367
|
+
for (const calledMethod of info.calledMethods) {
|
|
368
|
+
const calledProps = getTransitiveProps(calledMethod, seen);
|
|
369
|
+
for (const propName of calledProps) props.add(propName);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
seen.delete(methodName);
|
|
374
|
+
memo.set(methodName, props);
|
|
375
|
+
return props;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const propToMethods = new Map();
|
|
379
|
+
for (const methodName of entryMethods) {
|
|
380
|
+
const info = methodInfo.get(methodName);
|
|
381
|
+
if (!info || info.isPrivate) continue;
|
|
382
|
+
|
|
383
|
+
if (info.isPrivate) continue;
|
|
384
|
+
|
|
385
|
+
for (const propName of getTransitiveProps(methodName)) {
|
|
386
|
+
if (!propertyNames.has(propName)) continue;
|
|
387
|
+
|
|
388
|
+
let methods = propToMethods.get(propName);
|
|
389
|
+
if (!methods) {
|
|
390
|
+
methods = new Set();
|
|
391
|
+
propToMethods.set(propName, methods);
|
|
392
|
+
}
|
|
393
|
+
methods.add(methodName);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return propToMethods;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function createUsedByProperty(methodNames, quote) {
|
|
401
|
+
return `usedBy: [${methodNames.map(name => `${quote}${name}${quote}`).join(', ')}]`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getIndent(text, pos) {
|
|
405
|
+
const lineStart = text.lastIndexOf('\n', pos - 1) + 1;
|
|
406
|
+
const match = /^[ \t]*/.exec(text.slice(lineStart));
|
|
407
|
+
return match ? match[0] : '';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function buildConfigText(sourceFile, member, methodNames, quote) {
|
|
411
|
+
const {text} = sourceFile;
|
|
412
|
+
const configObject = member.initializer;
|
|
413
|
+
const existingMembers = configObject.properties.filter(
|
|
414
|
+
property =>
|
|
415
|
+
!(
|
|
416
|
+
ts.isPropertyAssignment(property) &&
|
|
417
|
+
getNameText(property.name) === 'usedBy'
|
|
418
|
+
)
|
|
419
|
+
);
|
|
420
|
+
const existingTexts = existingMembers.map(property =>
|
|
421
|
+
text.slice(property.getStart(sourceFile), property.end).trim()
|
|
422
|
+
);
|
|
423
|
+
if (methodNames.length > 0)
|
|
424
|
+
existingTexts.push(createUsedByProperty(methodNames, quote));
|
|
425
|
+
|
|
426
|
+
const original = text.slice(
|
|
427
|
+
configObject.getStart(sourceFile),
|
|
428
|
+
configObject.end
|
|
429
|
+
);
|
|
430
|
+
const multiline = original.includes('\n');
|
|
431
|
+
if (!multiline) return `{${existingTexts.join(', ')}}`;
|
|
432
|
+
|
|
433
|
+
const memberIndent = getIndent(text, member.getStart(sourceFile));
|
|
434
|
+
const firstExisting = existingMembers[0];
|
|
435
|
+
const innerIndent = firstExisting
|
|
436
|
+
? getIndent(text, firstExisting.getStart(sourceFile))
|
|
437
|
+
: memberIndent + ' ';
|
|
438
|
+
return `{\n${existingTexts.map(part => `${innerIndent}${part}`).join(',\n')}\n${memberIndent}}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function transformSourceFile(sourceFile) {
|
|
442
|
+
const edits = [];
|
|
443
|
+
const {names: wrecNames, quote} = getWrecImportInfo(sourceFile);
|
|
444
|
+
let foundWrecSubclass = false;
|
|
445
|
+
|
|
446
|
+
for (const node of sourceFile.statements) {
|
|
447
|
+
if (!ts.isClassDeclaration(node) || !extendsWrec(node, wrecNames)) continue;
|
|
448
|
+
foundWrecSubclass = true;
|
|
449
|
+
|
|
450
|
+
let propertiesObject = null;
|
|
451
|
+
for (const member of node.members) {
|
|
452
|
+
if (
|
|
453
|
+
ts.isPropertyDeclaration(member) &&
|
|
454
|
+
hasStaticModifier(member) &&
|
|
455
|
+
getNameText(member.name) === 'properties' &&
|
|
456
|
+
member.initializer &&
|
|
457
|
+
ts.isObjectLiteralExpression(member.initializer)
|
|
458
|
+
) {
|
|
459
|
+
propertiesObject = member.initializer;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (!propertiesObject) continue;
|
|
465
|
+
|
|
466
|
+
const propertyNames = new Set(
|
|
467
|
+
propertiesObject.properties
|
|
468
|
+
.filter(ts.isPropertyAssignment)
|
|
469
|
+
.map(property => getNameText(property.name))
|
|
470
|
+
.filter(Boolean)
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const propToMethods = getMethodUsages(node, propertyNames);
|
|
474
|
+
for (const member of propertiesObject.properties) {
|
|
475
|
+
if (!ts.isPropertyAssignment(member)) continue;
|
|
476
|
+
|
|
477
|
+
const propName = getNameText(member.name);
|
|
478
|
+
if (!propName || !ts.isObjectLiteralExpression(member.initializer))
|
|
479
|
+
continue;
|
|
480
|
+
|
|
481
|
+
const methodNames = [
|
|
482
|
+
...(propToMethods.get(propName) ?? new Set())
|
|
483
|
+
].sort();
|
|
484
|
+
const configObject = member.initializer;
|
|
485
|
+
const existingMembers = configObject.properties.filter(
|
|
486
|
+
property =>
|
|
487
|
+
!(
|
|
488
|
+
ts.isPropertyAssignment(property) &&
|
|
489
|
+
getNameText(property.name) === 'usedBy'
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
const hadUsedBy =
|
|
493
|
+
existingMembers.length !== configObject.properties.length;
|
|
494
|
+
const needsUsedBy = methodNames.length > 0;
|
|
495
|
+
if (!hadUsedBy && !needsUsedBy) continue;
|
|
496
|
+
|
|
497
|
+
const nextText = buildConfigText(sourceFile, member, methodNames, quote);
|
|
498
|
+
const currentText = sourceFile.text.slice(
|
|
499
|
+
configObject.getStart(sourceFile),
|
|
500
|
+
configObject.end
|
|
501
|
+
);
|
|
502
|
+
if (nextText !== currentText) {
|
|
503
|
+
edits.push({
|
|
504
|
+
start: configObject.getStart(sourceFile),
|
|
505
|
+
end: configObject.end,
|
|
506
|
+
propName,
|
|
507
|
+
oldText: currentText,
|
|
508
|
+
text: nextText
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!foundWrecSubclass) {
|
|
515
|
+
return {
|
|
516
|
+
changed: false,
|
|
517
|
+
edits: [],
|
|
518
|
+
foundWrecSubclass: false,
|
|
519
|
+
text: sourceFile.text
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (edits.length === 0) {
|
|
524
|
+
return {
|
|
525
|
+
changed: false,
|
|
526
|
+
edits: [],
|
|
527
|
+
foundWrecSubclass: true,
|
|
528
|
+
text: sourceFile.text
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let nextSource = sourceFile.text;
|
|
533
|
+
edits.sort((a, b) => b.start - a.start);
|
|
534
|
+
for (const edit of edits) {
|
|
535
|
+
nextSource =
|
|
536
|
+
nextSource.slice(0, edit.start) + edit.text + nextSource.slice(edit.end);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {changed: true, edits, foundWrecSubclass: true, text: nextSource};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let changedCount = 0;
|
|
543
|
+
|
|
544
|
+
for (const file of files) {
|
|
545
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
546
|
+
const scriptKind = file.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
547
|
+
const sourceFile = ts.createSourceFile(
|
|
548
|
+
file,
|
|
549
|
+
text,
|
|
550
|
+
ts.ScriptTarget.Latest,
|
|
551
|
+
true,
|
|
552
|
+
scriptKind
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
const {
|
|
556
|
+
changed,
|
|
557
|
+
edits,
|
|
558
|
+
foundWrecSubclass,
|
|
559
|
+
text: nextText
|
|
560
|
+
} = transformSourceFile(sourceFile);
|
|
561
|
+
if (!foundWrecSubclass) {
|
|
562
|
+
console.error('No class extending Wrec was found.');
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
if (!changed) continue;
|
|
566
|
+
|
|
567
|
+
changedCount++;
|
|
568
|
+
if (dry) {
|
|
569
|
+
for (const edit of edits.toReversed()) {
|
|
570
|
+
const match = edit.text.match(/usedBy:\s*\[[^\]]*\]/);
|
|
571
|
+
const suggestion = match ? match[0] : 'remove usedBy';
|
|
572
|
+
console.log(`${edit.propName} - ${suggestion}`);
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
fs.writeFileSync(file, nextText);
|
|
576
|
+
}
|
|
577
|
+
if (verbose && !dry) console.log('updated');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (dry && changedCount > 0) process.exit(1);
|
|
581
|
+
|
|
582
|
+
if (dry && changedCount === 0) {
|
|
583
|
+
console.log('usedBy is already up to date.');
|
|
584
|
+
}
|