@ethlete/query 6.0.0-next.51 → 6.0.0-next.53

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +393 -0
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
  4. package/fesm2022/ethlete-query-testing.mjs +67 -25
  5. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  6. package/fesm2022/ethlete-query.mjs +7895 -7226
  7. package/fesm2022/ethlete-query.mjs.map +1 -1
  8. package/generators/generators.json +5 -0
  9. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
  10. package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
  11. package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
  12. package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
  13. package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
  14. package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
  15. package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
  16. package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
  17. package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
  18. package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
  19. package/generators/migrate-query-opt-in-features/migration.js +10 -9
  20. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
  21. package/generators/migrate-to-query-v3/cleanup-migration.js +163 -53
  22. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
  23. package/generators/migrate-to-query-v3/migration-scope.js +2 -0
  24. package/generators/migrate-to-query-v3/migration.js +5 -4
  25. package/generators/migrate-to-query-v3/module-graph.js +41 -29
  26. package/generators/migrate-to-query-v3/query-client-migration.js +209 -126
  27. package/generators/migrate-to-query-v3/rename-symbols.js +38 -11
  28. package/generators/migrate-to-query-v3/report.js +40 -7
  29. package/generators/migrate-to-query-v3/shared.js +46 -1
  30. package/generators/prep-for-query-v3/migration.js +400 -57
  31. package/migrations/query-field-default-types.md +43 -0
  32. package/migrations/search-query-field-string.md +43 -0
  33. package/migrations.json +16 -0
  34. package/package.json +5 -1
  35. package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
  36. package/types/ethlete-query-testing.d.ts +28 -11
  37. package/types/ethlete-query.d.ts +789 -584
@@ -1,11 +1,11 @@
1
1
  import * as ts from 'typescript';
2
- import { createSourceFile } from './shared.js';
2
+ import { createSourceFile, printImportDeclaration } from './shared.js';
3
3
  /**
4
4
  * Whether an identifier is a *name* rather than a *reference* - a declaration, a property key, the
5
5
  * right-hand side of a member access. Renaming those is what turned `PeopleDetailDataSource.getPerson`
6
6
  * and the `postLogin` property of a config type into `legacy…` during the first migration run.
7
7
  */
