@canmi/seam-server 0.5.27 → 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 +121 -87
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +788 -540
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
import { validate } from "jtd";
|
|
2
1
|
import { existsSync, readFileSync, watch } from "node:fs";
|
|
3
2
|
import { extname, join } from "node:path";
|
|
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) {
|
|
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) {
|
|
@@ -485,7 +477,9 @@ async function* handleSubscription(subscriptions, name, rawInput, shouldValidate
|
|
|
485
477
|
}
|
|
486
478
|
for await (const value of sub.handler({
|
|
487
479
|
input: rawInput,
|
|
488
|
-
ctx: ctx ?? {}
|
|
480
|
+
ctx: ctx ?? {},
|
|
481
|
+
state,
|
|
482
|
+
lastEventId
|
|
489
483
|
})) {
|
|
490
484
|
if (validateOutput) {
|
|
491
485
|
const outValidation = validateInput(sub.outputSchema, value);
|
|
@@ -494,7 +488,7 @@ async function* handleSubscription(subscriptions, name, rawInput, shouldValidate
|
|
|
494
488
|
yield value;
|
|
495
489
|
}
|
|
496
490
|
}
|
|
497
|
-
async function handleUploadRequest(uploads, procedureName, rawBody, file, shouldValidateInput = true, validateOutput, ctx) {
|
|
491
|
+
async function handleUploadRequest(uploads, procedureName, rawBody, file, shouldValidateInput = true, validateOutput, ctx, state) {
|
|
498
492
|
const upload = uploads.get(procedureName);
|
|
499
493
|
if (!upload) return {
|
|
500
494
|
status: 404,
|
|
@@ -514,7 +508,8 @@ async function handleUploadRequest(uploads, procedureName, rawBody, file, should
|
|
|
514
508
|
const result = await upload.handler({
|
|
515
509
|
input: rawBody,
|
|
516
510
|
file,
|
|
517
|
-
ctx: ctx ?? {}
|
|
511
|
+
ctx: ctx ?? {},
|
|
512
|
+
state
|
|
518
513
|
});
|
|
519
514
|
if (validateOutput) {
|
|
520
515
|
const outValidation = validateInput(upload.outputSchema, result);
|
|
@@ -541,7 +536,7 @@ async function handleUploadRequest(uploads, procedureName, rawBody, file, should
|
|
|
541
536
|
};
|
|
542
537
|
}
|
|
543
538
|
}
|
|
544
|
-
async function* handleStream(streams, name, rawInput, shouldValidateInput = true, validateOutput, ctx) {
|
|
539
|
+
async function* handleStream(streams, name, rawInput, shouldValidateInput = true, validateOutput, ctx, state) {
|
|
545
540
|
const stream = streams.get(name);
|
|
546
541
|
if (!stream) throw new SeamError("NOT_FOUND", `Stream '${name}' not found`);
|
|
547
542
|
if (shouldValidateInput) {
|
|
@@ -553,7 +548,8 @@ async function* handleStream(streams, name, rawInput, shouldValidateInput = true
|
|
|
553
548
|
}
|
|
554
549
|
for await (const value of stream.handler({
|
|
555
550
|
input: rawInput,
|
|
556
|
-
ctx: ctx ?? {}
|
|
551
|
+
ctx: ctx ?? {},
|
|
552
|
+
state
|
|
557
553
|
})) {
|
|
558
554
|
if (validateOutput) {
|
|
559
555
|
const outValidation = validateInput(stream.chunkOutputSchema, value);
|
|
@@ -562,7 +558,6 @@ async function* handleStream(streams, name, rawInput, shouldValidateInput = true
|
|
|
562
558
|
yield value;
|
|
563
559
|
}
|
|
564
560
|
}
|
|
565
|
-
|
|
566
561
|
//#endregion
|
|
567
562
|
//#region src/router/categorize.ts
|
|
568
563
|
function resolveKind(name, def) {
|
|
@@ -581,6 +576,7 @@ function categorizeProcedures(definitions, contextConfig) {
|
|
|
581
576
|
const uploadMap = /* @__PURE__ */ new Map();
|
|
582
577
|
const kindMap = /* @__PURE__ */ new Map();
|
|
583
578
|
for (const [name, def] of Object.entries(definitions)) {
|
|
579
|
+
if (name.startsWith("seam.")) throw new Error(`Procedure name "${name}" uses reserved "seam." namespace`);
|
|
584
580
|
const kind = resolveKind(name, def);
|
|
585
581
|
kindMap.set(name, kind);
|
|
586
582
|
const contextKeys = def.context ?? [];
|
|
@@ -620,13 +616,32 @@ function categorizeProcedures(definitions, contextConfig) {
|
|
|
620
616
|
kindMap
|
|
621
617
|
};
|
|
622
618
|
}
|
|
623
|
-
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/page/head.ts
|
|
621
|
+
/**
|
|
622
|
+
* Convert a HeadConfig with real values into escaped HTML.
|
|
623
|
+
* Used at request-time by TS server backends.
|
|
624
|
+
*/
|
|
625
|
+
function headConfigToHtml(config) {
|
|
626
|
+
let html = "";
|
|
627
|
+
if (config.title !== void 0) html += `<title>${escapeHtml(config.title)}</title>`;
|
|
628
|
+
for (const meta of config.meta ?? []) {
|
|
629
|
+
html += "<meta";
|
|
630
|
+
for (const [k, v] of Object.entries(meta)) if (v !== void 0) html += ` ${k}="${escapeHtml(v)}"`;
|
|
631
|
+
html += ">";
|
|
632
|
+
}
|
|
633
|
+
for (const link of config.link ?? []) {
|
|
634
|
+
html += "<link";
|
|
635
|
+
for (const [k, v] of Object.entries(link)) if (v !== void 0) html += ` ${k}="${escapeHtml(v)}"`;
|
|
636
|
+
html += ">";
|
|
637
|
+
}
|
|
638
|
+
return html;
|
|
639
|
+
}
|
|
624
640
|
//#endregion
|
|
625
641
|
//#region src/page/loader-error.ts
|
|
626
642
|
function isLoaderError(value) {
|
|
627
643
|
return typeof value === "object" && value !== null && value.__error === true && typeof value.code === "string" && typeof value.message === "string";
|
|
628
644
|
}
|
|
629
|
-
|
|
630
645
|
//#endregion
|
|
631
646
|
//#region src/page/projection.ts
|
|
632
647
|
/** Set a nested field by dot-separated path, creating intermediate objects as needed. */
|
|
@@ -635,10 +650,13 @@ function setNestedField(target, path, value) {
|
|
|
635
650
|
let current = target;
|
|
636
651
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
637
652
|
const key = parts[i];
|
|
653
|
+
if (key === "__proto__" || key === "prototype" || key === "constructor") return;
|
|
638
654
|
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
639
655
|
current = current[key];
|
|
640
656
|
}
|
|
641
|
-
|
|
657
|
+
const lastPart = parts[parts.length - 1];
|
|
658
|
+
if (lastPart === "__proto__" || lastPart === "prototype" || lastPart === "constructor") return;
|
|
659
|
+
current[lastPart] = value;
|
|
642
660
|
}
|
|
643
661
|
/** Get a nested field by dot-separated path. */
|
|
644
662
|
function getNestedField(source, path) {
|
|
@@ -692,13 +710,12 @@ function applyProjection(data, projections) {
|
|
|
692
710
|
}
|
|
693
711
|
return result;
|
|
694
712
|
}
|
|
695
|
-
|
|
696
713
|
//#endregion
|
|
697
714
|
//#region src/page/handler.ts
|
|
698
715
|
/** Execute loaders, returning keyed results and metadata.
|
|
699
716
|
* Each loader is wrapped in its own try-catch so a single failure
|
|
700
717
|
* does not abort sibling loaders — the page renders at 200 with partial data. */
|
|
701
|
-
async function executeLoaders(loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput) {
|
|
718
|
+
async function executeLoaders(loaders, params, procedures, searchParams, ctxResolver, appState, shouldValidateInput) {
|
|
702
719
|
const entries = Object.entries(loaders);
|
|
703
720
|
const results = await Promise.all(entries.map(async ([key, loader]) => {
|
|
704
721
|
const { procedure, input } = loader(params, searchParams);
|
|
@@ -714,7 +731,8 @@ async function executeLoaders(loaders, params, procedures, searchParams, ctxReso
|
|
|
714
731
|
key,
|
|
715
732
|
result: await proc.handler({
|
|
716
733
|
input,
|
|
717
|
-
ctx
|
|
734
|
+
ctx,
|
|
735
|
+
state: appState
|
|
718
736
|
}),
|
|
719
737
|
procedure,
|
|
720
738
|
input
|
|
@@ -764,12 +782,28 @@ function lookupMessages(config, routePattern, locale) {
|
|
|
764
782
|
}
|
|
765
783
|
return config.messages[locale]?.[routeHash] ?? {};
|
|
766
784
|
}
|
|
767
|
-
|
|
785
|
+
/** Build JSON payload for engine i18n injection */
|
|
786
|
+
function buildI18nPayload(opts) {
|
|
787
|
+
const { config: i18nConfig, routePattern, locale } = opts;
|
|
788
|
+
const messages = lookupMessages(i18nConfig, routePattern, locale);
|
|
789
|
+
const routeHash = i18nConfig.routeHashes[routePattern];
|
|
790
|
+
const i18nData = {
|
|
791
|
+
locale,
|
|
792
|
+
default_locale: i18nConfig.default,
|
|
793
|
+
messages
|
|
794
|
+
};
|
|
795
|
+
if (i18nConfig.cache && routeHash) {
|
|
796
|
+
i18nData.hash = i18nConfig.contentHashes[routeHash]?.[locale];
|
|
797
|
+
i18nData.router = i18nConfig.contentHashes;
|
|
798
|
+
}
|
|
799
|
+
return JSON.stringify(i18nData);
|
|
800
|
+
}
|
|
801
|
+
async function handlePageRequest(page, params, procedures, i18nOpts, searchParams, ctxResolver, appState, shouldValidateInput) {
|
|
768
802
|
try {
|
|
769
803
|
const t0 = performance.now();
|
|
770
804
|
const layoutChain = page.layoutChain ?? [];
|
|
771
805
|
const locale = i18nOpts?.locale;
|
|
772
|
-
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)]);
|
|
773
807
|
const t1 = performance.now();
|
|
774
808
|
const allData = {};
|
|
775
809
|
const allMeta = {};
|
|
@@ -783,32 +817,23 @@ async function handlePageRequest(page, params, procedures, i18nOpts, searchParam
|
|
|
783
817
|
const layout = layoutChain[i];
|
|
784
818
|
composedTemplate = selectTemplate(layout.template, layout.localeTemplates, locale).replace("<!--seam:outlet-->", composedTemplate);
|
|
785
819
|
}
|
|
820
|
+
let resolvedHeadMeta = page.headMeta;
|
|
821
|
+
if (page.headFn) try {
|
|
822
|
+
resolvedHeadMeta = headConfigToHtml(page.headFn(allData));
|
|
823
|
+
} catch (err) {
|
|
824
|
+
console.error("[seam] head function failed:", err);
|
|
825
|
+
}
|
|
786
826
|
const config = {
|
|
787
827
|
layout_chain: layoutChain.map((l) => ({
|
|
788
828
|
id: l.id,
|
|
789
829
|
loader_keys: Object.keys(l.loaders)
|
|
790
830
|
})),
|
|
791
831
|
data_id: page.dataId ?? "__data",
|
|
792
|
-
head_meta:
|
|
832
|
+
head_meta: resolvedHeadMeta,
|
|
793
833
|
loader_metadata: allMeta
|
|
794
834
|
};
|
|
795
835
|
if (page.pageAssets) config.page_assets = page.pageAssets;
|
|
796
|
-
|
|
797
|
-
if (i18nOpts) {
|
|
798
|
-
const { config: i18nConfig, routePattern } = i18nOpts;
|
|
799
|
-
const messages = lookupMessages(i18nConfig, routePattern, i18nOpts.locale);
|
|
800
|
-
const routeHash = i18nConfig.routeHashes[routePattern];
|
|
801
|
-
const i18nData = {
|
|
802
|
-
locale: i18nOpts.locale,
|
|
803
|
-
default_locale: i18nConfig.default,
|
|
804
|
-
messages
|
|
805
|
-
};
|
|
806
|
-
if (i18nConfig.cache && routeHash) {
|
|
807
|
-
i18nData.hash = i18nConfig.contentHashes[routeHash]?.[i18nOpts.locale];
|
|
808
|
-
i18nData.router = i18nConfig.contentHashes;
|
|
809
|
-
}
|
|
810
|
-
i18nOptsJson = JSON.stringify(i18nData);
|
|
811
|
-
}
|
|
836
|
+
const i18nOptsJson = i18nOpts ? buildI18nPayload(i18nOpts) : void 0;
|
|
812
837
|
const html = renderPage(composedTemplate, JSON.stringify(prunedData), JSON.stringify(config), i18nOptsJson);
|
|
813
838
|
const t2 = performance.now();
|
|
814
839
|
return {
|
|
@@ -822,11 +847,14 @@ async function handlePageRequest(page, params, procedures, i18nOpts, searchParam
|
|
|
822
847
|
} catch (error) {
|
|
823
848
|
return {
|
|
824
849
|
status: 500,
|
|
825
|
-
html: `<!DOCTYPE html><html><body><h1>500 Internal Server Error</h1><p>${escapeHtml(error instanceof Error ? error.message : "Unknown error")}</p></body></html
|
|
850
|
+
html: `<!DOCTYPE html><html><body><h1>500 Internal Server Error</h1><p>${escapeHtml(error instanceof Error ? error.message : "Unknown error")}</p></body></html>`,
|
|
851
|
+
timing: {
|
|
852
|
+
dataFetch: 0,
|
|
853
|
+
inject: 0
|
|
854
|
+
}
|
|
826
855
|
};
|
|
827
856
|
}
|
|
828
857
|
}
|
|
829
|
-
|
|
830
858
|
//#endregion
|
|
831
859
|
//#region src/resolve.ts
|
|
832
860
|
/** URL prefix strategy: trusts pathLocale if it is a known locale */
|
|
@@ -916,7 +944,6 @@ function defaultStrategies() {
|
|
|
916
944
|
fromAcceptLanguage()
|
|
917
945
|
];
|
|
918
946
|
}
|
|
919
|
-
|
|
920
947
|
//#endregion
|
|
921
948
|
//#region src/router/helpers.ts
|
|
922
949
|
/** Resolve a ValidationMode to a boolean flag */
|
|
@@ -934,14 +961,16 @@ function buildStrategies(opts) {
|
|
|
934
961
|
hasUrlPrefix: strategies.some((s) => s.kind === "url_prefix")
|
|
935
962
|
};
|
|
936
963
|
}
|
|
937
|
-
/** Register built-in
|
|
964
|
+
/** Register built-in seam.i18n.query procedure (route-hash-based lookup) */
|
|
938
965
|
function registerI18nQuery(procedureMap, config) {
|
|
939
|
-
|
|
966
|
+
const localeSet = new Set(config.locales);
|
|
967
|
+
procedureMap.set("seam.i18n.query", {
|
|
940
968
|
inputSchema: {},
|
|
941
969
|
outputSchema: {},
|
|
942
970
|
contextKeys: [],
|
|
943
971
|
handler: ({ input }) => {
|
|
944
|
-
const { route, locale } = input;
|
|
972
|
+
const { route, locale: rawLocale } = input;
|
|
973
|
+
const locale = localeSet.has(rawLocale) ? rawLocale : config.default;
|
|
945
974
|
const messages = lookupI18nMessages(config, route, locale);
|
|
946
975
|
return {
|
|
947
976
|
hash: config.contentHashes[route]?.[locale] ?? "",
|
|
@@ -975,7 +1004,7 @@ function resolveCtxFor(map, name, rawCtx, ctxConfig) {
|
|
|
975
1004
|
return resolveContext(ctxConfig, rawCtx, proc.contextKeys);
|
|
976
1005
|
}
|
|
977
1006
|
/** Resolve locale and match page route */
|
|
978
|
-
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) {
|
|
979
1008
|
let pathLocale = null;
|
|
980
1009
|
let routePath = path;
|
|
981
1010
|
if (hasUrlPrefix && i18nConfig) {
|
|
@@ -998,6 +1027,15 @@ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strateg
|
|
|
998
1027
|
});
|
|
999
1028
|
const match = pageMatcher.match(routePath);
|
|
1000
1029
|
if (!match) return null;
|
|
1030
|
+
if (match.value.prerender && match.value.staticDir) {
|
|
1031
|
+
const htmlPath = join(match.value.staticDir, routePath === "/" ? "" : routePath, "index.html");
|
|
1032
|
+
try {
|
|
1033
|
+
return {
|
|
1034
|
+
status: 200,
|
|
1035
|
+
html: readFileSync(htmlPath, "utf-8")
|
|
1036
|
+
};
|
|
1037
|
+
} catch {}
|
|
1038
|
+
}
|
|
1001
1039
|
let searchParams;
|
|
1002
1040
|
if (headers?.url) try {
|
|
1003
1041
|
const url = new URL(headers.url, "http://localhost");
|
|
@@ -1012,7 +1050,7 @@ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strateg
|
|
|
1012
1050
|
if (proc.contextKeys.length === 0) return {};
|
|
1013
1051
|
return resolveContext(ctxConfig ?? {}, rawCtx, proc.contextKeys);
|
|
1014
1052
|
} : void 0;
|
|
1015
|
-
return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams, ctxResolver, shouldValidateInput);
|
|
1053
|
+
return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams, ctxResolver, appState, shouldValidateInput);
|
|
1016
1054
|
}
|
|
1017
1055
|
/** Catch context resolution errors and return them as HandleResult */
|
|
1018
1056
|
function resolveCtxSafe(map, name, rawCtx, ctxConfig) {
|
|
@@ -1026,7 +1064,6 @@ function resolveCtxSafe(map, name, rawCtx, ctxConfig) {
|
|
|
1026
1064
|
throw err;
|
|
1027
1065
|
}
|
|
1028
1066
|
}
|
|
1029
|
-
|
|
1030
1067
|
//#endregion
|
|
1031
1068
|
//#region src/router/state.ts
|
|
1032
1069
|
/** Build all shared state that createRouter methods close over */
|
|
@@ -1056,7 +1093,10 @@ function initRouterState(procedures, opts) {
|
|
|
1056
1093
|
strategies,
|
|
1057
1094
|
hasUrlPrefix,
|
|
1058
1095
|
channelsMeta: collectChannelMeta(opts?.channels),
|
|
1059
|
-
hasCtx: contextHasExtracts(ctxConfig)
|
|
1096
|
+
hasCtx: contextHasExtracts(ctxConfig),
|
|
1097
|
+
appState: opts?.state,
|
|
1098
|
+
rpcHashMap: opts?.rpcHashMap,
|
|
1099
|
+
publicDir: opts?.publicDir
|
|
1060
1100
|
};
|
|
1061
1101
|
}
|
|
1062
1102
|
/** Build request-response methods: handle, handleBatch, handleUpload */
|
|
@@ -1065,23 +1105,22 @@ function buildRpcMethods(state) {
|
|
|
1065
1105
|
async handle(procedureName, body, rawCtx) {
|
|
1066
1106
|
const { ctx, error } = resolveCtxSafe(state.procedureMap, procedureName, rawCtx, state.ctxConfig);
|
|
1067
1107
|
if (error) return error;
|
|
1068
|
-
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);
|
|
1069
1109
|
},
|
|
1070
1110
|
handleBatch(calls, rawCtx) {
|
|
1071
1111
|
const ctxResolver = rawCtx ? (name) => resolveCtxFor(state.procedureMap, name, rawCtx, state.ctxConfig) ?? {} : void 0;
|
|
1072
|
-
return handleBatchRequest(state.procedureMap, calls, state.shouldValidateInput, state.shouldValidateOutput, ctxResolver);
|
|
1112
|
+
return handleBatchRequest(state.procedureMap, calls, state.shouldValidateInput, state.shouldValidateOutput, ctxResolver, state.appState);
|
|
1073
1113
|
},
|
|
1074
1114
|
async handleUpload(name, body, file, rawCtx) {
|
|
1075
1115
|
const { ctx, error } = resolveCtxSafe(state.uploadMap, name, rawCtx, state.ctxConfig);
|
|
1076
1116
|
if (error) return error;
|
|
1077
|
-
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);
|
|
1078
1118
|
}
|
|
1079
1119
|
};
|
|
1080
1120
|
}
|
|
1081
1121
|
/** Build all Router method implementations from shared state */
|
|
1082
1122
|
function buildRouterMethods(state, procedures, opts) {
|
|
1083
1123
|
return {
|
|
1084
|
-
hasPages: !!state.pages && Object.keys(state.pages).length > 0,
|
|
1085
1124
|
ctxConfig: state.ctxConfig,
|
|
1086
1125
|
hasContext() {
|
|
1087
1126
|
return state.hasCtx;
|
|
@@ -1090,34 +1129,82 @@ function buildRouterMethods(state, procedures, opts) {
|
|
|
1090
1129
|
return buildManifest(procedures, state.channelsMeta, state.ctxConfig, opts?.transportDefaults);
|
|
1091
1130
|
},
|
|
1092
1131
|
...buildRpcMethods(state),
|
|
1093
|
-
handleSubscription(name, input, rawCtx) {
|
|
1132
|
+
handleSubscription(name, input, rawCtx, lastEventId) {
|
|
1094
1133
|
const ctx = resolveCtxFor(state.subscriptionMap, name, rawCtx, state.ctxConfig);
|
|
1095
|
-
return handleSubscription(state.subscriptionMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
1134
|
+
return handleSubscription(state.subscriptionMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState, lastEventId);
|
|
1096
1135
|
},
|
|
1097
1136
|
handleStream(name, input, rawCtx) {
|
|
1098
1137
|
const ctx = resolveCtxFor(state.streamMap, name, rawCtx, state.ctxConfig);
|
|
1099
|
-
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);
|
|
1100
1139
|
},
|
|
1101
1140
|
getKind(name) {
|
|
1102
1141
|
return state.kindMap.get(name) ?? null;
|
|
1103
1142
|
},
|
|
1104
1143
|
handlePage(path, headers, rawCtx) {
|
|
1105
|
-
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;
|
|
1154
|
+
},
|
|
1155
|
+
handlePageData(path) {
|
|
1156
|
+
const match = state.pageMatcher?.match(path);
|
|
1157
|
+
if (!match) return Promise.resolve(null);
|
|
1158
|
+
const page = match.value;
|
|
1159
|
+
if (!page.prerender || !page.staticDir) return Promise.resolve(null);
|
|
1160
|
+
const dataPath = join(page.staticDir, path === "/" ? "" : path, "__data.json");
|
|
1161
|
+
try {
|
|
1162
|
+
if (!existsSync(dataPath)) return Promise.resolve(null);
|
|
1163
|
+
return Promise.resolve(JSON.parse(readFileSync(dataPath, "utf-8")));
|
|
1164
|
+
} catch {
|
|
1165
|
+
return Promise.resolve(null);
|
|
1166
|
+
}
|
|
1106
1167
|
}
|
|
1107
1168
|
};
|
|
1108
1169
|
}
|
|
1109
|
-
|
|
1110
1170
|
//#endregion
|
|
1111
1171
|
//#region src/router/index.ts
|
|
1172
|
+
function isProcedureDef(value) {
|
|
1173
|
+
return typeof value === "object" && value !== null && "input" in value && "handler" in value;
|
|
1174
|
+
}
|
|
1175
|
+
function flattenDefinitions(nested, prefix = "") {
|
|
1176
|
+
const flat = {};
|
|
1177
|
+
for (const [key, value] of Object.entries(nested)) {
|
|
1178
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1179
|
+
if (isProcedureDef(value)) flat[fullKey] = value;
|
|
1180
|
+
else Object.assign(flat, flattenDefinitions(value, fullKey));
|
|
1181
|
+
}
|
|
1182
|
+
return flat;
|
|
1183
|
+
}
|
|
1112
1184
|
function createRouter(procedures, opts) {
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
...buildRouterMethods(state,
|
|
1185
|
+
const flat = flattenDefinitions(procedures);
|
|
1186
|
+
const state = initRouterState(flat, opts);
|
|
1187
|
+
const router = {
|
|
1188
|
+
procedures: flat,
|
|
1189
|
+
...buildRouterMethods(state, flat, opts)
|
|
1118
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;
|
|
1119
1207
|
}
|
|
1120
|
-
|
|
1121
1208
|
//#endregion
|
|
1122
1209
|
//#region src/factory.ts
|
|
1123
1210
|
function query(def) {
|
|
@@ -1150,11 +1237,10 @@ function upload(def) {
|
|
|
1150
1237
|
kind: "upload"
|
|
1151
1238
|
};
|
|
1152
1239
|
}
|
|
1153
|
-
|
|
1154
1240
|
//#endregion
|
|
1155
1241
|
//#region src/seam-router.ts
|
|
1156
1242
|
function createSeamRouter(config) {
|
|
1157
|
-
const { context, ...restConfig } = config;
|
|
1243
|
+
const { context, state, ...restConfig } = config;
|
|
1158
1244
|
const define = {
|
|
1159
1245
|
query(def) {
|
|
1160
1246
|
return {
|
|
@@ -1191,6 +1277,7 @@ function createSeamRouter(config) {
|
|
|
1191
1277
|
return createRouter(procedures, {
|
|
1192
1278
|
...restConfig,
|
|
1193
1279
|
context,
|
|
1280
|
+
state,
|
|
1194
1281
|
...extraOpts
|
|
1195
1282
|
});
|
|
1196
1283
|
}
|
|
@@ -1199,7 +1286,6 @@ function createSeamRouter(config) {
|
|
|
1199
1286
|
define
|
|
1200
1287
|
};
|
|
1201
1288
|
}
|
|
1202
|
-
|
|
1203
1289
|
//#endregion
|
|
1204
1290
|
//#region src/channel.ts
|
|
1205
1291
|
/** Merge channel-level and message-level JTD properties schemas */
|
|
@@ -1274,7 +1360,6 @@ function createChannel(name, def) {
|
|
|
1274
1360
|
channelMeta
|
|
1275
1361
|
};
|
|
1276
1362
|
}
|
|
1277
|
-
|
|
1278
1363
|
//#endregion
|
|
1279
1364
|
//#region src/page/index.ts
|
|
1280
1365
|
function definePage(config) {
|
|
@@ -1283,103 +1368,383 @@ function definePage(config) {
|
|
|
1283
1368
|
layoutChain: config.layoutChain ?? []
|
|
1284
1369
|
};
|
|
1285
1370
|
}
|
|
1286
|
-
|
|
1287
|
-
//#endregion
|
|
1288
|
-
//#region src/mime.ts
|
|
1289
|
-
const MIME_TYPES = {
|
|
1290
|
-
".js": "application/javascript",
|
|
1291
|
-
".mjs": "application/javascript",
|
|
1292
|
-
".css": "text/css",
|
|
1293
|
-
".html": "text/html",
|
|
1294
|
-
".json": "application/json",
|
|
1295
|
-
".svg": "image/svg+xml",
|
|
1296
|
-
".png": "image/png",
|
|
1297
|
-
".jpg": "image/jpeg",
|
|
1298
|
-
".jpeg": "image/jpeg",
|
|
1299
|
-
".gif": "image/gif",
|
|
1300
|
-
".woff": "font/woff",
|
|
1301
|
-
".woff2": "font/woff2",
|
|
1302
|
-
".ttf": "font/ttf",
|
|
1303
|
-
".ico": "image/x-icon",
|
|
1304
|
-
".map": "application/json",
|
|
1305
|
-
".ts": "application/javascript",
|
|
1306
|
-
".tsx": "application/javascript"
|
|
1307
|
-
};
|
|
1308
|
-
|
|
1309
1371
|
//#endregion
|
|
1310
|
-
//#region src/
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
+
}
|
|
1321
1388
|
};
|
|
1322
|
-
|
|
1323
|
-
|
|
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();
|
|
1324
1454
|
return {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1455
|
+
close() {
|
|
1456
|
+
stopWatcher();
|
|
1457
|
+
stopPoller();
|
|
1458
|
+
closeAll();
|
|
1459
|
+
},
|
|
1460
|
+
nextReload
|
|
1328
1461
|
};
|
|
1329
1462
|
}
|
|
1330
|
-
function
|
|
1331
|
-
return
|
|
1463
|
+
function watchReloadTrigger(distDir, onReload) {
|
|
1464
|
+
return createReloadWatcher(distDir, onReload, nodeReloadWatcherBackend);
|
|
1332
1465
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
+
}
|
|
1339
1479
|
return {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
"Content-Type": contentType,
|
|
1343
|
-
"Cache-Control": IMMUTABLE_CACHE
|
|
1344
|
-
},
|
|
1345
|
-
body: content
|
|
1480
|
+
procedure: config.procedure,
|
|
1481
|
+
input
|
|
1346
1482
|
};
|
|
1347
|
-
}
|
|
1348
|
-
return errorResponse(404, "NOT_FOUND", "Asset not found");
|
|
1349
|
-
}
|
|
1483
|
+
};
|
|
1350
1484
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1485
|
+
function buildLoaderFns(configs) {
|
|
1486
|
+
const fns = {};
|
|
1487
|
+
for (const [key, config] of Object.entries(configs)) fns[key] = buildLoaderFn(config);
|
|
1488
|
+
return fns;
|
|
1354
1489
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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'");
|
|
1358
1499
|
}
|
|
1359
|
-
/**
|
|
1360
|
-
function
|
|
1361
|
-
return
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
})}\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;
|
|
1366
1506
|
}
|
|
1367
|
-
/**
|
|
1368
|
-
function
|
|
1369
|
-
|
|
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;
|
|
1370
1525
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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;
|
|
1374
1537
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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;
|
|
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)
|
|
1572
|
+
};
|
|
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;
|
|
1383
1748
|
const signal = () => {
|
|
1384
1749
|
if (resolve) {
|
|
1385
1750
|
resolve();
|
|
@@ -1399,6 +1764,7 @@ async function* withSseLifecycle(inner, opts) {
|
|
|
1399
1764
|
queue.push({ type: "heartbeat" });
|
|
1400
1765
|
signal();
|
|
1401
1766
|
}, heartbeatMs);
|
|
1767
|
+
queue.push({ type: "heartbeat" });
|
|
1402
1768
|
resetIdle();
|
|
1403
1769
|
(async () => {
|
|
1404
1770
|
try {
|
|
@@ -1433,9 +1799,10 @@ async function* withSseLifecycle(inner, opts) {
|
|
|
1433
1799
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1434
1800
|
}
|
|
1435
1801
|
}
|
|
1436
|
-
async function* sseStream(router, name, input, rawCtx) {
|
|
1802
|
+
async function* sseStream(router, name, input, rawCtx, lastEventId) {
|
|
1437
1803
|
try {
|
|
1438
|
-
|
|
1804
|
+
let seq = 0;
|
|
1805
|
+
for await (const value of router.handleSubscription(name, input, rawCtx, lastEventId)) yield sseDataEventWithId(value, seq++);
|
|
1439
1806
|
yield sseCompleteEvent();
|
|
1440
1807
|
} catch (error) {
|
|
1441
1808
|
yield formatSseError(error);
|
|
@@ -1454,120 +1821,147 @@ async function* sseStreamForStream(router, name, input, signal, rawCtx) {
|
|
|
1454
1821
|
yield formatSseError(error);
|
|
1455
1822
|
}
|
|
1456
1823
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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 {}
|
|
1463
1845
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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
|
+
};
|
|
1473
1858
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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;
|
|
1478
1908
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
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);
|
|
1481
1922
|
try {
|
|
1482
|
-
|
|
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
|
+
};
|
|
1483
1933
|
} catch {
|
|
1484
|
-
return errorResponse(
|
|
1934
|
+
return errorResponse(404, "NOT_FOUND", "Asset not found");
|
|
1485
1935
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
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);
|
|
1488
1943
|
return {
|
|
1489
1944
|
status: 200,
|
|
1490
|
-
headers:
|
|
1491
|
-
|
|
1492
|
-
|
|
1945
|
+
headers: {
|
|
1946
|
+
"Content-Type": contentType,
|
|
1947
|
+
"Cache-Control": PUBLIC_CACHE
|
|
1948
|
+
},
|
|
1949
|
+
body: content
|
|
1493
1950
|
};
|
|
1951
|
+
} catch {
|
|
1952
|
+
return null;
|
|
1494
1953
|
}
|
|
1495
|
-
if (router.getKind(name) === "upload") {
|
|
1496
|
-
if (!req.file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires multipart/form-data");
|
|
1497
|
-
const file = await req.file();
|
|
1498
|
-
if (!file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires file in multipart body");
|
|
1499
|
-
const result = await router.handleUpload(name, body, file, rawCtx);
|
|
1500
|
-
return jsonResponse(result.status, result.body);
|
|
1501
|
-
}
|
|
1502
|
-
const result = await router.handle(name, body, rawCtx);
|
|
1503
|
-
return jsonResponse(result.status, result.body);
|
|
1504
|
-
}
|
|
1505
|
-
function createHttpHandler(router, opts) {
|
|
1506
|
-
const effectiveHashMap = opts?.rpcHashMap ?? router.rpcHashMap;
|
|
1507
|
-
const hashToName = effectiveHashMap ? new Map(Object.entries(effectiveHashMap.procedures).map(([n, h]) => [h, n])) : null;
|
|
1508
|
-
if (hashToName) hashToName.set("__seam_i18n_query", "__seam_i18n_query");
|
|
1509
|
-
const batchHash = effectiveHashMap?.batch ?? null;
|
|
1510
|
-
const hasCtx = router.hasContext();
|
|
1511
|
-
return async (req) => {
|
|
1512
|
-
const url = new URL(req.url, "http://localhost");
|
|
1513
|
-
const { pathname } = url;
|
|
1514
|
-
const rawCtx = hasCtx ? buildRawContext(router.ctxConfig, req.header, url) : void 0;
|
|
1515
|
-
if (req.method === "GET" && pathname === MANIFEST_PATH) {
|
|
1516
|
-
if (effectiveHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
|
|
1517
|
-
return jsonResponse(200, router.manifest());
|
|
1518
|
-
}
|
|
1519
|
-
if (pathname.startsWith(PROCEDURE_PREFIX)) {
|
|
1520
|
-
const rawName = pathname.slice(17);
|
|
1521
|
-
if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
|
|
1522
|
-
if (req.method === "POST") {
|
|
1523
|
-
if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
|
|
1524
|
-
return handleProcedurePost(req, router, resolveHashName(hashToName, rawName), rawCtx, opts?.sseOptions);
|
|
1525
|
-
}
|
|
1526
|
-
if (req.method === "GET") {
|
|
1527
|
-
const name = resolveHashName(hashToName, rawName);
|
|
1528
|
-
const rawInput = url.searchParams.get("input");
|
|
1529
|
-
let input;
|
|
1530
|
-
try {
|
|
1531
|
-
input = rawInput ? JSON.parse(rawInput) : {};
|
|
1532
|
-
} catch {
|
|
1533
|
-
return errorResponse(400, "VALIDATION_ERROR", "Invalid input query parameter");
|
|
1534
|
-
}
|
|
1535
|
-
return {
|
|
1536
|
-
status: 200,
|
|
1537
|
-
headers: SSE_HEADER,
|
|
1538
|
-
stream: withSseLifecycle(sseStream(router, name, input, rawCtx), opts?.sseOptions)
|
|
1539
|
-
};
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
if (req.method === "GET" && pathname.startsWith(PAGE_PREFIX) && router.hasPages) {
|
|
1543
|
-
const pagePath = "/" + pathname.slice(12);
|
|
1544
|
-
const headers = req.header ? {
|
|
1545
|
-
url: req.url,
|
|
1546
|
-
cookie: req.header("cookie") ?? void 0,
|
|
1547
|
-
acceptLanguage: req.header("accept-language") ?? void 0
|
|
1548
|
-
} : void 0;
|
|
1549
|
-
const result = await router.handlePage(pagePath, headers, rawCtx);
|
|
1550
|
-
if (result) return {
|
|
1551
|
-
status: result.status,
|
|
1552
|
-
headers: HTML_HEADER,
|
|
1553
|
-
body: result.html
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
if (req.method === "GET" && pathname.startsWith(STATIC_PREFIX) && opts?.staticDir) return handleStaticAsset(pathname.slice(14), opts.staticDir);
|
|
1557
|
-
if (opts?.fallback) return opts.fallback(req);
|
|
1558
|
-
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
1559
|
-
};
|
|
1560
1954
|
}
|
|
1561
1955
|
function serialize(body) {
|
|
1562
|
-
|
|
1956
|
+
if (typeof body === "string") return body;
|
|
1957
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return normalizeBinaryBody(body);
|
|
1958
|
+
return JSON.stringify(body);
|
|
1563
1959
|
}
|
|
1564
|
-
/** Consume an async stream chunk-by-chunk; return false from write to stop early. */
|
|
1565
1960
|
async function drainStream(stream, write) {
|
|
1566
1961
|
try {
|
|
1567
1962
|
for await (const chunk of stream) if (write(chunk) === false) break;
|
|
1568
1963
|
} catch {}
|
|
1569
1964
|
}
|
|
1570
|
-
/** Convert an HttpResponse to a Web API Response (for adapters using fetch-compatible runtimes) */
|
|
1571
1965
|
function toWebResponse(result) {
|
|
1572
1966
|
if ("stream" in result) {
|
|
1573
1967
|
const stream = result.stream;
|
|
@@ -1590,234 +1984,147 @@ function toWebResponse(result) {
|
|
|
1590
1984
|
});
|
|
1591
1985
|
}
|
|
1592
1986
|
return new Response(serialize(result.body), {
|
|
1593
|
-
status: result.status,
|
|
1594
|
-
headers: result.headers
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
//#endregion
|
|
1599
|
-
//#region src/page/build-loader.ts
|
|
1600
|
-
function normalizeParamConfig(value) {
|
|
1601
|
-
return typeof value === "string" ? { from: value } : value;
|
|
1602
|
-
}
|
|
1603
|
-
function buildLoaderFn(config) {
|
|
1604
|
-
return (params, searchParams) => {
|
|
1605
|
-
const input = {};
|
|
1606
|
-
if (config.params) for (const [key, raw_mapping] of Object.entries(config.params)) {
|
|
1607
|
-
const mapping = normalizeParamConfig(raw_mapping);
|
|
1608
|
-
const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
|
|
1609
|
-
if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
|
|
1610
|
-
}
|
|
1611
|
-
return {
|
|
1612
|
-
procedure: config.procedure,
|
|
1613
|
-
input
|
|
1614
|
-
};
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
function buildLoaderFns(configs) {
|
|
1618
|
-
const fns = {};
|
|
1619
|
-
for (const [key, config] of Object.entries(configs)) fns[key] = buildLoaderFn(config);
|
|
1620
|
-
return fns;
|
|
1621
|
-
}
|
|
1622
|
-
function resolveTemplatePath(entry, defaultLocale) {
|
|
1623
|
-
if (entry.template) return entry.template;
|
|
1624
|
-
if (entry.templates) {
|
|
1625
|
-
const locale = defaultLocale ?? Object.keys(entry.templates)[0];
|
|
1626
|
-
const path = entry.templates[locale];
|
|
1627
|
-
if (!path) throw new Error(`No template for locale "${locale}"`);
|
|
1628
|
-
return path;
|
|
1629
|
-
}
|
|
1630
|
-
throw new Error("Manifest entry has neither 'template' nor 'templates'");
|
|
1631
|
-
}
|
|
1632
|
-
/** Load all locale templates for a manifest entry, keyed by locale */
|
|
1633
|
-
function loadLocaleTemplates(entry, distDir) {
|
|
1634
|
-
if (!entry.templates) return void 0;
|
|
1635
|
-
const result = {};
|
|
1636
|
-
for (const [locale, relPath] of Object.entries(entry.templates)) result[locale] = readFileSync(join(distDir, relPath), "utf-8");
|
|
1637
|
-
return result;
|
|
1638
|
-
}
|
|
1639
|
-
/** Resolve parent chain for a layout, returning outer-to-inner order */
|
|
1640
|
-
function resolveLayoutChain(layoutId, layoutEntries, getTemplates) {
|
|
1641
|
-
const chain = [];
|
|
1642
|
-
let currentId = layoutId;
|
|
1643
|
-
while (currentId) {
|
|
1644
|
-
const entry = layoutEntries[currentId];
|
|
1645
|
-
if (!entry) break;
|
|
1646
|
-
const { template, localeTemplates } = getTemplates(currentId, entry);
|
|
1647
|
-
chain.push({
|
|
1648
|
-
id: currentId,
|
|
1649
|
-
template,
|
|
1650
|
-
localeTemplates,
|
|
1651
|
-
loaders: buildLoaderFns(entry.loaders ?? {})
|
|
1652
|
-
});
|
|
1653
|
-
currentId = entry.parent;
|
|
1654
|
-
}
|
|
1655
|
-
chain.reverse();
|
|
1656
|
-
return chain;
|
|
1657
|
-
}
|
|
1658
|
-
/** Create a proxy object that lazily reads locale templates from disk */
|
|
1659
|
-
function makeLocaleTemplateGetters(templates, distDir) {
|
|
1660
|
-
const obj = {};
|
|
1661
|
-
for (const [locale, relPath] of Object.entries(templates)) {
|
|
1662
|
-
const fullPath = join(distDir, relPath);
|
|
1663
|
-
Object.defineProperty(obj, locale, {
|
|
1664
|
-
get: () => readFileSync(fullPath, "utf-8"),
|
|
1665
|
-
enumerable: true
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
return obj;
|
|
1669
|
-
}
|
|
1670
|
-
/** Merge i18n_keys from route + layout chain into a single list */
|
|
1671
|
-
function mergeI18nKeys(route, layoutEntries) {
|
|
1672
|
-
const keys = [];
|
|
1673
|
-
if (route.layout) {
|
|
1674
|
-
let currentId = route.layout;
|
|
1675
|
-
while (currentId) {
|
|
1676
|
-
const entry = layoutEntries[currentId];
|
|
1677
|
-
if (!entry) break;
|
|
1678
|
-
if (entry.i18n_keys) keys.push(...entry.i18n_keys);
|
|
1679
|
-
currentId = entry.parent;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
if (route.i18n_keys) keys.push(...route.i18n_keys);
|
|
1683
|
-
return keys.length > 0 ? keys : void 0;
|
|
1684
|
-
}
|
|
1685
|
-
/** Load all build artifacts (pages, rpcHashMap, i18n) in one call */
|
|
1686
|
-
function loadBuild(distDir) {
|
|
1687
|
-
return {
|
|
1688
|
-
pages: loadBuildOutput(distDir),
|
|
1689
|
-
rpcHashMap: loadRpcHashMap(distDir),
|
|
1690
|
-
i18n: loadI18nMessages(distDir)
|
|
1691
|
-
};
|
|
1987
|
+
status: result.status,
|
|
1988
|
+
headers: result.headers
|
|
1989
|
+
});
|
|
1692
1990
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1991
|
+
//#endregion
|
|
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;
|
|
1695
2002
|
return {
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2003
|
+
url: req.url,
|
|
2004
|
+
cookie: req.header("cookie") ?? void 0,
|
|
2005
|
+
acceptLanguage: req.header("accept-language") ?? void 0
|
|
1699
2006
|
};
|
|
1700
2007
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
const hashMapPath = join(distDir, "rpc-hash-map.json");
|
|
2008
|
+
async function handleBatchHttp(req, router, hashToName, rawCtx) {
|
|
2009
|
+
let body;
|
|
1704
2010
|
try {
|
|
1705
|
-
|
|
2011
|
+
body = await req.body();
|
|
1706
2012
|
} catch {
|
|
1707
|
-
return;
|
|
2013
|
+
return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
|
|
1708
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
|
+
});
|
|
1709
2024
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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;
|
|
1713
2031
|
try {
|
|
1714
|
-
|
|
1715
|
-
if (!manifest.i18n) return null;
|
|
1716
|
-
const mode = manifest.i18n.mode ?? "memory";
|
|
1717
|
-
const cache = manifest.i18n.cache ?? false;
|
|
1718
|
-
const routeHashes = manifest.i18n.route_hashes ?? {};
|
|
1719
|
-
const contentHashes = manifest.i18n.content_hashes ?? {};
|
|
1720
|
-
const messages = {};
|
|
1721
|
-
if (mode === "memory") {
|
|
1722
|
-
const i18nDir = join(distDir, "i18n");
|
|
1723
|
-
for (const locale of manifest.i18n.locales) {
|
|
1724
|
-
const localePath = join(i18nDir, `${locale}.json`);
|
|
1725
|
-
if (existsSync(localePath)) messages[locale] = JSON.parse(readFileSync(localePath, "utf-8"));
|
|
1726
|
-
else messages[locale] = {};
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
return {
|
|
1730
|
-
locales: manifest.i18n.locales,
|
|
1731
|
-
default: manifest.i18n.default,
|
|
1732
|
-
mode,
|
|
1733
|
-
cache,
|
|
1734
|
-
routeHashes,
|
|
1735
|
-
contentHashes,
|
|
1736
|
-
messages,
|
|
1737
|
-
distDir: mode === "paged" ? distDir : void 0
|
|
1738
|
-
};
|
|
2032
|
+
body = await req.body();
|
|
1739
2033
|
} catch {
|
|
1740
|
-
return
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
function loadBuildOutput(distDir) {
|
|
1744
|
-
const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
|
|
1745
|
-
const manifest = JSON.parse(raw);
|
|
1746
|
-
const defaultLocale = manifest.i18n?.default;
|
|
1747
|
-
const layoutTemplates = {};
|
|
1748
|
-
const layoutLocaleTemplates = {};
|
|
1749
|
-
const layoutEntries = manifest.layouts ?? {};
|
|
1750
|
-
for (const [id, entry] of Object.entries(layoutEntries)) {
|
|
1751
|
-
layoutTemplates[id] = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
|
|
1752
|
-
const lt = loadLocaleTemplates(entry, distDir);
|
|
1753
|
-
if (lt) layoutLocaleTemplates[id] = lt;
|
|
2034
|
+
return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
|
|
1754
2035
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
})) : [];
|
|
1763
|
-
const i18nKeys = mergeI18nKeys(entry, layoutEntries);
|
|
1764
|
-
pages[path] = {
|
|
1765
|
-
template,
|
|
1766
|
-
localeTemplates: loadLocaleTemplates(entry, distDir),
|
|
1767
|
-
loaders,
|
|
1768
|
-
layoutChain,
|
|
1769
|
-
headMeta: entry.head_meta,
|
|
1770
|
-
dataId: manifest.data_id,
|
|
1771
|
-
i18nKeys,
|
|
1772
|
-
pageAssets: entry.assets,
|
|
1773
|
-
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()
|
|
1774
2043
|
};
|
|
1775
2044
|
}
|
|
1776
|
-
|
|
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);
|
|
1777
2054
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
const
|
|
1781
|
-
const
|
|
1782
|
-
const
|
|
1783
|
-
const
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
|
1793
2110
|
};
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
return
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
projections: entry.projections
|
|
1811
|
-
};
|
|
1812
|
-
Object.defineProperty(page, "template", {
|
|
1813
|
-
get: () => readFileSync(templatePath, "utf-8"),
|
|
1814
|
-
enumerable: true
|
|
1815
|
-
});
|
|
1816
|
-
pages[path] = page;
|
|
1817
|
-
}
|
|
1818
|
-
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
|
+
};
|
|
1819
2127
|
}
|
|
1820
|
-
|
|
1821
2128
|
//#endregion
|
|
1822
2129
|
//#region src/subscription.ts
|
|
1823
2130
|
function fromCallback(setup) {
|
|
@@ -1876,10 +2183,9 @@ function fromCallback(setup) {
|
|
|
1876
2183
|
}
|
|
1877
2184
|
return generate();
|
|
1878
2185
|
}
|
|
1879
|
-
|
|
1880
2186
|
//#endregion
|
|
1881
2187
|
//#region src/ws.ts
|
|
1882
|
-
const DEFAULT_HEARTBEAT_MS =
|
|
2188
|
+
const DEFAULT_HEARTBEAT_MS = 15e3;
|
|
1883
2189
|
const DEFAULT_PONG_TIMEOUT_MS = 5e3;
|
|
1884
2190
|
function sendError(ws, id, code, message) {
|
|
1885
2191
|
ws.send(JSON.stringify({
|
|
@@ -2020,7 +2326,6 @@ function startChannelWs(router, channelName, channelInput, ws, opts) {
|
|
|
2020
2326
|
}
|
|
2021
2327
|
};
|
|
2022
2328
|
}
|
|
2023
|
-
|
|
2024
2329
|
//#endregion
|
|
2025
2330
|
//#region src/proxy.ts
|
|
2026
2331
|
/** Forward non-seam requests to a dev server (e.g. Vite) */
|
|
@@ -2082,64 +2387,7 @@ function createStaticHandler(opts) {
|
|
|
2082
2387
|
}
|
|
2083
2388
|
};
|
|
2084
2389
|
}
|
|
2085
|
-
|
|
2086
|
-
//#endregion
|
|
2087
|
-
//#region src/dev/reload-watcher.ts
|
|
2088
|
-
function watchReloadTrigger(distDir, onReload) {
|
|
2089
|
-
const triggerPath = join(distDir, ".reload-trigger");
|
|
2090
|
-
let watcher = null;
|
|
2091
|
-
let closed = false;
|
|
2092
|
-
let pending = [];
|
|
2093
|
-
const notify = () => {
|
|
2094
|
-
onReload();
|
|
2095
|
-
const batch = pending;
|
|
2096
|
-
pending = [];
|
|
2097
|
-
for (const p of batch) p.resolve();
|
|
2098
|
-
};
|
|
2099
|
-
const nextReload = () => {
|
|
2100
|
-
if (closed) return Promise.reject(/* @__PURE__ */ new Error("watcher closed"));
|
|
2101
|
-
return new Promise((resolve, reject) => {
|
|
2102
|
-
pending.push({
|
|
2103
|
-
resolve,
|
|
2104
|
-
reject
|
|
2105
|
-
});
|
|
2106
|
-
});
|
|
2107
|
-
};
|
|
2108
|
-
const closeAll = () => {
|
|
2109
|
-
closed = true;
|
|
2110
|
-
const batch = pending;
|
|
2111
|
-
pending = [];
|
|
2112
|
-
const err = /* @__PURE__ */ new Error("watcher closed");
|
|
2113
|
-
for (const p of batch) p.reject(err);
|
|
2114
|
-
};
|
|
2115
|
-
try {
|
|
2116
|
-
watcher = watch(triggerPath, () => notify());
|
|
2117
|
-
} catch {
|
|
2118
|
-
const dirWatcher = watch(distDir, (_event, filename) => {
|
|
2119
|
-
if (filename === ".reload-trigger") {
|
|
2120
|
-
dirWatcher.close();
|
|
2121
|
-
watcher = watch(triggerPath, () => notify());
|
|
2122
|
-
notify();
|
|
2123
|
-
}
|
|
2124
|
-
});
|
|
2125
|
-
return {
|
|
2126
|
-
close() {
|
|
2127
|
-
dirWatcher.close();
|
|
2128
|
-
watcher?.close();
|
|
2129
|
-
closeAll();
|
|
2130
|
-
},
|
|
2131
|
-
nextReload
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
return {
|
|
2135
|
-
close() {
|
|
2136
|
-
watcher?.close();
|
|
2137
|
-
closeAll();
|
|
2138
|
-
},
|
|
2139
|
-
nextReload
|
|
2140
|
-
};
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
2390
|
//#endregion
|
|
2144
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
|
+
|
|
2145
2393
|
//# sourceMappingURL=index.js.map
|