@loozo/angular-router 0.0.1 → 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 (40) hide show
  1. package/README.md +455 -63
  2. package/builders.json +2 -2
  3. package/fesm2022/loozo-angular-router-_core.mjs +348 -521
  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 -6
  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 +58 -52
  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/emit.js +26 -1
  18. package/node/src/generation/files.d.ts +26 -0
  19. package/node/src/generation/files.js +42 -10
  20. package/node/src/generation/filesystem.d.ts +7 -0
  21. package/node/src/generation/filesystem.js +343 -0
  22. package/node/src/generation/generate.d.ts +16 -0
  23. package/node/src/generation/generate.js +18 -6
  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/overlay.js +84 -0
  28. package/node/src/generation/selectors.d.ts +2 -0
  29. package/node/src/generation/sources.d.ts +50 -0
  30. package/node/src/generation/sources.js +109 -28
  31. package/node/src/generation/validate.d.ts +12 -0
  32. package/node/src/generation/validate.js +53 -33
  33. package/package.json +7 -2
  34. package/schemas/application.json +765 -7
  35. package/schemas/serve.json +11 -9
  36. package/types/loozo-angular-router-_core.d.ts +401 -124
  37. package/types/loozo-angular-router-_generated.d.ts +181 -44
  38. package/types/loozo-angular-router.d.ts +2 -1
  39. package/.npmignore +0 -3
  40. 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>;
@@ -165,7 +165,12 @@ export function emit(sources, selected) {
165
165
  for (const group of [before, ordinary, after]) {
166
166
  group.sort(order);
167
167
  }
168
- routes.push(`\nexport const routes: Routes = [${[...before, ...ordinary, ...after].map(record).join(",\n")}];\n`);
168
+ routes.push(`
169
+ /**
170
+ * Generated native Angular route configuration; install with provideRouter(routes).
171
+ * Regenerate through the router's Architect companions instead of editing this file.
172
+ */
173
+ export const routes: Routes = [${[...before, ...ordinary, ...after].map(record).join(",\n")}];\n`);
169
174
  desired.set(routesFile, routes.join("\n"));
170
175
  const retained = declarations.filter((declaration) => declaration.retained);
171
176
  if (retained.length === 0) {
@@ -202,7 +207,9 @@ export type Selected<Id extends keyof RelativeTargets> = {
202
207
  Definitions[(Expression extends keyof AbsoluteTargets ? AbsoluteTargets[Expression] :
203
208
  Expression extends keyof RelativeTargets[Id] ? RelativeTargets[Id][Expression] : never) & keyof Definitions];
204
209
  };
210
+ /** A declaration-bound handle; obtain it through its colocated injectRoute facade. */
205
211
  export type Handle<Id extends keyof RelativeTargets> = GeneratedHandle<Definitions[Id], Selected<Id>, typeof setIdentity, Pick<Definitions, keyof RelativeTargets>>;
212
+ /** A same-set nominal reference; use the destination exported by a colocated facade. */
206
213
  export type Destination<Id extends keyof RelativeTargets> = GeneratedDestination<Definitions[Id], typeof setIdentity, Id>;
207
214
  const routeSet: RouteSet = { records: {`);
208
215
  for (const declaration of retained) {
@@ -235,7 +242,25 @@ const routeSet: RouteSet = { records: {`);
235
242
  "import { createDestination, injectGeneratedRoute } from '@loozo/angular-router/_generated';",
236
243
  `import { getRouteSet, type Destination, type Handle } from ${JSON.stringify(specifier(facade, sharedFile))};`,
237
244
  "export { defineRoute } from '@loozo/angular-router';",
245
+ `/**
246
+ * Inert reference to this exact declaration for same-set handle link/navigate calls.
247
+ * No injection context is required; copies and foreign-set references are invalid.
248
+ * This is not an authored route and cannot be passed to the root createUrl function.
249
+ */`,
238
250
  `export const destination: Destination<${JSON.stringify(declaration.id)}> = createDestination(() => getRouteSet(), ${JSON.stringify(declaration.id)});`,
251
+ `/**
252
+ * Injects a typed handle bound to this exact generated activated route record.
253
+ *
254
+ * @remarks
255
+ * Call in an Angular injection context. The handle exposes readonly native
256
+ * params/search/data Resources and destination-typed link/navigate methods.
257
+ * Data includes local resolver results only, not static or inherited data.
258
+ * All navigation values are explicit; current inputs are never reused implicitly.
259
+ *
260
+ * @returns A noncallable handle for this declaration.
261
+ * @throws A synchronous wiring error outside an injection context or when the
262
+ * activated record does not carry this declaration's own generated identity.
263
+ */`,
239
264
  `export function injectRoute(): Handle<${JSON.stringify(declaration.id)}> { return injectGeneratedRoute(getRouteSet(), ${JSON.stringify(declaration.id)}); }`,
240
265
  "",
241
266
  ];
@@ -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;
@@ -1,11 +1,16 @@
1
1
  import { lstat, readdir, readFile, realpath } from "node:fs/promises";
2
- import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
2
+ import { lstatSync, realpathSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
3
3
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  export const markers = {
5
5
  "routes.generated.ts": "// @loozo/angular-router routegen: owned routes.generated.ts",
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}`)));
@@ -26,21 +31,31 @@ export async function workspacePaths(workspace) {
26
31
  const canonical = await realpath(workspace);
27
32
  return {
28
33
  canonical,
29
- async resolve(path) {
30
- let absolute = resolve(workspace, path);
34
+ // Config matching needs synchronous directory-entry lookup. The optional
35
+ // directory leaf uses the same restricted prefix policy as file ancestors.
36
+ resolve(path, directory = false) {
37
+ const absolute = resolve(workspace, path);
38
+ // Inside the workspace every link is a suffix, even a link back to its root.
39
+ // Preserve that spelling for audit; only external access aliases may map.
40
+ if (contains(canonical, absolute)) {
41
+ return absolute;
42
+ }
31
43
  if (contains(workspace, absolute)) {
32
- absolute = resolve(canonical, relative(workspace, absolute));
44
+ return resolve(canonical, relative(workspace, absolute));
33
45
  }
34
46
  const ancestors = [];
35
47
  let current = dirname(absolute);
48
+ if (directory) {
49
+ current = absolute;
50
+ }
36
51
  while (dirname(current) !== current) {
37
52
  ancestors.unshift(current);
38
53
  current = dirname(current);
39
54
  }
40
55
  for (const ancestor of ancestors) {
41
- const info = await metadata(ancestor);
56
+ const info = lstatSync(ancestor, { throwIfNoEntry: false });
42
57
  if (info?.isSymbolicLink()) {
43
- const target = await realpath(ancestor);
58
+ const target = realpathSync(ancestor);
44
59
  if (target === canonical) {
45
60
  return resolve(canonical, relative(ancestor, absolute));
46
61
  }
@@ -100,13 +115,15 @@ export async function walk(directory) {
100
115
  await visit(directory);
101
116
  return result;
102
117
  }
103
- export async function planOutputs(desired, encountered, globals) {
118
+ export async function planOutputs(desired, encountered, globals, mode) {
104
119
  const changed = [];
105
120
  const missing = [];
106
121
  const stale = [];
107
122
  const possible = new Set([...desired.keys(), ...globals]);
108
123
  for (const path of encountered) {
109
- 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}`)))) {
110
127
  possible.add(path);
111
128
  }
