@loozo/angular-router 0.0.2 → 0.0.4
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 +565 -80
- package/builders.json +2 -2
- package/fesm2022/loozo-angular-router-_core.mjs +332 -595
- package/fesm2022/loozo-angular-router-_core.mjs.map +1 -1
- package/fesm2022/loozo-angular-router-_generated.mjs +335 -627
- package/fesm2022/loozo-angular-router-_generated.mjs.map +1 -1
- package/fesm2022/loozo-angular-router.mjs +2 -19
- package/node/_core/src/path.d.ts +17 -0
- package/node/src/builders/application.js +19 -7
- package/node/src/builders/options.js +47 -61
- package/node/src/builders/serve.js +66 -16
- package/node/src/generation/anchor.d.ts +13 -0
- package/node/src/generation/anchor.js +92 -0
- package/node/src/generation/compiler.d.ts +6 -0
- package/node/src/generation/compiler.js +68 -0
- package/node/src/generation/emit.d.ts +3 -0
- package/node/src/generation/emit.js +26 -8
- package/node/src/generation/files.d.ts +26 -0
- package/node/src/generation/files.js +14 -3
- package/node/src/generation/filesystem.d.ts +7 -0
- package/node/src/generation/filesystem.js +261 -69
- package/node/src/generation/generate.d.ts +16 -0
- package/node/src/generation/generate.js +10 -4
- package/node/src/generation/index.d.ts +16 -0
- package/node/src/generation/index.js +15 -0
- package/node/src/generation/overlay.d.ts +3 -0
- package/node/src/generation/selectors.d.ts +2 -0
- package/node/src/generation/sources.d.ts +50 -0
- package/node/src/generation/sources.js +57 -12
- package/node/src/generation/validate.d.ts +12 -0
- package/node/src/generation/validate.js +53 -57
- package/package.json +7 -2
- package/schemas/application.json +764 -20
- package/schemas/serve.json +144 -30
- package/snippets/angular.json +29 -0
- package/snippets/generated/destinations.generated.ts +126 -0
- package/snippets/generated/routes.generated.ts +35 -0
- package/snippets/src/catalog.ts +20 -0
- package/snippets/src/docs-url.ts +8 -0
- package/snippets/src/index.html +11 -0
- package/snippets/src/main.ts +6 -0
- package/snippets/src/product-url.ts +8 -0
- package/snippets/src/routes/products/$productId/page.ts +20 -0
- package/snippets/src/routes/products/$productId/route.generated.ts +24 -0
- package/snippets/src/routes/products/$productId/route.ts +7 -0
- package/snippets/src/routes/reports/$reportId/page.ts +9 -0
- package/snippets/src/routes/reports/$reportId/route.generated.ts +24 -0
- package/snippets/src/routes/reports/$reportId/route.ts +6 -0
- package/snippets/src/routes/teams/$teamId/items/$id/inputs.ts +15 -0
- package/snippets/src/routes/teams/$teamId/items/$id/links.ts +13 -0
- package/snippets/src/routes/teams/$teamId/items/$id/page.ts +9 -0
- package/snippets/src/routes/teams/$teamId/items/$id/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/items/$id/route.ts +18 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/page.ts +9 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/route.ts +7 -0
- package/snippets/src/routes/teams/$teamId/old-items/$id/route.ts +13 -0
- package/snippets/src/routes/teams/$teamId/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/route.ts +6 -0
- package/snippets/src/shell.ts +9 -0
- package/snippets/src/slug.ts +14 -0
- package/snippets/tsconfig.app.json +14 -0
- package/snippets/tsconfig.routes.json +5 -0
- package/types/loozo-angular-router-_core.d.ts +262 -242
- package/types/loozo-angular-router-_generated.d.ts +155 -116
- package/types/loozo-angular-router.d.ts +2 -1
- package/.npmignore +0 -3
- package/fesm2022/loozo-angular-router.mjs.map +0 -1
|
@@ -7,29 +7,104 @@ import { specifier } from "./emit.js";
|
|
|
7
7
|
export function transparent(root, directory) {
|
|
8
8
|
return directory !== root && basename(directory).startsWith("-");
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Directive comments bind to a following line or the module start, so edits must not move them.
|
|
11
|
+
const directive = /@ts-(?:expect-error|ignore|check|nocheck)\b|@jsx(?:frag|importsource|runtime)?\b|\/\/\/\s*<(?:reference|amd-dependency|amd-module)\b|(?:eslint|oxlint)-disable-next-line\b/i;
|
|
12
|
+
function contribution(root, directory) {
|
|
13
|
+
if (directory === root || transparent(root, directory) || basename(directory).startsWith("+")) {
|
|
12
14
|
return "";
|
|
13
15
|
}
|
|
14
16
|
return basename(directory);
|
|
15
17
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
// Names that bindings, declarations and references use; property names are not bindings.
|
|
19
|
+
function identifiers(source) {
|
|
20
|
+
const names = new Set();
|
|
18
21
|
function visit(node) {
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const propertyName = (ts.isPropertyAssignment(node.parent) || ts.isPropertyAccessExpression(node.parent)) &&
|
|
23
|
+
node.parent.name === node;
|
|
24
|
+
if (ts.isIdentifier(node) && !propertyName) {
|
|
25
|
+
names.add(node.text);
|
|
21
26
|
}
|
|
22
27
|
ts.forEachChild(node, visit);
|
|
23
28
|
}
|
|
24
|
-
|
|
29
|
+
ts.forEachChild(source, visit);
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
function unusedBinding(source, preferred) {
|
|
33
|
+
const used = identifiers(source);
|
|
25
34
|
let name = preferred;
|
|
26
35
|
let index = 0;
|
|
27
|
-
while (
|
|
36
|
+
while (used.has(name)) {
|
|
28
37
|
index += 1;
|
|
29
38
|
name = `${preferred}${index}`;
|
|
30
39
|
}
|
|
31
40
|
return name;
|
|
32
41
|
}
|
|
42
|
+
function parentBinding(source) {
|
|
43
|
+
if (identifiers(source).has("parent")) {
|
|
44
|
+
return unusedBinding(source, "filesystemParent");
|
|
45
|
+
}
|
|
46
|
+
return "parent";
|
|
47
|
+
}
|
|
48
|
+
// Reuse a stringParam import from the router, else import it as `stringParam` when that
|
|
49
|
+
// identifier is free.
|
|
50
|
+
function stringParamBinding(source, imports) {
|
|
51
|
+
for (const statement of source.statements) {
|
|
52
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
53
|
+
statement.importClause?.isTypeOnly ||
|
|
54
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
55
|
+
statement.moduleSpecifier.text !== "@loozo/angular-router") {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const bindings = statement.importClause?.namedBindings;
|
|
59
|
+
if (!bindings || !ts.isNamedImports(bindings)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const binding of bindings.elements) {
|
|
63
|
+
if (!binding.isTypeOnly && (binding.propertyName ?? binding.name).text === "stringParam") {
|
|
64
|
+
return binding.name.text;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (identifiers(source).has("stringParam")) {
|
|
69
|
+
const name = unusedBinding(source, "filesystemStringParam");
|
|
70
|
+
imports.push(`import { stringParam as ${name} } from "@loozo/angular-router";`);
|
|
71
|
+
return name;
|
|
72
|
+
}
|
|
73
|
+
imports.push('import { stringParam } from "@loozo/angular-router";');
|
|
74
|
+
return "stringParam";
|
|
75
|
+
}
|
|
76
|
+
// After the last import, keeping ordinary same-line comments with the import they annotate. A
|
|
77
|
+
// directive or multiline comment there can bind to the next line, so insert before it instead.
|
|
78
|
+
function importPosition(source) {
|
|
79
|
+
let last;
|
|
80
|
+
for (const statement of source.statements) {
|
|
81
|
+
if (ts.isImportDeclaration(statement)) {
|
|
82
|
+
last = statement;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Every admitted declaration imports defineRoute.
|
|
86
|
+
let position = last.end;
|
|
87
|
+
for (const range of ts.getTrailingCommentRanges(source.text, last.end) ?? []) {
|
|
88
|
+
const comment = source.text.slice(range.pos, range.end);
|
|
89
|
+
if (directive.test(comment) || /[\r\n]/.test(comment)) {
|
|
90
|
+
return last.end;
|
|
91
|
+
}
|
|
92
|
+
position = range.end;
|
|
93
|
+
}
|
|
94
|
+
return position;
|
|
95
|
+
}
|
|
96
|
+
// Point the parent property at a binding, as shorthand when it is named `parent` and nothing
|
|
97
|
+
// but whitespace separates the key from its value.
|
|
98
|
+
function parentValue(property, value, name, source) {
|
|
99
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
100
|
+
return { start: value.getStart(source), end: value.end, text: `parent: ${name}` };
|
|
101
|
+
}
|
|
102
|
+
const separator = source.text.slice(property.name.end, value.getStart(source));
|
|
103
|
+
if (name === "parent" && /^\s*:\s*$/.test(separator)) {
|
|
104
|
+
return { start: property.getStart(source), end: property.end, text: name };
|
|
105
|
+
}
|
|
106
|
+
return { start: value.getStart(source), end: value.end, text: name };
|
|
107
|
+
}
|
|
33
108
|
function removeSyntax(node, source) {
|
|
34
109
|
const start = node.getStart(source);
|
|
35
110
|
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, source.text);
|
|
@@ -46,8 +121,7 @@ function removeSyntax(node, source) {
|
|
|
46
121
|
}
|
|
47
122
|
// Include adjacent retained trivia: it too can become module-leading after
|
|
48
123
|
// deletion. Stop at the next actual token, without copying trailing bytes.
|
|
49
|
-
if (comment &&
|
|
50
|
-
/@ts-(?:expect-error|ignore|check|nocheck)\b|@jsx(?:frag|importsource|runtime)?\b|\/\/\/\s*<(?:reference|amd-dependency|amd-module)\b|(?:eslint|oxlint)-disable-next-line\b/i.test(scanner.getTokenText())) {
|
|
124
|
+
if (comment && directive.test(scanner.getTokenText())) {
|
|
51
125
|
fail(node, "Cannot remove directive-bound filesystem parent syntax; repair it explicitly");
|
|
52
126
|
}
|
|
53
127
|
if (scanner.getTokenPos() >= start && scanner.getTokenPos() < node.end && trivia) {
|
|
@@ -123,6 +197,47 @@ function otherUses(source, imported, parent, checker) {
|
|
|
123
197
|
visit(source);
|
|
124
198
|
return used;
|
|
125
199
|
}
|
|
200
|
+
// The builder owns the path, so the fix is in the params or the directories, never the path.
|
|
201
|
+
// `opaque` says why the builder can't read the params' keys.
|
|
202
|
+
function ambiguousParams(node, path, removed, added, opaque) {
|
|
203
|
+
const changes = [];
|
|
204
|
+
if (removed.length > 0) {
|
|
205
|
+
changes.push(`removed params: ${removed.join(", ")}`);
|
|
206
|
+
}
|
|
207
|
+
if (added.length > 0) {
|
|
208
|
+
changes.push(`added params: ${added.join(", ")}`);
|
|
209
|
+
}
|
|
210
|
+
let repair = "Edit this file's params to match";
|
|
211
|
+
if (!parsePath(path).some((segment) => segment.kind !== "static")) {
|
|
212
|
+
repair = "Remove this file's params";
|
|
213
|
+
}
|
|
214
|
+
else if (opaque) {
|
|
215
|
+
repair = `This file's params ${opaque}; write each one as a plain \`name: codec\` property to match`;
|
|
216
|
+
}
|
|
217
|
+
fail(node, `Ambiguous filesystem params: the directories give path "${path}" (${changes.join("; ")}). ` +
|
|
218
|
+
`${repair}, or move or rename the directory`);
|
|
219
|
+
}
|
|
220
|
+
// The keys of a direct inline params object, or why the builder can't read them.
|
|
221
|
+
function paramKeys(params) {
|
|
222
|
+
if (!ts.isObjectLiteralExpression(params)) {
|
|
223
|
+
return "are not an inline object";
|
|
224
|
+
}
|
|
225
|
+
const keys = new Set();
|
|
226
|
+
for (const property of params.properties) {
|
|
227
|
+
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) {
|
|
228
|
+
return "use a spread or accessor";
|
|
229
|
+
}
|
|
230
|
+
const name = property.name;
|
|
231
|
+
if (!ts.isIdentifier(name) && !ts.isStringLiteral(name)) {
|
|
232
|
+
return "use a computed or numeric key";
|
|
233
|
+
}
|
|
234
|
+
if (keys.has(name.text)) {
|
|
235
|
+
return `repeat the key ${name.text}`;
|
|
236
|
+
}
|
|
237
|
+
keys.add(name.text);
|
|
238
|
+
}
|
|
239
|
+
return keys;
|
|
240
|
+
}
|
|
126
241
|
function patchDeclaration(declaration, path, parentFile, options, host, checker) {
|
|
127
242
|
const source = declaration.source;
|
|
128
243
|
const assignment = source.statements.find(ts.isExportAssignment);
|
|
@@ -149,34 +264,85 @@ function patchDeclaration(declaration, path, parentFile, options, host, checker)
|
|
|
149
264
|
desiredNames.add(segment.name);
|
|
150
265
|
}
|
|
151
266
|
}
|
|
152
|
-
const
|
|
153
|
-
|
|
267
|
+
const removed = [];
|
|
268
|
+
const added = [];
|
|
269
|
+
for (const name of names) {
|
|
270
|
+
if (!desiredNames.has(name)) {
|
|
271
|
+
removed.push(name);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const name of desiredNames) {
|
|
275
|
+
if (!names.has(name)) {
|
|
276
|
+
added.push(name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (removed.length > 0 || added.length > 0) {
|
|
154
280
|
const params = declaration.config.get("params");
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
fail(params, "Ambiguous filesystem codec change; update local params explicitly");
|
|
161
|
-
}
|
|
162
|
-
const inline = params;
|
|
163
|
-
const property = inline.properties[0];
|
|
164
|
-
if (!ts.isPropertyAssignment(property)) {
|
|
165
|
-
fail(property, "Filesystem param rename requires a direct property with its codec expression");
|
|
281
|
+
let keys = new Set();
|
|
282
|
+
if (params) {
|
|
283
|
+
const readable = paramKeys(params);
|
|
284
|
+
if (typeof readable === "string") {
|
|
285
|
+
ambiguousParams(params, path, removed, added, readable);
|
|
166
286
|
}
|
|
167
|
-
|
|
168
|
-
start: property.name.getStart(source),
|
|
169
|
-
end: property.name.end,
|
|
170
|
-
text: JSON.stringify(newName),
|
|
171
|
-
});
|
|
287
|
+
keys = readable;
|
|
172
288
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
289
|
+
// Params the author already made match the directories need only the path rewritten. A
|
|
290
|
+
// path without params matches only a declaration without `params`, as at runtime.
|
|
291
|
+
let reconciled = keys.size === desiredNames.size;
|
|
292
|
+
if (desiredNames.size === 0 && params) {
|
|
293
|
+
reconciled = false;
|
|
177
294
|
}
|
|
178
|
-
|
|
179
|
-
|
|
295
|
+
for (const name of desiredNames) {
|
|
296
|
+
if (!keys.has(name)) {
|
|
297
|
+
reconciled = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!reconciled) {
|
|
301
|
+
if (removed.length === 1 && added.length === 1 && params) {
|
|
302
|
+
const entries = properties(params, "Filesystem local params");
|
|
303
|
+
const [oldName] = removed;
|
|
304
|
+
const [newName] = added;
|
|
305
|
+
if (entries.size !== names.size || !entries.has(oldName)) {
|
|
306
|
+
ambiguousParams(params, path, removed, added);
|
|
307
|
+
}
|
|
308
|
+
const property = entries.get(oldName).parent;
|
|
309
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
310
|
+
ambiguousParams(property, path, removed, added, "use a shorthand property");
|
|
311
|
+
}
|
|
312
|
+
patches.push({
|
|
313
|
+
start: property.name.getStart(source),
|
|
314
|
+
end: property.name.end,
|
|
315
|
+
text: JSON.stringify(newName),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
else if (removed.length === 0 && params) {
|
|
319
|
+
// Pure additions keep every authored codec and give each new param the seed's codec.
|
|
320
|
+
for (const name of names) {
|
|
321
|
+
if (!keys.has(name)) {
|
|
322
|
+
ambiguousParams(params, path, removed, added);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (keys.size !== names.size) {
|
|
326
|
+
ambiguousParams(params, path, removed, added);
|
|
327
|
+
}
|
|
328
|
+
const name = stringParamBinding(source, imports);
|
|
329
|
+
const appended = [];
|
|
330
|
+
for (const param of added) {
|
|
331
|
+
appended.push(`${JSON.stringify(param)}: ${name}`);
|
|
332
|
+
}
|
|
333
|
+
patches.push(addProperty(params, appended.join(", ")));
|
|
334
|
+
}
|
|
335
|
+
else if (names.size === 0 && !params) {
|
|
336
|
+
const name = stringParamBinding(source, imports);
|
|
337
|
+
const entries = [];
|
|
338
|
+
for (const param of added) {
|
|
339
|
+
entries.push(`${JSON.stringify(param)}: ${name}`);
|
|
340
|
+
}
|
|
341
|
+
additions.push(`params: { ${entries.join(", ")} }`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
ambiguousParams(literal, path, removed, added);
|
|
345
|
+
}
|
|
180
346
|
}
|
|
181
347
|
}
|
|
182
348
|
patches.push({ start: literal.getStart(source), end: literal.end, text: JSON.stringify(path) });
|
|
@@ -195,6 +361,13 @@ function patchDeclaration(declaration, path, parentFile, options, host, checker)
|
|
|
195
361
|
end: module.end,
|
|
196
362
|
text: JSON.stringify(specifier(declaration.file, parentFile)),
|
|
197
363
|
});
|
|
364
|
+
// A specifier that still resolves names a different parent, and so does its binding. An
|
|
365
|
+
// unresolvable one went stale because a file moved or was deleted; its name may still fit.
|
|
366
|
+
const binding = imported.importClause.name;
|
|
367
|
+
if (resolved && binding.text !== "parent" && !identifiers(source).has("parent")) {
|
|
368
|
+
patches.push({ start: binding.getStart(source), end: binding.end, text: "parent" });
|
|
369
|
+
patches.push(parentValue(parent.parent, parent, "parent", source));
|
|
370
|
+
}
|
|
198
371
|
}
|
|
199
372
|
else {
|
|
200
373
|
if (!resolved && (usedElsewhere || imported.importClause?.namedBindings)) {
|
|
@@ -210,35 +383,35 @@ function patchDeclaration(declaration, path, parentFile, options, host, checker)
|
|
|
210
383
|
patches.push(...removeProperty(property, source));
|
|
211
384
|
}
|
|
212
385
|
else {
|
|
213
|
-
parentName =
|
|
214
|
-
|
|
215
|
-
if (ts.isShorthandPropertyAssignment(property)) {
|
|
216
|
-
text = `parent: ${parentName}`;
|
|
217
|
-
}
|
|
218
|
-
patches.push({ start: parent.getStart(source), end: parent.end, text });
|
|
386
|
+
parentName = parentBinding(source);
|
|
387
|
+
patches.push(parentValue(property, parent, parentName, source));
|
|
219
388
|
}
|
|
220
389
|
}
|
|
221
390
|
}
|
|
222
391
|
}
|
|
223
392
|
else if (parentFile) {
|
|
224
|
-
parentName =
|
|
225
|
-
|
|
393
|
+
parentName = parentBinding(source);
|
|
394
|
+
if (parentName === "parent") {
|
|
395
|
+
additions.push("parent");
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
additions.push(`parent: ${parentName}`);
|
|
399
|
+
}
|
|
226
400
|
}
|
|
227
401
|
if (parentFile && parentName) {
|
|
228
402
|
// A new binding leaves a still-valid old import and its unrelated uses alone.
|
|
229
|
-
imports.push(`import ${parentName} from ${JSON.stringify(specifier(declaration.file, parentFile))}
|
|
403
|
+
imports.push(`import ${parentName} from ${JSON.stringify(specifier(declaration.file, parentFile))};`);
|
|
230
404
|
}
|
|
231
405
|
if (additions.length > 0) {
|
|
232
406
|
patches.push(addProperty(object, additions.join(", ")));
|
|
233
407
|
}
|
|
234
408
|
if (imports.length > 0) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
409
|
+
const position = importPosition(source);
|
|
410
|
+
let text = "";
|
|
411
|
+
for (const statement of imports) {
|
|
412
|
+
text += newline + statement;
|
|
413
|
+
}
|
|
414
|
+
patches.push({ start: position, end: position, text });
|
|
242
415
|
}
|
|
243
416
|
patches.sort((a, b) => b.start - a.start);
|
|
244
417
|
let bytes = source.text;
|
|
@@ -254,6 +427,8 @@ function patchDeclaration(declaration, path, parentFile, options, host, checker)
|
|
|
254
427
|
}
|
|
255
428
|
export async function synchronize(root, declarations, pages, options, { filename = "route.ts" }, checker) {
|
|
256
429
|
const byDirectory = new Map();
|
|
430
|
+
// Only directories with a page or a declaration become records. Others contribute their
|
|
431
|
+
// path segment to the records below them.
|
|
257
432
|
const participating = new Set();
|
|
258
433
|
for (const declaration of declarations) {
|
|
259
434
|
const directory = dirname(declaration.file);
|
|
@@ -277,15 +452,6 @@ export async function synchronize(root, declarations, pages, options, { filename
|
|
|
277
452
|
}
|
|
278
453
|
participating.add(directory);
|
|
279
454
|
}
|
|
280
|
-
for (const directory of participating) {
|
|
281
|
-
let ancestor = directory;
|
|
282
|
-
while (ancestor !== root) {
|
|
283
|
-
ancestor = dirname(ancestor);
|
|
284
|
-
if (!transparent(root, ancestor)) {
|
|
285
|
-
participating.add(ancestor);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
455
|
const files = new Map();
|
|
290
456
|
for (const directory of [...participating].sort()) {
|
|
291
457
|
let file = byDirectory.get(directory)?.file;
|
|
@@ -305,16 +471,30 @@ export async function synchronize(root, declarations, pages, options, { filename
|
|
|
305
471
|
};
|
|
306
472
|
const authored = new Map();
|
|
307
473
|
for (const [directory, file] of files) {
|
|
474
|
+
const contributions = [contribution(root, directory)];
|
|
475
|
+
let ancestor = directory;
|
|
308
476
|
let parentFile;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
477
|
+
while (ancestor !== root && !parentFile) {
|
|
478
|
+
ancestor = dirname(ancestor);
|
|
479
|
+
parentFile = files.get(ancestor);
|
|
480
|
+
if (!parentFile) {
|
|
481
|
+
contributions.unshift(contribution(root, ancestor));
|
|
313
482
|
}
|
|
314
|
-
parentFile = files.get(parent);
|
|
315
483
|
}
|
|
316
|
-
const
|
|
317
|
-
const
|
|
484
|
+
const parts = [];
|
|
485
|
+
for (const part of contributions) {
|
|
486
|
+
if (part !== "") {
|
|
487
|
+
parts.push(part);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const path = parts.join("/");
|
|
491
|
+
let segments;
|
|
492
|
+
try {
|
|
493
|
+
segments = parsePath(path);
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
throw new Error(`Invalid filesystem path "${path}" for ${file}: ${error.message}`);
|
|
497
|
+
}
|
|
318
498
|
const declaration = byDirectory.get(directory);
|
|
319
499
|
if (declaration) {
|
|
320
500
|
const bytes = patchDeclaration(declaration, path, parentFile, options, host, checker);
|
|
@@ -330,14 +510,26 @@ export async function synchronize(root, declarations, pages, options, { filename
|
|
|
330
510
|
imports.push(`import parent from ${JSON.stringify(specifier(file, parentFile))};`);
|
|
331
511
|
config.push("parent");
|
|
332
512
|
}
|
|
513
|
+
const params = new Set();
|
|
333
514
|
for (const segment of segments) {
|
|
334
515
|
if (segment.kind !== "static") {
|
|
335
|
-
|
|
336
|
-
|
|
516
|
+
params.add(segment.name);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (params.size > 0) {
|
|
520
|
+
const entries = [];
|
|
521
|
+
for (const name of params) {
|
|
522
|
+
entries.push(`${JSON.stringify(name)}: stringParam`);
|
|
337
523
|
}
|
|
524
|
+
imported.push("stringParam");
|
|
525
|
+
config.push(`params: { ${entries.join(", ")} }`);
|
|
526
|
+
}
|
|
527
|
+
let object = "{}";
|
|
528
|
+
if (config.length > 0) {
|
|
529
|
+
object = `{ ${config.join(", ")} }`;
|
|
338
530
|
}
|
|
339
531
|
imports.unshift(`import { ${imported.join(", ")} } from "@loozo/angular-router";`);
|
|
340
|
-
authored.set(file, `${imports.join("\n")}\n\nexport default defineRoute(${JSON.stringify(path)},
|
|
532
|
+
authored.set(file, `${imports.join("\n")}\n\nexport default defineRoute(${JSON.stringify(path)}, ${object});\n`);
|
|
341
533
|
}
|
|
342
534
|
return authored;
|
|
343
535
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AutoDefineRoutes } from "./filesystem.js";
|
|
2
|
+
import type { CompilerState } from "./compiler.js";
|
|
3
|
+
import type { AnchorState } from "./anchor.js";
|
|
4
|
+
export interface GenerationOptions {
|
|
5
|
+
tsconfig: string;
|
|
6
|
+
scanDirectories: string[];
|
|
7
|
+
outDirectory: string;
|
|
8
|
+
check?: boolean;
|
|
9
|
+
autoDefineRoutes?: AutoDefineRoutes | false;
|
|
10
|
+
}
|
|
11
|
+
export interface GenerationState {
|
|
12
|
+
discovery: CompilerState;
|
|
13
|
+
overlay: CompilerState;
|
|
14
|
+
anchor: AnchorState;
|
|
15
|
+
}
|
|
16
|
+
export declare function generate(workspace: string, options: GenerationOptions, stopped: () => boolean, state?: GenerationState): Promise<void>;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { discover } from "./sources.js";
|
|
3
|
-
import { selections,
|
|
3
|
+
import { createValidationProgram, selections, validateRoutes } from "./validate.js";
|
|
4
4
|
import { emit } from "./emit.js";
|
|
5
5
|
import { auditPath, metadata, planOutputs, publish, walk } from "./files.js";
|
|
6
|
-
export async function generate(workspace, options, stopped) {
|
|
7
|
-
const
|
|
6
|
+
export async function generate(workspace, options, stopped, state) {
|
|
7
|
+
const { autoDefineRoutes = {} } = options;
|
|
8
|
+
let auto;
|
|
9
|
+
if (autoDefineRoutes !== false) {
|
|
10
|
+
auto = autoDefineRoutes;
|
|
11
|
+
}
|
|
12
|
+
const sources = await discover(workspace, options.tsconfig, options.scanDirectories, options.outDirectory, auto, undefined, undefined, undefined, state?.discovery);
|
|
8
13
|
const selected = selections(sources);
|
|
9
14
|
const desired = emit(sources, selected);
|
|
10
15
|
const globalFiles = [
|
|
@@ -25,6 +30,7 @@ export async function generate(workspace, options, stopped) {
|
|
|
25
30
|
throw new Error(`Filesystem definition is not a regular file: ${file}`);
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
|
-
|
|
33
|
+
const validation = createValidationProgram(sources, desired, plan.stale, state?.overlay);
|
|
34
|
+
await validateRoutes(sources, desired, validation, state?.anchor);
|
|
29
35
|
publish(plan, options.check === true, stopped, sources.authored, seeds);
|
|
30
36
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CompilerState } from "./compiler.js";
|
|
2
|
+
export declare function prepareLaunchRoutes(workspace: string, options: {
|
|
3
|
+
tsconfig: string;
|
|
4
|
+
scanDirectories: string[];
|
|
5
|
+
outDirectory: string;
|
|
6
|
+
}, typeEntry: string, compiler?: CompilerState): Promise<{
|
|
7
|
+
sources: import("./sources.js").Sources;
|
|
8
|
+
desired: Map<string, string>;
|
|
9
|
+
}>;
|
|
10
|
+
export { CompilerState } from "./compiler.js";
|
|
11
|
+
export { AnchorState } from "./anchor.js";
|
|
12
|
+
export { createValidationProgram, validateRoutes } from "./validate.js";
|
|
13
|
+
export { planOutputs, publish, walk, auditPath, launchMarkers } from "./files.js";
|
|
14
|
+
export { fail, properties, namedImport, genuineImport } from "./sources.js";
|
|
15
|
+
export { specifier } from "./emit.js";
|
|
16
|
+
export type { Sources, Declaration, LaunchDeclaration } from "./sources.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Closed integration seam for @loozo/angular-launch. Node-only, not a plugin API.
|
|
2
|
+
import { discover } from "./sources.js";
|
|
3
|
+
import { selections } from "./validate.js";
|
|
4
|
+
import { emit } from "./emit.js";
|
|
5
|
+
export async function prepareLaunchRoutes(workspace, options, typeEntry, compiler) {
|
|
6
|
+
const sources = await discover(workspace, options.tsconfig, options.scanDirectories, options.outDirectory, undefined, new Map(), false, { typeEntry }, compiler);
|
|
7
|
+
const desired = emit(sources, selections(sources));
|
|
8
|
+
return { sources, desired };
|
|
9
|
+
}
|
|
10
|
+
export { CompilerState } from "./compiler.js";
|
|
11
|
+
export { AnchorState } from "./anchor.js";
|
|
12
|
+
export { createValidationProgram, validateRoutes } from "./validate.js";
|
|
13
|
+
export { planOutputs, publish, walk, auditPath, launchMarkers } from "./files.js";
|
|
14
|
+
export { fail, properties, namedImport, genuineImport } from "./sources.js";
|
|
15
|
+
export { specifier } from "./emit.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
export declare function overlayHost(options: ts.CompilerOptions, authored: ReadonlyMap<string, string>): ts.CompilerHost;
|
|
3
|
+
export declare function overlayDirectory(authored: ReadonlyMap<string, string>, resolveDirectory: (directory: string) => string): ts.ParseConfigHost["readDirectory"];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { type Segment } from "../../_core/src/path.js";
|
|
3
|
+
import type { CompilerState } from "./compiler.js";
|
|
4
|
+
import { type AutoDefineRoutes } from "./filesystem.js";
|
|
5
|
+
export interface Declaration {
|
|
6
|
+
file: string;
|
|
7
|
+
id: string;
|
|
8
|
+
source: ts.SourceFile;
|
|
9
|
+
rawText: string;
|
|
10
|
+
config: Map<string, ts.Expression>;
|
|
11
|
+
path: string;
|
|
12
|
+
segments: readonly Segment[];
|
|
13
|
+
effective: readonly Segment[];
|
|
14
|
+
parent?: Declaration;
|
|
15
|
+
target?: Declaration;
|
|
16
|
+
children: Declaration[];
|
|
17
|
+
page?: string;
|
|
18
|
+
retained: boolean;
|
|
19
|
+
full: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface LaunchDeclaration {
|
|
22
|
+
file: string;
|
|
23
|
+
kind: "defineContext" | "defineRpc";
|
|
24
|
+
call: ts.CallExpression;
|
|
25
|
+
}
|
|
26
|
+
export interface LaunchMode {
|
|
27
|
+
typeEntry: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Sources {
|
|
30
|
+
configFile: string;
|
|
31
|
+
parsed: ts.ParsedCommandLine;
|
|
32
|
+
roots: string[];
|
|
33
|
+
output: string;
|
|
34
|
+
encountered: string[];
|
|
35
|
+
declarations: Declaration[];
|
|
36
|
+
factoryEntry: string;
|
|
37
|
+
deferredFactories: Declaration[];
|
|
38
|
+
authored: ReadonlyMap<string, string>;
|
|
39
|
+
filesystem: boolean;
|
|
40
|
+
launch?: {
|
|
41
|
+
typeEntry: string;
|
|
42
|
+
declarations: LaunchDeclaration[];
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export declare function fail(node: ts.Node, message: string): never;
|
|
46
|
+
export declare function properties(node: ts.Expression, label: string): Map<string, ts.Expression>;
|
|
47
|
+
export declare function resolveModule(name: string, source: string, options: ts.CompilerOptions, host?: ts.ModuleResolutionHost): string | undefined;
|
|
48
|
+
export declare function namedImport(source: ts.SourceFile, name: ts.Expression, imported: string): ts.ImportDeclaration | undefined;
|
|
49
|
+
export declare function genuineImport(statement: ts.ImportDeclaration, expected: string, options: ts.CompilerOptions, host?: ts.ModuleResolutionHost): Promise<boolean>;
|
|
50
|
+
export declare function discover(workspace: string, config: string, scans: readonly string[], out: string, auto?: AutoDefineRoutes, authored?: ReadonlyMap<string, string>, filesystem?: boolean, launch?: LaunchMode, compiler?: CompilerState): Promise<Sources>;
|