@canmi/seam-server 0.5.31 → 0.5.36
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 +13 -10
- package/dist/index.d.ts +103 -86
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +692 -531
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -3,23 +3,17 @@ import { extname, join } from "node:path";
|
|
|
3
3
|
import { validate } from "jtd";
|
|
4
4
|
import { escapeHtml, renderPage } from "@canmi/seam-engine";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
-
|
|
7
6
|
//#region \0rolldown/runtime.js
|
|
8
7
|
var __defProp = Object.defineProperty;
|
|
9
8
|
var __exportAll = (all, no_symbols) => {
|
|
10
9
|
let target = {};
|
|
11
|
-
for (var name in all) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
if (!no_symbols) {
|
|
18
|
-
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
19
|
-
}
|
|
10
|
+
for (var name in all) __defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true
|
|
13
|
+
});
|
|
14
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
20
15
|
return target;
|
|
21
16
|
};
|
|
22
|
-
|
|
23
17
|
//#endregion
|
|
24
18
|
//#region src/types/schema.ts
|
|
25
19
|
function createSchemaNode(schema) {
|
|
@@ -31,7 +25,6 @@ function createOptionalSchemaNode(schema) {
|
|
|
31
25
|
_optional: true
|
|
32
26
|
};
|
|
33
27
|
}
|
|
34
|
-
|
|
35
28
|
//#endregion
|
|
36
29
|
//#region src/types/primitives.ts
|
|
37
30
|
var primitives_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -87,7 +80,6 @@ function html() {
|
|
|
87
80
|
metadata: { format: "html" }
|
|
88
81
|
});
|
|
89
82
|
}
|
|
90
|
-
|
|
91
83
|
//#endregion
|
|
92
84
|
//#region src/types/composites.ts
|
|
93
85
|
function object(fields) {
|
|
@@ -126,7 +118,6 @@ function discriminator(tag, mapping) {
|
|
|
126
118
|
mapping: jtdMapping
|
|
127
119
|
});
|
|
128
120
|
}
|
|
129
|
-
|
|
130
121
|
//#endregion
|
|
131
122
|
//#region src/types/index.ts
|
|
132
123
|
const t = {
|
|
@@ -139,7 +130,6 @@ const t = {
|
|
|
139
130
|
values,
|
|
140
131
|
discriminator
|
|
141
132
|
};
|
|
142
|
-
|
|
143
133
|
//#endregion
|
|
144
134
|
//#region src/validation/index.ts
|
|
145
135
|
function validateInput(schema, data) {
|
|
@@ -178,7 +168,6 @@ function formatValidationDetails(errors, schema, data) {
|
|
|
178
168
|
return detail;
|
|
179
169
|
});
|
|
180
170
|
}
|
|
181
|
-
|
|
182
171
|
//#endregion
|
|
183
172
|
//#region src/errors.ts
|
|
184
173
|
const DEFAULT_STATUS = {
|
|
@@ -213,7 +202,6 @@ var SeamError = class extends Error {
|
|
|
213
202
|
};
|
|
214
203
|
}
|
|
215
204
|
};
|
|
216
|
-
|
|
217
205
|
//#endregion
|
|
218
206
|
//#region src/context.ts
|
|
219
207
|
/** Parse extract rule into source type and key, e.g. "header:authorization" -> { source: "header", key: "authorization" } */
|
|
@@ -302,7 +290,6 @@ const extract = {
|
|
|
302
290
|
cookie: (name) => `cookie:${name}`,
|
|
303
291
|
query: (name) => `query:${name}`
|
|
304
292
|
};
|
|
305
|
-
|
|
306
293
|
//#endregion
|
|
307
294
|
//#region src/manifest/index.ts
|
|
308
295
|
function normalizeInvalidates(targets) {
|
|
@@ -347,7 +334,6 @@ function buildManifest(definitions, channels, contextConfig, transportDefaults)
|
|
|
347
334
|
if (channels && Object.keys(channels).length > 0) manifest.channels = channels;
|
|
348
335
|
return manifest;
|
|
349
336
|
}
|
|
350
|
-
|
|
351
337
|
//#endregion
|
|
352
338
|
//#region src/page/route-matcher.ts
|
|
353
339
|
function compileRoute(pattern) {
|
|
@@ -397,6 +383,12 @@ var RouteMatcher = class {
|
|
|
397
383
|
value
|
|
398
384
|
});
|
|
399
385
|
}
|
|
386
|
+
clear() {
|
|
387
|
+
this.routes = [];
|
|
388
|
+
}
|
|
389
|
+
get size() {
|
|
390
|
+
return this.routes.length;
|
|
391
|
+
}
|
|
400
392
|
match(path) {
|
|
401
393
|
const parts = path.split("/").filter(Boolean);
|
|
402
394
|
for (const route of this.routes) {
|
|
@@ -410,10 +402,9 @@ var RouteMatcher = class {
|
|
|
410
402
|
return null;
|
|
411
403
|
}
|
|
412
404
|
};
|
|
413
|
-
|
|
414
405
|
//#endregion
|
|
415
406
|
//#region src/router/handler.ts
|
|
416
|
-
async function handleRequest(procedures, procedureName, rawBody, shouldValidateInput = true, validateOutput, ctx) {
|
|
407
|
+
async function handleRequest(procedures, procedureName, rawBody, shouldValidateInput = true, validateOutput, ctx, state) {
|
|
417
408
|
const procedure = procedures.get(procedureName);
|
|
418
409
|
if (!procedure) return {
|
|
419
410
|
status: 404,
|
|
@@ -432,7 +423,8 @@ async function handleRequest(procedures, procedureName, rawBody, shouldValidateI
|
|
|
432
423
|
try {
|
|
433
424
|
const result = await procedure.handler({
|
|
434
425
|
input: rawBody,
|
|
435
|
-
ctx: ctx ?? {}
|
|
426
|
+
ctx: ctx ?? {},
|
|
427
|
+
state
|
|
436
428
|
});
|
|
437
429
|
if (validateOutput) {
|
|
438
430
|
const outValidation = validateInput(procedure.outputSchema, result);
|
|
@@ -459,10 +451,10 @@ async function handleRequest(procedures, procedureName, rawBody, shouldValidateI
|
|
|
459
451
|
};
|
|
460
452
|
}
|
|
461
453
|
}
|
|
462
|
-
async function handleBatchRequest(procedures, calls, shouldValidateInput = true, validateOutput, ctxResolver) {
|
|
454
|
+
async function handleBatchRequest(procedures, calls, shouldValidateInput = true, validateOutput, ctxResolver, state) {
|
|
463
455
|
return { results: await Promise.all(calls.map(async (call) => {
|
|
464
456
|
const ctx = ctxResolver ? ctxResolver(call.procedure) : void 0;
|
|
465
|
-
const result = await handleRequest(procedures, call.procedure, call.input, shouldValidateInput, validateOutput, ctx);
|
|
457
|
+
const result = await handleRequest(procedures, call.procedure, call.input, shouldValidateInput, validateOutput, ctx, state);
|
|
466
458
|
if (result.status === 200) return {
|
|
467
459
|
ok: true,
|
|
468
460
|
data: result.body.data
|
|
@@ -473,7 +465,7 @@ async function handleBatchRequest(procedures, calls, shouldValidateInput = true,
|
|
|
473
465
|
};
|
|
474
466
|
})) };
|
|
475
467
|
}
|
|
476
|
-
async function* handleSubscription(subscriptions, name, rawInput, shouldValidateInput = true, validateOutput, ctx, lastEventId) {
|
|
468
|
+
async function* handleSubscription(subscriptions, name, rawInput, shouldValidateInput = true, validateOutput, ctx, state, lastEventId) {
|
|
477
469
|
const sub = subscriptions.get(name);
|
|
478
470
|
if (!sub) throw new SeamError("NOT_FOUND", `Subscription '${name}' not found`);
|
|
479
471
|
if (shouldValidateInput) {
|
|
@@ -486,6 +478,7 @@ async function* handleSubscription(subscriptions, name, rawInput, shouldValidate
|
|
|
486
478
|
for await (const value of sub.handler({
|
|
487
479
|
input: rawInput,
|
|
488
480
|
ctx: ctx ?? {},
|
|
481
|
+
state,
|
|
489
482
|
lastEventId
|
|
490
483
|
})) {
|
|
491
484
|
if (validateOutput) {
|
|
@@ -495,7 +488,7 @@ async function* handleSubscription(subscriptions, name, rawInput, shouldValidate
|
|
|
495
488
|
yield value;
|
|
496
489
|
}
|
|
497
490
|
}
|
|
498
|
-
async function handleUploadRequest(uploads, procedureName, rawBody, file, shouldValidateInput = true, validateOutput, ctx) {
|
|
491
|
+
async function handleUploadRequest(uploads, procedureName, rawBody, file, shouldValidateInput = true, validateOutput, ctx, state) {
|
|
499
492
|
const upload = uploads.get(procedureName);
|
|
500
493
|
if (!upload) return {
|
|
501
494
|
status: 404,
|
|
@@ -515,7 +508,8 @@ async function handleUploadRequest(uploads, procedureName, rawBody, file, should
|
|
|
515
508
|
const result = await upload.handler({
|
|
516
509
|
input: rawBody,
|
|
517
510
|
file,
|
|
518
|
-
ctx: ctx ?? {}
|
|
511
|
+
ctx: ctx ?? {},
|
|
512
|
+
state
|
|
519
513
|
});
|
|
520
514
|
if (validateOutput) {
|
|
521
515
|
const outValidation = validateInput(upload.outputSchema, result);
|
|
@@ -542,7 +536,7 @@ async function handleUploadRequest(uploads, procedureName, rawBody, file, should
|
|
|
542
536
|
};
|
|
543
537
|
}
|
|
544
538
|
}
|
|
545
|
-
async function* handleStream(streams, name, rawInput, shouldValidateInput = true, validateOutput, ctx) {
|
|
539
|
+
async function* handleStream(streams, name, rawInput, shouldValidateInput = true, validateOutput, ctx, state) {
|
|
546
540
|
const stream = streams.get(name);
|
|
547
541
|
if (!stream) throw new SeamError("NOT_FOUND", `Stream '${name}' not found`);
|
|
548
542
|
if (shouldValidateInput) {
|
|
@@ -554,7 +548,8 @@ async function* handleStream(streams, name, rawInput, shouldValidateInput = true
|
|
|
554
548
|
}
|
|
555
549
|
for await (const value of stream.handler({
|
|
556
550
|
input: rawInput,
|
|
557
|
-
ctx: ctx ?? {}
|
|
551
|
+
ctx: ctx ?? {},
|
|
552
|
+
state
|
|
558
553
|
})) {
|
|
559
554
|
if (validateOutput) {
|
|
560
555
|
const outValidation = validateInput(stream.chunkOutputSchema, value);
|
|
@@ -563,7 +558,6 @@ async function* handleStream(streams, name, rawInput, shouldValidateInput = true
|
|
|
563
558
|
yield value;
|
|
564
559
|
}
|
|
565
560
|
}
|
|
566
|
-
|
|
567
561
|
//#endregion
|
|
568
562
|
//#region src/router/categorize.ts
|
|
569
563
|
function resolveKind(name, def) {
|
|
@@ -622,7 +616,6 @@ function categorizeProcedures(definitions, contextConfig) {
|
|
|
622
616
|
kindMap
|
|
623
617
|
};
|
|
624
618
|
}
|
|
625
|
-
|
|
626
619
|
//#endregion
|
|
627
620
|
//#region src/page/head.ts
|
|
628
621
|
/**
|
|
@@ -644,13 +637,11 @@ function headConfigToHtml(config) {
|
|
|
644
637
|
}
|
|
645
638
|
return html;
|
|
646
639
|
}
|
|
647
|
-
|
|
648
640
|
//#endregion
|
|
649
641
|
//#region src/page/loader-error.ts
|
|
650
642
|
function isLoaderError(value) {
|
|
651
643
|
return typeof value === "object" && value !== null && value.__error === true && typeof value.code === "string" && typeof value.message === "string";
|
|
652
644
|
}
|
|
653
|
-
|
|
654
645
|
//#endregion
|
|
655
646
|
//#region src/page/projection.ts
|
|
656
647
|
/** Set a nested field by dot-separated path, creating intermediate objects as needed. */
|
|
@@ -659,10 +650,13 @@ function setNestedField(target, path, value) {
|
|
|
659
650
|
let current = target;
|
|
660
651
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
661
652
|
const key = parts[i];
|
|
653
|
+
if (key === "__proto__" || key === "prototype" || key === "constructor") return;
|
|
662
654
|
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
663
655
|
current = current[key];
|
|
664
656
|
}
|
|
665
|
-
|
|
657
|
+
const lastPart = parts[parts.length - 1];
|
|
658
|
+
if (lastPart === "__proto__" || lastPart === "prototype" || lastPart === "constructor") return;
|
|
659
|
+
current[lastPart] = value;
|
|
666
660
|
}
|
|
667
661
|
/** Get a nested field by dot-separated path. */
|
|
668
662
|
function getNestedField(source, path) {
|
|
@@ -716,13 +710,12 @@ function applyProjection(data, projections) {
|
|
|
716
710
|
}
|
|
717
711
|
return result;
|
|
718
712
|
}
|
|
719
|
-
|
|
720
713
|
//#endregion
|
|
721
714
|
//#region src/page/handler.ts
|
|
722
715
|
/** Execute loaders, returning keyed results and metadata.
|
|
723
716
|
* Each loader is wrapped in its own try-catch so a single failure
|
|
724
717
|
* does not abort sibling loaders — the page renders at 200 with partial data. */
|
|
725
|
-
async function executeLoaders(loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput) {
|
|
718
|
+
async function executeLoaders(loaders, params, procedures, searchParams, ctxResolver, appState, shouldValidateInput) {
|
|
726
719
|
const entries = Object.entries(loaders);
|
|
727
720
|
const results = await Promise.all(entries.map(async ([key, loader]) => {
|
|
728
721
|
const { procedure, input } = loader(params, searchParams);
|
|
@@ -738,7 +731,8 @@ async function executeLoaders(loaders, params, procedures, searchParams, ctxReso
|
|
|
738
731
|
key,
|
|
739
732
|
result: await proc.handler({
|
|
740
733
|
input,
|
|
741
|
-
ctx
|
|
734
|
+
ctx,
|
|
735
|
+
state: appState
|
|
742
736
|
}),
|
|
743
737
|
procedure,
|
|
744
738
|
input
|
|
@@ -804,12 +798,12 @@ function buildI18nPayload(opts) {
|
|
|
804
798
|
}
|
|
805
799
|
return JSON.stringify(i18nData);
|
|
806
800
|
}
|
|
807
|
-
async function handlePageRequest(page, params, procedures, i18nOpts, searchParams, ctxResolver, shouldValidateInput) {
|
|
801
|
+
async function handlePageRequest(page, params, procedures, i18nOpts, searchParams, ctxResolver, appState, shouldValidateInput) {
|
|
808
802
|
try {
|
|
809
803
|
const t0 = performance.now();
|
|
810
804
|
const layoutChain = page.layoutChain ?? [];
|
|
811
805
|
const locale = i18nOpts?.locale;
|
|
812
|
-
const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput)), executeLoaders(page.loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput)]);
|
|
806
|
+
const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures, searchParams, ctxResolver, appState, shouldValidateInput)), executeLoaders(page.loaders, params, procedures, searchParams, ctxResolver, appState, shouldValidateInput)]);
|
|
813
807
|
const t1 = performance.now();
|
|
814
808
|
const allData = {};
|
|
815
809
|
const allMeta = {};
|
|
@@ -861,7 +855,6 @@ async function handlePageRequest(page, params, procedures, i18nOpts, searchParam
|
|
|
861
855
|
};
|
|
862
856
|
}
|
|
863
857
|
}
|
|
864
|
-
|
|
865
858
|
//#endregion
|
|
866
859
|
//#region src/resolve.ts
|
|
867
860
|
/** URL prefix strategy: trusts pathLocale if it is a known locale */
|
|
@@ -951,7 +944,6 @@ function defaultStrategies() {
|
|
|
951
944
|
fromAcceptLanguage()
|
|
952
945
|
];
|
|
953
946
|
}
|
|
954
|
-
|
|
955
947
|
//#endregion
|
|
956
948
|
//#region src/router/helpers.ts
|
|
957
949
|
/** Resolve a ValidationMode to a boolean flag */
|
|
@@ -1012,7 +1004,7 @@ function resolveCtxFor(map, name, rawCtx, ctxConfig) {
|
|
|
1012
1004
|
return resolveContext(ctxConfig, rawCtx, proc.contextKeys);
|
|
1013
1005
|
}
|
|
1014
1006
|
/** Resolve locale and match page route */
|
|
1015
|
-
async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers, rawCtx, ctxConfig, shouldValidateInput) {
|
|
1007
|
+
async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers, rawCtx, ctxConfig, appState, shouldValidateInput) {
|
|
1016
1008
|
let pathLocale = null;
|
|
1017
1009
|
let routePath = path;
|
|
1018
1010
|
if (hasUrlPrefix && i18nConfig) {
|
|
@@ -1058,7 +1050,7 @@ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strateg
|
|
|
1058
1050
|
if (proc.contextKeys.length === 0) return {};
|
|
1059
1051
|
return resolveContext(ctxConfig ?? {}, rawCtx, proc.contextKeys);
|
|
1060
1052
|
} : void 0;
|
|
1061
|
-
return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams, ctxResolver, shouldValidateInput);
|
|
1053
|
+
return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams, ctxResolver, appState, shouldValidateInput);
|
|
1062
1054
|
}
|
|
1063
1055
|
/** Catch context resolution errors and return them as HandleResult */
|
|
1064
1056
|
function resolveCtxSafe(map, name, rawCtx, ctxConfig) {
|
|
@@ -1072,7 +1064,6 @@ function resolveCtxSafe(map, name, rawCtx, ctxConfig) {
|
|
|
1072
1064
|
throw err;
|
|
1073
1065
|
}
|
|
1074
1066
|
}
|
|
1075
|
-
|
|
1076
1067
|
//#endregion
|
|
1077
1068
|
//#region src/router/state.ts
|
|
1078
1069
|
/** Build all shared state that createRouter methods close over */
|
|
@@ -1102,7 +1093,10 @@ function initRouterState(procedures, opts) {
|
|
|
1102
1093
|
strategies,
|
|
1103
1094
|
hasUrlPrefix,
|
|
1104
1095
|
channelsMeta: collectChannelMeta(opts?.channels),
|
|
1105
|
-
hasCtx: contextHasExtracts(ctxConfig)
|
|
1096
|
+
hasCtx: contextHasExtracts(ctxConfig),
|
|
1097
|
+
appState: opts?.state,
|
|
1098
|
+
rpcHashMap: opts?.rpcHashMap,
|
|
1099
|
+
publicDir: opts?.publicDir
|
|
1106
1100
|
};
|
|
1107
1101
|
}
|
|
1108
1102
|
/** Build request-response methods: handle, handleBatch, handleUpload */
|
|
@@ -1111,23 +1105,22 @@ function buildRpcMethods(state) {
|
|
|
1111
1105
|
async handle(procedureName, body, rawCtx) {
|
|
1112
1106
|
const { ctx, error } = resolveCtxSafe(state.procedureMap, procedureName, rawCtx, state.ctxConfig);
|
|
1113
1107
|
if (error) return error;
|
|
1114
|
-
return handleRequest(state.procedureMap, procedureName, body, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
1108
|
+
return handleRequest(state.procedureMap, procedureName, body, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState);
|
|
1115
1109
|
},
|
|
1116
1110
|
handleBatch(calls, rawCtx) {
|
|
1117
1111
|
const ctxResolver = rawCtx ? (name) => resolveCtxFor(state.procedureMap, name, rawCtx, state.ctxConfig) ?? {} : void 0;
|
|
1118
|
-
return handleBatchRequest(state.procedureMap, calls, state.shouldValidateInput, state.shouldValidateOutput, ctxResolver);
|
|
1112
|
+
return handleBatchRequest(state.procedureMap, calls, state.shouldValidateInput, state.shouldValidateOutput, ctxResolver, state.appState);
|
|
1119
1113
|
},
|
|
1120
1114
|
async handleUpload(name, body, file, rawCtx) {
|
|
1121
1115
|
const { ctx, error } = resolveCtxSafe(state.uploadMap, name, rawCtx, state.ctxConfig);
|
|
1122
1116
|
if (error) return error;
|
|
1123
|
-
return handleUploadRequest(state.uploadMap, name, body, file, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
1117
|
+
return handleUploadRequest(state.uploadMap, name, body, file, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState);
|
|
1124
1118
|
}
|
|
1125
1119
|
};
|
|
1126
1120
|
}
|
|
1127
1121
|
/** Build all Router method implementations from shared state */
|
|
1128
1122
|
function buildRouterMethods(state, procedures, opts) {
|
|
1129
1123
|
return {
|
|
1130
|
-
hasPages: !!state.pages && Object.keys(state.pages).length > 0,
|
|
1131
1124
|
ctxConfig: state.ctxConfig,
|
|
1132
1125
|
hasContext() {
|
|
1133
1126
|
return state.hasCtx;
|
|
@@ -1138,17 +1131,26 @@ function buildRouterMethods(state, procedures, opts) {
|
|
|
1138
1131
|
...buildRpcMethods(state),
|
|
1139
1132
|
handleSubscription(name, input, rawCtx, lastEventId) {
|
|
1140
1133
|
const ctx = resolveCtxFor(state.subscriptionMap, name, rawCtx, state.ctxConfig);
|
|
1141
|
-
return handleSubscription(state.subscriptionMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx, lastEventId);
|
|
1134
|
+
return handleSubscription(state.subscriptionMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState, lastEventId);
|
|
1142
1135
|
},
|
|
1143
1136
|
handleStream(name, input, rawCtx) {
|
|
1144
1137
|
const ctx = resolveCtxFor(state.streamMap, name, rawCtx, state.ctxConfig);
|
|
1145
|
-
return handleStream(state.streamMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
1138
|
+
return handleStream(state.streamMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState);
|
|
1146
1139
|
},
|
|
1147
1140
|
getKind(name) {
|
|
1148
1141
|
return state.kindMap.get(name) ?? null;
|
|
1149
1142
|
},
|
|
1150
1143
|
handlePage(path, headers, rawCtx) {
|
|
1151
|
-
return matchAndHandlePage(state.pageMatcher, state.procedureMap, state.i18nConfig, state.strategies, state.hasUrlPrefix, path, headers, rawCtx, state.ctxConfig, state.shouldValidateInput);
|
|
1144
|
+
return matchAndHandlePage(state.pageMatcher, state.procedureMap, state.i18nConfig, state.strategies, state.hasUrlPrefix, path, headers, rawCtx, state.ctxConfig, state.appState, state.shouldValidateInput);
|
|
1145
|
+
},
|
|
1146
|
+
reload(build) {
|
|
1147
|
+
state.pageMatcher.clear();
|
|
1148
|
+
for (const [pattern, page] of Object.entries(build.pages)) state.pageMatcher.add(pattern, page);
|
|
1149
|
+
state.pages = build.pages;
|
|
1150
|
+
state.i18nConfig = build.i18n ?? null;
|
|
1151
|
+
if (state.i18nConfig) registerI18nQuery(state.procedureMap, state.i18nConfig);
|
|
1152
|
+
state.rpcHashMap = build.rpcHashMap;
|
|
1153
|
+
state.publicDir = build.publicDir;
|
|
1152
1154
|
},
|
|
1153
1155
|
handlePageData(path) {
|
|
1154
1156
|
const match = state.pageMatcher?.match(path);
|
|
@@ -1165,7 +1167,6 @@ function buildRouterMethods(state, procedures, opts) {
|
|
|
1165
1167
|
}
|
|
1166
1168
|
};
|
|
1167
1169
|
}
|
|
1168
|
-
|
|
1169
1170
|
//#endregion
|
|
1170
1171
|
//#region src/router/index.ts
|
|
1171
1172
|
function isProcedureDef(value) {
|
|
@@ -1183,13 +1184,27 @@ function flattenDefinitions(nested, prefix = "") {
|
|
|
1183
1184
|
function createRouter(procedures, opts) {
|
|
1184
1185
|
const flat = flattenDefinitions(procedures);
|
|
1185
1186
|
const state = initRouterState(flat, opts);
|
|
1186
|
-
|
|
1187
|
+
const router = {
|
|
1187
1188
|
procedures: flat,
|
|
1188
|
-
rpcHashMap: opts?.rpcHashMap,
|
|
1189
1189
|
...buildRouterMethods(state, flat, opts)
|
|
1190
1190
|
};
|
|
1191
|
+
Object.defineProperty(router, "hasPages", {
|
|
1192
|
+
get: () => state.pageMatcher.size > 0,
|
|
1193
|
+
enumerable: true,
|
|
1194
|
+
configurable: true
|
|
1195
|
+
});
|
|
1196
|
+
Object.defineProperty(router, "rpcHashMap", {
|
|
1197
|
+
get: () => state.rpcHashMap,
|
|
1198
|
+
enumerable: true,
|
|
1199
|
+
configurable: true
|
|
1200
|
+
});
|
|
1201
|
+
Object.defineProperty(router, "publicDir", {
|
|
1202
|
+
get: () => state.publicDir,
|
|
1203
|
+
enumerable: true,
|
|
1204
|
+
configurable: true
|
|
1205
|
+
});
|
|
1206
|
+
return router;
|
|
1191
1207
|
}
|
|
1192
|
-
|
|
1193
1208
|
//#endregion
|
|
1194
1209
|
//#region src/factory.ts
|
|
1195
1210
|
function query(def) {
|
|
@@ -1222,11 +1237,10 @@ function upload(def) {
|
|
|
1222
1237
|
kind: "upload"
|
|
1223
1238
|
};
|
|
1224
1239
|
}
|
|
1225
|
-
|
|
1226
1240
|
//#endregion
|
|
1227
1241
|
//#region src/seam-router.ts
|
|
1228
1242
|
function createSeamRouter(config) {
|
|
1229
|
-
const { context, ...restConfig } = config;
|
|
1243
|
+
const { context, state, ...restConfig } = config;
|
|
1230
1244
|
const define = {
|
|
1231
1245
|
query(def) {
|
|
1232
1246
|
return {
|
|
@@ -1263,6 +1277,7 @@ function createSeamRouter(config) {
|
|
|
1263
1277
|
return createRouter(procedures, {
|
|
1264
1278
|
...restConfig,
|
|
1265
1279
|
context,
|
|
1280
|
+
state,
|
|
1266
1281
|
...extraOpts
|
|
1267
1282
|
});
|
|
1268
1283
|
}
|
|
@@ -1271,7 +1286,6 @@ function createSeamRouter(config) {
|
|
|
1271
1286
|
define
|
|
1272
1287
|
};
|
|
1273
1288
|
}
|
|
1274
|
-
|
|
1275
1289
|
//#endregion
|
|
1276
1290
|
//#region src/channel.ts
|
|
1277
1291
|
/** Merge channel-level and message-level JTD properties schemas */
|
|
@@ -1346,7 +1360,6 @@ function createChannel(name, def) {
|
|
|
1346
1360
|
channelMeta
|
|
1347
1361
|
};
|
|
1348
1362
|
}
|
|
1349
|
-
|
|
1350
1363
|
//#endregion
|
|
1351
1364
|
//#region src/page/index.ts
|
|
1352
1365
|
function definePage(config) {
|
|
@@ -1355,115 +1368,394 @@ function definePage(config) {
|
|
|
1355
1368
|
layoutChain: config.layoutChain ?? []
|
|
1356
1369
|
};
|
|
1357
1370
|
}
|
|
1358
|
-
|
|
1359
|
-
//#endregion
|
|
1360
|
-
//#region src/mime.ts
|
|
1361
|
-
const MIME_TYPES = {
|
|
1362
|
-
".js": "application/javascript",
|
|
1363
|
-
".mjs": "application/javascript",
|
|
1364
|
-
".css": "text/css",
|
|
1365
|
-
".html": "text/html",
|
|
1366
|
-
".json": "application/json",
|
|
1367
|
-
".svg": "image/svg+xml",
|
|
1368
|
-
".png": "image/png",
|
|
1369
|
-
".jpg": "image/jpeg",
|
|
1370
|
-
".jpeg": "image/jpeg",
|
|
1371
|
-
".gif": "image/gif",
|
|
1372
|
-
".woff": "font/woff",
|
|
1373
|
-
".woff2": "font/woff2",
|
|
1374
|
-
".ttf": "font/ttf",
|
|
1375
|
-
".ico": "image/x-icon",
|
|
1376
|
-
".map": "application/json",
|
|
1377
|
-
".ts": "application/javascript",
|
|
1378
|
-
".tsx": "application/javascript"
|
|
1379
|
-
};
|
|
1380
|
-
|
|
1381
1371
|
//#endregion
|
|
1382
|
-
//#region src/
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1372
|
+
//#region src/dev/reload-watcher.ts
|
|
1373
|
+
const nodeReloadWatcherBackend = {
|
|
1374
|
+
watchFile(path, onChange, onError) {
|
|
1375
|
+
const watcher = watch(path, () => onChange());
|
|
1376
|
+
watcher.on("error", onError);
|
|
1377
|
+
return watcher;
|
|
1378
|
+
},
|
|
1379
|
+
fileExists(path) {
|
|
1380
|
+
return existsSync(path);
|
|
1381
|
+
},
|
|
1382
|
+
setPoll(callback, intervalMs) {
|
|
1383
|
+
const timer = setInterval(callback, intervalMs);
|
|
1384
|
+
return { close() {
|
|
1385
|
+
clearInterval(timer);
|
|
1386
|
+
} };
|
|
1387
|
+
}
|
|
1394
1388
|
};
|
|
1395
|
-
|
|
1396
|
-
|
|
1389
|
+
function isMissingFileError(error) {
|
|
1390
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1391
|
+
}
|
|
1392
|
+
function createReloadWatcher(distDir, onReload, backend) {
|
|
1393
|
+
const triggerPath = join(distDir, ".reload-trigger");
|
|
1394
|
+
let watcher = null;
|
|
1395
|
+
let poller = null;
|
|
1396
|
+
let closed = false;
|
|
1397
|
+
let pending = [];
|
|
1398
|
+
const notify = () => {
|
|
1399
|
+
onReload();
|
|
1400
|
+
const batch = pending;
|
|
1401
|
+
pending = [];
|
|
1402
|
+
for (const p of batch) p.resolve();
|
|
1403
|
+
};
|
|
1404
|
+
const nextReload = () => {
|
|
1405
|
+
if (closed) return Promise.reject(/* @__PURE__ */ new Error("watcher closed"));
|
|
1406
|
+
return new Promise((resolve, reject) => {
|
|
1407
|
+
pending.push({
|
|
1408
|
+
resolve,
|
|
1409
|
+
reject
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
};
|
|
1413
|
+
const closeAll = () => {
|
|
1414
|
+
closed = true;
|
|
1415
|
+
const batch = pending;
|
|
1416
|
+
pending = [];
|
|
1417
|
+
const err = /* @__PURE__ */ new Error("watcher closed");
|
|
1418
|
+
for (const p of batch) p.reject(err);
|
|
1419
|
+
};
|
|
1420
|
+
const stopWatcher = () => {
|
|
1421
|
+
watcher?.close();
|
|
1422
|
+
watcher = null;
|
|
1423
|
+
};
|
|
1424
|
+
const stopPoller = () => {
|
|
1425
|
+
poller?.close();
|
|
1426
|
+
poller = null;
|
|
1427
|
+
};
|
|
1428
|
+
const startPolling = () => {
|
|
1429
|
+
if (closed || poller) return;
|
|
1430
|
+
poller = backend.setPoll(() => {
|
|
1431
|
+
if (!backend.fileExists(triggerPath)) return;
|
|
1432
|
+
stopPoller();
|
|
1433
|
+
notify();
|
|
1434
|
+
startWatchingFile();
|
|
1435
|
+
}, 50);
|
|
1436
|
+
};
|
|
1437
|
+
const startWatchingFile = () => {
|
|
1438
|
+
if (closed) return;
|
|
1439
|
+
try {
|
|
1440
|
+
watcher = backend.watchFile(triggerPath, () => notify(), (error) => {
|
|
1441
|
+
stopWatcher();
|
|
1442
|
+
if (!closed && isMissingFileError(error)) startPolling();
|
|
1443
|
+
});
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
stopWatcher();
|
|
1446
|
+
if (isMissingFileError(error)) {
|
|
1447
|
+
startPolling();
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
throw error;
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
startWatchingFile();
|
|
1397
1454
|
return {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1455
|
+
close() {
|
|
1456
|
+
stopWatcher();
|
|
1457
|
+
stopPoller();
|
|
1458
|
+
closeAll();
|
|
1459
|
+
},
|
|
1460
|
+
nextReload
|
|
1401
1461
|
};
|
|
1402
1462
|
}
|
|
1403
|
-
function
|
|
1404
|
-
return
|
|
1463
|
+
function watchReloadTrigger(distDir, onReload) {
|
|
1464
|
+
return createReloadWatcher(distDir, onReload, nodeReloadWatcherBackend);
|
|
1405
1465
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1466
|
+
//#endregion
|
|
1467
|
+
//#region src/page/build-loader.ts
|
|
1468
|
+
function normalizeParamConfig(value) {
|
|
1469
|
+
return typeof value === "string" ? { from: value } : value;
|
|
1470
|
+
}
|
|
1471
|
+
function buildLoaderFn(config) {
|
|
1472
|
+
return (params, searchParams) => {
|
|
1473
|
+
const input = {};
|
|
1474
|
+
if (config.params) for (const [key, raw_mapping] of Object.entries(config.params)) {
|
|
1475
|
+
const mapping = normalizeParamConfig(raw_mapping);
|
|
1476
|
+
const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
|
|
1477
|
+
if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
|
|
1478
|
+
}
|
|
1412
1479
|
return {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
"Content-Type": contentType,
|
|
1416
|
-
"Cache-Control": IMMUTABLE_CACHE
|
|
1417
|
-
},
|
|
1418
|
-
body: content
|
|
1480
|
+
procedure: config.procedure,
|
|
1481
|
+
input
|
|
1419
1482
|
};
|
|
1420
|
-
}
|
|
1421
|
-
return errorResponse(404, "NOT_FOUND", "Asset not found");
|
|
1422
|
-
}
|
|
1483
|
+
};
|
|
1423
1484
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1485
|
+
function buildLoaderFns(configs) {
|
|
1486
|
+
const fns = {};
|
|
1487
|
+
for (const [key, config] of Object.entries(configs)) fns[key] = buildLoaderFn(config);
|
|
1488
|
+
return fns;
|
|
1427
1489
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1490
|
+
function resolveTemplatePath(entry, defaultLocale) {
|
|
1491
|
+
if (entry.template) return entry.template;
|
|
1492
|
+
if (entry.templates) {
|
|
1493
|
+
const locale = defaultLocale ?? Object.keys(entry.templates)[0];
|
|
1494
|
+
const path = entry.templates[locale];
|
|
1495
|
+
if (!path) throw new Error(`No template for locale "${locale}"`);
|
|
1496
|
+
return path;
|
|
1497
|
+
}
|
|
1498
|
+
throw new Error("Manifest entry has neither 'template' nor 'templates'");
|
|
1431
1499
|
}
|
|
1432
|
-
/**
|
|
1433
|
-
function
|
|
1434
|
-
return
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
})}\n\n`;
|
|
1500
|
+
/** Load all locale templates for a manifest entry, keyed by locale */
|
|
1501
|
+
function loadLocaleTemplates(entry, distDir) {
|
|
1502
|
+
if (!entry.templates) return void 0;
|
|
1503
|
+
const result = {};
|
|
1504
|
+
for (const [locale, relPath] of Object.entries(entry.templates)) result[locale] = readFileSync(join(distDir, relPath), "utf-8");
|
|
1505
|
+
return result;
|
|
1439
1506
|
}
|
|
1440
|
-
/**
|
|
1441
|
-
function
|
|
1442
|
-
|
|
1507
|
+
/** Resolve parent chain for a layout, returning outer-to-inner order */
|
|
1508
|
+
function resolveLayoutChain(layoutId, layoutEntries, getTemplates) {
|
|
1509
|
+
const chain = [];
|
|
1510
|
+
let currentId = layoutId;
|
|
1511
|
+
while (currentId) {
|
|
1512
|
+
const entry = layoutEntries[currentId];
|
|
1513
|
+
if (!entry) break;
|
|
1514
|
+
const { template, localeTemplates } = getTemplates(currentId, entry);
|
|
1515
|
+
chain.push({
|
|
1516
|
+
id: currentId,
|
|
1517
|
+
template,
|
|
1518
|
+
localeTemplates,
|
|
1519
|
+
loaders: buildLoaderFns(entry.loaders ?? {})
|
|
1520
|
+
});
|
|
1521
|
+
currentId = entry.parent;
|
|
1522
|
+
}
|
|
1523
|
+
chain.reverse();
|
|
1524
|
+
return chain;
|
|
1443
1525
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1526
|
+
/** Create a proxy object that lazily reads locale templates from disk */
|
|
1527
|
+
function makeLocaleTemplateGetters(templates, distDir) {
|
|
1528
|
+
const obj = {};
|
|
1529
|
+
for (const [locale, relPath] of Object.entries(templates)) {
|
|
1530
|
+
const fullPath = join(distDir, relPath);
|
|
1531
|
+
Object.defineProperty(obj, locale, {
|
|
1532
|
+
get: () => readFileSync(fullPath, "utf-8"),
|
|
1533
|
+
enumerable: true
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
return obj;
|
|
1447
1537
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
resolve();
|
|
1459
|
-
resolve = null;
|
|
1538
|
+
/** Merge i18n_keys from route + layout chain into a single list */
|
|
1539
|
+
function mergeI18nKeys(route, layoutEntries) {
|
|
1540
|
+
const keys = [];
|
|
1541
|
+
if (route.layout) {
|
|
1542
|
+
let currentId = route.layout;
|
|
1543
|
+
while (currentId) {
|
|
1544
|
+
const entry = layoutEntries[currentId];
|
|
1545
|
+
if (!entry) break;
|
|
1546
|
+
if (entry.i18n_keys) keys.push(...entry.i18n_keys);
|
|
1547
|
+
currentId = entry.parent;
|
|
1460
1548
|
}
|
|
1549
|
+
}
|
|
1550
|
+
if (route.i18n_keys) keys.push(...route.i18n_keys);
|
|
1551
|
+
return keys.length > 0 ? keys : void 0;
|
|
1552
|
+
}
|
|
1553
|
+
/** Detect public-root directory from production build output */
|
|
1554
|
+
function detectBuiltPublicDir(distDir) {
|
|
1555
|
+
const publicRootDir = join(distDir, "public-root");
|
|
1556
|
+
return existsSync(publicRootDir) ? publicRootDir : void 0;
|
|
1557
|
+
}
|
|
1558
|
+
/** Detect source public/ directory for dev mode. */
|
|
1559
|
+
function detectDevPublicDir(distDir) {
|
|
1560
|
+
const explicitDir = process.env.SEAM_PUBLIC_DIR;
|
|
1561
|
+
if (explicitDir && existsSync(explicitDir)) return explicitDir;
|
|
1562
|
+
const sourcePublicDir = join(distDir, "..", "..", "public");
|
|
1563
|
+
return existsSync(sourcePublicDir) ? sourcePublicDir : void 0;
|
|
1564
|
+
}
|
|
1565
|
+
/** Load all build artifacts (pages, rpcHashMap, i18n) in one call */
|
|
1566
|
+
function loadBuild(distDir) {
|
|
1567
|
+
return {
|
|
1568
|
+
pages: loadBuildOutput(distDir),
|
|
1569
|
+
rpcHashMap: loadRpcHashMap(distDir),
|
|
1570
|
+
i18n: loadI18nMessages(distDir),
|
|
1571
|
+
publicDir: detectBuiltPublicDir(distDir)
|
|
1461
1572
|
};
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1573
|
+
}
|
|
1574
|
+
/** Load all build artifacts with lazy template getters (for dev mode) */
|
|
1575
|
+
function loadBuildDev(distDir) {
|
|
1576
|
+
return {
|
|
1577
|
+
pages: loadBuildOutputDev(distDir),
|
|
1578
|
+
rpcHashMap: loadRpcHashMap(distDir),
|
|
1579
|
+
i18n: loadI18nMessages(distDir),
|
|
1580
|
+
publicDir: detectDevPublicDir(distDir) ?? detectBuiltPublicDir(distDir)
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
/** Load the RPC hash map from build output (returns undefined when obfuscation is off) */
|
|
1584
|
+
function loadRpcHashMap(distDir) {
|
|
1585
|
+
const hashMapPath = join(distDir, "rpc-hash-map.json");
|
|
1586
|
+
try {
|
|
1587
|
+
return JSON.parse(readFileSync(hashMapPath, "utf-8"));
|
|
1588
|
+
} catch {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
/** Load i18n config and messages from build output */
|
|
1593
|
+
function loadI18nMessages(distDir) {
|
|
1594
|
+
const manifestPath = join(distDir, "route-manifest.json");
|
|
1595
|
+
try {
|
|
1596
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1597
|
+
if (!manifest.i18n) return null;
|
|
1598
|
+
const mode = manifest.i18n.mode ?? "memory";
|
|
1599
|
+
const cache = manifest.i18n.cache ?? false;
|
|
1600
|
+
const routeHashes = manifest.i18n.route_hashes ?? {};
|
|
1601
|
+
const contentHashes = manifest.i18n.content_hashes ?? {};
|
|
1602
|
+
const messages = {};
|
|
1603
|
+
if (mode === "memory") {
|
|
1604
|
+
const i18nDir = join(distDir, "i18n");
|
|
1605
|
+
for (const locale of manifest.i18n.locales) {
|
|
1606
|
+
const localePath = join(i18nDir, `${locale}.json`);
|
|
1607
|
+
if (existsSync(localePath)) messages[locale] = JSON.parse(readFileSync(localePath, "utf-8"));
|
|
1608
|
+
else messages[locale] = {};
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
locales: manifest.i18n.locales,
|
|
1613
|
+
default: manifest.i18n.default,
|
|
1614
|
+
mode,
|
|
1615
|
+
cache,
|
|
1616
|
+
routeHashes,
|
|
1617
|
+
contentHashes,
|
|
1618
|
+
messages,
|
|
1619
|
+
distDir: mode === "paged" ? distDir : void 0
|
|
1620
|
+
};
|
|
1621
|
+
} catch {
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function loadBuildOutput(distDir) {
|
|
1626
|
+
const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
|
|
1627
|
+
const manifest = JSON.parse(raw);
|
|
1628
|
+
const defaultLocale = manifest.i18n?.default;
|
|
1629
|
+
const layoutTemplates = {};
|
|
1630
|
+
const layoutLocaleTemplates = {};
|
|
1631
|
+
const layoutEntries = manifest.layouts ?? {};
|
|
1632
|
+
for (const [id, entry] of Object.entries(layoutEntries)) {
|
|
1633
|
+
layoutTemplates[id] = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
|
|
1634
|
+
const lt = loadLocaleTemplates(entry, distDir);
|
|
1635
|
+
if (lt) layoutLocaleTemplates[id] = lt;
|
|
1636
|
+
}
|
|
1637
|
+
const staticDir = join(distDir, "..", "static");
|
|
1638
|
+
const hasStaticDir = existsSync(staticDir);
|
|
1639
|
+
const pages = {};
|
|
1640
|
+
for (const [path, entry] of Object.entries(manifest.routes)) {
|
|
1641
|
+
const template = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
|
|
1642
|
+
const loaders = buildLoaderFns(entry.loaders);
|
|
1643
|
+
const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id) => ({
|
|
1644
|
+
template: layoutTemplates[id] ?? "",
|
|
1645
|
+
localeTemplates: layoutLocaleTemplates[id]
|
|
1646
|
+
})) : [];
|
|
1647
|
+
const i18nKeys = mergeI18nKeys(entry, layoutEntries);
|
|
1648
|
+
const page = {
|
|
1649
|
+
template,
|
|
1650
|
+
localeTemplates: loadLocaleTemplates(entry, distDir),
|
|
1651
|
+
loaders,
|
|
1652
|
+
layoutChain,
|
|
1653
|
+
headMeta: entry.head_meta,
|
|
1654
|
+
dataId: manifest.data_id,
|
|
1655
|
+
i18nKeys,
|
|
1656
|
+
pageAssets: entry.assets,
|
|
1657
|
+
projections: entry.projections
|
|
1658
|
+
};
|
|
1659
|
+
if (entry.prerender && hasStaticDir) {
|
|
1660
|
+
page.prerender = true;
|
|
1661
|
+
page.staticDir = staticDir;
|
|
1662
|
+
}
|
|
1663
|
+
pages[path] = page;
|
|
1664
|
+
}
|
|
1665
|
+
return pages;
|
|
1666
|
+
}
|
|
1667
|
+
/** Load build output with lazy template getters -- templates re-read from disk on each access */
|
|
1668
|
+
function loadBuildOutputDev(distDir) {
|
|
1669
|
+
const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
|
|
1670
|
+
const manifest = JSON.parse(raw);
|
|
1671
|
+
const defaultLocale = manifest.i18n?.default;
|
|
1672
|
+
const layoutEntries = manifest.layouts ?? {};
|
|
1673
|
+
const pages = {};
|
|
1674
|
+
for (const [path, entry] of Object.entries(manifest.routes)) {
|
|
1675
|
+
const templatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
|
|
1676
|
+
const loaders = buildLoaderFns(entry.loaders);
|
|
1677
|
+
const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id, layoutEntry) => {
|
|
1678
|
+
const tmplPath = join(distDir, resolveTemplatePath(layoutEntry, defaultLocale));
|
|
1679
|
+
const def = {
|
|
1680
|
+
template: "",
|
|
1681
|
+
localeTemplates: layoutEntry.templates ? makeLocaleTemplateGetters(layoutEntry.templates, distDir) : void 0
|
|
1682
|
+
};
|
|
1683
|
+
Object.defineProperty(def, "template", {
|
|
1684
|
+
get: () => readFileSync(tmplPath, "utf-8"),
|
|
1685
|
+
enumerable: true
|
|
1686
|
+
});
|
|
1687
|
+
return def;
|
|
1688
|
+
}) : [];
|
|
1689
|
+
const localeTemplates = entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0;
|
|
1690
|
+
const i18nKeys = mergeI18nKeys(entry, layoutEntries);
|
|
1691
|
+
const page = {
|
|
1692
|
+
template: "",
|
|
1693
|
+
localeTemplates,
|
|
1694
|
+
loaders,
|
|
1695
|
+
layoutChain,
|
|
1696
|
+
headMeta: entry.head_meta,
|
|
1697
|
+
dataId: manifest.data_id,
|
|
1698
|
+
i18nKeys,
|
|
1699
|
+
pageAssets: entry.assets,
|
|
1700
|
+
projections: entry.projections
|
|
1701
|
+
};
|
|
1702
|
+
Object.defineProperty(page, "template", {
|
|
1703
|
+
get: () => readFileSync(templatePath, "utf-8"),
|
|
1704
|
+
enumerable: true
|
|
1705
|
+
});
|
|
1706
|
+
pages[path] = page;
|
|
1707
|
+
}
|
|
1708
|
+
return pages;
|
|
1709
|
+
}
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region src/http-sse.ts
|
|
1712
|
+
const SSE_HEADER = {
|
|
1713
|
+
"Content-Type": "text/event-stream",
|
|
1714
|
+
"Cache-Control": "no-cache",
|
|
1715
|
+
Connection: "keep-alive"
|
|
1716
|
+
};
|
|
1717
|
+
const DEFAULT_HEARTBEAT_MS$1 = 8e3;
|
|
1718
|
+
const DEFAULT_SSE_IDLE_MS = 12e3;
|
|
1719
|
+
function getSseHeaders() {
|
|
1720
|
+
return SSE_HEADER;
|
|
1721
|
+
}
|
|
1722
|
+
function sseDataEvent(data) {
|
|
1723
|
+
return `event: data\ndata: ${JSON.stringify(data)}\n\n`;
|
|
1724
|
+
}
|
|
1725
|
+
function sseDataEventWithId(data, id) {
|
|
1726
|
+
return `event: data\nid: ${id}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
1727
|
+
}
|
|
1728
|
+
function sseErrorEvent(code, message, transient = false) {
|
|
1729
|
+
return `event: error\ndata: ${JSON.stringify({
|
|
1730
|
+
code,
|
|
1731
|
+
message,
|
|
1732
|
+
transient
|
|
1733
|
+
})}\n\n`;
|
|
1734
|
+
}
|
|
1735
|
+
function sseCompleteEvent() {
|
|
1736
|
+
return "event: complete\ndata: {}\n\n";
|
|
1737
|
+
}
|
|
1738
|
+
function formatSseError(error) {
|
|
1739
|
+
if (error instanceof SeamError) return sseErrorEvent(error.code, error.message);
|
|
1740
|
+
return sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
|
|
1741
|
+
}
|
|
1742
|
+
async function* withSseLifecycle(inner, opts) {
|
|
1743
|
+
const heartbeatMs = opts?.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS$1;
|
|
1744
|
+
const idleMs = opts?.sseIdleTimeout ?? DEFAULT_SSE_IDLE_MS;
|
|
1745
|
+
const idleEnabled = idleMs > 0;
|
|
1746
|
+
const queue = [];
|
|
1747
|
+
let resolve = null;
|
|
1748
|
+
const signal = () => {
|
|
1749
|
+
if (resolve) {
|
|
1750
|
+
resolve();
|
|
1751
|
+
resolve = null;
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
let idleTimer = null;
|
|
1755
|
+
const resetIdle = () => {
|
|
1756
|
+
if (!idleEnabled) return;
|
|
1757
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1758
|
+
idleTimer = setTimeout(() => {
|
|
1467
1759
|
queue.push({ type: "idle" });
|
|
1468
1760
|
signal();
|
|
1469
1761
|
}, idleMs);
|
|
@@ -1472,6 +1764,7 @@ async function* withSseLifecycle(inner, opts) {
|
|
|
1472
1764
|
queue.push({ type: "heartbeat" });
|
|
1473
1765
|
signal();
|
|
1474
1766
|
}, heartbeatMs);
|
|
1767
|
+
queue.push({ type: "heartbeat" });
|
|
1475
1768
|
resetIdle();
|
|
1476
1769
|
(async () => {
|
|
1477
1770
|
try {
|
|
@@ -1528,126 +1821,147 @@ async function* sseStreamForStream(router, name, input, signal, rawCtx) {
|
|
|
1528
1821
|
yield formatSseError(error);
|
|
1529
1822
|
}
|
|
1530
1823
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1824
|
+
function buildHashLookup(hashMap) {
|
|
1825
|
+
if (!hashMap) return null;
|
|
1826
|
+
const map = new Map(Object.entries(hashMap.procedures).map(([n, h]) => [h, n]));
|
|
1827
|
+
map.set("seam.i18n.query", "seam.i18n.query");
|
|
1828
|
+
return map;
|
|
1829
|
+
}
|
|
1830
|
+
function createDevReloadResponse(devState, sseOptions) {
|
|
1831
|
+
const controller = new AbortController();
|
|
1832
|
+
async function* devStream() {
|
|
1833
|
+
yield ": connected\n\n";
|
|
1834
|
+
const aborted = new Promise((_, reject) => {
|
|
1835
|
+
controller.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("aborted")), { once: true });
|
|
1836
|
+
});
|
|
1837
|
+
try {
|
|
1838
|
+
while (!controller.signal.aborted) {
|
|
1839
|
+
await Promise.race([new Promise((r) => {
|
|
1840
|
+
devState.resolvers.add(r);
|
|
1841
|
+
}), aborted]);
|
|
1842
|
+
yield "data: reload\n\n";
|
|
1843
|
+
}
|
|
1844
|
+
} catch {}
|
|
1537
1845
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1846
|
+
return {
|
|
1847
|
+
status: 200,
|
|
1848
|
+
headers: {
|
|
1849
|
+
...SSE_HEADER,
|
|
1850
|
+
"X-Accel-Buffering": "no"
|
|
1851
|
+
},
|
|
1852
|
+
stream: withSseLifecycle(devStream(), {
|
|
1853
|
+
...sseOptions,
|
|
1854
|
+
sseIdleTimeout: 0
|
|
1855
|
+
}),
|
|
1856
|
+
onCancel: () => controller.abort()
|
|
1857
|
+
};
|
|
1547
1858
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1859
|
+
//#endregion
|
|
1860
|
+
//#region src/mime.ts
|
|
1861
|
+
const MIME_TYPES = {
|
|
1862
|
+
".js": "application/javascript",
|
|
1863
|
+
".mjs": "application/javascript",
|
|
1864
|
+
".css": "text/css",
|
|
1865
|
+
".html": "text/html",
|
|
1866
|
+
".json": "application/json",
|
|
1867
|
+
".svg": "image/svg+xml",
|
|
1868
|
+
".png": "image/png",
|
|
1869
|
+
".jpg": "image/jpeg",
|
|
1870
|
+
".jpeg": "image/jpeg",
|
|
1871
|
+
".gif": "image/gif",
|
|
1872
|
+
".woff": "font/woff",
|
|
1873
|
+
".woff2": "font/woff2",
|
|
1874
|
+
".ttf": "font/ttf",
|
|
1875
|
+
".ico": "image/x-icon",
|
|
1876
|
+
".webp": "image/webp",
|
|
1877
|
+
".txt": "text/plain",
|
|
1878
|
+
".xml": "application/xml",
|
|
1879
|
+
".webmanifest": "application/manifest+json",
|
|
1880
|
+
".map": "application/json",
|
|
1881
|
+
".ts": "application/javascript",
|
|
1882
|
+
".tsx": "application/javascript"
|
|
1883
|
+
};
|
|
1884
|
+
//#endregion
|
|
1885
|
+
//#region src/http-response.ts
|
|
1886
|
+
const JSON_HEADER = { "Content-Type": "application/json" };
|
|
1887
|
+
const PUBLIC_CACHE = "public, max-age=3600";
|
|
1888
|
+
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
1889
|
+
const TEXT_ENCODINGS = new Set([
|
|
1890
|
+
"application/javascript",
|
|
1891
|
+
"application/json",
|
|
1892
|
+
"application/manifest+json",
|
|
1893
|
+
"application/xml",
|
|
1894
|
+
"image/svg+xml"
|
|
1895
|
+
]);
|
|
1896
|
+
function normalizeBinaryBody(body) {
|
|
1897
|
+
if (body instanceof Uint8Array) return body;
|
|
1898
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
1899
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
1900
|
+
}
|
|
1901
|
+
function isTextContentType(contentType) {
|
|
1902
|
+
const mime = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
1903
|
+
return mime.startsWith("text/") || TEXT_ENCODINGS.has(mime);
|
|
1904
|
+
}
|
|
1905
|
+
async function readResponseBody(filePath, contentType) {
|
|
1906
|
+
const content = await readFile(filePath);
|
|
1907
|
+
return isTextContentType(contentType) ? content.toString("utf-8") : content;
|
|
1552
1908
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1909
|
+
function jsonResponse(status, body) {
|
|
1910
|
+
return {
|
|
1911
|
+
status,
|
|
1912
|
+
headers: JSON_HEADER,
|
|
1913
|
+
body
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function errorResponse(status, code, message) {
|
|
1917
|
+
return jsonResponse(status, new SeamError(code, message).toJSON());
|
|
1918
|
+
}
|
|
1919
|
+
async function handleStaticAsset(assetPath, staticDir) {
|
|
1920
|
+
if (assetPath.includes("..")) return errorResponse(403, "VALIDATION_ERROR", "Forbidden");
|
|
1921
|
+
const filePath = join(staticDir, assetPath);
|
|
1555
1922
|
try {
|
|
1556
|
-
|
|
1923
|
+
const contentType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
|
|
1924
|
+
const content = await readResponseBody(filePath, contentType);
|
|
1925
|
+
return {
|
|
1926
|
+
status: 200,
|
|
1927
|
+
headers: {
|
|
1928
|
+
"Content-Type": contentType,
|
|
1929
|
+
"Cache-Control": IMMUTABLE_CACHE
|
|
1930
|
+
},
|
|
1931
|
+
body: content
|
|
1932
|
+
};
|
|
1557
1933
|
} catch {
|
|
1558
|
-
return errorResponse(
|
|
1934
|
+
return errorResponse(404, "NOT_FOUND", "Asset not found");
|
|
1559
1935
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1936
|
+
}
|
|
1937
|
+
async function handlePublicFile(pathname, publicDir) {
|
|
1938
|
+
if (pathname.includes("..")) return null;
|
|
1939
|
+
const filePath = join(publicDir, pathname);
|
|
1940
|
+
try {
|
|
1941
|
+
const contentType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
|
|
1942
|
+
const content = await readResponseBody(filePath, contentType);
|
|
1562
1943
|
return {
|
|
1563
1944
|
status: 200,
|
|
1564
|
-
headers:
|
|
1565
|
-
|
|
1566
|
-
|
|
1945
|
+
headers: {
|
|
1946
|
+
"Content-Type": contentType,
|
|
1947
|
+
"Cache-Control": PUBLIC_CACHE
|
|
1948
|
+
},
|
|
1949
|
+
body: content
|
|
1567
1950
|
};
|
|
1951
|
+
} catch {
|
|
1952
|
+
return null;
|
|
1568
1953
|
}
|
|
1569
|
-
if (router.getKind(name) === "upload") {
|
|
1570
|
-
if (!req.file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires multipart/form-data");
|
|
1571
|
-
const file = await req.file();
|
|
1572
|
-
if (!file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires file in multipart body");
|
|
1573
|
-
const result = await router.handleUpload(name, body, file, rawCtx);
|
|
1574
|
-
return jsonResponse(result.status, result.body);
|
|
1575
|
-
}
|
|
1576
|
-
const result = await router.handle(name, body, rawCtx);
|
|
1577
|
-
return jsonResponse(result.status, result.body);
|
|
1578
|
-
}
|
|
1579
|
-
function createHttpHandler(router, opts) {
|
|
1580
|
-
const effectiveHashMap = opts?.rpcHashMap ?? router.rpcHashMap;
|
|
1581
|
-
const hashToName = effectiveHashMap ? new Map(Object.entries(effectiveHashMap.procedures).map(([n, h]) => [h, n])) : null;
|
|
1582
|
-
if (hashToName) hashToName.set("seam.i18n.query", "seam.i18n.query");
|
|
1583
|
-
const batchHash = effectiveHashMap?.batch ?? null;
|
|
1584
|
-
const hasCtx = router.hasContext();
|
|
1585
|
-
return async (req) => {
|
|
1586
|
-
const url = new URL(req.url, "http://localhost");
|
|
1587
|
-
const { pathname } = url;
|
|
1588
|
-
const rawCtx = hasCtx ? buildRawContext(router.ctxConfig, req.header, url) : void 0;
|
|
1589
|
-
if (req.method === "GET" && pathname === MANIFEST_PATH) {
|
|
1590
|
-
if (effectiveHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
|
|
1591
|
-
return jsonResponse(200, router.manifest());
|
|
1592
|
-
}
|
|
1593
|
-
if (pathname.startsWith(PROCEDURE_PREFIX)) {
|
|
1594
|
-
const rawName = pathname.slice(17);
|
|
1595
|
-
if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
|
|
1596
|
-
if (req.method === "POST") {
|
|
1597
|
-
if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
|
|
1598
|
-
return handleProcedurePost(req, router, resolveHashName(hashToName, rawName), rawCtx, opts?.sseOptions);
|
|
1599
|
-
}
|
|
1600
|
-
if (req.method === "GET") {
|
|
1601
|
-
const name = resolveHashName(hashToName, rawName);
|
|
1602
|
-
const rawInput = url.searchParams.get("input");
|
|
1603
|
-
let input;
|
|
1604
|
-
try {
|
|
1605
|
-
input = rawInput ? JSON.parse(rawInput) : {};
|
|
1606
|
-
} catch {
|
|
1607
|
-
return errorResponse(400, "VALIDATION_ERROR", "Invalid input query parameter");
|
|
1608
|
-
}
|
|
1609
|
-
const lastEventId = req.header?.("last-event-id") ?? void 0;
|
|
1610
|
-
return {
|
|
1611
|
-
status: 200,
|
|
1612
|
-
headers: SSE_HEADER,
|
|
1613
|
-
stream: withSseLifecycle(sseStream(router, name, input, rawCtx, lastEventId), opts?.sseOptions)
|
|
1614
|
-
};
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
if (req.method === "GET" && pathname.startsWith(PAGE_PREFIX) && router.hasPages) {
|
|
1618
|
-
const pagePath = "/" + pathname.slice(12);
|
|
1619
|
-
const headers = req.header ? {
|
|
1620
|
-
url: req.url,
|
|
1621
|
-
cookie: req.header("cookie") ?? void 0,
|
|
1622
|
-
acceptLanguage: req.header("accept-language") ?? void 0
|
|
1623
|
-
} : void 0;
|
|
1624
|
-
const result = await router.handlePage(pagePath, headers, rawCtx);
|
|
1625
|
-
if (result) return {
|
|
1626
|
-
status: result.status,
|
|
1627
|
-
headers: HTML_HEADER,
|
|
1628
|
-
body: result.html
|
|
1629
|
-
};
|
|
1630
|
-
}
|
|
1631
|
-
if (req.method === "GET" && pathname.startsWith(DATA_PREFIX) && router.hasPages) {
|
|
1632
|
-
const pagePath = "/" + pathname.slice(12).replace(/\/$/, "");
|
|
1633
|
-
const dataResult = await router.handlePageData(pagePath);
|
|
1634
|
-
if (dataResult !== null) return jsonResponse(200, dataResult);
|
|
1635
|
-
}
|
|
1636
|
-
if (req.method === "GET" && pathname.startsWith(STATIC_PREFIX) && opts?.staticDir) return handleStaticAsset(pathname.slice(14), opts.staticDir);
|
|
1637
|
-
if (opts?.fallback) return opts.fallback(req);
|
|
1638
|
-
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
1639
|
-
};
|
|
1640
1954
|
}
|
|
1641
1955
|
function serialize(body) {
|
|
1642
|
-
|
|
1956
|
+
if (typeof body === "string") return body;
|
|
1957
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return normalizeBinaryBody(body);
|
|
1958
|
+
return JSON.stringify(body);
|
|
1643
1959
|
}
|
|
1644
|
-
/** Consume an async stream chunk-by-chunk; return false from write to stop early. */
|
|
1645
1960
|
async function drainStream(stream, write) {
|
|
1646
1961
|
try {
|
|
1647
1962
|
for await (const chunk of stream) if (write(chunk) === false) break;
|
|
1648
1963
|
} catch {}
|
|
1649
1964
|
}
|
|
1650
|
-
/** Convert an HttpResponse to a Web API Response (for adapters using fetch-compatible runtimes) */
|
|
1651
1965
|
function toWebResponse(result) {
|
|
1652
1966
|
if ("stream" in result) {
|
|
1653
1967
|
const stream = result.stream;
|
|
@@ -1674,237 +1988,143 @@ function toWebResponse(result) {
|
|
|
1674
1988
|
headers: result.headers
|
|
1675
1989
|
});
|
|
1676
1990
|
}
|
|
1677
|
-
|
|
1678
1991
|
//#endregion
|
|
1679
|
-
//#region src/
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
|
|
1690
|
-
}
|
|
1691
|
-
return {
|
|
1692
|
-
procedure: config.procedure,
|
|
1693
|
-
input
|
|
1694
|
-
};
|
|
1695
|
-
};
|
|
1696
|
-
}
|
|
1697
|
-
function buildLoaderFns(configs) {
|
|
1698
|
-
const fns = {};
|
|
1699
|
-
for (const [key, config] of Object.entries(configs)) fns[key] = buildLoaderFn(config);
|
|
1700
|
-
return fns;
|
|
1701
|
-
}
|
|
1702
|
-
function resolveTemplatePath(entry, defaultLocale) {
|
|
1703
|
-
if (entry.template) return entry.template;
|
|
1704
|
-
if (entry.templates) {
|
|
1705
|
-
const locale = defaultLocale ?? Object.keys(entry.templates)[0];
|
|
1706
|
-
const path = entry.templates[locale];
|
|
1707
|
-
if (!path) throw new Error(`No template for locale "${locale}"`);
|
|
1708
|
-
return path;
|
|
1709
|
-
}
|
|
1710
|
-
throw new Error("Manifest entry has neither 'template' nor 'templates'");
|
|
1711
|
-
}
|
|
1712
|
-
/** Load all locale templates for a manifest entry, keyed by locale */
|
|
1713
|
-
function loadLocaleTemplates(entry, distDir) {
|
|
1714
|
-
if (!entry.templates) return void 0;
|
|
1715
|
-
const result = {};
|
|
1716
|
-
for (const [locale, relPath] of Object.entries(entry.templates)) result[locale] = readFileSync(join(distDir, relPath), "utf-8");
|
|
1717
|
-
return result;
|
|
1718
|
-
}
|
|
1719
|
-
/** Resolve parent chain for a layout, returning outer-to-inner order */
|
|
1720
|
-
function resolveLayoutChain(layoutId, layoutEntries, getTemplates) {
|
|
1721
|
-
const chain = [];
|
|
1722
|
-
let currentId = layoutId;
|
|
1723
|
-
while (currentId) {
|
|
1724
|
-
const entry = layoutEntries[currentId];
|
|
1725
|
-
if (!entry) break;
|
|
1726
|
-
const { template, localeTemplates } = getTemplates(currentId, entry);
|
|
1727
|
-
chain.push({
|
|
1728
|
-
id: currentId,
|
|
1729
|
-
template,
|
|
1730
|
-
localeTemplates,
|
|
1731
|
-
loaders: buildLoaderFns(entry.loaders ?? {})
|
|
1732
|
-
});
|
|
1733
|
-
currentId = entry.parent;
|
|
1734
|
-
}
|
|
1735
|
-
chain.reverse();
|
|
1736
|
-
return chain;
|
|
1737
|
-
}
|
|
1738
|
-
/** Create a proxy object that lazily reads locale templates from disk */
|
|
1739
|
-
function makeLocaleTemplateGetters(templates, distDir) {
|
|
1740
|
-
const obj = {};
|
|
1741
|
-
for (const [locale, relPath] of Object.entries(templates)) {
|
|
1742
|
-
const fullPath = join(distDir, relPath);
|
|
1743
|
-
Object.defineProperty(obj, locale, {
|
|
1744
|
-
get: () => readFileSync(fullPath, "utf-8"),
|
|
1745
|
-
enumerable: true
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
return obj;
|
|
1749
|
-
}
|
|
1750
|
-
/** Merge i18n_keys from route + layout chain into a single list */
|
|
1751
|
-
function mergeI18nKeys(route, layoutEntries) {
|
|
1752
|
-
const keys = [];
|
|
1753
|
-
if (route.layout) {
|
|
1754
|
-
let currentId = route.layout;
|
|
1755
|
-
while (currentId) {
|
|
1756
|
-
const entry = layoutEntries[currentId];
|
|
1757
|
-
if (!entry) break;
|
|
1758
|
-
if (entry.i18n_keys) keys.push(...entry.i18n_keys);
|
|
1759
|
-
currentId = entry.parent;
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
if (route.i18n_keys) keys.push(...route.i18n_keys);
|
|
1763
|
-
return keys.length > 0 ? keys : void 0;
|
|
1764
|
-
}
|
|
1765
|
-
/** Load all build artifacts (pages, rpcHashMap, i18n) in one call */
|
|
1766
|
-
function loadBuild(distDir) {
|
|
1767
|
-
return {
|
|
1768
|
-
pages: loadBuildOutput(distDir),
|
|
1769
|
-
rpcHashMap: loadRpcHashMap(distDir),
|
|
1770
|
-
i18n: loadI18nMessages(distDir)
|
|
1771
|
-
};
|
|
1772
|
-
}
|
|
1773
|
-
/** Load all build artifacts with lazy template getters (for dev mode) */
|
|
1774
|
-
function loadBuildDev(distDir) {
|
|
1992
|
+
//#region src/http.ts
|
|
1993
|
+
const PROCEDURE_PREFIX = "/_seam/procedure/";
|
|
1994
|
+
const PAGE_PREFIX = "/_seam/page/";
|
|
1995
|
+
const DATA_PREFIX = "/_seam/data/";
|
|
1996
|
+
const STATIC_PREFIX = "/_seam/static/";
|
|
1997
|
+
const MANIFEST_PATH = "/_seam/manifest.json";
|
|
1998
|
+
const DEV_RELOAD_PATH = "/_seam/dev/reload";
|
|
1999
|
+
const HTML_HEADER = { "Content-Type": "text/html; charset=utf-8" };
|
|
2000
|
+
function getPageRequestHeaders(req) {
|
|
2001
|
+
if (!req.header) return void 0;
|
|
1775
2002
|
return {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2003
|
+
url: req.url,
|
|
2004
|
+
cookie: req.header("cookie") ?? void 0,
|
|
2005
|
+
acceptLanguage: req.header("accept-language") ?? void 0
|
|
1779
2006
|
};
|
|
1780
2007
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const hashMapPath = join(distDir, "rpc-hash-map.json");
|
|
2008
|
+
async function handleBatchHttp(req, router, hashToName, rawCtx) {
|
|
2009
|
+
let body;
|
|
1784
2010
|
try {
|
|
1785
|
-
|
|
2011
|
+
body = await req.body();
|
|
1786
2012
|
} catch {
|
|
1787
|
-
return;
|
|
2013
|
+
return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
|
|
1788
2014
|
}
|
|
2015
|
+
if (!body || typeof body !== "object" || !Array.isArray(body.calls)) return errorResponse(400, "VALIDATION_ERROR", "Batch request must have a 'calls' array");
|
|
2016
|
+
const calls = body.calls.map((c) => ({
|
|
2017
|
+
procedure: typeof c.procedure === "string" ? hashToName?.get(c.procedure) ?? c.procedure : "",
|
|
2018
|
+
input: c.input ?? {}
|
|
2019
|
+
}));
|
|
2020
|
+
return jsonResponse(200, {
|
|
2021
|
+
ok: true,
|
|
2022
|
+
data: await router.handleBatch(calls, rawCtx)
|
|
2023
|
+
});
|
|
1789
2024
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2025
|
+
function resolveHashName(hashToName, name) {
|
|
2026
|
+
if (!hashToName) return name;
|
|
2027
|
+
return hashToName.get(name) ?? name;
|
|
2028
|
+
}
|
|
2029
|
+
async function handleProcedurePost(req, router, name, rawCtx, sseOptions) {
|
|
2030
|
+
let body;
|
|
1793
2031
|
try {
|
|
1794
|
-
|
|
1795
|
-
if (!manifest.i18n) return null;
|
|
1796
|
-
const mode = manifest.i18n.mode ?? "memory";
|
|
1797
|
-
const cache = manifest.i18n.cache ?? false;
|
|
1798
|
-
const routeHashes = manifest.i18n.route_hashes ?? {};
|
|
1799
|
-
const contentHashes = manifest.i18n.content_hashes ?? {};
|
|
1800
|
-
const messages = {};
|
|
1801
|
-
if (mode === "memory") {
|
|
1802
|
-
const i18nDir = join(distDir, "i18n");
|
|
1803
|
-
for (const locale of manifest.i18n.locales) {
|
|
1804
|
-
const localePath = join(i18nDir, `${locale}.json`);
|
|
1805
|
-
if (existsSync(localePath)) messages[locale] = JSON.parse(readFileSync(localePath, "utf-8"));
|
|
1806
|
-
else messages[locale] = {};
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
return {
|
|
1810
|
-
locales: manifest.i18n.locales,
|
|
1811
|
-
default: manifest.i18n.default,
|
|
1812
|
-
mode,
|
|
1813
|
-
cache,
|
|
1814
|
-
routeHashes,
|
|
1815
|
-
contentHashes,
|
|
1816
|
-
messages,
|
|
1817
|
-
distDir: mode === "paged" ? distDir : void 0
|
|
1818
|
-
};
|
|
2032
|
+
body = await req.body();
|
|
1819
2033
|
} catch {
|
|
1820
|
-
return
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
function loadBuildOutput(distDir) {
|
|
1824
|
-
const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
|
|
1825
|
-
const manifest = JSON.parse(raw);
|
|
1826
|
-
const defaultLocale = manifest.i18n?.default;
|
|
1827
|
-
const layoutTemplates = {};
|
|
1828
|
-
const layoutLocaleTemplates = {};
|
|
1829
|
-
const layoutEntries = manifest.layouts ?? {};
|
|
1830
|
-
for (const [id, entry] of Object.entries(layoutEntries)) {
|
|
1831
|
-
layoutTemplates[id] = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
|
|
1832
|
-
const lt = loadLocaleTemplates(entry, distDir);
|
|
1833
|
-
if (lt) layoutLocaleTemplates[id] = lt;
|
|
2034
|
+
return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
|
|
1834
2035
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
template: layoutTemplates[id] ?? "",
|
|
1843
|
-
localeTemplates: layoutLocaleTemplates[id]
|
|
1844
|
-
})) : [];
|
|
1845
|
-
const i18nKeys = mergeI18nKeys(entry, layoutEntries);
|
|
1846
|
-
const page = {
|
|
1847
|
-
template,
|
|
1848
|
-
localeTemplates: loadLocaleTemplates(entry, distDir),
|
|
1849
|
-
loaders,
|
|
1850
|
-
layoutChain,
|
|
1851
|
-
headMeta: entry.head_meta,
|
|
1852
|
-
dataId: manifest.data_id,
|
|
1853
|
-
i18nKeys,
|
|
1854
|
-
pageAssets: entry.assets,
|
|
1855
|
-
projections: entry.projections
|
|
2036
|
+
if (router.getKind(name) === "stream") {
|
|
2037
|
+
const controller = new AbortController();
|
|
2038
|
+
return {
|
|
2039
|
+
status: 200,
|
|
2040
|
+
headers: getSseHeaders(),
|
|
2041
|
+
stream: withSseLifecycle(sseStreamForStream(router, name, body, controller.signal, rawCtx), sseOptions),
|
|
2042
|
+
onCancel: () => controller.abort()
|
|
1856
2043
|
};
|
|
1857
|
-
if (entry.prerender && hasStaticDir) {
|
|
1858
|
-
page.prerender = true;
|
|
1859
|
-
page.staticDir = staticDir;
|
|
1860
|
-
}
|
|
1861
|
-
pages[path] = page;
|
|
1862
2044
|
}
|
|
1863
|
-
|
|
2045
|
+
if (router.getKind(name) === "upload") {
|
|
2046
|
+
if (!req.file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires multipart/form-data");
|
|
2047
|
+
const file = await req.file();
|
|
2048
|
+
if (!file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires file in multipart body");
|
|
2049
|
+
const result = await router.handleUpload(name, body, file, rawCtx);
|
|
2050
|
+
return jsonResponse(result.status, result.body);
|
|
2051
|
+
}
|
|
2052
|
+
const result = await router.handle(name, body, rawCtx);
|
|
2053
|
+
return jsonResponse(result.status, result.body);
|
|
1864
2054
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
const
|
|
1868
|
-
const
|
|
1869
|
-
const
|
|
1870
|
-
const
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2055
|
+
function createHttpHandler(router, opts) {
|
|
2056
|
+
const effectiveHashMap = opts?.rpcHashMap ?? router.rpcHashMap;
|
|
2057
|
+
const hashToName = buildHashLookup(effectiveHashMap);
|
|
2058
|
+
const batchHash = effectiveHashMap?.batch ?? null;
|
|
2059
|
+
const hasCtx = router.hasContext();
|
|
2060
|
+
const devDir = opts?.devBuildDir ?? (process.env.SEAM_DEV === "1" && process.env.SEAM_VITE !== "1" ? process.env.SEAM_OUTPUT_DIR : void 0);
|
|
2061
|
+
const devState = devDir ? { resolvers: /* @__PURE__ */ new Set() } : null;
|
|
2062
|
+
if (devState && devDir) watchReloadTrigger(devDir, () => {
|
|
2063
|
+
try {
|
|
2064
|
+
router.reload(loadBuildDev(devDir));
|
|
2065
|
+
} catch {}
|
|
2066
|
+
const batch = devState.resolvers;
|
|
2067
|
+
devState.resolvers = /* @__PURE__ */ new Set();
|
|
2068
|
+
for (const r of batch) r();
|
|
2069
|
+
});
|
|
2070
|
+
return async (req) => {
|
|
2071
|
+
const url = new URL(req.url, "http://localhost");
|
|
2072
|
+
const { pathname } = url;
|
|
2073
|
+
const rawCtx = hasCtx ? buildRawContext(router.ctxConfig, req.header, url) : void 0;
|
|
2074
|
+
if (req.method === "GET" && pathname === MANIFEST_PATH) {
|
|
2075
|
+
if (effectiveHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
|
|
2076
|
+
return jsonResponse(200, router.manifest());
|
|
2077
|
+
}
|
|
2078
|
+
if (pathname.startsWith(PROCEDURE_PREFIX)) {
|
|
2079
|
+
const rawName = pathname.slice(17);
|
|
2080
|
+
if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
|
|
2081
|
+
if (req.method === "POST") {
|
|
2082
|
+
if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
|
|
2083
|
+
return handleProcedurePost(req, router, resolveHashName(hashToName, rawName), rawCtx, opts?.sseOptions);
|
|
2084
|
+
}
|
|
2085
|
+
if (req.method === "GET") {
|
|
2086
|
+
const name = resolveHashName(hashToName, rawName);
|
|
2087
|
+
const rawInput = url.searchParams.get("input");
|
|
2088
|
+
let input;
|
|
2089
|
+
try {
|
|
2090
|
+
input = rawInput ? JSON.parse(rawInput) : {};
|
|
2091
|
+
} catch {
|
|
2092
|
+
return errorResponse(400, "VALIDATION_ERROR", "Invalid input query parameter");
|
|
2093
|
+
}
|
|
2094
|
+
const lastEventId = req.header?.("last-event-id") ?? void 0;
|
|
2095
|
+
return {
|
|
2096
|
+
status: 200,
|
|
2097
|
+
headers: getSseHeaders(),
|
|
2098
|
+
stream: withSseLifecycle(sseStream(router, name, input, rawCtx, lastEventId), opts?.sseOptions)
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
if (req.method === "GET" && pathname.startsWith(PAGE_PREFIX) && router.hasPages) {
|
|
2103
|
+
const pagePath = "/" + pathname.slice(12);
|
|
2104
|
+
const headers = getPageRequestHeaders(req);
|
|
2105
|
+
const result = await router.handlePage(pagePath, headers, rawCtx);
|
|
2106
|
+
if (result) return {
|
|
2107
|
+
status: result.status,
|
|
2108
|
+
headers: HTML_HEADER,
|
|
2109
|
+
body: result.html
|
|
1880
2110
|
};
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
return
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
projections: entry.projections
|
|
1898
|
-
};
|
|
1899
|
-
Object.defineProperty(page, "template", {
|
|
1900
|
-
get: () => readFileSync(templatePath, "utf-8"),
|
|
1901
|
-
enumerable: true
|
|
1902
|
-
});
|
|
1903
|
-
pages[path] = page;
|
|
1904
|
-
}
|
|
1905
|
-
return pages;
|
|
2111
|
+
}
|
|
2112
|
+
if (req.method === "GET" && pathname.startsWith(DATA_PREFIX) && router.hasPages) {
|
|
2113
|
+
const pagePath = "/" + pathname.slice(12).replace(/\/$/, "");
|
|
2114
|
+
const dataResult = await router.handlePageData(pagePath);
|
|
2115
|
+
if (dataResult !== null) return jsonResponse(200, dataResult);
|
|
2116
|
+
}
|
|
2117
|
+
if (req.method === "GET" && pathname.startsWith(STATIC_PREFIX) && opts?.staticDir) return handleStaticAsset(pathname.slice(14), opts.staticDir);
|
|
2118
|
+
if (req.method === "GET" && pathname === DEV_RELOAD_PATH && devState) return createDevReloadResponse(devState, opts?.sseOptions);
|
|
2119
|
+
const publicDir = opts?.publicDir ?? router.publicDir;
|
|
2120
|
+
if (req.method === "GET" && publicDir) {
|
|
2121
|
+
const publicResult = await handlePublicFile(pathname, publicDir);
|
|
2122
|
+
if (publicResult) return publicResult;
|
|
2123
|
+
}
|
|
2124
|
+
if (opts?.fallback) return opts.fallback(req);
|
|
2125
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
2126
|
+
};
|
|
1906
2127
|
}
|
|
1907
|
-
|
|
1908
2128
|
//#endregion
|
|
1909
2129
|
//#region src/subscription.ts
|
|
1910
2130
|
function fromCallback(setup) {
|
|
@@ -1963,10 +2183,9 @@ function fromCallback(setup) {
|
|
|
1963
2183
|
}
|
|
1964
2184
|
return generate();
|
|
1965
2185
|
}
|
|
1966
|
-
|
|
1967
2186
|
//#endregion
|
|
1968
2187
|
//#region src/ws.ts
|
|
1969
|
-
const DEFAULT_HEARTBEAT_MS =
|
|
2188
|
+
const DEFAULT_HEARTBEAT_MS = 15e3;
|
|
1970
2189
|
const DEFAULT_PONG_TIMEOUT_MS = 5e3;
|
|
1971
2190
|
function sendError(ws, id, code, message) {
|
|
1972
2191
|
ws.send(JSON.stringify({
|
|
@@ -2107,7 +2326,6 @@ function startChannelWs(router, channelName, channelInput, ws, opts) {
|
|
|
2107
2326
|
}
|
|
2108
2327
|
};
|
|
2109
2328
|
}
|
|
2110
|
-
|
|
2111
2329
|
//#endregion
|
|
2112
2330
|
//#region src/proxy.ts
|
|
2113
2331
|
/** Forward non-seam requests to a dev server (e.g. Vite) */
|
|
@@ -2169,64 +2387,7 @@ function createStaticHandler(opts) {
|
|
|
2169
2387
|
}
|
|
2170
2388
|
};
|
|
2171
2389
|
}
|
|
2172
|
-
|
|
2173
|
-
//#endregion
|
|
2174
|
-
//#region src/dev/reload-watcher.ts
|
|
2175
|
-
function watchReloadTrigger(distDir, onReload) {
|
|
2176
|
-
const triggerPath = join(distDir, ".reload-trigger");
|
|
2177
|
-
let watcher = null;
|
|
2178
|
-
let closed = false;
|
|
2179
|
-
let pending = [];
|
|
2180
|
-
const notify = () => {
|
|
2181
|
-
onReload();
|
|
2182
|
-
const batch = pending;
|
|
2183
|
-
pending = [];
|
|
2184
|
-
for (const p of batch) p.resolve();
|
|
2185
|
-
};
|
|
2186
|
-
const nextReload = () => {
|
|
2187
|
-
if (closed) return Promise.reject(/* @__PURE__ */ new Error("watcher closed"));
|
|
2188
|
-
return new Promise((resolve, reject) => {
|
|
2189
|
-
pending.push({
|
|
2190
|
-
resolve,
|
|
2191
|
-
reject
|
|
2192
|
-
});
|
|
2193
|
-
});
|
|
2194
|
-
};
|
|
2195
|
-
const closeAll = () => {
|
|
2196
|
-
closed = true;
|
|
2197
|
-
const batch = pending;
|
|
2198
|
-
pending = [];
|
|
2199
|
-
const err = /* @__PURE__ */ new Error("watcher closed");
|
|
2200
|
-
for (const p of batch) p.reject(err);
|
|
2201
|
-
};
|
|
2202
|
-
try {
|
|
2203
|
-
watcher = watch(triggerPath, () => notify());
|
|
2204
|
-
} catch {
|
|
2205
|
-
const dirWatcher = watch(distDir, (_event, filename) => {
|
|
2206
|
-
if (filename === ".reload-trigger") {
|
|
2207
|
-
dirWatcher.close();
|
|
2208
|
-
watcher = watch(triggerPath, () => notify());
|
|
2209
|
-
notify();
|
|
2210
|
-
}
|
|
2211
|
-
});
|
|
2212
|
-
return {
|
|
2213
|
-
close() {
|
|
2214
|
-
dirWatcher.close();
|
|
2215
|
-
watcher?.close();
|
|
2216
|
-
closeAll();
|
|
2217
|
-
},
|
|
2218
|
-
nextReload
|
|
2219
|
-
};
|
|
2220
|
-
}
|
|
2221
|
-
return {
|
|
2222
|
-
close() {
|
|
2223
|
-
watcher?.close();
|
|
2224
|
-
closeAll();
|
|
2225
|
-
},
|
|
2226
|
-
nextReload
|
|
2227
|
-
};
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
2390
|
//#endregion
|
|
2231
2391
|
export { SeamError, buildRawContext, command, contextHasExtracts, createChannel, createDevProxy, createHttpHandler, createRouter, createSeamRouter, createStaticHandler, defaultStrategies, definePage, drainStream, extract, fromAcceptLanguage, fromCallback, fromCookie, fromUrlPrefix, fromUrlQuery, isLoaderError, loadBuild, loadBuildDev, loadBuildOutput, loadBuildOutputDev, loadI18nMessages, loadRpcHashMap, parseCookieHeader, query, resolveChain, serialize, sseCompleteEvent, sseDataEvent, sseDataEventWithId, sseErrorEvent, startChannelWs, stream, subscription, t, toWebResponse, upload, watchReloadTrigger };
|
|
2392
|
+
|
|
2232
2393
|
//# sourceMappingURL=index.js.map
|