8
- const isNamePosition = (node) => {
8
+ export const isNamePosition = (node) => {
9
9
  const parent = node.parent;
10
10
  if (!parent)
11
11
  return false;
@@ -65,7 +65,7 @@ const isNamePosition = (node) => {
65
65
  return true;
66
66
  return false;
67
67
  };
68
- const renamedReference = (node, nextName) => {
68
+ export const renamedReference = (node, nextName) => {
69
69
  const parent = node.parent;
70
70
  if (ts.isShorthandPropertyAssignment(parent))
71
71
  return `${node.text}: ${nextName}`;
@@ -73,6 +73,38 @@ const renamedReference = (node, nextName) => {
73
73
  return `${nextName} as ${node.text}`;
74
74
  return nextName;
75
75
  };
76
+ const bindingDeclares = (name, text) => ts.isIdentifier(name)
77
+ ? name.text === text
78
+ : name.elements.some((element) => !ts.isOmittedExpression(element) && bindingDeclares(element.name, text));
79
+ const statementDeclares = (statement, text) => (ts.isVariableStatement(statement) &&
80
+ statement.declarationList.declarations.some((declaration) => bindingDeclares(declaration.name, text))) ||
81
+ ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement)) &&
82
+ statement.name?.text === text);
83
+ export const isShadowedInNestedScope = (identifier) => {
84
+ const text = identifier.text;
85
+ for (let scope = identifier.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
86
+ if (ts.isFunctionLike(scope) && scope.parameters.some((parameter) => bindingDeclares(parameter.name, text))) {
87
+ return true;
88
+ }
89
+ if ((ts.isFunctionExpression(scope) || ts.isClassExpression(scope)) && scope.name?.text === text) {
90
+ return true;
91
+ }
92
+ if ((ts.isBlock(scope) || ts.isModuleBlock(scope) || ts.isCaseClause(scope) || ts.isDefaultClause(scope)) &&
93
+ scope.statements.some((statement) => statementDeclares(statement, text))) {
94
+ return true;
95
+ }
96
+ if ((ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) &&
97
+ scope.initializer &&
98
+ ts.isVariableDeclarationList(scope.initializer) &&
99
+ scope.initializer.declarations.some((declaration) => bindingDeclares(declaration.name, text))) {
100
+ return true;
101
+ }
102
+ if (ts.isCatchClause(scope) && scope.variableDeclaration && bindingDeclares(scope.variableDeclaration.name, text)) {
103
+ return true;
104
+ }
105
+ }
106
+ return false;
107
+ };
76
108
  /** Every name the file declares itself - a local by the same name means the import is shadowed. */
77
109
  const collectLocalDeclarations = (sourceFile) => {
78
110
  const declared = new Set();
@@ -183,18 +215,13 @@ export const pruneUnusedNamedImports = (content) => {
183
215
  const kept = node.importClause.namedBindings.elements.filter((element) => used.has(element.name.text));
184
216
  if (kept.length === node.importClause.namedBindings.elements.length)
185
217
  continue;
186
- const hasDefaultImport = !!node.importClause.name;
187
- if (kept.length === 0 && !hasDefaultImport) {
218
+ const nextImport = printImportDeclaration(node, sourceFile, kept.map((element) => element.getText(sourceFile)));
219
+ if (!nextImport) {
188
220
  const end = content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
189
221
  replacements.push({ start: node.getStart(sourceFile), end, replacement: '' });
190
222
  continue;
191
223
  }
192
- const defaultPart = hasDefaultImport ? `${node.importClause.name.text}, ` : '';
193
- replacements.push({
194
- start: node.getStart(sourceFile),
195
- end: node.getEnd(),
196
- replacement: `import ${defaultPart}{ ${kept.map((element) => element.getText(sourceFile)).join(', ')} } from '${node.moduleSpecifier.text}';`,
197
- });
224
+ replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement: nextImport });
198
225
  }
199
226
  let result = content;
200
227
  replacements.sort((left, right) => right.start - left.start);
@@ -25,7 +25,7 @@ export class QueryV3MigrationReport {
25
25
  return;
26
26
  }
27
27
  const nextTask = {
28
- id: `QV3-${String(this.tasks.length + 1).padStart(3, '0')}`,
28
+ id: formatTaskId(this.tasks.length + 1),
29
29
  kind: input.kind,
30
30
  title: input.title,
31
31
  summary: input.summary,
@@ -58,11 +58,37 @@ export class QueryV3MigrationReport {
58
58
  });
59
59
  console.warn(`\n📄 See ${QUERY_V3_MIGRATION_REPORT_PATH} for the AI-ready follow-up list.`);
60
60
  }
61
- writeToTree(tree) {
62
- tree.write(QUERY_V3_MIGRATION_REPORT_PATH, renderMigrationReportMarkdown(this.tasks));
61
+ /**
62
+ * Writes the report. A scoped run (`scopeLabel` set) replaces its own section of an existing report
63
+ * or appends one, so the tasks of earlier runs over other projects survive.
64
+ */
65
+ writeToTree(tree, scopeLabel) {
66
+ const existing = scopeLabel ? tree.read(QUERY_V3_MIGRATION_REPORT_PATH, 'utf-8') : null;
67
+ if (!scopeLabel || !existing) {
68
+ tree.write(QUERY_V3_MIGRATION_REPORT_PATH, renderMigrationReportMarkdown(this.tasks, scopeLabel));
69
+ return;
70
+ }
71
+ tree.write(QUERY_V3_MIGRATION_REPORT_PATH, mergeScopedSection(existing, this.tasks, scopeLabel));
63
72
  }
64
73
  }
