@craft-ng/dev-tools 0.5.1-beta.0 → 0.6.0-beta.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 (51) hide show
  1. package/README.md +66 -0
  2. package/generators.json +26 -0
  3. package/package.json +18 -1
  4. package/src/bin/craft.d.ts +2 -0
  5. package/src/bin/craft.js +166 -0
  6. package/src/bin/craft.js.map +1 -0
  7. package/src/eslint-rules/craft-computed-name-match.cjs +8 -125
  8. package/src/eslint-rules/craft-method-name-match.cjs +8 -166
  9. package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
  10. package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
  11. package/src/eslint-rules/craft-source-name-match.cjs +8 -0
  12. package/src/eslint-rules/global-exception-registry-match.cjs +78 -13
  13. package/src/eslint-rules/index.cjs +8 -2
  14. package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
  15. package/src/eslint-rules/require-primitive-generator-unwrap.cjs +267 -0
  16. package/src/generators/route/compat.d.ts +3 -0
  17. package/src/generators/route/compat.js +6 -0
  18. package/src/generators/route/compat.js.map +1 -0
  19. package/src/generators/route/generator.d.ts +31 -0
  20. package/src/generators/route/generator.js +434 -0
  21. package/src/generators/route/generator.js.map +1 -0
  22. package/src/generators/route/schema.json +50 -0
  23. package/src/generators/route/split-schema.json +19 -0
  24. package/src/index.d.ts +1 -0
  25. package/src/index.js +1 -0
  26. package/src/index.js.map +1 -1
  27. package/src/scripts/primitives/migrate-primitive-generators.d.ts +27 -0
  28. package/src/scripts/primitives/migrate-primitive-generators.js +315 -0
  29. package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
  30. package/src/scripts/primitives/migrate-primitive-generators.ts +402 -0
  31. package/src/scripts/primitives/migrate-primitives.js +36 -4
  32. package/src/scripts/primitives/migrate-primitives.js.map +1 -1
  33. package/src/scripts/primitives/migrate-primitives.spec.ts +43 -0
  34. package/src/scripts/primitives/migrate-primitives.ts +62 -4
  35. package/src/scripts/primitives/migration-diagnostic.d.ts +1 -1
  36. package/src/scripts/primitives/migration-diagnostic.ts +1 -0
  37. package/src/scripts/routes/migrate-routes.js +4 -4
  38. package/src/scripts/routes/migrate-routes.js.map +1 -1
  39. package/src/scripts/routes/migrate-routes.ts +4 -6
  40. package/src/scripts/routes/route-command.d.ts +75 -0
  41. package/src/scripts/routes/route-command.js +1187 -0
  42. package/src/scripts/routes/route-command.js.map +1 -0
  43. package/src/scripts/routes/route-command.spec.ts +553 -0
  44. package/src/scripts/routes/route-command.ts +1790 -0
  45. package/src/scripts/services/migrate-services.js +45 -11
  46. package/src/scripts/services/migrate-services.js.map +1 -1
  47. package/src/scripts/services/migrate-services.spec.ts +46 -4
  48. package/src/scripts/services/migrate-services.ts +64 -13
  49. package/src/scripts/services/migration-diagnostic.d.ts +1 -1
  50. package/src/scripts/services/migration-diagnostic.ts +1 -0
  51. package/src/eslint-rules/require-track-on-dependent-primitives.cjs +0 -219
