@canmi/seam-server 0.4.18 → 0.5.10
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.d.ts +136 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -173
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -141,24 +141,21 @@ const t = {
|
|
|
141
141
|
};
|
|
142
142
|
|
|
143
143
|
//#endregion
|
|
144
|
-
//#region src/
|
|
145
|
-
function
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (def.error) entry.error = def.error._schema;
|
|
154
|
-
mapped[name] = entry;
|
|
155
|
-
}
|
|
156
|
-
const manifest = {
|
|
157
|
-
version: 1,
|
|
158
|
-
procedures: mapped
|
|
144
|
+
//#region src/validation/index.ts
|
|
145
|
+
function validateInput(schema, data) {
|
|
146
|
+
const errors = validate(schema, data, {
|
|
147
|
+
maxDepth: 32,
|
|
148
|
+
maxErrors: 10
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
valid: errors.length === 0,
|
|
152
|
+
errors
|
|
159
153
|
};
|
|
160
|
-
|
|
161
|
-
|
|
154
|
+
}
|
|
155
|
+
function formatValidationErrors(errors) {
|
|
156
|
+
return errors.map((e) => {
|
|
157
|
+
return `${e.instancePath.length > 0 ? e.instancePath.join("/") : "(root)"} (schema: ${e.schemaPath.join("/")})`;
|
|
158
|
+
}).join("; ");
|
|
162
159
|
}
|
|
163
160
|
|
|
164
161
|
//#endregion
|
|
@@ -193,26 +190,111 @@ var SeamError = class extends Error {
|
|
|
193
190
|
};
|
|
194
191
|
|
|
195
192
|
//#endregion
|
|
196
|
-
//#region src/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
//#region src/context.ts
|
|
194
|
+
/** Parse extract rule into source type and key, e.g. "header:authorization" -> { source: "header", key: "authorization" } */
|
|
195
|
+
function parseExtractRule(rule) {
|
|
196
|
+
const idx = rule.indexOf(":");
|
|
197
|
+
if (idx === -1) throw new Error(`Invalid extract rule "${rule}": expected "source:key" format`);
|
|
198
|
+
const source = rule.slice(0, idx);
|
|
199
|
+
const key = rule.slice(idx + 1);
|
|
200
|
+
if (!source || !key) throw new Error(`Invalid extract rule "${rule}": source and key must be non-empty`);
|
|
202
201
|
return {
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
source,
|
|
203
|
+
key
|
|
205
204
|
};
|
|
206
205
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
/** Collect all header names needed by the context config */
|
|
207
|
+
function contextExtractKeys(config) {
|
|
208
|
+
const keys = [];
|
|
209
|
+
for (const field of Object.values(config)) {
|
|
210
|
+
const { source, key } = parseExtractRule(field.extract);
|
|
211
|
+
if (source === "header") keys.push(key);
|
|
212
|
+
}
|
|
213
|
+
return [...new Set(keys)];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Resolve raw strings into validated context object.
|
|
217
|
+
*
|
|
218
|
+
* For each requested key:
|
|
219
|
+
* - If raw value is null/missing -> pass null to JTD; schema decides via nullable()
|
|
220
|
+
* - If schema expects string -> use raw value directly
|
|
221
|
+
* - If schema expects object -> JSON.parse then validate
|
|
222
|
+
*/
|
|
223
|
+
function resolveContext(config, raw, requestedKeys) {
|
|
224
|
+
const result = {};
|
|
225
|
+
for (const key of requestedKeys) {
|
|
226
|
+
const field = config[key];
|
|
227
|
+
if (!field) throw new SeamError("CONTEXT_ERROR", `Context field "${key}" is not defined in router context config`, 400);
|
|
228
|
+
const { source, key: extractKey } = parseExtractRule(field.extract);
|
|
229
|
+
const rawValue = raw[source === "header" ? extractKey : extractKey] ?? null;
|
|
230
|
+
let value;
|
|
231
|
+
if (rawValue === null) value = null;
|
|
232
|
+
else {
|
|
233
|
+
const schema = field.schema._schema;
|
|
234
|
+
const isStringSchema = "type" in schema && schema.type === "string" && !("nullable" in schema && schema.nullable);
|
|
235
|
+
const isNullableStringSchema = "type" in schema && schema.type === "string" && "nullable" in schema && schema.nullable;
|
|
236
|
+
if (isStringSchema || isNullableStringSchema) value = rawValue;
|
|
237
|
+
else try {
|
|
238
|
+
value = JSON.parse(rawValue);
|
|
239
|
+
} catch {
|
|
240
|
+
throw new SeamError("CONTEXT_ERROR", `Context field "${key}": failed to parse value as JSON`, 400);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const validation = validateInput(field.schema._schema, value);
|
|
244
|
+
if (!validation.valid) throw new SeamError("CONTEXT_ERROR", `Context field "${key}" validation failed: ${formatValidationErrors(validation.errors)}`, 400);
|
|
245
|
+
result[key] = value;
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/manifest/index.ts
|
|
252
|
+
function normalizeInvalidates(targets) {
|
|
253
|
+
return targets.map((t) => {
|
|
254
|
+
if (typeof t === "string") return { query: t };
|
|
255
|
+
const normalized = { query: t.query };
|
|
256
|
+
if (t.mapping) normalized.mapping = Object.fromEntries(Object.entries(t.mapping).map(([k, v]) => [k, typeof v === "string" ? { from: v } : v]));
|
|
257
|
+
return normalized;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function buildManifest(definitions, channels, contextConfig, transportDefaults) {
|
|
261
|
+
const mapped = {};
|
|
262
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
263
|
+
const k = def.kind ?? def.type;
|
|
264
|
+
const kind = k === "upload" ? "upload" : k === "stream" ? "stream" : k === "subscription" ? "subscription" : k === "command" ? "command" : "query";
|
|
265
|
+
const entry = {
|
|
266
|
+
kind,
|
|
267
|
+
input: def.input._schema
|
|
268
|
+
};
|
|
269
|
+
if (kind === "stream") entry.chunkOutput = def.output._schema;
|
|
270
|
+
else entry.output = def.output._schema;
|
|
271
|
+
if (def.error) entry.error = def.error._schema;
|
|
272
|
+
if (kind === "command" && def.invalidates && def.invalidates.length > 0) entry.invalidates = normalizeInvalidates(def.invalidates);
|
|
273
|
+
if (def.context && def.context.length > 0) entry.context = def.context;
|
|
274
|
+
const defAny = def;
|
|
275
|
+
if (defAny.transport) entry.transport = defAny.transport;
|
|
276
|
+
if (defAny.suppress) entry.suppress = defAny.suppress;
|
|
277
|
+
if (defAny.cache !== void 0) entry.cache = defAny.cache;
|
|
278
|
+
mapped[name] = entry;
|
|
279
|
+
}
|
|
280
|
+
const context = {};
|
|
281
|
+
if (contextConfig) for (const [key, field] of Object.entries(contextConfig)) context[key] = {
|
|
282
|
+
extract: field.extract,
|
|
283
|
+
schema: field.schema._schema
|
|
284
|
+
};
|
|
285
|
+
const manifest = {
|
|
286
|
+
version: 2,
|
|
287
|
+
context,
|
|
288
|
+
procedures: mapped,
|
|
289
|
+
transportDefaults: transportDefaults ?? {}
|
|
290
|
+
};
|
|
291
|
+
if (channels && Object.keys(channels).length > 0) manifest.channels = channels;
|
|
292
|
+
return manifest;
|
|
211
293
|
}
|
|
212
294
|
|
|
213
295
|
//#endregion
|
|
214
296
|
//#region src/router/handler.ts
|
|
215
|
-
async function handleRequest(procedures, procedureName, rawBody, validateOutput) {
|
|
297
|
+
async function handleRequest(procedures, procedureName, rawBody, validateOutput, ctx) {
|
|
216
298
|
const procedure = procedures.get(procedureName);
|
|
217
299
|
if (!procedure) return {
|
|
218
300
|
status: 404,
|
|
@@ -224,7 +306,10 @@ async function handleRequest(procedures, procedureName, rawBody, validateOutput)
|
|
|
224
306
|
body: new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`).toJSON()
|
|
225
307
|
};
|
|
226
308
|
try {
|
|
227
|
-
const result = await procedure.handler({
|
|
309
|
+
const result = await procedure.handler({
|
|
310
|
+
input: rawBody,
|
|
311
|
+
ctx: ctx ?? {}
|
|
312
|
+
});
|
|
228
313
|
if (validateOutput) {
|
|
229
314
|
const outValidation = validateInput(procedure.outputSchema, result);
|
|
230
315
|
if (!outValidation.valid) return {
|
|
@@ -250,9 +335,10 @@ async function handleRequest(procedures, procedureName, rawBody, validateOutput)
|
|
|
250
335
|
};
|
|
251
336
|
}
|
|
252
337
|
}
|
|
253
|
-
async function handleBatchRequest(procedures, calls, validateOutput) {
|
|
338
|
+
async function handleBatchRequest(procedures, calls, validateOutput, ctxResolver) {
|
|
254
339
|
return { results: await Promise.all(calls.map(async (call) => {
|
|
255
|
-
const
|
|
340
|
+
const ctx = ctxResolver ? ctxResolver(call.procedure) : void 0;
|
|
341
|
+
const result = await handleRequest(procedures, call.procedure, call.input, validateOutput, ctx);
|
|
256
342
|
if (result.status === 200) return {
|
|
257
343
|
ok: true,
|
|
258
344
|
data: result.body.data
|
|
@@ -263,12 +349,15 @@ async function handleBatchRequest(procedures, calls, validateOutput) {
|
|
|
263
349
|
};
|
|
264
350
|
})) };
|
|
265
351
|
}
|
|
266
|
-
async function* handleSubscription(subscriptions, name, rawInput, validateOutput) {
|
|
352
|
+
async function* handleSubscription(subscriptions, name, rawInput, validateOutput, ctx) {
|
|
267
353
|
const sub = subscriptions.get(name);
|
|
268
354
|
if (!sub) throw new SeamError("NOT_FOUND", `Subscription '${name}' not found`);
|
|
269
355
|
const validation = validateInput(sub.inputSchema, rawInput);
|
|
270
356
|
if (!validation.valid) throw new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`);
|
|
271
|
-
for await (const value of sub.handler({
|
|
357
|
+
for await (const value of sub.handler({
|
|
358
|
+
input: rawInput,
|
|
359
|
+
ctx: ctx ?? {}
|
|
360
|
+
})) {
|
|
272
361
|
if (validateOutput) {
|
|
273
362
|
const outValidation = validateInput(sub.outputSchema, value);
|
|
274
363
|
if (!outValidation.valid) throw new SeamError("INTERNAL_ERROR", `Output validation failed: ${formatValidationErrors(outValidation.errors)}`);
|
|
@@ -276,17 +365,140 @@ async function* handleSubscription(subscriptions, name, rawInput, validateOutput
|
|
|
276
365
|
yield value;
|
|
277
366
|
}
|
|
278
367
|
}
|
|
368
|
+
async function handleUploadRequest(uploads, procedureName, rawBody, file, validateOutput, ctx) {
|
|
369
|
+
const upload = uploads.get(procedureName);
|
|
370
|
+
if (!upload) return {
|
|
371
|
+
status: 404,
|
|
372
|
+
body: new SeamError("NOT_FOUND", `Procedure '${procedureName}' not found`).toJSON()
|
|
373
|
+
};
|
|
374
|
+
const validation = validateInput(upload.inputSchema, rawBody);
|
|
375
|
+
if (!validation.valid) return {
|
|
376
|
+
status: 400,
|
|
377
|
+
body: new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`).toJSON()
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
const result = await upload.handler({
|
|
381
|
+
input: rawBody,
|
|
382
|
+
file,
|
|
383
|
+
ctx: ctx ?? {}
|
|
384
|
+
});
|
|
385
|
+
if (validateOutput) {
|
|
386
|
+
const outValidation = validateInput(upload.outputSchema, result);
|
|
387
|
+
if (!outValidation.valid) return {
|
|
388
|
+
status: 500,
|
|
389
|
+
body: new SeamError("INTERNAL_ERROR", `Output validation failed: ${formatValidationErrors(outValidation.errors)}`).toJSON()
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
status: 200,
|
|
394
|
+
body: {
|
|
395
|
+
ok: true,
|
|
396
|
+
data: result
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof SeamError) return {
|
|
401
|
+
status: error.status,
|
|
402
|
+
body: error.toJSON()
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
status: 500,
|
|
406
|
+
body: new SeamError("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error").toJSON()
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function* handleStream(streams, name, rawInput, validateOutput, ctx) {
|
|
411
|
+
const stream = streams.get(name);
|
|
412
|
+
if (!stream) throw new SeamError("NOT_FOUND", `Stream '${name}' not found`);
|
|
413
|
+
const validation = validateInput(stream.inputSchema, rawInput);
|
|
414
|
+
if (!validation.valid) throw new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`);
|
|
415
|
+
for await (const value of stream.handler({
|
|
416
|
+
input: rawInput,
|
|
417
|
+
ctx: ctx ?? {}
|
|
418
|
+
})) {
|
|
419
|
+
if (validateOutput) {
|
|
420
|
+
const outValidation = validateInput(stream.chunkOutputSchema, value);
|
|
421
|
+
if (!outValidation.valid) throw new SeamError("INTERNAL_ERROR", `Output validation failed: ${formatValidationErrors(outValidation.errors)}`);
|
|
422
|
+
}
|
|
423
|
+
yield value;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/page/projection.ts
|
|
429
|
+
/** Set a nested field by dot-separated path, creating intermediate objects as needed. */
|
|
430
|
+
function setNestedField(target, path, value) {
|
|
431
|
+
const parts = path.split(".");
|
|
432
|
+
let current = target;
|
|
433
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
434
|
+
const key = parts[i];
|
|
435
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
436
|
+
current = current[key];
|
|
437
|
+
}
|
|
438
|
+
current[parts[parts.length - 1]] = value;
|
|
439
|
+
}
|
|
440
|
+
/** Get a nested field by dot-separated path. */
|
|
441
|
+
function getNestedField(source, path) {
|
|
442
|
+
const parts = path.split(".");
|
|
443
|
+
let current = source;
|
|
444
|
+
for (const part of parts) {
|
|
445
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
446
|
+
current = current[part];
|
|
447
|
+
}
|
|
448
|
+
return current;
|
|
449
|
+
}
|
|
450
|
+
/** Prune a single value according to its projected field paths. */
|
|
451
|
+
function pruneValue(value, fields) {
|
|
452
|
+
const arrayFields = [];
|
|
453
|
+
const plainFields = [];
|
|
454
|
+
for (const f of fields) if (f === "$") return value;
|
|
455
|
+
else if (f.startsWith("$.")) arrayFields.push(f.slice(2));
|
|
456
|
+
else plainFields.push(f);
|
|
457
|
+
if (arrayFields.length > 0 && Array.isArray(value)) return value.map((item) => {
|
|
458
|
+
if (typeof item !== "object" || item === null) return item;
|
|
459
|
+
const pruned = {};
|
|
460
|
+
for (const field of arrayFields) {
|
|
461
|
+
const val = getNestedField(item, field);
|
|
462
|
+
if (val !== void 0) setNestedField(pruned, field, val);
|
|
463
|
+
}
|
|
464
|
+
return pruned;
|
|
465
|
+
});
|
|
466
|
+
if (plainFields.length > 0 && typeof value === "object" && value !== null) {
|
|
467
|
+
const source = value;
|
|
468
|
+
const pruned = {};
|
|
469
|
+
for (const field of plainFields) {
|
|
470
|
+
const val = getNestedField(source, field);
|
|
471
|
+
if (val !== void 0) setNestedField(pruned, field, val);
|
|
472
|
+
}
|
|
473
|
+
return pruned;
|
|
474
|
+
}
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
/** Prune data to only include projected fields. Missing projection = keep all. */
|
|
478
|
+
function applyProjection(data, projections) {
|
|
479
|
+
if (!projections) return data;
|
|
480
|
+
const result = {};
|
|
481
|
+
for (const [key, value] of Object.entries(data)) {
|
|
482
|
+
const fields = projections[key];
|
|
483
|
+
if (!fields) result[key] = value;
|
|
484
|
+
else result[key] = pruneValue(value, fields);
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
279
488
|
|
|
280
489
|
//#endregion
|
|
281
490
|
//#region src/page/handler.ts
|
|
282
491
|
/** Execute loaders, returning keyed results */
|
|
283
|
-
async function executeLoaders(loaders, params, procedures) {
|
|
492
|
+
async function executeLoaders(loaders, params, procedures, searchParams) {
|
|
284
493
|
const entries = Object.entries(loaders);
|
|
285
494
|
const results = await Promise.all(entries.map(async ([key, loader]) => {
|
|
286
|
-
const { procedure, input } = loader(params);
|
|
495
|
+
const { procedure, input } = loader(params, searchParams);
|
|
287
496
|
const proc = procedures.get(procedure);
|
|
288
497
|
if (!proc) throw new SeamError("INTERNAL_ERROR", `Procedure '${procedure}' not found`);
|
|
289
|
-
return [key, await proc.handler({
|
|
498
|
+
return [key, await proc.handler({
|
|
499
|
+
input,
|
|
500
|
+
ctx: {}
|
|
501
|
+
})];
|
|
290
502
|
}));
|
|
291
503
|
return Object.fromEntries(results);
|
|
292
504
|
}
|
|
@@ -306,15 +518,16 @@ function lookupMessages(config, routePattern, locale) {
|
|
|
306
518
|
}
|
|
307
519
|
return config.messages[locale]?.[routeHash] ?? {};
|
|
308
520
|
}
|
|
309
|
-
async function handlePageRequest(page, params, procedures, i18nOpts) {
|
|
521
|
+
async function handlePageRequest(page, params, procedures, i18nOpts, searchParams) {
|
|
310
522
|
try {
|
|
311
523
|
const t0 = performance.now();
|
|
312
524
|
const layoutChain = page.layoutChain ?? [];
|
|
313
525
|
const locale = i18nOpts?.locale;
|
|
314
|
-
const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures)), executeLoaders(page.loaders, params, procedures)]);
|
|
526
|
+
const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures, searchParams)), executeLoaders(page.loaders, params, procedures, searchParams)]);
|
|
315
527
|
const t1 = performance.now();
|
|
316
528
|
const allData = {};
|
|
317
529
|
for (const result of loaderResults) Object.assign(allData, result);
|
|
530
|
+
const prunedData = applyProjection(allData, page.projections);
|
|
318
531
|
let composedTemplate = selectTemplate(page.template, page.localeTemplates, locale);
|
|
319
532
|
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
320
533
|
const layout = layoutChain[i];
|
|
@@ -345,7 +558,7 @@ async function handlePageRequest(page, params, procedures, i18nOpts) {
|
|
|
345
558
|
}
|
|
346
559
|
i18nOptsJson = JSON.stringify(i18nData);
|
|
347
560
|
}
|
|
348
|
-
const html = renderPage(composedTemplate, JSON.stringify(
|
|
561
|
+
const html = renderPage(composedTemplate, JSON.stringify(prunedData), JSON.stringify(config), i18nOptsJson);
|
|
349
562
|
const t2 = performance.now();
|
|
350
563
|
return {
|
|
351
564
|
status: 200,
|
|
@@ -517,10 +730,65 @@ function defaultStrategies() {
|
|
|
517
730
|
}
|
|
518
731
|
|
|
519
732
|
//#endregion
|
|
520
|
-
//#region src/router/
|
|
521
|
-
function
|
|
522
|
-
|
|
733
|
+
//#region src/router/categorize.ts
|
|
734
|
+
function resolveKind(name, def) {
|
|
735
|
+
if ("kind" in def && def.kind) return def.kind;
|
|
736
|
+
if ("type" in def && def.type) {
|
|
737
|
+
console.warn(`[seam] "${name}": "type" field in procedure definition is deprecated, use "kind" instead`);
|
|
738
|
+
return def.type;
|
|
739
|
+
}
|
|
740
|
+
return "query";
|
|
741
|
+
}
|
|
742
|
+
/** Split a flat definition map into typed procedure/subscription/stream maps */
|
|
743
|
+
function categorizeProcedures(definitions, contextConfig) {
|
|
744
|
+
const procedureMap = /* @__PURE__ */ new Map();
|
|
745
|
+
const subscriptionMap = /* @__PURE__ */ new Map();
|
|
746
|
+
const streamMap = /* @__PURE__ */ new Map();
|
|
747
|
+
const uploadMap = /* @__PURE__ */ new Map();
|
|
748
|
+
const kindMap = /* @__PURE__ */ new Map();
|
|
749
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
750
|
+
const kind = resolveKind(name, def);
|
|
751
|
+
kindMap.set(name, kind);
|
|
752
|
+
const contextKeys = def.context ?? [];
|
|
753
|
+
if (contextConfig && contextKeys.length > 0) {
|
|
754
|
+
for (const key of contextKeys) if (!(key in contextConfig)) throw new Error(`Procedure "${name}" references undefined context field "${key}"`);
|
|
755
|
+
}
|
|
756
|
+
if (kind === "upload") uploadMap.set(name, {
|
|
757
|
+
inputSchema: def.input._schema,
|
|
758
|
+
outputSchema: def.output._schema,
|
|
759
|
+
contextKeys,
|
|
760
|
+
handler: def.handler
|
|
761
|
+
});
|
|
762
|
+
else if (kind === "stream") streamMap.set(name, {
|
|
763
|
+
inputSchema: def.input._schema,
|
|
764
|
+
chunkOutputSchema: def.output._schema,
|
|
765
|
+
contextKeys,
|
|
766
|
+
handler: def.handler
|
|
767
|
+
});
|
|
768
|
+
else if (kind === "subscription") subscriptionMap.set(name, {
|
|
769
|
+
inputSchema: def.input._schema,
|
|
770
|
+
outputSchema: def.output._schema,
|
|
771
|
+
contextKeys,
|
|
772
|
+
handler: def.handler
|
|
773
|
+
});
|
|
774
|
+
else procedureMap.set(name, {
|
|
775
|
+
inputSchema: def.input._schema,
|
|
776
|
+
outputSchema: def.output._schema,
|
|
777
|
+
contextKeys,
|
|
778
|
+
handler: def.handler
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
procedureMap,
|
|
783
|
+
subscriptionMap,
|
|
784
|
+
streamMap,
|
|
785
|
+
uploadMap,
|
|
786
|
+
kindMap
|
|
787
|
+
};
|
|
523
788
|
}
|
|
789
|
+
|
|
790
|
+
//#endregion
|
|
791
|
+
//#region src/router/index.ts
|
|
524
792
|
/** Build the resolve strategy list from options */
|
|
525
793
|
function buildStrategies(opts) {
|
|
526
794
|
const strategies = opts?.resolve ?? defaultStrategies();
|
|
@@ -534,6 +802,7 @@ function registerI18nQuery(procedureMap, config) {
|
|
|
534
802
|
procedureMap.set("__seam_i18n_query", {
|
|
535
803
|
inputSchema: {},
|
|
536
804
|
outputSchema: {},
|
|
805
|
+
contextKeys: [],
|
|
537
806
|
handler: ({ input }) => {
|
|
538
807
|
const { route, locale } = input;
|
|
539
808
|
const messages = lookupI18nMessages(config, route, locale);
|
|
@@ -553,19 +822,72 @@ function lookupI18nMessages(config, routeHash, locale) {
|
|
|
553
822
|
}
|
|
554
823
|
return config.messages[locale]?.[routeHash] ?? {};
|
|
555
824
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
825
|
+
/** Collect channel metadata from channel results for manifest */
|
|
826
|
+
function collectChannelMeta(channels) {
|
|
827
|
+
if (!channels || channels.length === 0) return void 0;
|
|
828
|
+
return Object.fromEntries(channels.map((ch) => {
|
|
829
|
+
const firstKey = Object.keys(ch.procedures)[0] ?? "";
|
|
830
|
+
return [firstKey.includes(".") ? firstKey.slice(0, firstKey.indexOf(".")) : firstKey, ch.channelMeta];
|
|
831
|
+
}));
|
|
832
|
+
}
|
|
833
|
+
/** Resolve context for a procedure, returning undefined if no context needed */
|
|
834
|
+
function resolveCtxFor(map, name, rawCtx, extractKeys, ctxConfig) {
|
|
835
|
+
if (!rawCtx || extractKeys.length === 0) return void 0;
|
|
836
|
+
const proc = map.get(name);
|
|
837
|
+
if (!proc || proc.contextKeys.length === 0) return void 0;
|
|
838
|
+
return resolveContext(ctxConfig, rawCtx, proc.contextKeys);
|
|
839
|
+
}
|
|
840
|
+
/** Resolve locale and match page route */
|
|
841
|
+
async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers) {
|
|
842
|
+
let pathLocale = null;
|
|
843
|
+
let routePath = path;
|
|
844
|
+
if (hasUrlPrefix && i18nConfig) {
|
|
845
|
+
const segments = path.split("/").filter(Boolean);
|
|
846
|
+
const localeSet = new Set(i18nConfig.locales);
|
|
847
|
+
const first = segments[0];
|
|
848
|
+
if (first && localeSet.has(first)) {
|
|
849
|
+
pathLocale = first;
|
|
850
|
+
routePath = "/" + segments.slice(1).join("/") || "/";
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
let locale;
|
|
854
|
+
if (i18nConfig) locale = resolveChain(strategies, {
|
|
855
|
+
url: headers?.url ?? "",
|
|
856
|
+
pathLocale,
|
|
857
|
+
cookie: headers?.cookie,
|
|
858
|
+
acceptLanguage: headers?.acceptLanguage,
|
|
859
|
+
locales: i18nConfig.locales,
|
|
860
|
+
defaultLocale: i18nConfig.default
|
|
568
861
|
});
|
|
862
|
+
const match = pageMatcher.match(routePath);
|
|
863
|
+
if (!match) return null;
|
|
864
|
+
let searchParams;
|
|
865
|
+
if (headers?.url) try {
|
|
866
|
+
const url = new URL(headers.url, "http://localhost");
|
|
867
|
+
if (url.search) searchParams = url.searchParams;
|
|
868
|
+
} catch {}
|
|
869
|
+
const i18nOpts = locale && i18nConfig ? {
|
|
870
|
+
locale,
|
|
871
|
+
config: i18nConfig,
|
|
872
|
+
routePattern: match.pattern
|
|
873
|
+
} : void 0;
|
|
874
|
+
return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams);
|
|
875
|
+
}
|
|
876
|
+
/** Catch context resolution errors and return them as HandleResult */
|
|
877
|
+
function resolveCtxSafe(map, name, rawCtx, extractKeys, ctxConfig) {
|
|
878
|
+
try {
|
|
879
|
+
return { ctx: resolveCtxFor(map, name, rawCtx, extractKeys, ctxConfig) };
|
|
880
|
+
} catch (err) {
|
|
881
|
+
if (err instanceof SeamError) return { error: {
|
|
882
|
+
status: err.status,
|
|
883
|
+
body: err.toJSON()
|
|
884
|
+
} };
|
|
885
|
+
throw err;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function createRouter(procedures, opts) {
|
|
889
|
+
const ctxConfig = opts?.context ?? {};
|
|
890
|
+
const { procedureMap, subscriptionMap, streamMap, uploadMap, kindMap } = categorizeProcedures(procedures, Object.keys(ctxConfig).length > 0 ? ctxConfig : void 0);
|
|
569
891
|
const shouldValidateOutput = opts?.validateOutput ?? (typeof process !== "undefined" && process.env.NODE_ENV !== "production");
|
|
570
892
|
const pageMatcher = new RouteMatcher();
|
|
571
893
|
const pages = opts?.pages;
|
|
@@ -573,54 +895,41 @@ function createRouter(procedures, opts) {
|
|
|
573
895
|
const i18nConfig = opts?.i18n ?? null;
|
|
574
896
|
const { strategies, hasUrlPrefix } = buildStrategies(opts);
|
|
575
897
|
if (i18nConfig) registerI18nQuery(procedureMap, i18nConfig);
|
|
576
|
-
const channelsMeta = opts?.channels
|
|
577
|
-
|
|
578
|
-
return [firstKey.includes(".") ? firstKey.slice(0, firstKey.indexOf(".")) : firstKey, ch.channelMeta];
|
|
579
|
-
})) : void 0;
|
|
898
|
+
const channelsMeta = collectChannelMeta(opts?.channels);
|
|
899
|
+
const extractKeys = contextExtractKeys(ctxConfig);
|
|
580
900
|
return {
|
|
581
901
|
procedures,
|
|
582
902
|
hasPages: !!pages && Object.keys(pages).length > 0,
|
|
903
|
+
contextExtractKeys() {
|
|
904
|
+
return extractKeys;
|
|
905
|
+
},
|
|
583
906
|
manifest() {
|
|
584
|
-
return buildManifest(procedures, channelsMeta);
|
|
907
|
+
return buildManifest(procedures, channelsMeta, ctxConfig, opts?.transportDefaults);
|
|
585
908
|
},
|
|
586
|
-
handle(procedureName, body) {
|
|
587
|
-
|
|
909
|
+
async handle(procedureName, body, rawCtx) {
|
|
910
|
+
const { ctx, error } = resolveCtxSafe(procedureMap, procedureName, rawCtx, extractKeys, ctxConfig);
|
|
911
|
+
if (error) return error;
|
|
912
|
+
return handleRequest(procedureMap, procedureName, body, shouldValidateOutput, ctx);
|
|
588
913
|
},
|
|
589
|
-
handleBatch(calls) {
|
|
590
|
-
return handleBatchRequest(procedureMap, calls, shouldValidateOutput);
|
|
914
|
+
handleBatch(calls, rawCtx) {
|
|
915
|
+
return handleBatchRequest(procedureMap, calls, shouldValidateOutput, rawCtx ? (name) => resolveCtxFor(procedureMap, name, rawCtx, extractKeys, ctxConfig) ?? {} : void 0);
|
|
591
916
|
},
|
|
592
|
-
handleSubscription(name, input) {
|
|
593
|
-
return handleSubscription(subscriptionMap, name, input, shouldValidateOutput);
|
|
917
|
+
handleSubscription(name, input, rawCtx) {
|
|
918
|
+
return handleSubscription(subscriptionMap, name, input, shouldValidateOutput, resolveCtxFor(subscriptionMap, name, rawCtx, extractKeys, ctxConfig));
|
|
594
919
|
},
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (i18nConfig) locale = resolveChain(strategies, {
|
|
609
|
-
url: headers?.url ?? "",
|
|
610
|
-
pathLocale,
|
|
611
|
-
cookie: headers?.cookie,
|
|
612
|
-
acceptLanguage: headers?.acceptLanguage,
|
|
613
|
-
locales: i18nConfig.locales,
|
|
614
|
-
defaultLocale: i18nConfig.default
|
|
615
|
-
});
|
|
616
|
-
const match = pageMatcher.match(routePath);
|
|
617
|
-
if (!match) return null;
|
|
618
|
-
const i18nOpts = locale && i18nConfig ? {
|
|
619
|
-
locale,
|
|
620
|
-
config: i18nConfig,
|
|
621
|
-
routePattern: match.pattern
|
|
622
|
-
} : void 0;
|
|
623
|
-
return handlePageRequest(match.value, match.params, procedureMap, i18nOpts);
|
|
920
|
+
handleStream(name, input, rawCtx) {
|
|
921
|
+
return handleStream(streamMap, name, input, shouldValidateOutput, resolveCtxFor(streamMap, name, rawCtx, extractKeys, ctxConfig));
|
|
922
|
+
},
|
|
923
|
+
async handleUpload(name, body, file, rawCtx) {
|
|
924
|
+
const { ctx, error } = resolveCtxSafe(uploadMap, name, rawCtx, extractKeys, ctxConfig);
|
|
925
|
+
if (error) return error;
|
|
926
|
+
return handleUploadRequest(uploadMap, name, body, file, shouldValidateOutput, ctx);
|
|
927
|
+
},
|
|
928
|
+
getKind(name) {
|
|
929
|
+
return kindMap.get(name) ?? null;
|
|
930
|
+
},
|
|
931
|
+
handlePage(path, headers) {
|
|
932
|
+
return matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers);
|
|
624
933
|
}
|
|
625
934
|
};
|
|
626
935
|
}
|
|
@@ -661,7 +970,7 @@ function createChannel(name, def) {
|
|
|
661
970
|
const channelInputSchema = def.input._schema;
|
|
662
971
|
for (const [msgName, msgDef] of Object.entries(def.incoming)) {
|
|
663
972
|
const command = {
|
|
664
|
-
|
|
973
|
+
kind: "command",
|
|
665
974
|
input: { _schema: mergeObjectSchemas(channelInputSchema, msgDef.input._schema) },
|
|
666
975
|
output: msgDef.output,
|
|
667
976
|
handler: msgDef.handler
|
|
@@ -671,7 +980,7 @@ function createChannel(name, def) {
|
|
|
671
980
|
}
|
|
672
981
|
const unionSchema = buildOutgoingUnionSchema(def.outgoing);
|
|
673
982
|
const subscription = {
|
|
674
|
-
|
|
983
|
+
kind: "subscription",
|
|
675
984
|
input: def.input,
|
|
676
985
|
output: { _schema: unionSchema },
|
|
677
986
|
handler: def.subscribe
|
|
@@ -688,13 +997,15 @@ function createChannel(name, def) {
|
|
|
688
997
|
}
|
|
689
998
|
const outgoingMeta = {};
|
|
690
999
|
for (const [eventName, node] of Object.entries(def.outgoing)) outgoingMeta[eventName] = node._schema;
|
|
1000
|
+
const channelMeta = {
|
|
1001
|
+
input: channelInputSchema,
|
|
1002
|
+
incoming: incomingMeta,
|
|
1003
|
+
outgoing: outgoingMeta
|
|
1004
|
+
};
|
|
1005
|
+
if (def.transport) channelMeta.transport = def.transport;
|
|
691
1006
|
return {
|
|
692
1007
|
procedures,
|
|
693
|
-
channelMeta
|
|
694
|
-
input: channelInputSchema,
|
|
695
|
-
incoming: incomingMeta,
|
|
696
|
-
outgoing: outgoingMeta
|
|
697
|
-
}
|
|
1008
|
+
channelMeta
|
|
698
1009
|
};
|
|
699
1010
|
}
|
|
700
1011
|
|
|
@@ -775,6 +1086,10 @@ async function handleStaticAsset(assetPath, staticDir) {
|
|
|
775
1086
|
function sseDataEvent(data) {
|
|
776
1087
|
return `event: data\ndata: ${JSON.stringify(data)}\n\n`;
|
|
777
1088
|
}
|
|
1089
|
+
/** Format an SSE data event with a sequence id (for streams) */
|
|
1090
|
+
function sseDataEventWithId(data, id) {
|
|
1091
|
+
return `event: data\nid: ${id}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
1092
|
+
}
|
|
778
1093
|
/** Format an SSE error event */
|
|
779
1094
|
function sseErrorEvent(code, message, transient = false) {
|
|
780
1095
|
return `event: error\ndata: ${JSON.stringify({
|
|
@@ -787,16 +1102,30 @@ function sseErrorEvent(code, message, transient = false) {
|
|
|
787
1102
|
function sseCompleteEvent() {
|
|
788
1103
|
return "event: complete\ndata: {}\n\n";
|
|
789
1104
|
}
|
|
790
|
-
async function* sseStream(router, name, input) {
|
|
1105
|
+
async function* sseStream(router, name, input, rawCtx) {
|
|
1106
|
+
try {
|
|
1107
|
+
for await (const value of router.handleSubscription(name, input, rawCtx)) yield sseDataEvent(value);
|
|
1108
|
+
yield sseCompleteEvent();
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
if (error instanceof SeamError) yield sseErrorEvent(error.code, error.message);
|
|
1111
|
+
else yield sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async function* sseStreamForStream(router, name, input, signal, rawCtx) {
|
|
1115
|
+
const gen = router.handleStream(name, input, rawCtx);
|
|
1116
|
+
if (signal) signal.addEventListener("abort", () => {
|
|
1117
|
+
gen.return(void 0);
|
|
1118
|
+
}, { once: true });
|
|
791
1119
|
try {
|
|
792
|
-
|
|
1120
|
+
let seq = 0;
|
|
1121
|
+
for await (const value of gen) yield sseDataEventWithId(value, seq++);
|
|
793
1122
|
yield sseCompleteEvent();
|
|
794
1123
|
} catch (error) {
|
|
795
1124
|
if (error instanceof SeamError) yield sseErrorEvent(error.code, error.message);
|
|
796
1125
|
else yield sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
|
|
797
1126
|
}
|
|
798
1127
|
}
|
|
799
|
-
async function handleBatchHttp(req, router, hashToName) {
|
|
1128
|
+
async function handleBatchHttp(req, router, hashToName, rawCtx) {
|
|
800
1129
|
let body;
|
|
801
1130
|
try {
|
|
802
1131
|
body = await req.body();
|
|
@@ -810,45 +1139,62 @@ async function handleBatchHttp(req, router, hashToName) {
|
|
|
810
1139
|
}));
|
|
811
1140
|
return jsonResponse(200, {
|
|
812
1141
|
ok: true,
|
|
813
|
-
data: await router.handleBatch(calls)
|
|
1142
|
+
data: await router.handleBatch(calls, rawCtx)
|
|
814
1143
|
});
|
|
815
1144
|
}
|
|
1145
|
+
/** Resolve hash -> original name when obfuscation is active. Returns null on miss. */
|
|
1146
|
+
function resolveHashName(hashToName, name) {
|
|
1147
|
+
if (!hashToName) return name;
|
|
1148
|
+
return hashToName.get(name) ?? null;
|
|
1149
|
+
}
|
|
816
1150
|
function createHttpHandler(router, opts) {
|
|
817
1151
|
const hashToName = opts?.rpcHashMap ? new Map(Object.entries(opts.rpcHashMap.procedures).map(([n, h]) => [h, n])) : null;
|
|
818
1152
|
if (hashToName) hashToName.set("__seam_i18n_query", "__seam_i18n_query");
|
|
819
1153
|
const batchHash = opts?.rpcHashMap?.batch ?? null;
|
|
1154
|
+
const ctxExtractKeys = router.contextExtractKeys();
|
|
820
1155
|
return async (req) => {
|
|
821
1156
|
const url = new URL(req.url, "http://localhost");
|
|
822
1157
|
const { pathname } = url;
|
|
1158
|
+
const rawCtx = ctxExtractKeys.length > 0 && req.header ? Object.fromEntries(ctxExtractKeys.map((k) => [k, req.header?.(k) ?? null])) : void 0;
|
|
823
1159
|
if (req.method === "GET" && pathname === MANIFEST_PATH) {
|
|
824
1160
|
if (opts?.rpcHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
|
|
825
1161
|
return jsonResponse(200, router.manifest());
|
|
826
1162
|
}
|
|
827
1163
|
if (pathname.startsWith(PROCEDURE_PREFIX)) {
|
|
828
|
-
|
|
829
|
-
if (!
|
|
1164
|
+
const rawName = pathname.slice(17);
|
|
1165
|
+
if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
|
|
830
1166
|
if (req.method === "POST") {
|
|
831
|
-
if (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
if (!resolved) return errorResponse(404, "NOT_FOUND", "Not found");
|
|
835
|
-
name = resolved;
|
|
836
|
-
}
|
|
1167
|
+
if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
|
|
1168
|
+
const name = resolveHashName(hashToName, rawName);
|
|
1169
|
+
if (!name) return errorResponse(404, "NOT_FOUND", "Not found");
|
|
837
1170
|
let body;
|
|
838
1171
|
try {
|
|
839
1172
|
body = await req.body();
|
|
840
1173
|
} catch {
|
|
841
1174
|
return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
|
|
842
1175
|
}
|
|
843
|
-
|
|
1176
|
+
if (router.getKind(name) === "stream") {
|
|
1177
|
+
const controller = new AbortController();
|
|
1178
|
+
return {
|
|
1179
|
+
status: 200,
|
|
1180
|
+
headers: SSE_HEADER,
|
|
1181
|
+
stream: sseStreamForStream(router, name, body, controller.signal, rawCtx),
|
|
1182
|
+
onCancel: () => controller.abort()
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
if (router.getKind(name) === "upload") {
|
|
1186
|
+
if (!req.file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires multipart/form-data");
|
|
1187
|
+
const file = await req.file();
|
|
1188
|
+
if (!file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires file in multipart body");
|
|
1189
|
+
const result = await router.handleUpload(name, body, file, rawCtx);
|
|
1190
|
+
return jsonResponse(result.status, result.body);
|
|
1191
|
+
}
|
|
1192
|
+
const result = await router.handle(name, body, rawCtx);
|
|
844
1193
|
return jsonResponse(result.status, result.body);
|
|
845
1194
|
}
|
|
846
1195
|
if (req.method === "GET") {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (!resolved) return errorResponse(404, "NOT_FOUND", "Not found");
|
|
850
|
-
name = resolved;
|
|
851
|
-
}
|
|
1196
|
+
const name = resolveHashName(hashToName, rawName);
|
|
1197
|
+
if (!name) return errorResponse(404, "NOT_FOUND", "Not found");
|
|
852
1198
|
const rawInput = url.searchParams.get("input");
|
|
853
1199
|
let input;
|
|
854
1200
|
try {
|
|
@@ -859,7 +1205,7 @@ function createHttpHandler(router, opts) {
|
|
|
859
1205
|
return {
|
|
860
1206
|
status: 200,
|
|
861
1207
|
headers: SSE_HEADER,
|
|
862
|
-
stream: sseStream(router, name, input)
|
|
1208
|
+
stream: sseStream(router, name, input, rawCtx)
|
|
863
1209
|
};
|
|
864
1210
|
}
|
|
865
1211
|
}
|
|
@@ -895,13 +1241,19 @@ async function drainStream(stream, write) {
|
|
|
895
1241
|
function toWebResponse(result) {
|
|
896
1242
|
if ("stream" in result) {
|
|
897
1243
|
const stream = result.stream;
|
|
1244
|
+
const onCancel = result.onCancel;
|
|
898
1245
|
const encoder = new TextEncoder();
|
|
899
|
-
const readable = new ReadableStream({
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1246
|
+
const readable = new ReadableStream({
|
|
1247
|
+
async start(controller) {
|
|
1248
|
+
await drainStream(stream, (chunk) => {
|
|
1249
|
+
controller.enqueue(encoder.encode(chunk));
|
|
1250
|
+
});
|
|
1251
|
+
controller.close();
|
|
1252
|
+
},
|
|
1253
|
+
cancel() {
|
|
1254
|
+
onCancel?.();
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
905
1257
|
return new Response(readable, {
|
|
906
1258
|
status: result.status,
|
|
907
1259
|
headers: result.headers
|
|
@@ -916,11 +1268,11 @@ function toWebResponse(result) {
|
|
|
916
1268
|
//#endregion
|
|
917
1269
|
//#region src/page/build-loader.ts
|
|
918
1270
|
function buildLoaderFn(config) {
|
|
919
|
-
return (params) => {
|
|
1271
|
+
return (params, searchParams) => {
|
|
920
1272
|
const input = {};
|
|
921
1273
|
if (config.params) for (const [key, mapping] of Object.entries(config.params)) {
|
|
922
|
-
const raw = params[key];
|
|
923
|
-
input[key] = mapping.type === "int" ? Number(raw) : raw;
|
|
1274
|
+
const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
|
|
1275
|
+
if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
|
|
924
1276
|
}
|
|
925
1277
|
return {
|
|
926
1278
|
procedure: config.procedure,
|
|
@@ -951,42 +1303,19 @@ function loadLocaleTemplates(entry, distDir) {
|
|
|
951
1303
|
return result;
|
|
952
1304
|
}
|
|
953
1305
|
/** Resolve parent chain for a layout, returning outer-to-inner order */
|
|
954
|
-
function resolveLayoutChain(layoutId, layoutEntries,
|
|
1306
|
+
function resolveLayoutChain(layoutId, layoutEntries, getTemplates) {
|
|
955
1307
|
const chain = [];
|
|
956
1308
|
let currentId = layoutId;
|
|
957
1309
|
while (currentId) {
|
|
958
1310
|
const entry = layoutEntries[currentId];
|
|
959
1311
|
if (!entry) break;
|
|
1312
|
+
const { template, localeTemplates } = getTemplates(currentId, entry);
|
|
960
1313
|
chain.push({
|
|
961
1314
|
id: currentId,
|
|
962
|
-
template
|
|
963
|
-
localeTemplates
|
|
964
|
-
loaders: buildLoaderFns(entry.loaders ?? {})
|
|
965
|
-
});
|
|
966
|
-
currentId = entry.parent;
|
|
967
|
-
}
|
|
968
|
-
chain.reverse();
|
|
969
|
-
return chain;
|
|
970
|
-
}
|
|
971
|
-
/** Resolve layout chain with lazy template getters (re-read from disk on each access) */
|
|
972
|
-
function resolveLayoutChainDev(layoutId, layoutEntries, distDir, defaultLocale) {
|
|
973
|
-
const chain = [];
|
|
974
|
-
let currentId = layoutId;
|
|
975
|
-
while (currentId) {
|
|
976
|
-
const entry = layoutEntries[currentId];
|
|
977
|
-
if (!entry) break;
|
|
978
|
-
const layoutTemplatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
|
|
979
|
-
const def = {
|
|
980
|
-
id: currentId,
|
|
981
|
-
template: "",
|
|
982
|
-
localeTemplates: entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0,
|
|
1315
|
+
template,
|
|
1316
|
+
localeTemplates,
|
|
983
1317
|
loaders: buildLoaderFns(entry.loaders ?? {})
|
|
984
|
-
};
|
|
985
|
-
Object.defineProperty(def, "template", {
|
|
986
|
-
get: () => readFileSync(layoutTemplatePath, "utf-8"),
|
|
987
|
-
enumerable: true
|
|
988
1318
|
});
|
|
989
|
-
chain.push(def);
|
|
990
1319
|
currentId = entry.parent;
|
|
991
1320
|
}
|
|
992
1321
|
chain.reverse();
|
|
@@ -1077,7 +1406,10 @@ function loadBuildOutput(distDir) {
|
|
|
1077
1406
|
for (const [path, entry] of Object.entries(manifest.routes)) {
|
|
1078
1407
|
const template = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
|
|
1079
1408
|
const loaders = buildLoaderFns(entry.loaders);
|
|
1080
|
-
const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries,
|
|
1409
|
+
const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id) => ({
|
|
1410
|
+
template: layoutTemplates[id] ?? "",
|
|
1411
|
+
localeTemplates: layoutLocaleTemplates[id]
|
|
1412
|
+
})) : [];
|
|
1081
1413
|
const i18nKeys = mergeI18nKeys(entry, layoutEntries);
|
|
1082
1414
|
pages[path] = {
|
|
1083
1415
|
template,
|
|
@@ -1087,7 +1419,8 @@ function loadBuildOutput(distDir) {
|
|
|
1087
1419
|
headMeta: entry.head_meta,
|
|
1088
1420
|
dataId: manifest.data_id,
|
|
1089
1421
|
i18nKeys,
|
|
1090
|
-
pageAssets: entry.assets
|
|
1422
|
+
pageAssets: entry.assets,
|
|
1423
|
+
projections: entry.projections
|
|
1091
1424
|
};
|
|
1092
1425
|
}
|
|
1093
1426
|
return pages;
|
|
@@ -1102,7 +1435,18 @@ function loadBuildOutputDev(distDir) {
|
|
|
1102
1435
|
for (const [path, entry] of Object.entries(manifest.routes)) {
|
|
1103
1436
|
const templatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
|
|
1104
1437
|
const loaders = buildLoaderFns(entry.loaders);
|
|
1105
|
-
const layoutChain = entry.layout ?
|
|
1438
|
+
const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id, layoutEntry) => {
|
|
1439
|
+
const tmplPath = join(distDir, resolveTemplatePath(layoutEntry, defaultLocale));
|
|
1440
|
+
const def = {
|
|
1441
|
+
template: "",
|
|
1442
|
+
localeTemplates: layoutEntry.templates ? makeLocaleTemplateGetters(layoutEntry.templates, distDir) : void 0
|
|
1443
|
+
};
|
|
1444
|
+
Object.defineProperty(def, "template", {
|
|
1445
|
+
get: () => readFileSync(tmplPath, "utf-8"),
|
|
1446
|
+
enumerable: true
|
|
1447
|
+
});
|
|
1448
|
+
return def;
|
|
1449
|
+
}) : [];
|
|
1106
1450
|
const localeTemplates = entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0;
|
|
1107
1451
|
const i18nKeys = mergeI18nKeys(entry, layoutEntries);
|
|
1108
1452
|
const page = {
|
|
@@ -1112,7 +1456,8 @@ function loadBuildOutputDev(distDir) {
|
|
|
1112
1456
|
layoutChain,
|
|
1113
1457
|
dataId: manifest.data_id,
|
|
1114
1458
|
i18nKeys,
|
|
1115
|
-
pageAssets: entry.assets
|
|
1459
|
+
pageAssets: entry.assets,
|
|
1460
|
+
projections: entry.projections
|
|
1116
1461
|
};
|
|
1117
1462
|
Object.defineProperty(page, "template", {
|
|
1118
1463
|
get: () => readFileSync(templatePath, "utf-8"),
|
|
@@ -1361,26 +1706,58 @@ function createStaticHandler(opts) {
|
|
|
1361
1706
|
function watchReloadTrigger(distDir, onReload) {
|
|
1362
1707
|
const triggerPath = join(distDir, ".reload-trigger");
|
|
1363
1708
|
let watcher = null;
|
|
1709
|
+
let closed = false;
|
|
1710
|
+
let pending = [];
|
|
1711
|
+
const notify = () => {
|
|
1712
|
+
onReload();
|
|
1713
|
+
const batch = pending;
|
|
1714
|
+
pending = [];
|
|
1715
|
+
for (const p of batch) p.resolve();
|
|
1716
|
+
};
|
|
1717
|
+
const nextReload = () => {
|
|
1718
|
+
if (closed) return Promise.reject(/* @__PURE__ */ new Error("watcher closed"));
|
|
1719
|
+
return new Promise((resolve, reject) => {
|
|
1720
|
+
pending.push({
|
|
1721
|
+
resolve,
|
|
1722
|
+
reject
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
};
|
|
1726
|
+
const closeAll = () => {
|
|
1727
|
+
closed = true;
|
|
1728
|
+
const batch = pending;
|
|
1729
|
+
pending = [];
|
|
1730
|
+
const err = /* @__PURE__ */ new Error("watcher closed");
|
|
1731
|
+
for (const p of batch) p.reject(err);
|
|
1732
|
+
};
|
|
1364
1733
|
try {
|
|
1365
|
-
watcher = watch(triggerPath, () =>
|
|
1734
|
+
watcher = watch(triggerPath, () => notify());
|
|
1366
1735
|
} catch {
|
|
1367
1736
|
const dirWatcher = watch(distDir, (_event, filename) => {
|
|
1368
1737
|
if (filename === ".reload-trigger") {
|
|
1369
1738
|
dirWatcher.close();
|
|
1370
|
-
watcher = watch(triggerPath, () =>
|
|
1371
|
-
|
|
1739
|
+
watcher = watch(triggerPath, () => notify());
|
|
1740
|
+
notify();
|
|
1372
1741
|
}
|
|
1373
1742
|
});
|
|
1374
|
-
return {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1743
|
+
return {
|
|
1744
|
+
close() {
|
|
1745
|
+
dirWatcher.close();
|
|
1746
|
+
watcher?.close();
|
|
1747
|
+
closeAll();
|
|
1748
|
+
},
|
|
1749
|
+
nextReload
|
|
1750
|
+
};
|
|
1378
1751
|
}
|
|
1379
|
-
return {
|
|
1380
|
-
|
|
1381
|
-
|
|
1752
|
+
return {
|
|
1753
|
+
close() {
|
|
1754
|
+
watcher?.close();
|
|
1755
|
+
closeAll();
|
|
1756
|
+
},
|
|
1757
|
+
nextReload
|
|
1758
|
+
};
|
|
1382
1759
|
}
|
|
1383
1760
|
|
|
1384
1761
|
//#endregion
|
|
1385
|
-
export { SeamError, createChannel, createDevProxy, createHttpHandler, createRouter, createStaticHandler, defaultStrategies, definePage, drainStream, fromAcceptLanguage, fromCallback, fromCookie, fromUrlPrefix, fromUrlQuery, loadBuildOutput, loadBuildOutputDev, loadI18nMessages, loadRpcHashMap, resolveChain, serialize, sseCompleteEvent, sseDataEvent, sseErrorEvent, startChannelWs, t, toWebResponse, watchReloadTrigger };
|
|
1762
|
+
export { SeamError, createChannel, createDevProxy, createHttpHandler, createRouter, createStaticHandler, defaultStrategies, definePage, drainStream, fromAcceptLanguage, fromCallback, fromCookie, fromUrlPrefix, fromUrlQuery, loadBuildOutput, loadBuildOutputDev, loadI18nMessages, loadRpcHashMap, resolveChain, serialize, sseCompleteEvent, sseDataEvent, sseDataEventWithId, sseErrorEvent, startChannelWs, t, toWebResponse, watchReloadTrigger };
|
|
1386
1763
|
//# sourceMappingURL=index.js.map
|