65
- export const renderMigrationReportMarkdown = (tasks) => {
74
+ const sectionStart = (scopeLabel) => `<!-- migrate-to-query-v3 scope: ${scopeLabel} -->`;
75
+ const SECTION_END = '<!-- /migrate-to-query-v3 scope -->';
76
+ const mergeScopedSection = (existing, tasks, scopeLabel) => {
77
+ const start = sectionStart(scopeLabel);
78
+ const startIndex = existing.indexOf(start);
79
+ const endIndex = startIndex === -1 ? -1 : existing.indexOf(SECTION_END, startIndex);
80
+ const [before, after] = startIndex === -1 || endIndex === -1
81
+ ? [existing.trimEnd(), '']
82
+ : [existing.slice(0, startIndex).trimEnd(), existing.slice(endIndex + SECTION_END.length).trimStart()];
83
+ const highestId = Math.max(0, ...Array.from(`${before}\n${after}`.matchAll(/QV3-(\d+)/g), (match) => Number(match[1])));
84
+ const renumbered = tasks.map((task, index) => ({ ...task, id: formatTaskId(highestId + index + 1) }));
85
+ return [before, renderTasksSection(renumbered, scopeLabel), after]
86
+ .filter((part) => part.length > 0)
87
+ .join('\n\n')
88
+ .concat('\n');
89
+ };
90
+ const formatTaskId = (index) => `QV3-${String(index).padStart(3, '0')}`;
91
+ export const renderMigrationReportMarkdown = (tasks, scopeLabel) => {
66
92
  const header = [
67
93
  '# Query V3 Migration Follow-Up',
68
94
  '',
@@ -83,8 +109,16 @@ export const renderMigrationReportMarkdown = (tasks) => {
83
109
  '- `Source`: which migration phase raised the task.',
84
110
  '',
85
111
  ].join('\n');
112
+ return `${header}${renderTasksSection(tasks, scopeLabel)}\n`;
113
+ };
114
+ const renderTasksSection = (tasks, scopeLabel) => {
115
+ const heading = scopeLabel ? `## Tasks (scope: ${scopeLabel})` : '## Tasks';
116
+ const body = renderTasksBody(tasks);
117
+ return scopeLabel ? `${sectionStart(scopeLabel)}\n${heading}\n\n${body}\n${SECTION_END}` : `${heading}\n\n${body}`;
118
+ };
119
+ const renderTasksBody = (tasks) => {
86
120
  if (tasks.length === 0) {
87
- return `${header}## Tasks\n\nNo open follow-up tasks were recorded during this migration run.\n`;
121
+ return 'No open follow-up tasks were recorded during this migration run.';
88
122
  }
89
123
  const tasksSection = tasks
90
124
  .map((task) => {
@@ -129,8 +163,7 @@ export const renderMigrationReportMarkdown = (tasks) => {
129
163
  ];
130
164
  }),
131
165
  '```',
132
- '',
133
166
  ].join('\n');
134
- return `${header}## Tasks\n\n${tasksSection}${machineSection}`;
167
+ return `${tasksSection}${machineSection}`;
135
168
  };
136
169
  //# sourceMappingURL=report.js.map
@@ -13,6 +13,23 @@ export const ensureConfigSuffix = (name) => {
13
13
  }
14
14
  return `${name}Config`;
15
15
  };