@@ -0,0 +1,1187 @@
1
+ import { __awaiter } from "tslib";
2
+ import { execFile } from 'node:child_process';
3
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
4
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
5
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, } from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ import { names } from '@nx/devkit';
8
+ import { Node, Project, QuoteKind, SyntaxKind, ts, } from 'ts-morph';
9
+ import { findAngularDecoratedClass, transformSourceFile as generateAngularDependencies, } from '../angular-brand-codemod.js';
10
+ const execFileAsync = promisify(execFile);
11
+ const nodeRouteFileSystem = {
12
+ exists: existsSync,
13
+ listFiles: listFilesOnDisk,
14
+ read: (filePath) => existsSync(filePath) ? readFileSync(filePath, 'utf8') : undefined,
15
+ write: (filePath, content) => __awaiter(void 0, void 0, void 0, function* () {
16
+ yield mkdir(dirname(filePath), { recursive: true });
17
+ yield writeFile(filePath, content, 'utf8');
18
+ }),
19
+ };
20
+ export function runRouteAdd(options) {
21
+ return __awaiter(this, void 0, void 0, function* () {
22
+ var _a, _b, _c, _d;
23
+ const diagnostics = [];
24
+ const rootDir = resolve((_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd());
25
+ const fileSystem = (_b = options.fileSystem) !== null && _b !== void 0 ? _b : nodeRouteFileSystem;
26
+ const routePath = normalizeRoutePath(options.path);
27
+ if (!routePath || routePath === '/') {
28
+ return failed('INVALID_ARGUMENT', 'Route path must contain at least one segment.');
29
+ }
30
+ const actionCount = [
31
+ options.component,
32
+ options.createComponent,
33
+ options.redirectTo,
34
+ ].filter(Boolean).length;
35
+ if (actionCount !== 1) {
36
+ return failed('INVALID_ARGUMENT', 'Choose exactly one of --component, --create-component, or --redirect-to.');
37
+ }
38
+ const { project, tsConfigFilePath, sourceRoot } = createRouteProject(rootDir, options.project, fileSystem);
39
+ const initialTexts = snapshotProject(project);
40
+ const collections = discoverRouteCollections(project, sourceRoot);
41
+ const parent = resolveParentCollection(collections, options.parent, routePath, rootDir);
42
+ if ('diagnostic' in parent)
43
+ return resultFromDiagnostics([parent.diagnostic]);
44
+ const segments = routePath.slice(1).split('/');
45
+ const featureDecision = decideFeatureTarget(project, collections, parent.collection, options, segments, rootDir);
46
+ const targetFilePath = featureDecision.targetFilePath;
47
+ const touched = new Set([targetFilePath]);
48
+ if (featureDecision.parentMount)
49
+ touched.add(parent.collection.sourceFile.getFilePath());
50
+ let componentTarget;
51
+ if (options.component) {
52
+ componentTarget = parseComponentTarget(options.component, rootDir);
53
+ if (!componentTarget || !fileSystem.exists(componentTarget.filePath)) {
54
+ return failed('COMPONENT_NOT_FOUND', `Component target does not exist: ${options.component}`);
55
+ }
56
+ touched.add(componentTarget.filePath);
57
+ }
58
+ const plan = {
59
+ action: 'add',
60
+ summary: options.redirectTo
61
+ ? `Add redirect ${routePath} -> ${options.redirectTo}`
62
+ : `Add component route ${routePath}`,
63
+ files: [...touched],
64
+ };
65
+ printPlan(plan, (_c = options.log) !== null && _c !== void 0 ? _c : console.log, options.json === true);
66
+ if (!(yield shouldContinue(options, plan))) {
67
+ return { changedFiles: [], diagnostics, exitCode: 0, plan };
68
+ }
69
+ if (options.dryRun) {
70
+ return { changedFiles: [], diagnostics, exitCode: 0, plan };
71
+ }
72
+ const eslintBaseline = options.validate === false
73
+ ? new Set()
74
+ : yield captureRouteEslintBaseline(rootDir, sourceRoot, plan.files);
75
+ if (options.createComponent) {
76
+ const created = yield createAngularComponent(rootDir, options, resolveAngularProjectName(rootDir, options.project, sourceRoot));
77
+ if ('diagnostic' in created)
78
+ return resultFromDiagnostics([created.diagnostic], plan);
79
+ componentTarget = created.component;
80
+ touched.add(componentTarget.filePath);
81
+ addSourceFileFromWorkspace(project, componentTarget.filePath, fileSystem);
82
+ }
83
+ const targetCollection = ensureFeatureCollection(project, featureDecision, parent.collection);
84
+ if (hasRoutePath(targetCollection.routes, featureDecision.localRoutePath)) {
85
+ if (routeMatchesRequest(targetCollection.routes, featureDecision.localRoutePath, options, componentTarget, targetCollection.sourceFile.getFilePath())) {
86
+ return { changedFiles: [], diagnostics: [], exitCode: 0, plan };
87
+ }
88
+ return resultFromDiagnostics([
89
+ {
90
+ code: 'DUPLICATE_ROUTE',
91
+ message: `Route ${featureDecision.localRoutePath || '<root>'} already exists in ${targetCollection.routesName}.`,
92
+ filePath: targetCollection.sourceFile.getFilePath(),
93
+ routePath,
94
+ },
95
+ ], plan);
96
+ }
97
+ if (options.redirectTo) {
98
+ targetCollection.routes.addElement(`{ path: ${quote(featureDecision.localRoutePath)}, redirectTo: ${quote(options.redirectTo)}, pathMatch: 'full' }`);
99
+ }
100
+ else if (componentTarget) {
101
+ const componentSource = (_d = project.getSourceFile(componentTarget.filePath)) !== null && _d !== void 0 ? _d : addSourceFileFromWorkspace(project, componentTarget.filePath, fileSystem);
102
+ if (!componentSource ||
103
+ !ensureGenDeps(componentSource, componentTarget.className)) {
104
+ return failed('COMPONENT_NOT_FOUND', `Could not generate GenDeps_${componentTarget.className} in ${componentTarget.filePath}.`, plan);
105
+ }
106
+ const moduleSpecifier = relativeModuleSpecifier(targetCollection.sourceFile.getFilePath(), componentTarget.filePath);
107
+ targetCollection.routes.addElement(`craftRoute(${quote(featureDecision.localRoutePath)}, {
108
+ componentDeps: {} as import(${quote(moduleSpecifier)}).GenDeps_${componentTarget.className},
109
+ loadComponent: ({ withRetry }: CraftRouteLazyLoadHelpers) => withRetry(import(${quote(moduleSpecifier)})).then((m) => m.${componentTarget.className}),
110
+ })`);
111
+ ensureImport(targetCollection.sourceFile, 'craftRoute');
112
+ ensureTypeImport(targetCollection.sourceFile, 'CraftRouteLazyLoadHelpers');
113
+ }
114
+ ensureCollectionBookkeeping(targetCollection);
115
+ if (featureDecision.parentMount) {
116
+ ensureParentMount(parent.collection, featureDecision.parentMount, targetCollection);
117
+ ensureCollectionBookkeeping(parent.collection);
118
+ }
119
+ for (const file of new Set([
120
+ targetCollection.sourceFile,
121
+ parent.collection.sourceFile,
122
+ ])) {
123
+ file.formatText();
124
+ }
125
+ const changedFiles = [...touched].filter((filePath) => sourceChanged(project, initialTexts, filePath));
126
+ yield saveFiles(project, changedFiles, fileSystem);
127
+ if (options.validate !== false) {
128
+ diagnostics.push(...(yield validateRouteChangedFiles(rootDir, sourceRoot, tsConfigFilePath, changedFiles, eslintBaseline)));
129
+ }
130
+ return {
131
+ changedFiles,
132
+ diagnostics,
133
+ exitCode: diagnostics.length > 0 ? 1 : 0,
134
+ plan,
135
+ };
136
+ });
137
+ }
138
+ export function runRouteSplit(options) {
139
+ return __awaiter(this, void 0, void 0, function* () {
140
+ var _a, _b, _c;
141
+ const rootDir = resolve((_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd());
142
+ const fileSystem = (_b = options.fileSystem) !== null && _b !== void 0 ? _b : nodeRouteFileSystem;
143
+ const { project, tsConfigFilePath, sourceRoot } = createRouteProject(rootDir, options.project, fileSystem);
144
+ const initialTexts = snapshotProject(project);
145
+ const collections = discoverRouteCollections(project, sourceRoot);
146
+ const parent = resolveParentCollection(collections, options.parent, `/${options.prefix}`, rootDir);
147
+ if ('diagnostic' in parent)
148
+ return resultFromDiagnostics([parent.diagnostic]);
149
+ const prefix = normalizeRoutePath(options.prefix).slice(1);
150
+ const targetFilePath = resolveFromRoot(options.target, rootDir);
151
+ if (fileSystem.exists(targetFilePath)) {
152
+ return failed('TARGET_EXISTS', `Split target already exists: ${targetFilePath}`);
153
+ }
154
+ const matches = parent.collection.routes.getElements().filter((element) => {
155
+ const path = readRoutePath(element);
156
+ return path === prefix || (path === null || path === void 0 ? void 0 : path.startsWith(`${prefix}/`));
157
+ });
158
+ if (matches.length === 0) {
159
+ return failed('NO_MATCHING_ROUTES', `No statically analyzable route starts with ${prefix} in ${parent.collection.routesName}.`);
160
+ }
161
+ const localDependencies = findLocalDependencies(parent.collection.sourceFile, matches);
162
+ if (localDependencies.length > 0) {
163
+ return resultFromDiagnostics(localDependencies.map((name) => ({
164
+ code: 'LOCAL_DEPENDENCY',
165
+ message: `Cannot move routes using local declaration ${name}; extract or import it first.`,
166
+ filePath: parent.collection.sourceFile.getFilePath(),
167
+ })));
168
+ }
169
+ const collectionName = collectionNameFromFile(targetFilePath);
170
+ const helperRenames = deriveMovedHelperRenames(parent.collection, matches, prefix, collectionName);
171
+ const helperConsumers = findHelperConsumers(project, parent.collection.sourceFile, helperRenames);
172
+ const plan = {
173
+ action: 'split',
174
+ summary: `Move ${matches.length} route(s) under ${prefix} to ${targetFilePath}`,
175
+ files: [
176
+ parent.collection.sourceFile.getFilePath(),
177
+ targetFilePath,
178
+ ...helperConsumers.map((sourceFile) => sourceFile.getFilePath()),
179
+ ],
180
+ };
181
+ printPlan(plan, (_c = options.log) !== null && _c !== void 0 ? _c : console.log, options.json === true);
182
+ if (!(yield shouldContinue(options, plan))) {
183
+ return { changedFiles: [], diagnostics: [], exitCode: 0, plan };
184
+ }
185
+ if (options.dryRun) {
186
+ return { changedFiles: [], diagnostics: [], exitCode: 0, plan };
187
+ }
188
+ const eslintBaseline = options.validate === false
189
+ ? new Set()
190
+ : yield captureRouteEslintBaseline(rootDir, sourceRoot, plan.files);
191
+ const routesName = `${uncapitalize(toPascalCase(collectionName))}Routes`;
192
+ const movedText = matches.map((element) => rewriteMovedRouteText(element.getText(), prefix));
193
+ const imports = parent.collection.sourceFile
194
+ .getImportDeclarations()
195
+ .map((declaration) => declaration.getText())
196
+ .join('\n');
197
+ const context = readCascadeContext(parent.collection);
198
+ const target = project.createSourceFile(targetFilePath, `${imports}\n\nexport const { ${routesName} } = craftRoutes(${quote(collectionName)}, [\n${movedText
199
+ .map((text) => ` ${text}`)
200
+ .join(',\n')}\n]).withParent<ParentRoutes<${quote(prefix)}>>();\n\n` +
201
+ `assertExhaustiveRouteExceptions(${routesName});\n\n` +
202
+ `type _Check${toPascalCase(collectionName)}DI = ValidateCascadeRoutesFile<${context.names}, ${context.values}, typeof ${routesName}>;\n` +
203
+ `type _CanRun${toPascalCase(collectionName)} = CanRun<_Check${toPascalCase(collectionName)}DI>;\n`, { overwrite: false });
204
+ rewriteRelativeSpecifiers(target, parent.collection.sourceFile.getFilePath());
205
+ ensureImport(target, 'craftRoutes');
206
+ ensureImport(target, 'assertExhaustiveRouteExceptions');
207
+ ensureTypeImport(target, 'ParentRoutes');
208
+ ensureTypeImport(target, 'ValidateCascadeRoutesFile');
209
+ ensureTypeImport(target, 'CanRun');
210
+ ensureTypeImport(target, 'Router', '@angular/router');
211
+ rewireMovedHelpers(project, parent.collection, target, helperRenames);
212
+ const insertionIndex = Math.min(...matches.map((element) => parent.collection.routes.getElements().indexOf(element)));
213
+ const matchedIndexes = matches
214
+ .map((element) => parent.collection.routes.getElements().indexOf(element))
215
+ .sort((a, b) => b - a);
216
+ for (const index of matchedIndexes)
217
+ parent.collection.routes.removeElement(index);
218
+ const moduleSpecifier = relativeModuleSpecifier(parent.collection.sourceFile.getFilePath(), targetFilePath);
219
+ parent.collection.routes.insertElement(insertionIndex, `{ path: ${quote(prefix)}, loadChildren: ({ withRetry }) => withRetry(import(${quote(moduleSpecifier)})).then((m) => m.${routesName}) }`);
220
+ ensureCollectionBookkeeping(parent.collection);
221
+ target.organizeImports();
222
+ target.formatText();
223
+ parent.collection.sourceFile.formatText();
224
+ const changedFiles = [...new Set(plan.files)].filter((filePath) => sourceChanged(project, initialTexts, filePath));
225
+ yield saveFiles(project, changedFiles, fileSystem);
226
+ const diagnostics = options.validate === false
227
+ ? []
228
+ : yield validateRouteChangedFiles(rootDir, sourceRoot, tsConfigFilePath, changedFiles, eslintBaseline);
229
+ return {
230
+ changedFiles,
231
+ diagnostics,
232
+ exitCode: diagnostics.length > 0 ? 1 : 0,
233
+ plan,
234
+ };
235
+ });
236
+ }
237
+ export function discoverRouteCollections(project, rootDir = process.cwd()) {
238
+ var _a, _b;
239
+ const result = [];
240
+ for (const sourceFile of project.getSourceFiles()) {
241
+ if (!isInside(sourceFile.getFilePath(), rootDir) ||
242
+ sourceFile.isDeclarationFile() ||
243
+ /\.(?:spec|test)\.ts$/.test(sourceFile.getFilePath()))
244
+ continue;
245
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
246
+ if (call.getExpression().getText() !== 'craftRoutes')
247
+ continue;
248
+ const routes = call.getArguments()[1];
249
+ const nameArg = call.getArguments()[0];
250
+ if (!Node.isArrayLiteralExpression(routes) ||
251
+ !Node.isStringLiteral(nameArg))
252
+ continue;
253
+ const declaration = call.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
254
+ if (((_a = declaration === null || declaration === void 0 ? void 0 : declaration.getVariableStatement()) === null || _a === void 0 ? void 0 : _a.getParent()) !== sourceFile)
255
+ continue;
256
+ const binding = declaration === null || declaration === void 0 ? void 0 : declaration.getNameNode();
257
+ if (!binding || !Node.isObjectBindingPattern(binding))
258
+ continue;
259
+ const expected = `${uncapitalize(toPascalCase(nameArg.getLiteralValue()))}Routes`;
260
+ const routeBinding = (_b = binding
261
+ .getElements()
262
+ .find((element) => {
263
+ var _a, _b;
264
+ return ((_b = (_a = element.getPropertyNameNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : element.getName()) === expected;
265
+ })) !== null && _b !== void 0 ? _b : binding
266
+ .getElements()
267
+ .filter((element) => {
268
+ var _a, _b;
269
+ return ((_b = (_a = element.getPropertyNameNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : element.getName()).endsWith('Routes');
270
+ })[0];
271
+ if (!routeBinding)
272
+ continue;
273
+ result.push({
274
+ collectionName: nameArg.getLiteralValue(),
275
+ routesName: routeBinding.getName(),
276
+ sourceFile,
277
+ call,
278
+ routes,
279
+ });
280
+ }
281
+ }
282
+ return result;
283
+ }
284
+ export function listRouteCollections(rootDir = process.cwd(), projectOption, fileSystem = nodeRouteFileSystem) {
285
+ const absoluteRoot = resolve(rootDir);
286
+ const { project, sourceRoot } = createRouteProject(absoluteRoot, projectOption, fileSystem);
287
+ return discoverRouteCollections(project, sourceRoot).map((collection) => ({
288
+ filePath: collection.sourceFile.getFilePath(),
289
+ collectionName: collection.collectionName,
290
+ routesName: collection.routesName,
291
+ }));
292
+ }
293
+ export function listAngularProjects(rootDir = process.cwd()) {
294
+ const absoluteRoot = resolve(rootDir);
295
+ return findTsConfigApps(absoluteRoot, nodeRouteFileSystem).map((filePath) => relative(absoluteRoot, dirname(filePath)).replace(/\\/g, '/'));
296
+ }
297
+ function createRouteProject(rootDir, projectOption, fileSystem) {
298
+ const tsConfigFilePath = resolveTsConfig(rootDir, projectOption, fileSystem);
299
+ const sourceRoot = basename(tsConfigFilePath) === 'tsconfig.app.json'
300
+ ? dirname(tsConfigFilePath)
301
+ : rootDir;
302
+ const project = fileSystem === nodeRouteFileSystem && fileSystem.exists(tsConfigFilePath)
303
+ ? new Project({ tsConfigFilePath })
304
+ : new Project({
305
+ compilerOptions: {
306
+ module: ts.ModuleKind.Preserve,
307
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
308
+ target: ts.ScriptTarget.ES2022,
309
+ },
310
+ });
311
+ if (fileSystem === nodeRouteFileSystem) {
312
+ project.addSourceFilesAtPaths([
313
+ join(sourceRoot, '**/*.ts'),
314
+ `!${join(sourceRoot, '**/node_modules/**')}`,
315
+ `!${join(sourceRoot, '**/dist/**')}`,
316
+ `!${join(sourceRoot, '**/.angular/**')}`,
317
+ ]);
318
+ }
319
+ else {
320
+ for (const filePath of fileSystem
321
+ .listFiles(sourceRoot)
322
+ .filter((filePath) => filePath.endsWith('.ts'))) {
323
+ const text = fileSystem.read(filePath);
324
+ if (text !== undefined) {
325
+ project.createSourceFile(filePath, text, { overwrite: true });
326
+ }
327
+ }
328
+ }
329
+ project.manipulationSettings.set({ quoteKind: QuoteKind.Single });
330
+ return { project, tsConfigFilePath, sourceRoot };
331
+ }
332
+ function resolveTsConfig(rootDir, projectOption, fileSystem) {
333
+ if (projectOption) {
334
+ const direct = resolve(rootDir, projectOption);
335
+ if (fileSystem.exists(direct) && direct.endsWith('.json'))
336
+ return direct;
337
+ for (const candidate of [
338
+ join(direct, 'tsconfig.app.json'),
339
+ join(rootDir, 'apps', projectOption, 'tsconfig.app.json'),
340
+ join(rootDir, 'projects', projectOption, 'tsconfig.app.json'),
341
+ ]) {
342
+ if (fileSystem.exists(candidate))
343
+ return candidate;
344
+ }
345
+ }
346
+ const detected = findTsConfigApps(rootDir, fileSystem);
347
+ if (detected.length === 1)
348
+ return detected[0];
349
+ for (const candidate of [
350
+ join(rootDir, 'tsconfig.app.json'),
351
+ join(rootDir, 'tsconfig.json'),
352
+ ]) {
353
+ if (fileSystem.exists(candidate))
354
+ return candidate;
355
+ }
356
+ return join(rootDir, 'tsconfig.json');
357
+ }
358
+ function findTsConfigApps(rootDir, fileSystem) {
359
+ return fileSystem
360
+ .listFiles(rootDir)
361
+ .filter((filePath) => basename(filePath) === 'tsconfig.app.json')
362
+ .sort();
363
+ }
364
+ function resolveParentCollection(collections, parentOption, routePath, rootDir) {
365
+ if (parentOption) {
366
+ const [filePart, collectionPart] = parentOption.split('#');
367
+ const filePath = resolveFromRoot(filePart, rootDir);
368
+ const matches = collections.filter((collection) => resolve(collection.sourceFile.getFilePath()) === resolve(filePath) &&
369
+ (!collectionPart ||
370
+ collection.routesName === collectionPart ||
371
+ collection.collectionName === collectionPart));
372
+ if (matches.length === 1)
373
+ return { collection: matches[0] };
374
+ return {
375
+ diagnostic: {
376
+ code: 'PARENT_NOT_FOUND',
377
+ message: `Cannot resolve parent collection ${parentOption}.`,
378
+ filePath,
379
+ },
380
+ };
381
+ }
382
+ const first = routePath.slice(1).split('/')[0];
383
+ const featureMatches = collections.filter((collection) => [
384
+ collection.collectionName,
385
+ basename(collection.sourceFile.getFilePath(), '.routes.ts'),
386
+ ]
387
+ .map((value) => value.toLowerCase())
388
+ .includes(first.toLowerCase()));
389
+ if (featureMatches.length === 1)
390
+ return { collection: featureMatches[0] };
391
+ const roots = collections.filter((collection) => /(?:^|[/\\])app\.routes\.ts$/.test(collection.sourceFile.getFilePath()));
392
+ if (roots.length === 1)
393
+ return { collection: roots[0] };
394
+ if (collections.length === 1)
395
+ return { collection: collections[0] };
396
+ return {
397
+ diagnostic: {
398
+ code: 'AMBIGUOUS_PARENT',
399
+ message: `Found ${collections.length} craftRoutes collections; pass --parent <file#collection>.`,
400
+ },
401
+ };
402
+ }
403
+ function decideFeatureTarget(project, collections, parent, options, segments, rootDir) {
404
+ if (options.featureFile) {
405
+ return {
406
+ targetFilePath: resolveFromRoot(options.featureFile, rootDir),
407
+ localRoutePath: segments.slice(1).join('/'),
408
+ parentMount: segments[0],
409
+ collectionName: collectionNameFromFile(options.featureFile),
410
+ };
411
+ }
412
+ if (options.redirectTo) {
413
+ const parentIsFeature = parent.collectionName.toLowerCase() === segments[0].toLowerCase();
414
+ return {
415
+ targetFilePath: parent.sourceFile.getFilePath(),
416
+ localRoutePath: parentIsFeature
417
+ ? segments.slice(1).join('/')
418
+ : segments.join('/'),
419
+ collectionName: parent.collectionName,
420
+ };
421
+ }
422
+ if (options.parent) {
423
+ return {
424
+ targetFilePath: parent.sourceFile.getFilePath(),
425
+ localRoutePath: segments.join('/'),
426
+ collectionName: parent.collectionName,
427
+ };
428
+ }
429
+ if (parent.collectionName.toLowerCase() === segments[0].toLowerCase()) {
430
+ return {
431
+ targetFilePath: parent.sourceFile.getFilePath(),
432
+ localRoutePath: segments.slice(1).join('/'),
433
+ collectionName: parent.collectionName,
434
+ };
435
+ }
436
+ const existingFeature = collections.find((collection) => collection !== parent &&
437
+ collection.collectionName.toLowerCase() === segments[0].toLowerCase());
438
+ if (existingFeature) {
439
+ return {
440
+ targetFilePath: existingFeature.sourceFile.getFilePath(),
441
+ localRoutePath: segments.slice(1).join('/'),
442
+ collectionName: existingFeature.collectionName,
443
+ };
444
+ }
445
+ const targetFilePath = join(dirname(parent.sourceFile.getFilePath()), segments[0], `${segments[0]}.routes.ts`);
446
+ return {
447
+ targetFilePath,
448
+ localRoutePath: segments.slice(1).join('/'),
449
+ parentMount: segments[0],
450
+ collectionName: segments[0],
451
+ };
452
+ }
453
+ function ensureFeatureCollection(project, decision, parent) {
454
+ const existing = discoverRouteCollections(project, dirname(decision.targetFilePath)).find((collection) => resolve(collection.sourceFile.getFilePath()) ===
455
+ resolve(decision.targetFilePath));
456
+ if (existing)
457
+ return existing;
458
+ const routesName = `${uncapitalize(toPascalCase(decision.collectionName))}Routes`;
459
+ const context = readCascadeContext(parent);
460
+ const parentSuffix = decision.parentMount
461
+ ? `.withParent<ParentRoutes<${quote(decision.parentMount)}>>()`
462
+ : '';
463
+ const sourceFile = project.createSourceFile(decision.targetFilePath, `import { assertExhaustiveRouteExceptions, craftRoutes, type CanRun, type ParentRoutes, type ValidateCascadeRoutesFile } from '@craft-ng/core';\n` +
464
+ `import type { Router } from '@angular/router';\n\n` +
465
+ `export const { ${routesName} } = craftRoutes(${quote(decision.collectionName)}, [])${parentSuffix};\n\n` +
466
+ `assertExhaustiveRouteExceptions(${routesName});\n\n` +
467
+ `type _Check${toPascalCase(decision.collectionName)}DI = ValidateCascadeRoutesFile<${context.names}, ${context.values}, typeof ${routesName}>;\n` +
468
+ `type _CanRun${toPascalCase(decision.collectionName)} = CanRun<_Check${toPascalCase(decision.collectionName)}DI>;\n`, { overwrite: false });
469
+ const created = discoverRouteCollections(project, dirname(decision.targetFilePath)).find((collection) => collection.sourceFile === sourceFile);
470
+ if (!created)
471
+ throw new Error(`Could not create route collection ${routesName}.`);
472
+ return created;
473
+ }
474
+ function ensureParentMount(parent, mountPath, child) {
475
+ const alreadyMounted = parent.routes
476
+ .getElements()
477
+ .some((element) => readRoutePath(element) === mountPath &&
478
+ element.getText().includes('loadChildren'));
479
+ if (!alreadyMounted) {
480
+ const moduleSpecifier = relativeModuleSpecifier(parent.sourceFile.getFilePath(), child.sourceFile.getFilePath());
481
+ parent.routes.addElement(`{ path: ${quote(mountPath)}, loadChildren: ({ withRetry }) => withRetry(import(${quote(moduleSpecifier)})).then((m) => m.${child.routesName}) }`);
482
+ }
483
+ ensureImport(parent.sourceFile, 'assertChildRouteMounts');
484
+ if (!hasCall(parent.sourceFile, 'assertChildRouteMounts', parent.routesName)) {
485
+ parent.sourceFile.addStatements(`assertChildRouteMounts(${parent.routesName});`);
486
+ }
487
+ }
488
+ function ensureCollectionBookkeeping(collection) {
489
+ ensureImport(collection.sourceFile, 'assertExhaustiveRouteExceptions');
490
+ if (!hasCall(collection.sourceFile, 'assertExhaustiveRouteExceptions', collection.routesName)) {
491
+ collection.sourceFile.addStatements(`assertExhaustiveRouteExceptions(${collection.routesName});`);
492
+ }
493
+ if (!hasCascadeCheck(collection)) {
494
+ ensureTypeImport(collection.sourceFile, 'ValidateCascadeRoutesFile');
495
+ ensureTypeImport(collection.sourceFile, 'CanRun');
496
+ ensureTypeImport(collection.sourceFile, 'Router', '@angular/router');
497
+ const suffix = toPascalCase(collection.collectionName);
498
+ collection.sourceFile.addStatements(`type _Check${suffix}DI = ValidateCascadeRoutesFile<never, Router, typeof ${collection.routesName}>;\ntype _CanRun${suffix} = CanRun<_Check${suffix}DI>;`);
499
+ }
500
+ }
501
+ function hasCascadeCheck(collection) {
502
+ return collection.sourceFile.getTypeAliases().some((alias) => {
503
+ var _a;
504
+ const type = alias.getTypeNode();
505
+ return (Node.isTypeReference(type) &&
506
+ type.getTypeName().getText() === 'ValidateCascadeRoutesFile' &&
507
+ ((_a = type.getTypeArguments()[2]) === null || _a === void 0 ? void 0 : _a.getText()) ===
508
+ `typeof ${collection.routesName}`);
509
+ });
510
+ }
511
+ function readCascadeContext(collection) {
512
+ var _a, _b, _c, _d, _e;
513
+ for (const alias of collection.sourceFile.getTypeAliases()) {
514
+ const type = alias.getTypeNode();
515
+ if (!Node.isTypeReference(type))
516
+ continue;
517
+ if (type.getTypeName().getText() !== 'ValidateCascadeRoutesFile')
518
+ continue;
519
+ const args = type.getTypeArguments();
520
+ if (((_a = args[2]) === null || _a === void 0 ? void 0 : _a.getText()) !== `typeof ${collection.routesName}`)
521
+ continue;
522
+ return {
523
+ names: (_c = (_b = args[0]) === null || _b === void 0 ? void 0 : _b.getText()) !== null && _c !== void 0 ? _c : 'never',
524
+ values: (_e = (_d = args[1]) === null || _d === void 0 ? void 0 : _d.getText()) !== null && _e !== void 0 ? _e : 'Router',
525
+ };
526
+ }
527
+ return { names: 'never', values: 'Router' };
528
+ }
529
+ function parseComponentTarget(value, rootDir) {
530
+ const separator = value.lastIndexOf('#');
531
+ if (separator <= 0 || separator === value.length - 1)
532
+ return undefined;
533
+ return {
534
+ filePath: resolveFromRoot(value.slice(0, separator), rootDir),
535
+ className: value.slice(separator + 1),
536
+ };
537
+ }
538
+ function createAngularComponent(rootDir, options, projectName) {
539
+ return __awaiter(this, void 0, void 0, function* () {
540
+ const angularBinary = findLocalBinary(rootDir, 'ng');
541
+ const nxBinary = findLocalBinary(rootDir, 'nx');
542
+ const angularWorkspace = existsSync(join(rootDir, 'angular.json'));
543
+ const nxWorkspace = existsSync(join(rootDir, 'nx.json'));
544
+ const runner = angularWorkspace && angularBinary
545
+ ? {
546
+ binary: angularBinary,
547
+ args: (name) => [
548
+ 'generate',
549
+ 'component',
550
+ name,
551
+ '--inline-template',
552
+ '--inline-style',
553
+ '--skip-tests',
554
+ ...(projectName ? ['--project', projectName] : []),
555
+ ],
556
+ }
557
+ : nxWorkspace && nxBinary
558
+ ? {
559
+ binary: nxBinary,
560
+ args: (name) => [
561
+ 'generate',
562
+ '@schematics/angular:component',
563
+ name,
564
+ '--inline-template',
565
+ '--inline-style',
566
+ ...(projectName ? ['--project', projectName] : []),
567
+ '--skip-tests',
568
+ ],
569
+ }
570
+ : undefined;
571
+ if (!runner) {
572
+ return {
573
+ diagnostic: {
574
+ code: 'NO_ANGULAR_CLI',
575
+ message: 'No compatible local Angular or Nx CLI workspace was found. Expected angular.json + node_modules/.bin/ng, or nx.json + node_modules/.bin/nx.',
576
+ },
577
+ };
578
+ }
579
+ const name = options.createComponent;
580
+ if (!name)
581
+ throw new Error('createAngularComponent requires createComponent.');
582
+ const expectedFileName = `${names(basename(name)).fileName}.ts`;
583
+ const filesBefore = new Set(yield findFiles(rootDir, expectedFileName));
584
+ try {
585
+ yield execFileAsync(runner.binary, runner.args(name), { cwd: rootDir });
586
+ }
587
+ catch (error) {
588
+ return {
589
+ diagnostic: {
590
+ code: 'COMPONENT_GENERATION_FAILED',
591
+ message: `Component generation failed through ${basename(runner.binary)}: ${commandError(error)}`,
592
+ },
593
+ };
594
+ }
595
+ const candidates = (yield findFiles(rootDir, expectedFileName)).sort((left, right) => Number(filesBefore.has(left)) - Number(filesBefore.has(right)));
596
+ const filePath = candidates
597
+ .filter((candidate) => !candidate.endsWith('.spec.ts'))
598
+ .sort((a, b) => b.length - a.length)[0];
599
+ if (!filePath) {
600
+ return {
601
+ diagnostic: {
602
+ code: 'COMPONENT_NOT_FOUND',
603
+ message: `Angular CLI completed but ${basename(name)}.ts could not be located.`,
604
+ },
605
+ };
606
+ }
607
+ const source = new Project().addSourceFileAtPath(filePath);
608
+ const decorated = findAngularDecoratedClass(source);
609
+ if (decorated.skipped || !decorated.className) {
610
+ return {
611
+ diagnostic: {
612
+ code: 'COMPONENT_NOT_FOUND',
613
+ message: `Generated component class could not be inferred in ${filePath}.`,
614
+ },
615
+ };
616
+ }
617
+ return { component: { filePath, className: decorated.className } };
618
+ });
619
+ }
620
+ function resolveAngularProjectName(rootDir, projectOption, sourceRoot) {
621
+ const projectFile = join(sourceRoot, 'project.json');
622
+ if (existsSync(projectFile)) {
623
+ try {
624
+ const parsed = JSON.parse(readFileSync(projectFile, 'utf8'));
625
+ if (typeof parsed.name === 'string' && parsed.name)
626
+ return parsed.name;
627
+ }
628
+ catch (_a) {
629
+ // Fall through to the explicit option/basename heuristic.
630
+ }
631
+ }
632
+ if (projectOption &&
633
+ !projectOption.endsWith('.json') &&
634
+ !projectOption.includes('/') &&
635
+ !projectOption.includes('\\')) {
636
+ return projectOption;
637
+ }
638
+ const relativeSourceRoot = relative(rootDir, sourceRoot);
639
+ return relativeSourceRoot && !relativeSourceRoot.startsWith('..')
640
+ ? basename(sourceRoot)
641
+ : undefined;
642
+ }
643
+ function ensureGenDeps(sourceFile, className) {
644
+ generateAngularDependencies(sourceFile);
645
+ return Boolean(sourceFile.getTypeAlias(`GenDeps_${className}`));
646
+ }
647
+ function hasRoutePath(routes, path) {
648
+ return routes
649
+ .getElements()
650
+ .some((element) => readRoutePath(element) === path);
651
+ }
652
+ function routeMatchesRequest(routes, path, options, component, routesFilePath) {
653
+ const element = routes
654
+ .getElements()
655
+ .find((candidate) => readRoutePath(candidate) === path);
656
+ if (!element)
657
+ return false;
658
+ const text = element.getText();
659
+ if (options.redirectTo) {
660
+ return (text.includes(`redirectTo: ${quote(options.redirectTo)}`) ||
661
+ text.includes(`redirectTo: "${options.redirectTo}"`));
662
+ }
663
+ if (!component)
664
+ return false;
665
+ const moduleSpecifier = relativeModuleSpecifier(routesFilePath, component.filePath);
666
+ return (text.includes(`GenDeps_${component.className}`) &&
667
+ text.includes(moduleSpecifier));
668
+ }
669
+ function readRoutePath(node) {
670
+ if (Node.isCallExpression(node) &&
671
+ node.getExpression().getText() === 'craftRoute') {
672
+ const arg = node.getArguments()[0];
673
+ return Node.isStringLiteral(arg) ? arg.getLiteralValue() : undefined;
674
+ }
675
+ if (!Node.isObjectLiteralExpression(node))
676
+ return undefined;
677
+ const property = node.getProperty('path');
678
+ if (!Node.isPropertyAssignment(property))
679
+ return undefined;
680
+ const initializer = property.getInitializer();
681
+ return Node.isStringLiteral(initializer)
682
+ ? initializer.getLiteralValue()
683
+ : undefined;
684
+ }
685
+ function rewriteMovedRouteText(text, prefix) {
686
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
687
+ return text
688
+ .replace(new RegExp(`(craftRoute\\(\\s*['"])${escaped}(?:/)?`), '$1')
689
+ .replace(new RegExp(`(path\\s*:\\s*['"])${escaped}(?:/)?`), '$1');
690
+ }
691
+ function deriveMovedHelperRenames(parent, nodes, prefix, childCollectionName) {
692
+ const parentName = toPascalCase(parent.collectionName);
693
+ const childName = toPascalCase(childCollectionName);
694
+ const candidates = new Map();
695
+ for (const node of nodes) {
696
+ const originalPath = readRoutePath(node);
697
+ if (originalPath === undefined)
698
+ continue;
699
+ const childPath = originalPath === prefix ? '' : originalPath.slice(prefix.length + 1);
700
+ for (const segment of originalPath.split('/')) {
701
+ if (!segment.startsWith(':'))
702
+ continue;
703
+ const param = segment.slice(1).replace(/\?$/, '');
704
+ candidates.set(`inject${parentName}${toPascalCase(param)}Params`, `inject${childName}${toPascalCase(param)}Params`);
705
+ }
706
+ const definition = getRouteDefinition(node);
707
+ if (!definition)
708
+ continue;
709
+ const parentBase = routeBaseServiceName(originalPath);
710
+ const childBase = routeBaseServiceName(childPath);
711
+ for (const [property, suffix] of [
712
+ ['data', 'Data'],
713
+ ['queryParams', 'QueryParams'],
714
+ ['resolve', 'ResolvedData'],
715
+ ['withLoaderViewTransitionImage', 'ViewTransition'],
716
+ ]) {
717
+ if (!definition.getProperty(property))
718
+ continue;
719
+ candidates.set(`inject${parentName}${parentBase}${suffix}`, `inject${childName}${childBase}${suffix}`);
720
+ }
721
+ if (definition.getProperty('canActivate') ||
722
+ definition.getProperty('canMatch')) {
723
+ candidates.set(`inject${parentName}${parentBase}GuardedData`, `inject${childName}${childBase}GuardedData`);
724
+ }
725
+ }
726
+ const declaration = parent.call.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
727
+ const binding = declaration === null || declaration === void 0 ? void 0 : declaration.getNameNode();
728
+ if (!binding || !Node.isObjectBindingPattern(binding))
729
+ return new Map();
730
+ const exportedBindings = new Set(binding
731
+ .getElements()
732
+ .map((element) => { var _a, _b; return (_b = (_a = element.getPropertyNameNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : element.getName(); }));
733
+ return new Map([...candidates].filter(([oldName]) => exportedBindings.has(oldName)));
734
+ }
735
+ function getRouteDefinition(node) {
736
+ if (Node.isObjectLiteralExpression(node))
737
+ return node;
738
+ if (Node.isCallExpression(node) &&
739
+ node.getExpression().getText() === 'craftRoute') {
740
+ const definition = node.getArguments()[1];
741
+ return Node.isObjectLiteralExpression(definition) ? definition : undefined;
742
+ }
743
+ return undefined;
744
+ }
745
+ function routeBaseServiceName(path) {
746
+ if (!path)
747
+ return 'Root';
748
+ return path
749
+ .split('/')
750
+ .map((segment) => {
751
+ if (segment === '**')
752
+ return 'Wildcard';
753
+ return toPascalCase(segment.replace(/^:/, '').replace(/\?$/, ''));
754
+ })
755
+ .join('');
756
+ }
757
+ function findHelperConsumers(project, parentFile, renames) {
758
+ if (renames.size === 0)
759
+ return [];
760
+ return project.getSourceFiles().filter((sourceFile) => sourceFile.getImportDeclarations().some((declaration) => {
761
+ if (declaration.getModuleSpecifierSourceFile() !== parentFile)
762
+ return false;
763
+ return declaration
764
+ .getNamedImports()
765
+ .some((namedImport) => renames.has(namedImport.getName()));
766
+ }));
767
+ }
768
+ function rewireMovedHelpers(project, parent, target, renames) {
769
+ var _a, _b, _c;
770
+ if (renames.size === 0)
771
+ return;
772
+ const parentDeclaration = parent.call.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
773
+ const parentBinding = parentDeclaration === null || parentDeclaration === void 0 ? void 0 : parentDeclaration.getNameNode();
774
+ const targetDeclaration = (_a = target
775
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
776
+ .find((call) => call.getExpression().getText() === 'craftRoutes')) === null || _a === void 0 ? void 0 : _a.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
777
+ const targetBinding = targetDeclaration === null || targetDeclaration === void 0 ? void 0 : targetDeclaration.getNameNode();
778
+ if (!parentBinding ||
779
+ !Node.isObjectBindingPattern(parentBinding) ||
780
+ !targetBinding ||
781
+ !Node.isObjectBindingPattern(targetBinding)) {
782
+ return;
783
+ }
784
+ const oldNames = new Set(renames.keys());
785
+ const parentElements = parentBinding
786
+ .getElements()
787
+ .filter((element) => {
788
+ var _a, _b;
789
+ return !oldNames.has((_b = (_a = element.getPropertyNameNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : element.getName());
790
+ })
791
+ .map((element) => element.getText());
792
+ parentBinding.replaceWithText(`{ ${parentElements.join(', ')} }`);
793
+ const targetElements = targetBinding
794
+ .getElements()
795
+ .map((element) => element.getText());
796
+ for (const newName of renames.values()) {
797
+ if (!targetElements.includes(newName))
798
+ targetElements.push(newName);
799
+ }
800
+ targetBinding.replaceWithText(`{ ${targetElements.join(', ')} }`);
801
+ for (const sourceFile of project.getSourceFiles()) {
802
+ for (const declaration of [...sourceFile.getImportDeclarations()]) {
803
+ if (declaration.getModuleSpecifierSourceFile() !== parent.sourceFile)
804
+ continue;
805
+ for (const namedImport of [...declaration.getNamedImports()]) {
806
+ const newName = renames.get(namedImport.getName());
807
+ if (!newName)
808
+ continue;
809
+ const localName = (_c = (_b = namedImport.getAliasNode()) === null || _b === void 0 ? void 0 : _b.getText()) !== null && _c !== void 0 ? _c : namedImport.getName();
810
+ let targetImport = sourceFile
811
+ .getImportDeclarations()
812
+ .find((candidate) => candidate.getModuleSpecifierSourceFile() === target);
813
+ if (!targetImport) {
814
+ targetImport = sourceFile.addImportDeclaration({
815
+ moduleSpecifier: relativeModuleSpecifier(sourceFile.getFilePath(), target.getFilePath()),
816
+ });
817
+ }
818
+ targetImport.addNamedImport({
819
+ name: newName,
820
+ alias: localName === newName ? undefined : localName,
821
+ });
822
+ namedImport.remove();
823
+ }
824
+ if (declaration.getNamedImports().length === 0 &&
825
+ !declaration.getDefaultImport() &&
826
+ !declaration.getNamespaceImport()) {
827
+ declaration.remove();
828
+ }
829
+ }
830
+ }
831
+ }
832
+ function findLocalDependencies(sourceFile, nodes) {
833
+ const imported = new Set(sourceFile.getImportDeclarations().flatMap((declaration) => {
834
+ const defaultImport = declaration.getDefaultImport();
835
+ const namespaceImport = declaration.getNamespaceImport();
836
+ return [
837
+ ...declaration
838
+ .getNamedImports()
839
+ .map((item) => { var _a, _b; return (_b = (_a = item.getAliasNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : item.getName(); }),
840
+ ...(defaultImport ? [defaultImport.getText()] : []),
841
+ ...(namespaceImport ? [namespaceImport.getText()] : []),
842
+ ];
843
+ }));
844
+ const locals = new Set([
845
+ ...sourceFile
846
+ .getVariableStatements()
847
+ .flatMap((statement) => statement.getDeclarations())
848
+ .map((declaration) => declaration.getName()),
849
+ ...sourceFile
850
+ .getFunctions()
851
+ .map((declaration) => declaration.getName())
852
+ .filter(Boolean),
853
+ ...sourceFile.getEnums().map((declaration) => declaration.getName()),
854
+ ...sourceFile.getTypeAliases().map((declaration) => declaration.getName()),
855
+ ...sourceFile.getInterfaces().map((declaration) => declaration.getName()),
856
+ ...sourceFile
857
+ .getClasses()
858
+ .map((declaration) => declaration.getName())
859
+ .filter(Boolean),
860
+ ]);
861
+ const ignored = new Set([
862
+ 'craftRoute',
863
+ 'import',
864
+ 'm',
865
+ 'withRetry',
866
+ 'true',
867
+ 'false',
868
+ 'undefined',
869
+ ]);
870
+ const used = new Set(nodes.flatMap((node) => node
871
+ .getDescendantsOfKind(SyntaxKind.Identifier)
872
+ .map((identifier) => identifier.getText())));
873
+ return [...used].filter((name) => locals.has(name) && !imported.has(name) && !ignored.has(name));
874
+ }
875
+ function rewriteRelativeSpecifiers(target, previousFilePath) {
876
+ for (const declaration of target.getImportDeclarations()) {
877
+ const specifier = declaration.getModuleSpecifierValue();
878
+ if (!specifier.startsWith('.'))
879
+ continue;
880
+ const absolute = resolve(dirname(previousFilePath), specifier);
881
+ declaration.setModuleSpecifier(relativeModuleSpecifier(target.getFilePath(), absolute));
882
+ }
883
+ for (const literal of target.getDescendantsOfKind(SyntaxKind.StringLiteral)) {
884
+ const parent = literal.getParent();
885
+ if (!literal.getLiteralValue().startsWith('.'))
886
+ continue;
887
+ if (Node.isCallExpression(parent) &&
888
+ (parent.getExpression().getText() === 'import' ||
889
+ parent.getExpression().getText() === 'require')) {
890
+ const absolute = resolve(dirname(previousFilePath), literal.getLiteralValue());
891
+ literal.setLiteralValue(relativeModuleSpecifier(target.getFilePath(), absolute));
892
+ }
893
+ else if (literal.getFirstAncestorByKind(SyntaxKind.ImportType)) {
894
+ const absolute = resolve(dirname(previousFilePath), literal.getLiteralValue());
895
+ literal.setLiteralValue(relativeModuleSpecifier(target.getFilePath(), absolute));
896
+ }
897
+ }
898
+ }
899
+ function ensureImport(sourceFile, name, moduleSpecifier = '@craft-ng/core') {
900
+ const declaration = sourceFile
901
+ .getImportDeclarations()
902
+ .find((item) => item.getModuleSpecifierValue() === moduleSpecifier &&
903
+ !item.isTypeOnly());
904
+ if (!declaration) {
905
+ sourceFile.addImportDeclaration({ moduleSpecifier, namedImports: [name] });
906
+ }
907
+ else if (!declaration.getNamedImports().some((item) => item.getName() === name)) {
908
+ declaration.addNamedImport(name);
909
+ }
910
+ }
911
+ function ensureTypeImport(sourceFile, name, moduleSpecifier = '@craft-ng/core') {
912
+ if (sourceFile
913
+ .getImportDeclarations()
914
+ .some((item) => item.getModuleSpecifierValue() === moduleSpecifier &&
915
+ item.getNamedImports().some((named) => named.getName() === name))) {
916
+ return;
917
+ }
918
+ const valueImport = sourceFile
919
+ .getImportDeclarations()
920
+ .find((item) => item.getModuleSpecifierValue() === moduleSpecifier &&
921
+ !item.isTypeOnly());
922
+ if (valueImport)
923
+ valueImport.addNamedImport({ name, isTypeOnly: true });
924
+ else
925
+ sourceFile.addImportDeclaration({
926
+ moduleSpecifier,
927
+ isTypeOnly: true,
928
+ namedImports: [name],
929
+ });
930
+ }
931
+ function hasCall(sourceFile, functionName, argument) {
932
+ return sourceFile
933
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
934
+ .some((call) => {
935
+ var _a;
936
+ return call.getExpression().getText() === functionName &&
937
+ ((_a = call.getArguments()[0]) === null || _a === void 0 ? void 0 : _a.getText()) === argument;
938
+ });
939
+ }
940
+ function saveFiles(project, filePaths, fileSystem) {
941
+ return __awaiter(this, void 0, void 0, function* () {
942
+ yield Promise.all([...new Set(filePaths)].map((filePath) => __awaiter(this, void 0, void 0, function* () {
943
+ const sourceFile = project.getSourceFile(filePath);
944
+ if (sourceFile) {
945
+ yield fileSystem.write(filePath, sourceFile.getFullText());
946
+ }
947
+ })));
948
+ });
949
+ }
950
+ function addSourceFileFromWorkspace(project, filePath, fileSystem) {
951
+ const existing = project.getSourceFile(filePath);
952
+ if (existing)
953
+ return existing;
954
+ const text = fileSystem.read(filePath);
955
+ return text === undefined
956
+ ? undefined
957
+ : project.createSourceFile(filePath, text, { overwrite: true });
958
+ }
959
+ function snapshotProject(project) {
960
+ return new Map(project
961
+ .getSourceFiles()
962
+ .map((sourceFile) => [
963
+ sourceFile.getFilePath(),
964
+ sourceFile.getFullText(),
965
+ ]));
966
+ }
967
+ function sourceChanged(project, initialTexts, filePath) {
968
+ const sourceFile = project.getSourceFile(filePath);
969
+ return Boolean(sourceFile && initialTexts.get(filePath) !== sourceFile.getFullText());
970
+ }
971
+ export function validateRouteChangedFiles(rootDir_1, sourceRoot_1, tsConfigFilePath_1, files_1, eslintBaseline_1) {
972
+ return __awaiter(this, arguments, void 0, function* (rootDir, sourceRoot, tsConfigFilePath, files, eslintBaseline, options = {}) {
973
+ const diagnostics = [];
974
+ const eslint = findLocalBinary(rootDir, 'eslint');
975
+ if (eslint && files.length > 0) {
976
+ const lint = yield runEslintJson(eslint, ['--fix', ...files], sourceRoot);
977
+ if (lint.failure) {
978
+ diagnostics.push({
979
+ code: 'VALIDATION_FAILED',
980
+ message: `ESLint failed after writing changes: ${lint.failure}`,
981
+ });
982
+ }
983
+ else if (lint.failed) {
984
+ const newMessages = lint.results.flatMap((result) => result.messages
985
+ .filter((message) => !eslintBaseline.has(eslintFingerprint(result.filePath, message)))
986
+ .map((message) => { var _a, _b; return `${result.filePath}:${(_a = message.line) !== null && _a !== void 0 ? _a : 0}:${(_b = message.column) !== null && _b !== void 0 ? _b : 0} ${message.message}${message.ruleId ? ` (${message.ruleId})` : ''}`; }));
987
+ if (newMessages.length > 0) {
988
+ diagnostics.push({
989
+ code: 'VALIDATION_FAILED',
990
+ message: `ESLint failed after writing changes: ${newMessages.join('\n')}`,
991
+ });
992
+ }
993
+ }
994
+ }
995
+ if (options.skipTypeCheck)
996
+ return diagnostics;
997
+ const ngc = findLocalBinary(rootDir, 'ngc');
998
+ const tsc = findLocalBinary(rootDir, 'tsc');
999
+ const typeChecker = basename(tsConfigFilePath) === 'tsconfig.app.json' && ngc ? ngc : tsc;
1000
+ if (typeChecker && existsSync(tsConfigFilePath)) {
1001
+ try {
1002
+ yield execFileAsync(typeChecker, ['--project', tsConfigFilePath, '--noEmit'], {
1003
+ cwd: sourceRoot,
1004
+ maxBuffer: 20 * 1024 * 1024,
1005
+ });
1006
+ }
1007
+ catch (error) {
1008
+ const detail = commandError(error);
1009
+ diagnostics.push({
1010
+ code: 'VALIDATION_FAILED',
1011
+ message: detail.includes('TS2589')
1012
+ ? `TypeScript hit TS2589 after writing changes. Keep the DI check and split the collection with loadChildren; rerun with --feature-file or a more specific --parent. ${detail}`
1013
+ : `TypeScript diagnostics failed after writing changes: ${detail}`,
1014
+ });
1015
+ }
1016
+ }
1017
+ return diagnostics;
1018
+ });
1019
+ }
1020
+ export function captureRouteEslintBaseline(rootDir, sourceRoot, files) {
1021
+ return __awaiter(this, void 0, void 0, function* () {
1022
+ const eslint = findLocalBinary(rootDir, 'eslint');
1023
+ const existingFiles = files.filter((file) => existsSync(file));
1024
+ if (!eslint || existingFiles.length === 0)
1025
+ return new Set();
1026
+ const lint = yield runEslintJson(eslint, existingFiles, sourceRoot);
1027
+ if (lint.failure)
1028
+ return new Set();
1029
+ return new Set(lint.results.flatMap((result) => result.messages.map((message) => eslintFingerprint(result.filePath, message))));
1030
+ });
1031
+ }
1032
+ function runEslintJson(eslint, args, cwd) {
1033
+ return __awaiter(this, void 0, void 0, function* () {
1034
+ var _a;
1035
+ try {
1036
+ const { stdout } = yield execFileAsync(eslint, ['--format', 'json', ...args], { cwd, maxBuffer: 20 * 1024 * 1024 });
1037
+ return { failed: false, results: (_a = parseEslintResults(stdout)) !== null && _a !== void 0 ? _a : [] };
1038
+ }
1039
+ catch (error) {
1040
+ const stdout = commandStdout(error);
1041
+ const results = parseEslintResults(stdout);
1042
+ return results
1043
+ ? { failed: true, results }
1044
+ : { failed: true, failure: commandError(error), results: [] };
1045
+ }
1046
+ });
1047
+ }
1048
+ function parseEslintResults(output) {
1049
+ if (!output.trim())
1050
+ return undefined;
1051
+ try {
1052
+ const value = JSON.parse(output);
1053
+ return Array.isArray(value) ? value : undefined;
1054
+ }
1055
+ catch (_a) {
1056
+ return undefined;
1057
+ }
1058
+ }
1059
+ function eslintFingerprint(filePath, message) {
1060
+ var _a;
1061
+ return `${resolve(filePath)}\0${(_a = message.ruleId) !== null && _a !== void 0 ? _a : ''}\0${message.message}`;
1062
+ }
1063
+ function findLocalBinary(rootDir, name) {
1064
+ let current = rootDir;
1065
+ while (true) {
1066
+ const candidate = join(current, 'node_modules', '.bin', name);
1067
+ if (existsSync(candidate))
1068
+ return candidate;
1069
+ const parent = dirname(current);
1070
+ if (parent === current)
1071
+ return undefined;
1072
+ current = parent;
1073
+ }
1074
+ }
1075
+ function listFilesOnDisk(rootDir) {
1076
+ if (!existsSync(rootDir))
1077
+ return [];
1078
+ const result = [];
1079
+ const visit = (directory) => {
1080
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
1081
+ if (['node_modules', 'dist', '.angular', '.git'].includes(entry.name)) {
1082
+ continue;
1083
+ }
1084
+ const filePath = join(directory, entry.name);
1085
+ if (entry.isDirectory())
1086
+ visit(filePath);
1087
+ else if (entry.isFile())
1088
+ result.push(filePath);
1089
+ }
1090
+ };
1091
+ visit(rootDir);
1092
+ return result;
1093
+ }
1094
+ function findFiles(rootDir, fileName) {
1095
+ return __awaiter(this, void 0, void 0, function* () {
1096
+ const entries = yield readdir(rootDir, {
1097
+ recursive: true,
1098
+ withFileTypes: true,
1099
+ });
1100
+ return entries
1101
+ .filter((entry) => entry.isFile() && entry.name === fileName)
1102
+ .map((entry) => join(entry.parentPath, entry.name))
1103
+ .filter((path) => !path.includes(`${sep}node_modules${sep}`));
1104
+ });
1105
+ }
1106
+ function printPlan(plan, log, json) {
1107
+ if (json)
1108
+ return;
1109
+ log(`${plan.summary}\nFiles:\n${plan.files.map((file) => ` ${file}`).join('\n')}`);
1110
+ }
1111
+ function shouldContinue(options, plan) {
1112
+ return __awaiter(this, void 0, void 0, function* () {
1113
+ if (options.dryRun || options.yes)
1114
+ return true;
1115
+ return options.confirm ? options.confirm(plan) : false;
1116
+ });
1117
+ }
1118
+ function failed(code, message, plan) {
1119
+ return resultFromDiagnostics([{ code, message }], plan);
1120
+ }
1121
+ function resultFromDiagnostics(diagnostics, plan) {
1122
+ return { changedFiles: [], diagnostics, exitCode: 1, plan };
1123
+ }
1124
+ function resolveFromRoot(filePath, rootDir) {
1125
+ return isAbsolute(filePath) ? resolve(filePath) : resolve(rootDir, filePath);
1126
+ }
1127
+ function normalizeRoutePath(path) {
1128
+ const clean = path
1129
+ .trim()
1130
+ .replace(/^\/+|\/+$/g, '')
1131
+ .replace(/\/{2,}/g, '/');
1132
+ return clean ? `/${clean}` : '/';
1133
+ }
1134
+ function relativeModuleSpecifier(fromFile, toFile) {
1135
+ let value = relative(dirname(fromFile), toFile).replace(/\\/g, '/');
1136
+ const extension = extname(value);
1137
+ if (['.ts', '.tsx', '.js', '.jsx'].includes(extension)) {
1138
+ value = value.slice(0, -extension.length);
1139
+ }
1140
+ if (!value.startsWith('.'))
1141
+ value = `./${value}`;
1142
+ return value;
1143
+ }
1144
+ function collectionNameFromFile(filePath) {
1145
+ return basename(filePath)
1146
+ .replace(/\.routes\.ts$/, '')
1147
+ .replace(/\.ts$/, '');
1148
+ }
1149
+ function isInside(filePath, rootDir) {
1150
+ const path = relative(resolve(rootDir), resolve(filePath));
1151
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
1152
+ }
1153
+ function quote(value) {
1154
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
1155
+ }
1156
+ function toPascalCase(value) {
1157
+ return (value
1158
+ .split(/[^A-Za-z0-9]+/)
1159
+ .filter(Boolean)
1160
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
1161
+ .join('') || 'Routes');
1162
+ }
1163
+ function uncapitalize(value) {
1164
+ return value.charAt(0).toLowerCase() + value.slice(1);
1165
+ }
1166
+ function commandError(error) {
1167
+ if (typeof error === 'object' && error) {
1168
+ const value = error;
1169
+ const output = [value.stdout, value.stderr]
1170
+ .filter((item) => typeof item === 'string' && item.trim())
1171
+ .join('\n')
1172
+ .trim();
1173
+ if (output)
1174
+ return output;
1175
+ if (value.message)
1176
+ return String(value.message).trim();
1177
+ }
1178
+ return error instanceof Error ? error.message : String(error);
1179
+ }
1180
+ function commandStdout(error) {
1181
+ var _a;
1182
+ if (typeof error === 'object' && error && 'stdout' in error) {
1183
+ return String((_a = error.stdout) !== null && _a !== void 0 ? _a : '');
1184
+ }
1185
+ return '';
1186
+ }
1187
+ //# sourceMappingURL=route-command.js.map