@ethlete/query 5.42.0 → 5.42.1

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.
@@ -60,6 +60,10 @@ async function migrate(tree, schema) {
60
60
  replaceAnyQueryWithLegacy(tree);
61
61
  // Remove devtools usage
62
62
  removeDevtoolsUsage(tree);
63
+ // Migrate empty .prepare() calls to .prepare({})
64
+ migrateEmptyPrepareCalls(tree);
65
+ // Check for legacy query creator usage (step 1: just collect and report)
66
+ checkLegacyQueryCreatorUsage(tree);
63
67
  if (!schema.skipFormat) {
64
68
  await (0, devkit_1.formatFiles)(tree);
65
69
  }
@@ -1196,9 +1200,14 @@ function updateLegacyCreatorUsages(content, legacyCreators) {
1196
1200
  }
1197
1201
  });
1198
1202
  }
1199
- // Handle regular identifier references
1203
+ // Handle regular identifier references - but skip declarations and property access names
1200
1204
  if (ts.isIdentifier(node) && legacyCreators.has(node.text)) {
1201
1205
  const parent = node.parent;
1206
+ // Skip if it's a property name in PropertyAccessExpression (e.g., obj.methodName)
1207
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
1208
+ ts.forEachChild(node, visit);
1209
+ return;
1210
+ }
1202
1211
  // Skip if it's a property name in an object literal (already handled above)
