@ethlete/query 6.0.0-next.2 → 6.0.0-next.20

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.
@@ -0,0 +1,193 @@
1
+ import * as ts from 'typescript';
2
+ import { createSourceFile } from './shared.js';
3
+ /**
4
+ * Whether an identifier is a *name* rather than a *reference* — a declaration, a property key, the
5
+ * right-hand side of a member access. Renaming those is what turned `PeopleDetailDataSource.getPerson`
6
+ * and the `postLogin` property of a config type into `legacy…` during the first migration run.
7
+ */
8
+ const isNamePosition = (node) => {
9
+ const parent = node.parent;
10
+ if (!parent)
11
+ return false;
12
+ // `foo.bar` / `Foo.Bar` — the part after the dot is never the imported binding.
13
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node)
14
+ return true;
15
+ if (ts.isQualifiedName(parent) && parent.right === node)
16
+ return true;
17
+ // Object literal / type member keys, enum members, JSX-ish attribute names.
18
+ if (ts.isPropertyAssignment(parent) && parent.name === node)
19
+ return true;
20
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node)
21
+ return true;
22
+ if (ts.isPropertySignature(parent) && parent.name === node)
23
+ return true;
24
+ if (ts.isMethodSignature(parent) && parent.name === node)
25
+ return true;
26
+ if (ts.isEnumMember(parent) && parent.name === node)
27
+ return true;
28
+ if (ts.isBindingElement(parent) && parent.propertyName === node)
29
+ return true;
30
+ // Declarations of any kind.
31
+ if (ts.isVariableDeclaration(parent) && parent.name === node)
32
+ return true;
33
+ if (ts.isFunctionDeclaration(parent) && parent.name === node)
34
+ return true;
35
+ if (ts.isClassDeclaration(parent) && parent.name === node)
36
+ return true;
37
+ if (ts.isInterfaceDeclaration(parent) && parent.name === node)
38
+ return true;
39
+ if (ts.isTypeAliasDeclaration(parent) && parent.name === node)
40
+ return true;
41
+ if (ts.isEnumDeclaration(parent) && parent.name === node)
42
+ return true;
43
+ if (ts.isModuleDeclaration(parent) && parent.name === node)
44
+ return true;
45
+ if (ts.isMethodDeclaration(parent) && parent.name === node)
46
+ return true;
47
+ if (ts.isPropertyDeclaration(parent) && parent.name === node)
48
+ return true;
49
+ if (ts.isGetAccessorDeclaration(parent) && parent.name === node)
50
+ return true;
51
+ if (ts.isSetAccessorDeclaration(parent) && parent.name === node)
52
+ return true;
53
+ if (ts.isParameter(parent) && parent.name === node)
54
+ return true;
55
+ if (ts.isTypeParameterDeclaration(parent) && parent.name === node)
56
+ return true;
57
+ if (ts.isLabeledStatement(parent) && parent.label === node)
58
+ return true;
59
+ // Import / export clauses are rewritten by the caller, never by the reference pass.
60
+ if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent))
61
+ return true;
62
+ if (ts.isImportClause(parent) || ts.isNamespaceImport(parent))
63
+ return true;
64
+ return false;
65
+ };
66
+ /** Every name the file declares itself — a local by the same name means the import is shadowed. */
67
+ const collectLocalDeclarations = (sourceFile) => {
68
+ const declared = new Set();
69
+ const add = (name) => {
70
+ if (name && ts.isIdentifier(name))
71
+ declared.add(name.text);
72
+ };
73
+ const visit = (node) => {
74
+ if (ts.isVariableDeclaration(node))
75
+ add(node.name);
76
+ if (ts.isFunctionDeclaration(node))
77
+ add(node.name);
78
+ if (ts.isClassDeclaration(node))
79
+ add(node.name);
80
+ if (ts.isInterfaceDeclaration(node))
81
+ add(node.name);
82
+ if (ts.isTypeAliasDeclaration(node))
83
+ add(node.name);
84
+ if (ts.isEnumDeclaration(node))
85
+ add(node.name);
86
+ if (ts.isParameter(node))
87
+ add(node.name);
88
+ if (ts.isBindingElement(node))
89
+ add(node.name);
90
+ ts.forEachChild(node, visit);
91
+ };
92
+ visit(sourceFile);
93
+ return declared;
94
+ };
95
+ /**
96
+ * Renames references to imported bindings, and only those.
97
+ *
98
+ * `renames` maps a *local binding name* to its new name. The caller is responsible for having
99
+ * verified that the binding really is the symbol being migrated (see `ModuleGraph`); this pass only
100
+ * makes sure the rewrite lands on references rather than on same-named properties, declarations or
101
+ * type members. A name the file also declares locally is skipped entirely and reported back, since
102
+ * a rename there would silently change which value the code reads.
103
+ */
104
+ export const renameImportedReferences = (content, renames) => {
105
+ if (renames.size === 0)
106
+ return { content, shadowed: [] };
107
+ const sourceFile = createSourceFile(content);
108
+ const declaredLocally = collectLocalDeclarations(sourceFile);
109
+ const shadowed = Array.from(renames.keys()).filter((name) => declaredLocally.has(name));
110
+ const applicable = new Map(Array.from(renames.entries()).filter(([name]) => !declaredLocally.has(name)));
111
+ if (applicable.size === 0)
112
+ return { content, shadowed };
113
+ const replacements = [];
114
+ const visit = (node) => {
115
+ if (ts.isIdentifier(node)) {
116
+ const nextName = applicable.get(node.text);
117
+ if (nextName && !isNamePosition(node)) {
118
+ replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement: nextName });
119
+ }
120
+ }
121
+ ts.forEachChild(node, visit);
122
+ };
123
+ visit(sourceFile);
124
+ let result = content;
125
+ replacements.sort((left, right) => right.start - left.start);
126
+ replacements.forEach(({ start, end, replacement }) => {
127
+ result = result.slice(0, start) + replacement + result.slice(end);
128
+ });
129
+ return { content: result, shadowed };
130
+ };
131
+ /**
132
+ * Drops named imports whose local name no longer appears anywhere else in the file.
133
+ *
134
+ * The migration rewrites away the constructs that needed `def`, `AnyLegacyQueryCreator` and friends;
135
+ * leaving the imports behind turns every touched file into a lint error, and `formatFiles` runs in
136
+ * the same generator invocation so they land that way. Identifiers inside template strings count as
137
+ * usage too — a component referenced only from an inline template must not be pruned.
138
+ */
139
+ export const pruneUnusedNamedImports = (content, moduleSpecifiers) => {
140
+ const sourceFile = createSourceFile(content);
141
+ const used = new Set();
142
+ const addWordsFrom = (text) => {
143
+ for (const word of text.match(/[A-Za-z_$][\w$]*/g) ?? []) {
144
+ used.add(word);
145
+ }
146
+ };
147
+ const visit = (node) => {
148
+ if (ts.isIdentifier(node) && !ts.isImportSpecifier(node.parent) && !ts.isImportClause(node.parent)) {
149
+ used.add(node.text);
150
+ }
151
+ // A symbol referenced only from an inline template or a `styleUrl`-style string is still used.
152
+ if (ts.isStringLiteralLike(node) && !ts.isImportDeclaration(node.parent)) {
153
+ addWordsFrom(node.text);
154
+ }
155
+ if (ts.isTemplateLiteralToken(node)) {
156
+ addWordsFrom(node.text);
157
+ }
158
+ ts.forEachChild(node, visit);
159
+ };
160
+ visit(sourceFile);
161
+ const replacements = [];
162
+ for (const node of sourceFile.statements) {
163
+ if (!ts.isImportDeclaration(node) ||
164
+ !ts.isStringLiteral(node.moduleSpecifier) ||
165
+ (moduleSpecifiers !== 'all' && !moduleSpecifiers.includes(node.moduleSpecifier.text)) ||
166
+ !node.importClause?.namedBindings ||
167
+ !ts.isNamedImports(node.importClause.namedBindings)) {
168
+ continue;
169
+ }
170
+ const kept = node.importClause.namedBindings.elements.filter((element) => used.has(element.name.text));
171
+ if (kept.length === node.importClause.namedBindings.elements.length)
172
+ continue;
173
+ const hasDefaultImport = !!node.importClause.name;
174
+ if (kept.length === 0 && !hasDefaultImport) {
175
+ const end = content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
176
+ replacements.push({ start: node.getStart(sourceFile), end, replacement: '' });
177
+ continue;
178
+ }
179
+ const defaultPart = hasDefaultImport ? `${node.importClause.name.text}, ` : '';
180
+ replacements.push({
181
+ start: node.getStart(sourceFile),
182
+ end: node.getEnd(),
183
+ replacement: `import ${defaultPart}{ ${kept.map((element) => element.getText(sourceFile)).join(', ')} } from '${node.moduleSpecifier.text}';`,
184
+ });
185
+ }
186
+ let result = content;
187
+ replacements.sort((left, right) => right.start - left.start);
188
+ replacements.forEach(({ start, end, replacement }) => {
189
+ result = result.slice(0, start) + replacement + result.slice(end);
190
+ });
191
+ return result;
192
+ };
193
+ //# sourceMappingURL=rename-symbols.js.map
@@ -0,0 +1,136 @@
1
+ export const QUERY_V3_MIGRATION_REPORT_PATH = 'query-v3-migration-tasks.md';
2
+ export const MIGRATION_TASK_KIND = {
3
+ WARNING: 'warning',
4
+ MANUAL_REVIEW: 'manual-review',
5
+ FOLLOW_UP: 'follow-up',
6
+ };
7
+ const renderTaskLocations = (locations) => {
8
+ if (locations.length === 0) {
9
+ return '- None recorded';
10
+ }
11
+ return locations
12
+ .map((location) => {
13
+ const lineSuffix = location.line ? `:${location.line}` : '';
14
+ return `- ${location.filePath}${lineSuffix}`;
15
+ })
16
+ .join('\n');
17
+ };
18
+ export class QueryV3MigrationReport {
19
+ constructor() {
20
+ this.tasks = [];
21
+ }
22
+ addTask(input) {
23
+ const dedupeKey = input.dedupeKey ?? `${input.kind}:${input.title}:${input.summary}:${input.action}`;
24
+ if (this.tasks.some((task) => task.dedupeKey === dedupeKey)) {
25
+ return;
26
+ }
27
+ const nextTask = {
28
+ id: `QV3-${String(this.tasks.length + 1).padStart(3, '0')}`,
29
+ kind: input.kind,
30
+ title: input.title,
31
+ summary: input.summary,
32
+ action: input.action,
33
+ locations: input.locations ?? [],
34
+ source: input.source,
35
+ dedupeKey,
36
+ };
37
+ this.tasks.push(nextTask);
38
+ }
39
+ addWarning(input) {
40
+ this.addTask({ ...input, kind: MIGRATION_TASK_KIND.WARNING });
41
+ }
42
+ addManualReview(input) {
43
+ this.addTask({ ...input, kind: MIGRATION_TASK_KIND.MANUAL_REVIEW });
44
+ }
45
+ addFollowUp(input) {
46
+ this.addTask({ ...input, kind: MIGRATION_TASK_KIND.FOLLOW_UP });
47
+ }
48
+ printSummary() {
49
+ if (this.tasks.length === 0) {
50
+ return;
51
+ }
52
+ console.warn(`\n⚠️ Generated ${this.tasks.length} follow-up task${this.tasks.length === 1 ? '' : 's'}:`);
53
+ this.tasks.forEach((task) => {
54
+ const locations = task.locations
55
+ .map((location) => `${location.filePath}${location.line ? `:${location.line}` : ''}`)
56
+ .join(', ');
57
+ console.warn(` - ${task.id} [${task.kind}] ${task.title}${locations ? ` (${locations})` : ''}`);
58
+ });
59
+ console.warn(`\n📄 See ${QUERY_V3_MIGRATION_REPORT_PATH} for the AI-ready follow-up list.`);
60
+ }
61
+ writeToTree(tree) {
62
+ tree.write(QUERY_V3_MIGRATION_REPORT_PATH, renderMigrationReportMarkdown(this.tasks));
63
+ }
64
+ }
65
+ export const renderMigrationReportMarkdown = (tasks) => {
66
+ const header = [
67
+ '# Query V3 Migration Follow-Up',
68
+ '',
69
+ 'This file is generated by the `migrate-to-query-v3` generator.',
70
+ '',
71
+ '## AI Instructions',
72
+ '',
73
+ '1. Re-read the referenced files before changing anything. The migration output is only a starting point.',
74
+ '2. Prefer minimal fixes that align with the current runtime APIs in `@ethlete/query`.',
75
+ '3. Update or remove a task only after the code and tests confirm that it is resolved.',
76
+ '4. If a task is still ambiguous after inspection, keep it open and append a short note instead of guessing.',
77
+ '5. Preserve unrelated user changes. Do not rewrite generated files wholesale unless required.',
78
+ '',
79
+ '## Task Format',
80
+ '',
81
+ '- `Status`: always treat generated tasks as `open` until verified manually or by tests.',
82
+ '- `Kind`: `warning`, `manual-review`, or `follow-up`.',
83
+ '- `Source`: which migration phase raised the task.',
84
+ '',
85
+ ].join('\n');
86
+ if (tasks.length === 0) {
87
+ return `${header}## Tasks\n\nNo open follow-up tasks were recorded during this migration run.\n`;
88
+ }
89
+ const tasksSection = tasks
90
+ .map((task) => {
91
+ return [
92
+ `### ${task.id} - ${task.title}`,
93
+ '',
94
+ '- Status: open',
95
+ `- Kind: ${task.kind}`,
96
+ `- Source: ${task.source}`,
97
+ `- Summary: ${task.summary}`,
98
+ `- Suggested action: ${task.action}`,
99
+ '- Locations:',
100
+ renderTaskLocations(task.locations),
101
+ '',
102
+ ].join('\n');
103
+ })
104
+ .join('');
105
+ const machineSection = [
106
+ '## Machine Summary',
107
+ '',
108
+ '```yaml',
109
+ 'tasks:',
110
+ ...tasks.flatMap((task) => {
111
+ const locationLines = task.locations.length === 0 ? [' []'] : [];
112
+ const nestedLocations = task.locations.flatMap((location) => {
113
+ const lines = [` - filePath: ${location.filePath}`];
114
+ if (location.line) {
115
+ lines.push(` line: ${location.line}`);
116
+ }
117
+ return lines;
118
+ });
119
+ return [
120
+ ` - id: ${task.id}`,
121
+ ` status: open`,
122
+ ` kind: ${task.kind}`,
123
+ ` source: ${task.source}`,
124
+ ` title: ${task.title}`,
125
+ ` summary: ${task.summary}`,
126
+ ` action: ${task.action}`,
127
+ ` locations:`,
128
+ ...(nestedLocations.length > 0 ? nestedLocations : locationLines),
129
+ ];
130
+ }),
131
+ '```',
132
+ '',
133
+ ].join('\n');
134
+ return `${header}## Tasks\n\n${tasksSection}${machineSection}`;
135
+ };
136
+ //# sourceMappingURL=report.js.map
@@ -4,6 +4,18 @@
4
4
  "title": "Migrate to Query v3",