16
+ const importSpecifierText = (element) => element.propertyName ? `${element.propertyName.text} as ${element.name.text}` : element.name.text;
17
+ /**
18
+ * Prints `node` with `namedElements` as its named bindings, keeping the default import, the
19
+ * `import type` modifier, the quote style and any import attributes. Returns an empty string when
20
+ * nothing would be left to import.
21
+ */
22
+ export const printImportDeclaration = (node, sourceFile, namedElements) => {
23
+ const clause = node.importClause;
24
+ const defaultName = clause?.name?.text;
25
+ const bindings = [defaultName, namedElements.length > 0 ? `{ ${namedElements.join(', ')} }` : undefined].filter((part) => !!part);
26
+ if (bindings.length === 0) {
27
+ return '';
28
+ }
29
+ const typePrefix = clause?.isTypeOnly ? 'type ' : '';
30
+ const attributes = node.attributes ? ` ${node.attributes.getText(sourceFile)}` : '';
31
+ return `import ${typePrefix}${bindings.join(', ')} from ${node.moduleSpecifier.getText(sourceFile)}${attributes};`;
32
+ };
16
33
  export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier }) => {
17
34
  const sourceFile = createSourceFile(content);
18
35
  let importNode;
@@ -32,13 +49,14 @@ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier })
32
49
  const namedBindings = importNode.importClause.namedBindings;
33
50
  namedBindings.elements.forEach((element) => {
34
51
  missingImports.delete(element.name.text);
52
+ missingImports.delete(importSpecifierText(element));
35
53
  });
36
54
  if (missingImports.size === 0) {
37
55
  return content;
38
56
  }
39
57
  const existingImports = namedBindings.elements.map((element) => element.getText(sourceFile));
40
58
  const nextImports = [...existingImports, ...Array.from(missingImports).sort()].sort();
41
- const nextImportStatement = `import { ${nextImports.join(', ')} } from '${moduleSpecifier}';`;
59
+ const nextImportStatement = printImportDeclaration(importNode, sourceFile, nextImports);
42
60
  return content.slice(0, importNode.getStart(sourceFile)) + nextImportStatement + content.slice(importNode.getEnd());
43
61
  }
44
62
  let lastImportEnd = 0;
@@ -143,4 +161,31 @@ export const addJsDocTag = (content, sourceFile, node, tag) => {
143
161
  const indentation = ' '.repeat(sourceFile.getLineAndCharacterOfPosition(start).character);
144
162
  return (content.slice(0, start) + `/**\n${indentation} * ${tag}\n${indentation} */\n${indentation}` + content.slice(start));
145
163
  };
164
+ const TUPLE_PROVIDER_ACTION = 'Delete the call: a v3 client, auth provider or socket is root-provided the moment something injects it. For a test or an override, provide it with `toProvideFn(ref)` from `@ethlete/core`.';
165
+ export const REMOVED_EXPERIMENTAL_QUERY_HELPERS = {
166
+ createQueryClientConfig: 'Replace it with `createQueryClient({ name, baseUrl })`, which returns the provider tuple, and derive `toProvideFn` / `toInjectFn` from it.',
167
+ createBearerAuthProviderConfig: 'Replace it with `createBearerAuthProvider({ name, queryClientRef, queries, features })`, which returns the provider tuple, and derive `toProvideFn` / `toInjectFn` from it.',
168
+ provideQueryClient: TUPLE_PROVIDER_ACTION,
169
+ provideBearerAuthProvider: TUPLE_PROVIDER_ACTION,
170
+ provideWebSocketClient: `Build the socket with \`createWebSocketClient({ name, url, io })\`. ${TUPLE_PROVIDER_ACTION}`,
171
+ };
172
+ export const findRemovedExperimentalQueryHelpers = (sourceFile) => {
173
+ const found = [];
174
+ sourceFile.statements.forEach((statement) => {
175
+ if (!ts.isImportDeclaration(statement) ||
176
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
177
+ statement.moduleSpecifier.text !== '@ethlete/query' ||
178
+ !statement.importClause?.namedBindings ||
179
+ !ts.isNamedImports(statement.importClause.namedBindings)) {
180
+ return;
181
+ }
182
+ statement.importClause.namedBindings.elements.forEach((element) => {
183
+ const name = (element.propertyName ?? element.name).text;
184
+ if (name in REMOVED_EXPERIMENTAL_QUERY_HELPERS) {
185
+ found.push({ name, line: getLineNumber(element, sourceFile) });
186
+ }
187
+ });
188
+ });
189
+ return found;
190
+ };
146
191
  //# sourceMappingURL=shared.js.map