@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.
- package/README.md +442 -79
- package/builders.json +2 -2
- package/fesm2022/loozo-angular-router-_core.mjs +348 -589
- package/fesm2022/loozo-angular-router-_core.mjs.map +1 -1
- package/fesm2022/loozo-angular-router-_generated.mjs +492 -617
- 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 +9 -6
- package/node/src/builders/options.js +38 -58
- package/node/src/builders/serve.js +51 -8
- 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/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/generate.d.ts +16 -0
- package/node/src/generation/generate.js +5 -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 +50 -10
- package/node/src/generation/validate.d.ts +12 -0
- package/node/src/generation/validate.js +38 -33
- package/package.json +7 -2
- package/schemas/application.json +743 -8
- package/schemas/serve.json +11 -32
- package/types/loozo-angular-router-_core.d.ts +159 -181
- package/types/loozo-angular-router-_generated.d.ts +131 -109
- package/types/loozo-angular-router.d.ts +2 -1
- package/.npmignore +0 -3
- 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,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,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] ===
|
|
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,
|
|
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
|
-
|
|
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,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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
115
|
-
export
|
|
116
|
-
const
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
180
|
-
|
|
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
|
|
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
|
|
224
|
-
const
|
|
225
|
-
|
|
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)) {
|