@loozo/angular-router 0.0.2 → 0.0.3

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 (37) hide show
  1. package/README.md +442 -79
  2. package/builders.json +2 -2
  3. package/fesm2022/loozo-angular-router-_core.mjs +348 -589
  4. package/fesm2022/loozo-angular-router-_core.mjs.map +1 -1
  5. package/fesm2022/loozo-angular-router-_generated.mjs +492 -617
  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 +9 -6
  10. package/node/src/builders/options.js +38 -58
  11. package/node/src/builders/serve.js +51 -8
  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/files.d.ts +26 -0
  18. package/node/src/generation/files.js +14 -3
  19. package/node/src/generation/filesystem.d.ts +7 -0
  20. package/node/src/generation/generate.d.ts +16 -0
  21. package/node/src/generation/generate.js +5 -4
  22. package/node/src/generation/index.d.ts +16 -0
  23. package/node/src/generation/index.js +15 -0
  24. package/node/src/generation/overlay.d.ts +3 -0
  25. package/node/src/generation/selectors.d.ts +2 -0
  26. package/node/src/generation/sources.d.ts +50 -0
  27. package/node/src/generation/sources.js +50 -10
  28. package/node/src/generation/validate.d.ts +12 -0
  29. package/node/src/generation/validate.js +38 -33
  30. package/package.json +7 -2
  31. package/schemas/application.json +743 -8
  32. package/schemas/serve.json +11 -32
  33. package/types/loozo-angular-router-_core.d.ts +159 -181
  34. package/types/loozo-angular-router-_generated.d.ts +131 -109
  35. package/types/loozo-angular-router.d.ts +2 -1
  36. package/.npmignore +0 -3
  37. package/fesm2022/loozo-angular-router.mjs.map +0 -1
