ignotum 0.0.13 → 0.0.15
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 +6 -6
- package/dist/cli/bin.mjs +6111 -4448
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
- package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
- package/dist/cli/ffi-7IElU4Co.mjs +148 -0
- package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
- package/dist/cli/managed-release.d.mts +1 -0
- package/dist/cli/managed-release.mjs +14 -0
- package/dist/cli/managed-release.mjs.map +1 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
- package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
- package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
- package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
- package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
- package/dist/runtime/client.d.ts +50 -15
- package/dist/runtime/client.js +7 -1138
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
- package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
- package/dist/runtime/id--1m0NuUL.js.map +1 -0
- package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
- package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
- package/dist/runtime/image-Djtjy4Z8.js +19 -0
- package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
- package/dist/runtime/index-D4seNHlX.d.ts +184 -0
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +30 -1
- package/dist/runtime/internal/api.js.map +1 -0
- package/dist/runtime/internal/bootstrap.d.ts +2 -0
- package/dist/runtime/internal/bootstrap.js +2 -0
- package/dist/runtime/internal/client.d.ts +2 -0
- package/dist/runtime/internal/client.js +75 -0
- package/dist/runtime/internal/client.js.map +1 -0
- package/dist/runtime/internal/host.d.ts +25 -8
- package/dist/runtime/internal/host.js +27 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/routes.d.ts +2 -0
- package/dist/runtime/internal/routes.js +2 -0
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +2 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/pagination-D3qd6s9N.js +33 -0
- package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
- package/dist/runtime/result-BkziOG1L.d.ts +1 -0
- package/dist/runtime/router-BSBI1oN1.js +2377 -0
- package/dist/runtime/router-BSBI1oN1.js.map +1 -0
- package/dist/runtime/routes-Ce8TVP-T.js +690 -0
- package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
- package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
- package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
- package/dist/runtime/server.d.ts +5 -5
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
- package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
- package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
- package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
- package/package.json +29 -4
- package/src/cli/agent-files.ts +14 -8
- package/src/cli/build/managed-client.ts +411 -0
- package/src/cli/build/managed-server.ts +294 -0
- package/src/cli/build/managed.ts +67 -0
- package/src/cli/build/output.ts +47 -0
- package/src/cli/build/server.ts +51 -220
- package/src/cli/client-config.ts +1 -140
- package/src/cli/client-styles.ts +1 -0
- package/src/cli/codegen.ts +9 -4
- package/src/cli/command.ts +28 -5
- package/src/cli/control-client.ts +84 -101
- package/src/cli/deploy.ts +133 -177
- package/src/cli/dev.ts +506 -107
- package/src/cli/image-assets.ts +499 -0
- package/src/cli/managed-client-boundaries.ts +103 -0
- package/src/cli/managed-dev-platform.ts +118 -0
- package/src/cli/managed-exports.ts +219 -0
- package/src/cli/managed-release-bin.ts +13 -0
- package/src/cli/managed-release.ts +260 -0
- package/src/cli/module-boundaries.ts +11 -1
- package/src/cli/new-app.ts +111 -87
- package/src/cli/route-codegen.ts +311 -0
- package/src/cli/route-static.ts +710 -0
- package/src/client/bootstrap.ts +110 -0
- package/src/client/errors.ts +13 -7
- package/src/client/files.ts +64 -0
- package/src/client/id.ts +9 -5
- package/src/client/image.ts +28 -0
- package/src/client/index.ts +25 -2
- package/src/client/managed-client.ts +729 -0
- package/src/client/managed-upgrade.ts +283 -0
- package/src/client/recovery-journal.ts +195 -0
- package/src/client/route-boundaries.ts +184 -0
- package/src/client/router-history.ts +152 -0
- package/src/client/router-store.ts +644 -0
- package/src/client/router.ts +324 -0
- package/src/client/routes.ts +28 -0
- package/src/client/sync.ts +66 -592
- package/src/dev-runtime/database.ts +77 -2
- package/src/dev-runtime/functions.ts +3 -3
- package/src/dev-runtime/id.ts +19 -4
- package/src/dev-runtime/managed-functions.ts +255 -0
- package/src/dev-runtime/managed-sync.ts +263 -0
- package/src/dev-runtime/managed-websocket.ts +71 -0
- package/src/dev-runtime/migrations.ts +20 -0
- package/src/dev-runtime/sync.ts +236 -236
- package/src/image-assets.d.ts +36 -0
- package/src/internal/client.ts +186 -0
- package/src/internal/host.ts +1 -1
- package/src/internal/http-paths.ts +0 -3
- package/src/internal/routes.ts +45 -0
- package/dist/runtime/api-CAgKDij7.js.map +0 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
- package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
- package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/index-D54flWtH.d.ts +0 -402
- package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
- package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
- package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
- package/src/cli/build/client.ts +0 -119
- package/src/cli/build/public.ts +0 -186
- package/src/cli/client-entry.ts +0 -152
- package/src/cli/client-plugin.ts +0 -136
- package/src/client/app.ts +0 -15
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Predicate, Schema } from "effect";
|
|
2
|
+
import { parseSync, type Node, type Expression, type Program } from "oxc-parser";
|
|
3
|
+
import {
|
|
4
|
+
LayoutMetadata,
|
|
5
|
+
StaticMetadata,
|
|
6
|
+
UrlValueDescriptor,
|
|
7
|
+
} from "@ignotum/contracts/app-definition";
|
|
8
|
+
|
|
9
|
+
export class InvalidRouteSource extends Schema.TaggedError<InvalidRouteSource>()(
|
|
10
|
+
"InvalidRouteSource",
|
|
11
|
+
{
|
|
12
|
+
path: Schema.String,
|
|
13
|
+
code: Schema.optional(Schema.Literal("MissingExport")),
|
|
14
|
+
field: Schema.String,
|
|
15
|
+
line: Schema.Int,
|
|
16
|
+
column: Schema.Int,
|
|
17
|
+
message: Schema.String,
|
|
18
|
+
},
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
interface SourceModule {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly source: string;
|
|
24
|
+
readonly program: Program;
|
|
25
|
+
}
|
|
26
|
+
export type StaticValue =
|
|
27
|
+
| string
|
|
28
|
+
| number
|
|
29
|
+
| boolean
|
|
30
|
+
| null
|
|
31
|
+
| ReadonlyArray<StaticValue>
|
|
32
|
+
| { readonly [key: string]: StaticValue };
|
|
33
|
+
export interface SourceExpression {
|
|
34
|
+
readonly module: SourceModule;
|
|
35
|
+
readonly node: Expression;
|
|
36
|
+
}
|
|
37
|
+
export type SourceResolver = (
|
|
38
|
+
specifier: string,
|
|
39
|
+
importer: string,
|
|
40
|
+
) => Effect.Effect<string, InvalidRouteSource>;
|
|
41
|
+
const unwrap = (node: Expression): Expression => {
|
|
42
|
+
switch (node.type) {
|
|
43
|
+
case "TSAsExpression":
|
|
44
|
+
case "TSSatisfiesExpression":
|
|
45
|
+
case "TSNonNullExpression":
|
|
46
|
+
case "ParenthesizedExpression":
|
|
47
|
+
return unwrap(node.expression);
|
|
48
|
+
default:
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const propertyName = (node: Node): string | undefined =>
|
|
53
|
+
node.type === "Identifier"
|
|
54
|
+
? node.name
|
|
55
|
+
: node.type === "Literal" && Predicate.isString(node.value)
|
|
56
|
+
? node.value
|
|
57
|
+
: undefined;
|
|
58
|
+
const children = (node: Node): ReadonlyArray<Node> =>
|
|
59
|
+
Object.values(node).flatMap((value) => {
|
|
60
|
+
const values = Array.isArray(value) ? value : [value];
|
|
61
|
+
// OXC owns these AST objects. Positions and node tags distinguish children from scalar fields.
|
|
62
|
+
return values.filter(
|
|
63
|
+
(child): child is Node => Predicate.isObject(child) && "type" in child && "start" in child,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
const walk = (node: Node, visit: (node: Node) => void): void => {
|
|
67
|
+
visit(node);
|
|
68
|
+
for (const child of children(node)) walk(child, visit);
|
|
69
|
+
};
|
|
70
|
+
const identifiers = (node: Node): Set<string> => {
|
|
71
|
+
const names = new Set<string>();
|
|
72
|
+
const visit = (child: Node): void => {
|
|
73
|
+
if (child.type === "Identifier") {
|
|
74
|
+
names.add(child.name);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (child.type === "MemberExpression") {
|
|
78
|
+
visit(child.object);
|
|
79
|
+
if (child.computed) visit(child.property);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (child.type === "Property") {
|
|
83
|
+
if (child.computed) visit(child.key);
|
|
84
|
+
visit(child.value);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const nested of children(child)) visit(nested);
|
|
88
|
+
};
|
|
89
|
+
visit(node);
|
|
90
|
+
return names;
|
|
91
|
+
};
|
|
92
|
+
export const sourceError = (
|
|
93
|
+
module: Pick<SourceModule, "path" | "source">,
|
|
94
|
+
node: Pick<Node, "start">,
|
|
95
|
+
field: string,
|
|
96
|
+
detail: string,
|
|
97
|
+
code?: "MissingExport",
|
|
98
|
+
) => {
|
|
99
|
+
const prefix = module.source.slice(0, node.start);
|
|
100
|
+
const line = prefix.split("\n").length;
|
|
101
|
+
const column = node.start - prefix.lastIndexOf("\n");
|
|
102
|
+
return new InvalidRouteSource({
|
|
103
|
+
path: module.path,
|
|
104
|
+
code,
|
|
105
|
+
field,
|
|
106
|
+
line,
|
|
107
|
+
column,
|
|
108
|
+
message: `${module.path}:${line}:${column}: ${field}: ${detail}`,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function* (
|
|
113
|
+
appDirectory: string,
|
|
114
|
+
resolve: SourceResolver,
|
|
115
|
+
) {
|
|
116
|
+
const fs = yield* FileSystem.FileSystem;
|
|
117
|
+
const path = yield* Path.Path;
|
|
118
|
+
const modules = new Map<string, SourceModule>();
|
|
119
|
+
const dependencies = new Set<string>();
|
|
120
|
+
const load = Effect.fn("Routes.loadSource")(function* (
|
|
121
|
+
file: string,
|
|
122
|
+
): Effect.fn.Return<SourceModule, InvalidRouteSource> {
|
|
123
|
+
const fail = (detail: string) =>
|
|
124
|
+
sourceError({ path: file, source: "" }, { start: 0 }, "module", detail);
|
|
125
|
+
const real = yield* fs
|
|
126
|
+
.realPath(file)
|
|
127
|
+
.pipe(Effect.mapError(() => fail("Cannot read source module.")));
|
|
128
|
+
if (
|
|
129
|
+
!real.startsWith(`${appDirectory}/`) ||
|
|
130
|
+
real.startsWith(`${appDirectory}/server/`) ||
|
|
131
|
+
!/\.(ts|tsx)$/.test(real) ||
|
|
132
|
+
real.endsWith(".d.ts")
|
|
133
|
+
)
|
|
134
|
+
return yield* fail(
|
|
135
|
+
"Static configuration must come from TypeScript app modules outside server/.",
|
|
136
|
+
);
|
|
137
|
+
dependencies.add(real);
|
|
138
|
+
const cached = modules.get(real);
|
|
139
|
+
if (cached !== undefined) return cached;
|
|
140
|
+
const source = yield* fs
|
|
141
|
+
.readFileString(real)
|
|
142
|
+
.pipe(Effect.mapError(() => fail("Cannot read source module.")));
|
|
143
|
+
const parsed = parseSync(real, source, {
|
|
144
|
+
lang: "tsx",
|
|
145
|
+
sourceType: "module",
|
|
146
|
+
preserveParens: true,
|
|
147
|
+
showSemanticErrors: true,
|
|
148
|
+
});
|
|
149
|
+
const error = parsed.errors[0];
|
|
150
|
+
if (error !== undefined)
|
|
151
|
+
return yield* sourceError(
|
|
152
|
+
{ path: real, source },
|
|
153
|
+
{ start: error.labels[0]?.start ?? 0 },
|
|
154
|
+
"module",
|
|
155
|
+
error.message,
|
|
156
|
+
);
|
|
157
|
+
const module = { path: real, source, program: parsed.program };
|
|
158
|
+
modules.set(real, module);
|
|
159
|
+
return module;
|
|
160
|
+
});
|
|
161
|
+
const imported = Effect.fn("Routes.importedSource")(function* (
|
|
162
|
+
module: SourceModule,
|
|
163
|
+
specifier: string,
|
|
164
|
+
) {
|
|
165
|
+
return yield* load(yield* resolve(specifier, module.path));
|
|
166
|
+
});
|
|
167
|
+
const helper = Effect.fn("Routes.helper")(function* (
|
|
168
|
+
module: SourceModule,
|
|
169
|
+
local: string,
|
|
170
|
+
expected: string,
|
|
171
|
+
) {
|
|
172
|
+
for (const statement of module.program.body) {
|
|
173
|
+
if (statement.type !== "ImportDeclaration" || statement.importKind === "type") continue;
|
|
174
|
+
for (const specifier of statement.specifiers) {
|
|
175
|
+
if (
|
|
176
|
+
specifier.type !== "ImportSpecifier" ||
|
|
177
|
+
specifier.importKind === "type" ||
|
|
178
|
+
specifier.local.name !== local ||
|
|
179
|
+
propertyName(specifier.imported) !== expected
|
|
180
|
+
)
|
|
181
|
+
continue;
|
|
182
|
+
const target = yield* resolve(statement.source.value, module.path);
|
|
183
|
+
if (path.normalize(target) === path.join(appDirectory, "_generated/client.ts")) return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
});
|
|
188
|
+
// Track aliases as well as direct writes. A const object is not immutable if it escapes to a call.
|
|
189
|
+
const auditBinding = Effect.fn("Routes.auditBinding")(function* (
|
|
190
|
+
module: SourceModule,
|
|
191
|
+
name: string,
|
|
192
|
+
field: string,
|
|
193
|
+
object: boolean,
|
|
194
|
+
) {
|
|
195
|
+
const aliases = new Set([name]);
|
|
196
|
+
let changed = object;
|
|
197
|
+
while (changed) {
|
|
198
|
+
changed = false;
|
|
199
|
+
walk(module.program, (node) => {
|
|
200
|
+
if (node.type !== "VariableDeclarator" || node.init == null) return;
|
|
201
|
+
if (![...identifiers(node.init)].some((id) => aliases.has(id))) return;
|
|
202
|
+
for (const alias of identifiers(node.id))
|
|
203
|
+
if (!aliases.has(alias)) {
|
|
204
|
+
aliases.add(alias);
|
|
205
|
+
changed = true;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const operations: Node[] = [];
|
|
210
|
+
walk(module.program, (node) => {
|
|
211
|
+
if (
|
|
212
|
+
node.type === "AssignmentExpression" ||
|
|
213
|
+
node.type === "UpdateExpression" ||
|
|
214
|
+
(node.type === "UnaryExpression" && node.operator === "delete")
|
|
215
|
+
)
|
|
216
|
+
operations.push(node);
|
|
217
|
+
if (object && (node.type === "CallExpression" || node.type === "NewExpression"))
|
|
218
|
+
operations.push(node);
|
|
219
|
+
});
|
|
220
|
+
for (const node of operations) {
|
|
221
|
+
const target =
|
|
222
|
+
node.type === "AssignmentExpression"
|
|
223
|
+
? node.left
|
|
224
|
+
: node.type === "UpdateExpression" || node.type === "UnaryExpression"
|
|
225
|
+
? node.argument
|
|
226
|
+
: node;
|
|
227
|
+
if (![...identifiers(target)].some((id) => aliases.has(id))) continue;
|
|
228
|
+
if (
|
|
229
|
+
node.type === "CallExpression" &&
|
|
230
|
+
node.callee.type === "Identifier" &&
|
|
231
|
+
((yield* helper(module, node.callee.name, "route")) ||
|
|
232
|
+
(yield* helper(module, node.callee.name, "layout")))
|
|
233
|
+
)
|
|
234
|
+
continue;
|
|
235
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression") {
|
|
236
|
+
let base: Expression = node.callee.object;
|
|
237
|
+
while (base.type === "CallExpression" && base.callee.type === "MemberExpression")
|
|
238
|
+
base = base.callee.object;
|
|
239
|
+
if (base.type === "Identifier" && (yield* helper(module, base.name, "values"))) continue;
|
|
240
|
+
}
|
|
241
|
+
return yield* sourceError(
|
|
242
|
+
module,
|
|
243
|
+
node,
|
|
244
|
+
field,
|
|
245
|
+
`Constant '${name}' is mutated or escapes to unsupported code.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const binding = Effect.fn("Routes.binding")(function* (
|
|
250
|
+
module: SourceModule,
|
|
251
|
+
name: string,
|
|
252
|
+
field: string,
|
|
253
|
+
trail: ReadonlySet<string>,
|
|
254
|
+
): Effect.fn.Return<SourceExpression, InvalidRouteSource> {
|
|
255
|
+
const key = `${module.path}:${name}`;
|
|
256
|
+
if (trail.has(key) || trail.size > 96)
|
|
257
|
+
return yield* sourceError(
|
|
258
|
+
module,
|
|
259
|
+
module.program,
|
|
260
|
+
field,
|
|
261
|
+
`Cyclic or excessively deep constant '${name}'.`,
|
|
262
|
+
);
|
|
263
|
+
const next = new Set([...trail, key]);
|
|
264
|
+
for (const statement of module.program.body) {
|
|
265
|
+
const declaration =
|
|
266
|
+
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
267
|
+
if (declaration?.type === "VariableDeclaration") {
|
|
268
|
+
for (const item of declaration.declarations) {
|
|
269
|
+
if (item.id.type !== "Identifier" || item.id.name !== name) continue;
|
|
270
|
+
if (declaration.kind !== "const" || item.init == null)
|
|
271
|
+
return yield* sourceError(
|
|
272
|
+
module,
|
|
273
|
+
item,
|
|
274
|
+
field,
|
|
275
|
+
`Binding '${name}' must be an initialized const.`,
|
|
276
|
+
);
|
|
277
|
+
const expression = yield* dereference({ module, node: item.init }, field, next);
|
|
278
|
+
yield* auditBinding(
|
|
279
|
+
module,
|
|
280
|
+
name,
|
|
281
|
+
field,
|
|
282
|
+
!["Literal", "TemplateLiteral", "UnaryExpression"].includes(expression.node.type),
|
|
283
|
+
);
|
|
284
|
+
return expression;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (statement.type === "ImportDeclaration" && statement.importKind !== "type") {
|
|
288
|
+
for (const specifier of statement.specifiers) {
|
|
289
|
+
if (specifier.local.name !== name) continue;
|
|
290
|
+
const target = yield* imported(module, statement.source.value);
|
|
291
|
+
const exportedName =
|
|
292
|
+
specifier.type === "ImportSpecifier" && specifier.importKind !== "type"
|
|
293
|
+
? propertyName(specifier.imported)
|
|
294
|
+
: specifier.type === "ImportDefaultSpecifier"
|
|
295
|
+
? "default"
|
|
296
|
+
: undefined;
|
|
297
|
+
if (exportedName === undefined) continue;
|
|
298
|
+
const expression = yield* exported(target, exportedName, field, next);
|
|
299
|
+
yield* auditBinding(
|
|
300
|
+
module,
|
|
301
|
+
name,
|
|
302
|
+
field,
|
|
303
|
+
!["Literal", "TemplateLiteral", "UnaryExpression"].includes(expression.node.type),
|
|
304
|
+
);
|
|
305
|
+
return expression;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return yield* sourceError(
|
|
310
|
+
module,
|
|
311
|
+
module.program,
|
|
312
|
+
field,
|
|
313
|
+
`No immutable constant '${name}' found.`,
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
const exported = Effect.fn("Routes.exportedConstant")(function* (
|
|
317
|
+
module: SourceModule,
|
|
318
|
+
name: string,
|
|
319
|
+
field: string,
|
|
320
|
+
trail: ReadonlySet<string>,
|
|
321
|
+
): Effect.fn.Return<SourceExpression, InvalidRouteSource> {
|
|
322
|
+
const key = `${module.path}:export:${name}`;
|
|
323
|
+
if (trail.has(key) || trail.size > 96)
|
|
324
|
+
return yield* sourceError(
|
|
325
|
+
module,
|
|
326
|
+
module.program,
|
|
327
|
+
field,
|
|
328
|
+
`Cyclic or excessively deep re-export '${name}'.`,
|
|
329
|
+
);
|
|
330
|
+
const next = new Set([...trail, key]);
|
|
331
|
+
for (const statement of module.program.body) {
|
|
332
|
+
if (statement.type === "ExportDefaultDeclaration" && name === "default") {
|
|
333
|
+
if (statement.declaration.type === "Identifier")
|
|
334
|
+
return yield* binding(module, statement.declaration.name, field, next);
|
|
335
|
+
const declaration = statement.declaration;
|
|
336
|
+
if (
|
|
337
|
+
declaration.type === "FunctionDeclaration" ||
|
|
338
|
+
declaration.type === "ClassDeclaration" ||
|
|
339
|
+
declaration.type === "TSInterfaceDeclaration"
|
|
340
|
+
)
|
|
341
|
+
return yield* sourceError(
|
|
342
|
+
module,
|
|
343
|
+
declaration,
|
|
344
|
+
field,
|
|
345
|
+
"Default metadata exports must be static values.",
|
|
346
|
+
);
|
|
347
|
+
return yield* dereference({ module, node: declaration }, field, next);
|
|
348
|
+
}
|
|
349
|
+
if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
|
|
350
|
+
if (
|
|
351
|
+
statement.declaration?.type === "VariableDeclaration" &&
|
|
352
|
+
statement.declaration.declarations.some(
|
|
353
|
+
(item) => item.id.type === "Identifier" && item.id.name === name,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
return yield* binding(module, name, field, next);
|
|
357
|
+
for (const specifier of statement.specifiers) {
|
|
358
|
+
if (specifier.exportKind === "type" || propertyName(specifier.exported) !== name) continue;
|
|
359
|
+
const local = propertyName(specifier.local)!;
|
|
360
|
+
return statement.source == null
|
|
361
|
+
? yield* binding(module, local, field, next)
|
|
362
|
+
: yield* exported(yield* imported(module, statement.source.value), local, field, next);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const stars = module.program.body.filter(
|
|
366
|
+
(s) => s.type === "ExportAllDeclaration" && s.exportKind !== "type" && s.exported == null,
|
|
367
|
+
);
|
|
368
|
+
const matches: SourceExpression[] = [];
|
|
369
|
+
for (const star of stars) {
|
|
370
|
+
if (star.type !== "ExportAllDeclaration" || name === "default") continue;
|
|
371
|
+
const candidate = yield* exported(
|
|
372
|
+
yield* imported(module, star.source.value),
|
|
373
|
+
name,
|
|
374
|
+
field,
|
|
375
|
+
next,
|
|
376
|
+
).pipe(Effect.result);
|
|
377
|
+
if (candidate._tag === "Success") {
|
|
378
|
+
if (
|
|
379
|
+
!matches.some(
|
|
380
|
+
(match) =>
|
|
381
|
+
match.module.path === candidate.success.module.path &&
|
|
382
|
+
match.node.start === candidate.success.node.start,
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
matches.push(candidate.success);
|
|
386
|
+
} else if (candidate.failure.code !== "MissingExport") return yield* candidate.failure;
|
|
387
|
+
}
|
|
388
|
+
if (matches.length === 1) return matches[0]!;
|
|
389
|
+
return yield* sourceError(
|
|
390
|
+
module,
|
|
391
|
+
module.program,
|
|
392
|
+
field,
|
|
393
|
+
`${matches.length > 1 ? "Ambiguous export" : "No export"} '${name}'.`,
|
|
394
|
+
matches.length > 1 ? undefined : "MissingExport",
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
const dereference = Effect.fn("Routes.dereference")(function* (
|
|
398
|
+
input: SourceExpression,
|
|
399
|
+
field: string,
|
|
400
|
+
trail: ReadonlySet<string> = new Set(),
|
|
401
|
+
): Effect.fn.Return<SourceExpression, InvalidRouteSource> {
|
|
402
|
+
const node = unwrap(input.node);
|
|
403
|
+
if (
|
|
404
|
+
node.type === "MemberExpression" &&
|
|
405
|
+
!node.computed &&
|
|
406
|
+
!node.optional &&
|
|
407
|
+
node.object.type === "Identifier" &&
|
|
408
|
+
node.property.type === "Identifier"
|
|
409
|
+
) {
|
|
410
|
+
const namespace = node.object.name;
|
|
411
|
+
for (const statement of input.module.program.body) {
|
|
412
|
+
if (
|
|
413
|
+
statement.type !== "ImportDeclaration" ||
|
|
414
|
+
statement.importKind === "type" ||
|
|
415
|
+
!statement.specifiers.some(
|
|
416
|
+
(specifier) =>
|
|
417
|
+
specifier.type === "ImportNamespaceSpecifier" && specifier.local.name === namespace,
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
continue;
|
|
421
|
+
yield* auditBinding(input.module, namespace, field, true);
|
|
422
|
+
return yield* exported(
|
|
423
|
+
yield* imported(input.module, statement.source.value),
|
|
424
|
+
node.property.name,
|
|
425
|
+
field,
|
|
426
|
+
trail,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return node.type === "Identifier"
|
|
431
|
+
? yield* binding(input.module, node.name, field, trail)
|
|
432
|
+
: { module: input.module, node };
|
|
433
|
+
});
|
|
434
|
+
const properties = Effect.fn("Routes.properties")(function* (
|
|
435
|
+
input: SourceExpression,
|
|
436
|
+
field: string,
|
|
437
|
+
trail: ReadonlySet<string> = new Set(),
|
|
438
|
+
): Effect.fn.Return<Map<string, SourceExpression>, InvalidRouteSource> {
|
|
439
|
+
const expression = yield* dereference(input, field, trail);
|
|
440
|
+
const { module, node } = expression;
|
|
441
|
+
const key = `${module.path}:${node.start}:${field}`;
|
|
442
|
+
if (trail.has(key) || trail.size > 96)
|
|
443
|
+
return yield* sourceError(module, node, field, "Cyclic or excessively deep configuration.");
|
|
444
|
+
const next = new Set([...trail, key]);
|
|
445
|
+
if (node.type !== "ObjectExpression")
|
|
446
|
+
return yield* sourceError(module, node, field, "Expected a statically extractable object.");
|
|
447
|
+
const output = new Map<string, SourceExpression>();
|
|
448
|
+
for (const property of node.properties) {
|
|
449
|
+
if (property.type === "SpreadElement") {
|
|
450
|
+
for (const [name, value] of yield* properties(
|
|
451
|
+
{ module, node: property.argument },
|
|
452
|
+
field,
|
|
453
|
+
next,
|
|
454
|
+
))
|
|
455
|
+
output.set(name, value);
|
|
456
|
+
} else {
|
|
457
|
+
const name = propertyName(property.key);
|
|
458
|
+
if (property.computed || property.method || property.kind !== "init" || name === undefined)
|
|
459
|
+
return yield* sourceError(
|
|
460
|
+
module,
|
|
461
|
+
property,
|
|
462
|
+
field,
|
|
463
|
+
"Computed keys, methods, and getters are unsupported.",
|
|
464
|
+
);
|
|
465
|
+
output.set(name, { module, node: property.value });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return output;
|
|
469
|
+
});
|
|
470
|
+
const value = Effect.fn("Routes.staticValue")(function* (
|
|
471
|
+
input: SourceExpression,
|
|
472
|
+
field: string,
|
|
473
|
+
trail: ReadonlySet<string> = new Set(),
|
|
474
|
+
): Effect.fn.Return<StaticValue, InvalidRouteSource> {
|
|
475
|
+
const { module, node } = yield* dereference(input, field, trail);
|
|
476
|
+
const key = `${module.path}:${node.start}`;
|
|
477
|
+
if (trail.has(key) || trail.size > 96)
|
|
478
|
+
return yield* sourceError(module, node, field, "Cyclic or excessively deep configuration.");
|
|
479
|
+
const next = new Set([...trail, key]);
|
|
480
|
+
if (
|
|
481
|
+
node.type === "Literal" &&
|
|
482
|
+
(node.value === null ||
|
|
483
|
+
Predicate.isString(node.value) ||
|
|
484
|
+
Predicate.isNumber(node.value) ||
|
|
485
|
+
Predicate.isBoolean(node.value))
|
|
486
|
+
)
|
|
487
|
+
return node.value;
|
|
488
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
489
|
+
return node.quasis[0]?.value.cooked ?? "";
|
|
490
|
+
if (
|
|
491
|
+
node.type === "UnaryExpression" &&
|
|
492
|
+
node.operator === "-" &&
|
|
493
|
+
node.argument.type === "Literal" &&
|
|
494
|
+
Predicate.isNumber(node.argument.value)
|
|
495
|
+
)
|
|
496
|
+
return -node.argument.value;
|
|
497
|
+
if (node.type === "ArrayExpression") {
|
|
498
|
+
const output: StaticValue[] = [];
|
|
499
|
+
for (const element of node.elements) {
|
|
500
|
+
if (element == null || element.type === "SpreadElement")
|
|
501
|
+
return yield* sourceError(
|
|
502
|
+
module,
|
|
503
|
+
node,
|
|
504
|
+
field,
|
|
505
|
+
"Array holes and spreads are unsupported.",
|
|
506
|
+
);
|
|
507
|
+
output.push(yield* value({ module, node: element }, field, next));
|
|
508
|
+
}
|
|
509
|
+
return output;
|
|
510
|
+
}
|
|
511
|
+
if (node.type === "ObjectExpression") {
|
|
512
|
+
const output: [string, StaticValue][] = [];
|
|
513
|
+
for (const [name, expression] of yield* properties({ module, node }, field))
|
|
514
|
+
output.push([name, yield* value(expression, `${field}.${name}`, next)]);
|
|
515
|
+
return Object.fromEntries(output);
|
|
516
|
+
}
|
|
517
|
+
return yield* sourceError(
|
|
518
|
+
module,
|
|
519
|
+
node,
|
|
520
|
+
field,
|
|
521
|
+
`Unsupported ${node.type}; configuration is never executed.`,
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
const descriptor = Effect.fn("Routes.urlDescriptor")(function* (
|
|
525
|
+
input: SourceExpression,
|
|
526
|
+
field: string,
|
|
527
|
+
depth = 0,
|
|
528
|
+
): Effect.fn.Return<UrlValueDescriptor, InvalidRouteSource> {
|
|
529
|
+
const { module, node } = yield* dereference(input, field);
|
|
530
|
+
if (depth > 32) return yield* sourceError(module, node, field, "URL definition is too deep.");
|
|
531
|
+
if (
|
|
532
|
+
node.type !== "CallExpression" ||
|
|
533
|
+
node.optional ||
|
|
534
|
+
node.typeArguments != null ||
|
|
535
|
+
node.callee.type !== "MemberExpression" ||
|
|
536
|
+
node.callee.computed ||
|
|
537
|
+
node.callee.optional ||
|
|
538
|
+
node.callee.property.type !== "Identifier"
|
|
539
|
+
)
|
|
540
|
+
return yield* sourceError(module, node, field, "Expected a supported values validator.");
|
|
541
|
+
const method = node.callee.property.name;
|
|
542
|
+
const args: SourceExpression[] = [];
|
|
543
|
+
for (const argument of node.arguments) {
|
|
544
|
+
if (argument.type === "SpreadElement")
|
|
545
|
+
return yield* sourceError(
|
|
546
|
+
module,
|
|
547
|
+
argument,
|
|
548
|
+
field,
|
|
549
|
+
"Validator argument spreads are unsupported.",
|
|
550
|
+
);
|
|
551
|
+
args.push({ module, node: argument });
|
|
552
|
+
}
|
|
553
|
+
let output: StaticValue;
|
|
554
|
+
if (method === "default") {
|
|
555
|
+
if (args.length !== 1)
|
|
556
|
+
return yield* sourceError(module, node, field, "default requires one value.");
|
|
557
|
+
output = {
|
|
558
|
+
type: "default",
|
|
559
|
+
value: yield* descriptor({ module, node: node.callee.object }, field, depth + 1),
|
|
560
|
+
default: yield* value(args[0]!, field),
|
|
561
|
+
};
|
|
562
|
+
} else {
|
|
563
|
+
if (
|
|
564
|
+
node.callee.object.type !== "Identifier" ||
|
|
565
|
+
!(yield* helper(module, node.callee.object.name, "values"))
|
|
566
|
+
)
|
|
567
|
+
return yield* sourceError(
|
|
568
|
+
module,
|
|
569
|
+
node,
|
|
570
|
+
field,
|
|
571
|
+
"Use values imported from _generated/client.",
|
|
572
|
+
);
|
|
573
|
+
if (["string", "number", "integer", "boolean"].includes(method) && args.length === 0)
|
|
574
|
+
output = { type: method };
|
|
575
|
+
else if (method === "id" && args.length === 1)
|
|
576
|
+
output = { type: method, table: yield* value(args[0]!, field) };
|
|
577
|
+
else if (method === "literal" && args.length === 1)
|
|
578
|
+
output = { type: method, value: yield* value(args[0]!, field) };
|
|
579
|
+
else if (method === "literals" && args.length > 0)
|
|
580
|
+
output = { type: method, values: yield* Effect.forEach(args, (arg) => value(arg, field)) };
|
|
581
|
+
else if ((method === "array" || method === "optional") && args.length === 1)
|
|
582
|
+
output = { type: method, value: yield* descriptor(args[0]!, field, depth + 1) };
|
|
583
|
+
else
|
|
584
|
+
return yield* sourceError(
|
|
585
|
+
module,
|
|
586
|
+
node,
|
|
587
|
+
field,
|
|
588
|
+
`Unsupported URL validator '${method}' or argument count.`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return yield* Schema.decodeUnknownEffect(UrlValueDescriptor)(output).pipe(
|
|
592
|
+
Effect.mapError((error) =>
|
|
593
|
+
sourceError(module, node, field, `Invalid ${method} URL descriptor: ${String(error)}`),
|
|
594
|
+
),
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
const extract = Effect.fn("Routes.extractModule")(function* (
|
|
598
|
+
file: string,
|
|
599
|
+
kind: "route" | "layout",
|
|
600
|
+
) {
|
|
601
|
+
const module = yield* load(file);
|
|
602
|
+
const defaults = module.program.body.filter((node) => node.type === "ExportDefaultDeclaration");
|
|
603
|
+
const call = defaults[0]?.declaration;
|
|
604
|
+
if (
|
|
605
|
+
defaults.length !== 1 ||
|
|
606
|
+
call?.type !== "CallExpression" ||
|
|
607
|
+
call.optional ||
|
|
608
|
+
call.typeArguments != null ||
|
|
609
|
+
call.callee.type !== "Identifier" ||
|
|
610
|
+
!(yield* helper(module, call.callee.name, kind)) ||
|
|
611
|
+
call.arguments.length !== 1 ||
|
|
612
|
+
call.arguments[0] === undefined ||
|
|
613
|
+
call.arguments[0].type === "SpreadElement" ||
|
|
614
|
+
unwrap(call.arguments[0]).type !== "ObjectExpression"
|
|
615
|
+
)
|
|
616
|
+
return yield* sourceError(
|
|
617
|
+
module,
|
|
618
|
+
call ?? module.program,
|
|
619
|
+
"default",
|
|
620
|
+
`Expected one direct default ${kind}({...}) call imported from _generated/client.`,
|
|
621
|
+
);
|
|
622
|
+
const registrations: Node[] = [];
|
|
623
|
+
walk(module.program, (node) => {
|
|
624
|
+
if (node.type === "CallExpression" && node !== call) registrations.push(node);
|
|
625
|
+
});
|
|
626
|
+
for (const registration of registrations) {
|
|
627
|
+
if (
|
|
628
|
+
registration.type === "CallExpression" &&
|
|
629
|
+
registration.callee.type === "Identifier" &&
|
|
630
|
+
((yield* helper(module, registration.callee.name, "route")) ||
|
|
631
|
+
(yield* helper(module, registration.callee.name, "layout")))
|
|
632
|
+
)
|
|
633
|
+
return yield* sourceError(
|
|
634
|
+
module,
|
|
635
|
+
registration,
|
|
636
|
+
"default",
|
|
637
|
+
"A source module cannot register additional routes or layouts.",
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const fields = yield* properties({ module, node: call.arguments[0] }, kind);
|
|
641
|
+
const allowed =
|
|
642
|
+
kind === "route"
|
|
643
|
+
? ["params", "searchParams", "title", "meta", "component", "error", "invalid"]
|
|
644
|
+
: ["title", "titleTemplate", "meta", "component", "error", "invalid", "notFound"];
|
|
645
|
+
for (const [name, expression] of fields)
|
|
646
|
+
if (!allowed.includes(name))
|
|
647
|
+
return yield* sourceError(
|
|
648
|
+
expression.module,
|
|
649
|
+
expression.node,
|
|
650
|
+
name,
|
|
651
|
+
`Unsupported ${kind} field.`,
|
|
652
|
+
);
|
|
653
|
+
if (!fields.has("component"))
|
|
654
|
+
return yield* sourceError(module, call, "component", "A component is required.");
|
|
655
|
+
const metadata: [string, StaticValue][] = [];
|
|
656
|
+
for (const name of ["title", "titleTemplate", "meta"]) {
|
|
657
|
+
const expression = fields.get(name);
|
|
658
|
+
if (expression === undefined) continue;
|
|
659
|
+
const extracted = yield* value(expression, name);
|
|
660
|
+
const validate =
|
|
661
|
+
name === "meta"
|
|
662
|
+
? Schema.decodeUnknownEffect(StaticMetadata.fields.meta)(extracted).pipe(Effect.asVoid)
|
|
663
|
+
: name === "titleTemplate"
|
|
664
|
+
? Schema.decodeUnknownEffect(LayoutMetadata.fields.titleTemplate)(extracted).pipe(
|
|
665
|
+
Effect.asVoid,
|
|
666
|
+
)
|
|
667
|
+
: Schema.decodeUnknownEffect(StaticMetadata.fields.title)(extracted).pipe(
|
|
668
|
+
Effect.asVoid,
|
|
669
|
+
);
|
|
670
|
+
yield* validate.pipe(
|
|
671
|
+
Effect.mapError((error) =>
|
|
672
|
+
sourceError(expression.module, expression.node, name, String(error)),
|
|
673
|
+
),
|
|
674
|
+
);
|
|
675
|
+
metadata.push([name, extracted]);
|
|
676
|
+
}
|
|
677
|
+
const decodedMetadata = yield* Schema.decodeUnknownEffect(
|
|
678
|
+
kind === "route" ? StaticMetadata : LayoutMetadata,
|
|
679
|
+
)(Object.fromEntries(metadata)).pipe(
|
|
680
|
+
Effect.mapError((error) => sourceError(module, call, "metadata", String(error))),
|
|
681
|
+
);
|
|
682
|
+
const urls = new Map<
|
|
683
|
+
string,
|
|
684
|
+
ReadonlyArray<{ readonly name: string; readonly value: UrlValueDescriptor }>
|
|
685
|
+
>();
|
|
686
|
+
for (const name of ["params", "searchParams"]) {
|
|
687
|
+
const expression = fields.get(name);
|
|
688
|
+
if (expression === undefined) continue;
|
|
689
|
+
const entries: { name: string; value: UrlValueDescriptor }[] = [];
|
|
690
|
+
for (const [key, validator] of yield* properties(expression, name))
|
|
691
|
+
entries.push({ name: key, value: yield* descriptor(validator, `${name}.${key}`) });
|
|
692
|
+
urls.set(
|
|
693
|
+
name,
|
|
694
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
module,
|
|
699
|
+
metadata: decodedMetadata,
|
|
700
|
+
params: urls.get("params"),
|
|
701
|
+
searchParams: urls.get("searchParams") ?? [],
|
|
702
|
+
boundaries: {
|
|
703
|
+
error: fields.has("error"),
|
|
704
|
+
invalid: fields.has("invalid"),
|
|
705
|
+
notFound: fields.has("notFound"),
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
return { extract, dependencies };
|
|
710
|
+
});
|