@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,301 @@
|
|
|
1
|
+
import { getProjects } from '@nx/devkit';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { toolkitArgsFitCreator } from './args-assignability.js';
|
|
4
|
+
import { capitalizeFirstLetter, createSourceFile } from '../migrate-to-query-v3/shared.js';
|
|
5
|
+
import { HTTP_VERBS, findTypeDeclaration } from './feature.js';
|
|
6
|
+
import { TOOLKIT_TASK } from './report.js';
|
|
7
|
+
import { hasExportModifier, lineOf, pruneUnusedImports, relativeSpecifier } from './ts-edits.js';
|
|
8
|
+
export const clientBaseName = (client) => client.replace(/Config$/, '').replace(/Client$/, '');
|
|
9
|
+
export const helperName = (helpers, verb, secure) => `${helpers.baseName}${capitalizeFirstLetter(verb)}${secure ? 'Secure' : ''}`;
|
|
10
|
+
const normalizeType = (text) => text.replace(/\s+/g, ' ').replace(/;\s*}/g, ' }').trim();
|
|
11
|
+
export const isPublicRoute = (helpers, staticPrefix) => {
|
|
12
|
+
if (!helpers.publicRoutes)
|
|
13
|
+
return true;
|
|
14
|
+
return helpers.publicRoutes.some((prefix) => {
|
|
15
|
+
if (!staticPrefix.startsWith(prefix))
|
|
16
|
+
return false;
|
|
17
|
+
const next = staticPrefix[prefix.length];
|
|
18
|
+
return next === undefined || next === '/' || next === '?' || prefix.endsWith('/');
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const COMPARED_ARG_KEYS = ['queryParams', 'body'];
|
|
22
|
+
const withoutComments = (text) => text.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '');
|
|
23
|
+
const memberText = (member) => `${member.questionToken ? '?' : ''}${normalizeType(withoutComments(member.type.getText()))}`;
|
|
24
|
+
const collectArgMembers = (members, into) => {
|
|
25
|
+
for (const member of members) {
|
|
26
|
+
if (!ts.isPropertySignature(member) || !member.type)
|
|
27
|
+
continue;
|
|
28
|
+
const key = member.name.getText();
|
|
29
|
+
if (key === 'queryParams' || key === 'body')
|
|
30
|
+
into[key] = memberText(member);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const referencedMembers = (tree, graph, filePath, sourceFile, reference) => {
|
|
34
|
+
if (reference.typeArguments || !ts.isIdentifier(reference.typeName))
|
|
35
|
+
return null;
|
|
36
|
+
const name = reference.typeName.text;
|
|
37
|
+
let declaration = findTypeDeclaration(sourceFile, name);
|
|
38
|
+
for (const statement of sourceFile.statements) {
|
|
39
|
+
if (declaration)
|
|
40
|
+
break;
|
|
41
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
42
|
+
continue;
|
|
43
|
+
const named = statement.importClause?.namedBindings;
|
|
44
|
+
if (!named || !ts.isNamedImports(named))
|
|
45
|
+
continue;
|
|
46
|
+
const element = named.elements.find((candidate) => candidate.name.text === name);
|
|
47
|
+
const importedName = element?.propertyName?.text ?? name;
|
|
48
|
+
const declaringFile = element
|
|
49
|
+
? graph.findDeclaringFile(filePath, statement.moduleSpecifier.text, importedName)
|
|
50
|
+
: null;
|
|
51
|
+
const content = declaringFile ? tree.read(declaringFile, 'utf-8') : null;
|
|
52
|
+
if (declaringFile && content) {
|
|
53
|
+
declaration = findTypeDeclaration(createSourceFile(content, declaringFile), importedName);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (declaration && ts.isInterfaceDeclaration(declaration) && !declaration.typeParameters) {
|
|
57
|
+
return declaration.heritageClauses?.length ? null : declaration.members;
|
|
58
|
+
}
|
|
59
|
+
if (declaration && ts.isTypeAliasDeclaration(declaration) && ts.isTypeLiteralNode(declaration.type)) {
|
|
60
|
+
return declaration.type.members;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
const creatorArgsOf = (tree, graph, filePath, sourceFile, typeArgument) => {
|
|
65
|
+
const texts = { queryParams: null, body: null };
|
|
66
|
+
if (!typeArgument)
|
|
67
|
+
return texts;
|
|
68
|
+
const parts = ts.isIntersectionTypeNode(typeArgument) ? typeArgument.types : [typeArgument];
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
const members = ts.isTypeLiteralNode(part)
|
|
71
|
+
? part.members
|
|
72
|
+
: ts.isTypeReferenceNode(part)
|
|
73
|
+
? referencedMembers(tree, graph, filePath, sourceFile, part)
|
|
74
|
+
: null;
|
|
75
|
+
if (!members)
|
|
76
|
+
return null;
|
|
77
|
+
collectArgMembers(members, texts);
|
|
78
|
+
}
|
|
79
|
+
return texts;
|
|
80
|
+
};
|
|
81
|
+
const toolkitArgTexts = (call) => {
|
|
82
|
+
const text = (key) => {
|
|
83
|
+
const member = call.args?.members.get(key);
|
|
84
|
+
return member ? `${member.optional ? '?' : ''}${normalizeType(withoutComments(member.declaredText))}` : null;
|
|
85
|
+
};
|
|
86
|
+
return { queryParams: text('params'), body: text('body') };
|
|
87
|
+
};
|
|
88
|
+
const routeOf = (node) => {
|
|
89
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
90
|
+
return { normalized: node.text, pathParams: [] };
|
|
91
|
+
}
|
|
92
|
+
if (!ts.isArrowFunction(node) || node.parameters.length !== 1)
|
|
93
|
+
return null;
|
|
94
|
+
const param = node.parameters[0].name;
|
|
95
|
+
const body = node.body;
|
|
96
|
+
if (ts.isStringLiteral(body) || ts.isNoSubstitutionTemplateLiteral(body)) {
|
|
97
|
+
return { normalized: body.text, pathParams: [] };
|
|
98
|
+
}
|
|
99
|
+
if (!ts.isIdentifier(param) || !ts.isTemplateExpression(body))
|
|
100
|
+
return null;
|
|
101
|
+
const pathParams = [];
|
|
102
|
+
let normalized = body.head.text;
|
|
103
|
+
for (const span of body.templateSpans) {
|
|
104
|
+
const expression = span.expression;
|
|
105
|
+
if (!ts.isPropertyAccessExpression(expression) ||
|
|
106
|
+
!ts.isIdentifier(expression.expression) ||
|
|
107
|
+
expression.expression.text !== param.text) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
pathParams.push(expression.name.text);
|
|
111
|
+
normalized += `{}${span.literal.text}`;
|
|
112
|
+
}
|
|
113
|
+
return { normalized, pathParams };
|
|
114
|
+
};
|
|
115
|
+
const responseTypeOf = (typeArgument, sourceFile) => {
|
|
116
|
+
if (!typeArgument)
|
|
117
|
+
return null;
|
|
118
|
+
const literals = ts.isIntersectionTypeNode(typeArgument)
|
|
119
|
+
? typeArgument.types.filter(ts.isTypeLiteralNode)
|
|
120
|
+
: ts.isTypeLiteralNode(typeArgument)
|
|
121
|
+
? [typeArgument]
|
|
122
|
+
: [];
|
|
123
|
+
for (const literal of literals) {
|
|
124
|
+
for (const member of literal.members) {
|
|
125
|
+
if (ts.isPropertySignature(member) && member.name.getText(sourceFile) === 'response' && member.type) {
|
|
126
|
+
return normalizeType(member.type.getText(sourceFile));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
/** Every `export const x = <client><Verb>[Secure](route)` in the scope - the v3 creators D6 may reuse. */
|
|
133
|
+
export const indexExistingCreators = (tree, graph, helpers, visit) => {
|
|
134
|
+
const helperVerbs = new Map();
|
|
135
|
+
for (const verb of HTTP_VERBS) {
|
|
136
|
+
helperVerbs.set(helperName(helpers, verb, false), verb);
|
|
137
|
+
helperVerbs.set(helperName(helpers, verb, true), verb);
|
|
138
|
+
}
|
|
139
|
+
const creators = [];
|
|
140
|
+
visit((filePath) => {
|
|
141
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts') || filePath.endsWith('.d.ts'))
|
|
142
|
+
return;
|
|
143
|
+
const content = tree.read(filePath, 'utf-8');
|
|
144
|
+
if (!content || ![...helperVerbs.keys()].some((name) => content.includes(name)))
|
|
145
|
+
return;
|
|
146
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
147
|
+
for (const statement of sourceFile.statements) {
|
|
148
|
+
if (!ts.isVariableStatement(statement) || !hasExportModifier(statement))
|
|
149
|
+
continue;
|
|
150
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
151
|
+
const call = declaration.initializer;
|
|
152
|
+
if (!ts.isIdentifier(declaration.name) ||
|
|
153
|
+
!call ||
|
|
154
|
+
!ts.isCallExpression(call) ||
|
|
155
|
+
!ts.isIdentifier(call.expression)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const verb = helperVerbs.get(call.expression.text);
|
|
159
|
+
const route = call.arguments[0] ? routeOf(call.arguments[0]) : null;
|
|
160
|
+
if (!verb || !route)
|
|
161
|
+
continue;
|
|
162
|
+
creators.push({
|
|
163
|
+
filePath,
|
|
164
|
+
name: declaration.name.text,
|
|
165
|
+
verb,
|
|
166
|
+
normalizedRoute: route.normalized,
|
|
167
|
+
pathParams: route.pathParams,
|
|
168
|
+
responseType: responseTypeOf(call.typeArguments?.[0], sourceFile),
|
|
169
|
+
args: creatorArgsOf(tree, graph, filePath, sourceFile, call.typeArguments?.[0]),
|
|
170
|
+
argsTypeText: call.typeArguments?.[0]?.getText(sourceFile) ?? null,
|
|
171
|
+
line: lineOf(sourceFile, declaration.getStart(sourceFile)),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return creators;
|
|
177
|
+
};
|
|
178
|
+
const projectRootOf = (tree, filePath) => {
|
|
179
|
+
let best = null;
|
|
180
|
+
for (const project of getProjects(tree).values()) {
|
|
181
|
+
const root = project.root === '.' ? '' : project.root;
|
|
182
|
+
if ((root === '' || filePath.startsWith(`${root}/`)) && (best === null || root.length > best.length)) {
|
|
183
|
+
best = root;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return best;
|
|
187
|
+
};
|
|
188
|
+
/** How `fromFile` can import `symbol` from `declaringFile`: relatively inside one project, else through a path alias. */
|
|
189
|
+
export const importSpecifierFor = (tree, graph, fromFile, declaringFile, symbol) => {
|
|
190
|
+
const fromRoot = projectRootOf(tree, fromFile);
|
|
191
|
+
if (fromRoot !== null && fromRoot === projectRootOf(tree, declaringFile)) {
|
|
192
|
+
return relativeSpecifier(fromFile, declaringFile);
|
|
193
|
+
}
|
|
194
|
+
for (const alias of graph.pathAliases()) {
|
|
195
|
+
if (alias.includes('*'))
|
|
196
|
+
continue;
|
|
197
|
+
if (graph.findDeclaringFile(fromFile, alias, symbol) === declaringFile)
|
|
198
|
+
return alias;
|
|
199
|
+
}
|
|
200
|
+
return fromRoot === null && projectRootOf(tree, declaringFile) === null
|
|
201
|
+
? relativeSpecifier(fromFile, declaringFile)
|
|
202
|
+
: null;
|
|
203
|
+
};
|
|
204
|
+
const creatorTypeParameter = (call) => {
|
|
205
|
+
const members = [];
|
|
206
|
+
const renamed = { queryParams: 'pathParams', params: 'queryParams', body: 'body' };
|
|
207
|
+
for (const [key, member] of call.args?.members ?? []) {
|
|
208
|
+
members.push(`${renamed[key]}${member.optional ? '?' : ''}: ${member.typeText}`);
|
|
209
|
+
}
|
|
210
|
+
if (call.responseType)
|
|
211
|
+
members.push(`response: ${call.responseType}`);
|
|
212
|
+
return members.length > 0 ? `<{ ${members.join('; ')} }>` : '';
|
|
213
|
+
};
|
|
214
|
+
const creatorRoute = (call) => {
|
|
215
|
+
const hasPathParams = call.args?.members.has('queryParams') ?? false;
|
|
216
|
+
return hasPathParams && call.route.pathParams.length === 0 && !call.route.text.includes('=>')
|
|
217
|
+
? `() => ${call.route.text}`
|
|
218
|
+
: call.route.text;
|
|
219
|
+
};
|
|
220
|
+
const decideReuse = (tree, graph, feature, call, existing) => {
|
|
221
|
+
if (call.route.normalized === null)
|
|
222
|
+
return null;
|
|
223
|
+
const sameRoute = existing.filter((creator) => creator.verb === call.verb && creator.normalizedRoute === call.route.normalized);
|
|
224
|
+
if (sameRoute.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
const response = call.responseType ? normalizeType(call.responseType) : null;
|
|
227
|
+
const argTexts = toolkitArgTexts(call);
|
|
228
|
+
const argMismatch = (creator) => COMPARED_ARG_KEYS.find((key) => creator.args?.[key] !== argTexts[key]) ?? null;
|
|
229
|
+
const argsFit = (creator) => argMismatch(creator) === null ||
|
|
230
|
+
toolkitArgsFitCreator(tree, graph, { file: feature.files.actions, typeText: call.args?.typeText ?? null }, { file: creator.filePath, typeText: creator.argsTypeText });
|
|
231
|
+
const match = sameRoute.find((creator) => creator.responseType === response &&
|
|
232
|
+
creator.pathParams.join(',') === call.route.pathParams.join(',') &&
|
|
233
|
+
argsFit(creator));
|
|
234
|
+
if (!match) {
|
|
235
|
+
const other = sameRoute[0];
|
|
236
|
+
const differingKey = argMismatch(other);
|
|
237
|
+
const describe = (text) => {
|
|
238
|
+
if (text === null)
|
|
239
|
+
return 'none';
|
|
240
|
+
const type = text.replace(/^\?/, '');
|
|
241
|
+
return `\`${type.length > 120 ? `${type.slice(0, 120)}…` : type}\``;
|
|
242
|
+
};
|
|
243
|
+
const reason = other.pathParams.join(',') !== call.route.pathParams.join(',')
|
|
244
|
+
? 'its path params are named differently'
|
|
245
|
+
: other.responseType === null
|
|
246
|
+
? 'its response type is not written inline, so it cannot be compared'
|
|
247
|
+
: other.responseType !== response
|
|
248
|
+
? `its response is \`${other.responseType}\`, not \`${response ?? 'unknown'}\``
|
|
249
|
+
: other.args === null
|
|
250
|
+
? 'its args type is not an interface or type literal, so it cannot be compared'
|
|
251
|
+
: `its ${differingKey === 'body' ? 'body' : 'query params'} are ${describe(other.args[differingKey])}, not ${describe(argTexts[differingKey])}`;
|
|
252
|
+
return {
|
|
253
|
+
mismatch: `\`${call.name}\` got its own creator: \`${other.name}\` (${other.filePath}:${other.line}) has the same ${call.verb.toUpperCase()} route, but ${reason}.`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const specifier = importSpecifierFor(tree, graph, feature.queriesFile, match.filePath, match.name);
|
|
257
|
+
if (!specifier) {
|
|
258
|
+
return {
|
|
259
|
+
mismatch: `\`${call.name}\` got its own creator: \`${match.name}\` (${match.filePath}:${match.line}) matches, but no path alias exports it to ${feature.queriesFile}.`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return { reuse: match, specifier };
|
|
263
|
+
};
|
|
264
|
+
const clientSpecifier = (tree, helpers, fromFile) => tree.exists(helpers.importFrom) ? relativeSpecifier(fromFile, helpers.importFrom) : helpers.importFrom;
|
|
265
|
+
/** Writes `x.queries.ts`: one v3 creator per action group, or a re-export of an existing creator for the same route. */
|
|
266
|
+
export const writeQueriesFile = (tree, graph, feature, helpers, existing, report) => {
|
|
267
|
+
const helperImports = new Set();
|
|
268
|
+
const reExports = [];
|
|
269
|
+
const declarations = [];
|
|
270
|
+
let reused = 0;
|
|
271
|
+
for (const call of feature.calls) {
|
|
272
|
+
const decision = decideReuse(tree, graph, feature, call, existing);
|
|
273
|
+
if (decision && 'reuse' in decision) {
|
|
274
|
+
const { reuse, specifier } = decision;
|
|
275
|
+
const binding = reuse.name === call.name ? call.name : `${reuse.name} as ${call.name}`;
|
|
276
|
+
reExports.push(`export { ${binding} } from '${specifier}';`);
|
|
277
|
+
reused += 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (decision && 'mismatch' in decision) {
|
|
281
|
+
report.add({
|
|
282
|
+
id: TOOLKIT_TASK.DUPLICATE_ROUTE_MISMATCH,
|
|
283
|
+
summary: decision.mismatch,
|
|
284
|
+
locations: [{ filePath: feature.queriesFile }],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const helper = helperName(helpers, call.verb, !isPublicRoute(helpers, call.route.staticPrefix));
|
|
288
|
+
helperImports.add(helper);
|
|
289
|
+
declarations.push(`export const ${call.name} = ${helper}${creatorTypeParameter(call)}(${creatorRoute(call)});`);
|
|
290
|
+
}
|
|
291
|
+
const imports = [...feature.actionImports];
|
|
292
|
+
if (helperImports.size > 0) {
|
|
293
|
+
imports.push(`import { ${[...helperImports].sort().join(', ')} } from '${clientSpecifier(tree, helpers, feature.queriesFile)}';`);
|
|
294
|
+
}
|
|
295
|
+
const content = [imports.join('\n'), reExports.join('\n'), declarations.join('\n\n')]
|
|
296
|
+
.filter((part) => part.length > 0)
|
|
297
|
+
.join('\n\n');
|
|
298
|
+
tree.write(feature.queriesFile, `${pruneUnusedImports(`${content}\n`)}`);
|
|
299
|
+
return { written: declarations.length, reused };
|
|
300
|
+
};
|
|
301
|
+
//# sourceMappingURL=creators.js.map
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { createSourceFile, ensureNamedImports } from '../migrate-to-query-v3/shared.js';
|
|
3
|
+
import { TOOLKIT_MODULE } from './feature.js';
|
|
4
|
+
import { TOOLKIT_TASK } from './report.js';
|
|
5
|
+
import { applyEdits, collapseBlankLines, lineOf, pruneUnusedImports, relativeSpecifier, removeStatement, } from './ts-edits.js';
|
|
6
|
+
export const INTEROP_MODULE = '@ethlete/query/ngrx-toolkit';
|
|
7
|
+
const STORE_TYPES = new Set(['Store', 'Actions']);
|
|
8
|
+
const FACADE_BASE_MEMBERS = new Set([
|
|
9
|
+
'call',
|
|
10
|
+
'select',
|
|
11
|
+
'selectIsInit',
|
|
12
|
+
'selectIsLoading',
|
|
13
|
+
'selectIsSuccess',
|
|
14
|
+
'selectIsError',
|
|
15
|
+
'selectResponse',
|
|
16
|
+
'selectCachedResponse',
|
|
17
|
+
'selectError',
|
|
18
|
+
'selectArgs',
|
|
19
|
+
'selectEntityId',
|
|
20
|
+
'selectTimestamp',
|
|
21
|
+
'selectType',
|
|
22
|
+
'selectCallState',
|
|
23
|
+
'remove',
|
|
24
|
+
'on',
|
|
25
|
+
'once',
|
|
26
|
+
]);
|
|
27
|
+
const importTable = (sourceFile) => {
|
|
28
|
+
const table = new Map();
|
|
29
|
+
for (const statement of sourceFile.statements) {
|
|
30
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
31
|
+
continue;
|
|
32
|
+
const specifier = statement.moduleSpecifier.text;
|
|
33
|
+
const clause = statement.importClause;
|
|
34
|
+
if (clause?.name)
|
|
35
|
+
table.set(clause.name.text, { specifier, imported: 'default' });
|
|
36
|
+
const bindings = clause?.namedBindings;
|
|
37
|
+
if (bindings && ts.isNamespaceImport(bindings))
|
|
38
|
+
table.set(bindings.name.text, { specifier, imported: null });
|
|
39
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
40
|
+
for (const element of bindings.elements) {
|
|
41
|
+
table.set(element.name.text, { specifier, imported: element.propertyName?.text ?? element.name.text });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return table;
|
|
46
|
+
};
|
|
47
|
+
const typeNameOf = (type) => type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) ? type.typeName.text : null;
|
|
48
|
+
const isInjectOfStore = (expression, storeTypeNames) => !!expression &&
|
|
49
|
+
ts.isCallExpression(expression) &&
|
|
50
|
+
ts.isIdentifier(expression.expression) &&
|
|
51
|
+
expression.expression.text === 'inject' &&
|
|
52
|
+
!!expression.arguments[0] &&
|
|
53
|
+
ts.isIdentifier(expression.arguments[0]) &&
|
|
54
|
+
storeTypeNames.has(expression.arguments[0].text);
|
|
55
|
+
/**
|
|
56
|
+
* Rewrites a `FacadeBase` facade to call the v3 creators through `toolkitCall`. Returns blockers instead of content
|
|
57
|
+
* when a member still needs the NgRx store.
|
|
58
|
+
*/
|
|
59
|
+
export const rewriteFacade = (content, feature, graph, storeFiles) => {
|
|
60
|
+
const facadeFile = feature.files.facade;
|
|
61
|
+
const sourceFile = createSourceFile(content, facadeFile);
|
|
62
|
+
const imports = importTable(sourceFile);
|
|
63
|
+
const callsByName = new Map(feature.calls.map((call) => [call.name, call]));
|
|
64
|
+
const storeTypeNames = new Set();
|
|
65
|
+
const storeImportNames = new Set();
|
|
66
|
+
for (const [local, { specifier, imported }] of imports) {
|
|
67
|
+
if ((specifier === '@ngrx/store' || specifier === '@ngrx/effects') && imported && STORE_TYPES.has(imported)) {
|
|
68
|
+
storeTypeNames.add(local);
|
|
69
|
+
}
|
|
70
|
+
if (specifier === TOOLKIT_MODULE || specifier.startsWith('@ngrx/'))
|
|
71
|
+
storeImportNames.add(local);
|
|
72
|
+
const resolved = graph.resolveFile(facadeFile, specifier);
|
|
73
|
+
if (resolved && storeFiles.has(resolved))
|
|
74
|
+
storeImportNames.add(local);
|
|
75
|
+
}
|
|
76
|
+
const facadeClass = sourceFile.statements.find((statement) => ts.isClassDeclaration(statement) &&
|
|
77
|
+
!!statement.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText() === 'FacadeBase')));
|
|
78
|
+
if (!facadeClass) {
|
|
79
|
+
return {
|
|
80
|
+
blockers: [
|
|
81
|
+
{
|
|
82
|
+
id: TOOLKIT_TASK.FACADE_STORE_MEMBER,
|
|
83
|
+
summary: `${facadeFile} has no class that extends \`FacadeBase\`.`,
|
|
84
|
+
locations: [{ filePath: facadeFile }],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const storeMembers = new Set();
|
|
90
|
+
for (const member of facadeClass.members) {
|
|
91
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
92
|
+
for (const parameter of member.parameters) {
|
|
93
|
+
if (ts.isIdentifier(parameter.name) && storeTypeNames.has(typeNameOf(parameter.type) ?? '')) {
|
|
94
|
+
storeMembers.add(parameter.name.text);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (ts.isPropertyDeclaration(member) &&
|
|
99
|
+
ts.isIdentifier(member.name) &&
|
|
100
|
+
(storeTypeNames.has(typeNameOf(member.type) ?? '') || isInjectOfStore(member.initializer, storeTypeNames))) {
|
|
101
|
+
storeMembers.add(member.name.text);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const blockers = [];
|
|
105
|
+
const edits = [];
|
|
106
|
+
const usedCreators = new Set();
|
|
107
|
+
let usesActionCallArgs = false;
|
|
108
|
+
const actionNameOf = (expression) => {
|
|
109
|
+
if (ts.isIdentifier(expression)) {
|
|
110
|
+
const imported = imports.get(expression.text);
|
|
111
|
+
const resolved = imported ? graph.resolveFile(facadeFile, imported.specifier) : null;
|
|
112
|
+
return resolved === feature.files.actions ? (imported?.imported ?? null) : null;
|
|
113
|
+
}
|
|
114
|
+
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
115
|
+
const imported = imports.get(expression.expression.text);
|
|
116
|
+
const resolved = imported && imported.imported === null ? graph.resolveFile(facadeFile, imported.specifier) : null;
|
|
117
|
+
return resolved === feature.files.actions ? expression.name.text : null;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
const simpleCall = (member) => {
|
|
122
|
+
if (!ts.isMethodDeclaration(member) || member.body?.statements.length !== 1)
|
|
123
|
+
return null;
|
|
124
|
+
const statement = member.body.statements[0];
|
|
125
|
+
const expression = ts.isReturnStatement(statement) ? statement.expression : undefined;
|
|
126
|
+
if (!expression ||
|
|
127
|
+
!ts.isCallExpression(expression) ||
|
|
128
|
+
!ts.isPropertyAccessExpression(expression.expression) ||
|
|
129
|
+
expression.expression.expression.kind !== ts.SyntaxKind.ThisKeyword ||
|
|
130
|
+
expression.expression.name.text !== 'call' ||
|
|
131
|
+
expression.arguments.length !== 2) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const name = actionNameOf(expression.arguments[0]);
|
|
135
|
+
const call = name ? callsByName.get(name) : undefined;
|
|
136
|
+
return call ? { call, argument: expression.arguments[1] } : null;
|
|
137
|
+
};
|
|
138
|
+
const storeDependency = (member) => {
|
|
139
|
+
let found = null;
|
|
140
|
+
const visit = (node) => {
|
|
141
|
+
if (found)
|
|
142
|
+
return;
|
|
143
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
144
|
+
(node.expression.kind === ts.SyntaxKind.ThisKeyword || node.expression.kind === ts.SyntaxKind.SuperKeyword) &&
|
|
145
|
+
(storeMembers.has(node.name.text) || FACADE_BASE_MEMBERS.has(node.name.text))) {
|
|
146
|
+
found = node.getText(sourceFile);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (ts.isIdentifier(node) && storeImportNames.has(node.text) && !ts.isPropertyAccessExpression(node.parent)) {
|
|
150
|
+
found = node.text;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (ts.isIdentifier(node) &&
|
|
154
|
+
storeImportNames.has(node.text) &&
|
|
155
|
+
ts.isPropertyAccessExpression(node.parent) &&
|
|
156
|
+
node.parent.expression === node) {
|
|
157
|
+
found = node.text;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
ts.forEachChild(node, visit);
|
|
161
|
+
};
|
|
162
|
+
visit(member);
|
|
163
|
+
return found;
|
|
164
|
+
};
|
|
165
|
+
const injectorName = facadeClass.members.some((member) => member.name?.getText(sourceFile) === 'injector')
|
|
166
|
+
? 'toolkitInjector'
|
|
167
|
+
: 'injector';
|
|
168
|
+
for (const member of facadeClass.members) {
|
|
169
|
+
const call = simpleCall(member);
|
|
170
|
+
if (call && ts.isMethodDeclaration(member)) {
|
|
171
|
+
usedCreators.add(call.call.name);
|
|
172
|
+
const parameter = member.parameters[0];
|
|
173
|
+
const parameterType = parameter?.type?.getText(sourceFile).replace(/\s+/g, ' ');
|
|
174
|
+
const args = call.call.args;
|
|
175
|
+
const toolkitArgsType = /^ActionCallArgs<typeof ([\w.]+)>$/.exec(parameterType ?? '');
|
|
176
|
+
const replaceType = !!parameter?.type &&
|
|
177
|
+
((args && parameterType === args.typeText && args.extraKeys.length === 0) ||
|
|
178
|
+
(!!toolkitArgsType && toolkitArgsType[1].split('.').pop() === call.call.name));
|
|
179
|
+
if (replaceType) {
|
|
180
|
+
usesActionCallArgs = true;
|
|
181
|
+
edits.push({
|
|
182
|
+
start: parameter.type.getStart(sourceFile),
|
|
183
|
+
end: parameter.type.getEnd(),
|
|
184
|
+
text: `ActionCallArgs<typeof ${call.call.name}>`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
edits.push({
|
|
188
|
+
start: member.body.getStart(sourceFile),
|
|
189
|
+
end: member.body.getEnd(),
|
|
190
|
+
text: `{\n return toolkitCall(${call.call.name}, ${call.argument.getText(sourceFile)}, { injector: this.${injectorName} });\n }`,
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && storeMembers.has(member.name.text)) {
|
|
195
|
+
edits.push(removeStatement(member, content));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
199
|
+
const body = member.body?.statements ?? ts.factory.createNodeArray();
|
|
200
|
+
const storeLocals = new Set();
|
|
201
|
+
const keptStatements = body.filter((statement) => {
|
|
202
|
+
if (ts.isExpressionStatement(statement) &&
|
|
203
|
+
ts.isCallExpression(statement.expression) &&
|
|
204
|
+
statement.expression.expression.kind === ts.SyntaxKind.SuperKeyword) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (ts.isVariableStatement(statement)) {
|
|
208
|
+
const declarations = statement.declarationList.declarations;
|
|
209
|
+
if (declarations.every((declaration) => isInjectOfStore(declaration.initializer, storeTypeNames))) {
|
|
210
|
+
declarations.forEach((declaration) => storeLocals.add(declaration.name.getText(sourceFile)));
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (ts.isExpressionStatement(statement) &&
|
|
215
|
+
ts.isBinaryExpression(statement.expression) &&
|
|
216
|
+
ts.isPropertyAccessExpression(statement.expression.left) &&
|
|
217
|
+
storeMembers.has(statement.expression.left.name.text)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
const keptParameters = member.parameters.filter((parameter) => !(ts.isIdentifier(parameter.name) && storeMembers.has(parameter.name.text)));
|
|
223
|
+
for (const statement of keptStatements) {
|
|
224
|
+
const dependency = storeDependency(statement);
|
|
225
|
+
if (dependency ||
|
|
226
|
+
[...storeLocals].some((local) => new RegExp(`\\b${local}\\b`).test(statement.getText(sourceFile)))) {
|
|
227
|
+
blockers.push({
|
|
228
|
+
id: TOOLKIT_TASK.FACADE_STORE_MEMBER,
|
|
229
|
+
summary: `The constructor of the facade uses the store (\`${dependency ?? statement.getText(sourceFile)}\`).`,
|
|
230
|
+
locations: [{ filePath: facadeFile, line: lineOf(sourceFile, statement.getStart(sourceFile)) }],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (keptStatements.length === 0 && keptParameters.length === 0) {
|
|
235
|
+
edits.push(removeStatement(member, content));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
edits.push({
|
|
239
|
+
start: member.getStart(sourceFile),
|
|
240
|
+
end: member.getEnd(),
|
|
241
|
+
text: `constructor(${keptParameters.map((parameter) => parameter.getText(sourceFile)).join(', ')}) {\n${keptStatements
|
|
242
|
+
.map((statement) => ` ${statement.getText(sourceFile)}`)
|
|
243
|
+
.join('\n')}\n }`,
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const dependency = storeDependency(member);
|
|
248
|
+
if (dependency) {
|
|
249
|
+
blockers.push({
|
|
250
|
+
id: TOOLKIT_TASK.FACADE_STORE_MEMBER,
|
|
251
|
+
summary: `\`${member.name?.getText(sourceFile) ?? 'A member'}\` uses \`${dependency}\`.`,
|
|
252
|
+
locations: [{ filePath: facadeFile, line: lineOf(sourceFile, member.getStart(sourceFile)) }],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (blockers.length > 0)
|
|
257
|
+
return { blockers };
|
|
258
|
+
const heritage = facadeClass.heritageClauses.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
259
|
+
edits.push({ start: heritage.getFullStart(), end: heritage.getEnd(), text: '' });
|
|
260
|
+
const bodyStart = facadeClass.members.pos;
|
|
261
|
+
edits.push({
|
|
262
|
+
start: bodyStart,
|
|
263
|
+
end: bodyStart,
|
|
264
|
+
text: `\n private ${injectorName} = inject(Injector);\n`,
|
|
265
|
+
});
|
|
266
|
+
for (const statement of sourceFile.statements) {
|
|
267
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
268
|
+
continue;
|
|
269
|
+
const specifier = statement.moduleSpecifier.text;
|
|
270
|
+
const resolved = graph.resolveFile(facadeFile, specifier);
|
|
271
|
+
if (specifier === TOOLKIT_MODULE || (resolved && storeFiles.has(resolved))) {
|
|
272
|
+
edits.push(removeStatement(statement, content));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
let result = applyEdits(content, edits);
|
|
276
|
+
result = ensureNamedImports({
|
|
277
|
+
content: result,
|
|
278
|
+
importsNeeded: ['inject', 'Injector'],
|
|
279
|
+
moduleSpecifier: '@angular/core',
|
|
280
|
+
});
|
|
281
|
+
result = ensureNamedImports({
|
|
282
|
+
content: result,
|
|
283
|
+
importsNeeded: usesActionCallArgs ? ['ActionCallArgs', 'toolkitCall'] : ['toolkitCall'],
|
|
284
|
+
moduleSpecifier: INTEROP_MODULE,
|
|
285
|
+
});
|
|
286
|
+
if (usedCreators.size > 0) {
|
|
287
|
+
result = ensureNamedImports({
|
|
288
|
+
content: result,
|
|
289
|
+
importsNeeded: [...usedCreators],
|
|
290
|
+
moduleSpecifier: relativeSpecifier(facadeFile, feature.queriesFile),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return { content: collapseBlankLines(pruneUnusedImports(result)) };
|
|
294
|
+
};
|
|
295
|
+
//# sourceMappingURL=facade.js.map
|