@@ -0,0 +1,92 @@
1
+ import ts from "typescript";
2
+ import { statSync } from "node:fs";
3
+ import { realpath, stat } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ async function stamp(file) {
6
+ try {
7
+ const info = await stat(file);
8
+ return `${info.mtimeMs}:${info.size}`;
9
+ }
10
+ catch {
11
+ return "missing";
12
+ }
13
+ }
14
+ // Immutable canonical factory descriptors for one generation session. Inputs are
15
+ // the effective options, entry, factory names and every file the anchor program
16
+ // read (sources and package metadata). A new higher-priority resolution candidate
17
+ // appearing mid-session is not detected; dependency changes need a new session.
18
+ // Recomputation builds a throwaway program, so no SourceFile or Symbol outlives it.
19
+ export class AnchorState {
20
+ #key;
21
+ #anchor;
22
+ #inputs = new Map();
23
+ clear() {
24
+ this.#key = undefined;
25
+ this.#anchor = undefined;
26
+ this.#inputs.clear();
27
+ }
28
+ async anchor(entry, names, options) {
29
+ const { configFile: _configFile, ...settings } = options;
30
+ const key = JSON.stringify({ entry, names, settings });
31
+ if (this.#anchor && key === this.#key) {
32
+ const files = [...this.#inputs.keys()];
33
+ const current = await Promise.all(files.map(stamp));
34
+ let unchanged = true;
35
+ for (let index = 0; index < files.length; index += 1) {
36
+ if (current[index] !== this.#inputs.get(files[index])) {
37
+ unchanged = false;
38
+ }
39
+ }
40
+ if (unchanged) {
41
+ return this.#anchor;
42
+ }
43
+ }
44
+ this.#key = undefined;
45
+ this.#anchor = undefined;
46
+ this.#inputs.clear();
47
+ const inputs = new Map();
48
+ const host = ts.createCompilerHost(options, true);
49
+ const read = host.readFile.bind(host);
50
+ // Stamp before reading, so an edit racing this pass invalidates the next one.
51
+ host.readFile = (file) => {
52
+ const path = resolve(file);
53
+ try {
54
+ const info = statSync(path);
55
+ inputs.set(path, `${info.mtimeMs}:${info.size}`);
56
+ }
57
+ catch {
58
+ inputs.set(path, "missing");
59
+ }
60
+ return read(file);
61
+ };
62
+ const program = ts.createProgram({ rootNames: [entry], options, host });
63
+ const checker = program.getTypeChecker();
64
+ const module = checker.getSymbolAtLocation(program.getSourceFile(entry));
65
+ const exports = checker.getExportsOfModule(module);
66
+ const anchor = new Map();
67
+ for (const name of names) {
68
+ let symbol = exports.find((value) => value.name === name);
69
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
70
+ symbol = checker.getAliasedSymbol(symbol);
71
+ }
72
+ if (!symbol?.declarations?.length) {
73
+ anchor.set(name, undefined);
74
+ continue;
75
+ }
76
+ const declarations = [];
77
+ for (const declaration of symbol.declarations) {
78
+ declarations.push({
79
+ file: await realpath(declaration.getSourceFile().fileName),
80
+ kind: declaration.kind,
81
+ pos: declaration.pos,
82
+ end: declaration.end,
83
+ });
84
+ }
85
+ anchor.set(name, declarations);
86
+ }
87
+ this.#key = key;
88
+ this.#anchor = anchor;
89
+ this.#inputs = inputs;
90
+ return anchor;
91
+ }
92
+ }
@@ -0,0 +1,6 @@
1
+ import ts from "typescript";
2
+ export declare class CompilerState {
3
+ #private;
4
+ clear(): void;
5
+ createProgram(rootNames: readonly string[], options: ts.CompilerOptions, host: ts.CompilerHost): ts.Program;
6
+ }
@@ -0,0 +1,68 @@
1
+ import ts from "typescript";
2
+ import { resolve } from "node:path";
3
+ // One graph's content-aware SourceFiles, including native binding data.
4
+ // Hosts, Programs, checkers and native resolution caches are fresh on every pass.
5
+ export class CompilerState {
6
+ #files = new Map();
7
+ #optionsKey;
8
+ clear() {
9
+ this.#files.clear();
10
+ this.#optionsKey = undefined;
11
+ }
12
+ createProgram(rootNames, options, host) {
13
+ // configFile is an AST, not an effective compiler setting. Configs and their
14
+ // diagnostics are parsed afresh by discovery; never serialize/retain that AST.
15
+ const { configFile: _configFile, ...settings } = options;
16
+ const optionsKey = JSON.stringify(settings);
17
+ if (optionsKey !== this.#optionsKey) {
18
+ this.clear();
19
+ this.#optionsKey = optionsKey;
20
+ }
21
+ host.getSourceFile = (file, language, _onError, shouldCreateNewSourceFile) => {
22
+ const key = host.getCanonicalFileName(resolve(file));
23
+ const text = host.readFile(file);
24
+ if (text === undefined) {
25
+ this.#files.delete(key);
26
+ return undefined;
27
+ }
28
+ let languageVersion;
29
+ let impliedNodeFormat;
30
+ let jsDocParsingMode = host.jsDocParsingMode;
31
+ if (typeof language === "object") {
32
+ languageVersion = language.languageVersion;
33
+ impliedNodeFormat = language.impliedNodeFormat;
34
+ jsDocParsingMode = language.jsDocParsingMode;
35
+ }
36
+ else {
37
+ languageVersion = language;
38
+ }
39
+ const cached = this.#files.get(key);
40
+ // The external-module indicator supplied in `language` derives from the
41
+ // effective settings above; implied format/JSDoc mode are per-file inputs.
42
+ if (!shouldCreateNewSourceFile &&
43
+ cached &&
44
+ cached.source.text === text &&
45
+ cached.languageVersion === languageVersion &&
46
+ cached.impliedNodeFormat === impliedNodeFormat &&
47
+ cached.jsDocParsingMode === jsDocParsingMode) {
48
+ return cached.source;
49
+ }
50
+ const source = ts.createSourceFile(file, text, language, true);
51
+ this.#files.set(key, { source, languageVersion, impliedNodeFormat, jsDocParsingMode });
52
+ return source;
53
+ };
54
+ // Deliberately do not supply oldProgram: package metadata, missing lookups,
55
+ // module/type/lib resolutions and directory existence must be current.
56
+ const program = ts.createProgram({ rootNames, options, host });
57
+ const retained = new Set();
58
+ for (const source of program.getSourceFiles()) {
59
+ retained.add(host.getCanonicalFileName(resolve(source.fileName)));
60
+ }
61
+ for (const file of this.#files.keys()) {
62
+ if (!retained.has(file)) {
63
+ this.#files.delete(file);
64
+ }
65
+ }
66
+ return program;
67
+ }
68
+ }
@@ -0,0 +1,3 @@
1
+ import { type Declaration, type Sources } from "./sources.js";
2
+ export declare function specifier(from: string, file: string): string;
3
+ export declare function emit(sources: Sources, selected: ReadonlyMap<string, Declaration>): Map<string, string>;
@@ -0,0 +1,26 @@
1
+ export declare const markers: {
2
+ readonly "routes.generated.ts": "// @loozo/angular-router routegen: owned routes.generated.ts";
3
+ readonly "destinations.generated.ts": "// @loozo/angular-router routegen: owned destinations.generated.ts";
4
+ readonly "route.generated.ts": "// @loozo/angular-router routegen: owned route.generated.ts";
5
+ };
6
+ export declare const launchMarkers: {
7
+ readonly "rpc.generated.ts": "// @loozo/angular-launch: owned rpc.generated.ts";
8
+ readonly "context.generated.d.ts": "// @loozo/angular-launch: owned context.generated.d.ts";
9
+ readonly "server.generated.ts": "// @loozo/angular-launch: owned server.generated.ts";
10
+ };
11
+ export declare function contains(directory: string, file: string): boolean;
12
+ export declare function metadata(path: string): Promise<import("fs").Stats | undefined>;
13
+ export declare function workspacePaths(workspace: string): Promise<{
14
+ canonical: string;
15
+ resolve(path: string, directory?: boolean): string;
16
+ }>;
17
+ export declare function auditPath(path: string): Promise<void>;
18
+ export declare function walk(directory: string): Promise<string[]>;
19
+ export interface OutputPlan {
20
+ changed: string[];
21
+ missing: string[];
22
+ stale: string[];
23
+ desired: ReadonlyMap<string, string>;
24
+ }
25
+ export declare function planOutputs(desired: ReadonlyMap<string, string>, encountered: readonly string[], globals: readonly string[], mode?: "router-and-launch"): Promise<OutputPlan>;
26
+ export declare function publish(plan: OutputPlan, check: boolean, stopped: () => boolean, authored: ReadonlyMap<string, string>, seeds: ReadonlySet<string>): void;
@@ -6,6 +6,11 @@ export const markers = {
6
6
  "destinations.generated.ts": "// @loozo/angular-router routegen: owned destinations.generated.ts",
7
7
  "route.generated.ts": "// @loozo/angular-router routegen: owned route.generated.ts",
8
8
  };
