@azure-net/cli 6.0.0-next.2 → 6.0.0-next.3

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/README.md CHANGED
@@ -60,7 +60,7 @@ classes plus accessors. The migrator preserves lexical state through a private
60
60
  state object and rewrites named consumer imports/re-exports to the accessor,
61
61
  retaining their local aliases. Async-helper factories are replaced by direct
62
62
  function references, preserving explicit custom error types. Unsupported custom
63
- injections and dynamic consumer imports are listed in the migration report for
63
+ unresolved injections and dynamic consumer imports are listed in the migration report for
64
64
  manual review. Original files remain in the migration snapshot.
65
65
 
66
66
  `scoped` means request-cached on the server and new-per-call in the browser, not
@@ -187,3 +187,15 @@ it writes `state.currLocale` and dictionary snapshots automatically. Call
187
187
  in useClient. Templates include these integration hints. In components use
188
188
  `t(key)` and `manager.locale`, not `$t` / `$locale`. No separate translations
189
189
  package, exportState or restoreState is required.
190
+
191
+ ### Presenter and translation API migration
192
+
193
+ - Presenter factory imports are resolved through local modules and barrels, including aliases.
194
+ - Statically declared custom injections (for example notification/modal accessors) become constructor arguments. The factory module retains a plain exported dependency object, not a createPresenterFactory call.
195
+ - Async helpers are imported from @azure-net/kit/delivery; factory-specific error types are preserved with type-only specialization. Custom overrides are injected rather than replaced with kit helpers.
196
+ - Closures, mutable private variables and detached callbacks are retained behind class accessors. Registration uses RegisterProvider with scoped lifecycle. Consumers and re-exports are updated.
197
+ - Opaque spreads, getter-based injection definitions, local per-presenter injection overrides, explicit factory result types and unsupported return shapes require manual review. No application configuration is executed to infer their meaning.
198
+ - A single identifiable translation manager is connected to useTranslations before the existing bootstrap chain. Known zero-argument legacy preload/initialization calls become resolved promises so existing helper consumers remain compatible. Custom locale resolvers and ambiguous managers are not rewritten automatically.
199
+ - Existing bootstrap dependencies/Container usage, stores and other removed kit APIs are not converted into new application-specific state architecture by this migration. Check the resulting project and migration report before publishing.
200
+
201
+ Run azure-net migrate --dry-run first. Migration also works on presenters left behind in an already migrated Contexts/Presentation/Platform structure.
@@ -0,0 +1,9 @@
1
+ export interface FactoryDefinition {
2
+ name: string;
3
+ exportName: string;
4
+ keys: Set<string>;
5
+ helpers: Set<string>;
6
+ }
7
+ /** Only evaluate syntax. Never import or execute the application's factory/configuration. */
8
+ export declare function inspectFactories(input: string, file: string): FactoryDefinition[];
9
+ export declare function replaceFactory(input: string, file: string, definition: FactoryDefinition, keepLegacy: boolean): string;
@@ -1,6 +1,12 @@
1
1
  export declare function migrateAsyncHelpers(input: string, file: string): string;
2
- /** Upgrade only recognized factories; arbitrary injected dependencies require a human decision. */
3
- export declare function migratePresenterSource(input: string, file: string, factories: Set<string>): {
2
+ export interface PresenterDependencies {
3
+ source: string;
4
+ exportName: string;
5
+ keys: Set<string>;
6
+ helpers: Set<string>;
7
+ }
8
+ /** Preserve lexical behavior while moving instance creation and dependencies to RegisterProvider. */
9
+ export declare function migratePresenterSource(input: string, file: string, factories: Set<string>, dependencies?: Map<string, PresenterDependencies>): {
4
10
  content: string;
5
11
  converted: Map<string, string>;
6
12
  issues: string[];
@@ -0,0 +1,8 @@
1
+ /** A conservative migration of known translation wiring, never a project-wide textual replacement. */
2
+ export declare function migrateTranslationsSources(sources: Map<string, string>, resolve: (from: string, specifier: string) => string | undefined): {
3
+ updates: Map<string, string>;
4
+ issues: {
5
+ file: string;
6
+ reason: string;
7
+ }[];
8
+ };
package/package.json CHANGED
@@ -1,49 +1,47 @@
1
1
  {
2
- "name": "@azure-net/cli",
3
- "version": "6.0.0-next.2",
4
- "type": "module",
5
- "bin": {
6
- "azure-net": "bin/azure-net.js"
7
- },
8
- "license": "MIT",
9
- "author": "Pixel1917",
10
- "publishConfig": {
11
- "access": "public",
12
- "registry": "https://registry.npmjs.org/"
13
- },
14
- "peerDependencies": {
15
- "@azure-net/kit": ">=6.0.0-next.1"
16
- },
17
- "dependencies": {
18
- "commander": "^11",
19
- "prompts": "^2",
20
- "typescript": "^5",
21
- "prettier": "^3.6.2"
22
- },
23
- "devDependencies": {
24
- "@eslint/js": "^9.31.0",
25
- "@types/node": "^24.0.3",
26
- "@types/prompts": "^2.4.9",
27
- "env-cmd": "^11.0.0",
28
- "eslint": "^9.31.0",
29
- "typescript-eslint": "^8.36.0"
30
- },
31
- "scripts": {
32
- "build": "node scripts/clean-build.mjs && tsc -p tsconfig.json && node scripts/sync-build.mjs",
33
- "typecheck": "tsc --noEmit -p tsconfig.json",
34
- "lint": "eslint \"bin/**/*.ts\" \"src/**/*.ts\"",
35
- "format": "prettier --write .",
36
- "test": "pnpm build && node --test tests/*.test.mjs",
37
- "prepack": "pnpm build"
38
- },
39
- "packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a",
40
- "files": [
41
- "bin/*.js",
42
- "src/**/*.js",
43
- "dist/**/*.d.ts",
44
- "README.md"
45
- ],
46
- "engines": {
47
- "node": ">=22.15.0"
48
- }
49
- }
2
+ "name": "@azure-net/cli",
3
+ "version": "6.0.0-next.3",
4
+ "type": "module",
5
+ "bin": {
6
+ "azure-net": "bin/azure-net.js"
7
+ },
8
+ "license": "MIT",
9
+ "author": "Pixel1917",
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "registry": "https://registry.npmjs.org/"
13
+ },
14
+ "peerDependencies": {
15
+ "@azure-net/kit": ">=6.0.0-next.1"
16
+ },
17
+ "dependencies": {
18
+ "commander": "^11",
19
+ "prompts": "^2",
20
+ "typescript": "^5",
21
+ "prettier": "^3.6.2"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.31.0",
25
+ "@types/node": "^24.0.3",
26
+ "@types/prompts": "^2.4.9",
27
+ "env-cmd": "^11.0.0",
28
+ "eslint": "^9.31.0",
29
+ "typescript-eslint": "^8.36.0"
30
+ },
31
+ "files": [
32
+ "bin/*.js",
33
+ "src/**/*.js",
34
+ "dist/**/*.d.ts",
35
+ "README.md"
36
+ ],
37
+ "engines": {
38
+ "node": ">=22.15.0"
39
+ },
40
+ "scripts": {
41
+ "build": "node scripts/clean-build.mjs && tsc -p tsconfig.json && node scripts/sync-build.mjs",
42
+ "typecheck": "tsc --noEmit -p tsconfig.json",
43
+ "lint": "eslint \"bin/**/*.ts\" \"src/**/*.ts\"",
44
+ "format": "prettier --write .",
45
+ "test": "pnpm build && node --test tests/*.test.mjs"
46
+ }
47
+ }
@@ -1,3 +1,4 @@
1
+ import { migrateTranslationsSources } from './translations.js';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { createHash } from 'node:crypto';
@@ -8,6 +9,7 @@ import { VITE_CONFIGS } from '../utils/viteConfig.js';
8
9
  import { mergePackageImports, resolvePackageImport } from '../utils/packageImports.js';
