@ethlete/query 6.0.0-next.50 → 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 +41 -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 +359 -139
- 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
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"factory": "./migrate-query-opt-in-features/migration",
|
|
25
25
|
"schema": "./migrate-query-opt-in-features/schema.json",
|
|
26
26
|
"description": "Migrate query clients to the opt-in error pipeline and bearer auth providers to the opt-in multi-tab sync feature"
|
|
27
|
+
},
|
|
28
|
+
"migrate-from-ngrx-toolkit": {
|
|
29
|
+
"factory": "./migrate-from-ngrx-toolkit/migration",
|
|
30
|
+
"schema": "./migrate-from-ngrx-toolkit/schema.json",
|
|
31
|
+
"description": "Move @tomtomb/ngrx-toolkit features onto v3 query creators and the @ethlete/query/ngrx-toolkit interop"
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
const TOOLKIT_PROBE = '__EtToolkitArgs';
|
|
4
|
+
const CREATOR_PROBE = '__EtCreatorArgs';
|
|
5
|
+
const PROBE_FILE = '/__et_args_probe__.ts';
|
|
6
|
+
const MAX_DEPTH = 12;
|
|
7
|
+
const PROBE_SOURCE = `
|
|
8
|
+
import type { ${TOOLKIT_PROBE} as ToolkitArgs } from '__toolkit__';
|
|
9
|
+
import type { ${CREATOR_PROBE} as CreatorArgs } from '__creator__';
|
|
10
|
+
|
|
11
|
+
type OptionalNullAsUndefined<T> = {
|
|
12
|
+
[K in keyof T]: undefined extends T[K] ? OptionalNullAsUndefined<Exclude<T[K], null>> : OptionalNullAsUndefined<T[K]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ToolkitSide = OptionalNullAsUndefined<{
|
|
16
|
+
[K in keyof ToolkitArgs as K extends 'params' ? 'queryParams' : K extends 'body' ? K : never]: ToolkitArgs[K];
|
|
17
|
+
}>;
|
|
18
|
+
export type CreatorSide = OptionalNullAsUndefined<{
|
|
19
|
+
[K in keyof CreatorArgs as K extends 'queryParams' | 'body' ? K : never]: CreatorArgs[K];
|
|
20
|
+
}>;
|
|
21
|
+
`;
|
|
22
|
+
const COMPILER_OPTIONS = {
|
|
23
|
+
strict: true,
|
|
24
|
+
noEmit: true,
|
|
25
|
+
skipLibCheck: true,
|
|
26
|
+
target: ts.ScriptTarget.ES2022,
|
|
27
|
+
module: ts.ModuleKind.ESNext,
|
|
28
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
29
|
+
lib: ['lib.es2022.d.ts'],
|
|
30
|
+
types: [],
|
|
31
|
+
};
|
|
32
|
+
const toTreePath = (fileName) => fileName.replace(/^\//, '');
|
|
33
|
+
const containsAny = (checker, type, seen, depth) => {
|
|
34
|
+
if (type.flags & ts.TypeFlags.Any)
|
|
35
|
+
return true;
|
|
36
|
+
if (seen.has(type) || depth > MAX_DEPTH)
|
|
37
|
+
return false;
|
|
38
|
+
seen.add(type);
|
|
39
|
+
if (type.isUnionOrIntersection())
|
|
40
|
+
return type.types.some((part) => containsAny(checker, part, seen, depth + 1));
|
|
41
|
+
if (!(type.flags & ts.TypeFlags.Object))
|
|
42
|
+
return false;
|
|
43
|
+
const typeArguments = type.objectFlags & ts.ObjectFlags.Reference
|
|
44
|
+
? checker.getTypeArguments(type)
|
|
45
|
+
: [];
|
|
46
|
+
return (typeArguments.some((argument) => containsAny(checker, argument, seen, depth + 1)) ||
|
|
47
|
+
checker
|
|
48
|
+
.getPropertiesOfType(type)
|
|
49
|
+
.some((property) => containsAny(checker, checker.getTypeOfSymbol(property), seen, depth + 1)));
|
|
50
|
+
};
|
|
51
|
+
const isRecord = (checker, type) => (type.isIntersection() ? type.types : [type]).every((part) => !!(part.flags & ts.TypeFlags.Object)) &&
|
|
52
|
+
!checker.isArrayType(type) &&
|
|
53
|
+
!checker.isTupleType(type) &&
|
|
54
|
+
checker.getSignaturesOfType(type, ts.SignatureKind.Call).length === 0;
|
|
55
|
+
const hasEveryKey = (checker, source, target, seen, depth) => {
|
|
56
|
+
const from = checker.getNonNullableType(source);
|
|
57
|
+
const to = checker.getNonNullableType(target);
|
|
58
|
+
if (seen.has(from) || depth > MAX_DEPTH || !isRecord(checker, from) || !isRecord(checker, to))
|
|
59
|
+
return true;
|
|
60
|
+
seen.add(from);
|
|
61
|
+
return checker.getPropertiesOfType(from).every((property) => {
|
|
62
|
+
const counterpart = checker.getPropertyOfType(to, property.name);
|
|
63
|
+
return (!!counterpart &&
|
|
64
|
+
hasEveryKey(checker, checker.getTypeOfSymbol(property), checker.getTypeOfSymbol(counterpart), seen, depth + 1));
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
export const toolkitArgsFitCreator = (tree, graph, toolkit, creator) => {
|
|
68
|
+
if (toolkit.file === creator.file)
|
|
69
|
+
return false;
|
|
70
|
+
const appended = new Map([
|
|
71
|
+
[toolkit.file, `\nexport type ${TOOLKIT_PROBE} = ${toolkit.typeText ?? '{}'};\n`],
|
|
72
|
+
[creator.file, `\nexport type ${CREATOR_PROBE} = ${creator.typeText ?? '{}'};\n`],
|
|
73
|
+
]);
|
|
74
|
+
const libDirectory = dirname(ts.getDefaultLibFilePath(COMPILER_OPTIONS));
|
|
75
|
+
const readFile = (fileName) => {
|
|
76
|
+
if (fileName === PROBE_FILE)
|
|
77
|
+
return PROBE_SOURCE;
|
|
78
|
+
if (fileName.startsWith(libDirectory))
|
|
79
|
+
return ts.sys.readFile(fileName);
|
|
80
|
+
const path = toTreePath(fileName);
|
|
81
|
+
const content = tree.read(path, 'utf-8');
|
|
82
|
+
return content === null ? undefined : content + (appended.get(path) ?? '');
|
|
83
|
+
};
|
|
84
|
+
const fileExists = (fileName) => fileName === PROBE_FILE ||
|
|
85
|
+
(fileName.startsWith(libDirectory) ? ts.sys.fileExists(fileName) : tree.isFile(toTreePath(fileName)));
|
|
86
|
+
const resolutionHost = {
|
|
87
|
+
fileExists,
|
|
88
|
+
readFile,
|
|
89
|
+
directoryExists: (name) => tree.exists(toTreePath(name)) && !tree.isFile(toTreePath(name)),
|
|
90
|
+
getCurrentDirectory: () => '/',
|
|
91
|
+
};
|
|
92
|
+
const resolveOne = (name, containingFile) => {
|
|
93
|
+
const workspaceFile = containingFile === PROBE_FILE
|
|
94
|
+
? name === '__toolkit__'
|
|
95
|
+
? toolkit.file
|
|
96
|
+
: name === '__creator__'
|
|
97
|
+
? creator.file
|
|
98
|
+
: null
|
|
99
|
+
: graph.resolveFile(toTreePath(containingFile), name);
|
|
100
|
+
if (workspaceFile)
|
|
101
|
+
return { resolvedFileName: `/${workspaceFile}`, extension: ts.Extension.Ts };
|
|
102
|
+
return ts.resolveModuleName(name, containingFile, COMPILER_OPTIONS, resolutionHost).resolvedModule;
|
|
103
|
+
};
|
|
104
|
+
const host = {
|
|
105
|
+
...resolutionHost,
|
|
106
|
+
getSourceFile: (fileName, languageVersion) => {
|
|
107
|
+
const text = readFile(fileName);
|
|
108
|
+
return text === undefined ? undefined : ts.createSourceFile(fileName, text, languageVersion, true);
|
|
109
|
+
},
|
|
110
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
111
|
+
writeFile: () => undefined,
|
|
112
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
113
|
+
useCaseSensitiveFileNames: () => true,
|
|
114
|
+
getNewLine: () => '\n',
|
|
115
|
+
resolveModuleNameLiterals: (literals, containingFile) => literals.map((literal) => ({ resolvedModule: resolveOne(literal.text, containingFile) })),
|
|
116
|
+
};
|
|
117
|
+
const program = ts.createProgram({ rootNames: [PROBE_FILE], options: COMPILER_OPTIONS, host });
|
|
118
|
+
const checker = program.getTypeChecker();
|
|
119
|
+
const probe = program.getSourceFile(PROBE_FILE);
|
|
120
|
+
const exportsOf = probe ? checker.getSymbolAtLocation(probe) : undefined;
|
|
121
|
+
const typeOf = (name) => {
|
|
122
|
+
const symbol = exportsOf ? checker.getExportsOfModule(exportsOf).find((each) => each.name === name) : undefined;
|
|
123
|
+
return symbol ? checker.getDeclaredTypeOfSymbol(symbol) : null;
|
|
124
|
+
};
|
|
125
|
+
const toolkitSide = typeOf('ToolkitSide');
|
|
126
|
+
const creatorSide = typeOf('CreatorSide');
|
|
127
|
+
if (!toolkitSide || !creatorSide)
|
|
128
|
+
return false;
|
|
129
|
+
if (containsAny(checker, toolkitSide, new Set(), 0) || containsAny(checker, creatorSide, new Set(), 0))
|
|
130
|
+
return false;
|
|
131
|
+
const keys = (type) => checker
|
|
132
|
+
.getPropertiesOfType(type)
|
|
133
|
+
.map((property) => property.name)
|
|
134
|
+
.sort()
|
|
135
|
+
.join(',');
|
|
136
|
+
return (keys(toolkitSide) === keys(creatorSide) &&
|
|
137
|
+
checker.isTypeAssignableTo(toolkitSide, creatorSide) &&
|
|
138
|
+
hasEveryKey(checker, toolkitSide, creatorSide, new Set(), 0));
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=args-assignability.js.map
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { createSourceFile, ensureNamedImports } from '../migrate-to-query-v3/shared.js';
|
|
3
|
+
import { INTEROP_MODULE } from './facade.js';
|
|
4
|
+
import { TOOLKIT_MODULE } from './feature.js';
|
|
5
|
+
import { applyEdits, collectUsedIdentifiers, lineOf, removeStatement } from './ts-edits.js';
|
|
6
|
+
export const CONSUMER_TASK = {
|
|
7
|
+
UNSUPPORTED_IMPORT: 'NTK-UNSUPPORTED-IMPORT',
|
|
8
|
+
UNCONVERTED_ACTION_GROUP: 'NTK-UNCONVERTED-ACTION-GROUP',
|
|
9
|
+
SELECT_BY_ACTION_ID: 'NTK-SELECT-BY-ACTION-ID',
|
|
10
|
+
AUTH_INTERCEPTOR: 'NTK-AUTH-INTERCEPTOR',
|
|
11
|
+
};
|
|
12
|
+
const defineConsumerTasks = (report) => {
|
|
13
|
+
report.define(CONSUMER_TASK.UNSUPPORTED_IMPORT, {
|
|
14
|
+
title: 'Toolkit symbol without an interop counterpart',
|
|
15
|
+
action: '`@ethlete/query/ngrx-toolkit` does not export the listed symbols, so the file still imports them from `@tomtomb/ngrx-toolkit`. Rewrite the code that uses them, then drop the import.',
|
|
16
|
+
});
|
|
17
|
+
report.define(CONSUMER_TASK.UNCONVERTED_ACTION_GROUP, {
|
|
18
|
+
title: 'Consumer types name an action group that is still on the toolkit',
|
|
19
|
+
action: 'The file was left on `@tomtomb/ngrx-toolkit` because a `MappedEntityState` / `ActionCallArgs` type names an action group whose feature was not converted. Resolve that feature’s task, then re-run the generator.',
|
|
20
|
+
});
|
|
21
|
+
report.define(CONSUMER_TASK.SELECT_BY_ACTION_ID, {
|
|
22
|
+
title: 'Handle looked up by action id',
|
|
23
|
+
action: 'The interop has no action ids. Pass the args instead and look the handle up with `toolkitSelect(creator, args, { injector })` - equal args return the handle `toolkitCall` created.',
|
|
24
|
+
});
|
|
25
|
+
report.define(CONSUMER_TASK.AUTH_INTERCEPTOR, {
|
|
26
|
+
title: 'HTTP interceptor attaches the bearer token',
|
|
27
|
+
action: 'v3 secure creators already send the token, so this interceptor sends it twice, and it adds it to every other `HttpClient` call too - third-party hosts included. Delete it and its registration. Requests it skipped must use public creators: pass their prefixes as `--publicRoutes` and re-run.',
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const INTEROP_SYMBOLS = new Set([
|
|
31
|
+
'toolkitCall',
|
|
32
|
+
'toolkitSelect',
|
|
33
|
+
'MappedEntityState',
|
|
34
|
+
'ActionCallArgs',
|
|
35
|
+
'ToolkitError',
|
|
36
|
+
'CallState',
|
|
37
|
+
'SuspensePipe',
|
|
38
|
+
'SuspenseMultiPipe',
|
|
39
|
+
'NgRxToolkitModule',
|
|
40
|
+
'joinLoading',
|
|
41
|
+
'joinErrors',
|
|
42
|
+
]);
|
|
43
|
+
const RENAMED_SYMBOLS = new Map([['Error', 'ToolkitError']]);
|
|
44
|
+
const STORE_SIDE_SYMBOLS = new Set([
|
|
45
|
+
'FacadeBase',
|
|
46
|
+
'EffectBase',
|
|
47
|
+
'ServiceBase',
|
|
48
|
+
'createHttpActionGroup',
|
|
49
|
+
'createReducerSlice',
|
|
50
|
+
'createEntitySelectors',
|
|
51
|
+
'defineArgTypes',
|
|
52
|
+
'defineResponseType',
|
|
53
|
+
]);
|
|
54
|
+
const HANDLE_TYPES = new Set(['MappedEntityState', 'ActionCallArgs']);
|
|
55
|
+
const code = (text) => (text.includes('`') ? `\`\` ${text} \`\`` : `\`${text}\``);
|
|
56
|
+
const toolkitImportsOf = (sourceFile) => sourceFile.statements.filter((statement) => ts.isImportDeclaration(statement) &&
|
|
57
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
58
|
+
statement.moduleSpecifier.text === TOOLKIT_MODULE);
|
|
59
|
+
const elementsOf = (declaration) => {
|
|
60
|
+
const bindings = declaration.importClause?.namedBindings;
|
|
61
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
62
|
+
return [];
|
|
63
|
+
return bindings.elements.map((element) => ({
|
|
64
|
+
imported: element.propertyName?.text ?? element.name.text,
|
|
65
|
+
local: element.name.text,
|
|
66
|
+
typeOnly: element.isTypeOnly || !!declaration.importClause?.isTypeOnly,
|
|
67
|
+
}));
|
|
68
|
+
};
|
|
69
|
+
const namedImportOf = (sourceFile, local) => {
|
|
70
|
+
for (const statement of sourceFile.statements) {
|
|
71
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
72
|
+
continue;
|
|
73
|
+
const bindings = statement.importClause?.namedBindings;
|
|
74
|
+
if (bindings && ts.isNamespaceImport(bindings) && bindings.name.text === local) {
|
|
75
|
+
return { statement, specifier: statement.moduleSpecifier.text, namespace: true, imported: local };
|
|
76
|
+
}
|
|
77
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
78
|
+
const element = bindings.elements.find((candidate) => candidate.name.text === local);
|
|
79
|
+
if (element) {
|
|
80
|
+
return {
|
|
81
|
+
statement,
|
|
82
|
+
specifier: statement.moduleSpecifier.text,
|
|
83
|
+
namespace: false,
|
|
84
|
+
imported: element.propertyName?.text ?? local,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
const isToolkitActionsFile = (tree, filePath) => !!filePath && filePath.endsWith('.actions.ts') && !!tree.read(filePath, 'utf-8')?.includes('createHttpActionGroup');
|
|
92
|
+
const unconvertedTypeQueries = (tree, graph, filePath, sourceFile, handleTypeNames) => {
|
|
93
|
+
const unconverted = [];
|
|
94
|
+
const resolveQuery = (query) => {
|
|
95
|
+
const name = query.exprName;
|
|
96
|
+
const local = ts.isIdentifier(name) ? name.text : ts.isIdentifier(name.left) ? name.left.text : null;
|
|
97
|
+
const origin = local ? namedImportOf(sourceFile, local) : null;
|
|
98
|
+
if (!origin || origin.namespace === ts.isIdentifier(name))
|
|
99
|
+
return;
|
|
100
|
+
const declaringFile = origin.namespace
|
|
101
|
+
? graph.resolveFile(filePath, origin.specifier)
|
|
102
|
+
: graph.findDeclaringFile(filePath, origin.specifier, origin.imported);
|
|
103
|
+
if (isToolkitActionsFile(tree, declaringFile))
|
|
104
|
+
unconverted.push(name.getText(sourceFile));
|
|
105
|
+
};
|
|
106
|
+
const visit = (node) => {
|
|
107
|
+
if (ts.isTypeReferenceNode(node) &&
|
|
108
|
+
ts.isIdentifier(node.typeName) &&
|
|
109
|
+
handleTypeNames.has(node.typeName.text) &&
|
|
110
|
+
node.typeArguments) {
|
|
111
|
+
const visitArgument = (inner) => {
|
|
112
|
+
if (ts.isTypeQueryNode(inner))
|
|
113
|
+
resolveQuery(inner);
|
|
114
|
+
ts.forEachChild(inner, visitArgument);
|
|
115
|
+
};
|
|
116
|
+
node.typeArguments.forEach(visitArgument);
|
|
117
|
+
}
|
|
118
|
+
ts.forEachChild(node, visit);
|
|
119
|
+
};
|
|
120
|
+
visit(sourceFile);
|
|
121
|
+
return unconverted;
|
|
122
|
+
};
|
|
123
|
+
const isCall = (node, name) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name;
|
|
124
|
+
const readActionId = (node, createActionId) => {
|
|
125
|
+
if (!node || !isCall(node, createActionId) || node.arguments.length !== 1)
|
|
126
|
+
return null;
|
|
127
|
+
const inner = node.arguments[0];
|
|
128
|
+
if (!ts.isCallExpression(inner) ||
|
|
129
|
+
!ts.isPropertyAccessExpression(inner.expression) ||
|
|
130
|
+
!ts.isIdentifier(inner.expression.expression) ||
|
|
131
|
+
inner.expression.name.text !== 'call' ||
|
|
132
|
+
inner.arguments.length !== 1) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const options = inner.arguments[0];
|
|
136
|
+
if (!ts.isObjectLiteralExpression(options) || options.properties.length !== 1)
|
|
137
|
+
return null;
|
|
138
|
+
const property = options.properties[0];
|
|
139
|
+
if (!ts.isPropertyAssignment(property) || property.name.getText() !== 'args')
|
|
140
|
+
return null;
|
|
141
|
+
return { group: inner.expression.expression.text, args: property.initializer };
|
|
142
|
+
};
|
|
143
|
+
const selectArgsText = (args, sourceFile) => {
|
|
144
|
+
if (!ts.isObjectLiteralExpression(args))
|
|
145
|
+
return args.getText(sourceFile);
|
|
146
|
+
const kept = args.properties.filter((property) => property.name?.getText(sourceFile) !== 'skipCache');
|
|
147
|
+
if (kept.length === args.properties.length)
|
|
148
|
+
return args.getText(sourceFile);
|
|
149
|
+
return `{ ${kept.map((property) => property.getText(sourceFile)).join(', ')} }`;
|
|
150
|
+
};
|
|
151
|
+
const isFacadeSelect = (node) => ts.isCallExpression(node) &&
|
|
152
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
153
|
+
node.expression.name.text === 'select' &&
|
|
154
|
+
node.arguments.length === 2 &&
|
|
155
|
+
ts.isIdentifier(node.arguments[0]) &&
|
|
156
|
+
/facade/i.test(node.expression.expression.getText()) &&
|
|
157
|
+
!/router/i.test(node.expression.expression.getText());
|
|
158
|
+
const countIdentifier = (root, name) => {
|
|
159
|
+
let count = 0;
|
|
160
|
+
const visit = (node) => {
|
|
161
|
+
if (ts.isIdentifier(node) && node.text === name)
|
|
162
|
+
count += 1;
|
|
163
|
+
ts.forEachChild(node, visit);
|
|
164
|
+
};
|
|
165
|
+
visit(root);
|
|
166
|
+
return count;
|
|
167
|
+
};
|
|
168
|
+
const injectorOf = (classNode) => {
|
|
169
|
+
for (const member of classNode.members) {
|
|
170
|
+
if (ts.isPropertyDeclaration(member) &&
|
|
171
|
+
ts.isIdentifier(member.name) &&
|
|
172
|
+
member.initializer &&
|
|
173
|
+
isCall(member.initializer, 'inject') &&
|
|
174
|
+
member.initializer.arguments[0]?.getText() === 'Injector') {
|
|
175
|
+
return member.name.text;
|
|
176
|
+
}
|
|
177
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
178
|
+
const parameter = member.parameters.find((candidate) => candidate.type?.getText() === 'Injector' &&
|
|
179
|
+
ts.isIdentifier(candidate.name) &&
|
|
180
|
+
!!ts
|
|
181
|
+
.getModifiers(candidate)
|
|
182
|
+
?.some((modifier) => [ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.PublicKeyword].includes(modifier.kind)));
|
|
183
|
+
if (parameter)
|
|
184
|
+
return parameter.name.text;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
const planRefreshSites = (filePath, sourceFile, content, createActionId) => {
|
|
190
|
+
const plan = { edits: [], rewritten: 0, needsInjectorImport: false, tasks: [] };
|
|
191
|
+
const consumedActionIds = new Set();
|
|
192
|
+
const classInjectors = new Map();
|
|
193
|
+
const task = (node, summary) => plan.tasks.push({
|
|
194
|
+
id: CONSUMER_TASK.SELECT_BY_ACTION_ID,
|
|
195
|
+
summary,
|
|
196
|
+
locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }],
|
|
197
|
+
});
|
|
198
|
+
const injectorFor = (node) => {
|
|
199
|
+
let current = node.parent;
|
|
200
|
+
while (current && !ts.isClassLike(current))
|
|
201
|
+
current = current.parent;
|
|
202
|
+
if (!current)
|
|
203
|
+
return null;
|
|
204
|
+
const classNode = current;
|
|
205
|
+
const known = classInjectors.get(classNode) ?? injectorOf(classNode);
|
|
206
|
+
if (known) {
|
|
207
|
+
classInjectors.set(classNode, known);
|
|
208
|
+
return known;
|
|
209
|
+
}
|
|
210
|
+
const taken = classNode.members.some((member) => member.name?.getText(sourceFile) === 'injector');
|
|
211
|
+
const name = taken ? 'toolkitInjector' : 'injector';
|
|
212
|
+
classInjectors.set(classNode, name);
|
|
213
|
+
plan.needsInjectorImport = true;
|
|
214
|
+
plan.edits.push({
|
|
215
|
+
start: classNode.members.pos,
|
|
216
|
+
end: classNode.members.pos,
|
|
217
|
+
text: `\n private ${name} = inject(Injector);\n`,
|
|
218
|
+
});
|
|
219
|
+
return name;
|
|
220
|
+
};
|
|
221
|
+
const localActionId = (select, identifier) => {
|
|
222
|
+
let block = select.parent;
|
|
223
|
+
while (block && !ts.isBlock(block))
|
|
224
|
+
block = block.parent;
|
|
225
|
+
if (!block || !ts.isBlock(block) || countIdentifier(block, identifier.text) !== 2)
|
|
226
|
+
return null;
|
|
227
|
+
for (const statement of block.statements) {
|
|
228
|
+
if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1)
|
|
229
|
+
continue;
|
|
230
|
+
const declaration = statement.declarationList.declarations[0];
|
|
231
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === identifier.text) {
|
|
232
|
+
const actionId = createActionId ? readActionId(declaration.initializer, createActionId) : null;
|
|
233
|
+
return actionId ? { ...actionId, statement, initializer: declaration.initializer } : null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
const visit = (node) => {
|
|
239
|
+
if (isFacadeSelect(node)) {
|
|
240
|
+
const group = node.arguments[0].text;
|
|
241
|
+
const idArgument = node.arguments[1];
|
|
242
|
+
const inline = createActionId ? readActionId(idArgument, createActionId) : null;
|
|
243
|
+
const local = !inline && ts.isIdentifier(idArgument) ? localActionId(node, idArgument) : null;
|
|
244
|
+
const source = inline ?? local;
|
|
245
|
+
if (source && source.group === group) {
|
|
246
|
+
const injector = injectorFor(node);
|
|
247
|
+
if (injector) {
|
|
248
|
+
plan.edits.push({
|
|
249
|
+
start: node.getStart(sourceFile),
|
|
250
|
+
end: node.getEnd(),
|
|
251
|
+
text: `toolkitSelect(${group}, ${selectArgsText(source.args, sourceFile)}, { injector: this.${injector} })`,
|
|
252
|
+
});
|
|
253
|
+
consumedActionIds.add(inline ? idArgument : local.initializer);
|
|
254
|
+
if (local)
|
|
255
|
+
plan.edits.push(removeStatement(local.statement, content));
|
|
256
|
+
plan.rewritten += 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
task(node, `${code(node.getText(sourceFile).replace(/\s+/g, ' '))} looks up a handle by an action id the generator cannot trace to its args.`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
ts.forEachChild(node, visit);
|
|
264
|
+
};
|
|
265
|
+
const visitActionIds = (node) => {
|
|
266
|
+
if (createActionId && isCall(node, createActionId) && !consumedActionIds.has(node)) {
|
|
267
|
+
task(node, `\`${createActionId}(…)\` builds an action id; pass the args on and call \`toolkitSelect\` where the handle is looked up.`);
|
|
268
|
+
}
|
|
269
|
+
ts.forEachChild(node, visitActionIds);
|
|
270
|
+
};
|
|
271
|
+
visit(sourceFile);
|
|
272
|
+
visitActionIds(sourceFile);
|
|
273
|
+
return plan;
|
|
274
|
+
};
|
|
275
|
+
/** Moves one consumer file from `@tomtomb/ngrx-toolkit` to `@ethlete/query/ngrx-toolkit`. */
|
|
276
|
+
export const rewriteConsumer = (tree, graph, filePath, content) => {
|
|
277
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
278
|
+
const toolkitImports = toolkitImportsOf(sourceFile);
|
|
279
|
+
if (toolkitImports.length === 0)
|
|
280
|
+
return null;
|
|
281
|
+
const elements = toolkitImports.flatMap(elementsOf);
|
|
282
|
+
const isInterop = (element) => INTEROP_SYMBOLS.has(RENAMED_SYMBOLS.get(element.imported) ?? element.imported);
|
|
283
|
+
const hasStoreSide = elements.some((element) => STORE_SIDE_SYMBOLS.has(element.imported));
|
|
284
|
+
if (hasStoreSide && !elements.some(isInterop))
|
|
285
|
+
return null;
|
|
286
|
+
const handleTypeNames = new Set(elements.filter((element) => HANDLE_TYPES.has(element.imported)).map((element) => element.local));
|
|
287
|
+
const unconverted = unconvertedTypeQueries(tree, graph, filePath, sourceFile, handleTypeNames);
|
|
288
|
+
if (unconverted.length > 0) {
|
|
289
|
+
if (hasStoreSide)
|
|
290
|
+
return null;
|
|
291
|
+
return {
|
|
292
|
+
content,
|
|
293
|
+
rewritten: false,
|
|
294
|
+
leftOnToolkit: true,
|
|
295
|
+
refreshSitesRewritten: 0,
|
|
296
|
+
tasks: [
|
|
297
|
+
{
|
|
298
|
+
id: CONSUMER_TASK.UNCONVERTED_ACTION_GROUP,
|
|
299
|
+
summary: `${[...new Set(unconverted)].map((name) => `\`${name}\``).join(', ')} still name toolkit action groups.`,
|
|
300
|
+
locations: [{ filePath }],
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const createActionId = elements.find((element) => element.imported === 'createActionId')?.local ?? null;
|
|
306
|
+
const refresh = planRefreshSites(filePath, sourceFile, content, createActionId);
|
|
307
|
+
let result = applyEdits(content, refresh.edits);
|
|
308
|
+
const rewrittenSource = createSourceFile(result, filePath);
|
|
309
|
+
const used = collectUsedIdentifiers(rewrittenSource);
|
|
310
|
+
const moved = [];
|
|
311
|
+
const kept = [];
|
|
312
|
+
for (const element of elements) {
|
|
313
|
+
if (!used.has(element.local))
|
|
314
|
+
continue;
|
|
315
|
+
const target = RENAMED_SYMBOLS.get(element.imported) ?? element.imported;
|
|
316
|
+
const binding = target === element.local ? target : `${target} as ${element.local}`;
|
|
317
|
+
if (INTEROP_SYMBOLS.has(target)) {
|
|
318
|
+
moved.push(binding);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
kept.push(element.imported === element.local ? element.local : `${element.imported} as ${element.local}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (refresh.rewritten > 0)
|
|
325
|
+
moved.push('toolkitSelect');
|
|
326
|
+
const hasInteropImport = rewrittenSource.statements.some((statement) => ts.isImportDeclaration(statement) &&
|
|
327
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
328
|
+
statement.moduleSpecifier.text === INTEROP_MODULE);
|
|
329
|
+
const replacement = [
|
|
330
|
+
moved.length > 0 && !hasInteropImport ? `import { ${moved.join(', ')} } from '${INTEROP_MODULE}';` : null,
|
|
331
|
+
kept.length > 0 ? `import { ${kept.join(', ')} } from '${TOOLKIT_MODULE}';` : null,
|
|
332
|
+
].filter((line) => line !== null);
|
|
333
|
+
const importEdits = toolkitImportsOf(rewrittenSource).map((statement, index) => index === 0 && replacement.length > 0
|
|
334
|
+
? { start: statement.getStart(rewrittenSource), end: statement.getEnd(), text: replacement.join('\n') }
|
|
335
|
+
: removeStatement(statement, result));
|
|
336
|
+
result = applyEdits(result, importEdits);
|
|
337
|
+
if (refresh.needsInjectorImport) {
|
|
338
|
+
result = ensureNamedImports({
|
|
339
|
+
content: result,
|
|
340
|
+
importsNeeded: ['inject', 'Injector'],
|
|
341
|
+
moduleSpecifier: '@angular/core',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (moved.length > 0 && hasInteropImport) {
|
|
345
|
+
result = ensureNamedImports({ content: result, importsNeeded: moved, moduleSpecifier: INTEROP_MODULE });
|
|
346
|
+
}
|
|
347
|
+
const tasks = [...refresh.tasks];
|
|
348
|
+
if (kept.length > 0) {
|
|
349
|
+
tasks.push({
|
|
350
|
+
id: CONSUMER_TASK.UNSUPPORTED_IMPORT,
|
|
351
|
+
summary: `Still imports ${kept.map((name) => `\`${name}\``).join(', ')} from \`${TOOLKIT_MODULE}\`.`,
|
|
352
|
+
locations: [{ filePath }],
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
content: result,
|
|
357
|
+
rewritten: result !== content,
|
|
358
|
+
leftOnToolkit: kept.length > 0,
|
|
359
|
+
refreshSitesRewritten: refresh.rewritten,
|
|
360
|
+
tasks,
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
const isInterceptor = (node, sourceFile) => {
|
|
364
|
+
if (ts.isClassDeclaration(node)) {
|
|
365
|
+
return !!node.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText(sourceFile) === 'HttpInterceptor'));
|
|
366
|
+
}
|
|
367
|
+
if (ts.isVariableDeclaration(node))
|
|
368
|
+
return node.type?.getText(sourceFile) === 'HttpInterceptorFn';
|
|
369
|
+
return false;
|
|
370
|
+
};
|
|
371
|
+
const skippedRequests = (node, sourceFile) => {
|
|
372
|
+
const conditions = [];
|
|
373
|
+
const prefixes = new Set();
|
|
374
|
+
const visit = (inner) => {
|
|
375
|
+
if (ts.isIfStatement(inner) && /\burl\b/i.test(inner.expression.getText(sourceFile))) {
|
|
376
|
+
const condition = inner.expression.getText(sourceFile).replace(/\s+/g, ' ');
|
|
377
|
+
conditions.push(condition);
|
|
378
|
+
const collectPrefixes = (part) => {
|
|
379
|
+
const text = ts.isStringLiteralLike(part) ? part.text : ts.isTemplateSpan(part) ? part.literal.text : null;
|
|
380
|
+
const prefix = text && /^\/[\w\-/]*/.exec(text)?.[0];
|
|
381
|
+
if (prefix && prefix.length > 1)
|
|
382
|
+
prefixes.add(prefix);
|
|
383
|
+
ts.forEachChild(part, collectPrefixes);
|
|
384
|
+
};
|
|
385
|
+
collectPrefixes(inner.expression);
|
|
386
|
+
}
|
|
387
|
+
ts.forEachChild(inner, visit);
|
|
388
|
+
};
|
|
389
|
+
visit(node);
|
|
390
|
+
return { conditions, prefixes: [...prefixes] };
|
|
391
|
+
};
|
|
392
|
+
const findInterceptors = (tree, inScope, filePath, content) => {
|
|
393
|
+
if (!/HttpInterceptor/.test(content) || !/authorization|bearer/i.test(content))
|
|
394
|
+
return [];
|
|
395
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
396
|
+
const tasks = [];
|
|
397
|
+
const visit = (node) => {
|
|
398
|
+
if (isInterceptor(node, sourceFile) && /authorization|bearer/i.test(node.getText(sourceFile))) {
|
|
399
|
+
const name = node.name?.getText(sourceFile) ?? 'interceptor';
|
|
400
|
+
const { conditions, prefixes } = skippedRequests(node, sourceFile);
|
|
401
|
+
const registrations = [...inScope]
|
|
402
|
+
.filter((candidate) => candidate !== filePath && tree.exists(candidate))
|
|
403
|
+
.filter((candidate) => new RegExp(`\\b${name}\\b`).test(tree.read(candidate, 'utf-8') ?? ''))
|
|
404
|
+
.map((candidate) => ({ filePath: candidate }));
|
|
405
|
+
const skipped = conditions.length > 0 ? ` It skips requests where ${conditions.map(code).join('; ')}.` : '';
|
|
406
|
+
const suggestion = prefixes.length > 0 ? ` Candidate \`--publicRoutes=${prefixes.join(',')}\`.` : '';
|
|
407
|
+
tasks.push({
|
|
408
|
+
id: CONSUMER_TASK.AUTH_INTERCEPTOR,
|
|
409
|
+
summary: `\`${name}\` adds an \`Authorization\` header.${skipped}${suggestion}`,
|
|
410
|
+
locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }, ...registrations],
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
ts.forEachChild(node, visit);
|
|
415
|
+
};
|
|
416
|
+
visit(sourceFile);
|
|
417
|
+
return tasks;
|
|
418
|
+
};
|
|
419
|
+
/** Runs the consumer side of the migration on every scoped file that still imports `@tomtomb/ngrx-toolkit`. */
|
|
420
|
+
export const migrateToolkitConsumers = (tree, graph, inScope, report) => {
|
|
421
|
+
defineConsumerTasks(report);
|
|
422
|
+
const stats = {
|
|
423
|
+
filesRewritten: 0,
|
|
424
|
+
filesLeftOnToolkit: 0,
|
|
425
|
+
refreshSitesRewritten: 0,
|
|
426
|
+
interceptorsFound: 0,
|
|
427
|
+
};
|
|
428
|
+
for (const filePath of inScope) {
|
|
429
|
+
if (filePath.endsWith('.d.ts'))
|
|
430
|
+
continue;
|
|
431
|
+
const content = tree.read(filePath, 'utf-8');
|
|
432
|
+
if (!content)
|
|
433
|
+
continue;
|
|
434
|
+
const interceptors = findInterceptors(tree, inScope, filePath, content);
|
|
435
|
+
interceptors.forEach((task) => report.add(task));
|
|
436
|
+
stats.interceptorsFound += interceptors.length;
|
|
437
|
+
if (!content.includes(TOOLKIT_MODULE))
|
|
438
|
+
continue;
|
|
439
|
+
const result = rewriteConsumer(tree, graph, filePath, content);
|
|
440
|
+
if (!result)
|
|
441
|
+
continue;
|
|
442
|
+
result.tasks.forEach((task) => report.add(task));
|
|
443
|
+
if (result.rewritten) {
|
|
444
|
+
tree.write(filePath, result.content);
|
|
445
|
+
stats.filesRewritten += 1;
|
|
446
|
+
}
|
|
447
|
+
if (result.leftOnToolkit)
|
|
448
|
+
stats.filesLeftOnToolkit += 1;
|
|
449
|
+
stats.refreshSitesRewritten += result.refreshSitesRewritten;
|
|
450
|
+
}
|
|
451
|
+
return stats;
|
|
452
|
+
};
|
|
453
|
+
//# sourceMappingURL=consumers.js.map
|