@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/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
- __defProp(target, name, {
13
- get: all[name],
14
- enumerable: true
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
- current[parts[parts.length - 1]] = value;
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
- return {
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/http.ts
1383
- const PROCEDURE_PREFIX = "/_seam/procedure/";
1384
- const PAGE_PREFIX = "/_seam/page/";
1385
- const DATA_PREFIX = "/_seam/data/";
1386
- const STATIC_PREFIX = "/_seam/static/";
1387
- const MANIFEST_PATH = "/_seam/manifest.json";
1388
- const JSON_HEADER = { "Content-Type": "application/json" };
1389
- const HTML_HEADER = { "Content-Type": "text/html; charset=utf-8" };
1390
- const SSE_HEADER = {
1391
- "Content-Type": "text/event-stream",
1392
- "Cache-Control": "no-cache",
1393
- Connection: "keep-alive"
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
- const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
1396
- function jsonResponse(status, body) {
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
- status,
1399
- headers: JSON_HEADER,
1400
- body
1455
+ close() {
1456
+ stopWatcher();
1457
+ stopPoller();
1458
+ closeAll();
1459
+ },
1460
+ nextReload
1401
1461
  };
1402
1462
  }
1403
- function errorResponse(status, code, message) {
1404
- return jsonResponse(status, new SeamError(code, message).toJSON());
1463
+ function watchReloadTrigger(distDir, onReload) {
1464
+ return createReloadWatcher(distDir, onReload, nodeReloadWatcherBackend);
1405
1465
  }
1406
- async function handleStaticAsset(assetPath, staticDir) {
1407
- if (assetPath.includes("..")) return errorResponse(403, "VALIDATION_ERROR", "Forbidden");
1408
- const filePath = join(staticDir, assetPath);
1409
- try {
1410
- const content = await readFile(filePath, "utf-8");
1411
- const contentType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
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
- status: 200,
1414
- headers: {
1415
- "Content-Type": contentType,
1416
- "Cache-Control": IMMUTABLE_CACHE
1417
- },
1418
- body: content
1480
+ procedure: config.procedure,
1481
+ input
1419
1482
  };
1420
- } catch {
1421
- return errorResponse(404, "NOT_FOUND", "Asset not found");
1422
- }
1483
+ };
1423
1484
  }
1424
- /** Format a single SSE data event */
1425
- function sseDataEvent(data) {
1426
- return `event: data\ndata: ${JSON.stringify(data)}\n\n`;
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
- /** Format an SSE data event with a sequence id (for streams) */
1429
- function sseDataEventWithId(data, id) {
1430
- return `event: data\nid: ${id}\ndata: ${JSON.stringify(data)}\n\n`;
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
- /** Format an SSE error event */
1433
- function sseErrorEvent(code, message, transient = false) {
1434
- return `event: error\ndata: ${JSON.stringify({
1435
- code,
1436
- message,
1437
- transient
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
- /** Format an SSE complete event */
1441
- function sseCompleteEvent() {
1442
- return "event: complete\ndata: {}\n\n";
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
- function formatSseError(error) {
1445
- if (error instanceof SeamError) return sseErrorEvent(error.code, error.message);
1446
- return sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
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
- const DEFAULT_HEARTBEAT_MS$1 = 21e3;
1449
- const DEFAULT_SSE_IDLE_MS = 3e4;
1450
- async function* withSseLifecycle(inner, opts) {
1451
- const heartbeatMs = opts?.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS$1;
1452
- const idleMs = opts?.sseIdleTimeout ?? DEFAULT_SSE_IDLE_MS;
1453
- const idleEnabled = idleMs > 0;
1454
- const queue = [];
1455
- let resolve = null;
1456
- const signal = () => {
1457
- if (resolve) {
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
- let idleTimer = null;
1463
- const resetIdle = () => {
1464
- if (!idleEnabled) return;
1465
- if (idleTimer) clearTimeout(idleTimer);
1466
- idleTimer = setTimeout(() => {
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
- async function handleBatchHttp(req, router, hashToName, rawCtx) {
1532
- let body;
1533
- try {
1534
- body = await req.body();
1535
- } catch {
1536
- return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
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
- if (!body || typeof body !== "object" || !Array.isArray(body.calls)) return errorResponse(400, "VALIDATION_ERROR", "Batch request must have a 'calls' array");
1539
- const calls = body.calls.map((c) => ({
1540
- procedure: typeof c.procedure === "string" ? hashToName?.get(c.procedure) ?? c.procedure : "",
1541
- input: c.input ?? {}
1542
- }));
1543
- return jsonResponse(200, {
1544
- ok: true,
1545
- data: await router.handleBatch(calls, rawCtx)
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
- /** Resolve hash -> original name when obfuscation is active. Accepts both hashed and raw names. */
1549
- function resolveHashName(hashToName, name) {
1550
- if (!hashToName) return name;
1551
- return hashToName.get(name) ?? name;
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
- async function handleProcedurePost(req, router, name, rawCtx, sseOptions) {
1554
- let body;
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
- body = await req.body();
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(400, "VALIDATION_ERROR", "Invalid JSON body");
1934
+ return errorResponse(404, "NOT_FOUND", "Asset not found");
1559
1935
  }
1560
- if (router.getKind(name) === "stream") {
1561
- const controller = new AbortController();
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: SSE_HEADER,
1565
- stream: withSseLifecycle(sseStreamForStream(router, name, body, controller.signal, rawCtx), sseOptions),
1566
- onCancel: () => controller.abort()
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
- return typeof body === "string" ? body : JSON.stringify(body);
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/page/build-loader.ts
1680
- function normalizeParamConfig(value) {
1681
- return typeof value === "string" ? { from: value } : value;
1682
- }
1683
- function buildLoaderFn(config) {
1684
- return (params, searchParams) => {
1685
- const input = {};
1686
- if (config.params) for (const [key, raw_mapping] of Object.entries(config.params)) {
1687
- const mapping = normalizeParamConfig(raw_mapping);
1688
- const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
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
- pages: loadBuildOutputDev(distDir),
1777
- rpcHashMap: loadRpcHashMap(distDir),
1778
- i18n: loadI18nMessages(distDir)
2003
+ url: req.url,
2004
+ cookie: req.header("cookie") ?? void 0,
2005
+ acceptLanguage: req.header("accept-language") ?? void 0
1779
2006
  };
1780
2007
  }
1781
- /** Load the RPC hash map from build output (returns undefined when obfuscation is off) */
1782
- function loadRpcHashMap(distDir) {
1783
- const hashMapPath = join(distDir, "rpc-hash-map.json");
2008
+ async function handleBatchHttp(req, router, hashToName, rawCtx) {
2009
+ let body;
1784
2010
  try {
1785
- return JSON.parse(readFileSync(hashMapPath, "utf-8"));
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
- /** Load i18n config and messages from build output */
1791
- function loadI18nMessages(distDir) {
1792
- const manifestPath = join(distDir, "route-manifest.json");
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
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
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 null;
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
- const staticDir = join(distDir, "..", "static");
1836
- const hasStaticDir = existsSync(staticDir);
1837
- const pages = {};
1838
- for (const [path, entry] of Object.entries(manifest.routes)) {
1839
- const template = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
1840
- const loaders = buildLoaderFns(entry.loaders);
1841
- const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id) => ({
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
- return pages;
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
- /** Load build output with lazy template getters -- templates re-read from disk on each access */
1866
- function loadBuildOutputDev(distDir) {
1867
- const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
1868
- const manifest = JSON.parse(raw);
1869
- const defaultLocale = manifest.i18n?.default;
1870
- const layoutEntries = manifest.layouts ?? {};
1871
- const pages = {};
1872
- for (const [path, entry] of Object.entries(manifest.routes)) {
1873
- const templatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
1874
- const loaders = buildLoaderFns(entry.loaders);
1875
- const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id, layoutEntry) => {
1876
- const tmplPath = join(distDir, resolveTemplatePath(layoutEntry, defaultLocale));
1877
- const def = {
1878
- template: "",
1879
- localeTemplates: layoutEntry.templates ? makeLocaleTemplateGetters(layoutEntry.templates, distDir) : void 0
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
- Object.defineProperty(def, "template", {
1882
- get: () => readFileSync(tmplPath, "utf-8"),
1883
- enumerable: true
1884
- });
1885
- return def;
1886
- }) : [];
1887
- const localeTemplates = entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0;
1888
- const i18nKeys = mergeI18nKeys(entry, layoutEntries);
1889
- const page = {
1890
- template: "",
1891
- localeTemplates,
1892
- loaders,
1893
- layoutChain,
1894
- dataId: manifest.data_id,
1895
- i18nKeys,
1896
- pageAssets: entry.assets,
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 = 21e3;
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