9
10
  import { flattenConfigLiterals } from '../utils/flattenConfig.js';
10
11
  import { parseSource } from '../checks/architecture.js';
12
+ import { inspectFactories, replaceFactory } from './presenterFactories.js';
11
13
  import { migrateAsyncHelpers, migratePresenterSource } from './presenters.js';
12
14
  const slash = (s) => s.split(path.sep).join('/');
13
15
  const textFile = /\.(?:[cm]?[jt]sx?|svelte|css|scss|sass|less|json|html|svg)$/;
@@ -289,9 +291,13 @@ export async function planMigration(root = process.cwd()) {
289
291
  const presenterNames = new Map();
290
292
  const upgradedSources = new Map();
291
293
  const factorySymbols = new Map();
294
+ const factoryDefinitions = new Map();
295
+ const legacyFactories = new Set();
296
+ const consumedFactories = new Set();
292
297
  for (const [file, bytes] of originals) {
293
298
  if (!/\.[jt]s$/.test(file))
294
299
  continue;
300
+ factoryDefinitions.set(file, inspectFactories(bytes.toString(), file));
295
301
  const source = parseSource(bytes.toString(), file);
296
302
  const creators = new Set();
297
303
  for (const statement of source.statements) {
@@ -329,6 +335,8 @@ export async function planMigration(root = process.cwd()) {
329
335
  continue;
330
336
  const source = parseSource(bytes.toString(), file);
331
337
  const factories = new Set();
338
+ const dependencies = new Map();
339
+ const definitions = new Map();
332
340
  for (const statement of source.statements) {
333
341
  if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
334
342
  continue;
@@ -344,11 +352,34 @@ export async function planMigration(root = process.cwd()) {
344
352
  ['@azure-net/kit/delivery', '@azure-net/edges'].includes(statement.moduleSpecifier.text)) ||
345
353
  (symbol && factorySymbols.get(symbol.file)?.has(symbol.name)))
346
354
  factories.add(binding.name.text);
355
+ if (symbol) {
356
+ const definition = factoryDefinitions.get(symbol.file)?.find((value) => value.name === symbol.name);
357
+ if (definition) {
358
+ let module = slash(path.relative(path.dirname(file), symbol.file)).replace(/\.[jt]s$/, '');
359
+ if (!module.startsWith('.'))
360
+ module = './' + module;
361
+ dependencies.set(binding.name.text, { ...definition, source: module });
362
+ definitions.set(binding.name.text, definition);
363
+ }
364
+ }
347
365
  }
348
366
  }
349
367
  const result = file.endsWith('.svelte')
350
368
  ? { content: bytes.toString(), converted: new Map(), issues: [] }
351
- : migratePresenterSource(bytes.toString(), file, factories);
369
+ : migratePresenterSource(bytes.toString(), file, factories, dependencies);
370
+ for (const [local, definition] of definitions) {
371
+ if (result.converted.size)
372
+ consumedFactories.add(definition);
373
+ const remaining = ts.createSourceFile(file, result.content, ts.ScriptTarget.Latest, true);
374
+ const visit = (node) => {
375
+ if (ts.isImportDeclaration(node))
376
+ return;
377
+ if (ts.isIdentifier(node) && node.text === local)
378
+ legacyFactories.add(definition);
379
+ ts.forEachChild(node, visit);
380
+ };
381
+ visit(remaining);
382
+ }
352
383
  if (result.converted.size)
353
384
  presenterNames.set(file, result.converted);
354
385
  for (const reason of result.issues)
@@ -363,6 +394,86 @@ export async function planMigration(root = process.cwd()) {
363
394
  if (upgraded !== bytes.toString())
364
395
  upgradedSources.set(file, upgraded);
365
396
  }
397
+ for (const [file, definitions] of factoryDefinitions) {
398
+ let content = originals.get(file).toString();
399
+ let changed = false;
400
+ for (const definition of definitions) {
401
+ if (!consumedFactories.has(definition))
402
+ continue;
403
+ const keepLegacy = legacyFactories.has(definition);
404
+ content = replaceFactory(content, file, definition, keepLegacy);
405
+ changed = true;
406
+ if (!keepLegacy) {
407
+ const names = presenterNames.get(file) ?? new Map();
408
+ names.set(definition.name, definition.exportName);
409
+ presenterNames.set(file, names);
410
+ }
411
+ else
412
+ issue(file, 'Some factory consumers could not be migrated; legacy factory retained alongside explicit dependencies.');
413
+ }
414
+ if (changed) {
415
+ try {
416
+ content = migrateAsyncHelpers(content, file);
417
+ }
418
+ catch (error) {
419
+ issue(file, String(error));
420
+ }
421
+ upgradedSources.set(file, content);
422
+ }
423
+ }
424
+ const translationMigration = migrateTranslationsSources(new Map([...originals]
425
+ .filter(([file]) => file.startsWith(path.join(root, 'src') + path.sep))
426
+ .map(([file, bytes]) => [file, upgradedSources.get(file) ?? bytes.toString()])), resolve);
427
+ for (const [file, content] of translationMigration.updates)
428
+ upgradedSources.set(file, content);
429
+ for (const problem of translationMigration.issues)
430
+ issue(problem.file, problem.reason);
431
+ for (const [file, bytes] of originals) {
432
+ if (!/\.[jt]s$/.test(file) || isBackup(mapping.get(file) ?? file))
433
+ continue;
434
+ const source = parseSource(upgradedSources.get(file) ?? bytes.toString(), file);
435
+ const removed = new Set(['createPresenterFactory', 'createPresenter', 'createStoreFactory', 'createStore', 'createAsyncHelpers']);
436
+ const apps = new Set();
437
+ for (const statement of source.statements) {
438
+ if (!ts.isImportDeclaration(statement) ||
439
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
440
+ !['@azure-net/kit', '@azure-net/kit/delivery'].includes(statement.moduleSpecifier.text))
441
+ continue;
442
+ const bindings = statement.importClause?.namedBindings;
443
+ if (!bindings || !ts.isNamedImports(bindings))
444
+ continue;
445
+ for (const binding of bindings.elements) {
446
+ const name = (binding.propertyName ?? binding.name).text;
447
+ if (removed.has(name))
448
+ issue(file, 'Removed kit API ' + name + ' remains; manual migration is required before build.');
449
+ if (name === 'createApp')
450
+ apps.add(binding.name.text);
451
+ }
452
+ }
453
+ const inspect = (node) => {
454
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && apps.has(node.expression.text)) {
455
+ const callback = node.arguments[0];
456
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
457
+ const parameter = callback.parameters[0]?.name;
458
+ const check = (child) => {
459
+ if (ts.isCallExpression(child) &&
460
+ ts.isPropertyAccessExpression(child.expression) &&
461
+ child.expression.name.text === 'dependencies') {
462
+ let base = child.expression.expression;
463
+ while (ts.isCallExpression(base) && ts.isPropertyAccessExpression(base.expression))
464
+ base = base.expression.expression;
465
+ if (parameter && ts.isIdentifier(parameter) && ts.isIdentifier(base) && base.text === parameter.text)
466
+ issue(file, 'createApp.dependencies/Container was removed. Move these bindings to a DiContainer; consumer changes require manual review.');
467
+ }
468
+ ts.forEachChild(child, check);
469
+ };
470
+ check(callback.body);
471
+ }
472
+ }
473
+ ts.forEachChild(node, inspect);
474
+ };
475
+ inspect(source);
476
+ }
366
477
  const providers = new Map();