1203
1212
  if (ts.isPropertyAssignment(parent) && parent.name === node) {
1204
1213
  ts.forEachChild(node, visit);
@@ -1224,6 +1233,25 @@ function updateLegacyCreatorUsages(content, legacyCreators) {
1224
1233
  ts.forEachChild(node, visit);
1225
1234
  return;
1226
1235
  }
1236
+ // Skip if it's a function/method name declaration
1237
+ if (ts.isFunctionDeclaration(parent) && parent.name === node) {
1238
+ ts.forEachChild(node, visit);
1239
+ return;
1240
+ }
1241
+ if (ts.isMethodDeclaration(parent) && parent.name === node) {
1242
+ ts.forEachChild(node, visit);
1243
+ return;
1244
+ }
1245
+ // Skip if it's a parameter name
1246
+ if (ts.isParameter(parent) && parent.name === node) {
1247
+ ts.forEachChild(node, visit);
1248
+ return;
1249
+ }
1250
+ // Skip if it's a property declaration name (class field)
1251
+ if (ts.isPropertyDeclaration(parent) && parent.name === node) {
1252
+ ts.forEachChild(node, visit);
1253
+ return;
1254
+ }
1227
1255
  // Replace with legacy name
1228
1256
  const newName = legacyCreators.get(node.text);
1229
1257
  replacements.push({
@@ -1244,9 +1272,6 @@ function updateLegacyCreatorUsages(content, legacyCreators) {
1244
1272
  return result;
1245
1273
  }
1246
1274
  //#endregion
1247
- //#region Check all files for usage of legacy query creators and add injector if needed
1248
- // TODO
1249
- //#endregion
1250
1275
  //#region Remove provideQueryClientForDevtools and QueryDevtoolsComponent in ts / et-query-devtools in html usage
1251
1276
  function removeDevtoolsUsage(tree) {
1252
1277
  const projects = (0, devkit_1.getProjects)(tree);
@@ -1740,5 +1765,1188 @@ function generateInjectFunction(configName) {
1740
1765
  const functionName = `inject${capitalizedName}`;
1741
1766
  return `export const ${functionName} = () => inject(${configName}Config.token);`;
1742
1767
  }
1768
+ function checkLegacyQueryCreatorUsage(tree) {
1769
+ console.log('\nšŸ” Checking for legacy query creator usages...');
1770
+ // Step 1: Collect all usages BEFORE any transformations
1771
+ const allUsages = [];
1772
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1773
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1774
+ return;
1775
+ const content = tree.read(filePath, 'utf-8');
1776
+ if (!content)
1777
+ return;
1778
+ const usages = findLegacyCreatorUsages(content, filePath);
1779
+ allUsages.push(...usages);
1780
+ });
1781
+ if (allUsages.length === 0) {
1782
+ console.log('\nāœ… No legacy query creator usages found');
1783
+ return;
1784
+ }
1785
+ console.log(`\nšŸ“Š Found ${allUsages.length} legacy query creator usages`);
1786
+ // Log statistics
1787
+ const contextCounts = new Map();
1788
+ allUsages.forEach((usage) => {
1789
+ contextCounts.set(usage.context, (contextCounts.get(usage.context) || 0) + 1);
1790
+ });
1791
+ contextCounts.forEach((count, context) => {
1792
+ console.log(` - ${context}: ${count} usages`);
1793
+ });
1794
+ // Step 1.5: Detect polling usage BEFORE any transformations
1795
+ console.log('\nšŸ” Detecting polling usage...');
1796
+ const pollingInfo = detectPollingUsage(tree);
1797
+ if (pollingInfo.size > 0) {
1798
+ console.log(`\nāš ļø Found ${pollingInfo.size} queries with polling:`);
1799
+ pollingInfo.forEach((info) => {
1800
+ console.log(` ${info.queryVariableName}:`);
1801
+ info.locations.forEach((loc) => console.log(` - ${loc}`));
1802
+ });
1803
+ console.log('\n → These queries will NOT have destroyOnResponse: true added');
1804
+ }
1805
+ // Step 2: Find or create injector members in classes
1806
+ const classInjectors = findOrCreateInjectorMembers(tree, allUsages);
1807
+ createInjectorMembers(tree, classInjectors);
1808
+ // Step 3: Transform prepare() calls with polling info
1809
+ transformLegacyCreatorPrepareCall(tree, classInjectors, pollingInfo);
1810
+ // Step 4: Report manual review locations (using original line numbers from allUsages)
1811
+ reportManualReviewLocations(tree, allUsages);
1812
+ }
1813
+ function findLegacyCreatorUsages(content, filePath) {
1814
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
1815
+ const usages = [];
1816
+ // First, find all legacy query creator names imported/defined in this file
1817
+ const legacyCreatorNames = findLegacyCreatorNames(sourceFile, content);
1818
+ if (legacyCreatorNames.size === 0) {
1819
+ return usages;
1820
+ }
1821
+ // Now find all .prepare() calls on these creators
1822
+ function visit(node) {
1823
+ // Look for: legacyCreator.prepare()
1824
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
1825
+ const propAccess = node.expression;
1826
+ if (ts.isIdentifier(propAccess.name) && propAccess.name.text === 'prepare') {
1827
+ // Check if the object is a legacy creator
1828
+ let creatorName;
1829
+ if (ts.isIdentifier(propAccess.expression)) {
1830
+ creatorName = propAccess.expression.text;
1831
+ }
1832
+ if (creatorName && legacyCreatorNames.has(creatorName)) {
1833
+ const context = determineUsageContext(node, sourceFile);
1834
+ const hasInjector = checkForExistingInjector(node, sourceFile);
1835
+ const lineNumber = getLineNumber(node, sourceFile);
1836
+ usages.push({
1837
+ filePath,
1838
+ creatorName,
1839
+ line: lineNumber,
1840
+ position: {
1841
+ start: node.getStart(sourceFile),
1842
+ end: node.getEnd(),
1843
+ },
1844
+ context,
1845
+ hasExistingInjector: hasInjector,
1846
+ });
1847
+ }
1848
+ }
1849
+ }
1850
+ ts.forEachChild(node, visit);
1851
+ }
1852
+ visit(sourceFile);
1853
+ return usages;
1854
+ }
1855
+ function findLegacyCreatorNames(sourceFile, content) {
1856
+ const names = new Set();
1857
+ function visit(node) {
1858
+ // Find imports of legacy creators (names starting with 'legacy')
1859
+ if (ts.isImportDeclaration(node) &&
1860
+ node.importClause?.namedBindings &&
1861
+ ts.isNamedImports(node.importClause.namedBindings)) {
1862
+ node.importClause.namedBindings.elements.forEach((element) => {
1863
+ const name = element.name.text;
1864
+ if (name.startsWith('legacy')) {
1865
+ names.add(name);
1866
+ }
1867
+ });
1868
+ }
1869
+ // Find local legacy creator definitions
1870
+ if (ts.isVariableDeclaration(node) &&
1871
+ ts.isIdentifier(node.name) &&
1872
+ node.name.text.startsWith('legacy') &&
1873
+ node.initializer &&
1874
+ ts.isCallExpression(node.initializer)) {
1875
+ const callExpr = node.initializer;
1876
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
1877
+ ts.isIdentifier(callExpr.expression.expression) &&
1878
+ callExpr.expression.expression.text === 'E' &&
1879
+ ts.isIdentifier(callExpr.expression.name) &&
1880
+ callExpr.expression.name.text === 'createLegacyQueryCreator') {
1881
+ names.add(node.name.text);
1882
+ }
1883
+ }
1884
+ ts.forEachChild(node, visit);
1885
+ }
1886
+ visit(sourceFile);
1887
+ return names;
1888
+ }
1889
+ function determineUsageContext(node, sourceFile) {
1890
+ let current = node;
1891
+ while (current) {
1892
+ // Check for queryComputed FIRST (before checking for generic functions)
1893
+ // We need to look at the parent of arrow/function expressions
1894
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
1895
+ const parent = current.parent;
1896
+ // Check if this function is a callback to queryComputed
1897
+ if (ts.isCallExpression(parent)) {
1898
+ const expr = parent.expression;
1899
+ // Check for queryComputed()
1900
+ if (ts.isIdentifier(expr) && expr.text === 'queryComputed') {
1901
+ return 'queryComputed';
1902
+ }
1903
+ // Check for E.queryComputed()
1904
+ if (ts.isPropertyAccessExpression(expr) &&
1905
+ ts.isIdentifier(expr.expression) &&
1906
+ expr.expression.text === 'E' &&
1907
+ ts.isIdentifier(expr.name) &&
1908
+ expr.name.text === 'queryComputed') {
1909
+ return 'queryComputed';
1910
+ }
1911
+ }
1912
+ }
1913
+ // Check for constructor
1914
+ if (ts.isConstructorDeclaration(current)) {
1915
+ return 'constructor';
1916
+ }
1917
+ // Check for method
1918
+ if (ts.isMethodDeclaration(current)) {
1919
+ return 'method';
1920
+ }
1921
+ // Check for class field
1922
+ if (ts.isPropertyDeclaration(current)) {
1923
+ return 'class-field';
1924
+ }
1925
+ // Check for function declarations
1926
+ if (ts.isFunctionDeclaration(current)) {
1927
+ return 'function';
1928
+ }
1929
+ // Check for arrow functions and function expressions (standalone, not callbacks)
1930
+ // But make sure we're not inside a class (which would make it a method/field)
1931
+ if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) && !isInsideClass(current)) {
1932
+ return 'function';
1933
+ }
1934
+ current = current.parent;
1935
+ }
1936
+ return 'unknown';
1937
+ }
1938
+ function isInsideClass(node) {
1939
+ let current = node.parent;
1940
+ while (current) {
1941
+ if (ts.isClassDeclaration(current)) {
1942
+ return true;
1943
+ }
1944
+ current = current.parent;
1945
+ }
1946
+ return false;
1947
+ }
1948
+ function checkForExistingInjector(node, sourceFile) {
1949
+ // Walk up to find the containing class
1950
+ let current = node;
1951
+ while (current && !ts.isClassDeclaration(current)) {
1952
+ current = current.parent;
1953
+ }
1954
+ if (!current || !ts.isClassDeclaration(current)) {
1955
+ return false;
1956
+ }
1957
+ const classDecl = current;
1958
+ // Check if class has any member that calls inject(Injector)
1959
+ for (const member of classDecl.members) {
1960
+ if (ts.isPropertyDeclaration(member) && member.initializer) {
1961
+ if (hasInjectInjectorCall(member.initializer)) {
1962
+ return true;
1963
+ }
1964
+ }
1965
+ if (ts.isConstructorDeclaration(member)) {
1966
+ for (const param of member.parameters) {
1967
+ if (param.initializer && hasInjectInjectorCall(param.initializer)) {
1968
+ return true;
1969
+ }
1970
+ }
1971
+ }
1972
+ }
1973
+ return false;
1974
+ }
1975
+ function hasInjectInjectorCall(node) {
1976
+ let found = false;
1977
+ function visit(n) {
1978
+ if (found)
1979
+ return;
1980
+ // Look for: inject(Injector)
1981
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'inject') {
1982
+ if (n.arguments.length > 0 && ts.isIdentifier(n.arguments[0]) && n.arguments[0].text === 'Injector') {
1983
+ found = true;
1984
+ return;
1985
+ }
1986
+ }
1987
+ ts.forEachChild(n, visit);
1988
+ }
1989
+ visit(node);
1990
+ return found;
1991
+ }
1992
+ function getLineNumber(node, sourceFile) {
1993
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
1994
+ return line + 1;
1995
+ }
1996
+ function findOrCreateInjectorMembers(tree, usages) {
1997
+ const classInjectors = new Map();
1998
+ // Group usages by file and class
1999
+ const usagesByFile = new Map();
2000
+ usages.forEach((usage) => {
2001
+ // Skip usages that don't need injector (queryComputed, class-field, constructor)
2002
+ if (['queryComputed', 'class-field', 'constructor'].includes(usage.context)) {
2003
+ return;
2004
+ }
2005
+ if (!usagesByFile.has(usage.filePath)) {
2006
+ usagesByFile.set(usage.filePath, []);
2007
+ }
2008
+ usagesByFile.get(usage.filePath).push(usage);
2009
+ });
2010
+ // Process each file
2011
+ usagesByFile.forEach((fileUsages, filePath) => {
2012
+ const content = tree.read(filePath, 'utf-8');
2013
+ if (!content)
2014
+ return;
2015
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
2016
+ // Find all classes in this file that have usages
2017
+ const classesNeedingInjector = findClassesNeedingInjector(sourceFile, fileUsages);
2018
+ classesNeedingInjector.forEach((classInfo) => {
2019
+ const key = `${filePath}:${classInfo.className}`;
2020
+ classInjectors.set(key, {
2021
+ ...classInfo,
2022
+ filePath,
2023
+ });
2024
+ });
2025
+ });
2026
+ return classInjectors;
2027
+ }
2028
+ function findClassesNeedingInjector(sourceFile, usages) {
2029
+ const classes = [];
2030
+ const actualFilePath = usages[0]?.filePath || sourceFile.fileName; // Get the real file path from usages
2031
+ function visit(node) {
2032
+ if (ts.isClassDeclaration(node)) {
2033
+ const className = node.name?.text || 'UnnamedClass';
2034
+ // Check if any usage is within this class
2035
+ const usagesInClass = usages.filter((usage) => {
2036
+ return usage.position.start >= node.getStart(sourceFile) && usage.position.end <= node.getEnd();
2037
+ });
2038
+ if (usagesInClass.length === 0) {
2039
+ ts.forEachChild(node, visit);
2040
+ return;
2041
+ }
2042
+ // Check if class already has an injector member
2043
+ const existingInjector = findExistingInjectorMember(node, sourceFile);
2044
+ if (existingInjector) {
2045
+ classes.push({
2046
+ filePath: actualFilePath,
2047
+ className,
2048
+ injectorMemberName: existingInjector.name,
2049
+ needsToCreate: false,
2050
+ classStart: node.getStart(sourceFile),
2051
+ classEnd: node.getEnd(),
2052
+ });
2053
+ }
2054
+ else {
2055
+ // Need to create one
2056
+ classes.push({
2057
+ filePath: actualFilePath,
2058
+ className,
2059
+ injectorMemberName: 'injector', // Default name
2060
+ needsToCreate: true,
2061
+ classStart: node.getStart(sourceFile),
2062
+ classEnd: node.getEnd(),
2063
+ });
2064
+ }
2065
+ }
2066
+ ts.forEachChild(node, visit);
2067
+ }
2068
+ visit(sourceFile);
2069
+ return classes;
2070
+ }
2071
+ function findExistingInjectorMember(classNode, sourceFile) {
2072
+ for (const member of classNode.members) {
2073
+ if (ts.isPropertyDeclaration(member) && member.initializer) {
2074
+ // Check if it's: inject(Injector)
2075
+ if (hasInjectInjectorCall(member.initializer)) {
2076
+ const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
2077
+ if (memberName) {
2078
+ return { name: memberName, node: member };
2079
+ }
2080
+ }
2081
+ }
2082
+ }
2083
+ return undefined;
2084
+ }
2085
+ function createInjectorMembers(tree, classInjectors) {
2086
+ const filesToUpdate = new Map();
2087
+ // Group by file
2088
+ classInjectors.forEach((classInfo) => {
2089
+ if (!classInfo.needsToCreate)
2090
+ return;
2091
+ if (!filesToUpdate.has(classInfo.filePath)) {
2092
+ filesToUpdate.set(classInfo.filePath, []);
2093
+ }
2094
+ filesToUpdate.get(classInfo.filePath).push(classInfo);
2095
+ });
2096
+ // Update each file
2097
+ filesToUpdate.forEach((classes, filePath) => {
2098
+ let content = tree.read(filePath, 'utf-8');
2099
+ if (!content)
2100
+ return;
2101
+ // Add inject and Injector imports if needed
2102
+ content = ensureInjectAndInjectorImports(content);
2103
+ // Add injector member to each class (in reverse order to maintain positions)
2104
+ const sortedClasses = [...classes].sort((a, b) => b.classStart - a.classStart);
2105
+ for (const classInfo of sortedClasses) {
2106
+ content = addInjectorMemberToClass(content, classInfo);
2107
+ }
2108
+ tree.write(filePath, content);
2109
+ });
2110
+ if (filesToUpdate.size > 0) {
2111
+ console.log(`\nāœ… Added injector members to ${Array.from(filesToUpdate.values()).flat().length} classes`);
2112
+ }
2113
+ }
2114
+ function ensureInjectAndInjectorImports(content) {
2115
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2116
+ let angularCoreImport;
2117
+ ts.forEachChild(sourceFile, (node) => {
2118
+ if (ts.isImportDeclaration(node) &&
2119
+ ts.isStringLiteral(node.moduleSpecifier) &&
2120
+ node.moduleSpecifier.text === '@angular/core') {
2121
+ angularCoreImport = node;
2122
+ }
2123
+ });
2124
+ const neededImports = new Set();
2125
+ let hasInject = false;
2126
+ let hasInjector = false;
2127
+ if (angularCoreImport?.importClause?.namedBindings &&
2128
+ ts.isNamedImports(angularCoreImport.importClause.namedBindings)) {
2129
+ const namedBindings = angularCoreImport.importClause.namedBindings;
2130
+ namedBindings.elements.forEach((el) => {
2131
+ if (el.name.text === 'inject')
2132
+ hasInject = true;
2133
+ if (el.name.text === 'Injector')
2134
+ hasInjector = true;
2135
+ });
2136
+ }
2137
+ if (!hasInject)
2138
+ neededImports.add('inject');
2139
+ if (!hasInjector)
2140
+ neededImports.add('Injector');
2141
+ if (neededImports.size === 0) {
2142
+ return content; // Already has both imports
2143
+ }
2144
+ if (angularCoreImport?.importClause?.namedBindings &&
2145
+ ts.isNamedImports(angularCoreImport.importClause.namedBindings)) {
2146
+ // Add to existing import
2147
+ const namedBindings = angularCoreImport.importClause.namedBindings;
2148
+ const existingImports = namedBindings.elements.map((el) => el.name.text);
2149
+ const allImports = [...existingImports, ...Array.from(neededImports)].sort();
2150
+ const newImportStatement = `import { ${allImports.join(', ')} } from '@angular/core';`;
2151
+ const importStart = angularCoreImport.getStart(sourceFile);
2152
+ const importEnd = angularCoreImport.getEnd();
2153
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
2154
+ }
2155
+ // Find last import to add after it
2156
+ let lastImportEnd = 0;
2157
+ ts.forEachChild(sourceFile, (node) => {
2158
+ if (ts.isImportDeclaration(node)) {
2159
+ const end = node.getEnd();
2160
+ if (end > lastImportEnd) {
2161
+ lastImportEnd = end;
2162
+ }
2163
+ }
2164
+ });
2165
+ const newImports = Array.from(neededImports).sort();
2166
+ const newImportStatement = `\nimport { ${newImports.join(', ')} } from '@angular/core';`;
2167
+ if (lastImportEnd > 0) {
2168
+ return content.slice(0, lastImportEnd) + newImportStatement + content.slice(lastImportEnd);
2169
+ }
2170
+ return `import { ${newImports.join(', ')} } from '@angular/core';\n\n${content}`;
2171
+ }
2172
+ function addInjectorMemberToClass(content, classInfo) {
2173
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2174
+ // Find the class
2175
+ let targetClass;
2176
+ function visit(node) {
2177
+ if (ts.isClassDeclaration(node)) {
2178
+ const className = node.name?.text;
2179
+ if (className === classInfo.className) {
2180
+ targetClass = node;
2181
+ }
2182
+ }
2183
+ ts.forEachChild(node, visit);
2184
+ }
2185
+ visit(sourceFile);
2186
+ if (!targetClass)
2187
+ return content;
2188
+ // Find where to insert the injector member (after the opening brace)
2189
+ const classBodyStart = targetClass.members.length > 0 ? targetClass.members[0].getStart(sourceFile) : targetClass.getEnd() - 1; // Before closing brace
2190
+ const indentation = getIndentation(content, classBodyStart);
2191
+ const injectorMember = `${indentation}private ${classInfo.injectorMemberName} = inject(Injector);\n\n`;
2192
+ return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
2193
+ }
2194
+ function getIndentation(content, position) {
2195
+ // Walk backwards to find the start of the line
2196
+ let lineStart = position;
2197
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
2198
+ lineStart--;
2199
+ }
2200
+ // Count spaces/tabs
2201
+ let indentation = '';
2202
+ for (let i = lineStart; i < position && /\s/.test(content[i]); i++) {
2203
+ indentation += content[i];
2204
+ }
2205
+ return indentation || ' '; // Default to 2 spaces if no indentation found
2206
+ }
2207
+ //#endregion
2208
+ //#region Step 3: Transform legacy query creator prepare() calls to add injector
2209
+ function transformLegacyCreatorPrepareCall(tree, classInjectors, pollingInfo) {
2210
+ const updatedFiles = [];
2211
+ // Get all files that have classes with injector OR have standalone function usages
2212
+ const filesByPath = new Map();
2213
+ classInjectors.forEach((classInfo) => {
2214
+ if (!filesByPath.has(classInfo.filePath)) {
2215
+ filesByPath.set(classInfo.filePath, []);
2216
+ }
2217
+ filesByPath.get(classInfo.filePath).push(classInfo);
2218
+ });
2219
+ // Also check files with standalone function usages
2220
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
2221
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
2222
+ return;
2223
+ if (filesByPath.has(filePath))
2224
+ return; // Already processing
2225
+ const content = tree.read(filePath, 'utf-8');
2226
+ if (!content)
2227
+ return;
2228
+ // Only analyze files that have .prepare() calls
2229
+ if (!content.includes('.prepare('))
2230
+ return;
2231
+ const usages = findLegacyCreatorUsages(content, filePath);
2232
+ const functionUsages = usages.filter((u) => u.context === 'function');
2233
+ if (functionUsages.length > 0) {
2234
+ filesByPath.set(filePath, []); // Empty array means standalone functions only
2235
+ }
2236
+ });
2237
+ filesByPath.forEach((classes, filePath) => {
2238
+ let content = tree.read(filePath, 'utf-8');
2239
+ if (!content)
2240
+ return;
2241
+ // Ensure imports are added (for both classes and functions)
2242
+ content = ensureInjectAndInjectorImports(content);
2243
+ // Transform prepare calls for each class and functions
2244
+ const newContent = transformPrepareCallsInFile(content, classes, pollingInfo);
2245
+ if (newContent !== content) {
2246
+ tree.write(filePath, newContent);
2247
+ updatedFiles.push(filePath);
2248
+ }
2249
+ });
2250
+ if (updatedFiles.length > 0) {
2251
+ console.log(`\nāœ… Transformed legacy query creator prepare() calls in ${updatedFiles.length} files`);
2252
+ }
2253
+ }
2254
+ function transformPrepareCallsInFile(content, classes, pollingInfo) {
2255
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2256
+ const replacements = [];
2257
+ // Find all legacy creator names
2258
+ const legacyCreatorNames = findLegacyCreatorNames(sourceFile, content);
2259
+ if (legacyCreatorNames.size === 0)
2260
+ return content;
2261
+ // Build a map of class NAME to their injector member name
2262
+ const classNameToInjector = new Map();
2263
+ classes.forEach((classInfo) => {
2264
+ classNameToInjector.set(classInfo.className, classInfo.injectorMemberName);
2265
+ });
2266
+ // Track standalone functions that need injector by their position
2267
+ const functionsNeedingInjector = new Map();
2268
+ function visit(node) {
2269
+ // Find: legacyCreator.prepare(...)
2270
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
2271
+ const propAccess = node.expression;
2272
+ if (ts.isIdentifier(propAccess.name) && propAccess.name.text === 'prepare') {
2273
+ let creatorName;
2274
+ if (ts.isIdentifier(propAccess.expression)) {
2275
+ creatorName = propAccess.expression.text;
2276
+ }
2277
+ if (!creatorName || !legacyCreatorNames.has(creatorName)) {
2278
+ ts.forEachChild(node, visit);
2279
+ return;
2280
+ }
2281
+ // Check if this is in a context that needs injector
2282
+ const context = determineUsageContext(node, sourceFile);
2283
+ if (['queryComputed', 'constructor', 'class-field'].includes(context)) {
2284
+ // Safe contexts that don't need injector or destroyOnResponse
2285
+ ts.forEachChild(node, visit);
2286
+ return;
2287
+ }
2288
+ // Find the containing class or function
2289
+ const containingClass = findContainingClass(node, sourceFile);
2290
+ const containingFunction = findContainingStandaloneFunction(node, sourceFile);
2291
+ if (containingClass) {
2292
+ // Handle class method (NOT class fields - they were filtered above)
2293
+ const className = containingClass.name?.text;
2294
+ if (!className) {
2295
+ ts.forEachChild(node, visit);
2296
+ return;
2297
+ }
2298
+ const injectorMember = classNameToInjector.get(className);
2299
+ if (!injectorMember) {
2300
+ ts.forEachChild(node, visit);
2301
+ return;
2302
+ }
2303
+ // Transform the prepare call
2304
+ const transformedCall = transformSinglePrepareCall(node, `this.${injectorMember}`, sourceFile, pollingInfo);
2305
+ if (transformedCall) {
2306
+ replacements.push({
2307
+ start: node.getStart(sourceFile),
2308
+ end: node.getEnd(),
2309
+ replacement: transformedCall,
2310
+ });
2311
+ }
2312
+ }
2313
+ else if (containingFunction) {
2314
+ // Handle standalone function - find the outermost function with inject()
2315
+ const functionWithInject = findOutermostFunctionWithInject(containingFunction, sourceFile);
2316
+ if (functionWithInject) {
2317
+ const funcPosition = functionWithInject.getStart(sourceFile);
2318
+ // Check if we haven't already processed this function
2319
+ if (!functionsNeedingInjector.has(funcPosition)) {
2320
+ // Check if the function already has an injector parameter
2321
+ const existingInjectorParam = findInjectorParameter(functionWithInject);
2322
+ if (existingInjectorParam) {
2323
+ // Use the existing parameter name
2324
+ functionsNeedingInjector.set(funcPosition, {
2325
+ injectorName: existingInjectorParam,
2326
+ bodyStart: -1, // No declaration needed
2327
+ needsDeclaration: false,
2328
+ });
2329
+ }
2330
+ else {
2331
+ // Need to declare injector
2332
+ let bodyStart;
2333
+ if ((ts.isFunctionDeclaration(functionWithInject) || ts.isFunctionExpression(functionWithInject)) &&
2334
+ functionWithInject.body &&
2335
+ ts.isBlock(functionWithInject.body)) {
2336
+ if (functionWithInject.body.statements.length > 0) {
2337
+ bodyStart = functionWithInject.body.statements[0].getStart(sourceFile);
2338
+ }
2339
+ else {
2340
+ bodyStart = functionWithInject.body.getStart(sourceFile) + 1;
2341
+ }
2342
+ }
2343
+ else if (ts.isArrowFunction(functionWithInject) &&
2344
+ functionWithInject.body &&
2345
+ ts.isBlock(functionWithInject.body)) {
2346
+ if (functionWithInject.body.statements.length > 0) {
2347
+ bodyStart = functionWithInject.body.statements[0].getStart(sourceFile);
2348
+ }
2349
+ else {
2350
+ bodyStart = functionWithInject.body.getStart(sourceFile) + 1;
2351
+ }
2352
+ }
2353
+ if (bodyStart !== undefined) {
2354
+ functionsNeedingInjector.set(funcPosition, {
2355
+ injectorName: 'injector',
2356
+ bodyStart,
2357
+ needsDeclaration: true,
2358
+ });
2359
+ }
2360
+ }
2361
+ }
2362
+ const injectorInfo = functionsNeedingInjector.get(funcPosition);
2363
+ const injectorName = injectorInfo?.injectorName || 'injector';
2364
+ // Transform the prepare call
2365
+ const transformedCall = transformSinglePrepareCall(node, injectorName, sourceFile, pollingInfo);
2366
+ if (transformedCall) {
2367
+ replacements.push({
2368
+ start: node.getStart(sourceFile),
2369
+ end: node.getEnd(),
2370
+ replacement: transformedCall,
2371
+ });
2372
+ }
2373
+ }
2374
+ }
2375
+ }
2376
+ }
2377
+ ts.forEachChild(node, visit);
2378
+ }
2379
+ visit(sourceFile);
2380
+ // Apply injector declarations first (in reverse order) - only for functions that need them
2381
+ let result = content;
2382
+ const injectorDeclarations = [];
2383
+ functionsNeedingInjector.forEach(({ injectorName, bodyStart, needsDeclaration }) => {
2384
+ if (needsDeclaration && bodyStart > 0) {
2385
+ const indentation = getIndentation(content, bodyStart);
2386
+ const injectorDecl = `${indentation}const ${injectorName} = inject(Injector);\n`;
2387
+ injectorDeclarations.push({
2388
+ position: bodyStart,
2389
+ text: injectorDecl,
2390
+ });
2391
+ }
2392
+ });
2393
+ injectorDeclarations.sort((a, b) => b.position - a.position);
2394
+ for (const { position, text } of injectorDeclarations) {
2395
+ result = result.slice(0, position) + text + result.slice(position);
2396
+ }
2397
+ // Then apply prepare call replacements (need to adjust positions for injector insertions)
2398
+ const sortedReplacements = [...replacements].sort((a, b) => b.start - a.start);
2399
+ for (const { start, end, replacement } of sortedReplacements) {
2400
+ // Calculate position adjustment from injector declarations that came before this replacement
2401
+ let adjustment = 0;
2402
+ for (const decl of injectorDeclarations) {
2403
+ if (decl.position <= start) {
2404
+ adjustment += decl.text.length;
2405
+ }
2406
+ }
2407
+ result = result.slice(0, start + adjustment) + replacement + result.slice(end + adjustment);
2408
+ }
2409
+ return result;
2410
+ }
2411
+ function findInjectorParameter(func) {
2412
+ if (!func.parameters || func.parameters.length === 0) {
2413
+ return undefined;
2414
+ }
2415
+ // Look for a parameter named 'injector' or with a type containing 'Injector'
2416
+ for (const param of func.parameters) {
2417
+ if (ts.isIdentifier(param.name)) {
2418
+ const paramName = param.name.text;
2419
+ // Check if parameter is named 'injector'
2420
+ if (paramName.toLowerCase() === 'injector') {
2421
+ return paramName;
2422
+ }
2423
+ // Check if parameter type contains 'Injector'
2424
+ if (param.type) {
2425
+ const typeText = param.type.getText();
2426
+ if (typeText.includes('Injector')) {
2427
+ return paramName;
2428
+ }
2429
+ }
2430
+ }
2431
+ }
2432
+ return undefined;
2433
+ }
2434
+ function findOutermostFunctionWithInject(startFunction, sourceFile) {
2435
+ let current = startFunction;
2436
+ let outermostWithInject;
2437
+ // Walk up the tree looking for functions with inject() calls
2438
+ while (current) {
2439
+ // Stop if we hit a class - we don't want to go outside the function scope
2440
+ if (ts.isClassDeclaration(current)) {
2441
+ break;
2442
+ }
2443
+ // Check if this is a function with inject()
2444
+ if ((ts.isFunctionDeclaration(current) || ts.isArrowFunction(current) || ts.isFunctionExpression(current)) &&
2445
+ containsInjectCall(current)) {
2446
+ outermostWithInject = current;
2447
+ }
2448
+ current = current.parent;
2449
+ }
2450
+ return outermostWithInject;
2451
+ }
2452
+ function findContainingStandaloneFunction(node, sourceFile) {
2453
+ let current = node;
2454
+ while (current) {
2455
+ // Check if we hit a class first - then it's not a standalone function
2456
+ if (ts.isClassDeclaration(current)) {
2457
+ return undefined;
2458
+ }
2459
+ // Check for function declarations, arrow functions, or function expressions
2460
+ if (ts.isFunctionDeclaration(current) || ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
2461
+ return current;
2462
+ }
2463
+ current = current.parent;
2464
+ }
2465
+ return undefined;
2466
+ }
2467
+ function findOrCreateInjectorInFunction(func, sourceFile, functionsNeedingInjector) {
2468
+ // Check if function already has an injector variable
2469
+ const existingInjector = findExistingInjectorVariable(func, sourceFile);
2470
+ if (existingInjector) {
2471
+ return existingInjector;
2472
+ }
2473
+ // Check if we already marked this function for injector creation
2474
+ const existing = functionsNeedingInjector.get(func);
2475
+ if (existing) {
2476
+ return existing;
2477
+ }
2478
+ // Mark for creation
2479
+ const injectorName = 'injector';
2480
+ functionsNeedingInjector.set(func, injectorName);
2481
+ return injectorName;
2482
+ }
2483
+ function findExistingInjectorVariable(func, sourceFile) {
2484
+ const body = func.body;
2485
+ if (!body || !ts.isBlock(body)) {
2486
+ return undefined;
2487
+ }
2488
+ for (const statement of body.statements) {
2489
+ if (ts.isVariableStatement(statement)) {
2490
+ for (const declaration of statement.declarationList.declarations) {
2491
+ if (ts.isIdentifier(declaration.name) &&
2492
+ declaration.initializer &&
2493
+ hasInjectInjectorCall(declaration.initializer)) {
2494
+ return declaration.name.text;
2495
+ }
2496
+ }
2497
+ }
2498
+ }
2499
+ return undefined;
2500
+ }
2501
+ function findContainingClass(node, sourceFile) {
2502
+ let current = node;
2503
+ while (current) {
2504
+ if (ts.isClassDeclaration(current)) {
2505
+ return current;
2506
+ }
2507
+ current = current.parent;
2508
+ }
2509
+ return undefined;
2510
+ }
2511
+ function transformSinglePrepareCall(callNode, injectorReference, sourceFile, pollingInfo) {
2512
+ const propAccess = callNode.expression;
2513
+ const creatorName = propAccess.expression.text;
2514
+ // Get existing arguments
2515
+ const existingArgs = callNode.arguments.length > 0 ? callNode.arguments[0] : undefined;
2516
+ // Build new argument object
2517
+ const newArgProperties = [];
2518
+ let existingConfig;
2519
+ let needsSpread = false;
2520
+ let spreadExpression;
2521
+ // If there's an existing argument and it's an object literal, extract its properties
2522
+ if (existingArgs) {
2523
+ if (ts.isObjectLiteralExpression(existingArgs)) {
2524
+ existingArgs.properties.forEach((prop) => {
2525
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
2526
+ const propName = prop.name.text;
2527
+ // Handle config specially - we need to merge it
2528
+ if (propName === 'config') {
2529
+ existingConfig = prop;
2530
+ return; // Don't add it yet
2531
+ }
2532
+ const propValue = prop.initializer.getText(sourceFile);
2533
+ newArgProperties.push(`${propName}: ${propValue}`);
2534
+ }
2535
+ else if (ts.isShorthandPropertyAssignment(prop)) {
2536
+ const propName = prop.name.text;
2537
+ newArgProperties.push(propName);
2538
+ }
2539
+ else if (ts.isSpreadAssignment(prop)) {
2540
+ // Existing spread - preserve it
2541
+ spreadExpression = prop.expression.getText(sourceFile);
2542
+ needsSpread = true;
2543
+ }
2544
+ });
2545
+ }
2546
+ else {
2547
+ // If it's not an object literal (e.g., a variable or expression), we need to spread it
2548
+ spreadExpression = existingArgs.getText(sourceFile);
2549
+ needsSpread = true;
2550
+ }
2551
+ }
2552
+ // Add injector
2553
+ newArgProperties.push(`injector: ${injectorReference}`);
2554
+ // Conditionally add or merge config
2555
+ const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
2556
+ const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo);
2557
+ if (existingConfig) {
2558
+ // Merge with existing config
2559
+ const existingConfigObj = existingConfig.initializer;
2560
+ if (ts.isObjectLiteralExpression(existingConfigObj)) {
2561
+ const configProps = [];
2562
+ // Add existing config properties
2563
+ existingConfigObj.properties.forEach((prop) => {
2564
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
2565
+ const propName = prop.name.text;
2566
+ const propValue = prop.initializer.getText(sourceFile);
2567
+ configProps.push(`${propName}: ${propValue}`);
2568
+ }
2569
+ else if (ts.isShorthandPropertyAssignment(prop)) {
2570
+ const propName = prop.name.text;
2571
+ configProps.push(propName);
2572
+ }
2573
+ });
2574
+ // Add destroyOnResponse if needed
2575
+ if (addDestroyOnResponse) {
2576
+ configProps.push('destroyOnResponse: true');
2577
+ }
2578
+ if (configProps.length > 2) {
2579
+ newArgProperties.push(`config: {\n ${configProps.join(',\n ')}\n }`);
2580
+ }
2581
+ else {
2582
+ newArgProperties.push(`config: { ${configProps.join(', ')} }`);
2583
+ }
2584
+ }
2585
+ else {
2586
+ // Config is not an object literal (e.g., a variable) - can't merge
2587
+ // Just keep the existing config
2588
+ newArgProperties.push(`config: ${existingConfigObj.getText(sourceFile)}`);
2589
+ }
2590
+ }
2591
+ else if (addDestroyOnResponse) {
2592
+ // No existing config - add destroyOnResponse
2593
+ newArgProperties.push(`config: { destroyOnResponse: true }`);
2594
+ }
2595
+ // Format based on whether we need to spread and number of properties
2596
+ let argsString;
2597
+ if (needsSpread && spreadExpression) {
2598
+ // Need to spread the existing argument
2599
+ if (newArgProperties.length <= 2 && !newArgProperties.some((p) => p.includes('\n'))) {
2600
+ // Single line for short objects
2601
+ argsString = `{ ...${spreadExpression}, ${newArgProperties.join(', ')} }`;
2602
+ }
2603
+ else {
2604
+ // Multi-line for longer objects
2605
+ argsString = `{\n ...${spreadExpression},\n ${newArgProperties.join(',\n ')}\n}`;
2606
+ }
2607
+ }
2608
+ else {
2609
+ // No spread needed
2610
+ if (newArgProperties.length <= 2 && !newArgProperties.some((p) => p.includes('\n'))) {
2611
+ // Single line for short objects
2612
+ argsString = `{ ${newArgProperties.join(', ')} }`;
2613
+ }
2614
+ else {
2615
+ // Multi-line for longer objects
2616
+ argsString = `{\n ${newArgProperties.join(',\n ')}\n}`;
2617
+ }
2618
+ }
2619
+ return `${creatorName}.prepare(${argsString})`;
2620
+ }
2621
+ function findQueryVariableNameForPrepareCall(prepareCallNode, sourceFile) {
2622
+ // Walk up to find if this prepare call is assigned to a variable
2623
+ // e.g., const query = legacyGetUsers.prepare()
2624
+ let parent = prepareCallNode.parent;
2625
+ while (parent) {
2626
+ // Check if assigned to a variable
2627
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
2628
+ return parent.name.text;
2629
+ }
2630
+ // Check if assigned to a class property
2631
+ if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
2632
+ return parent.name.text;
2633
+ }
2634
+ // Check if it's a return statement - look for the method/function name
2635
+ if (ts.isReturnStatement(parent)) {
2636
+ const containingFunction = findContainingMethod(parent, sourceFile);
2637
+ if (containingFunction) {
2638
+ return containingFunction;
2639
+ }
2640
+ }
2641
+ parent = parent.parent;
2642
+ }
2643
+ return undefined;
2644
+ }
2645
+ function findContainingMethod(node, sourceFile) {
2646
+ let current = node;
2647
+ while (current) {
2648
+ if (ts.isMethodDeclaration(current) && ts.isIdentifier(current.name)) {
2649
+ return current.name.text;
2650
+ }
2651
+ if (ts.isFunctionDeclaration(current) && current.name && ts.isIdentifier(current.name)) {
2652
+ return current.name.text;
2653
+ }
2654
+ if (ts.isArrowFunction(current)) {
2655
+ // Check if it's assigned to a variable
2656
+ const parent = current.parent;
2657
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
2658
+ return parent.name.text;
2659
+ }
2660
+ }
2661
+ current = current.parent;
2662
+ }
2663
+ return undefined;
2664
+ }
2665
+ function reportManualReviewLocations(tree, allUsages) {
2666
+ const needsReview = [];
2667
+ allUsages.forEach((usage) => {
2668
+ // Check if this usage needs manual review
2669
+ if (usage.context === 'function') {
2670
+ // For function context, we need to check if it was in a function WITHOUT inject()
2671
+ // This means it should have been flagged for manual review during transformation
2672
+ // We can check if the function had inject by looking at the original file
2673
+ // (before transformation) since we need to report the original line number anyway
2674
+ const originalContent = tree.read(usage.filePath, 'utf-8');
2675
+ if (!originalContent)
2676
+ return;
2677
+ // Parse the CURRENT file to check if inject was added
2678
+ // But use the ORIGINAL line number from usage
2679
+ const sourceFile = ts.createSourceFile(usage.filePath, originalContent, ts.ScriptTarget.Latest, true);
2680
+ // Find any function that contains a .prepare() call around the original position
2681
+ let foundFunctionWithoutInject = false;
2682
+ function checkFunctions(node) {
2683
+ // Look for functions that might contain our usage
2684
+ if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
2685
+ const funcStart = node.getStart(sourceFile);
2686
+ const funcEnd = node.getEnd();
2687
+ // Check if our usage position is within this function
2688
+ if (usage.position.start >= funcStart && usage.position.end <= funcEnd) {
2689
+ // Check if this function or any parent function has inject()
2690
+ if (!containsInjectCall(node) && !findOutermostFunctionWithInject(node, sourceFile)) {
2691
+ foundFunctionWithoutInject = true;
2692
+ }
2693
+ }
2694
+ }
2695
+ ts.forEachChild(node, checkFunctions);
2696
+ }
2697
+ checkFunctions(sourceFile);
2698
+ if (foundFunctionWithoutInject) {
2699
+ needsReview.push({
2700
+ filePath: usage.filePath,
2701
+ line: usage.line, // Use the original line number from Step 1
2702
+ creatorName: usage.creatorName,
2703
+ reason: 'Used in standalone function without inject() - may need manual injector passing',
2704
+ });
2705
+ }
2706
+ }
2707
+ else if (usage.context === 'unknown') {
2708
+ needsReview.push({
2709
+ filePath: usage.filePath,
2710
+ line: usage.line, // Use the original line number from Step 1
2711
+ creatorName: usage.creatorName,
2712
+ reason: 'Could not determine execution context - please verify manually',
2713
+ });
2714
+ }
2715
+ });
2716
+ if (needsReview.length > 0) {
2717
+ console.warn(`\nāš ļø Found ${needsReview.length} locations that may need manual review:`);
2718
+ // Group by file for cleaner output
2719
+ const byFile = new Map();
2720
+ needsReview.forEach((location) => {
2721
+ if (!byFile.has(location.filePath)) {
2722
+ byFile.set(location.filePath, []);
2723
+ }
2724
+ byFile.get(location.filePath).push(location);
2725
+ });
2726
+ byFile.forEach((locations, filePath) => {
2727
+ console.warn(`\n šŸ“„ ${filePath}:`);
2728
+ locations.forEach((location) => {
2729
+ console.warn(` Line ${location.line}: ${location.creatorName}`);
2730
+ console.warn(` → ${location.reason}`);
2731
+ });
2732
+ });
2733
+ console.warn('\nšŸ’” Solve these warnings by:');
2734
+ console.warn(' - [HIGHLY RECOMMENDED] Passing an injector as a parameter to the prepare method call alongside a config: { destroyOnResponse: true }');
2735
+ console.warn(' - Using queryComputed() if appropriate');
2736
+ console.warn(' - Putting the prepare call into an injection context');
2737
+ }
2738
+ }
2739
+ //#endregion
2740
+ //#region Helper functions for inject detection
2741
+ function containsInjectCall(node) {
2742
+ let hasInject = false;
2743
+ function visit(n) {
2744
+ if (hasInject)
2745
+ return;
2746
+ // Check for direct inject() call
2747
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'inject') {
2748
+ hasInject = true;
2749
+ return;
2750
+ }
2751
+ // Check for any function call that starts with "inject"
2752
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
2753
+ if (n.expression.text.startsWith('inject')) {
2754
+ hasInject = true;
2755
+ return;
2756
+ }
2757
+ }
2758
+ ts.forEachChild(n, visit);
2759
+ }
2760
+ visit(node);
2761
+ return hasInject;
2762
+ }
2763
+ //#endregion
2764
+ //#region Migrate empty .prepare() calls to .prepare({})
2765
+ function migrateEmptyPrepareCalls(tree) {
2766
+ const updatedFiles = [];
2767
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
2768
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
2769
+ return;
2770
+ const content = tree.read(filePath, 'utf-8');
2771
+ if (!content)
2772
+ return;
2773
+ // Only analyze files that have .prepare() calls
2774
+ if (!content.includes('.prepare()'))
2775
+ return;
2776
+ const newContent = transformEmptyPrepareCalls(content);
2777
+ if (newContent !== content) {
2778
+ tree.write(filePath, newContent);
2779
+ updatedFiles.push(filePath);
2780
+ }
2781
+ });
2782
+ if (updatedFiles.length > 0) {
2783
+ console.log(`\nāœ… Migrated empty .prepare() calls to .prepare({}) in ${updatedFiles.length} files`);
2784
+ }
2785
+ }
2786
+ function transformEmptyPrepareCalls(content) {
2787
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2788
+ const replacements = [];
2789
+ function visit(node) {
2790
+ // Find: something.prepare()
2791
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
2792
+ const propAccess = node.expression;
2793
+ if (ts.isIdentifier(propAccess.name) && propAccess.name.text === 'prepare') {
2794
+ // Check if it has no arguments
2795
+ if (node.arguments.length === 0) {
2796
+ // Get the full text of the call: "something.prepare()"
2797
+ const callStart = node.getStart(sourceFile);
2798
+ const callEnd = node.getEnd();
2799
+ const callText = content.slice(callStart, callEnd);
2800
+ // Replace with: "something.prepare({})"
2801
+ const replacement = callText.replace(/\.prepare\(\)/, '.prepare({})');
2802
+ replacements.push({
2803
+ start: callStart,
2804
+ end: callEnd,
2805
+ replacement,
2806
+ });
2807
+ }
2808
+ }
2809
+ }
2810
+ ts.forEachChild(node, visit);
2811
+ }
2812
+ visit(sourceFile);
2813
+ // Apply replacements in reverse order
2814
+ let result = content;
2815
+ replacements.sort((a, b) => b.start - a.start);
2816
+ for (const { start, end, replacement } of replacements) {
2817
+ result = result.slice(0, start) + replacement + result.slice(end);
2818
+ }
2819
+ return result;
2820
+ }
2821
+ function detectPollingUsage(tree) {
2822
+ const pollingInfo = new Map();
2823
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
2824
+ if (filePath.endsWith('.spec.ts'))
2825
+ return;
2826
+ const content = tree.read(filePath, 'utf-8');
2827
+ if (!content)
2828
+ return;
2829
+ // Check TypeScript files
2830
+ if (filePath.endsWith('.ts')) {
2831
+ detectPollingInTypeScript(content, filePath, pollingInfo);
2832
+ }
2833
+ // Check HTML templates
2834
+ if (filePath.endsWith('.html')) {
2835
+ detectPollingInTemplate(content, filePath, pollingInfo);
2836
+ }
2837
+ });
2838
+ return pollingInfo;
2839
+ }
2840
+ function detectPollingInTypeScript(content, filePath, pollingInfo) {
2841
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
2842
+ function visit(node) {
2843
+ // Find .poll() or .stopPolling() calls
2844
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
2845
+ const propAccess = node.expression;
2846
+ const methodName = propAccess.name;
2847
+ if (ts.isIdentifier(methodName) && (methodName.text === 'poll' || methodName.text === 'stopPolling')) {
2848
+ // Walk back to find the root variable name
2849
+ const queryVariableName = getQueryVariableName(propAccess.expression, sourceFile);
2850
+ if (queryVariableName) {
2851
+ const existing = pollingInfo.get(queryVariableName);
2852
+ if (existing) {
2853
+ existing.hasPolling = true;
2854
+ existing.locations.push(`${filePath}:${getLineNumber(node, sourceFile)} (.${methodName.text})`);
2855
+ }
2856
+ else {
2857
+ pollingInfo.set(queryVariableName, {
2858
+ queryVariableName,
2859
+ hasPolling: true,
2860
+ locations: [`${filePath}:${getLineNumber(node, sourceFile)} (.${methodName.text})`],
2861
+ });
2862
+ }
2863
+ }
2864
+ }
2865
+ }
2866
+ ts.forEachChild(node, visit);
2867
+ }
2868
+ visit(sourceFile);
2869
+ }
2870
+ function getQueryVariableName(expr, sourceFile) {
2871
+ // Walk back through the chain to find the root variable
2872
+ let current = expr;
2873
+ while (true) {
2874
+ if (ts.isIdentifier(current)) {
2875
+ return current.text;
2876
+ }
2877
+ if (ts.isPropertyAccessExpression(current)) {
2878
+ current = current.expression;
2879
+ continue;
2880
+ }
2881
+ if (ts.isCallExpression(current)) {
2882
+ // For chained calls like query.prepare().execute().poll()
2883
+ // We want to get the root query variable
2884
+ current = current.expression;
2885
+ continue;
2886
+ }
2887
+ // Can't determine the root variable
2888
+ return undefined;
2889
+ }
2890
+ }
2891
+ function detectPollingInTemplate(content, filePath, pollingInfo) {
2892
+ // Use regex to find .poll() or .stopPolling() in templates
2893
+ // This is not perfect but catches most cases
2894
+ const pollRegex = /(\w+)\.poll\(\)/g;
2895
+ const stopPollingRegex = /(\w+)\.stopPolling\(\)/g;
2896
+ let match;
2897
+ // Find .poll() calls
2898
+ while ((match = pollRegex.exec(content)) !== null) {
2899
+ const queryVariableName = match[1];
2900
+ const lineNumber = getLineNumberFromPosition(content, match.index);
2901
+ const existing = pollingInfo.get(queryVariableName);
2902
+ if (existing) {
2903
+ existing.hasPolling = true;
2904
+ existing.locations.push(`${filePath}:${lineNumber} (.poll() in template)`);
2905
+ }
2906
+ else {
2907
+ pollingInfo.set(queryVariableName, {
2908
+ queryVariableName,
2909
+ hasPolling: true,
2910
+ locations: [`${filePath}:${lineNumber} (.poll() in template)`],
2911
+ });
2912
+ }
2913
+ }
2914
+ // Find .stopPolling() calls
2915
+ while ((match = stopPollingRegex.exec(content)) !== null) {
2916
+ const queryVariableName = match[1];
2917
+ const lineNumber = getLineNumberFromPosition(content, match.index);
2918
+ const existing = pollingInfo.get(queryVariableName);
2919
+ if (existing) {
2920
+ existing.hasPolling = true;
2921
+ existing.locations.push(`${filePath}:${lineNumber} (.stopPolling() in template)`);
2922
+ }
2923
+ else {
2924
+ pollingInfo.set(queryVariableName, {
2925
+ queryVariableName,
2926
+ hasPolling: true,
2927
+ locations: [`${filePath}:${lineNumber} (.stopPolling() in template)`],
2928
+ });
2929
+ }
2930
+ }
2931
+ }
2932
+ function getLineNumberFromPosition(content, position) {
2933
+ const lines = content.slice(0, position).split('\n');
2934
+ return lines.length;
2935
+ }
2936
+ function shouldAddDestroyOnResponse(queryVariableName, pollingInfo) {
2937
+ if (!queryVariableName) {
2938
+ // Can't determine - be conservative and don't add destroyOnResponse
2939
+ return false;
2940
+ }
2941
+ const info = pollingInfo.get(queryVariableName);
2942
+ // If we found polling usage, don't add destroyOnResponse
2943
+ if (info && info.hasPolling) {
2944
+ return false;
2945
+ }
2946
+ // No polling detected - safe to add destroyOnResponse
2947
+ return true;
2948
+ }
1743
2949
  //#endregion
2950
+ // TODO:
2951
+ // - Only add destroyOnResponse if we are 00% sure there is no .poll / stopPolling calls on it
1744
2952
  //# sourceMappingURL=migration.js.map