@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.
Files changed (68) hide show
  1. package/README.md +565 -80
  2. package/builders.json +2 -2
  3. package/fesm2022/loozo-angular-router-_core.mjs +332 -595
  4. package/fesm2022/loozo-angular-router-_core.mjs.map +1 -1
  5. package/fesm2022/loozo-angular-router-_generated.mjs +335 -627
  6. package/fesm2022/loozo-angular-router-_generated.mjs.map +1 -1
  7. package/fesm2022/loozo-angular-router.mjs +2 -19
  8. package/node/_core/src/path.d.ts +17 -0
  9. package/node/src/builders/application.js +19 -7
  10. package/node/src/builders/options.js +47 -61
  11. package/node/src/builders/serve.js +66 -16
  12. package/node/src/generation/anchor.d.ts +13 -0
  13. package/node/src/generation/anchor.js +92 -0
  14. package/node/src/generation/compiler.d.ts +6 -0
  15. package/node/src/generation/compiler.js +68 -0
  16. package/node/src/generation/emit.d.ts +3 -0
  17. package/node/src/generation/emit.js +26 -8
  18. package/node/src/generation/files.d.ts +26 -0
  19. package/node/src/generation/files.js +14 -3
  20. package/node/src/generation/filesystem.d.ts +7 -0
  21. package/node/src/generation/filesystem.js +261 -69
  22. package/node/src/generation/generate.d.ts +16 -0
  23. package/node/src/generation/generate.js +10 -4
  24. package/node/src/generation/index.d.ts +16 -0
  25. package/node/src/generation/index.js +15 -0
  26. package/node/src/generation/overlay.d.ts +3 -0
  27. package/node/src/generation/selectors.d.ts +2 -0
  28. package/node/src/generation/sources.d.ts +50 -0
  29. package/node/src/generation/sources.js +57 -12
  30. package/node/src/generation/validate.d.ts +12 -0
  31. package/node/src/generation/validate.js +53 -57
  32. package/package.json +7 -2
  33. package/schemas/application.json +764 -20
  34. package/schemas/serve.json +144 -30
  35. package/snippets/angular.json +29 -0
  36. package/snippets/generated/destinations.generated.ts +126 -0
  37. package/snippets/generated/routes.generated.ts +35 -0
  38. package/snippets/src/catalog.ts +20 -0
  39. package/snippets/src/docs-url.ts +8 -0
  40. package/snippets/src/index.html +11 -0
  41. package/snippets/src/main.ts +6 -0
  42. package/snippets/src/product-url.ts +8 -0
  43. package/snippets/src/routes/products/$productId/page.ts +20 -0
  44. package/snippets/src/routes/products/$productId/route.generated.ts +24 -0
  45. package/snippets/src/routes/products/$productId/route.ts +7 -0
  46. package/snippets/src/routes/reports/$reportId/page.ts +9 -0
  47. package/snippets/src/routes/reports/$reportId/route.generated.ts +24 -0
  48. package/snippets/src/routes/reports/$reportId/route.ts +6 -0
  49. package/snippets/src/routes/teams/$teamId/items/$id/inputs.ts +15 -0
  50. package/snippets/src/routes/teams/$teamId/items/$id/links.ts +13 -0
  51. package/snippets/src/routes/teams/$teamId/items/$id/page.ts +9 -0
  52. package/snippets/src/routes/teams/$teamId/items/$id/route.generated.ts +24 -0
  53. package/snippets/src/routes/teams/$teamId/items/$id/route.ts +18 -0
  54. package/snippets/src/routes/teams/$teamId/members/$memberId/page.ts +9 -0
  55. package/snippets/src/routes/teams/$teamId/members/$memberId/route.generated.ts +24 -0
  56. package/snippets/src/routes/teams/$teamId/members/$memberId/route.ts +7 -0
  57. package/snippets/src/routes/teams/$teamId/old-items/$id/route.ts +13 -0
  58. package/snippets/src/routes/teams/$teamId/route.generated.ts +24 -0
  59. package/snippets/src/routes/teams/$teamId/route.ts +6 -0
  60. package/snippets/src/shell.ts +9 -0
  61. package/snippets/src/slug.ts +14 -0
  62. package/snippets/tsconfig.app.json +14 -0
  63. package/snippets/tsconfig.routes.json +5 -0
  64. package/types/loozo-angular-router-_core.d.ts +262 -242
  65. package/types/loozo-angular-router-_generated.d.ts +155 -116
  66. package/types/loozo-angular-router.d.ts +2 -1
  67. package/.npmignore +0 -3
  68. 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
- function localPath(root, directory) {
11
- if (directory === root || basename(directory).startsWith("+")) {
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
- function unusedBinding(source, preferred) {
17
- const identifiers = new Set();
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
- if (ts.isIdentifier(node)) {
20
- identifiers.add(node.text);
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
- visit(source);
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 (identifiers.has(name)) {
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 sameNames = names.size === desiredNames.size && [...names].every((name) => desiredNames.has(name));
153
- if (!sameNames) {
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
- if (names.size === 1 && desiredNames.size === 1 && params) {
156
- const entries = properties(params, "Filesystem local params");
157
- const oldName = [...names][0];
158
- const newName = [...desiredNames][0];
159
- if (entries.size !== 1 || !entries.has(oldName)) {
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
- patches.push({
168
- start: property.name.getStart(source),
169
- end: property.name.end,
170
- text: JSON.stringify(newName),
171
- });
287
+ keys = readable;
172
288
  }
173
- else if (names.size === 0 && desiredNames.size === 1 && !params) {
174
- const name = unusedBinding(source, "filesystemStringParam");
175
- imports.push(`import { stringParam as ${name} } from "@loozo/angular-router";${newline}`);
176
- additions.push(`params: { ${JSON.stringify([...desiredNames][0])}: ${name} }`);
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
- else {
179
- fail(literal, "Ambiguous filesystem codec change; update path and local params explicitly before synchronization");
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 = unusedBinding(source, "filesystemParent");
214
- let text = parentName;
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 = unusedBinding(source, "filesystemParent");
225
- additions.push(`parent: ${parentName}`);
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))};${newline}`);
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
- // Static imports are hoisted. Appending at the complete module boundary
236
- // preserves every existing statement/comment attachment without parsing it.
237
- patches.push({
238
- start: source.text.length,
239
- end: source.text.length,
240
- text: newline + imports.join(""),
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
- if (directory !== root) {
310
- let parent = dirname(directory);
311
- while (transparent(root, parent)) {
312
- parent = dirname(parent);
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 path = localPath(root, directory);
317
- const segments = parsePath(path);
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
- imported.push("stringParam");
336
- config.push(`params: { ${JSON.stringify(segment.name)}: stringParam }`);
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)}, { ${config.join(", ")} });\n`);
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, validateProgram } from "./validate.js";
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 sources = await discover(workspace, options.tsconfig, options.scanDirectories, options.outDirectory, options.autoDefineRoutes);
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
- await validateProgram(sources, desired, plan.stale);
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,2 @@
1
+ import type { Declaration, Sources } from "./sources.js";
2
+ export declare function relativeSelections(sources: Sources, selected: ReadonlyMap<string, Declaration>): Map<string, Map<string, string>>;
@@ -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>;