5
5
  "type": "object",
6
6
  "properties": {
7
+ "projects": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Nx projects to migrate. Defaults to every project in the workspace. Use this to adopt v3 one app or library at a time instead of rewriting the whole monorepo in one run — but keep a query client and the creators built from it in the same run, since they are migrated together.",
11
+ "alias": "project"
12
+ },
13
+ "include": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/queries'). Unioned with `projects`. Defaults to the whole workspace.",
17
+ "alias": "includePath"
18
+ },
7
19
  "skipFormat": {
8
20
  "type": "boolean",
9
21
  "description": "Skip formatting files after migration",
@@ -0,0 +1,97 @@
1
+ import * as ts from 'typescript';
2
+ const TEMP_SOURCE_FILE = 'temp.ts';
3
+ export const createSourceFile = (content, filePath = TEMP_SOURCE_FILE) => ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
4
+ export const capitalizeFirstLetter = (value) => {
5
+ if (value.length === 0) {
6
+ return value;
7
+ }
8
+ return `${value[0].toUpperCase()}${value.slice(1)}`;
9
+ };
10
+ export const ensureConfigSuffix = (name) => {
11
+ if (/Config$/i.test(name)) {
12
+ return name;
13
+ }
14
+ return `${name}Config`;
15
+ };
16
+ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier }) => {
17
+ const sourceFile = createSourceFile(content);
18
+ let importNode;
19
+ ts.forEachChild(sourceFile, (node) => {
20
+ if (ts.isImportDeclaration(node) &&
21
+ ts.isStringLiteral(node.moduleSpecifier) &&
22
+ node.moduleSpecifier.text === moduleSpecifier) {
23
+ importNode = node;
24
+ }
25
+ });
26
+ const missingImports = new Set(importsNeeded);
27
+ if (importNode?.importClause?.namedBindings && ts.isNamedImports(importNode.importClause.namedBindings)) {
28
+ const namedBindings = importNode.importClause.namedBindings;
29
+ namedBindings.elements.forEach((element) => {
30
+ missingImports.delete(element.name.text);
31
+ });
32
+ if (missingImports.size === 0) {
33
+ return content;
34
+ }
35
+ const existingImports = namedBindings.elements.map((element) => element.getText(sourceFile));
36
+ const nextImports = [...existingImports, ...Array.from(missingImports).sort()].sort();
37
+ const nextImportStatement = `import { ${nextImports.join(', ')} } from '${moduleSpecifier}';`;
38
+ return content.slice(0, importNode.getStart(sourceFile)) + nextImportStatement + content.slice(importNode.getEnd());
39
+ }
40
+ let lastImportEnd = 0;
41
+ ts.forEachChild(sourceFile, (node) => {
42
+ if (!ts.isImportDeclaration(node)) {
43
+ return;
44
+ }
45
+ const end = node.getEnd();
46
+ if (end > lastImportEnd) {
47
+ lastImportEnd = end;
48
+ }
49
+ });
50
+ const nextImportStatement = `\nimport { ${Array.from(missingImports).sort().join(', ')} } from '${moduleSpecifier}';`;
51
+ if (lastImportEnd > 0) {
52
+ return content.slice(0, lastImportEnd) + nextImportStatement + content.slice(lastImportEnd);
53
+ }
54
+ return `import { ${Array.from(missingImports).sort().join(', ')} } from '${moduleSpecifier}';\n\n${content}`;
55
+ };
56
+ export const ensureImportFromQuery = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/query' });
57
+ export const ensureAngularCoreImports = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@angular/core' });
58
+ export const getVariableStatementEnd = (sourceFile, variableName, initializerName) => {
59
+ let variableStatementEnd;
60
+ const visit = (node) => {
61
+ if (ts.isVariableDeclaration(node) &&
62
+ ts.isIdentifier(node.name) &&
63
+ node.name.text === variableName &&
64
+ node.initializer &&
65
+ ts.isCallExpression(node.initializer) &&
66
+ ts.isIdentifier(node.initializer.expression) &&
67
+ node.initializer.expression.text === initializerName) {
68
+ let parent = node.parent;
69
+ while (parent && !ts.isVariableStatement(parent)) {
70
+ parent = parent.parent;
71
+ }
72
+ if (parent && ts.isVariableStatement(parent)) {
73
+ variableStatementEnd = parent.getEnd();
74
+ }
75
+ }
76
+ ts.forEachChild(node, visit);
77
+ };
78
+ visit(sourceFile);
79
+ return variableStatementEnd;
80
+ };
81
+ export const getIndentation = (content, position) => {
82
+ let lineStart = position;
83
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
84
+ lineStart -= 1;
85
+ }
86
+ let indentation = '';
87
+ for (let index = lineStart; index < position && /\s/.test(content[index]); index += 1) {
88
+ indentation += content[index];
89
+ }
90
+ return indentation || ' ';
91
+ };
92
+ export const getLineNumber = (node, sourceFile) => {
93
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
94
+ return line + 1;
95
+ };
96
+ export const getLineNumberFromPosition = (content, position) => content.slice(0, position).split('\n').length;
97
+ //# sourceMappingURL=shared.js.map
@@ -58,7 +58,7 @@ function renameConflictingSymbols(tree) {
58
58
  // Log statistics
59
59
  if (updatedFiles.length > 0) {
60
60
  console.log(`\n📊 Renamed symbols in ${updatedFiles.length} files:`);
61
- const hasRenames = Array.from(symbolUsageCounts.entries()).filter(([_, count]) => count > 0);
61
+ const hasRenames = Array.from(symbolUsageCounts.entries()).filter(([, count]) => count > 0);
62
62
  if (hasRenames.length > 0) {
63
63
  console.log('\n Symbol usage counts:');
64
64
  hasRenames.forEach(([symbol, count]) => {
@@ -123,7 +123,7 @@ function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
123
123
  }
124
124
  visit(sourceFile);
125
125
  // Update imports in @ethlete/query import declarations
126
- const importReplacements = updateImports(sourceFile, content);
126
+ const importReplacements = updateImports(sourceFile);
127
127
  replacements.push(...importReplacements);
128
128
  // Count usages
129
129
  replacements.forEach((r) => {
@@ -138,7 +138,7 @@ function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
138
138
  }
139
139
  return result;
140
140
  }
141
- function updateImports(sourceFile, content) {
141
+ function updateImports(sourceFile) {
142
142
  const replacements = [];
143
143
  function visit(node) {
144
144
  if (ts.isImportDeclaration(node)) {
package/package.json CHANGED
@@ -1,22 +1,21 @@
1
1
  {
2
2
  "name": "@ethlete/query",
3
- "version": "6.0.0-next.2",
3
+ "version": "6.0.0-next.20",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@analogjs/vite-plugin-angular": "2.3.1",
8
- "@angular/common": "21.2.1",
9
- "@angular/core": "21.2.1",
10
- "@angular/forms": "21.2.1",
11
- "@angular/router": "21.2.1",
12
- "@ethlete/core": "^5.0.0-next.0",
13
- "@ethlete/types": "^2.0.0-next.0",
14
- "@nx/devkit": "22.5.4",
15
- "@nx/vite": "22.5.4",
7
+ "@analogjs/vite-plugin-angular": "2.6.3",
8
+ "@angular/common": "22.0.7",
9
+ "@angular/core": "22.0.7",
10
+ "@angular/forms": "22.0.7",
11
+ "@angular/router": "22.0.7",
12
+ "@ethlete/core": "^5.0.0-beta.11",
13
+ "@ethlete/types": "^2.0.0-beta.4",
14
+ "@nx/devkit": "23.1.0",
16
15
  "rxjs": "7.8.2",
17
16
  "socket.io-client": "4.8.3",
18
- "typescript": "5.9.3",
19
- "vite": "^7.0.0"
17
+ "typescript": "6.0.3",
18
+ "vite": "8.1.5"
20
19
  },
21
20
  "generators": "./generators/generators.json",
22
21
  "module": "fesm2022/ethlete-query.mjs",
@@ -34,6 +33,7 @@
34
33
  "default": "./fesm2022/ethlete-query-testing.mjs"
35
34
  }
36
35
  },
36
+ "type": "module",
37
37
  "dependencies": {
38
38
  "tslib": "^2.3.0"
39
39
  }