9
+ export const launchMarkers = {
10
+ "rpc.generated.ts": "// @loozo/angular-launch: owned rpc.generated.ts",
11
+ "context.generated.d.ts": "// @loozo/angular-launch: owned context.generated.d.ts",
12
+ "server.generated.ts": "// @loozo/angular-launch: owned server.generated.ts",
13
+ };
9
14
  export function contains(directory, file) {
10
15
  const suffix = relative(directory, file);
11
16
  return (suffix === "" || (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
@@ -110,13 +115,15 @@ export async function walk(directory) {
110
115
  await visit(directory);
111
116
  return result;
112
117
  }
113
- export async function planOutputs(desired, encountered, globals) {
118
+ export async function planOutputs(desired, encountered, globals, mode) {
114
119
  const changed = [];
115
120
  const missing = [];
116
121
  const stale = [];
117
122
  const possible = new Set([...desired.keys(), ...globals]);
118
123
  for (const path of encountered) {
119
- if (path.endsWith(`${sep}route.generated.ts`)) {
124
+ if (path.endsWith(`${sep}route.generated.ts`) ||
125
+ (mode === "router-and-launch" &&
126
+ Object.keys(launchMarkers).some((name) => path.endsWith(`${sep}${name}`)))) {
120
127
  possible.add(path);
121
128
  }
122
129
  }
@@ -134,8 +141,12 @@ export async function planOutputs(desired, encountered, globals) {
134
141
  throw new Error(`Generated output is not a regular file: ${path}`);
135
142
  }
136
143
  const bytes = await readFile(path, "utf8");
144
+ const ownership = { ...markers };
145
+ if (mode === "router-and-launch") {
146
+ Object.assign(ownership, launchMarkers);
147
+ }
137
148
  const name = path.slice(path.lastIndexOf(sep) + 1);
138
- const owned = bytes.split("\n", 1)[0] === markers[name];
149
+ const owned = bytes.split("\n", 1)[0] === ownership[name];
139
150
  if (!owned) {
140
151
  if (expected !== undefined) {
141
152
  throw new Error(`Unmarked generated-output collision: ${path}`);
@@ -0,0 +1,7 @@
1
+ import ts from "typescript";
2
+ import { type Declaration } from "./sources.js";
3
+ export interface AutoDefineRoutes {
4
+ filename?: string;
5
+ }
6
+ export declare function transparent(root: string, directory: string): boolean;
7
+ export declare function synchronize(root: string, declarations: readonly Declaration[], pages: ReadonlyMap<string, string>, options: ts.CompilerOptions, { filename }: AutoDefineRoutes, checker: ts.TypeChecker): Promise<Map<string, string>>;
@@ -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;
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,10 @@
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 sources = await discover(workspace, options.tsconfig, options.scanDirectories, options.outDirectory, options.autoDefineRoutes, undefined, undefined, undefined, state?.discovery);
8
8
  const selected = selections(sources);
9
9
  const desired = emit(sources, selected);
10
10
  const globalFiles = [
@@ -25,6 +25,7 @@ export async function generate(workspace, options, stopped) {
25
25
  throw new Error(`Filesystem definition is not a regular file: ${file}`);
26
26
  }
27
27
  }
28
- await validateProgram(sources, desired, plan.stale);
28
+ const validation = createValidationProgram(sources, desired, plan.stale, state?.overlay);
29
+ await validateRoutes(sources, desired, validation, state?.anchor);
29
30
  publish(plan, options.check === true, stopped, sources.authored, seeds);
30
31
  }
@@ -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>;
@@ -41,7 +41,7 @@ export function properties(node, label) {
41
41
  export function resolveModule(name, source, options, host = ts.sys) {
42
42
  return ts.resolveModuleName(name, source, options, host).resolvedModule?.resolvedFileName;
43
43
  }
44
- function namedImport(source, name, imported) {
44
+ export function namedImport(source, name, imported) {
45
45
  if (!ts.isIdentifier(name)) {
46
46
  return undefined;
47
47
  }
@@ -90,7 +90,7 @@ function moduleText(statement) {
90
90
  }
91
91
  return statement.moduleSpecifier.text;
92
92
  }
93
- async function genuineImport(statement, expected, options, host = ts.sys) {
93
+ export async function genuineImport(statement, expected, options, host = ts.sys) {
94
94
  const actual = resolveModule(moduleText(statement), statement.getSourceFile().fileName, options, host);
95
95
  if (!actual) {
96
96
  return false;
@@ -172,7 +172,7 @@ const keys = new Set([
172
172
  "title",
173
173
  "data",
174
174
  ]);
175
- export async function discover(workspace, config, scans, out, auto, authored = new Map(), filesystem = auto !== undefined) {
175
+ export async function discover(workspace, config, scans, out, auto, authored = new Map(), filesystem = auto !== undefined, launch, compiler) {
176
176
  if (scans.length !== 1) {
177
177
  throw new Error("scanDirectories must contain exactly one directory");
178
178
  }
@@ -223,7 +223,7 @@ export async function discover(workspace, config, scans, out, auto, authored = n
223
223
  continue;
224
224
  }
225
225
  await auditPath(normalized);
226
- if (contains(root, normalized)) {
226
+ if (launch || contains(root, normalized)) {
227
227
  candidates.push(normalized);
228
228
  }
229
229
  }
@@ -233,14 +233,22 @@ export async function discover(workspace, config, scans, out, auto, authored = n
233
233
  const manifest = JSON.parse(manifestBytes);
234
234
  const factoryEntry = await realpath(resolve(packageRoot, manifest.exports["."].types));
235
235
  const declarations = [];
236
+ const launchDeclarations = [];
237
+ const launchEntry = launch && (await realpath(launch.typeEntry));
236
238
  const pages = new Map();
237
239
  const deferredFactories = [];
238
240
  const syntaxHost = overlayHost(parsed.options, authored);
239
- const syntaxProgram = ts.createProgram({
240
- rootNames: candidates,
241
- options: parsed.options,
242
- host: syntaxHost,
243
- });
241
+ let syntaxProgram;
242
+ if (compiler) {
243
+ syntaxProgram = compiler.createProgram(candidates, parsed.options, syntaxHost);
244
+ }
245
+ else {
246
+ syntaxProgram = ts.createProgram({
247
+ rootNames: candidates,
248
+ options: parsed.options,
249
+ host: syntaxHost,
250
+ });
251
+ }
244
252
  for (const file of candidates) {
245
253
  const bytes = syntaxHost.readFile(file);
246
254
  if (bytes === undefined) {
@@ -281,6 +289,33 @@ export async function discover(workspace, config, scans, out, auto, authored = n
281
289
  fail(defaults[1], "Multiple competing default exports");
282
290
  }
283
291
  const node = defaults[0];
292
+ const inRoutes = contains(root, file);
293
+ if (launchEntry && ts.isExportAssignment(node) && ts.isCallExpression(node.expression)) {
294
+ const call = node.expression;
295
+ let kind;
296
+ let imported = namedImport(source, call.expression, "defineContext");
297
+ if (imported) {
298
+ kind = "defineContext";
299
+ }
300
+ else if (inRoutes) {
301
+ imported = namedImport(source, call.expression, "defineRpc");
302
+ if (imported) {
303
+ kind = "defineRpc";
304
+ }
305
+ }
306
+ if (kind && imported) {
307
+ if (!(await genuineImport(imported, launchEntry, parsed.options, syntaxHost))) {
308
+ fail(imported, `${kind} must resolve to Launch's concrete server entry`);
309
+ }
310
+ launchDeclarations.push({ file, kind, call });
311
+ continue;
312
+ }
313
+ }
314
+ // Context is audited over all application roots; unrelated bootstrap defaults
315
+ // outside route scans are not route declarations.
316
+ if (!inRoutes) {
317
+ continue;
318
+ }
284
319
  if (ts.isClassDeclaration(node)) {
285
320
  await component(source, node, parsed.options);
286
321
  if (pages.has(dirname(file))) {
@@ -363,7 +398,7 @@ export async function discover(workspace, config, scans, out, auto, authored = n
363
398
  }
364
399
  if (auto) {
365
400
  const edits = await synchronize(roots[0], declarations, pages, parsed.options, auto, syntaxProgram.getTypeChecker());
366
- return discover(workspace, config, scans, out, undefined, edits, true);
401
+ return discover(workspace, config, scans, out, undefined, edits, true, launch, compiler);
367
402
  }
368
403
  if (filesystem) {
369
404
  for (let index = declarations.length - 1; index >= 0; index -= 1) {
@@ -519,6 +554,10 @@ export async function discover(workspace, config, scans, out, auto, authored = n
519
554
  fail(declaration.source, "Factory facade is not a final retained generated location");
520
555
  }
521
556
  }
557
+ let launchSources;
558
+ if (launchEntry) {
559
+ launchSources = { typeEntry: launchEntry, declarations: launchDeclarations };
560
+ }
522
561
  return {
523
562
  configFile,
524
563
  parsed,
@@ -530,5 +569,6 @@ export async function discover(workspace, config, scans, out, auto, authored = n
530
569
  deferredFactories,
531
570
  authored,
532
571
  filesystem,
572
+ launch: launchSources,
533
573
  };
534
574
  }
@@ -0,0 +1,12 @@
1
+ import ts from "typescript";
2
+ import { type Declaration, type Sources } from "./sources.js";
3
+ import type { CompilerState } from "./compiler.js";
4
+ import { AnchorState } from "./anchor.js";
5
+ export declare function authoredPath(declaration: Declaration): string;
6
+ export declare function selections(sources: Sources): Map<string, Declaration>;
7
+ export declare function createValidationProgram(sources: Sources, generated: ReadonlyMap<string, string>, stale: readonly string[], compiler?: CompilerState): {
8
+ program: ts.Program;
9
+ host: ts.CompilerHost;
10
+ obsolete: Set<string>;
11
+ };
12
+ export declare function validateRoutes(sources: Sources, generated: ReadonlyMap<string, string>, validation: ReturnType<typeof createValidationProgram>, canonical?: AnchorState): Promise<void>;
@@ -4,6 +4,7 @@ import { basename, dirname, resolve, sep } from "node:path";
4
4
  import { auditPath, contains } from "./files.js";
5
5
  import { fail, properties, resolveModule } from "./sources.js";
6
6
  import { overlayHost } from "./overlay.js";
7
+ import { AnchorState } from "./anchor.js";
7
8
  export function authoredPath(declaration) {
8
9
  const parts = [];
9
10
  for (const segment of declaration.effective) {
@@ -111,9 +112,13 @@ export function selections(sources) {
111
112
  }
112
113
  return result;
113
114
  }
114
- // Overlay only the final generated locations. Never evaluate application code.
115
- export async function validateProgram(sources, generated, stale) {
116
- const host = overlayHost(sources.parsed.options, sources.authored);
115
+ // Overlay only final generated locations. Never evaluate application code.
116
+ export function createValidationProgram(sources, generated, stale, compiler) {
117
+ const options = { ...sources.parsed.options };
118
+ if (sources.launch) {
119
+ options.skipLibCheck = false;
120
+ }
121
+ const host = overlayHost(options, sources.authored);
117
122
  const originalRead = host.readFile.bind(host);
118
123
  const originalExists = host.fileExists.bind(host);
119
124
  const originalDirectoryExists = host.directoryExists.bind(host);
@@ -141,14 +146,24 @@ export async function validateProgram(sources, generated, stale) {
141
146
  }
142
147
  return ts.createSourceFile(file, text, language, true);
143
148
  };
144
- const program = ts.createProgram({
145
- rootNames: [
146
- ...sources.parsed.fileNames.filter((file) => !obsolete.has(resolve(file))),
147
- sources.factoryEntry,
148
- ],
149
- options: sources.parsed.options,
150
- host,
151
- });
149
+ const rootNames = [
150
+ ...sources.parsed.fileNames.filter((file) => !obsolete.has(resolve(file))),
151
+ sources.factoryEntry,
152
+ ];
153
+ if (sources.launch) {
154
+ rootNames.push(sources.launch.typeEntry, ...generated.keys());
155
+ }
156
+ let program;
157
+ if (compiler) {
158
+ program = compiler.createProgram(rootNames, options, host);
159
+ }
160
+ else {
161
+ program = ts.createProgram({ rootNames, options, host });
162
+ }
163
+ return { program, host, obsolete };
164
+ }
165
+ export async function validateRoutes(sources, generated, validation, canonical) {
166
+ const { program, host, obsolete } = validation;
152
167
  if (sources.filesystem) {
153
168
  const diagnostics = [];
154
169
  for (const declaration of sources.declarations) {
@@ -176,20 +191,8 @@ export async function validateProgram(sources, generated, stale) {
176
191
  // Anchor the public export's declaration independently of application paths,
177
192
  // which can also redirect imports inside the concrete public type entry.
178
193
  const canonicalOptions = { ...sources.parsed.options, paths: undefined, baseUrl: undefined };
179
- const canonicalProgram = ts.createProgram({
180
- rootNames: [sources.factoryEntry],
181
- options: canonicalOptions,
182
- host: ts.createCompilerHost(canonicalOptions, true),
183
- });
184
- const canonicalChecker = canonicalProgram.getTypeChecker();
185
- const canonicalEntry = canonicalProgram.getSourceFile(sources.factoryEntry);
186
- const canonicalModule = canonicalChecker.getSymbolAtLocation(canonicalEntry);
187
- let canonicalFactory = canonicalChecker
188
- .getExportsOfModule(canonicalModule)
189
- .find((symbol) => symbol.name === "defineRoute");
190
- if (canonicalFactory.flags & ts.SymbolFlags.Alias) {
191
- canonicalFactory = canonicalChecker.getAliasedSymbol(canonicalFactory);
192
- }
194
+ const anchor = await (canonical ?? new AnchorState()).anchor(sources.factoryEntry, ["defineRoute"], canonicalOptions);
195
+ const canonicalFactory = anchor.get("defineRoute");
193
196
  // Tentative own-facade admission is not authority for its re-exported binding.
194
197
  // Prove both the concrete module boundary and the final native alias identity.
195
198
  for (const declaration of sources.declarations) {
@@ -215,17 +218,14 @@ export async function validateProgram(sources, generated, stale) {
215
218
  }
216
219
  if (!actualFactory?.declarations?.length ||
217
220
  actualFactory !== concreteFactory ||
218
- actualFactory.declarations.length !== canonicalFactory.declarations?.length) {
221
+ actualFactory.declarations.length !== canonicalFactory?.length) {
219
222
  fail(call, "Factory binding does not resolve to the concrete defineRoute export");
220
223
  }
221
224
  for (let index = 0; index < actualFactory.declarations.length; index += 1) {
222
225
  const actual = actualFactory.declarations[index];
223
- const canonical = canonicalFactory.declarations[index];
224
- const [actualFile, canonicalFile] = await Promise.all([
225
- realpath(actual.getSourceFile().fileName),
226
- realpath(canonical.getSourceFile().fileName),
227
- ]);
228
- if (actualFile !== canonicalFile ||
226
+ const canonical = canonicalFactory[index];
227
+ const actualFile = await realpath(actual.getSourceFile().fileName);
228
+ if (actualFile !== canonical.file ||
229
229
  actual.kind !== canonical.kind ||
230
230
  actual.pos !== canonical.pos ||
231
231
  actual.end !== canonical.end) {
@@ -237,6 +237,10 @@ export async function validateProgram(sources, generated, stale) {
237
237
  resolve(sources.output, "routes.generated.ts"),
238
238
  resolve(sources.output, "destinations.generated.ts"),
239
239
  ]);
240
+ if (sources.launch) {
241
+ globalTargets.add(resolve(sources.output, "context.generated.d.ts"));
242
+ globalTargets.add(resolve(sources.output, "server.generated.ts"));
243
+ }
240
244
  for (const source of program.getSourceFiles()) {
241
245
  if (source.fileName.includes("/node_modules/") ||
242
246
  program.isSourceFileFromExternalLibrary(source) ||
@@ -292,7 +296,8 @@ export async function validateProgram(sources, generated, stale) {
292
296
  candidates.add(resolve(file));
293
297
  }
294
298
  for (const candidate of candidates) {
295
- const facadeTarget = basename(candidate) === "route.generated.ts" &&
299
+ const facadeTarget = (basename(candidate) === "route.generated.ts" ||
300
+ (sources.launch && basename(candidate) === "rpc.generated.ts")) &&
296
301
  !candidate.split(sep).includes("node_modules") &&
297
302
  sources.roots.some((root) => contains(root, candidate));
298
303
  if ((facadeTarget || globalTargets.has(candidate)) && !generated.has(candidate)) {