@loozo/angular-router 0.0.0-stage → 0.0.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/.npmignore +3 -0
- package/LICENSE +21 -0
- package/README.md +143 -2
- package/builders.json +15 -0
- package/fesm2022/loozo-angular-router-_core.mjs +633 -0
- package/fesm2022/loozo-angular-router-_core.mjs.map +1 -0
- package/fesm2022/loozo-angular-router-_generated.mjs +658 -0
- package/fesm2022/loozo-angular-router-_generated.mjs.map +1 -0
- package/fesm2022/loozo-angular-router.mjs +19 -0
- package/fesm2022/loozo-angular-router.mjs.map +1 -0
- package/node/_core/src/path.js +50 -0
- package/node/src/builders/application.js +40 -0
- package/node/src/builders/options.js +96 -0
- package/node/src/builders/serve.js +155 -0
- package/node/src/generation/emit.js +270 -0
- package/node/src/generation/files.js +189 -0
- package/node/src/generation/filesystem.js +343 -0
- package/node/src/generation/generate.js +30 -0
- package/node/src/generation/overlay.js +84 -0
- package/node/src/generation/selectors.js +60 -0
- package/node/src/generation/sources.js +534 -0
- package/node/src/generation/validate.js +478 -0
- package/package.json +47 -3
- package/schemas/application.json +40 -0
- package/schemas/serve.json +39 -0
- package/types/loozo-angular-router-_core.d.ts +484 -0
- package/types/loozo-angular-router-_generated.d.ts +171 -0
- package/types/loozo-angular-router.d.ts +1 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parsePath } from "../../_core/src/path.js";
|
|
6
|
+
import { auditPath, contains, metadata, walk, workspacePaths, markers } from "./files.js";
|
|
7
|
+
import { overlayDirectory, overlayHost } from "./overlay.js";
|
|
8
|
+
import { synchronize, transparent } from "./filesystem.js";
|
|
9
|
+
export function fail(node, message) {
|
|
10
|
+
const source = node.getSourceFile();
|
|
11
|
+
const position = source.getLineAndCharacterOfPosition(node.getStart());
|
|
12
|
+
throw new Error(`${source.fileName}:${position.line + 1}:${position.character + 1}: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
export function properties(node, label) {
|
|
15
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
16
|
+
fail(node, `${label} must be an inline object`);
|
|
17
|
+
}
|
|
18
|
+
const result = new Map();
|
|
19
|
+
for (const property of node.properties) {
|
|
20
|
+
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) {
|
|
21
|
+
fail(property, `${label} requires explicit unique keys; no spreads/accessors`);
|
|
22
|
+
}
|
|
23
|
+
const name = property.name;
|
|
24
|
+
if (!ts.isIdentifier(name) && !ts.isStringLiteral(name)) {
|
|
25
|
+
fail(name, `${label} requires noncomputed keys`);
|
|
26
|
+
}
|
|
27
|
+
if (result.has(name.text)) {
|
|
28
|
+
fail(property, `Duplicate ${label} key: ${name.text}`);
|
|
29
|
+
}
|
|
30
|
+
let value;
|
|
31
|
+
if (ts.isPropertyAssignment(property)) {
|
|
32
|
+
value = property.initializer;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
value = property.name;
|
|
36
|
+
}
|
|
37
|
+
result.set(name.text, value);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
export function resolveModule(name, source, options, host = ts.sys) {
|
|
42
|
+
return ts.resolveModuleName(name, source, options, host).resolvedModule?.resolvedFileName;
|
|
43
|
+
}
|
|
44
|
+
function namedImport(source, name, imported) {
|
|
45
|
+
if (!ts.isIdentifier(name)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
for (const statement of source.statements) {
|
|
49
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const bindings = statement.importClause?.namedBindings;
|
|
53
|
+
if (!bindings || !ts.isNamedImports(bindings)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const binding of bindings.elements) {
|
|
57
|
+
if (!binding.isTypeOnly &&
|
|
58
|
+
binding.name.text === name.text &&
|
|
59
|
+
(binding.propertyName?.text ?? binding.name.text) === imported) {
|
|
60
|
+
return statement;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
async function installedAngular(source) {
|
|
67
|
+
let directory = dirname(source);
|
|
68
|
+
while (true) {
|
|
69
|
+
const packageFile = resolve(directory, "node_modules/@angular/core/package.json");
|
|
70
|
+
const info = await metadata(packageFile);
|
|
71
|
+
if (info) {
|
|
72
|
+
const bytes = await readFile(packageFile, "utf8");
|
|
73
|
+
const json = JSON.parse(bytes);
|
|
74
|
+
const entry = json.exports?.["."]?.types;
|
|
75
|
+
if (typeof entry !== "string") {
|
|
76
|
+
throw new Error(`Installed Angular has no types entry: ${packageFile}`);
|
|
77
|
+
}
|
|
78
|
+
return realpath(resolve(dirname(packageFile), entry));
|
|
79
|
+
}
|
|
80
|
+
const parent = dirname(directory);
|
|
81
|
+
if (parent === directory) {
|
|
82
|
+
throw new Error(`Cannot find consumer-installed @angular/core for ${source}`);
|
|
83
|
+
}
|
|
84
|
+
directory = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function moduleText(statement) {
|
|
88
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
89
|
+
fail(statement, "Import module must be a string literal");
|
|
90
|
+
}
|
|
91
|
+
return statement.moduleSpecifier.text;
|
|
92
|
+
}
|
|
93
|
+
async function genuineImport(statement, expected, options, host = ts.sys) {
|
|
94
|
+
const actual = resolveModule(moduleText(statement), statement.getSourceFile().fileName, options, host);
|
|
95
|
+
if (!actual) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const physical = await realpath(actual);
|
|
99
|
+
return physical === expected;
|
|
100
|
+
}
|
|
101
|
+
function isDefault(node) {
|
|
102
|
+
if (!ts.canHaveModifiers(node)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return (ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ===
|
|
106
|
+
true);
|
|
107
|
+
}
|
|
108
|
+
async function component(source, node, options) {
|
|
109
|
+
let found = false;
|
|
110
|
+
for (const decorator of ts.getDecorators(node) ?? []) {
|
|
111
|
+
if (!ts.isCallExpression(decorator.expression)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const call = decorator.expression;
|
|
115
|
+
const imported = namedImport(source, call.expression, "Component");
|
|
116
|
+
if (!imported) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const expected = await installedAngular(source.fileName);
|
|
120
|
+
const genuine = await genuineImport(imported, expected, options);
|
|
121
|
+
if (!genuine) {
|
|
122
|
+
fail(decorator, "Component decorator is not from consumer-installed @angular/core");
|
|
123
|
+
}
|
|
124
|
+
if (found || call.arguments.length !== 1 || !ts.isObjectLiteralExpression(call.arguments[0])) {
|
|
125
|
+
fail(decorator, "Component requires one inline metadata object");
|
|
126
|
+
}
|
|
127
|
+
found = true;
|
|
128
|
+
let proven = true;
|
|
129
|
+
let explicit = false;
|
|
130
|
+
for (const property of call.arguments[0].properties) {
|
|
131
|
+
if (ts.isSpreadAssignment(property) || ts.isComputedPropertyName(property.name)) {
|
|
132
|
+
proven = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const name = property.name;
|
|
136
|
+
if (name &&
|
|
137
|
+
(ts.isIdentifier(name) || ts.isStringLiteral(name)) &&
|
|
138
|
+
name.text === "standalone") {
|
|
139
|
+
if (explicit) {
|
|
140
|
+
fail(property, "Duplicate standalone property");
|
|
141
|
+
}
|
|
142
|
+
explicit = true;
|
|
143
|
+
proven =
|
|
144
|
+
ts.isPropertyAssignment(property) &&
|
|
145
|
+
property.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
146
|
+
if (!proven) {
|
|
147
|
+
fail(property, "Component standalone must be final literal true or safely omitted");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!proven) {
|
|
152
|
+
fail(decorator, "Component metadata cannot prove standalone after opaque override");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!found) {
|
|
156
|
+
fail(node, "Default class must have a genuine Angular Component decorator");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const keys = new Set([
|
|
160
|
+
"parent",
|
|
161
|
+
"params",
|
|
162
|
+
"search",
|
|
163
|
+
"resolve",
|
|
164
|
+
"runGuardsAndResolvers",
|
|
165
|
+
"redirect",
|
|
166
|
+
"fragments",
|
|
167
|
+
"canActivate",
|
|
168
|
+
"canActivateChild",
|
|
169
|
+
"canMatch",
|
|
170
|
+
"canDeactivate",
|
|
171
|
+
"providers",
|
|
172
|
+
"title",
|
|
173
|
+
"data",
|
|
174
|
+
]);
|
|
175
|
+
export async function discover(workspace, config, scans, out, auto, authored = new Map(), filesystem = auto !== undefined) {
|
|
176
|
+
if (scans.length !== 1) {
|
|
177
|
+
throw new Error("scanDirectories must contain exactly one directory");
|
|
178
|
+
}
|
|
179
|
+
const access = await workspacePaths(workspace);
|
|
180
|
+
const configFile = access.resolve(config);
|
|
181
|
+
const configDirectory = dirname(configFile);
|
|
182
|
+
const output = access.resolve(out);
|
|
183
|
+
const root = access.resolve(resolve(configDirectory, scans[0]));
|
|
184
|
+
const roots = [root];
|
|
185
|
+
if (contains(root, output) || contains(output, root)) {
|
|
186
|
+
throw new Error(`Output and scan directory overlap: ${output}; ${root}`);
|
|
187
|
+
}
|
|
188
|
+
await Promise.all([auditPath(configFile), auditPath(output)]);
|
|
189
|
+
const encountered = (await walk(root)).sort();
|
|
190
|
+
const fatal = [];
|
|
191
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configFile, {}, {
|
|
192
|
+
...ts.sys,
|
|
193
|
+
readDirectory: overlayDirectory(authored, (directory) => access.resolve(directory, true)),
|
|
194
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => fatal.push(diagnostic),
|
|
195
|
+
});
|
|
196
|
+
const errors = [...fatal, ...(parsed?.errors ?? [])].filter((error) => error.code !== 18003);
|
|
197
|
+
if (!parsed || errors.length > 0) {
|
|
198
|
+
const codes = errors.map((diagnostic) => diagnostic.code).join(", ");
|
|
199
|
+
throw new Error(`TypeScript diagnostic codes: ${codes}\n` +
|
|
200
|
+
ts.formatDiagnosticsWithColorAndContext(errors, {
|
|
201
|
+
getCanonicalFileName: (name) => name,
|
|
202
|
+
getCurrentDirectory: () => configDirectory,
|
|
203
|
+
getNewLine: () => "\n",
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
// Canonicalize only permitted workspace-access prefixes, never source links.
|
|
207
|
+
// Membership, candidate discovery and the final program share these roots.
|
|
208
|
+
const admitted = new Set();
|
|
209
|
+
for (const file of parsed.fileNames) {
|
|
210
|
+
admitted.add(access.resolve(file));
|
|
211
|
+
}
|
|
212
|
+
parsed.fileNames = [...admitted];
|
|
213
|
+
for (const file of authored.keys()) {
|
|
214
|
+
if (!admitted.has(file)) {
|
|
215
|
+
throw new Error(`TypeScript config does not admit filesystem definition: ${file}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const candidates = [];
|
|
219
|
+
for (const normalized of parsed.fileNames) {
|
|
220
|
+
if (!normalized.endsWith(".ts") ||
|
|
221
|
+
normalized.endsWith(".d.ts") ||
|
|
222
|
+
normalized.split(sep).includes("node_modules")) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
await auditPath(normalized);
|
|
226
|
+
if (contains(root, normalized)) {
|
|
227
|
+
candidates.push(normalized);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
candidates.sort();
|
|
231
|
+
const packageRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
|
232
|
+
const manifestBytes = await readFile(resolve(packageRoot, "package.json"), "utf8");
|
|
233
|
+
const manifest = JSON.parse(manifestBytes);
|
|
234
|
+
const factoryEntry = await realpath(resolve(packageRoot, manifest.exports["."].types));
|
|
235
|
+
const declarations = [];
|
|
236
|
+
const pages = new Map();
|
|
237
|
+
const deferredFactories = [];
|
|
238
|
+
const syntaxHost = overlayHost(parsed.options, authored);
|
|
239
|
+
const syntaxProgram = ts.createProgram({
|
|
240
|
+
rootNames: candidates,
|
|
241
|
+
options: parsed.options,
|
|
242
|
+
host: syntaxHost,
|
|
243
|
+
});
|
|
244
|
+
for (const file of candidates) {
|
|
245
|
+
const bytes = syntaxHost.readFile(file);
|
|
246
|
+
if (bytes === undefined) {
|
|
247
|
+
if (auto) {
|
|
248
|
+
// An explicit tsconfig files entry may name a not-yet-seeded definition.
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
throw new Error(`Cannot read admitted TypeScript source: ${file}`);
|
|
252
|
+
}
|
|
253
|
+
if (file.endsWith(`${sep}route.generated.ts`) &&
|
|
254
|
+
bytes.split("\n", 1)[0] === markers["route.generated.ts"]) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const source = syntaxProgram.getSourceFile(file);
|
|
258
|
+
const syntax = syntaxProgram.getSyntacticDiagnostics(source);
|
|
259
|
+
if (syntax.length > 0) {
|
|
260
|
+
throw new Error(ts.formatDiagnostics(syntax, {
|
|
261
|
+
getCanonicalFileName: (name) => name,
|
|
262
|
+
getCurrentDirectory: () => configDirectory,
|
|
263
|
+
getNewLine: () => "\n",
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
const defaults = source.statements.filter((node) => {
|
|
267
|
+
if ((ts.isExportAssignment(node) && !node.isExportEquals) || isDefault(node)) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (ts.isExportDeclaration(node) &&
|
|
271
|
+
node.exportClause &&
|
|
272
|
+
ts.isNamedExports(node.exportClause)) {
|
|
273
|
+
return node.exportClause.elements.some((element) => element.name.text === "default");
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
});
|
|
277
|
+
if (defaults.length === 0) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (defaults.length !== 1) {
|
|
281
|
+
fail(defaults[1], "Multiple competing default exports");
|
|
282
|
+
}
|
|
283
|
+
const node = defaults[0];
|
|
284
|
+
if (ts.isClassDeclaration(node)) {
|
|
285
|
+
await component(source, node, parsed.options);
|
|
286
|
+
if (pages.has(dirname(file))) {
|
|
287
|
+
fail(node, `Competing colocated components: ${pages.get(dirname(file))}; ${file}`);
|
|
288
|
+
}
|
|
289
|
+
pages.set(dirname(file), file);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!ts.isExportAssignment(node) || !ts.isCallExpression(node.expression)) {
|
|
293
|
+
fail(node, "Default export must be a direct defineRoute call or Component class");
|
|
294
|
+
}
|
|
295
|
+
const call = node.expression;
|
|
296
|
+
const imported = namedImport(source, call.expression, "defineRoute");
|
|
297
|
+
if (!imported) {
|
|
298
|
+
fail(call, "Factory must be a genuine named value import of defineRoute, not a local wrapper/alias");
|
|
299
|
+
}
|
|
300
|
+
const [path, configNode] = call.arguments;
|
|
301
|
+
if (call.arguments.length !== 2 || !path || !ts.isStringLiteral(path) || !configNode) {
|
|
302
|
+
fail(call, "defineRoute requires a literal path and inline config");
|
|
303
|
+
}
|
|
304
|
+
const config = properties(configNode, "Route config");
|
|
305
|
+
for (const key of config.keys()) {
|
|
306
|
+
if (!keys.has(key)) {
|
|
307
|
+
fail(configNode, `Unknown or generation-owned route config key: ${key}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
let segments;
|
|
311
|
+
try {
|
|
312
|
+
segments = parsePath(path.text);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
fail(path, String(error));
|
|
316
|
+
}
|
|
317
|
+
const needsParams = segments.some((segment) => segment.kind !== "static");
|
|
318
|
+
const ignored = filesystem && transparent(roots[0], dirname(file));
|
|
319
|
+
if (!auto && !ignored && needsParams !== config.has("params")) {
|
|
320
|
+
fail(configNode, "Local params must be present exactly when the path declares placeholders");
|
|
321
|
+
}
|
|
322
|
+
const data = config.get("data");
|
|
323
|
+
if (data && ts.isObjectLiteralExpression(data)) {
|
|
324
|
+
for (const property of data.properties) {
|
|
325
|
+
const name = property.name;
|
|
326
|
+
if (name &&
|
|
327
|
+
(ts.isIdentifier(name) || ts.isStringLiteral(name)) &&
|
|
328
|
+
name.text === "__loozoAngularRouterRouteId") {
|
|
329
|
+
fail(data, "Static data cannot supply reserved generated identity");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const id = relative(configDirectory, file).split(sep).join("/");
|
|
334
|
+
if (isAbsolute(id)) {
|
|
335
|
+
fail(node, "Source identity cannot be represented relative to tsconfig");
|
|
336
|
+
}
|
|
337
|
+
const declaration = {
|
|
338
|
+
file,
|
|
339
|
+
id,
|
|
340
|
+
source,
|
|
341
|
+
rawText: authored.get(resolve(file)) ?? (await readFile(file, "utf8")),
|
|
342
|
+
config,
|
|
343
|
+
path: path.text,
|
|
344
|
+
segments,
|
|
345
|
+
effective: [],
|
|
346
|
+
children: [],
|
|
347
|
+
retained: true,
|
|
348
|
+
full: false,
|
|
349
|
+
};
|
|
350
|
+
const ownFacade = resolve(dirname(file), "route.generated.ts");
|
|
351
|
+
const resolved = resolveModule(moduleText(imported), file, parsed.options, syntaxHost);
|
|
352
|
+
const facadeSpelling = resolve(dirname(file), moduleText(imported)).replace(/\.js$/, ".ts");
|
|
353
|
+
if (resolved === ownFacade || (!resolved && facadeSpelling === ownFacade)) {
|
|
354
|
+
deferredFactories.push(declaration);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
const genuine = await genuineImport(imported, factoryEntry, parsed.options, syntaxHost);
|
|
358
|
+
if (!genuine) {
|
|
359
|
+
fail(imported, "defineRoute import does not resolve to this package's concrete public entry");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
declarations.push(declaration);
|
|
363
|
+
}
|
|
364
|
+
if (auto) {
|
|
365
|
+
const edits = await synchronize(roots[0], declarations, pages, parsed.options, auto, syntaxProgram.getTypeChecker());
|
|
366
|
+
return discover(workspace, config, scans, out, undefined, edits, true);
|
|
367
|
+
}
|
|
368
|
+
if (filesystem) {
|
|
369
|
+
for (let index = declarations.length - 1; index >= 0; index -= 1) {
|
|
370
|
+
const declaration = declarations[index];
|
|
371
|
+
if (transparent(roots[0], dirname(declaration.file))) {
|
|
372
|
+
declarations.splice(index, 1);
|
|
373
|
+
const deferred = deferredFactories.indexOf(declaration);
|
|
374
|
+
if (deferred >= 0) {
|
|
375
|
+
deferredFactories.splice(deferred, 1);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const byDirectory = new Map();
|
|
381
|
+
const byFile = new Map();
|
|
382
|
+
for (const declaration of declarations) {
|
|
383
|
+
const directory = dirname(declaration.file);
|
|
384
|
+
if (byDirectory.has(directory)) {
|
|
385
|
+
fail(declaration.source, `Competing colocated declarations: ${byDirectory.get(directory).file}; ${declaration.file}`);
|
|
386
|
+
}
|
|
387
|
+
byDirectory.set(directory, declaration);
|
|
388
|
+
byFile.set(declaration.file, declaration);
|
|
389
|
+
declaration.page = pages.get(directory);
|
|
390
|
+
}
|
|
391
|
+
for (const [directory, page] of pages) {
|
|
392
|
+
if (!byDirectory.has(directory)) {
|
|
393
|
+
throw new Error(`Component without a colocated admitted declaration: ${page}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const compilerOptions = parsed.options;
|
|
397
|
+
function referenced(node, declaration) {
|
|
398
|
+
if (!ts.isIdentifier(node)) {
|
|
399
|
+
fail(node, "Parent/redirect target must directly reference a default import");
|
|
400
|
+
}
|
|
401
|
+
for (const statement of declaration.source.statements) {
|
|
402
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
403
|
+
statement.importClause?.isTypeOnly ||
|
|
404
|
+
statement.importClause?.name?.text !== node.text) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const file = resolveModule(moduleText(statement), declaration.file, compilerOptions, syntaxHost);
|
|
408
|
+
const target = file && byFile.get(file);
|
|
409
|
+
if (target) {
|
|
410
|
+
return target;
|
|
411
|
+
}
|
|
412
|
+
fail(node, `Parent/redirect target is not an admitted declaration: ${file ?? moduleText(statement)}`);
|
|
413
|
+
}
|
|
414
|
+
fail(node, "Parent/redirect target must directly reference a default import");
|
|
415
|
+
}
|
|
416
|
+
for (const declaration of declarations) {
|
|
417
|
+
const parent = declaration.config.get("parent");
|
|
418
|
+
if (parent) {
|
|
419
|
+
declaration.parent = referenced(parent, declaration);
|
|
420
|
+
declaration.parent.children.push(declaration);
|
|
421
|
+
}
|
|
422
|
+
const redirect = declaration.config.get("redirect");
|
|
423
|
+
if (redirect) {
|
|
424
|
+
const fields = properties(redirect, "Redirect");
|
|
425
|
+
for (const key of fields.keys()) {
|
|
426
|
+
if (!["to", "params", "search", "fragment"].includes(key)) {
|
|
427
|
+
fail(redirect, `Unknown redirect key: ${key}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const target = fields.get("to");
|
|
431
|
+
if (!target) {
|
|
432
|
+
fail(redirect, "Redirect requires a direct target");
|
|
433
|
+
}
|
|
434
|
+
declaration.target = referenced(target, declaration);
|
|
435
|
+
if (declaration.page ||
|
|
436
|
+
declaration.children.length ||
|
|
437
|
+
declaration.config.has("canActivate") ||
|
|
438
|
+
declaration.config.has("canMatch")) {
|
|
439
|
+
fail(redirect, "Redirect cannot have a component, children, canActivate or canMatch");
|
|
440
|
+
}
|
|
441
|
+
declaration.retained = false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const visiting = new Set();
|
|
445
|
+
const completed = new Set();
|
|
446
|
+
function ancestry(declaration) {
|
|
447
|
+
if (completed.has(declaration)) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (visiting.has(declaration)) {
|
|
451
|
+
fail(declaration.source, "Parent cycle");
|
|
452
|
+
}
|
|
453
|
+
visiting.add(declaration);
|
|
454
|
+
if (declaration.parent) {
|
|
455
|
+
ancestry(declaration.parent);
|
|
456
|
+
if (declaration.parent.effective.some((segment) => segment.kind === "catchall")) {
|
|
457
|
+
fail(declaration.source, "Catchall declarations cannot have children");
|
|
458
|
+
}
|
|
459
|
+
const owned = new Set();
|
|
460
|
+
for (const segment of declaration.parent.effective) {
|
|
461
|
+
if (segment.kind !== "static") {
|
|
462
|
+
owned.add(segment.name);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const segment of declaration.segments) {
|
|
466
|
+
if (segment.kind !== "static" && owned.has(segment.name)) {
|
|
467
|
+
fail(declaration.source, `Ancestor already owns path param: ${segment.name}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
declaration.effective = [...declaration.parent.effective, ...declaration.segments];
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
declaration.effective = declaration.segments;
|
|
474
|
+
}
|
|
475
|
+
visiting.delete(declaration);
|
|
476
|
+
completed.add(declaration);
|
|
477
|
+
}
|
|
478
|
+
for (const declaration of declarations) {
|
|
479
|
+
ancestry(declaration);
|
|
480
|
+
const chain = new Set();
|
|
481
|
+
let current = declaration;
|
|
482
|
+
while (current?.target) {
|
|
483
|
+
if (chain.has(current)) {
|
|
484
|
+
fail(declaration.source, "Redirect cycle/self redirect");
|
|
485
|
+
}
|
|
486
|
+
chain.add(current);
|
|
487
|
+
current = current.target;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Prune only ancestors made empty by redirect hoisting, not ordinary missing pages.
|
|
491
|
+
function retain(declaration) {
|
|
492
|
+
if (declaration.target) {
|
|
493
|
+
if (declaration.children.length > 0) {
|
|
494
|
+
fail(declaration.source, "Redirect cannot have authored children");
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
let retainedChild = false;
|
|
499
|
+
for (const child of declaration.children) {
|
|
500
|
+
if (retain(child)) {
|
|
501
|
+
retainedChild = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (!declaration.page && !retainedChild) {
|
|
505
|
+
if (declaration.children.length === 0) {
|
|
506
|
+
fail(declaration.source, "Ordinary componentless leaf");
|
|
507
|
+
}
|
|
508
|
+
declaration.retained = false;
|
|
509
|
+
}
|
|
510
|
+
return declaration.retained;
|
|
511
|
+
}
|
|
512
|
+
for (const declaration of declarations) {
|
|
513
|
+
if (!declaration.parent) {
|
|
514
|
+
retain(declaration);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const declaration of deferredFactories) {
|
|
518
|
+
if (!declaration.retained) {
|
|
519
|
+
fail(declaration.source, "Factory facade is not a final retained generated location");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
configFile,
|
|
524
|
+
parsed,
|
|
525
|
+
roots,
|
|
526
|
+
output,
|
|
527
|
+
encountered,
|
|
528
|
+
declarations,
|
|
529
|
+
factoryEntry,
|
|
530
|
+
deferredFactories,
|
|
531
|
+
authored,
|
|
532
|
+
filesystem,
|
|
533
|
+
};
|
|
534
|
+
}
|