@ethlete/query 6.0.0-next.51 → 6.0.0-next.52
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/CHANGELOG.md +35 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +321 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query.mjs +340 -136
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +5 -0
- package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/consumers.js +453 -0
- package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
- package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
- package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
- package/generators/migrate-from-ngrx-toolkit/migration.js +139 -0
- package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
- package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
- package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
- package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
- package/generators/migrate-query-opt-in-features/migration.js +10 -9
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
- package/generators/migrate-to-query-v3/cleanup-migration.js +22 -1
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
- package/generators/migrate-to-query-v3/migration.js +2 -1
- package/generators/migrate-to-query-v3/module-graph.js +2 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +71 -2
- package/generators/migrate-to-query-v3/shared.js +27 -0
- package/generators/prep-for-query-v3/migration.js +83 -1
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +16 -0
- package/package.json +5 -1
- package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
- package/types/ethlete-query.d.ts +129 -34
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { createSourceFile } from '../migrate-to-query-v3/shared.js';
|
|
4
|
+
import { TOOLKIT_TASK } from './report.js';
|
|
5
|
+
import { applyEdits, collapseBlankLines, declaredNames, lineOf, pruneUnusedImports, relativeSpecifier, removeStatement, } from './ts-edits.js';
|
|
6
|
+
const MAX_CASCADE_ROUNDS = 6;
|
|
7
|
+
const FEATURE_REGISTRATIONS = new Set(['StoreModule.forFeature', 'provideState']);
|
|
8
|
+
const EFFECTS_REGISTRATIONS = new Set(['EffectsModule.forFeature', 'provideEffects']);
|
|
9
|
+
const calleeText = (call) => call.expression.getText().replace(/\s+/g, '');
|
|
10
|
+
const isDeclarationName = (node) => {
|
|
11
|
+
const parent = node.parent;
|
|
12
|
+
if ((ts.isVariableDeclaration(parent) ||
|
|
13
|
+
ts.isFunctionDeclaration(parent) ||
|
|
14
|
+
ts.isClassDeclaration(parent) ||
|
|
15
|
+
ts.isInterfaceDeclaration(parent) ||
|
|
16
|
+
ts.isTypeAliasDeclaration(parent) ||
|
|
17
|
+
ts.isEnumDeclaration(parent) ||
|
|
18
|
+
ts.isParameter(parent) ||
|
|
19
|
+
ts.isPropertyDeclaration(parent) ||
|
|
20
|
+
ts.isMethodDeclaration(parent) ||
|
|
21
|
+
ts.isPropertySignature(parent) ||
|
|
22
|
+
ts.isMethodSignature(parent) ||
|
|
23
|
+
ts.isPropertyAssignment(parent) ||
|
|
24
|
+
ts.isBindingElement(parent) ||
|
|
25
|
+
ts.isEnumMember(parent) ||
|
|
26
|
+
ts.isGetAccessor(parent) ||
|
|
27
|
+
ts.isSetAccessor(parent)) &&
|
|
28
|
+
parent.name === node) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
32
|
+
return true;
|
|
33
|
+
if (ts.isQualifiedName(parent) && parent.right === node)
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
const listOf = (node) => {
|
|
38
|
+
const parent = node.parent;
|
|
39
|
+
if (ts.isArrayLiteralExpression(parent))
|
|
40
|
+
return parent.elements;
|
|
41
|
+
if (ts.isObjectLiteralExpression(parent))
|
|
42
|
+
return parent.properties;
|
|
43
|
+
if (ts.isCallExpression(parent) && parent.arguments.includes(node))
|
|
44
|
+
return parent.arguments;
|
|
45
|
+
if (ts.isHeritageClause(parent))
|
|
46
|
+
return parent.types;
|
|
47
|
+
if (ts.isNamedExports(parent))
|
|
48
|
+
return parent.elements;
|
|
49
|
+
if (ts.isIntersectionTypeNode(parent))
|
|
50
|
+
return parent.types;
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
const EMPTY_STATE_TYPE = 'Record<string, never>';
|
|
54
|
+
/** A registration call (`StoreModule.forFeature(…)`) can only go if it sits in an `imports` or providers list. */
|
|
55
|
+
const registrationTarget = (call) => {
|
|
56
|
+
const parent = call.parent;
|
|
57
|
+
if (ts.isArrayLiteralExpression(parent))
|
|
58
|
+
return call;
|
|
59
|
+
if (ts.isCallExpression(parent) && calleeText(parent) === 'importProvidersFrom')
|
|
60
|
+
return call;
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
const classify = (reference, isDeletedExpression) => {
|
|
64
|
+
const parent = reference.parent;
|
|
65
|
+
if (ts.isTypeReferenceNode(parent) && parent.typeName === reference) {
|
|
66
|
+
const owner = parent.parent;
|
|
67
|
+
if (parent.typeArguments)
|
|
68
|
+
return null;
|
|
69
|
+
if (ts.isIntersectionTypeNode(owner))
|
|
70
|
+
return parent;
|
|
71
|
+
if (ts.isTypeAliasDeclaration(owner) && owner.type === parent)
|
|
72
|
+
return parent;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (ts.isCallExpression(parent) && parent.arguments.includes(reference)) {
|
|
76
|
+
const callee = calleeText(parent);
|
|
77
|
+
if (FEATURE_REGISTRATIONS.has(callee) || EFFECTS_REGISTRATIONS.has(callee))
|
|
78
|
+
return registrationTarget(parent);
|
|
79
|
+
if (callee === 'importProvidersFrom')
|
|
80
|
+
return reference;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (ts.isArrayLiteralExpression(parent)) {
|
|
84
|
+
const owner = parent.parent;
|
|
85
|
+
if (ts.isCallExpression(owner) &&
|
|
86
|
+
EFFECTS_REGISTRATIONS.has(calleeText(owner)) &&
|
|
87
|
+
parent.elements.every((element) => isDeletedExpression(element))) {
|
|
88
|
+
return registrationTarget(owner);
|
|
89
|
+
}
|
|
90
|
+
return reference;
|
|
91
|
+
}
|
|
92
|
+
if (ts.isComputedPropertyName(parent) && ts.isPropertyAssignment(parent.parent))
|
|
93
|
+
return parent.parent;
|
|
94
|
+
if (ts.isPropertyAssignment(parent) && parent.initializer === reference)
|
|
95
|
+
return parent;
|
|
96
|
+
if (ts.isShorthandPropertyAssignment(parent))
|
|
97
|
+
return parent;
|
|
98
|
+
if (ts.isExpressionWithTypeArguments(parent) && ts.isHeritageClause(parent.parent))
|
|
99
|
+
return parent;
|
|
100
|
+
if (ts.isExportSpecifier(parent))
|
|
101
|
+
return parent;
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
const scanReferences = (tree, graph, inScope, options) => {
|
|
105
|
+
const result = { references: [], rewrites: [] };
|
|
106
|
+
visitNotIgnoredFiles(tree, '', (file) => {
|
|
107
|
+
if (!file.endsWith('.ts') || file.endsWith('.d.ts') || options.skipFiles.has(file))
|
|
108
|
+
return;
|
|
109
|
+
const content = tree.read(file, 'utf-8');
|
|
110
|
+
const locals = options.localSymbols.get(file);
|
|
111
|
+
if (!content || (!locals && !options.needles.some((needle) => content.includes(needle))))
|
|
112
|
+
return;
|
|
113
|
+
const sourceFile = createSourceFile(content, file);
|
|
114
|
+
const ranges = options.skipRanges.get(file) ?? [];
|
|
115
|
+
const named = new Map();
|
|
116
|
+
const namespaces = new Map();
|
|
117
|
+
const editable = inScope.has(file);
|
|
118
|
+
for (const statement of sourceFile.statements) {
|
|
119
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
120
|
+
continue;
|
|
121
|
+
const specifier = statement.moduleSpecifier.text;
|
|
122
|
+
const bindings = statement.importClause?.namedBindings;
|
|
123
|
+
if (bindings && ts.isNamespaceImport(bindings))
|
|
124
|
+
namespaces.set(bindings.name.text, specifier);
|
|
125
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
126
|
+
bindings.elements.forEach((element) => named.set(element.name.text, { specifier, imported: element.propertyName?.text ?? element.name.text }));
|
|
127
|
+
}
|
|
128
|
+
const resolved = graph.resolveFile(file, specifier);
|
|
129
|
+
const movedTo = resolved ? options.movedFiles.get(resolved) : undefined;
|
|
130
|
+
if (movedTo) {
|
|
131
|
+
result.rewrites.push({
|
|
132
|
+
file,
|
|
133
|
+
sourceFile,
|
|
134
|
+
literal: statement.moduleSpecifier,
|
|
135
|
+
to: relativeSpecifier(file, movedTo),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const statusOfExpression = (expression) => {
|
|
140
|
+
if (ts.isIdentifier(expression)) {
|
|
141
|
+
const imported = named.get(expression.text);
|
|
142
|
+
if (imported) {
|
|
143
|
+
const declaringFile = graph.findDeclaringFile(file, imported.specifier, imported.imported);
|
|
144
|
+
return declaringFile
|
|
145
|
+
? { status: options.statusOf(declaringFile, imported.imported), declaringFile, symbol: imported.imported }
|
|
146
|
+
: null;
|
|
147
|
+
}
|
|
148
|
+
if (locals?.has(expression.text) && !isDeclarationName(expression)) {
|
|
149
|
+
return { status: 'deleted', declaringFile: file, symbol: expression.text };
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if ((ts.isPropertyAccessExpression(expression) || ts.isQualifiedName(expression)) &&
|
|
154
|
+
ts.isIdentifier(ts.isPropertyAccessExpression(expression) ? expression.expression : expression.left)) {
|
|
155
|
+
const left = ts.isPropertyAccessExpression(expression) ? expression.expression : expression.left;
|
|
156
|
+
const right = ts.isPropertyAccessExpression(expression) ? expression.name : expression.right;
|
|
157
|
+
const specifier = namespaces.get(left.text);
|
|
158
|
+
if (!specifier)
|
|
159
|
+
return null;
|
|
160
|
+
const declaringFile = graph.findDeclaringFile(file, specifier, right.text);
|
|
161
|
+
return declaringFile
|
|
162
|
+
? { status: options.statusOf(declaringFile, right.text), declaringFile, symbol: right.text }
|
|
163
|
+
: null;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
167
|
+
const isDeletedExpression = (expression) => statusOfExpression(expression)?.status === 'deleted';
|
|
168
|
+
const record = (node, declaringFile, symbol) => {
|
|
169
|
+
const start = node.getStart(sourceFile);
|
|
170
|
+
if (ranges.some(([from, to]) => start >= from && start < to))
|
|
171
|
+
return;
|
|
172
|
+
result.references.push({
|
|
173
|
+
file,
|
|
174
|
+
sourceFile,
|
|
175
|
+
declaringFile,
|
|
176
|
+
symbol,
|
|
177
|
+
line: lineOf(sourceFile, start),
|
|
178
|
+
target: editable ? classify(node, isDeletedExpression) : null,
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
const visit = (node) => {
|
|
182
|
+
if (ts.isImportDeclaration(node))
|
|
183
|
+
return;
|
|
184
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
185
|
+
const resolved = graph.resolveFile(file, node.moduleSpecifier.text);
|
|
186
|
+
if (resolved && options.deletedFiles.has(resolved) && !editable) {
|
|
187
|
+
result.references.push({
|
|
188
|
+
file,
|
|
189
|
+
sourceFile,
|
|
190
|
+
declaringFile: resolved,
|
|
191
|
+
symbol: '*',
|
|
192
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
193
|
+
target: null,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (ts.isPropertyAccessExpression(node) ||
|
|
199
|
+
ts.isQualifiedName(node) ||
|
|
200
|
+
(ts.isIdentifier(node) && !isDeclarationName(node) && !ts.isExportSpecifier(node.parent))) {
|
|
201
|
+
const status = statusOfExpression(node);
|
|
202
|
+
if (status?.status === 'deleted') {
|
|
203
|
+
record(node, status.declaringFile, status.symbol);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (status)
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (ts.isExportSpecifier(node) && !node.parent.parent.moduleSpecifier) {
|
|
210
|
+
const local = node.propertyName ?? node.name;
|
|
211
|
+
const status = ts.isIdentifier(local) ? statusOfExpression(local) : null;
|
|
212
|
+
if (status?.status === 'deleted') {
|
|
213
|
+
record(local, status.declaringFile, status.symbol);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
ts.forEachChild(node, visit);
|
|
218
|
+
};
|
|
219
|
+
visit(sourceFile);
|
|
220
|
+
});
|
|
221
|
+
return result;
|
|
222
|
+
};
|
|
223
|
+
const indentationAt = (content, position) => {
|
|
224
|
+
let lineStart = position;
|
|
225
|
+
while (lineStart > 0 && content[lineStart - 1] !== '\n')
|
|
226
|
+
lineStart -= 1;
|
|
227
|
+
return /^[ \t]*/.exec(content.slice(lineStart))[0];
|
|
228
|
+
};
|
|
229
|
+
/** One edit per list that loses elements; a list left empty in a heritage clause or export takes its owner with it. */
|
|
230
|
+
const listRemovalEdits = (content, sourceFile, targets) => {
|
|
231
|
+
const byList = new Map();
|
|
232
|
+
const edits = [];
|
|
233
|
+
for (const target of targets) {
|
|
234
|
+
const list = listOf(target);
|
|
235
|
+
if (!list && ts.isTypeAliasDeclaration(target.parent)) {
|
|
236
|
+
edits.push({ start: target.getStart(sourceFile), end: target.getEnd(), text: EMPTY_STATE_TYPE });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!list)
|
|
240
|
+
continue;
|
|
241
|
+
const removed = byList.get(list) ?? new Set();
|
|
242
|
+
removed.add(target);
|
|
243
|
+
byList.set(list, removed);
|
|
244
|
+
}
|
|
245
|
+
for (const [list, removed] of byList) {
|
|
246
|
+
const kept = list.filter((node) => !removed.has(node));
|
|
247
|
+
const owner = list[0].parent;
|
|
248
|
+
if (ts.isIntersectionTypeNode(owner)) {
|
|
249
|
+
edits.push({
|
|
250
|
+
start: owner.getStart(sourceFile),
|
|
251
|
+
end: owner.getEnd(),
|
|
252
|
+
text: kept.length > 0 ? kept.map((node) => node.getText(sourceFile)).join(' & ') : EMPTY_STATE_TYPE,
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (kept.length === 0 && ts.isHeritageClause(owner)) {
|
|
257
|
+
edits.push({ start: owner.getFullStart(), end: owner.getEnd(), text: '' });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (kept.length === 0 && ts.isNamedExports(owner)) {
|
|
261
|
+
edits.push(removeStatement(owner.parent, content));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (kept.length === 0) {
|
|
265
|
+
let end = list.end;
|
|
266
|
+
while (/\s/.test(content[end] ?? ''))
|
|
267
|
+
end += 1;
|
|
268
|
+
edits.push({ start: list.pos, end: content[end] === ',' ? end + 1 : list.end, text: '' });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const first = list[0].getStart(sourceFile);
|
|
272
|
+
const multiline = content.slice(first, list[list.length - 1].getEnd()).includes('\n');
|
|
273
|
+
const separator = multiline ? `,\n${indentationAt(content, first)}` : ', ';
|
|
274
|
+
edits.push({
|
|
275
|
+
start: first,
|
|
276
|
+
end: list[list.length - 1].getEnd(),
|
|
277
|
+
text: kept.map((node) => node.getText(sourceFile)).join(separator),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return edits;
|
|
281
|
+
};
|
|
282
|
+
const EMPTY_INITIALIZER = /^(\{\s*\}|\[\s*\])$/;
|
|
283
|
+
const ngModuleConfig = (declaration) => {
|
|
284
|
+
const decorator = ts
|
|
285
|
+
.getDecorators(declaration)
|
|
286
|
+
?.find((candidate) => ts.isCallExpression(candidate.expression) && candidate.expression.expression.getText() === 'NgModule');
|
|
287
|
+
const argument = decorator && ts.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
|
|
288
|
+
return argument && ts.isObjectLiteralExpression(argument) ? argument : null;
|
|
289
|
+
};
|
|
290
|
+
const isTrivialNgModule = (declaration) => {
|
|
291
|
+
const config = ngModuleConfig(declaration);
|
|
292
|
+
if (!config || declaration.members.length > 0)
|
|
293
|
+
return false;
|
|
294
|
+
return config.properties.every((property) => {
|
|
295
|
+
if (!ts.isPropertyAssignment(property) || !ts.isArrayLiteralExpression(property.initializer))
|
|
296
|
+
return false;
|
|
297
|
+
return property.initializer.elements.every((element) => element.getText() === 'CommonModule');
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
const emptiableDeclarations = (sourceFile) => {
|
|
301
|
+
const found = new Map();
|
|
302
|
+
for (const statement of sourceFile.statements) {
|
|
303
|
+
if (ts.isVariableStatement(statement) && statement.declarationList.declarations.length === 1) {
|
|
304
|
+
const declaration = statement.declarationList.declarations[0];
|
|
305
|
+
const initializer = declaration.initializer;
|
|
306
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
307
|
+
initializer &&
|
|
308
|
+
(ts.isObjectLiteralExpression(initializer) || ts.isArrayLiteralExpression(initializer))) {
|
|
309
|
+
found.set(declaration.name.text, { kind: 'value', statement, sourceFile });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (ts.isInterfaceDeclaration(statement) && statement.heritageClauses?.length) {
|
|
313
|
+
found.set(statement.name.text, { kind: 'interface', statement, sourceFile });
|
|
314
|
+
}
|
|
315
|
+
if (ts.isClassDeclaration(statement) && statement.name && ngModuleConfig(statement)) {
|
|
316
|
+
found.set(statement.name.text, { kind: 'module', statement, sourceFile });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return found;
|
|
320
|
+
};
|
|
321
|
+
const isEmptied = (candidate) => {
|
|
322
|
+
const { statement, sourceFile } = candidate;
|
|
323
|
+
if (candidate.kind === 'value' && ts.isVariableStatement(statement)) {
|
|
324
|
+
return EMPTY_INITIALIZER.test(statement.declarationList.declarations[0].initializer.getText(sourceFile));
|
|
325
|
+
}
|
|
326
|
+
if (candidate.kind === 'interface' && ts.isInterfaceDeclaration(statement)) {
|
|
327
|
+
return !statement.heritageClauses?.length && statement.members.length === 0;
|
|
328
|
+
}
|
|
329
|
+
return ts.isClassDeclaration(statement) && isTrivialNgModule(statement);
|
|
330
|
+
};
|
|
331
|
+
const hasOnlyImports = (content) => createSourceFile(content).statements.every((statement) => ts.isImportDeclaration(statement));
|
|
332
|
+
/**
|
|
333
|
+
* Deletes the store files of converted features and unhooks them: the domain `ActionReducerMap`, effect lists,
|
|
334
|
+
* `StoreModule.forFeature` / `EffectsModule.forFeature` registrations, and barrels. A feature whose store files are
|
|
335
|
+
* still used somewhere this cannot rewrite keeps them, and gets a task.
|
|
336
|
+
*/
|
|
337
|
+
export const removeFeatureStores = (tree, graph, inScope, features, report) => {
|
|
338
|
+
const storeFilesOf = (feature) => [
|
|
339
|
+
feature.files.actions,
|
|
340
|
+
feature.files.reducer,
|
|
341
|
+
feature.files.effects,
|
|
342
|
+
feature.files.selectors,
|
|
343
|
+
feature.files.service,
|
|
344
|
+
].filter((file) => !!file);
|
|
345
|
+
const movedNames = new Map(features.map((feature) => [feature.files.actions, new Set(feature.calls.map((call) => call.name))]));
|
|
346
|
+
const featureOfFile = new Map();
|
|
347
|
+
features.forEach((feature) => storeFilesOf(feature).forEach((file) => featureOfFile.set(file, feature)));
|
|
348
|
+
const declaredIn = (file) => {
|
|
349
|
+
const content = tree.read(file, 'utf-8');
|
|
350
|
+
return content ? createSourceFile(content, file).statements.flatMap(declaredNames) : [];
|
|
351
|
+
};
|
|
352
|
+
let converting = new Set(features);
|
|
353
|
+
let scan;
|
|
354
|
+
const keptLocations = new Map();
|
|
355
|
+
for (;;) {
|
|
356
|
+
const deletedFiles = new Set([...converting].flatMap(storeFilesOf));
|
|
357
|
+
const movedFiles = new Map([...converting].map((feature) => [feature.files.actions, feature.queriesFile]));
|
|
358
|
+
const needles = [
|
|
359
|
+
...new Set([...deletedFiles].flatMap((file) => [...declaredIn(file), file.split('/').pop().replace(/\.ts$/, '')])),
|
|
360
|
+
];
|
|
361
|
+
scan = scanReferences(tree, graph, inScope, {
|
|
362
|
+
statusOf: (declaringFile, symbol) => {
|
|
363
|
+
if (!deletedFiles.has(declaringFile))
|
|
364
|
+
return null;
|
|
365
|
+
return movedFiles.has(declaringFile) && movedNames.get(declaringFile)?.has(symbol) ? 'moved' : 'deleted';
|
|
366
|
+
},
|
|
367
|
+
skipFiles: deletedFiles,
|
|
368
|
+
localSymbols: new Map(),
|
|
369
|
+
skipRanges: new Map(),
|
|
370
|
+
deletedFiles,
|
|
371
|
+
movedFiles,
|
|
372
|
+
needles,
|
|
373
|
+
});
|
|
374
|
+
const blocked = new Set();
|
|
375
|
+
for (const reference of scan.references) {
|
|
376
|
+
const feature = featureOfFile.get(reference.declaringFile);
|
|
377
|
+
if (!feature || reference.target || !converting.has(feature))
|
|
378
|
+
continue;
|
|
379
|
+
blocked.add(feature);
|
|
380
|
+
keptLocations.set(feature, [
|
|
381
|
+
...(keptLocations.get(feature) ?? []),
|
|
382
|
+
{ filePath: reference.file, line: reference.line },
|
|
383
|
+
]);
|
|
384
|
+
}
|
|
385
|
+
if (blocked.size === 0)
|
|
386
|
+
break;
|
|
387
|
+
converting = new Set([...converting].filter((feature) => !blocked.has(feature)));
|
|
388
|
+
}
|
|
389
|
+
for (const [feature, locations] of keptLocations) {
|
|
390
|
+
if (converting.has(feature))
|
|
391
|
+
continue;
|
|
392
|
+
report.add({
|
|
393
|
+
id: TOOLKIT_TASK.STORE_FILE_KEPT,
|
|
394
|
+
summary: `The store files of \`${feature.base}\` (${feature.dir}) are still used.`,
|
|
395
|
+
locations: [{ filePath: feature.files.actions }, ...locations],
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const deletedFiles = new Set([...converting].flatMap(storeFilesOf));
|
|
399
|
+
const movedFiles = new Map([...converting].map((feature) => [feature.files.actions, feature.queriesFile]));
|
|
400
|
+
const editedFiles = new Set();
|
|
401
|
+
const applyScan = (result, extraEdits) => {
|
|
402
|
+
const byFile = new Map();
|
|
403
|
+
const entry = (file, sourceFile) => {
|
|
404
|
+
const existing = byFile.get(file) ?? { sourceFile, targets: [], edits: [] };
|
|
405
|
+
byFile.set(file, existing);
|
|
406
|
+
return existing;
|
|
407
|
+
};
|
|
408
|
+
result.references.forEach((reference) => {
|
|
409
|
+
if (reference.target)
|
|
410
|
+
entry(reference.file, reference.sourceFile).targets.push(reference.target);
|
|
411
|
+
});
|
|
412
|
+
result.rewrites.forEach((rewrite) => entry(rewrite.file, rewrite.sourceFile).edits.push({
|
|
413
|
+
start: rewrite.literal.getStart(rewrite.sourceFile),
|
|
414
|
+
end: rewrite.literal.getEnd(),
|
|
415
|
+
text: `'${rewrite.to}'`,
|
|
416
|
+
}));
|
|
417
|
+
const snapshots = new Map();
|
|
418
|
+
for (const [file, { sourceFile, targets, edits }] of byFile) {
|
|
419
|
+
const content = sourceFile.text;
|
|
420
|
+
snapshots.set(file, emptiableDeclarations(sourceFile));
|
|
421
|
+
const next = applyEdits(content, [
|
|
422
|
+
...listRemovalEdits(content, sourceFile, targets),
|
|
423
|
+
...edits,
|
|
424
|
+
...(extraEdits.get(file) ?? []),
|
|
425
|
+
]);
|
|
426
|
+
tree.write(file, next);
|
|
427
|
+
editedFiles.add(file);
|
|
428
|
+
}
|
|
429
|
+
for (const [file, edits] of extraEdits) {
|
|
430
|
+
if (byFile.has(file))
|
|
431
|
+
continue;
|
|
432
|
+
const content = tree.read(file, 'utf-8');
|
|
433
|
+
snapshots.set(file, emptiableDeclarations(createSourceFile(content, file)));
|
|
434
|
+
tree.write(file, applyEdits(content, edits));
|
|
435
|
+
editedFiles.add(file);
|
|
436
|
+
}
|
|
437
|
+
return snapshots;
|
|
438
|
+
};
|
|
439
|
+
let snapshots = applyScan(scan, new Map());
|
|
440
|
+
for (let round = 0; round < MAX_CASCADE_ROUNDS; round += 1) {
|
|
441
|
+
let candidates = new Map();
|
|
442
|
+
for (const [file, before] of snapshots) {
|
|
443
|
+
const sourceFile = createSourceFile(tree.read(file, 'utf-8'), file);
|
|
444
|
+
const after = emptiableDeclarations(sourceFile);
|
|
445
|
+
const emptied = new Map();
|
|
446
|
+
for (const [name, candidate] of after) {
|
|
447
|
+
const previous = before.get(name);
|
|
448
|
+
if (previous && !isEmptied(previous) && isEmptied(candidate)) {
|
|
449
|
+
emptied.set(name, candidate);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (emptied.size > 0)
|
|
453
|
+
candidates.set(file, emptied);
|
|
454
|
+
}
|
|
455
|
+
if (candidates.size === 0)
|
|
456
|
+
break;
|
|
457
|
+
let cascadeScan;
|
|
458
|
+
for (;;) {
|
|
459
|
+
const current = candidates;
|
|
460
|
+
cascadeScan = scanReferences(tree, graph, inScope, {
|
|
461
|
+
statusOf: (declaringFile, symbol) => (current.get(declaringFile)?.has(symbol) ? 'deleted' : null),
|
|
462
|
+
skipFiles: deletedFiles,
|
|
463
|
+
localSymbols: new Map([...current].map(([file, names]) => [file, new Set(names.keys())])),
|
|
464
|
+
skipRanges: new Map([...current].map(([file, names]) => [
|
|
465
|
+
file,
|
|
466
|
+
[...names.values()].map(({ statement }) => [statement.getFullStart(), statement.getEnd()]),
|
|
467
|
+
])),
|
|
468
|
+
deletedFiles: new Set(),
|
|
469
|
+
movedFiles: new Map(),
|
|
470
|
+
needles: [...new Set([...current.values()].flatMap((names) => [...names.keys()]))],
|
|
471
|
+
});
|
|
472
|
+
const blocked = new Set(cascadeScan.references
|
|
473
|
+
.filter((reference) => !reference.target)
|
|
474
|
+
.map((reference) => `${reference.declaringFile}#${reference.symbol}`));
|
|
475
|
+
if (blocked.size === 0)
|
|
476
|
+
break;
|
|
477
|
+
const next = new Map();
|
|
478
|
+
for (const [file, names] of current) {
|
|
479
|
+
const kept = new Map([...names].filter(([name]) => !blocked.has(`${file}#${name}`)));
|
|
480
|
+
if (kept.size > 0)
|
|
481
|
+
next.set(file, kept);
|
|
482
|
+
}
|
|
483
|
+
candidates = next;
|
|
484
|
+
if (candidates.size === 0)
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
if (candidates.size === 0)
|
|
488
|
+
break;
|
|
489
|
+
const declarationEdits = new Map();
|
|
490
|
+
for (const [file, names] of candidates) {
|
|
491
|
+
const content = tree.read(file, 'utf-8');
|
|
492
|
+
declarationEdits.set(file, [...names.values()].map(({ statement }) => removeStatement(statement, content)));
|
|
493
|
+
}
|
|
494
|
+
snapshots = applyScan({
|
|
495
|
+
references: cascadeScan.references.filter((reference) => candidates.get(reference.declaringFile)?.has(reference.symbol)),
|
|
496
|
+
rewrites: [],
|
|
497
|
+
}, declarationEdits);
|
|
498
|
+
}
|
|
499
|
+
removeOrphanedStateDeclarations(tree, graph, inScope, editedFiles, deletedFiles);
|
|
500
|
+
for (const file of editedFiles) {
|
|
501
|
+
if (deletedFiles.has(file) || !tree.exists(file))
|
|
502
|
+
continue;
|
|
503
|
+
tree.write(file, collapseBlankLines(pruneUnusedImports(tree.read(file, 'utf-8'))));
|
|
504
|
+
}
|
|
505
|
+
const emptiedFiles = [...editedFiles].filter((file) => !deletedFiles.has(file) && tree.exists(file) && hasOnlyImports(tree.read(file, 'utf-8')));
|
|
506
|
+
emptiedFiles.forEach((file) => deletedFiles.add(file));
|
|
507
|
+
rewriteBarrels(tree, graph, inScope, deletedFiles, movedFiles, movedNames);
|
|
508
|
+
for (const file of deletedFiles) {
|
|
509
|
+
if (tree.exists(file))
|
|
510
|
+
tree.delete(file);
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
deletedFiles: [...deletedFiles].sort(),
|
|
514
|
+
keptFeatures: features.filter((feature) => !converting.has(feature)),
|
|
515
|
+
};
|
|
516
|
+
};
|
|
517
|
+
const isOrphanCandidate = (statement, sourceFile) => {
|
|
518
|
+
if (ts.isInterfaceDeclaration(statement))
|
|
519
|
+
return !statement.heritageClauses?.length && statement.members.length === 0;
|
|
520
|
+
if (ts.isTypeAliasDeclaration(statement))
|
|
521
|
+
return statement.type.getText(sourceFile) === EMPTY_STATE_TYPE;
|
|
522
|
+
if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1)
|
|
523
|
+
return false;
|
|
524
|
+
const declaration = statement.declarationList.declarations[0];
|
|
525
|
+
const initializer = declaration.initializer;
|
|
526
|
+
if (!initializer || !ts.isIdentifier(declaration.name))
|
|
527
|
+
return false;
|
|
528
|
+
if (ts.isStringLiteral(initializer))
|
|
529
|
+
return /FEATURE_KEY$/.test(declaration.name.text);
|
|
530
|
+
return (ts.isCallExpression(initializer) &&
|
|
531
|
+
ts.isIdentifier(initializer.expression) &&
|
|
532
|
+
initializer.expression.text === 'createFeatureSelector');
|
|
533
|
+
};
|
|
534
|
+
/** Drops the feature selector, feature key and state type a domain index keeps once its last feature is gone. */
|
|
535
|
+
const removeOrphanedStateDeclarations = (tree, graph, inScope, editedFiles, deletedFiles) => {
|
|
536
|
+
let candidates = new Map();
|
|
537
|
+
for (const file of editedFiles) {
|
|
538
|
+
if (deletedFiles.has(file) || !tree.exists(file))
|
|
539
|
+
continue;
|
|
540
|
+
const sourceFile = createSourceFile(tree.read(file, 'utf-8'), file);
|
|
541
|
+
const found = new Map();
|
|
542
|
+
for (const statement of sourceFile.statements) {
|
|
543
|
+
if (isOrphanCandidate(statement, sourceFile))
|
|
544
|
+
declaredNames(statement).forEach((name) => found.set(name, statement));
|
|
545
|
+
}
|
|
546
|
+
if (found.size > 0)
|
|
547
|
+
candidates.set(file, found);
|
|
548
|
+
}
|
|
549
|
+
while (candidates.size > 0) {
|
|
550
|
+
const current = candidates;
|
|
551
|
+
const scan = scanReferences(tree, graph, inScope, {
|
|
552
|
+
statusOf: (declaringFile, symbol) => (current.get(declaringFile)?.has(symbol) ? 'deleted' : null),
|
|
553
|
+
skipFiles: deletedFiles,
|
|
554
|
+
localSymbols: new Map([...current].map(([file, names]) => [file, new Set(names.keys())])),
|
|
555
|
+
skipRanges: new Map([...current].map(([file, names]) => [
|
|
556
|
+
file,
|
|
557
|
+
[...names.values()].map((statement) => [statement.getFullStart(), statement.getEnd()]),
|
|
558
|
+
])),
|
|
559
|
+
deletedFiles: new Set(),
|
|
560
|
+
movedFiles: new Map(),
|
|
561
|
+
needles: [...new Set([...current.values()].flatMap((names) => [...names.keys()]))],
|
|
562
|
+
});
|
|
563
|
+
const referenced = new Set(scan.references.map((reference) => `${reference.declaringFile}#${reference.symbol}`));
|
|
564
|
+
if (referenced.size === 0)
|
|
565
|
+
break;
|
|
566
|
+
const next = new Map();
|
|
567
|
+
for (const [file, names] of current) {
|
|
568
|
+
const kept = new Map([...names].filter(([name]) => !referenced.has(`${file}#${name}`)));
|
|
569
|
+
if (kept.size > 0)
|
|
570
|
+
next.set(file, kept);
|
|
571
|
+
}
|
|
572
|
+
candidates = next;
|
|
573
|
+
}
|
|
574
|
+
for (const [file, names] of candidates) {
|
|
575
|
+
const content = tree.read(file, 'utf-8');
|
|
576
|
+
tree.write(file, applyEdits(content, [...new Set(names.values())].map((statement) => removeStatement(statement, content))));
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
/** Points `export … from` of a deleted actions file at its queries file and drops those of other deleted files. */
|
|
580
|
+
const rewriteBarrels = (tree, graph, inScope, deletedFiles, movedFiles, movedNames) => {
|
|
581
|
+
for (let changed = true; changed;) {
|
|
582
|
+
changed = false;
|
|
583
|
+
for (const file of inScope) {
|
|
584
|
+
if (!file.endsWith('.ts') || deletedFiles.has(file) || !tree.exists(file))
|
|
585
|
+
continue;
|
|
586
|
+
const content = tree.read(file, 'utf-8');
|
|
587
|
+
if (!content.includes('export'))
|
|
588
|
+
continue;
|
|
589
|
+
const sourceFile = createSourceFile(content, file);
|
|
590
|
+
const edits = [];
|
|
591
|
+
for (const statement of sourceFile.statements) {
|
|
592
|
+
if (!ts.isExportDeclaration(statement) ||
|
|
593
|
+
!statement.moduleSpecifier ||
|
|
594
|
+
!ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const resolved = graph.resolveFile(file, statement.moduleSpecifier.text);
|
|
598
|
+
if (!resolved || !deletedFiles.has(resolved))
|
|
599
|
+
continue;
|
|
600
|
+
const movedTo = movedFiles.get(resolved);
|
|
601
|
+
const clause = statement.exportClause;
|
|
602
|
+
if (!movedTo) {
|
|
603
|
+
edits.push(removeStatement(statement, content));
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (clause && ts.isNamedExports(clause)) {
|
|
607
|
+
const moved = clause.elements.filter((element) => movedNames.get(resolved)?.has((element.propertyName ?? element.name).text));
|
|
608
|
+
edits.push(moved.length === 0
|
|
609
|
+
? removeStatement(statement, content)
|
|
610
|
+
: {
|
|
611
|
+
start: statement.getStart(sourceFile),
|
|
612
|
+
end: statement.getEnd(),
|
|
613
|
+
text: `export { ${moved.map((element) => element.getText(sourceFile)).join(', ')} } from '${relativeSpecifier(file, movedTo)}';`,
|
|
614
|
+
});
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
edits.push({
|
|
618
|
+
start: statement.moduleSpecifier.getStart(sourceFile),
|
|
619
|
+
end: statement.moduleSpecifier.getEnd(),
|
|
620
|
+
text: `'${relativeSpecifier(file, movedTo)}'`,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if (edits.length === 0)
|
|
624
|
+
continue;
|
|
625
|
+
const next = applyEdits(content, edits);
|
|
626
|
+
tree.write(file, next);
|
|
627
|
+
changed = true;
|
|
628
|
+
if (createSourceFile(next).statements.length === 0)
|
|
629
|
+
deletedFiles.add(file);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
//# sourceMappingURL=store-removal.js.map
|