@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.
- package/README.md +66 -0
- package/generators.json +26 -0
- package/package.json +18 -1
- package/src/bin/craft.d.ts +2 -0
- package/src/bin/craft.js +166 -0
- package/src/bin/craft.js.map +1 -0
- package/src/eslint-rules/craft-computed-name-match.cjs +8 -125
- package/src/eslint-rules/craft-method-name-match.cjs +8 -166
- package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
- package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
- package/src/eslint-rules/craft-source-name-match.cjs +8 -0
- package/src/eslint-rules/global-exception-registry-match.cjs +78 -13
- package/src/eslint-rules/index.cjs +8 -2
- package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
- package/src/eslint-rules/require-primitive-generator-unwrap.cjs +267 -0
- package/src/generators/route/compat.d.ts +3 -0
- package/src/generators/route/compat.js +6 -0
- package/src/generators/route/compat.js.map +1 -0
- package/src/generators/route/generator.d.ts +31 -0
- package/src/generators/route/generator.js +434 -0
- package/src/generators/route/generator.js.map +1 -0
- package/src/generators/route/schema.json +50 -0
- package/src/generators/route/split-schema.json +19 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/scripts/primitives/migrate-primitive-generators.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitive-generators.js +315 -0
- package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
- package/src/scripts/primitives/migrate-primitive-generators.ts +402 -0
- package/src/scripts/primitives/migrate-primitives.js +36 -4
- package/src/scripts/primitives/migrate-primitives.js.map +1 -1
- package/src/scripts/primitives/migrate-primitives.spec.ts +43 -0
- package/src/scripts/primitives/migrate-primitives.ts +62 -4
- package/src/scripts/primitives/migration-diagnostic.d.ts +1 -1
- package/src/scripts/primitives/migration-diagnostic.ts +1 -0
- package/src/scripts/routes/migrate-routes.js +4 -4
- package/src/scripts/routes/migrate-routes.js.map +1 -1
- package/src/scripts/routes/migrate-routes.ts +4 -6
- package/src/scripts/routes/route-command.d.ts +75 -0
- package/src/scripts/routes/route-command.js +1187 -0
- package/src/scripts/routes/route-command.js.map +1 -0
- package/src/scripts/routes/route-command.spec.ts +553 -0
- package/src/scripts/routes/route-command.ts +1790 -0
- package/src/scripts/services/migrate-services.js +45 -11
- package/src/scripts/services/migrate-services.js.map +1 -1
- package/src/scripts/services/migrate-services.spec.ts +46 -4
- package/src/scripts/services/migrate-services.ts +64 -13
- package/src/scripts/services/migration-diagnostic.d.ts +1 -1
- package/src/scripts/services/migration-diagnostic.ts +1 -0
- 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
|