@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 +13 -1
- package/dist/src/migrate/presenterFactories.d.ts +9 -0
- package/dist/src/migrate/presenters.d.ts +8 -2
- package/dist/src/migrate/translations.d.ts +8 -0
- package/package.json +46 -48
- package/src/migrate/migrate.js +115 -2
- package/src/migrate/presenterFactories.js +189 -0
- package/src/migrate/presenters.js +123 -15
- package/src/migrate/translations.js +213 -0
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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
package/src/migrate/migrate.js
CHANGED
|
@@ -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: `({
|
|
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
|
-
/**
|
|
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
|
|
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 (!
|
|
138
|
-
throw new Error(
|
|
139
|
-
|
|
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} =
|
|
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
|
-
|
|
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.
|
|
257
|
-
|
|
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
|
+
}
|