@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.
- package/README.md +565 -80
- package/builders.json +2 -2
- package/fesm2022/loozo-angular-router-_core.mjs +332 -595
- package/fesm2022/loozo-angular-router-_core.mjs.map +1 -1
- package/fesm2022/loozo-angular-router-_generated.mjs +335 -627
- 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 +19 -7
- package/node/src/builders/options.js +47 -61
- package/node/src/builders/serve.js +66 -16
- 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/emit.js +26 -8
- 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/filesystem.js +261 -69
- package/node/src/generation/generate.d.ts +16 -0
- package/node/src/generation/generate.js +10 -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 +57 -12
- package/node/src/generation/validate.d.ts +12 -0
- package/node/src/generation/validate.js +53 -57
- package/package.json +7 -2
- package/schemas/application.json +764 -20
- package/schemas/serve.json +144 -30
- package/snippets/angular.json +29 -0
- package/snippets/generated/destinations.generated.ts +126 -0
- package/snippets/generated/routes.generated.ts +35 -0
- package/snippets/src/catalog.ts +20 -0
- package/snippets/src/docs-url.ts +8 -0
- package/snippets/src/index.html +11 -0
- package/snippets/src/main.ts +6 -0
- package/snippets/src/product-url.ts +8 -0
- package/snippets/src/routes/products/$productId/page.ts +20 -0
- package/snippets/src/routes/products/$productId/route.generated.ts +24 -0
- package/snippets/src/routes/products/$productId/route.ts +7 -0
- package/snippets/src/routes/reports/$reportId/page.ts +9 -0
- package/snippets/src/routes/reports/$reportId/route.generated.ts +24 -0
- package/snippets/src/routes/reports/$reportId/route.ts +6 -0
- package/snippets/src/routes/teams/$teamId/items/$id/inputs.ts +15 -0
- package/snippets/src/routes/teams/$teamId/items/$id/links.ts +13 -0
- package/snippets/src/routes/teams/$teamId/items/$id/page.ts +9 -0
- package/snippets/src/routes/teams/$teamId/items/$id/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/items/$id/route.ts +18 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/page.ts +9 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/route.ts +7 -0
- package/snippets/src/routes/teams/$teamId/old-items/$id/route.ts +13 -0
- package/snippets/src/routes/teams/$teamId/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/route.ts +6 -0
- package/snippets/src/shell.ts +9 -0
- package/snippets/src/slug.ts +14 -0
- package/snippets/tsconfig.app.json +14 -0
- package/snippets/tsconfig.routes.json +5 -0
- package/types/loozo-angular-router-_core.d.ts +262 -242
- package/types/loozo-angular-router-_generated.d.ts +155 -116
- 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,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,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
|
+
}
|
|
@@ -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 (
|
|
75
|
+
if (usesNative(declaration)) {
|
|
56
76
|
needsNative = true;
|
|
57
77
|
}
|
|
58
|
-
if (
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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] ===
|
|
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>>;
|