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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +189 -0
  2. package/bin/azure-net.js +236 -221
  3. package/dist/bin/azure-net.d.ts +2 -0
  4. package/dist/src/checks/architecture.d.ts +20 -0
  5. package/dist/src/checks/checkDomain.d.ts +1 -0
  6. package/dist/src/checks/checkFoldersStructure.d.ts +1 -0
  7. package/dist/src/checks/checkHooks.d.ts +1 -0
  8. package/dist/src/checks/checkImports.d.ts +1 -0
  9. package/dist/src/checks/checkInternal.d.ts +1 -0
  10. package/dist/src/checks/checkLayerBoundaries.d.ts +1 -0
  11. package/dist/src/checks/checkLayerImports.d.ts +1 -0
  12. package/dist/src/checks/checkPresenterNames.d.ts +1 -0
  13. package/dist/src/checks/checkProject.d.ts +1 -0
  14. package/dist/src/checks/checkProviderGraph.d.ts +1 -0
  15. package/dist/src/checks/checkProviderNames.d.ts +1 -0
  16. package/dist/src/generate/layers/generateRepository.d.ts +1 -0
  17. package/dist/src/generate/layers/generateRepositoryMethod.d.ts +1 -0
  18. package/dist/src/generate/layers/generateUseCases.d.ts +1 -0
  19. package/dist/src/generate/module/generateDomain.d.ts +8 -0
  20. package/dist/src/generate/module/generateModule.d.ts +1 -0
  21. package/dist/src/generate/module/generateModulePreset.d.ts +1 -0
  22. package/dist/src/generate/module/generatePresenter.d.ts +29 -0
  23. package/dist/src/generate/module/generateRepository.d.ts +15 -0
  24. package/dist/src/generate/module/generateRepositoryMethod.d.ts +2 -0
  25. package/dist/src/generate/module/generateSchemaFromType.d.ts +9 -0
  26. package/dist/src/generate/module/generateStore.d.ts +1 -0
  27. package/dist/src/generate/module/generateTypesJson.d.ts +2 -0
  28. package/dist/src/generate/module/generateUseCases.d.ts +1 -0
  29. package/dist/src/generate/module/repositoryModuleShared.d.ts +89 -0
  30. package/dist/src/generate/routes/generateRoutes.d.ts +14 -0
  31. package/dist/src/generate/ui/generateComponent.d.ts +1 -0
  32. package/dist/src/generate/ui/generateDesignComponent.d.ts +1 -0
  33. package/dist/src/generate/ui/generateWidget.d.ts +1 -0
  34. package/dist/src/git/commit.d.ts +23 -0
  35. package/dist/src/git/push.d.ts +11 -0
  36. package/dist/src/git/runGit.d.ts +2 -0
  37. package/dist/src/init/initAliases.d.ts +12 -0
  38. package/dist/src/init/initEdges.d.ts +2 -0
  39. package/dist/src/init/initStructure.d.ts +1 -0
  40. package/dist/src/install/configureCommitTools.d.ts +1 -0
  41. package/dist/src/install/installFresh.d.ts +1 -0
  42. package/dist/src/migrate/migrate.d.ts +22 -0
  43. package/dist/src/migrate/presenters.d.ts +7 -0
  44. package/dist/src/plugins/addLint.d.ts +5 -0
  45. package/dist/src/scaffolds/createCoreSchema.d.ts +1 -0
  46. package/dist/src/scaffolds/createDatasource.d.ts +1 -0
  47. package/dist/src/scaffolds/createProgram.d.ts +1 -0
  48. package/dist/src/scaffolds/createResponse.d.ts +1 -0
  49. package/dist/src/scaffolds/createSchemaRule.d.ts +1 -0
  50. package/dist/src/scaffolds/createTranslationManager.d.ts +1 -0
  51. package/{src/scaffolds/index.ts → dist/src/scaffolds/index.d.ts} +0 -2
  52. package/dist/src/scaffolds/registerDatasource.d.ts +2 -0
  53. package/dist/src/scaffolds/updateDefaultFactoryConfig.d.ts +1 -0
  54. package/dist/src/utils/architecture.d.ts +31 -0
  55. package/dist/src/utils/contextUtils.d.ts +6 -0
  56. package/dist/src/utils/fileUtils.d.ts +5 -0
  57. package/dist/src/utils/flattenConfig.d.ts +2 -0
  58. package/dist/src/utils/legacyAliases.d.ts +1 -0
  59. package/dist/src/utils/loadConfig.d.ts +16 -0
  60. package/dist/src/utils/packageImports.d.ts +4 -0
  61. package/dist/src/utils/sharedFoundation.d.ts +20 -0
  62. package/dist/src/utils/viteConfig.d.ts +9 -0
  63. package/package.json +48 -36
  64. package/src/checks/architecture.js +154 -0
  65. package/src/checks/checkDomain.js +124 -154
  66. package/src/checks/checkFoldersStructure.js +64 -52
  67. package/src/checks/checkHooks.js +12 -12
  68. package/src/checks/checkImports.js +26 -142
  69. package/src/checks/checkInternal.js +27 -24
  70. package/src/checks/checkLayerBoundaries.js +22 -197
  71. package/src/checks/checkLayerImports.js +14 -134
  72. package/src/checks/checkPresenterNames.js +156 -143
  73. package/src/checks/checkProject.js +27 -26
  74. package/src/checks/checkProviderGraph.js +182 -166
  75. package/src/checks/checkProviderNames.js +123 -112
  76. package/src/generate/layers/generateRepository.js +1 -109
  77. package/src/generate/layers/generateRepositoryMethod.js +1 -90
  78. package/src/generate/layers/generateUseCases.js +1 -44
  79. package/src/generate/module/generateDomain.js +81 -105
  80. package/src/generate/module/generateModule.js +34 -68
  81. package/src/generate/module/generateModulePreset.js +188 -248
  82. package/src/generate/module/generatePresenter.js +441 -369
  83. package/src/generate/module/generateRepository.js +102 -121
  84. package/src/generate/module/generateRepositoryMethod.js +117 -0
  85. package/src/generate/module/generateSchemaFromType.js +283 -280
  86. package/src/generate/module/generateStore.js +41 -0
  87. package/src/generate/module/generateTypesJson.js +196 -195
  88. package/src/generate/module/generateUseCases.js +47 -55
  89. package/src/generate/module/repositoryModuleShared.js +480 -459
  90. package/src/generate/routes/generateRoutes.js +4 -7
  91. package/src/generate/ui/generateComponent.js +44 -64
  92. package/src/generate/ui/generateDesignComponent.js +21 -41
  93. package/src/generate/ui/generateWidget.js +21 -41
  94. package/src/git/commit.js +67 -0
  95. package/src/git/push.js +19 -0
  96. package/src/git/runGit.js +11 -0
  97. package/src/init/initAliases.js +19 -135
  98. package/src/init/initEdges.js +79 -54
  99. package/src/init/initStructure.js +31 -145
  100. package/src/install/configureCommitTools.js +1 -0
  101. package/src/install/installFresh.js +39 -68
  102. package/src/migrate/migrate.js +914 -0
  103. package/src/migrate/presenters.js +265 -0
  104. package/src/plugins/addLint.js +52 -219
  105. package/src/scaffolds/createCoreSchema.js +65 -91
  106. package/src/scaffolds/createDatasource.js +67 -101
  107. package/src/scaffolds/createProgram.js +8 -8
  108. package/src/scaffolds/createResponse.js +29 -29
  109. package/src/scaffolds/createSchemaRule.js +19 -19
  110. package/src/scaffolds/createTranslationManager.js +84 -37
  111. package/src/scaffolds/index.js +0 -2
  112. package/src/scaffolds/registerDatasource.js +59 -0
  113. package/src/scaffolds/updateDefaultFactoryConfig.js +31 -0
  114. package/src/utils/architecture.js +145 -0
  115. package/src/utils/contextUtils.js +32 -65
  116. package/src/utils/fileUtils.js +69 -50
  117. package/src/utils/flattenConfig.js +38 -0
  118. package/src/utils/legacyAliases.js +75 -0
  119. package/src/utils/loadConfig.js +76 -40
  120. package/src/utils/packageImports.js +63 -0
  121. package/src/utils/sharedFoundation.js +36 -42
  122. package/src/utils/viteConfig.js +295 -0
  123. package/.prettierrc +0 -7
  124. package/bin/azure-net.ts +0 -308
  125. package/eslint.config.js +0 -34
  126. package/src/checks/checkDomain.ts +0 -223
  127. package/src/checks/checkFoldersStructure.ts +0 -85
  128. package/src/checks/checkHooks.ts +0 -22
  129. package/src/checks/checkImports.ts +0 -193
  130. package/src/checks/checkInternal.ts +0 -48
  131. package/src/checks/checkLayerBoundaries.ts +0 -255
  132. package/src/checks/checkLayerImports.ts +0 -185
  133. package/src/checks/checkPresenterNames.ts +0 -203
  134. package/src/checks/checkProject.ts +0 -45
  135. package/src/checks/checkProviderGraph.ts +0 -234
  136. package/src/checks/checkProviderNames.ts +0 -165
  137. package/src/generate/layers/generateRepository.ts +0 -123
  138. package/src/generate/layers/generateRepositoryMethod.ts +0 -102
  139. package/src/generate/layers/generateUseCases.ts +0 -50
  140. package/src/generate/layers/repositoryShared.js +0 -506
  141. package/src/generate/layers/repositoryShared.ts +0 -662
  142. package/src/generate/module/generateDomain.ts +0 -147
  143. package/src/generate/module/generateModule.ts +0 -84
  144. package/src/generate/module/generateModulePreset.ts +0 -338
  145. package/src/generate/module/generatePresenter.ts +0 -498
  146. package/src/generate/module/generateRepository.ts +0 -158
  147. package/src/generate/module/generateSchemaFromType.ts +0 -368
  148. package/src/generate/module/generateTypesJson.ts +0 -259
  149. package/src/generate/module/generateUseCases.ts +0 -67
  150. package/src/generate/module/repositoryModuleShared.ts +0 -669
  151. package/src/generate/routes/generateRoutes.ts +0 -275
  152. package/src/generate/ui/generateComponent.ts +0 -100
  153. package/src/generate/ui/generateDesignComponent.ts +0 -71
  154. package/src/generate/ui/generateWidget.ts +0 -71
  155. package/src/init/initAliases.ts +0 -172
  156. package/src/init/initEdges.ts +0 -74
  157. package/src/init/initStructure.ts +0 -172
  158. package/src/install/installFresh.ts +0 -93
  159. package/src/plugins/addLint.ts +0 -274
  160. package/src/scaffolds/createCoreSchema.ts +0 -130
  161. package/src/scaffolds/createDatasource.ts +0 -161
  162. package/src/scaffolds/createDatasourceProvider.js +0 -127
  163. package/src/scaffolds/createDatasourceProvider.ts +0 -161
  164. package/src/scaffolds/createPresenterFactory.js +0 -83
  165. package/src/scaffolds/createPresenterFactory.ts +0 -102
  166. package/src/scaffolds/createProgram.ts +0 -19
  167. package/src/scaffolds/createResponse.ts +0 -66
  168. package/src/scaffolds/createSchemaRule.ts +0 -44
  169. package/src/scaffolds/createTranslationManager.ts +0 -80
  170. package/src/utils/contextUtils.ts +0 -88
  171. package/src/utils/fileUtils.ts +0 -74
  172. package/src/utils/loadConfig.ts +0 -68
  173. package/src/utils/sharedFoundation.ts +0 -70
  174. package/tsconfig.json +0 -19
  175. /package/{src/checks/index.ts → dist/src/checks/index.d.ts} +0 -0
  176. /package/{src/plugins/index.ts → dist/src/plugins/index.d.ts} +0 -0
