@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
@@ -0,0 +1,13 @@
1
+ import ts from "typescript";
2
+ export interface AnchoredDeclaration {
3
+ file: string;
4
+ kind: ts.SyntaxKind;
5
+ pos: number;
6
+ end: number;
7
+ }
8
+ export type Anchor = ReadonlyMap<string, readonly AnchoredDeclaration[] | undefined>;
9
+ export declare class AnchorState {
10
+ #private;
11
+ clear(): void;
12
+ anchor(entry: string, names: readonly string[], options: ts.CompilerOptions): Promise<Anchor>;
13
+ }
@@ -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>;
@@ -51,11 +51,31 @@ export function emit(sources, selected) {
51
51
  let needsNative = false;
52
52
  let needsCallbacks = false;
53
53
  let needsRedirect = false;
54
+ // Options Angular takes as they are, read from the definition itself.
55
+ function usesNative(declaration) {
56
+ return ["providers", "data", "canDeactivate", "runGuardsAndResolvers"].some((key) => declaration.config.has(key));
57
+ }
58
+ // Parsed-input callbacks, adapted through the route set; a redirect record never activates.
59
+ // Local path params and effective search always get the generated canMatch that parses them.
60
+ function usesCallbacks(declaration) {
61
+ if (declaration.target) {
62
+ return false;
63
+ }
64
+ if (declaration.segments.some((segment) => segment.kind !== "static")) {
65
+ return true;
66
+ }
67
+ for (let owner = declaration; owner; owner = owner.parent) {
68
+ if (owner.config.has("search")) {
69
+ return true;
70
+ }
71
+ }
72
+ return ["canActivate", "canActivateChild", "canMatch", "resolve", "title"].some((key) => declaration.config.has(key));
73
+ }
54
74
  for (const declaration of declarations) {
55
- if (["providers", "title", "data", "canDeactivate", "runGuardsAndResolvers"].some((key) => declaration.config.has(key))) {
75
+ if (usesNative(declaration)) {
56
76
  needsNative = true;
57
77
  }
58
- if (["canActivate", "canActivateChild", "canMatch", "resolve"].some((key) => declaration.config.has(key))) {
78
+ if (usesCallbacks(declaration)) {
59
79
  needsCallbacks = true;
60
80
  }
61
81
  if (declaration.target) {
@@ -77,8 +97,7 @@ export function emit(sources, selected) {
77
97
  routes.push(`import { ${helpers.join(", ")} } from '@loozo/angular-router/_generated';`);
78
98
  }
79
99
  for (const declaration of declarations) {
80
- if (declaration.config.size > 0 &&
81
- [...declaration.config.keys()].some((key) => !["parent", "params", "search", "fragments"].includes(key))) {
100
+ if (usesNative(declaration) || declaration.target) {
82
101
  routes.push(`import ${names.get(declaration)} from ${JSON.stringify(specifier(routesFile, declaration.file))};`);
83
102
  }
84
103
  }
@@ -104,8 +123,7 @@ export function emit(sources, selected) {
104
123
  function record(declaration) {
105
124
  const parts = [];
106
125
  const name = names.get(declaration);
107
- const native = ["providers", "title", "data", "canDeactivate", "runGuardsAndResolvers"].some((key) => declaration.config.has(key));
108
- if (native) {
126
+ if (usesNative(declaration)) {
109
127
  parts.push(`...nativeOptions(${name})`);
110
128
  }
111
129
  parts.push(`path: ${JSON.stringify(nativePath(declaration, !!declaration.target))}`);
@@ -121,7 +139,7 @@ export function emit(sources, selected) {
121
139
  parts.push(`redirectTo: redirect(() => ${name})`);
122
140
  }
123
141
  else {
124
- if (["canActivate", "canActivateChild", "canMatch", "resolve"].some((key) => declaration.config.has(key))) {
142
+ if (usesCallbacks(declaration)) {
125
143
  parts.push(`...callbacks(getRouteSet(), ${JSON.stringify(declaration.id)})`);
126
144
  }
127
145
  if (declaration.page) {
@@ -253,7 +271,7 @@ const routeSet: RouteSet = { records: {`);
253
271
  *
254
272
  * @remarks
255
273
  * Call in an Angular injection context. The handle exposes readonly native
256
- * params/search/data Resources and destination-typed link/navigate methods.
274
+ * params/search/data Signals and destination-typed link/navigate methods.
257
275
  * Data includes local resolver results only, not static or inherited data.
258
276
  * All navigation values are explicit; current inputs are never reused implicitly.
259
277
  *
@@ -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>>;