@loozo/angular-router 0.0.0-stage → 0.0.1
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/.npmignore +3 -0
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/builders.json +15 -0
- package/fesm2022/loozo-angular-router-_core.mjs +565 -0
- package/fesm2022/loozo-angular-router-_core.mjs.map +1 -0
- package/fesm2022/loozo-angular-router-_generated.mjs +658 -0
- package/fesm2022/loozo-angular-router-_generated.mjs.map +1 -0
- package/fesm2022/loozo-angular-router.mjs +6 -0
- package/fesm2022/loozo-angular-router.mjs.map +1 -0
- package/node/_core/src/path.js +50 -0
- package/node/src/builders/application.js +40 -0
- package/node/src/builders/options.js +70 -0
- package/node/src/builders/serve.js +155 -0
- package/node/src/generation/emit.js +245 -0
- package/node/src/generation/files.js +168 -0
- package/node/src/generation/generate.js +19 -0
- package/node/src/generation/selectors.js +60 -0
- package/node/src/generation/sources.js +493 -0
- package/node/src/generation/validate.js +463 -0
- package/package.json +47 -3
- package/schemas/application.json +17 -0
- package/schemas/serve.json +16 -0
- package/types/loozo-angular-router-_core.d.ts +185 -0
- package/types/loozo-angular-router-_generated.d.ts +56 -0
- package/types/loozo-angular-router.d.ts +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { markers } from "./files.js";
|
|
4
|
+
import { resolveModule } from "./sources.js";
|
|
5
|
+
import { authoredPath } from "./validate.js";
|
|
6
|
+
import { relativeSelections } from "./selectors.js";
|
|
7
|
+
export function specifier(from, file) {
|
|
8
|
+
let name = relative(dirname(from), file).split(sep).join("/").replace(/\.ts$/, ".js");
|
|
9
|
+
if (!name.startsWith(".")) {
|
|
10
|
+
name = `./${name}`;
|
|
11
|
+
}
|
|
12
|
+
return name;
|
|
13
|
+
}
|
|
14
|
+
function lazySpecifier(from, file, options) {
|
|
15
|
+
const emitted = specifier(from, file);
|
|
16
|
+
const extensionless = emitted.slice(0, -3);
|
|
17
|
+
const ordinary = resolveModule(extensionless, from, {
|
|
18
|
+
...options,
|
|
19
|
+
module: ts.ModuleKind.ESNext,
|
|
20
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
21
|
+
});
|
|
22
|
+
if (ordinary === file) {
|
|
23
|
+
return extensionless;
|
|
24
|
+
}
|
|
25
|
+
const fallback = resolveModule(emitted, from, {
|
|
26
|
+
...options,
|
|
27
|
+
module: ts.ModuleKind.ESNext,
|
|
28
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
29
|
+
});
|
|
30
|
+
if (fallback !== file) {
|
|
31
|
+
throw new Error(`Cannot emit an identity-preserving lazy page import: ${file}`);
|
|
32
|
+
}
|
|
33
|
+
return emitted;
|
|
34
|
+
}
|
|
35
|
+
export function emit(sources, selected) {
|
|
36
|
+
const desired = new Map();
|
|
37
|
+
const declarations = sources.declarations.filter((declaration) => declaration.retained || declaration.target);
|
|
38
|
+
if (declarations.length === 0) {
|
|
39
|
+
return desired;
|
|
40
|
+
}
|
|
41
|
+
const routesFile = resolve(sources.output, "routes.generated.ts");
|
|
42
|
+
const sharedFile = resolve(sources.output, "destinations.generated.ts");
|
|
43
|
+
const names = new Map();
|
|
44
|
+
for (let index = 0; index < declarations.length; index += 1) {
|
|
45
|
+
names.set(declarations[index], `definition${index}`);
|
|
46
|
+
}
|
|
47
|
+
const routes = [
|
|
48
|
+
markers["routes.generated.ts"],
|
|
49
|
+
"import type { Routes } from '@angular/router';",
|
|
50
|
+
];
|
|
51
|
+
let needsNative = false;
|
|
52
|
+
let needsCallbacks = false;
|
|
53
|
+
let needsRedirect = false;
|
|
54
|
+
for (const declaration of declarations) {
|
|
55
|
+
if (["providers", "title", "data", "canDeactivate", "runGuardsAndResolvers"].some((key) => declaration.config.has(key))) {
|
|
56
|
+
needsNative = true;
|
|
57
|
+
}
|
|
58
|
+
if (["canActivate", "canActivateChild", "canMatch", "resolve"].some((key) => declaration.config.has(key))) {
|
|
59
|
+
needsCallbacks = true;
|
|
60
|
+
}
|
|
61
|
+
if (declaration.target) {
|
|
62
|
+
needsRedirect = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const helpers = [];
|
|
66
|
+
if (needsNative) {
|
|
67
|
+
helpers.push("nativeOptions");
|
|
68
|
+
}
|
|
69
|
+
if (needsCallbacks) {
|
|
70
|
+
helpers.push("callbacks");
|
|
71
|
+
routes.push(`import { getRouteSet } from './destinations.generated.js';`);
|
|
72
|
+
}
|
|
73
|
+
if (needsRedirect) {
|
|
74
|
+
helpers.push("redirect");
|
|
75
|
+
}
|
|
76
|
+
if (helpers.length > 0) {
|
|
77
|
+
routes.push(`import { ${helpers.join(", ")} } from '@loozo/angular-router/_generated';`);
|
|
78
|
+
}
|
|
79
|
+
for (const declaration of declarations) {
|
|
80
|
+
if (declaration.config.size > 0 &&
|
|
81
|
+
[...declaration.config.keys()].some((key) => !["parent", "params", "search", "fragments"].includes(key))) {
|
|
82
|
+
routes.push(`import ${names.get(declaration)} from ${JSON.stringify(specifier(routesFile, declaration.file))};`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function nativePath(declaration, effective) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
let segments = declaration.segments;
|
|
88
|
+
if (effective) {
|
|
89
|
+
segments = declaration.effective;
|
|
90
|
+
}
|
|
91
|
+
for (const segment of segments) {
|
|
92
|
+
if (segment.kind === "static") {
|
|
93
|
+
parts.push(segment.text);
|
|
94
|
+
}
|
|
95
|
+
else if (segment.kind === "catchall") {
|
|
96
|
+
parts.push("**");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
parts.push(`:${segment.name}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return parts.join("/");
|
|
103
|
+
}
|
|
104
|
+
function record(declaration) {
|
|
105
|
+
const parts = [];
|
|
106
|
+
const name = names.get(declaration);
|
|
107
|
+
const native = ["providers", "title", "data", "canDeactivate", "runGuardsAndResolvers"].some((key) => declaration.config.has(key));
|
|
108
|
+
if (native) {
|
|
109
|
+
parts.push(`...nativeOptions(${name})`);
|
|
110
|
+
}
|
|
111
|
+
parts.push(`path: ${JSON.stringify(nativePath(declaration, !!declaration.target))}`);
|
|
112
|
+
if (declaration.full || declaration.target) {
|
|
113
|
+
parts.push("pathMatch: 'full'");
|
|
114
|
+
}
|
|
115
|
+
let data = "";
|
|
116
|
+
if (declaration.config.has("data")) {
|
|
117
|
+
data = `...nativeOptions(${name}).data as object, `;
|
|
118
|
+
}
|
|
119
|
+
parts.push(`data: { ${data}__loozoAngularRouterRouteId: ${JSON.stringify(declaration.id)} }`);
|
|
120
|
+
if (declaration.target) {
|
|
121
|
+
parts.push(`redirectTo: redirect(() => ${name})`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
if (["canActivate", "canActivateChild", "canMatch", "resolve"].some((key) => declaration.config.has(key))) {
|
|
125
|
+
parts.push(`...callbacks(getRouteSet(), ${JSON.stringify(declaration.id)})`);
|
|
126
|
+
}
|
|
127
|
+
if (declaration.page) {
|
|
128
|
+
parts.push(`loadComponent: () => import(${JSON.stringify(lazySpecifier(routesFile, declaration.page, sources.parsed.options))})`);
|
|
129
|
+
}
|
|
130
|
+
const children = declaration.children.filter((child) => child.retained);
|
|
131
|
+
children.sort(order);
|
|
132
|
+
if (children.length > 0) {
|
|
133
|
+
parts.push(`children: [${children.map(record).join(",\n")}]`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return `{ ${parts.join(",\n")} }`;
|
|
137
|
+
}
|
|
138
|
+
function order(a, b) {
|
|
139
|
+
const catchA = a.segments.at(-1)?.kind === "catchall";
|
|
140
|
+
const catchB = b.segments.at(-1)?.kind === "catchall";
|
|
141
|
+
if (catchA !== catchB) {
|
|
142
|
+
if (catchA) {
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
return authoredPath(a).localeCompare(authoredPath(b), "en") || a.id.localeCompare(b.id, "en");
|
|
148
|
+
}
|
|
149
|
+
const before = [];
|
|
150
|
+
const ordinary = [];
|
|
151
|
+
const after = [];
|
|
152
|
+
for (const declaration of declarations) {
|
|
153
|
+
if (declaration.target) {
|
|
154
|
+
if (declaration.effective.at(-1)?.kind === "catchall") {
|
|
155
|
+
after.push(declaration);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
before.push(declaration);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (!declaration.parent) {
|
|
162
|
+
ordinary.push(declaration);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const group of [before, ordinary, after]) {
|
|
166
|
+
group.sort(order);
|
|
167
|
+
}
|
|
168
|
+
routes.push(`\nexport const routes: Routes = [${[...before, ...ordinary, ...after].map(record).join(",\n")}];\n`);
|
|
169
|
+
desired.set(routesFile, routes.join("\n"));
|
|
170
|
+
const retained = declarations.filter((declaration) => declaration.retained);
|
|
171
|
+
if (retained.length === 0) {
|
|
172
|
+
return desired;
|
|
173
|
+
}
|
|
174
|
+
const shared = [
|
|
175
|
+
markers["destinations.generated.ts"],
|
|
176
|
+
"import type { RouteSet, Handle as GeneratedHandle, Destination as GeneratedDestination } from '@loozo/angular-router/_generated';",
|
|
177
|
+
];
|
|
178
|
+
for (const declaration of declarations) {
|
|
179
|
+
shared.push(`import ${names.get(declaration)} from ${JSON.stringify(specifier(sharedFile, declaration.file))};`);
|
|
180
|
+
}
|
|
181
|
+
shared.push("\nexport type Definitions = {");
|
|
182
|
+
for (const declaration of declarations) {
|
|
183
|
+
shared.push(`${JSON.stringify(declaration.id)}: typeof ${names.get(declaration)};`);
|
|
184
|
+
}
|
|
185
|
+
shared.push("};\ndeclare const setIdentity: unique symbol;");
|
|
186
|
+
shared.push("export type AbsoluteTargets = {");
|
|
187
|
+
for (const [path, declaration] of selected) {
|
|
188
|
+
shared.push(`${JSON.stringify(path)}: ${JSON.stringify(declaration.id)};`);
|
|
189
|
+
}
|
|
190
|
+
const relativeTargets = relativeSelections(sources, selected);
|
|
191
|
+
shared.push("};\nexport type RelativeTargets = {");
|
|
192
|
+
for (const [id, expressions] of relativeTargets) {
|
|
193
|
+
shared.push(`${JSON.stringify(id)}: {`);
|
|
194
|
+
for (const [expression, target] of expressions) {
|
|
195
|
+
shared.push(`${JSON.stringify(expression)}: ${JSON.stringify(target)};`);
|
|
196
|
+
}
|
|
197
|
+
shared.push("};");
|
|
198
|
+
}
|
|
199
|
+
shared.push(`};
|
|
200
|
+
export type Selected<Id extends keyof RelativeTargets> = {
|
|
201
|
+
readonly [Expression in keyof AbsoluteTargets | keyof RelativeTargets[Id]]:
|
|
202
|
+
Definitions[(Expression extends keyof AbsoluteTargets ? AbsoluteTargets[Expression] :
|
|
203
|
+
Expression extends keyof RelativeTargets[Id] ? RelativeTargets[Id][Expression] : never) & keyof Definitions];
|
|
204
|
+
};
|
|
205
|
+
export type Handle<Id extends keyof RelativeTargets> = GeneratedHandle<Definitions[Id], Selected<Id>, typeof setIdentity, Pick<Definitions, keyof RelativeTargets>>;
|
|
206
|
+
export type Destination<Id extends keyof RelativeTargets> = GeneratedDestination<Definitions[Id], typeof setIdentity, Id>;
|
|
207
|
+
const routeSet: RouteSet = { records: {`);
|
|
208
|
+
for (const declaration of retained) {
|
|
209
|
+
const owners = [];
|
|
210
|
+
let current = declaration;
|
|
211
|
+
while (current) {
|
|
212
|
+
owners.unshift(current.id);
|
|
213
|
+
current = current.parent;
|
|
214
|
+
}
|
|
215
|
+
shared.push(`${JSON.stringify(declaration.id)}: { definition: () => ${names.get(declaration)}, owners: ${JSON.stringify(owners)} },`);
|
|
216
|
+
}
|
|
217
|
+
shared.push("}, destinations: {");
|
|
218
|
+
for (const declaration of declarations) {
|
|
219
|
+
shared.push(`${JSON.stringify(declaration.id)}: () => ${names.get(declaration)},`);
|
|
220
|
+
}
|
|
221
|
+
const absoluteSelectors = Object.create(null);
|
|
222
|
+
const relativeSelectors = Object.create(null);
|
|
223
|
+
for (const [path, declaration] of selected) {
|
|
224
|
+
absoluteSelectors[path] = declaration.id;
|
|
225
|
+
}
|
|
226
|
+
for (const [id, expressions] of relativeTargets) {
|
|
227
|
+
relativeSelectors[id] = Object.fromEntries(expressions);
|
|
228
|
+
}
|
|
229
|
+
shared.push(`}, absolute: ${JSON.stringify(absoluteSelectors)}, relative: ${JSON.stringify(relativeSelectors)} };\nexport function getRouteSet(): RouteSet { return routeSet; }\n`);
|
|
230
|
+
desired.set(sharedFile, shared.join("\n"));
|
|
231
|
+
for (const declaration of retained) {
|
|
232
|
+
const facade = resolve(dirname(declaration.file), "route.generated.ts");
|
|
233
|
+
const body = [
|
|
234
|
+
markers["route.generated.ts"],
|
|
235
|
+
"import { createDestination, injectGeneratedRoute } from '@loozo/angular-router/_generated';",
|
|
236
|
+
`import { getRouteSet, type Destination, type Handle } from ${JSON.stringify(specifier(facade, sharedFile))};`,
|
|
237
|
+
"export { defineRoute } from '@loozo/angular-router';",
|
|
238
|
+
`export const destination: Destination<${JSON.stringify(declaration.id)}> = createDestination(() => getRouteSet(), ${JSON.stringify(declaration.id)});`,
|
|
239
|
+
`export function injectRoute(): Handle<${JSON.stringify(declaration.id)}> { return injectGeneratedRoute(getRouteSet(), ${JSON.stringify(declaration.id)}); }`,
|
|
240
|
+
"",
|
|
241
|
+
];
|
|
242
|
+
desired.set(facade, body.join("\n"));
|
|
243
|
+
}
|
|
244
|
+
return desired;
|
|
245
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
export const markers = {
|
|
5
|
+
"routes.generated.ts": "// @loozo/angular-router routegen: owned routes.generated.ts",
|
|
6
|
+
"destinations.generated.ts": "// @loozo/angular-router routegen: owned destinations.generated.ts",
|
|
7
|
+
"route.generated.ts": "// @loozo/angular-router routegen: owned route.generated.ts",
|
|
8
|
+
};
|
|
9
|
+
export function contains(directory, file) {
|
|
10
|
+
const suffix = relative(directory, file);
|
|
11
|
+
return (suffix === "" || (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
|
|
12
|
+
}
|
|
13
|
+
export async function metadata(path) {
|
|
14
|
+
try {
|
|
15
|
+
return await lstat(path);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === "ENOENT") {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Resolve only aliases for the workspace access prefix, never a source/output suffix.
|
|
25
|
+
export async function workspacePaths(workspace) {
|
|
26
|
+
const canonical = await realpath(workspace);
|
|
27
|
+
return {
|
|
28
|
+
canonical,
|
|
29
|
+
async resolve(path) {
|
|
30
|
+
let absolute = resolve(workspace, path);
|
|
31
|
+
if (contains(workspace, absolute)) {
|
|
32
|
+
absolute = resolve(canonical, relative(workspace, absolute));
|
|
33
|
+
}
|
|
34
|
+
const ancestors = [];
|
|
35
|
+
let current = dirname(absolute);
|
|
36
|
+
while (dirname(current) !== current) {
|
|
37
|
+
ancestors.unshift(current);
|
|
38
|
+
current = dirname(current);
|
|
39
|
+
}
|
|
40
|
+
for (const ancestor of ancestors) {
|
|
41
|
+
const info = await metadata(ancestor);
|
|
42
|
+
if (info?.isSymbolicLink()) {
|
|
43
|
+
const target = await realpath(ancestor);
|
|
44
|
+
if (target === canonical) {
|
|
45
|
+
return resolve(canonical, relative(ancestor, absolute));
|
|
46
|
+
}
|
|
47
|
+
// A source/output link cannot become legitimate by resolving its target.
|
|
48
|
+
return absolute;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return absolute;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function auditPath(path) {
|
|
56
|
+
const parent = dirname(path);
|
|
57
|
+
if (parent !== path) {
|
|
58
|
+
await auditPath(parent);
|
|
59
|
+
}
|
|
60
|
+
const info = await metadata(path);
|
|
61
|
+
if (info?.isSymbolicLink()) {
|
|
62
|
+
throw new Error(`Unsafe source/output symbolic link: ${path}`);
|
|
63
|
+
}
|
|
64
|
+
if (info && parent !== path && !info.isDirectory() && !info.isFile()) {
|
|
65
|
+
throw new Error(`Unsafe filesystem entry: ${path}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function walk(directory) {
|
|
69
|
+
await auditPath(directory);
|
|
70
|
+
const info = await metadata(directory);
|
|
71
|
+
if (!info) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
if (!info.isDirectory()) {
|
|
75
|
+
throw new Error(`Scan/output directory is not a directory: ${directory}`);
|
|
76
|
+
}
|
|
77
|
+
const result = [];
|
|
78
|
+
async function visit(path) {
|
|
79
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
80
|
+
entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const child = resolve(path, entry.name);
|
|
83
|
+
if (entry.isSymbolicLink()) {
|
|
84
|
+
throw new Error(`Unsafe source/output symbolic link: ${child}`);
|
|
85
|
+
}
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
// Dependency package-manager links are not application sources.
|
|
88
|
+
if (entry.name !== "node_modules") {
|
|
89
|
+
await visit(child);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (entry.isFile()) {
|
|
93
|
+
result.push(child);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
throw new Error(`Unsafe filesystem entry: ${child}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await visit(directory);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
export async function planOutputs(desired, encountered, globals) {
|
|
104
|
+
const changed = [];
|
|
105
|
+
const missing = [];
|
|
106
|
+
const stale = [];
|
|
107
|
+
const possible = new Set([...desired.keys(), ...globals]);
|
|
108
|
+
for (const path of encountered) {
|
|
109
|
+
if (path.endsWith(`${sep}route.generated.ts`)) {
|
|
110
|
+
possible.add(path);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const path of [...possible].sort()) {
|
|
114
|
+
await auditPath(path);
|
|
115
|
+
const info = await metadata(path);
|
|
116
|
+
const expected = desired.get(path);
|
|
117
|
+
if (!info) {
|
|
118
|
+
if (expected !== undefined) {
|
|
119
|
+
missing.push(path);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!info.isFile()) {
|
|
124
|
+
throw new Error(`Generated output is not a regular file: ${path}`);
|
|
125
|
+
}
|
|
126
|
+
const bytes = await readFile(path, "utf8");
|
|
127
|
+
const name = path.slice(path.lastIndexOf(sep) + 1);
|
|
128
|
+
const owned = bytes.split("\n", 1)[0] === markers[name];
|
|
129
|
+
if (!owned) {
|
|
130
|
+
if (expected !== undefined) {
|
|
131
|
+
throw new Error(`Unmarked generated-output collision: ${path}`);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (expected === undefined) {
|
|
136
|
+
stale.push(path);
|
|
137
|
+
}
|
|
138
|
+
else if (bytes !== expected) {
|
|
139
|
+
changed.push(path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { changed, missing, stale, desired };
|
|
143
|
+
}
|
|
144
|
+
export function publish(plan, check, stopped) {
|
|
145
|
+
if (stopped()) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (check) {
|
|
149
|
+
for (const [kind, paths] of [
|
|
150
|
+
["changed", plan.changed],
|
|
151
|
+
["missing", plan.missing],
|
|
152
|
+
["stale", plan.stale],
|
|
153
|
+
]) {
|
|
154
|
+
if (paths.length > 0) {
|
|
155
|
+
throw new Error(`Generated artifacts ${kind}:\n${paths.join("\n")}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// No awaits from the final stop check through this short publication section.
|
|
161
|
+
for (const path of [...plan.changed, ...plan.missing]) {
|
|
162
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
163
|
+
writeFileSync(path, plan.desired.get(path), "utf8");
|
|
164
|
+
}
|
|
165
|
+
for (const path of plan.stale) {
|
|
166
|
+
unlinkSync(path);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { discover } from "./sources.js";
|
|
3
|
+
import { selections, validateProgram } from "./validate.js";
|
|
4
|
+
import { emit } from "./emit.js";
|
|
5
|
+
import { 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);
|
|
8
|
+
const selected = selections(sources);
|
|
9
|
+
const desired = emit(sources, selected);
|
|
10
|
+
const globalFiles = [
|
|
11
|
+
resolve(sources.output, "routes.generated.ts"),
|
|
12
|
+
resolve(sources.output, "destinations.generated.ts"),
|
|
13
|
+
];
|
|
14
|
+
await walk(sources.output);
|
|
15
|
+
const plan = await planOutputs(desired, sources.encountered, globalFiles);
|
|
16
|
+
// Unrelated output contents are audited but never become cleanup candidates.
|
|
17
|
+
await validateProgram(sources, desired, plan.stale);
|
|
18
|
+
publish(plan, options.check === true, stopped);
|
|
19
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { authoredPath } from "./validate.js";
|
|
2
|
+
// Compile URL-template segment relationships, never authored directory ancestry.
|
|
3
|
+
export function relativeSelections(sources, selected) {
|
|
4
|
+
const result = new Map();
|
|
5
|
+
for (const origin of sources.declarations) {
|
|
6
|
+
if (!origin.retained) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
const expressions = new Map();
|
|
10
|
+
result.set(origin.id, expressions);
|
|
11
|
+
if (origin.effective.some((segment) => segment.kind === "catchall")) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
let originParts = [];
|
|
15
|
+
if (origin.effective.length > 0) {
|
|
16
|
+
originParts = authoredPath(origin).slice(1).split("/");
|
|
17
|
+
}
|
|
18
|
+
for (let climb = 0; climb <= originParts.length; climb += 1) {
|
|
19
|
+
const prefix = originParts.slice(0, originParts.length - climb);
|
|
20
|
+
const steps = [];
|
|
21
|
+
for (let index = 0; index < climb; index += 1) {
|
|
22
|
+
steps.push("..");
|
|
23
|
+
}
|
|
24
|
+
for (const [path, target] of selected) {
|
|
25
|
+
let targetParts = [];
|
|
26
|
+
if (target.effective.length > 0) {
|
|
27
|
+
targetParts = path.slice(1).split("/");
|
|
28
|
+
}
|
|
29
|
+
if (prefix.length > targetParts.length) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
let matches = true;
|
|
33
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
34
|
+
if (prefix[index] !== targetParts[index]) {
|
|
35
|
+
matches = false;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!matches) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const suffix = targetParts.slice(prefix.length);
|
|
43
|
+
if (suffix.some((part) => part.includes("?") || part.includes("#"))) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let expression = "./";
|
|
47
|
+
if (climb > 0) {
|
|
48
|
+
expression = steps.join("/");
|
|
49
|
+
if (suffix.length === 0) {
|
|
50
|
+
expressions.set(expression, target.id);
|
|
51
|
+
}
|
|
52
|
+
expression += "/";
|
|
53
|
+
}
|
|
54
|
+
expression += suffix.join("/");
|
|
55
|
+
expressions.set(expression, target.id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|