112
129
  }
@@ -124,8 +141,12 @@ export async function planOutputs(desired, encountered, globals) {
124
141
  throw new Error(`Generated output is not a regular file: ${path}`);
125
142
  }
126
143
  const bytes = await readFile(path, "utf8");
144
+ const ownership = { ...markers };
145
+ if (mode === "router-and-launch") {
146
+ Object.assign(ownership, launchMarkers);
147
+ }
127
148
  const name = path.slice(path.lastIndexOf(sep) + 1);
128
- const owned = bytes.split("\n", 1)[0] === markers[name];
149
+ const owned = bytes.split("\n", 1)[0] === ownership[name];
129
150
  if (!owned) {
130
151
  if (expected !== undefined) {
131
152
  throw new Error(`Unmarked generated-output collision: ${path}`);
@@ -141,11 +162,14 @@ export async function planOutputs(desired, encountered, globals) {
141
162
  }
142
163
  return { changed, missing, stale, desired };
143
164
  }
144
- export function publish(plan, check, stopped) {
165
+ export function publish(plan, check, stopped, authored, seeds) {
145
166
  if (stopped()) {
146
167
  return;
147
168
  }
148
169
  if (check) {
170
+ if (authored.size > 0) {
171
+ throw new Error(`Filesystem definitions missing/changed:\n${[...authored.keys()].join("\n")}`);
172
+ }
149
173
  for (const [kind, paths] of [
150
174
  ["changed", plan.changed],
151
175
  ["missing", plan.missing],
@@ -158,6 +182,14 @@ export function publish(plan, check, stopped) {
158
182
  return;
159
183
  }
160
184
  // No awaits from the final stop check through this short publication section.
185
+ for (const [file, bytes] of authored) {
186
+ if (seeds.has(file)) {
187
+ writeFileSync(file, bytes, { encoding: "utf8", flag: "wx" });
188
+ }
189
+ else {
190
+ writeFileSync(file, bytes, "utf8");
191
+ }
192
+ }
161
193
  for (const path of [...plan.changed, ...plan.missing]) {
162
194
  mkdirSync(dirname(path), { recursive: true });
163
195
  writeFileSync(path, plan.desired.get(path), "utf8");
@@ -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>>;