@@ -0,0 +1,914 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import ts from 'typescript';
5
+ import prompts from 'prompts';
6
+ import { CONTEXT_DIRS, normalizePascalName } from '../utils/architecture.js';
7
+ import { VITE_CONFIGS } from '../utils/viteConfig.js';
8
+ import { mergePackageImports, resolvePackageImport } from '../utils/packageImports.js';
9
+ import { flattenConfigLiterals } from '../utils/flattenConfig.js';
10
+ import { parseSource } from '../checks/architecture.js';
11
+ import { migrateAsyncHelpers, migratePresenterSource } from './presenters.js';
12
+ const slash = (s) => s.split(path.sep).join('/');
13
+ const textFile = /\.(?:[cm]?[jt]sx?|svelte|css|scss|sass|less|json|html|svg)$/;
14
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
15
+ const unwrap = (n) => (ts.isParenthesizedExpression(n) ? unwrap(n.expression) : n);
16
+ const propName = (n) => (n && (ts.isIdentifier(n) || ts.isStringLiteral(n)) ? n.text : undefined);
17
+ async function files(root) {
18
+ const out = [];
19
+ for (const entry of await fs.readdir(root, { withFileTypes: true })) {
20
+ if (['.backup', '.git', 'node_modules', '.svelte-kit'].includes(entry.name))
21
+ continue;
22
+ const file = path.join(root, entry.name);
23
+ if (entry.isSymbolicLink())
24
+ throw new Error(`Migration refuses symbolic links in source: ${file}`);
25
+ if (entry.isDirectory())
26
+ out.push(...(await files(file)));
27
+ else
28
+ out.push(file);
29
+ }
30
+ return out.sort();
31
+ }
32
+ function pascalFile(file) {
33
+ if (file === 'index.ts' || file === 'index.js')
34
+ return file;
35
+ const dot = file.indexOf('.');
36
+ const stem = (dot < 0 ? file : file.slice(0, dot)).replace(/^_(?=.*)/, '');
37
+ return normalizePascalName(stem) + (dot < 0 ? '' : file.slice(dot));
38
+ }
39
+ function partsPath(parts) {
40
+ return parts.map((p, i) => (i === parts.length - 1 ? pascalFile(p) : normalizePascalName(p))).join('/');
41
+ }
42
+ export async function planMigration(root = process.cwd()) {
43
+ root = path.resolve(root);
44
+ const app = path.join(root, 'src/app');
45
+ const originals = new Map();
46
+ for (const file of await files(path.join(root, 'src')))
47
+ originals.set(file, await fs.readFile(file));
48
+ for (const name of [
49
+ 'azure-net.config.ts',
50
+ 'azure-net.config.js',
51
+ 'svelte.config.js',
52
+ 'svelte.config.ts',
53
+ 'svelte.config.mjs',
54
+ 'tsconfig.json',
55
+ 'package.json',
56
+ ...VITE_CONFIGS
57
+ ]) {
58
+ try {
59
+ originals.set(path.join(root, name), await fs.readFile(path.join(root, name)));
60
+ }
61
+ catch (error) {
62
+ if (error.code !== 'ENOENT')
63
+ throw error;
64
+ }
65
+ }
66
+ const issues = [];
67
+ const issueKeys = new Set();
68
+ const issue = (file, reason) => {
69
+ const key = file + reason;
70
+ if (!issueKeys.has(key)) {
71
+ issueKeys.add(key);
72
+ issues.push({ file, reason });
73
+ }
74
+ };
75
+ const mapping = new Map();
76
+ const workspaces = new Set();
77
+ const contexts = new Set(['SharedKernel']);
78
+ const aliases = new Map([
79
+ ['$lib', path.join(root, 'src/lib')],
80
+ ['$program', path.join(root, 'src/program.ts')]
81
+ ]);
82
+ const oldRoots = new Set();
83
+ for (const file of originals.keys()) {
84
+ const relative = slash(path.relative(app, file));
85
+ if (relative.startsWith('../'))
86
+ continue;
87
+ const first = relative.split('/')[0];
88
+ if (!['Contexts', 'Presentation', 'Platform'].includes(first))
89
+ oldRoots.add(first);
90
+ }
91
+ for (const folder of oldRoots)
92
+ aliases.set(`$${folder}`, path.join(app, folder));
93
+ const svelteConfig = [...originals.keys()].find((p) => /svelte\.config\.(?:js|ts|mjs)$/.test(p));
94
+ const viteConfig = [...originals.keys()].find((p) => VITE_CONFIGS.includes(path.basename(p)));
95
+ const configPath = svelteConfig ?? viteConfig;
96
+ const configAst = ts.createSourceFile(configPath ?? 'config.ts', configPath ? originals.get(configPath).toString() : '', ts.ScriptTarget.Latest, true);
97
+ const packageImports = JSON.parse(originals.get(path.join(root, 'package.json'))?.toString() ?? '{}').imports ?? {};
98
+ const readAliases = (node) => {
99
+ if (ts.isPropertyAssignment(node) && propName(node.name) === 'alias' && ts.isObjectLiteralExpression(node.initializer)) {
100
+ for (const p of node.initializer.properties)
101
+ if (ts.isPropertyAssignment(p) && ts.isStringLiteral(p.initializer)) {
102
+ const name = propName(p.name);
103
+ if (name)
104
+ aliases.set(name, path.resolve(root, p.initializer.text));
105
+ }
106
+ }
107
+ ts.forEachChild(node, readAliases);
108
+ };
109
+ readAliases(configAst);
110
+ if (viteConfig && svelteConfig)
111
+ readAliases(ts.createSourceFile(viteConfig, originals.get(viteConfig).toString(), ts.ScriptTarget.Latest, true));
112
+ function resolve(from, specifier) {
113
+ const clean = specifier[0] + specifier.slice(1).split(/[?#]/)[0];
114
+ let base;
115
+ if (clean.startsWith('.'))
116
+ base = path.resolve(path.dirname(from), clean);
117
+ else if (clean.startsWith('#'))
118
+ base = resolvePackageImport(packageImports, clean, root) ?? undefined;
119
+ else
120
+ for (const [alias, target] of [...aliases].sort((a, b) => b[0].length - a[0].length)) {
121
+ if (clean === alias || clean.startsWith(alias + '/')) {
122
+ base = path.join(target, clean.slice(alias.length));
123
+ break;
124
+ }
125
+ }
126
+ if (!base)
127
+ return undefined;
128
+ for (const candidate of [
129
+ base,
130
+ ...['.ts', '.js', '.svelte', '.scss', '.css', '/index.ts', '/index.js'].map((ext) => base + ext),
131
+ ...['.scss', '.sass', '.css'].flatMap((ext) => [
132
+ path.join(path.dirname(base), '_' + path.basename(base) + ext),
133
+ base + '/index' + ext,
134
+ base + '/_index' + ext
135
+ ]),
136
+ base.replace(/\.js$/, '.ts')
137
+ ]) {
138
+ if (originals.has(candidate))
139
+ return candidate;
140
+ }
141
+ return undefined;
142
+ }
143
+ const domains = new Map();
144
+ for (const file of originals.keys()) {
145
+ const m = slash(path.relative(app, file)).match(/^([^/]+)\/layers\/domain\/([^/]+)\//);
146
+ if (m) {
147
+ const names = domains.get(m[1]) ?? new Set();
148
+ names.add(m[2]);
149
+ domains.set(m[1], names);
150
+ }
151
+ }
152
+ const backup = (file, why) => {
153
+ issue(file, why);
154
+ return path.join(app, '.backup', path.relative(app, file));
155
+ };
156
+ for (const file of originals.keys()) {
157
+ const rel = slash(path.relative(app, file));
158
+ const p = rel.split('/');
159
+ if (!oldRoots.has(p[0]))
160
+ continue;
161
+ const area = p[0];
162
+ const workspace = /^(shared-kernel|shared)$/i.test(area) ? 'Shared' : normalizePascalName(area);
163
+ let dest;
164
+ try {
165
+ if (p[1] === 'ui') {
166
+ workspaces.add(workspace);
167
+ dest = `Presentation/${workspace}/Ui/${partsPath(p.slice(2))}`;
168
+ }
169
+ else if (p[1] === 'layers' && p[2] === 'presentation') {
170
+ workspaces.add(workspace);
171
+ dest = `Presentation/${workspace}/Scopes/${partsPath(p.slice(3))}`;
172
+ }
173
+ else if (p[1] === 'layers' && p[2] === 'domain' && p.length >= 5) {
174
+ const context = normalizePascalName(p[3]);
175
+ contexts.add(context);
176
+ const folders = { model: 'Entities', models: 'Entities', ports: 'Ports', enums: 'Enums', enum: 'Enums' };
177
+ const rest = [...p.slice(4)];
178
+ if (folders[rest[0]])
179
+ rest[0] = folders[rest[0]];
180
+ dest = `Contexts/${context}/Domain/${partsPath(rest)}`;
181
+ if (/\/I[A-Z][^/]*\.ts$/.test(dest))
182
+ dest = dest.replace(/\/I([A-Z][^/]*)\.ts$/, '/$1.ts');
183
+ }
184
+ else if (p[1] === 'layers' &&
185
+ (p.includes('repositories') || p.includes('repositores') || p.includes('use-cases')) &&
186
+ !/^index\./.test(p.at(-1))) {
187
+ const base = path.basename(file).replace(/(?:Repository|UseCases?)\.[^.]+$/, '');
188
+ const match = [...(domains.get(area) ?? [])].filter((d) => normalizePascalName(d) === base);
189
+ if (match.length === 1) {
190
+ contexts.add(base);
191
+ dest = `Contexts/${base}/${p.includes('use-cases') ? 'Application/UseCases' : 'Infrastructure/Repositories'}/${pascalFile(p.at(-1))}`;
192
+ }
193
+ }
194
+ else if (p[1] === 'runtime')
195
+ dest = `Platform/Bootstrap/${partsPath(p.slice(p[2] === 'bootstrap' ? 3 : 2))}`;
196
+ else if (['essentials', 'internal'].includes(p[1])) {
197
+ if (p[2] === 'foundation') {
198
+ const rest = p.slice(p[3] === 'constructs' ? 4 : 3);
199
+ if (rest[0] !== 'provider' && rest[0] !== 'providers')
200
+ dest = `Platform/Foundation/${partsPath(rest)}`;
201
+ }
202
+ else if (p[2] === 'localization' || p[2] === 'pipelines')
203
+ dest = `Platform/Bootstrap/${partsPath(p.slice(2))}`;
204
+ else if (p[2] === 'config')
205
+ dest = `Platform/Foundation/${partsPath(p.slice(2))}`;
206
+ }
207
+ }
208
+ catch {
209
+ /* Invalid names are archived instead of guessed. */
210
+ }
211
+ mapping.set(file, dest ? path.join(app, dest) : backup(file, 'No unambiguous target (legacy provider, aggregate barrel, or unsupported folder).'));
212
+ }
213
+ // No last-writer wins: preserve every conflicting file for manual resolution.
214
+ const destinations = new Map();
215
+ for (const [from, to] of mapping) {
216
+ const key = to.toLowerCase();
217
+ const list = destinations.get(key) ?? [];
218
+ list.push(from);
219
+ destinations.set(key, list);
220
+ }
221
+ for (const list of destinations.values()) {
222
+ const target = mapping.get(list[0]);
223
+ if (target.includes(`${path.sep}.backup${path.sep}`))
224
+ continue;
225
+ if (list.length > 1 || (originals.has(target) && !mapping.has(target))) {
226
+ // Domain barrels are safely reconstituted later from each original export list.
227
+ if (list.length > 1 &&
228
+ path.basename(target) === 'index.ts' &&
229
+ !originals.has(target) &&
230
+ list.every((file) => ts.createSourceFile(file, originals.get(file).toString(), ts.ScriptTarget.Latest, true).statements.every(ts.isExportDeclaration)))
231
+ continue;
232
+ for (const file of list)
233
+ mapping.set(file, backup(file, `Target collision: ${target}; no file overwritten.`));
234
+ }
235
+ }
236
+ const isBackup = (file) => file.includes(`${path.sep}.backup${path.sep}`);
237
+ function specifier(to, from) {
238
+ const relative = slash(path.relative(app, to));
239
+ let value;
240
+ const first = relative.split('/')[0];
241
+ const prefix = { Contexts: '#contexts', Presentation: '#presentation', Platform: '#platform' };
242
+ if (prefix[first])
243
+ value = prefix[first] + relative.slice(first.length);
244
+ else {
245
+ value = slash(path.relative(path.dirname(from), to));
246
+ if (!value.startsWith('.'))
247
+ value = './' + value;
248
+ }
249
+ return value.replace(/\.(?:ts|js)$/, '').replace(/\/index$/, '');
250
+ }
251
+ function exportedSymbol(file, name, seen = new Set()) {
252
+ const key = `${file}:${name}`;
253
+ if (seen.has(key))
254
+ return undefined;
255
+ seen.add(key);
256
+ if (file.endsWith('.svelte') && name === 'default')
257
+ return { file, name };
258
+ const source = parseSource(originals.get(file)?.toString() ?? '', file);
259
+ for (const statement of source.statements) {
260
+ if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
261
+ const target = resolve(file, statement.moduleSpecifier.text);
262
+ if (!target)
263
+ continue;
264
+ if (!statement.exportClause) {
265
+ const found = exportedSymbol(target, name, seen);
266
+ if (found)
267
+ return found;
268
+ }
269
+ else if (ts.isNamedExports(statement.exportClause))
270
+ for (const element of statement.exportClause.elements) {
271
+ if (element.name.text === name)
272
+ return (exportedSymbol(target, element.propertyName?.text ?? name, seen) ?? {
273
+ file: target,
274
+ name: element.propertyName?.text ?? name
275
+ });
276
+ }
277
+ }
278
+ else {
279
+ if (name === 'default' && ts.isExportAssignment(statement))
280
+ return { file, name };
281
+ if ('name' in statement && propName(statement.name) === name)
282
+ return { file, name };
283
+ if (ts.isVariableStatement(statement) && statement.declarationList.declarations.some((d) => propName(d.name) === name))
284
+ return { file, name };
285
+ }
286
+ }
287
+ return undefined;
288
+ }
289
+ const presenterNames = new Map();
290
+ const upgradedSources = new Map();
291
+ const factorySymbols = new Map();
292
+ for (const [file, bytes] of originals) {
293
+ if (!/\.[jt]s$/.test(file))
294
+ continue;
295
+ const source = parseSource(bytes.toString(), file);
296
+ const creators = new Set();
297
+ for (const statement of source.statements) {
298
+ if (!ts.isImportDeclaration(statement) ||
299
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
300
+ !['@azure-net/kit/delivery', '@azure-net/edges'].includes(statement.moduleSpecifier.text))
301
+ continue;
302
+ const bindings = statement.importClause?.namedBindings;
303
+ if (bindings && ts.isNamedImports(bindings))
304
+ for (const binding of bindings.elements)
305
+ if ((binding.propertyName ?? binding.name).text === 'createPresenterFactory')
306
+ creators.add(binding.name.text);
307
+ }
308
+ const names = new Set();
309
+ for (const statement of source.statements) {
310
+ if (!ts.isVariableStatement(statement))
311
+ continue;
312
+ for (const declaration of statement.declarationList.declarations) {
313
+ const init = declaration.initializer;
314
+ if (ts.isIdentifier(declaration.name) &&
315
+ init &&
316
+ ts.isCallExpression(init) &&
317
+ ts.isIdentifier(init.expression) &&
318
+ creators.has(init.expression.text))
319
+ names.add(declaration.name.text);
320
+ }
321
+ }
322
+ if (names.size)
323
+ factorySymbols.set(file, names);
324
+ }
325
+ for (const [file, bytes] of originals) {
326
+ if (!file.startsWith(path.join(root, 'src') + path.sep) || !/\.(?:ts|js|svelte)$/.test(file))
327
+ continue;
328
+ if (mapping.has(file) && isBackup(mapping.get(file)))
329
+ continue;
330
+ const source = parseSource(bytes.toString(), file);
331
+ const factories = new Set();
332
+ for (const statement of source.statements) {
333
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
334
+ continue;
335
+ const bindings = statement.importClause?.namedBindings;
336
+ if (!bindings || !ts.isNamedImports(bindings))
337
+ continue;
338
+ for (const binding of bindings.elements) {
339
+ const name = (binding.propertyName ?? binding.name).text;
340
+ const target = resolve(file, statement.moduleSpecifier.text);
341
+ const symbol = target && exportedSymbol(target, name);
342
+ if ((!target &&
343
+ name === 'createPresenter' &&
344
+ ['@azure-net/kit/delivery', '@azure-net/edges'].includes(statement.moduleSpecifier.text)) ||
345
+ (symbol && factorySymbols.get(symbol.file)?.has(symbol.name)))
346
+ factories.add(binding.name.text);
347
+ }
348
+ }
349
+ const result = file.endsWith('.svelte')
350
+ ? { content: bytes.toString(), converted: new Map(), issues: [] }
351
+ : migratePresenterSource(bytes.toString(), file, factories);
352
+ if (result.converted.size)
353
+ presenterNames.set(file, result.converted);
354
+ for (const reason of result.issues)
355
+ issue(file, reason);
356
+ let upgraded = result.content;
357
+ try {
358
+ upgraded = migrateAsyncHelpers(upgraded, file);
359
+ }
360
+ catch (error) {
361
+ issue(file, `Async helpers need manual migration: ${String(error)}`);
362
+ }
363
+ if (upgraded !== bytes.toString())
364
+ upgradedSources.set(file, upgraded);
365
+ }
366
+ const providers = new Map();
367
+ for (const [file, bytes] of originals)
368
+ if (/Provider\.[jt]s$/.test(file) && mapping.has(file)) {
369
+ const source = parseSource(bytes.toString(), file);
370
+ const imports = new Map();
371
+ for (const s of source.statements)
372
+ if (ts.isImportDeclaration(s) && ts.isStringLiteral(s.moduleSpecifier)) {
373
+ const target = resolve(file, s.moduleSpecifier.text);
374
+ if (s.importClause?.name)
375
+ imports.set(s.importClause.name.text, { file: target, source: s.moduleSpecifier.text, exported: 'default' });
376
+ const bindings = s.importClause?.namedBindings;
377
+ if (bindings && ts.isNamedImports(bindings))
378
+ for (const e of bindings.elements)
379
+ imports.set(e.name.text, {
380
+ file: target && (exportedSymbol(target, e.propertyName?.text ?? e.name.text)?.file ?? target),
381
+ source: s.moduleSpecifier.text,
382
+ exported: (target && exportedSymbol(target, e.propertyName?.text ?? e.name.text)?.name) || e.propertyName?.text || e.name.text
383
+ });
384
+ }
385
+ for (const statement of source.statements)
386
+ if (ts.isVariableStatement(statement))
387
+ for (const d of statement.declarationList.declarations) {
388
+ if (!ts.isIdentifier(d.name) || !d.initializer || !ts.isCallExpression(d.initializer))
389
+ continue;
390
+ if (!ts.isIdentifier(d.initializer.expression) ||
391
+ !['createBoundaryProvider', 'CreateDiContainer'].includes(imports.get(d.initializer.expression.text)?.exported ?? ''))
392
+ continue;
393
+ if (source.statements.some((s) => ts.isImportDeclaration(s) && s.importClause?.namedBindings && ts.isNamespaceImport(s.importClause.namedBindings))) {
394
+ issue(file, 'Namespace imports in a provider require manual DI migration.');
395
+ continue;
396
+ }
397
+ const settings = d.initializer.arguments[1];
398
+ if (!settings || !ts.isObjectLiteralExpression(settings))
399
+ continue;
400
+ const register = settings.properties.find((p) => propName(p.name) === 'register');
401
+ if (!register || !ts.isPropertyAssignment(register) || !ts.isArrowFunction(register.initializer))
402
+ continue;
403
+ const body = register.initializer.body;
404
+ if (ts.isBlock(body)) {
405
+ issue(file, 'Custom block register requires manual DI migration.');
406
+ continue;
407
+ }
408
+ const values = unwrap(body);
409
+ if (!ts.isObjectLiteralExpression(values))
410
+ continue;
411
+ if (settings.properties.some((p) => !['dependsOn', 'register'].includes(propName(p.name) ?? ''))) {
412
+ issue(file, 'Provider has custom settings/boot; not inlined.');
413
+ continue;
414
+ }
415
+ if (source.statements.some((s) => !ts.isImportDeclaration(s) && s !== statement && !ts.isEmptyStatement(s))) {
416
+ issue(file, 'Provider has extra module declarations; manual migration required.');
417
+ continue;
418
+ }
419
+ providers.set(d.name.text, { file, name: d.name.text, source, settings, values, imports });
420
+ }
421
+ }
422
+ const diFile = path.join(app, 'Platform/Foundation/DI/ApplicationProvider.ts');
423
+ const diImports = new Set(["import { CreateDiContainer } from '@azure-net/kit';"]);
424
+ const expressions = new Map();
425
+ const locals = [];
426
+ const cached = new Map();
427
+ const active = new Set();
428
+ let counter = 0;
429
+ function expand(provider, key) {
430
+ const id = `${provider.name}.${key}`;
431
+ if (cached.has(id))
432
+ return cached.get(id);
433
+ if (active.has(id))
434
+ throw new Error(`Circular DI: ${id}`);
435
+ active.add(id);
436
+ const property = provider.values.properties.find((p) => propName(p.name) === key);
437
+ if (!property ||
438
+ !ts.isPropertyAssignment(property) ||
439
+ !ts.isArrowFunction(property.initializer) ||
440
+ property.initializer.parameters.length ||
441
+ ts.isBlock(property.initializer.body))
442
+ throw new Error(`Unsupported factory ${id}`);
443
+ const expression = property.initializer.body;
444
+ const result = ts.transform(expression, [
445
+ (context) => {
446
+ const visit = (node) => {
447
+ if (ts.isShorthandPropertyAssignment(node) && provider.imports.has(node.name.text)) {
448
+ const visited = visit(node.name);
449
+ const imported = provider.imports.get(node.name.text);
450
+ if (!providers.has(imported.exported))
451
+ return ts.factory.createPropertyAssignment(node.name.text, ts.factory.createIdentifier(`M${[...providers.keys()].indexOf(provider.name)}_${visited.text}`));
452
+ }
453
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
454
+ const imported = provider.imports.get(node.expression.text);
455
+ const dep = providers.get(imported?.exported ?? node.expression.text);
456
+ if (dep)
457
+ return ts.factory.createCallExpression(ts.factory.createIdentifier(expand(dep, node.name.text)), undefined, []);
458
+ }
459
+ if (ts.isIdentifier(node) &&
460
+ !(node.parent &&
461
+ ((ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) ||
462
+ (ts.isPropertyAssignment(node.parent) && node.parent.name === node)))) {
463
+ const imported = provider.imports.get(node.text);
464
+ if (imported &&
465
+ !providers.has(imported.exported) &&
466
+ !['createBoundaryProvider', 'CreateDiContainer'].includes(imported.exported)) {
467
+ const to = imported.file && mapping.get(imported.file);
468
+ if (to && isBackup(to))
469
+ throw new Error(`Dependency is archived: ${imported.file}`);
470
+ const local = `M${[...providers.keys()].indexOf(provider.name)}_${node.text}`;
471
+ const source = imported.file ? specifier(to ?? imported.file, diFile) : imported.source;
472
+ diImports.add(imported.exported === 'default'
473
+ ? `import ${local} from ${JSON.stringify(source)};`
474
+ : `import { ${imported.exported} as ${local} } from ${JSON.stringify(source)};`);
475
+ if (node.parent && ts.isShorthandPropertyAssignment(node.parent))
476
+ return node;
477
+ return ts.factory.createIdentifier(local);
478
+ }
479
+ }
480
+ return ts.visitEachChild(node, visit, context);
481
+ };
482
+ return (node) => ts.visitNode(node, visit);
483
+ }
484
+ ]);
485
+ const code = printer.printNode(ts.EmitHint.Expression, result.transformed[0], provider.source);
486
+ result.dispose();
487
+ expressions.set(id, code);
488
+ const local = `get${++counter}_${key}`;
489
+ // Private lazy getters preserve original per-service sharing without nested providers.
490
+ if (/DatasourceProvider$/.test(provider.name)) {
491
+ locals.push(`const datasource${counter} = ${code};\nconst ${local} = () => datasource${counter};`);
492
+ }
493
+ else if (!/ApplicationProvider$/.test(provider.name))
494
+ locals.push(`const ${local} = (() => {\nlet initialized = false;\nlet value: ReturnType<typeof create>;\nconst create = () => (${code});\nreturn () => { if (!initialized) { value = create(); initialized = true; } return value; };\n})();`);
495
+ cached.set(id, local);
496
+ active.delete(id);
497
+ return local;
498
+ }
499
+ const publicServices = new Map();
500
+ const converted = new Set();
501
+ for (const provider of providers.values())
502
+ if (/ApplicationProvider$/.test(provider.name)) {
503
+ const checkpoint = {
504
+ locals: locals.length,
505
+ counter,
506
+ cached: new Map(cached),
507
+ expressions: new Map(expressions),
508
+ imports: new Set(diImports)
509
+ };
510
+ try {
511
+ const entries = [];
512
+ for (const property of provider.values.properties) {
513
+ const key = propName(property.name);
514
+ if (!key || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key))
515
+ throw new Error('Unsupported service key');
516
+ if (publicServices.has(key))
517
+ throw new Error(`Duplicate application service ${key}`);
518
+ expand(provider, key);
519
+ entries.push([key, `() => (${expressions.get(`${provider.name}.${key}`)})`]);
520
+ }
521
+ for (const [key, value] of entries)
522
+ publicServices.set(key, value);
523
+ converted.add(provider.name);
524
+ }
525
+ catch (error) {
526
+ locals.length = checkpoint.locals;
527
+ counter = checkpoint.counter;
528
+ cached.clear();
529
+ for (const [key, value] of checkpoint.cached)
530
+ cached.set(key, value);
531
+ expressions.clear();
532
+ for (const [key, value] of checkpoint.expressions)
533
+ expressions.set(key, value);
534
+ diImports.clear();
535
+ for (const value of checkpoint.imports)
536
+ diImports.add(value);
537
+ active.clear();
538
+ issue(provider.file, `DI not migrated: ${String(error)}`);
539
+ }
540
+ }
541
+ const writes = new Map();
542
+ const replacements = [];
543
+ function namespacePresenterName(file, name, seen = new Set()) {
544
+ if (seen.has(file))
545
+ return undefined;
546
+ seen.add(file);
547
+ const direct = presenterNames.get(file)?.get(name);
548
+ if (direct)
549
+ return direct;
550
+ const source = parseSource(originals.get(file)?.toString() ?? '', file);
551
+ for (const statement of source.statements) {
552
+ if (!ts.isExportDeclaration(statement))
553
+ continue;
554
+ // Named exports keep their public alias when rewritten below.
555
+ if (statement.exportClause &&
556
+ ts.isNamedExports(statement.exportClause) &&
557
+ statement.exportClause.elements.some((entry) => entry.name.text === name))
558
+ return name;
559
+ }
560
+ for (const statement of source.statements) {
561
+ if (!ts.isExportDeclaration(statement) ||
562
+ statement.exportClause ||
563
+ !statement.moduleSpecifier ||
564
+ !ts.isStringLiteral(statement.moduleSpecifier))
565
+ continue;
566
+ const target = resolve(file, statement.moduleSpecifier.text);
567
+ const renamed = target && namespacePresenterName(target, name, seen);
568
+ if (renamed)
569
+ return renamed;
570
+ }
571
+ return undefined;
572
+ }
573
+ function rewrite(file, destination, input) {
574
+ replacements.length = 0;
575
+ const source = parseSource(input, file);
576
+ const namespaces = new Map();
577
+ for (const statement of source.statements) {
578
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
579
+ continue;
580
+ const bindings = statement.importClause?.namedBindings;
581
+ const target = resolve(file, statement.moduleSpecifier.text);
582
+ if (target && bindings && ts.isNamespaceImport(bindings))
583
+ namespaces.set(bindings.name.text, target);
584
+ }
585
+ const replacement = (start, end, text) => {
586
+ replacements.push({ start, end, text });
587
+ };
588
+ const checkBoundary = (to) => {
589
+ const a = slash(path.relative(app, destination)).split('/'), b = slash(path.relative(app, to)).split('/');
590
+ if (a[0] === 'Contexts' && b[0] === 'Contexts' && a[1] !== b[1] && b[1] !== 'SharedKernel')
591
+ issue(destination, `Review cross-context dependency on ${to}; not moved into SharedKernel automatically.`);
592
+ if (a[0] === 'Presentation' && b[0] === 'Presentation' && a[1] !== b[1] && b[1] !== 'Shared')
593
+ issue(destination, `Review cross-workspace dependency on ${to}.`);
594
+ };
595
+ const newTarget = (value) => {
596
+ const resolved = resolve(file, value);
597
+ const target = resolved && (mapping.get(resolved) ?? resolved);
598
+ if (!target) {
599
+ if (value.startsWith('.') || [...aliases.keys()].some((a) => value === a || value.startsWith(a + '/')))
600
+ issue(destination, `Unresolved local module/asset: ${value}`);
601
+ return undefined;
602
+ }
603
+ if (isBackup(target)) {
604
+ issue(destination, `Import needs review: ${value} -> ${target}`);
605
+ return undefined;
606
+ }
607
+ checkBoundary(target);
608
+ return specifier(target, destination) + (value.slice(1).match(/[?#].*$/)?.[0] ?? '');
609
+ };
610
+ const visit = (node) => {
611
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
612
+ const target = namespaces.get(node.expression.text);
613
+ const name = target && namespacePresenterName(target, node.name.text);
614
+ if (name)
615
+ replacement(node.name.getStart(source), node.name.end, name);
616
+ }
617
+ if (ts.isElementAccessExpression(node) && ts.isIdentifier(node.expression) && ts.isStringLiteral(node.argumentExpression)) {
618
+ const target = namespaces.get(node.expression.text);
619
+ const name = target && namespacePresenterName(target, node.argumentExpression.text);
620
+ if (name)
621
+ replacement(node.argumentExpression.getStart(source), node.argumentExpression.end, JSON.stringify(name));
622
+ }
623
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
624
+ const target = resolve(file, node.moduleSpecifier.text);
625
+ const bindings = node.importClause?.namedBindings;
626
+ if (target && bindings && ts.isNamedImports(bindings) && !node.importClause?.name) {
627
+ const groups = new Map();
628
+ for (const e of bindings.elements) {
629
+ const name = e.propertyName?.text ?? e.name.text;
630
+ const symbol = exportedSymbol(target, name);
631
+ const declared = symbol?.file ?? target;
632
+ const to = mapping.get(declared) ?? declared;
633
+ const isProvider = converted.has(name) && providers.get(name)?.file === declared;
634
+ const spec = isProvider ? '#platform/Foundation/DI' : isBackup(to) ? undefined : specifier(to, destination);
635
+ if (!spec) {
636
+ issue(destination, `Unresolved imported symbol ${name} from ${node.moduleSpecifier.text}`);
637
+ continue;
638
+ }
639
+ checkBoundary(to);
640
+ const imported = isProvider
641
+ ? 'ApplicationProvider'
642
+ : (presenterNames.get(declared)?.get(symbol?.name ?? name) ?? symbol?.name ?? name);
643
+ const parts = groups.get(spec) ?? [];
644
+ parts.push(`${!node.importClause?.isTypeOnly && e.isTypeOnly ? 'type ' : ''}${imported}${imported === e.name.text ? '' : ` as ${e.name.text}`}`);
645
+ groups.set(spec, parts);
646
+ }
647
+ if ([...groups.values()].reduce((n, g) => n + g.length, 0) === bindings.elements.length) {
648
+ replacement(node.getStart(source), node.end, [...groups]
649
+ .map(([spec, names]) => `import ${node.importClause?.isTypeOnly ? 'type ' : ''}{ ${names.join(', ')} } from ${JSON.stringify(spec)};`)
650
+ .join('\n'));
651
+ return;
652
+ }
653
+ }
654
+ const next = newTarget(node.moduleSpecifier.text);
655
+ if (next)
656
+ replacement(node.moduleSpecifier.getStart(source), node.moduleSpecifier.end, JSON.stringify(next));
657
+ return;
658
+ }
659
+ if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
660
+ const target = resolve(file, node.moduleSpecifier.text);
661
+ if (target && node.exportClause && ts.isNamedExports(node.exportClause)) {
662
+ for (const entry of node.exportClause.elements) {
663
+ const name = (entry.propertyName ?? entry.name).text;
664
+ const symbol = exportedSymbol(target, name);
665
+ const renamed = symbol && presenterNames.get(symbol.file)?.get(symbol.name);
666
+ if (renamed)
667
+ replacement(entry.getStart(source), entry.end, `${entry.isTypeOnly ? 'type ' : ''}${renamed} as ${entry.name.text}`);
668
+ }
669
+ }
670
+ const next = newTarget(node.moduleSpecifier.text);
671
+ if (next)
672
+ replacement(node.moduleSpecifier.getStart(source), node.moduleSpecifier.end, JSON.stringify(next));
673
+ return;
674
+ }
675
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'URL') {
676
+ const arg = node.arguments?.[0];
677
+ if (arg && ts.isStringLiteral(arg)) {
678
+ const resolved = resolve(file, arg.text);
679
+ const target = resolved && mapping.get(resolved);
680
+ if (target && !isBackup(target)) {
681
+ let relative = slash(path.relative(path.dirname(destination), target));
682
+ if (!relative.startsWith('.'))
683
+ relative = './' + relative;
684
+ replacement(arg.getStart(source), arg.end, JSON.stringify(relative));
685
+ }
686
+ }
687
+ }
688
+ if (ts.isCallExpression(node) &&
689
+ ts.isPropertyAccessExpression(node.expression) &&
690
+ node.expression.getText(source).startsWith('import.meta.glob'))
691
+ issue(destination, 'Review import.meta.glob patterns after folder relocation.');
692
+ if (ts.isCallExpression(node) &&
693
+ (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
694
+ const arg = node.arguments[0];
695
+ if (arg && (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg))) {
696
+ const target = resolve(file, arg.text);
697
+ if (target && presenterNames.has(target))
698
+ issue(destination, `Dynamic presenter import: use the new use{Name}Presenter export from ${arg.text}.`);
699
+ const next = newTarget(arg.text);
700
+ if (next)
701
+ replacement(arg.getStart(source), arg.end, JSON.stringify(next));
702
+ }
703
+ else
704
+ issue(destination, 'Computed import/require needs manual review.');
705
+ }
706
+ if (ts.isStringLiteral(node) && node.parent && ts.isLiteralTypeNode(node.parent) && ts.isImportTypeNode(node.parent.parent)) {
707
+ const next = newTarget(node.text);
708
+ if (next)
709
+ replacement(node.getStart(source), node.end, JSON.stringify(next));
710
+ }
711
+ ts.forEachChild(node, visit);
712
+ };
713
+ visit(source);
714
+ let output = input;
715
+ for (const edit of replacements.sort((a, b) => b.start - a.start))
716
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
717
+ // Local style/asset references are filesystem links too; ordinary strings are not rewritten.
718
+ output = output.replace(/((?:@use|@forward|@import)\s+|\b(?:src|href)\s*=\s*|\burl\(\s*)(['"])([^'"\n]+)\2/g, (whole, prefix, quote, value, offset) => {
719
+ const resolved = resolve(file, value) ?? (/^@/.test(prefix) ? resolve(file, './' + value) : undefined);
720
+ const target = resolved && (mapping.get(resolved) ?? resolved);
721
+ if (!target || isBackup(target))
722
+ return whole;
723
+ let relative = slash(path.relative(path.dirname(destination), target));
724
+ if (!relative.startsWith('.'))
725
+ relative = './' + relative;
726
+ const oldNamespace = path
727
+ .basename(value)
728
+ .replace(/^_/, '')
729
+ .replace(/\.(scss|sass|css)$/, '');
730
+ const keepNamespace = prefix.startsWith('@use') && !/^\s+as\b/.test(output.slice(offset + whole.length)) && /^[A-Za-z_][\w-]*$/.test(oldNamespace);
731
+ return prefix + quote + relative + quote + (keepNamespace ? ` as ${oldNamespace}` : '');
732
+ });
733
+ return output;
734
+ }
735
+ for (const [file, bytes] of originals) {
736
+ if (!file.startsWith(path.join(root, 'src') + path.sep))
737
+ continue;
738
+ const to = mapping.get(file) ?? file;
739
+ const output = isBackup(to) || !textFile.test(file) ? bytes : Buffer.from(rewrite(file, to, upgradedSources.get(file) ?? bytes.toString()));
740
+ if (writes.has(to) && path.basename(to) === 'index.ts') {
741
+ const barrel = ts.createSourceFile(to, writes.get(to).toString() + '\n' + output.toString(), ts.ScriptTarget.Latest, true);
742
+ writes.set(to, Buffer.from([...new Set(barrel.statements.map((statement) => printer.printNode(ts.EmitHint.Unspecified, statement, barrel)))].join('\n') +
743
+ '\n'));
744
+ }
745
+ else if (to !== file || !bytes.equals(output))
746
+ writes.set(to, output);
747
+ }
748
+ if (oldRoots.size) {
749
+ if (originals.has(diFile))
750
+ throw new Error(`Refusing to overwrite existing ${diFile}`);
751
+ writes.set(diFile, Buffer.from(`${[...diImports].join('\n')}\n\nexport const ApplicationProvider = CreateDiContainer('ApplicationProvider', {\nregister: () => {\n${locals.join('\n')}\nreturn {\n${[...publicServices].map(([name, getter]) => `${name}: ${getter}`).join(',\n')}\n};\n}\n});\n`));
752
+ writes.set(path.join(path.dirname(diFile), 'index.ts'), Buffer.from("export * from './ApplicationProvider';\n"));
753
+ workspaces.add('Shared');
754
+ for (const context of contexts)
755
+ for (const folder of CONTEXT_DIRS) {
756
+ const index = path.join(app, 'Contexts', context, folder, 'index.ts');
757
+ if (!writes.has(index) && !originals.has(index))
758
+ writes.set(index, Buffer.from('export {};\n'));
759
+ }
760
+ for (const workspace of workspaces)
761
+ for (const folder of ['Ui', 'Scopes', 'Shell']) {
762
+ const index = path.join(app, 'Presentation', workspace, folder, 'index.ts');
763
+ if (!writes.has(index) && !originals.has(index))
764
+ writes.set(index, Buffer.from('export {};\n'));
765
+ }
766
+ const packageFile = path.join(root, 'package.json');
767
+ writes.set(packageFile, Buffer.from(await mergePackageImports(originals.get(packageFile)?.toString() ?? '{}')));
768
+ }
769
+ const configFile = [...originals.keys()].find((p) => /azure-net\.config\.[jt]s$/.test(p)) ?? path.join(root, 'azure-net.config.ts');
770
+ let config = flattenConfigLiterals(originals.get(configFile)?.toString() ?? 'export default {};');
771
+ const configSource = ts.createSourceFile(configFile, config, ts.ScriptTarget.Latest, true);
772
+ const exported = configSource.statements.find(ts.isExportAssignment);
773
+ if (!exported || !ts.isObjectLiteralExpression(exported.expression))
774
+ throw new Error('Migration requires an object-literal default config; normalize it first.');
775
+ const kept = exported.expression.properties.filter((p) => !['defaultPresenterFactory', ...(oldRoots.size ? ['contexts', 'coreAlias', 'sharedAlias', 'presentationWorkspaces'] : [])].includes(propName(p.name) ?? ''));
776
+ if (oldRoots.size) {
777
+ kept.push(ts.factory.createPropertyAssignment('presentationWorkspaces', ts.factory.createArrayLiteralExpression([...workspaces].sort().map((w) => ts.factory.createStringLiteral(w)))));
778
+ if (!kept.some((p) => propName(p.name) === 'runChecksBeforePush'))
779
+ kept.push(ts.factory.createPropertyAssignment('runChecksBeforePush', ts.factory.createTrue()));
780
+ }
781
+ config =
782
+ config.slice(0, exported.expression.getStart(configSource)) +
783
+ printer.printNode(ts.EmitHint.Expression, ts.factory.createObjectLiteralExpression(kept, true), configSource) +
784
+ config.slice(exported.expression.end);
785
+ if (oldRoots.size || kept.length !== exported.expression.properties.length)
786
+ writes.set(configFile, Buffer.from(config));
787
+ const tsconfig = path.join(root, 'tsconfig.json');
788
+ if (oldRoots.size && originals.has(tsconfig)) {
789
+ const parsed = ts.parseConfigFileTextToJson(tsconfig, originals.get(tsconfig).toString());
790
+ if (parsed.error)
791
+ throw new Error('Cannot parse tsconfig.json safely.');
792
+ const exclude = parsed.config.exclude ?? ['node_modules', 'bower_components', 'jspm_packages'];
793
+ parsed.config.exclude = [...new Set([...exclude, 'src/app/.backup/**'])];
794
+ writes.set(tsconfig, Buffer.from(JSON.stringify(parsed.config, null, '\t') + '\n'));
795
+ }
796
+ if (oldRoots.size) {
797
+ const diSource = ts.createSourceFile(diFile, writes.get(diFile).toString(), ts.ScriptTarget.Latest, true);
798
+ writes.set(diFile, Buffer.from(printer.printFile(diSource)));
799
+ }
800
+ if (!oldRoots.size && !upgradedSources.size && !issues.length)
801
+ throw new Error('Nothing to migrate.');
802
+ const moves = [...mapping].map(([from, to]) => ({ from, to }));
803
+ for (const item of issues)
804
+ item.file = mapping.get(item.file) ?? item.file;
805
+ return {
806
+ root,
807
+ moves,
808
+ issues,
809
+ writes,
810
+ originals,
811
+ removes: [...mapping.keys()],
812
+ workspaces: [...workspaces]
813
+ };
814
+ }
815
+ export async function applyMigration(plan) {
816
+ // Verify the plan before creating any backup or touching user files.
817
+ for (const file of await files(path.join(plan.root, 'src')))
818
+ if (!plan.originals.has(file))
819
+ throw new Error(`New source file since planning: ${file}`);
820
+ for (const [file, bytes] of plan.originals)
821
+ if (!(await fs.readFile(file)).equals(bytes))
822
+ throw new Error(`File changed since planning: ${file}`);
823
+ for (const target of plan.writes.keys())
824
+ if (!plan.originals.has(target)) {
825
+ try {
826
+ await fs.access(target);
827
+ throw new Error(`Migration target already exists: ${target}`);
828
+ }
829
+ catch (error) {
830
+ if (error.code !== 'ENOENT')
831
+ throw error;
832
+ }
833
+ }
834
+ const snapshot = await fs.mkdtemp(path.join(await fs
835
+ .mkdir(path.join(plan.root, '.azure-net/migrations'), { recursive: true })
836
+ .then(() => path.join(plan.root, '.azure-net/migrations')), 'migration-'));
837
+ const touched = new Set([...plan.removes, ...plan.writes.keys()]);
838
+ for (const file of touched)
839
+ if (plan.originals.has(file)) {
840
+ const target = path.join(snapshot, 'original', path.relative(plan.root, file));
841
+ await fs.mkdir(path.dirname(target), { recursive: true });
842
+ await fs.writeFile(target, plan.originals.get(file));
843
+ }
844
+ await fs.writeFile(path.join(snapshot, 'manifest.json'), JSON.stringify({
845
+ moves: plan.moves,
846
+ generated: [...plan.writes.keys()].filter((p) => !plan.originals.has(p)),
847
+ issues: plan.issues,
848
+ hashes: [...touched]
849
+ .filter((p) => plan.originals.has(p))
850
+ .map((p) => ({ file: p, sha256: createHash('sha256').update(plan.originals.get(p)).digest('hex') }))
851
+ }, null, 2));
852
+ const report = path.join(snapshot, 'Report.md');
853
+ await fs.writeFile(report, `# Migration report\n\n${plan.moves.length} files classified. Original bytes: ./original. Unknown files: src/app/.backup.\n\nRecognized presenter factories are converted to classes with scoped RegisterProvider accessors. Closure state is preserved. Async helpers are imported directly. Review unsupported factories and lifecycle requirements. Run azure-net check internal and your package manager's check/build.\n\n## Review\n${plan.issues.map((i) => `- [${slash(path.relative(plan.root, i.file))}](${i.file}): ${i.reason}`).join('\n')}\n\n## Moves\n${plan.moves.map((m) => `- ${slash(path.relative(plan.root, m.from))} -> [${slash(path.relative(plan.root, m.to))}](${m.to})`).join('\n')}\n`);
854
+ try {
855
+ for (const [file, bytes] of plan.writes) {
856
+ await fs.mkdir(path.dirname(file), { recursive: true });
857
+ await fs.writeFile(file, bytes);
858
+ }
859
+ for (const file of plan.removes)
860
+ if (!plan.writes.has(file))
861
+ await fs.unlink(file);
862
+ }
863
+ catch (error) {
864
+ for (const file of touched) {
865
+ const original = plan.originals.get(file);
866
+ if (original) {
867
+ await fs.mkdir(path.dirname(file), { recursive: true });
868
+ await fs.writeFile(file, original);
869
+ }
870
+ else
871
+ await fs.rm(file, { force: true });
872
+ }
873
+ throw new Error(`Migration rolled back after I/O failure. Snapshot: ${snapshot}`, { cause: error });
874
+ }
875
+ async function removeEmpty(dir) {
876
+ for (const entry of await fs.readdir(dir, { withFileTypes: true }))
877
+ if (entry.isDirectory() && entry.name !== '.backup')
878
+ await removeEmpty(path.join(dir, entry.name));
879
+ if (!(await fs.readdir(dir)).length)
880
+ await fs.rmdir(dir);
881
+ }
882
+ try {
883
+ await removeEmpty(path.join(plan.root, 'src/app'));
884
+ }
885
+ catch (error) {
886
+ console.warn(`Files migrated, but empty directory cleanup failed: ${String(error)}`);
887
+ }
888
+ return report;
889
+ }
890
+ export default async function migrate(options = {}) {
891
+ const plan = await planMigration();
892
+ console.log(`Migration plan: ${plan.moves.length} files, ${plan.workspaces.length} workspaces, ${plan.issues.length} review items.`);
893
+ for (const move of plan.moves)
894
+ console.log(`${path.relative(plan.root, move.from)} -> ${path.relative(plan.root, move.to)}`);
895
+ if (options.dryRun) {
896
+ for (const issue of plan.issues)
897
+ console.warn(`${issue.file}: ${issue.reason}`);
898
+ return;
899
+ }
900
+ if (!options.yes) {
901
+ const answer = await prompts({
902
+ type: 'confirm',
903
+ name: 'apply',
904
+ initial: false,
905
+ message: 'Apply migration? Original files will be backed up; review/check/build is required afterwards.'
906
+ });
907
+ if (!answer.apply)
908
+ return;
909
+ }
910
+ const report = await applyMigration(plan);
911
+ console.log(`Migration complete. Review report: ${report}`);
912
+ for (const issue of plan.issues)
913
+ console.warn(`${issue.file}: ${issue.reason}`);
914
+ }