367
478
  for (const [file, bytes] of originals)
368
479
  if (/Provider\.[jt]s$/.test(file) && mapping.has(file)) {
@@ -631,7 +742,7 @@ export async function planMigration(root = process.cwd()) {
631
742
  const declared = symbol?.file ?? target;
632
743
  const to = mapping.get(declared) ?? declared;
633
744
  const isProvider = converted.has(name) && providers.get(name)?.file === declared;
634
- const spec = isProvider ? '#platform/Foundation/DI' : isBackup(to) ? undefined : specifier(to, destination);
745
+ const spec = isProvider ? '#platform/Foundation/DI/ApplicationProvider' : isBackup(to) ? undefined : specifier(to, destination);
635
746
  if (!spec) {
636
747
  issue(destination, `Unresolved imported symbol ${name} from ${node.moduleSpecifier.text}`);
637
748
  continue;
@@ -736,6 +847,8 @@ export async function planMigration(root = process.cwd()) {
736
847
  if (!file.startsWith(path.join(root, 'src') + path.sep))
737
848
  continue;
738
849
  const to = mapping.get(file) ?? file;
850
+ if (!oldRoots.size && !upgradedSources.size)
851
+ continue;
739
852
  const output = isBackup(to) || !textFile.test(file) ? bytes : Buffer.from(rewrite(file, to, upgradedSources.get(file) ?? bytes.toString()));
740
853
  if (writes.has(to) && path.basename(to) === 'index.ts') {
741
854
  const barrel = ts.createSourceFile(to, writes.get(to).toString() + '\n' + output.toString(), ts.ScriptTarget.Latest, true);
@@ -0,0 +1,189 @@
1
+ import ts from 'typescript';
2
+ const kitModules = new Set(['@azure-net/kit/delivery', '@azure-net/edges']);
3
+ const helperNames = new Set(['createAsyncAction', 'createAsyncResource']);
4
+ const unwrap = (node) => ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node)
5
+ ? unwrap(node.expression)
6
+ : node;
7
+ /** Only evaluate syntax. Never import or execute the application's factory/configuration. */
8
+ export function inspectFactories(input, file) {
9
+ const source = ts.createSourceFile(file, input, ts.ScriptTarget.Latest, true);
10
+ const creators = new Set();
11
+ const helperFactories = new Set();
12
+ const helpers = new Map();
13
+ const occupied = new Set();
14
+ const visit = (node) => {
15
+ if (ts.isIdentifier(node))
16
+ occupied.add(node.text);
17
+ ts.forEachChild(node, visit);
18
+ };
19
+ visit(source);
20
+ for (const statement of source.statements) {
21
+ if (!ts.isImportDeclaration(statement) ||
22
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
23
+ !kitModules.has(statement.moduleSpecifier.text) ||
24
+ statement.importClause?.isTypeOnly)
25
+ continue;
26
+ const bindings = statement.importClause?.namedBindings;
27
+ if (!bindings || !ts.isNamedImports(bindings))
28
+ continue;
29
+ for (const binding of bindings.elements) {
30
+ if (binding.isTypeOnly)
31
+ continue;
32
+ const name = (binding.propertyName ?? binding.name).text;
33
+ if (name === 'createPresenterFactory')
34
+ creators.add(binding.name.text);
35
+ if (name === 'createAsyncHelpers')
36
+ helperFactories.add(binding.name.text);
37
+ if (helperNames.has(name))
38
+ helpers.set(binding.name.text, name);
39
+ }
40
+ }
41
+ for (const statement of source.statements) {
42
+ if (!ts.isVariableStatement(statement))
43
+ continue;
44
+ for (const declaration of statement.declarationList.declarations) {
45
+ const init = declaration.initializer && unwrap(declaration.initializer);
46
+ if (!init || !ts.isObjectBindingPattern(declaration.name))
47
+ continue;
48
+ const available = new Set();
49
+ if (ts.isCallExpression(init) &&
50
+ ts.isIdentifier(init.expression) &&
51
+ helperFactories.has(init.expression.text) &&
52
+ !init.arguments.length) {
53
+ for (const helper of helperNames)
54
+ available.add(helper);
55
+ }
56
+ else if (ts.isObjectLiteralExpression(init)) {
57
+ for (const property of init.properties) {
58
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name))
59
+ continue;
60
+ const value = unwrap(property.initializer);
61
+ if (ts.isIdentifier(value) && helpers.get(value.text) === property.name.text)
62
+ available.add(property.name.text);
63
+ }
64
+ }
65
+ for (const binding of declaration.name.elements) {
66
+ const key = binding.propertyName ?? binding.name;
67
+ if (ts.isIdentifier(key) &&
68
+ ts.isIdentifier(binding.name) &&
69
+ available.has(key.text) &&
70
+ !binding.initializer &&
71
+ !binding.dotDotDotToken)
72
+ helpers.set(binding.name.text, key.text);
73
+ }
74
+ }
75
+ }
76
+ const found = [];
77
+ for (const statement of source.statements) {
78
+ if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1)
79
+ continue;
80
+ const declaration = statement.declarationList.declarations[0];
81
+ const init = declaration.initializer;
82
+ if (!ts.isIdentifier(declaration.name) ||
83
+ !init ||
84
+ !ts.isCallExpression(init) ||
85
+ !ts.isIdentifier(init.expression) ||
86
+ !creators.has(init.expression.text) ||
87
+ init.arguments.length !== 1)
88
+ continue;
89
+ const config = init.arguments[0];
90
+ if (!ts.isObjectLiteralExpression(config))
91
+ continue;
92
+ const keys = new Set();
93
+ const directHelpers = new Set();
94
+ let safe = true;
95
+ for (const property of config.properties) {
96
+ if (ts.isSpreadAssignment(property)) {
97
+ const call = property.expression;
98
+ if (!ts.isCallExpression(call) ||
99
+ !ts.isIdentifier(call.expression) ||
100
+ !helperFactories.has(call.expression.text) ||
101
+ call.arguments.length) {
102
+ safe = false;
103
+ break;
104
+ }
105
+ for (const helper of helperNames) {
106
+ keys.add(helper);
107
+ directHelpers.add(helper);
108
+ }
109
+ continue;
110
+ }
111
+ if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) ||
112
+ !property.name ||
113
+ (!ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name))) {
114
+ safe = false;
115
+ break;
116
+ }
117
+ const key = property.name.text;
118
+ if (keys.has(key)) {
119
+ safe = false;
120
+ break;
121
+ }
122
+ keys.add(key);
123
+ const value = ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
124
+ if (ts.isIdentifier(value) && helpers.get(value.text) === key)
125
+ directHelpers.add(key);
126
+ }
127
+ if (!safe)
128
+ continue;
129
+ let exportName = declaration.name.text + 'Dependencies';
130
+ while (occupied.has(exportName))
131
+ exportName += '_';
132
+ occupied.add(exportName);
133
+ found.push({ name: declaration.name.text, exportName, keys, helpers: directHelpers });
134
+ }
135
+ return found;
136
+ }
137
+ export function replaceFactory(input, file, definition, keepLegacy) {
138
+ const source = ts.createSourceFile(file, input, ts.ScriptTarget.Latest, true);
139
+ let creator;
140
+ for (const statement of source.statements) {
141
+ if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1)
142
+ continue;
143
+ const declaration = statement.declarationList.declarations[0];
144
+ if (!ts.isIdentifier(declaration.name) ||
145
+ declaration.name.text !== definition.name ||
146
+ !declaration.initializer ||
147
+ !ts.isCallExpression(declaration.initializer))
148
+ continue;
149
+ const call = declaration.initializer;
150
+ creator = call.expression.getText(source);
151
+ const config = call.arguments[0].getText(source);
152
+ const replacement = `export const ${definition.exportName} = ${config};\n` +
153
+ (keepLegacy ? `export const ${definition.name} = ${creator}(${definition.exportName});` : '');
154
+ input = input.slice(0, statement.getStart(source)) + replacement + input.slice(statement.end);
155
+ break;
156
+ }
157
+ if (!creator || keepLegacy)
158
+ return input;
159
+ const parsed = ts.createSourceFile(file, input, ts.ScriptTarget.Latest, true);
160
+ let used = false;
161
+ const visit = (node) => {
162
+ if (ts.isImportDeclaration(node))
163
+ return;
164
+ if (ts.isIdentifier(node) && node.text === creator)
165
+ used = true;
166
+ ts.forEachChild(node, visit);
167
+ };
168
+ visit(parsed);
169
+ if (used)
170
+ return input;
171
+ for (const statement of [...parsed.statements].reverse()) {
172
+ if (!ts.isImportDeclaration(statement) ||
173
+ !statement.importClause?.namedBindings ||
174
+ !ts.isNamedImports(statement.importClause.namedBindings))
175
+ continue;
176
+ const bindings = statement.importClause.namedBindings;
177
+ const kept = bindings.elements.filter((binding) => binding.name.text !== creator);
178
+ if (kept.length === bindings.elements.length)
179
+ continue;
180
+ if (!kept.length && !statement.importClause.name)
181
+ input = input.slice(0, statement.getStart(parsed)) + input.slice(statement.end);
182
+ else
183
+ input =
184
+ input.slice(0, bindings.getStart(parsed)) +
185
+ `{ ${kept.map((binding) => binding.getText(parsed)).join(', ')} }` +
186
+ input.slice(bindings.end);
187
+ }
188
+ return input;
189
+ }
@@ -71,10 +71,19 @@ export function migrateAsyncHelpers(input, file) {
71
71
  const visit = (node) => {
72
72
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && factories.has(node.expression.text) && !node.arguments.length) {
73
73
  const errorType = node.typeArguments?.[0]?.getText(source);
74
+ let keys = [...helperNames];
75
+ const parent = node.parent;
76
+ if (ts.isVariableDeclaration(parent) &&
77
+ ts.isObjectBindingPattern(parent.name) &&
78
+ !parent.name.elements.some((b) => b.dotDotDotToken)) {
79
+ keys = parent.name.elements.map((b) => (b.propertyName ?? b.name).getText(source));
80
+ if (keys.some((key) => !helperNames.has(key)))
81
+ throw new Error('Removed async helper members require manual migration');
82
+ }
74
83
  edits.push({
75
84
  start: node.getStart(source),
76
85
  end: node.end,
77
- text: `({ createAsyncAction: ${specialize(action, errorType)}, createAsyncResource: ${specialize(resource, errorType)} })`
86
+ text: `({ ${keys.map((key) => `${key}: ${specialize(key === 'createAsyncAction' ? action : resource, errorType)}`).join(', ')} })`
78
87
  });
79
88
  return;
80
89
  }
@@ -83,8 +92,8 @@ export function migrateAsyncHelpers(input, file) {
83
92
  visit(source);
84
93
  return apply(input, edits);
85
94
  }
86
- /** Upgrade only recognized factories; arbitrary injected dependencies require a human decision. */
87
- export function migratePresenterSource(input, file, factories) {
95
+ /** Preserve lexical behavior while moving instance creation and dependencies to RegisterProvider. */
96
+ export function migratePresenterSource(input, file, factories, dependencies = new Map()) {
88
97
  const source = ts.createSourceFile(file, input, ts.ScriptTarget.Latest, true);
89
98
  const edits = [];
90
99
  const converted = new Map();
@@ -92,6 +101,21 @@ export function migratePresenterSource(input, file, factories) {
92
101
  const imports = new Set();
93
102
  const removedFactories = new Set();
94
103
  const existingImports = new Set();
104
+ const dependencyImports = [];
105
+ const occupied = new Set();
106
+ const collectNames = (node) => {
107
+ if (ts.isIdentifier(node))
108
+ occupied.add(node.text);
109
+ ts.forEachChild(node, collectNames);
110
+ };
111
+ collectNames(source);
112
+ const unique = (base) => {
113
+ let value = base;
114
+ while (occupied.has(value))
115
+ value += '_';
116
+ occupied.add(value);
117
+ return value;
118
+ };
95
119
  for (const statement of source.statements) {
96
120
  if (!ts.isImportDeclaration(statement) ||
97
121
  !ts.isStringLiteral(statement.moduleSpecifier) ||
@@ -105,6 +129,8 @@ export function migratePresenterSource(input, file, factories) {
105
129
  existingImports.add(entry.propertyName ? `${entry.propertyName.text} as ${entry.name.text}` : entry.name.text);
106
130
  }
107
131
  }
132
+ const existingRegistration = [...existingImports].find((name) => name === 'RegisterProvider' || name.startsWith('RegisterProvider as '));
133
+ const registrationName = existingRegistration ? existingRegistration.split(' as ').at(-1) : unique('RegisterProvider');
108
134
  for (const statement of source.statements) {
109
135
  if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1)
110
136
  continue;
@@ -118,6 +144,8 @@ export function migratePresenterSource(input, file, factories) {
118
144
  continue;
119
145
  const name = declaration.name.text;
120
146
  try {
147
+ if (call.typeArguments?.length || declaration.type)
148
+ throw new Error('Explicit factory result types require manual review');
121
149
  const callback = call.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
122
150
  if (!callback ||
123
151
  (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) ||
@@ -125,18 +153,53 @@ export function migratePresenterSource(input, file, factories) {
125
153
  callback.typeParameters ||
126
154
  callback.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))
127
155
  throw new Error('Expected a synchronous block factory');
128
- if (call.arguments.length !== 2 || !ts.isStringLiteral(call.arguments[0]))
156
+ if (!((call.arguments.length === 2 && ts.isStringLiteral(call.arguments[0])) ||
157
+ (call.arguments.length === 1 && call.arguments[0] === callback)))
129
158
  throw new Error('Custom factory arguments need manual review');
130
159
  const requestedImports = new Set();
160
+ const dependency = dependencies.get(call.expression.text);
161
+ const dependencyAlias = unique(name + 'Dependencies');
162
+ const extraParameters = [];
163
+ const extraArguments = [];
164
+ const extraRegistrations = [];
165
+ const helperSetup = [];
166
+ const javascript = file.endsWith('.js');
167
+ let needsDependencyImport = false;
168
+ let runtimeDependencyImport = false;
131
169
  if (callback.parameters.length) {
132
170
  const param = callback.parameters[0];
133
171
  if (callback.parameters.length !== 1 || !ts.isObjectBindingPattern(param.name) || param.initializer)
134
172
  throw new Error('Unsupported injected dependencies');
135
173
  for (const binding of param.name.elements) {
136
174
  const original = (binding.propertyName ?? binding.name).getText(source);
137
- if (!helperNames.has(original) || !ts.isIdentifier(binding.name) || binding.initializer || binding.dotDotDotToken)
138
- throw new Error(`Injected dependency ${original} requires manual conversion`);
139
- requestedImports.add(original === binding.name.text ? original : `${original} as ${binding.name.text}`);
175
+ if (!ts.isIdentifier(binding.name) || binding.initializer || binding.dotDotDotToken)
176
+ throw new Error('Nested, default or rest injected bindings need manual conversion');
177
+ if (dependency) {
178
+ if (!dependency.keys.has(original) && helperNames.has(original)) {
179
+ requestedImports.add(original === binding.name.text ? original : `${original} as ${binding.name.text}`);
180
+ continue;
181
+ }
182
+ if (!dependency.keys.has(original))
183
+ throw new Error(`Injected dependency ${original} is not statically known in its factory`);
184
+ if (helperNames.has(original) && dependency.helpers.has(original)) {
185
+ const helperAlias = unique('kit' + original[0].toUpperCase() + original.slice(1));
186
+ requestedImports.add(`${original} as ${helperAlias}`);
187
+ helperSetup.push(`const ${binding.name.text}${javascript ? '' : ': typeof ' + dependencyAlias + '[' + JSON.stringify(original) + ']'} = ${helperAlias};`);
188
+ if (!javascript)
189
+ needsDependencyImport = true;
190
+ }
191
+ else {
192
+ needsDependencyImport = runtimeDependencyImport = true;
193
+ extraParameters.push(`${binding.name.text}: typeof ${dependencyAlias}[${JSON.stringify(original)}]`);
194
+ extraArguments.push(binding.name.text);
195
+ extraRegistrations.push(`${dependencyAlias}[${JSON.stringify(original)}]`);
196
+ }
197
+ }
198
+ else {
199
+ if (!helperNames.has(original))
200
+ throw new Error(`Injected dependency ${original} requires manual conversion`);
201
+ requestedImports.add(original === binding.name.text ? original : `${original} as ${binding.name.text}`);
202
+ }
140
203
  }
141
204
  }
142
205
  const statements = [...callback.body.statements];
@@ -152,6 +215,16 @@ export function migratePresenterSource(input, file, factories) {
152
215
  const result = statements.at(-1);
153
216
  if (!result || !ts.isReturnStatement(result) || !result.expression || !ts.isObjectLiteralExpression(result.expression))
154
217
  throw new Error('Expected a final object-literal return');
218
+ const validateFlow = (node) => {
219
+ if (ts.isFunctionLike(node) || ts.isClassLike(node))
220
+ return;
221
+ if (ts.isReturnStatement(node) && node !== result)
222
+ throw new Error('Conditional early returns require manual conversion');
223
+ if (ts.isIdentifier(node) && node.text === 'arguments')
224
+ throw new Error('Factory arguments binding requires manual conversion');
225
+ ts.forEachChild(node, validateFlow);
226
+ };
227
+ validateFlow(callback.body);
155
228
  const keys = result.expression.properties.map((prop) => {
156
229
  if (!prop.name || !ts.isIdentifier(prop.name))
157
230
  throw new Error('Spread/computed return keys require manual conversion');
@@ -161,8 +234,9 @@ export function migratePresenterSource(input, file, factories) {
161
234
  });
162
235
  if (new Set(keys).size !== keys.length)
163
236
  throw new Error('Duplicate return keys');
164
- const parameters = [];
165
- const argumentsList = [];
237
+ const parameters = [...extraParameters];
238
+ const argumentsList = [...extraArguments];
239
+ const registrationArguments = [...extraRegistrations];
166
240
  const registrations = [];
167
241
  const body = statements.filter((stmt) => {
168
242
  if (!ts.isVariableStatement(stmt) || stmt.declarationList.declarations.length !== 1)
@@ -183,6 +257,7 @@ export function migratePresenterSource(input, file, factories) {
183
257
  const key = (entry.propertyName ?? entry.name).getText(source).replace(/^['"]|['"]$/g, '');
184
258
  parameters.push(`${entry.name.text}: ReturnType<typeof ${value.expression.text}>[${JSON.stringify(key)}]`);
185
259
  argumentsList.push(entry.name.text);
260
+ registrationArguments.push(entry.name.text);
186
261
  }
187
262
  registrations.push(stmt.getText(source));
188
263
  return false;
@@ -191,7 +266,6 @@ export function migratePresenterSource(input, file, factories) {
191
266
  const setup = `create${name}State`;
192
267
  if (new RegExp(`\\b(?:use${name}|${setup})\\b`).test(input))
193
268
  throw new Error('Generated declaration name already exists');
194
- const javascript = file.endsWith('.js');
195
269
  const parameterList = (javascript ? argumentsList : parameters).join(', ');
196
270
  const members = result.expression.properties.map((property, index) => {
197
271
  const key = keys[index];
@@ -200,11 +274,13 @@ export function migratePresenterSource(input, file, factories) {
200
274
  ? ''
201
275
  : `\nset ${key}(value${javascript ? '' : `: ReturnType<typeof ${setup}>[${JSON.stringify(key)}]`}) { this.#state.${key} = value; }`));
202
276
  });
203
- const text = `function ${setup}(${parameterList})${callback.type ? ': ' + callback.type.getText(source) : ''} {\n${body.map((stmt) => stmt.getText(source)).join('\n')}\n}\n\nexport class ${name} {\n#state${javascript ? '' : `: ReturnType<typeof ${setup}>`};\nconstructor(${parameterList}) { this.#state = ${setup}(${argumentsList.join(', ')}); }\n${members.join('\n')}\n}\n\nexport const use${name} = RegisterProvider(() => {\n${registrations.join('\n')}\nreturn new ${name}(${argumentsList.join(', ')});\n}, 'scoped');`;
277
+ const text = `function ${setup}(${parameterList})${callback.type ? ': ' + callback.type.getText(source) : ''} {\n${helperSetup.join('\n')}\n${body.map((stmt) => stmt.getText(source)).join('\n')}\n}\n\nexport class ${name} {\n#state${javascript ? '' : `: ReturnType<typeof ${setup}>`};\nconstructor(${parameterList}) { this.#state = ${setup}(${argumentsList.join(', ')}); }\n${members.join('\n')}\n}\n\nexport const use${name} = ${registrationName}(() => {\n${registrations.join('\n')}\nreturn new ${name}(${registrationArguments.join(', ')});\n}, 'scoped');`;
278
+ if (needsDependencyImport && dependency)
279
+ dependencyImports.push(`import ${runtimeDependencyImport ? '' : 'type '}{ ${dependency.exportName} as ${dependencyAlias} } from ${JSON.stringify(dependency.source)};`);
204
280
  edits.push({ start: statement.getStart(source), end: statement.end, text });
205
281
  converted.set(name, `use${name}`);
206
282
  removedFactories.add(call.expression.text);
207
- imports.add('RegisterProvider');
283
+ imports.add(registrationName === 'RegisterProvider' ? 'RegisterProvider' : `RegisterProvider as ${registrationName}`);
208
284
  for (const helper of requestedImports)
209
285
  imports.add(helper);
210
286
  }
@@ -249,12 +325,44 @@ export function migratePresenterSource(input, file, factories) {
249
325
  }
250
326
  const neededImports = [...imports].filter((name) => !existingImports.has(name));
251
327
  output =
252
- (neededImports.length ? `import { ${neededImports.join(', ')} } from '@azure-net/kit/delivery';\n` : '') + apply(output, importEdits);
328
+ dependencyImports.join('\n') +
329
+ '\n' +
330
+ (neededImports.length ? `import { ${neededImports.join(', ')} } from '@azure-net/kit/delivery';\n` : '') +
331
+ apply(output, importEdits);
253
332
  const updated = ts.createSourceFile(file, output, ts.ScriptTarget.Latest, true);
333
+ // Bind only this in-memory module: distinguish lexical shadows without loading dependencies.
334
+ const options = { noLib: true, noResolve: true, target: ts.ScriptTarget.ES2022 };
335
+ const host = ts.createCompilerHost(options);
336
+ host.getSourceFile = (name) => (name === file ? updated : undefined);
337
+ host.fileExists = (name) => name === file;
338
+ host.readFile = (name) => (name === file ? output : undefined);
339
+ const checker = ts.createProgram([file], options, host).getTypeChecker();
340
+ const symbols = new Map();
341
+ for (const statement of updated.statements) {
342
+ if (!ts.isClassDeclaration(statement) || !statement.name || !converted.has(statement.name.text))
343
+ continue;
344
+ const symbol = checker.getSymbolAtLocation(statement.name);
345
+ if (symbol)
346
+ symbols.set(symbol, converted.get(statement.name.text));
347
+ }
254
348
  const references = [];
255
349
  const visit = (node) => {
256
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && converted.has(node.expression.text)) {
257
- references.push({ start: node.expression.getStart(updated), end: node.expression.end, text: converted.get(node.expression.text) });
350
+ if (ts.isShorthandPropertyAssignment(node)) {
351
+ const symbol = checker.getShorthandAssignmentValueSymbol(node);
352
+ const name = symbol && symbols.get(symbol);
353
+ if (name) {
354
+ references.push({ start: node.getStart(updated), end: node.end, text: `${node.name.text}: ${name}` });
355
+ return;
356
+ }
357
+ }
358
+ if (ts.isIdentifier(node) && converted.has(node.text)) {
359
+ const parent = node.parent;
360
+ const declaration = ts.isClassDeclaration(parent) && parent.name === node;
361
+ const construction = ts.isNewExpression(parent) && parent.expression === node;
362
+ const symbol = checker.getSymbolAtLocation(node);
363
+ const name = symbol && symbols.get(symbol);
364
+ if (name && !declaration && !construction)
365
+ references.push({ start: node.getStart(updated), end: node.end, text: name });
258
366
  }
259
367
  ts.forEachChild(node, visit);
260
368
  };
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ const patch = (input, edits) => {
4
+ for (const e of edits.sort((a, b) => b.start - a.start))
5
+ input = input.slice(0, e.start) + e.text + input.slice(e.end);
6
+ return input;
7
+ };
8
+ const internal = new Set(['preloadTranslation', 'initializeClient', 'syncFromState']);
9
+ const translationModules = new Set(['@azure-net/kit/i18n', '@azure-net/edges-translations']);
10
+ /** A conservative migration of known translation wiring, never a project-wide textual replacement. */
11
+ export function migrateTranslationsSources(sources, resolve) {
12
+ const updates = new Map();
13
+ const issues = [];
14
+ const managers = [];
15
+ for (const [file, input] of sources) {
16
+ if (!/\.[jt]s$/.test(file))
17
+ continue;
18
+ const source = ts.createSourceFile(file, input, ts.ScriptTarget.Latest, true);
19
+ const factories = new Set();
20
+ for (const stmt of source.statements) {
21
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier) || !translationModules.has(stmt.moduleSpecifier.text))
22
+ continue;
23
+ const bindings = stmt.importClause?.namedBindings;
24
+ if (bindings && ts.isNamedImports(bindings))
25
+ for (const b of bindings.elements)
26
+ if ((b.propertyName ?? b.name).text === 'createTranslations')
27
+ factories.add(b.name.text);
28
+ }
29
+ for (const stmt of source.statements)
30
+ if (ts.isVariableStatement(stmt) && stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
31
+ for (const d of stmt.declarationList.declarations) {
32
+ if (ts.isIdentifier(d.name) &&
33
+ d.initializer &&
34
+ ts.isCallExpression(d.initializer) &&
35
+ ts.isIdentifier(d.initializer.expression) &&
36
+ factories.has(d.initializer.expression.text))
37
+ managers.push({ file, name: d.name.text });
38
+ }
39
+ }
40
+ if (!managers.length)
41
+ return { updates, issues };
42
+ if (managers.length !== 1) {
43
+ for (const m of managers)
44
+ issues.push({ file: m.file, reason: 'Multiple translation managers: choose the manager for app.useTranslations manually.' });
45
+ return { updates, issues };
46
+ }
47
+ const manager = managers[0];
48
+ const reaches = (file, seen = new Set()) => {
49
+ if (file === manager.file)
50
+ return true;
51
+ if (seen.has(file))
52
+ return false;
53
+ seen.add(file);
54
+ const source = ts.createSourceFile(file, sources.get(file) ?? '', ts.ScriptTarget.Latest, true);
55
+ return source.statements.some((s) => {
56
+ if ((!ts.isImportDeclaration(s) && !ts.isExportDeclaration(s)) || !s.moduleSpecifier || !ts.isStringLiteral(s.moduleSpecifier))
57
+ return false;
58
+ const target = resolve(file, s.moduleSpecifier.text);
59
+ return !!target && reaches(target, seen);
60
+ });
61
+ };
62
+ let connected = false;
63
+ for (const [file, input] of sources) {
64
+ if (!/\.[jt]s$/.test(file) || !reaches(file))
65
+ continue;
66
+ const source = ts.createSourceFile(file, input, ts.ScriptTarget.Latest, true);
67
+ const creators = new Set();
68
+ for (const stmt of source.statements)
69
+ if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === '@azure-net/kit') {
70
+ const b = stmt.importClause?.namedBindings;
71
+ if (b && ts.isNamedImports(b))
72
+ for (const e of b.elements)
73
+ if ((e.propertyName ?? e.name).text === 'createApp')
74
+ creators.add(e.name.text);
75
+ }
76
+ const calls = [];
77
+ const visit = (n) => {
78
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && creators.has(n.expression.text))
79
+ calls.push(n);
80
+ ts.forEachChild(n, visit);
81
+ };
82
+ visit(source);
83
+ if (!calls.length)
84
+ continue;
85
+ if (calls.length !== 1) {
86
+ issues.push({ file, reason: 'Multiple createApp calls: connect translations manually.' });
87
+ continue;
88
+ }
89
+ const callback = calls[0].arguments[0];
90
+ if (!callback || !ts.isArrowFunction(callback) || !callback.parameters[0] || !ts.isIdentifier(callback.parameters[0].name)) {
91
+ issues.push({ file, reason: 'Unsupported createApp callback: add useTranslations manually.' });
92
+ continue;
93
+ }
94
+ let body = ts.isBlock(callback.body)
95
+ ? callback.body.statements.find(ts.isReturnStatement)?.expression
96
+ : callback.body;
97
+ let already = false;
98
+ while (body && ts.isCallExpression(body) && ts.isPropertyAccessExpression(body.expression)) {
99
+ if (body.expression.name.text === 'useTranslations')
100
+ already = true;
101
+ body = body.expression.expression;
102
+ }
103
+ if (already) {
104
+ connected = true;
105
+ continue;
106
+ }
107
+ if (!body || !ts.isIdentifier(body) || body.text !== callback.parameters[0].name.text) {
108
+ issues.push({ file, reason: 'Unsupported createApp chain: add useTranslations manually.' });
109
+ continue;
110
+ }
111
+ let local = 'MigratedTranslationManager';
112
+ while (new RegExp(`\\b${local}\\b`).test(input))
113
+ local += '_';
114
+ let spec = path
115
+ .relative(path.dirname(file), manager.file)
116
+ .split(path.sep)
117
+ .join('/')
118
+ .replace(/\.[jt]s$/, '');
119
+ if (!spec.startsWith('.'))
120
+ spec = './' + spec;
121
+ updates.set(file, `import { ${manager.name} as ${local} } from ${JSON.stringify(spec)};\n` +
122
+ input.slice(0, body.end) +
123
+ `.useTranslations(${local})` +
124
+ input.slice(body.end));
125
+ connected = true;
126
+ }
127
+ if (!connected) {
128
+ issues.push({
129
+ file: manager.file,
130
+ reason: 'Translations require app.useTranslations(TranslationManager); no unambiguous connected bootstrap found.'
131
+ });
132
+ return { updates, issues };
133
+ }
134
+ let unsafeLifecycle = false;
135
+ for (const [file, original] of sources) {
136
+ if (!/\.[jt]s$/.test(file))
137
+ continue;
138
+ const input = updates.get(file) ?? original;
139
+ const source = ts.createSourceFile(file, input, ts.ScriptTarget.Latest, true);
140
+ const options = { noLib: true, noResolve: true };
141
+ const host = ts.createCompilerHost(options);
142
+ host.getSourceFile = (name) => (name === file ? source : undefined);
143
+ host.fileExists = (name) => name === file;
144
+ host.readFile = (name) => (name === file ? input : undefined);
145
+ const checker = ts.createProgram([file], options, host).getTypeChecker();
146
+ const accessors = new Set();
147
+ for (const stmt of source.statements)
148
+ if (ts.isImportDeclaration(stmt) &&
149
+ ts.isStringLiteral(stmt.moduleSpecifier) &&
150
+ resolve(file, stmt.moduleSpecifier.text) === manager.file) {
151
+ const bindings = stmt.importClause?.namedBindings;
152
+ if (bindings && ts.isNamedImports(bindings))
153
+ for (const e of bindings.elements)
154
+ if ((e.propertyName ?? e.name).text === manager.name)
155
+ accessors.add(e.name.text);
156
+ }
157
+ const edits = [];
158
+ let unsafe = false;
159
+ const scan = (node) => {
160
+ if (ts.isVariableDeclaration(node) &&
161
+ ts.isObjectBindingPattern(node.name) &&
162
+ node.initializer &&
163
+ ts.isCallExpression(node.initializer) &&
164
+ ts.isIdentifier(node.initializer.expression) &&
165
+ accessors.has(node.initializer.expression.text)) {
166
+ const removed = node.name.elements.filter((b) => ts.isIdentifier(b.name) && internal.has((b.propertyName ?? b.name).getText(source)));
167
+ for (const b of removed) {
168
+ const binding = checker.getSymbolAtLocation(b.name);
169
+ const refs = [];
170
+ const find = (n) => {
171
+ if (n === node.name)
172
+ return;
173
+ if (binding && ts.isIdentifier(n) && checker.getSymbolAtLocation(n) === binding)
174
+ refs.push(n);
175
+ ts.forEachChild(n, find);
176
+ };
177
+ find(source);
178
+ for (const ref of refs) {
179
+ const call = ref.parent;
180
+ if (!ts.isCallExpression(call) || call.expression !== ref || call.arguments.length) {
181
+ unsafe = true;
182
+ continue;
183
+ }
184
+ edits.push({ start: call.getStart(source), end: call.end, text: 'Promise.resolve()' });
185
+ }
186
+ }
187
+ if (removed.length) {
188
+ const kept = node.name.elements.filter((b) => !removed.includes(b));
189
+ edits.push({
190
+ start: node.name.getStart(source),
191
+ end: node.name.end,
192
+ text: `{ ${kept.map((b) => b.getText(source)).join(', ')} }`
193
+ });
194
+ }
195
+ }
196
+ if (ts.isImportDeclaration(node) &&
197
+ ts.isStringLiteral(node.moduleSpecifier) &&
198
+ node.moduleSpecifier.text === '@azure-net/edges-translations')
199
+ edits.push({ start: node.moduleSpecifier.getStart(source), end: node.moduleSpecifier.end, text: "'@azure-net/kit/i18n'" });
200
+ ts.forEachChild(node, scan);
201
+ };
202
+ scan(source);
203
+ if (unsafe) {
204
+ unsafeLifecycle = true;
205
+ issues.push({ file, reason: 'Custom translation lifecycle arguments/references: migrate these calls manually to useTranslations.' });
206
+ continue;
207
+ }
208
+ if (edits.length)
209
+ updates.set(file, patch(input, edits));
210
+ }
211
+ // Do not connect automatic loading while an old custom locale resolver is still active.
212
+ return { updates: unsafeLifecycle ? new Map() : updates, issues };
213
+ }