@solidjs/web 2.0.0-beta.26 → 2.0.0-beta.28

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.
Files changed (43) hide show
  1. package/dist/dev.cjs +3 -1
  2. package/dist/dev.js +3 -2
  3. package/dist/server.cjs +64 -84
  4. package/dist/server.js +65 -86
  5. package/dist/web.cjs +3 -1
  6. package/dist/web.js +3 -2
  7. package/frames/dist/client.cjs +69 -21
  8. package/frames/dist/client.dev.cjs +69 -21
  9. package/frames/dist/client.dev.js +70 -22
  10. package/frames/dist/client.js +70 -22
  11. package/frames/dist/server.cjs +55 -50
  12. package/frames/dist/server.js +55 -50
  13. package/package.json +4 -4
  14. package/serialization/dist/serialization.cjs +6 -3
  15. package/serialization/dist/serialization.js +6 -3
  16. package/serialization/types/index.d.ts +7 -1
  17. package/serialization/types-cjs/index.d.cts +7 -1
  18. package/server-functions/dist/client.cjs +28 -2
  19. package/server-functions/dist/client.js +25 -3
  20. package/server-functions/dist/server.cjs +194 -5
  21. package/server-functions/dist/server.js +187 -6
  22. package/types/client.d.ts +3 -3
  23. package/types/frames/client.d.ts +1 -0
  24. package/types/frames/serializer.d.ts +7 -1
  25. package/types/frames/server.d.ts +28 -0
  26. package/types/jsx.d.ts +1 -1
  27. package/types/response.d.ts +10 -0
  28. package/types/serializer.d.ts +7 -1
  29. package/types/server-functions/client.d.ts +4 -0
  30. package/types/server-functions/flash.d.ts +38 -0
  31. package/types/server-functions/server.d.ts +109 -6
  32. package/types/server-functions/shared.d.ts +48 -0
  33. package/types-cjs/client.d.cts +3 -3
  34. package/types-cjs/frames/client.d.cts +1 -0
  35. package/types-cjs/frames/serializer.d.cts +7 -1
  36. package/types-cjs/frames/server.d.cts +28 -0
  37. package/types-cjs/jsx.d.cts +1 -1
  38. package/types-cjs/response.d.cts +10 -0
  39. package/types-cjs/serializer.d.cts +7 -1
  40. package/types-cjs/server-functions/client.d.cts +4 -0
  41. package/types-cjs/server-functions/flash.d.cts +38 -0
  42. package/types-cjs/server-functions/server.d.cts +109 -6
  43. package/types-cjs/server-functions/shared.d.cts +48 -0
@@ -1,6 +1,8 @@
1
1
  import { fromCrossJSON, Feature } from 'seroval';
2
2
  import { AbortSignalPlugin, CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLSearchParamsPlugin, URLPlugin } from 'seroval-plugins/web';
3
3
 
4
+ const REVALIDATE_HEADER = "X-Revalidate";
5
+
4
6
  Feature.AggregateError | Feature.BigIntTypedArray;
5
7
  const DEFAULT_WEB_PLUGINS = Object.freeze([AbortSignalPlugin,
6
8
  CustomEventPlugin, DOMExceptionPlugin, EventPlugin,
@@ -96,6 +98,14 @@ const INSTANCE_HEADER = "X-Server-Function-Instance";
96
98
  const BODY_FORMAT_HEADER = "X-Server-Function-Format";
97
99
  const SINGLE_FLIGHT_HEADER = "X-Single-Flight";
98
100
  const FILE_FORM_KEY = "__server_function_file__";
101
+ const FLASH_COOKIE = "flash";
102
+ const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
103
+ function hasFlashCookie(cookieHeader) {
104
+ return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
105
+ }
106
+ function clearFlashCookie() {
107
+ return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
108
+ }
99
109
  const BodyFormat = {
100
110
  Serialized: "0",
101
111
  String: "1",
@@ -275,6 +285,18 @@ async function decodeResponse(response, codecOptions) {
275
285
  if (!response.body) return undefined;
276
286
  return await extractBody(response, codecOptions === undefined ? codecConfig.codec : codecOptions);
277
287
  }
288
+ async function decodeResponsePayload(response, codecOptions) {
289
+ const decoded = await decodeResponse(response, codecOptions);
290
+ if (decoded !== undefined && response.headers.has(SINGLE_FLIGHT_HEADER)) {
291
+ return {
292
+ value: decoded.value,
293
+ flightData: decoded.data
294
+ };
295
+ }
296
+ return {
297
+ value: decoded
298
+ };
299
+ }
278
300
 
279
301
  const config = {
280
302
  endpoint: "/_server",
@@ -401,13 +423,13 @@ async function fetchServerFunction(base, id, options, args, meta) {
401
423
  await consumer(payload.data, {
402
424
  response
403
425
  });
404
- if (response.headers.has(ERROR_HEADER) && !response.headers.has("Location") && !response.headers.has("X-Revalidate")) {
426
+ if (response.headers.has(ERROR_HEADER) && !response.headers.has("Location") && !response.headers.has(REVALIDATE_HEADER)) {
405
427
  throw payload.value;
406
428
  }
407
429
  return payload.value;
408
430
  }
409
431
  }
410
- if (response.headers.has("Location") || response.headers.has("X-Revalidate") || response.headers.has(SINGLE_FLIGHT_HEADER)) {
432
+ if (response.headers.has("Location") || response.headers.has(REVALIDATE_HEADER) || response.headers.has(SINGLE_FLIGHT_HEADER)) {
411
433
  return response;
412
434
  }
413
435
  const result = await decodeResponse(response.clone());
@@ -484,4 +506,4 @@ function registerServerReference() {
484
506
  throw new Error("registerServerReference must not be called in the client build");
485
507
  }
486
508
 
487
- export { ERROR_HEADER, FUNCTION_HEADER, GET, INSTANCE_HEADER, SINGLE_FLIGHT_HEADER, configureServerFunctionsClient, createServerReference, decodeErrorHeaderValue, decodeResponse, encodeErrorHeaderValue, getServerFunctionMetadata, isServerFunction, registerServerReference, subscribeFlightData, withMeta };
509
+ export { ERROR_HEADER, FLASH_COOKIE, FUNCTION_HEADER, GET, INSTANCE_HEADER, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsClient, createServerReference, decodeErrorHeaderValue, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, getServerFunctionMetadata, hasFlashCookie, isServerFunction, registerServerReference, subscribeFlightData, withMeta };
@@ -8,8 +8,10 @@ const ENVELOPE = Symbol.for("solid.ResponseEnvelope");
8
8
  function isResponseEnvelope(value) {
9
9
  return !!(value && typeof value === "object" && value[ENVELOPE]);
10
10
  }
11
+ const REVALIDATE_HEADER = "X-Revalidate";
11
12
 
12
13
  seroval.Feature.AggregateError | seroval.Feature.BigIntTypedArray;
14
+ const serializeOnlyDisabledFeatures = () => process.env.NODE_ENV === "development" ? 0 : seroval.Feature.ErrorPrototypeStack;
13
15
  const DEFAULT_WEB_PLUGINS = Object.freeze([web.AbortSignalPlugin,
14
16
  web.CustomEventPlugin, web.DOMExceptionPlugin, web.EventPlugin,
15
17
  web.FormDataPlugin, web.HeadersPlugin, web.ReadableStreamPlugin, web.RequestPlugin, web.ResponsePlugin, web.URLSearchParamsPlugin, web.URLPlugin]);
@@ -35,11 +37,13 @@ function serializeJSON(value, {
35
37
  onError,
36
38
  ...codecOptions
37
39
  }) {
40
+ const resolved = resolveCodecOptions(codecOptions);
38
41
  return seroval.toCrossJSONStream(value, {
39
42
  onParse,
40
43
  onDone,
41
44
  onError,
42
- ...resolveCodecOptions(codecOptions)
45
+ ...resolved,
46
+ disabledFeatures: resolved.disabledFeatures | serializeOnlyDisabledFeatures()
43
47
  });
44
48
  }
45
49
  function createJSONDeserializer(options) {
@@ -117,6 +121,18 @@ const INSTANCE_HEADER = "X-Server-Function-Instance";
117
121
  const BODY_FORMAT_HEADER = "X-Server-Function-Format";
118
122
  const SINGLE_FLIGHT_HEADER = "X-Single-Flight";
119
123
  const FILE_FORM_KEY = "__server_function_file__";
124
+ const FLASH_COOKIE = "flash";
125
+ const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
126
+ function hasFlashCookie(cookieHeader) {
127
+ return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
128
+ }
129
+ function matchFlashCookie(cookieHeader) {
130
+ const match = cookieHeader && cookieHeader.match(FLASH_MATCHER);
131
+ return match ? match[1] : undefined;
132
+ }
133
+ function clearFlashCookie() {
134
+ return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
135
+ }
120
136
  const BodyFormat = {
121
137
  Serialized: "0",
122
138
  String: "1",
@@ -328,12 +344,74 @@ async function decodeResponse(response, codecOptions) {
328
344
  if (!response.body) return undefined;
329
345
  return await extractBody(response, codecOptions === undefined ? codecConfig.codec : codecOptions);
330
346
  }
347
+ async function decodeResponsePayload(response, codecOptions) {
348
+ const decoded = await decodeResponse(response, codecOptions);
349
+ if (decoded !== undefined && response.headers.has(SINGLE_FLIGHT_HEADER)) {
350
+ return {
351
+ value: decoded.value,
352
+ flightData: decoded.data
353
+ };
354
+ }
355
+ return {
356
+ value: decoded
357
+ };
358
+ }
359
+
360
+ function encodeInputValue(value) {
361
+ if (value instanceof FormData) return {
362
+ $f: [...value.entries()].filter(([, v]) => typeof v === "string")
363
+ };
364
+ if (value instanceof URLSearchParams) return {
365
+ $u: [...value.entries()]
366
+ };
367
+ return value;
368
+ }
369
+ function decodeInputValue(value) {
370
+ if (value && typeof value === "object") {
371
+ if (Array.isArray(value.$f)) {
372
+ const form = new FormData();
373
+ for (const [k, v] of value.$f) form.append(k, v);
374
+ return form;
375
+ }
376
+ if (Array.isArray(value.$u)) return new URLSearchParams(value.$u);
377
+ }
378
+ return value;
379
+ }
380
+ function encodeFlashCookie(url, result, input, thrown) {
381
+ const isError = result instanceof Error;
382
+ const payload = {
383
+ url,
384
+ result: isError ? result.message : result,
385
+ error: isError,
386
+ thrown: !!thrown,
387
+ input: input.map(encodeInputValue)
388
+ };
389
+ return `${FLASH_COOKIE}=${encodeURIComponent(JSON.stringify(payload))}; Secure; HttpOnly; Path=/`;
390
+ }
391
+ function decodeFlashCookie(cookieHeader) {
392
+ const match = matchFlashCookie(cookieHeader);
393
+ if (!match) return;
394
+ try {
395
+ const payload = JSON.parse(decodeURIComponent(match));
396
+ if (!payload || !payload.result) return;
397
+ const result = payload.error ? new Error(payload.result) : payload.result;
398
+ return {
399
+ input: Array.isArray(payload.input) ? payload.input.map(decodeInputValue) : [],
400
+ url: payload.url,
401
+ result: payload.thrown ? undefined : result,
402
+ error: payload.thrown ? result : undefined
403
+ };
404
+ } catch (error) {
405
+ console.error(error);
406
+ }
407
+ }
331
408
 
332
409
  const config = {
333
410
  provideEvent: undefined,
334
411
  collectFlightData: undefined,
335
412
  transformResult: undefined,
336
413
  transformDirectResult: undefined,
414
+ handleNoJS: undefined,
337
415
  endpoint: "/_server"
338
416
  };
339
417
  function configureServerFunctionsServer({
@@ -341,6 +419,7 @@ function configureServerFunctionsServer({
341
419
  collectFlightData,
342
420
  transformResult,
343
421
  transformDirectResult,
422
+ handleNoJS,
344
423
  endpoint,
345
424
  codec
346
425
  } = {}) {
@@ -348,6 +427,7 @@ function configureServerFunctionsServer({
348
427
  if (collectFlightData !== undefined) config.collectFlightData = collectFlightData;
349
428
  if (transformResult !== undefined) config.transformResult = transformResult;
350
429
  if (transformDirectResult !== undefined) config.transformDirectResult = transformDirectResult;
430
+ if (handleNoJS !== undefined) config.handleNoJS = handleNoJS;
351
431
  if (endpoint !== undefined) config.endpoint = endpoint;
352
432
  if (codec !== undefined) configureServerFunctionsCodec(codec);
353
433
  }
@@ -465,6 +545,8 @@ async function parseArguments(request, url, instance, codec) {
465
545
  return parsed;
466
546
  }
467
547
  async function foldFlightData(hook, event, headers, outcome) {
548
+ if (outcome.value instanceof Response && outcome.value.body) return outcome.value;
549
+ digestOutcome(event, outcome);
468
550
  const data = await hook(event, outcome);
469
551
  if (data === undefined) return outcome.value;
470
552
  headers.set(SINGLE_FLIGHT_HEADER, "true");
@@ -473,6 +555,104 @@ async function foldFlightData(hook, event, headers, outcome) {
473
555
  data
474
556
  };
475
557
  }
558
+ function digestOutcome(event, outcome) {
559
+ const {
560
+ request,
561
+ response
562
+ } = outcome;
563
+ outcome.revalidateKeys = response?.headers.get(REVALIDATE_HEADER)?.split(",");
564
+ outcome.foldedHeaders = foldSetCookies(request.headers, [...(event.response?.headers?.getSetCookie() ?? []), ...(response?.headers?.getSetCookie() ?? [])]);
565
+ try {
566
+ const referrer = request.headers.get("referer");
567
+ if (referrer) {
568
+ const location = response?.headers.get("Location");
569
+ const target = location ? new URL(location, request.url) : new URL(referrer);
570
+ if (target.origin === new URL(request.url).origin) outcome.targetUrl = target.toString();
571
+ }
572
+ } catch {
573
+ }
574
+ }
575
+ function parseSetCookie(setCookie) {
576
+ const [pair, ...attributes] = setCookie.split(";");
577
+ const eq = pair.indexOf("=");
578
+ if (eq < 0) return undefined;
579
+ const parsed = {
580
+ name: pair.slice(0, eq).trim(),
581
+ value: pair.slice(eq + 1).trim()
582
+ };
583
+ for (const attribute of attributes) {
584
+ const attrEq = attribute.indexOf("=");
585
+ const key = (attrEq < 0 ? attribute : attribute.slice(0, attrEq)).trim().toLowerCase();
586
+ const value = attrEq < 0 ? "" : attribute.slice(attrEq + 1).trim();
587
+ if (key === "max-age") parsed.maxAge = Number(value);else if (key === "expires") parsed.expires = new Date(value);
588
+ }
589
+ return parsed;
590
+ }
591
+ function foldSetCookies(headers, setCookies) {
592
+ const folded = new Headers(headers);
593
+ if (!setCookies.length) return folded;
594
+ const cookies = {};
595
+ for (const pair of folded.get("cookie")?.split(";") ?? []) {
596
+ const eq = pair.indexOf("=");
597
+ if (eq > -1) cookies[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
598
+ }
599
+ for (const setCookie of setCookies) {
600
+ const parsed = parseSetCookie(setCookie);
601
+ if (!parsed) continue;
602
+ if (parsed.maxAge != null && parsed.maxAge <= 0 || parsed.expires != null && parsed.expires.getTime() <= Date.now()) {
603
+ delete cookies[parsed.name];
604
+ } else {
605
+ cookies[parsed.name] = parsed.value;
606
+ }
607
+ }
608
+ folded.delete("cookie");
609
+ const serialized = Object.entries(cookies).map(([name, value]) => `${name}=${value}`).join("; ");
610
+ if (serialized) folded.set("cookie", serialized);
611
+ return folded;
612
+ }
613
+ const validRedirectStatuses = new Set([301, 302, 303, 307, 308]);
614
+ function createNoJSHandler({
615
+ base = ""
616
+ } = {}) {
617
+ return function handleNoJS(result, request, args, thrown) {
618
+ const url = new URL(request.url);
619
+ let back = new URL(base || "/", url.origin).toString();
620
+ try {
621
+ const referer = request.headers.get("referer");
622
+ if (referer) back = new URL(referer).toString();
623
+ } catch {}
624
+ let status = 303;
625
+ let headers;
626
+ if (result instanceof Response) {
627
+ headers = new Headers(result.headers);
628
+ if (result.headers.has("Location")) {
629
+ headers.set("Location", new URL(result.headers.get("Location"), url.origin + base).toString());
630
+ if (validRedirectStatuses.has(result.status)) status = result.status;
631
+ } else {
632
+ headers.set("Location", back);
633
+ }
634
+ headers.delete("Content-Type");
635
+ headers.delete("Content-Length");
636
+ } else {
637
+ headers = new Headers({
638
+ Location: back
639
+ });
640
+ }
641
+ if (result && !(result instanceof Response)) {
642
+ headers.append("Set-Cookie", encodeFlashCookie(url.pathname + url.search, result, args, thrown));
643
+ }
644
+ return new Response(null, {
645
+ status,
646
+ headers
647
+ });
648
+ };
649
+ }
650
+ let defaultNoJSHandler;
651
+ function isFormPost(request) {
652
+ if (request.method !== "POST" || request.headers.has(BODY_FORMAT_HEADER)) return false;
653
+ const type = request.headers.get("content-type") || "";
654
+ return type.startsWith("application/x-www-form-urlencoded") || type.startsWith("multipart/form-data");
655
+ }
476
656
  function serializedResponse(value, headers, codec) {
477
657
  headers.set(BODY_FORMAT_HEADER, BodyFormat.Serialized);
478
658
  headers.set("Content-Type", "text/plain");
@@ -530,6 +710,7 @@ async function handleServerFunctionRequest(request, options = {}) {
530
710
  const provide = options.provideEvent || provideEvent;
531
711
  const flightHook = options.collectFlightData !== undefined ? options.collectFlightData : config.collectFlightData;
532
712
  const transformResult = options.transformResult !== undefined ? options.transformResult : config.transformResult;
713
+ const handleNoJS = options.handleNoJS !== undefined ? options.handleNoJS : config.handleNoJS !== undefined ? config.handleNoJS : isFormPost(request) ? defaultNoJSHandler || (defaultNoJSHandler = createNoJSHandler()) : undefined;
533
714
  const collectsFlight = !!(flightHook && instance && request.headers.has(SINGLE_FLIGHT_HEADER));
534
715
  const parsed = await parseArguments(request, url, instance, codec);
535
716
  const headers = new Headers();
@@ -553,7 +734,7 @@ async function handleServerFunctionRequest(request, options = {}) {
553
734
  response,
554
735
  value
555
736
  } = result;
556
- if (!instance && !options.handleNoJS && response && response.body) {
737
+ if (!instance && !handleNoJS && response && response.body) {
557
738
  return response;
558
739
  }
559
740
  if (response && response.headers) {
@@ -589,7 +770,7 @@ async function handleServerFunctionRequest(request, options = {}) {
589
770
  });
590
771
  }
591
772
  if (!instance) {
592
- if (options.handleNoJS) return options.handleNoJS(result, request, parsed);
773
+ if (handleNoJS) return handleNoJS(result, request, parsed);
593
774
  if (result instanceof Response) return result;
594
775
  return encodeResult(result, headers, 200, codec);
595
776
  }
@@ -641,13 +822,13 @@ async function handleServerFunctionRequest(request, options = {}) {
641
822
  }
642
823
  headers.set(ERROR_HEADER, "true");
643
824
  if (!instance) {
644
- if (options.handleNoJS) return options.handleNoJS(x, request, parsed, true);
825
+ if (handleNoJS) return handleNoJS(x ?? metadata, request, parsed, true);
645
826
  if (x instanceof Response) return x;
646
827
  }
647
828
  return encodeResult(x, headers, status, codec);
648
829
  }
649
830
  if (!instance) {
650
- if (options.handleNoJS) return options.handleNoJS(x, request, parsed, true);
831
+ if (handleNoJS) return handleNoJS(x, request, parsed, true);
651
832
  const message = x instanceof Error ? x.message : String(x);
652
833
  return new Response(process.env.NODE_ENV === "development" ? message : null, {
653
834
  status: 500
@@ -660,19 +841,27 @@ async function handleServerFunctionRequest(request, options = {}) {
660
841
  }
661
842
 
662
843
  exports.ERROR_HEADER = ERROR_HEADER;
844
+ exports.FLASH_COOKIE = FLASH_COOKIE;
663
845
  exports.FUNCTION_HEADER = FUNCTION_HEADER;
664
846
  exports.GET = GET;
665
847
  exports.INSTANCE_HEADER = INSTANCE_HEADER;
666
848
  exports.SINGLE_FLIGHT_HEADER = SINGLE_FLIGHT_HEADER;
849
+ exports.clearFlashCookie = clearFlashCookie;
667
850
  exports.configureServerFunctionsServer = configureServerFunctionsServer;
851
+ exports.createNoJSHandler = createNoJSHandler;
668
852
  exports.createServerReference = createServerReference;
669
853
  exports.decodeErrorHeaderValue = decodeErrorHeaderValue;
854
+ exports.decodeFlashCookie = decodeFlashCookie;
670
855
  exports.decodeResponse = decodeResponse;
856
+ exports.decodeResponsePayload = decodeResponsePayload;
671
857
  exports.encodeErrorHeaderValue = encodeErrorHeaderValue;
858
+ exports.encodeFlashCookie = encodeFlashCookie;
859
+ exports.foldSetCookies = foldSetCookies;
672
860
  exports.getServerFunction = getServerFunction;
673
861
  exports.getServerFunctionMeta = getServerFunctionMeta;
674
862
  exports.getServerFunctionMetadata = getServerFunctionMetadata;
675
863
  exports.handleServerFunctionRequest = handleServerFunctionRequest;
864
+ exports.hasFlashCookie = hasFlashCookie;
676
865
  exports.isServerFunction = isServerFunction;
677
866
  exports.registerServerFunction = registerServerFunction;
678
867
  exports.registerServerReference = registerServerReference;
@@ -6,8 +6,10 @@ const ENVELOPE = Symbol.for("solid.ResponseEnvelope");
6
6
  function isResponseEnvelope(value) {
7
7
  return !!(value && typeof value === "object" && value[ENVELOPE]);
8
8
  }
9
+ const REVALIDATE_HEADER = "X-Revalidate";
9
10
 
10
11
  Feature.AggregateError | Feature.BigIntTypedArray;
12
+ const serializeOnlyDisabledFeatures = () => process.env.NODE_ENV === "development" ? 0 : Feature.ErrorPrototypeStack;
11
13
  const DEFAULT_WEB_PLUGINS = Object.freeze([AbortSignalPlugin,
12
14
  CustomEventPlugin, DOMExceptionPlugin, EventPlugin,
13
15
  FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLSearchParamsPlugin, URLPlugin]);
@@ -33,11 +35,13 @@ function serializeJSON(value, {
33
35
  onError,
34
36
  ...codecOptions
35
37
  }) {
38
+ const resolved = resolveCodecOptions(codecOptions);
36
39
  return toCrossJSONStream(value, {
37
40
  onParse,
38
41
  onDone,
39
42
  onError,
40
- ...resolveCodecOptions(codecOptions)
43
+ ...resolved,
44
+ disabledFeatures: resolved.disabledFeatures | serializeOnlyDisabledFeatures()
41
45
  });
42
46
  }
43
47
  function createJSONDeserializer(options) {
@@ -115,6 +119,18 @@ const INSTANCE_HEADER = "X-Server-Function-Instance";
115
119
  const BODY_FORMAT_HEADER = "X-Server-Function-Format";
116
120
  const SINGLE_FLIGHT_HEADER = "X-Single-Flight";
117
121
  const FILE_FORM_KEY = "__server_function_file__";
122
+ const FLASH_COOKIE = "flash";
123
+ const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
124
+ function hasFlashCookie(cookieHeader) {
125
+ return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
126
+ }
127
+ function matchFlashCookie(cookieHeader) {
128
+ const match = cookieHeader && cookieHeader.match(FLASH_MATCHER);
129
+ return match ? match[1] : undefined;
130
+ }
131
+ function clearFlashCookie() {
132
+ return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
133
+ }
118
134
  const BodyFormat = {
119
135
  Serialized: "0",
120
136
  String: "1",
@@ -326,12 +342,74 @@ async function decodeResponse(response, codecOptions) {
326
342
  if (!response.body) return undefined;
327
343
  return await extractBody(response, codecOptions === undefined ? codecConfig.codec : codecOptions);
328
344
  }
345
+ async function decodeResponsePayload(response, codecOptions) {
346
+ const decoded = await decodeResponse(response, codecOptions);
347
+ if (decoded !== undefined && response.headers.has(SINGLE_FLIGHT_HEADER)) {
348
+ return {
349
+ value: decoded.value,
350
+ flightData: decoded.data
351
+ };
352
+ }
353
+ return {
354
+ value: decoded
355
+ };
356
+ }
357
+
358
+ function encodeInputValue(value) {
359
+ if (value instanceof FormData) return {
360
+ $f: [...value.entries()].filter(([, v]) => typeof v === "string")
361
+ };
362
+ if (value instanceof URLSearchParams) return {
363
+ $u: [...value.entries()]
364
+ };
365
+ return value;
366
+ }
367
+ function decodeInputValue(value) {
368
+ if (value && typeof value === "object") {
369
+ if (Array.isArray(value.$f)) {
370
+ const form = new FormData();
371
+ for (const [k, v] of value.$f) form.append(k, v);
372
+ return form;
373
+ }
374
+ if (Array.isArray(value.$u)) return new URLSearchParams(value.$u);
375
+ }
376
+ return value;
377
+ }
378
+ function encodeFlashCookie(url, result, input, thrown) {
379
+ const isError = result instanceof Error;
380
+ const payload = {
381
+ url,
382
+ result: isError ? result.message : result,
383
+ error: isError,
384
+ thrown: !!thrown,
385
+ input: input.map(encodeInputValue)
386
+ };
387
+ return `${FLASH_COOKIE}=${encodeURIComponent(JSON.stringify(payload))}; Secure; HttpOnly; Path=/`;
388
+ }
389
+ function decodeFlashCookie(cookieHeader) {
390
+ const match = matchFlashCookie(cookieHeader);
391
+ if (!match) return;
392
+ try {
393
+ const payload = JSON.parse(decodeURIComponent(match));
394
+ if (!payload || !payload.result) return;
395
+ const result = payload.error ? new Error(payload.result) : payload.result;
396
+ return {
397
+ input: Array.isArray(payload.input) ? payload.input.map(decodeInputValue) : [],
398
+ url: payload.url,
399
+ result: payload.thrown ? undefined : result,
400
+ error: payload.thrown ? result : undefined
401
+ };
402
+ } catch (error) {
403
+ console.error(error);
404
+ }
405
+ }
329
406
 
330
407
  const config = {
331
408
  provideEvent: undefined,
332
409
  collectFlightData: undefined,
333
410
  transformResult: undefined,
334
411
  transformDirectResult: undefined,
412
+ handleNoJS: undefined,
335
413
  endpoint: "/_server"
336
414
  };
337
415
  function configureServerFunctionsServer({
@@ -339,6 +417,7 @@ function configureServerFunctionsServer({
339
417
  collectFlightData,
340
418
  transformResult,
341
419
  transformDirectResult,
420
+ handleNoJS,
342
421
  endpoint,
343
422
  codec
344
423
  } = {}) {
@@ -346,6 +425,7 @@ function configureServerFunctionsServer({
346
425
  if (collectFlightData !== undefined) config.collectFlightData = collectFlightData;
347
426
  if (transformResult !== undefined) config.transformResult = transformResult;
348
427
  if (transformDirectResult !== undefined) config.transformDirectResult = transformDirectResult;
428
+ if (handleNoJS !== undefined) config.handleNoJS = handleNoJS;
349
429
  if (endpoint !== undefined) config.endpoint = endpoint;
350
430
  if (codec !== undefined) configureServerFunctionsCodec(codec);
351
431
  }
@@ -463,6 +543,8 @@ async function parseArguments(request, url, instance, codec) {
463
543
  return parsed;
464
544
  }
465
545
  async function foldFlightData(hook, event, headers, outcome) {
546
+ if (outcome.value instanceof Response && outcome.value.body) return outcome.value;
547
+ digestOutcome(event, outcome);
466
548
  const data = await hook(event, outcome);
467
549
  if (data === undefined) return outcome.value;
468
550
  headers.set(SINGLE_FLIGHT_HEADER, "true");
@@ -471,6 +553,104 @@ async function foldFlightData(hook, event, headers, outcome) {
471
553
  data
472
554
  };
473
555
  }
556
+ function digestOutcome(event, outcome) {
557
+ const {
558
+ request,
559
+ response
560
+ } = outcome;
561
+ outcome.revalidateKeys = response?.headers.get(REVALIDATE_HEADER)?.split(",");
562
+ outcome.foldedHeaders = foldSetCookies(request.headers, [...(event.response?.headers?.getSetCookie() ?? []), ...(response?.headers?.getSetCookie() ?? [])]);
563
+ try {
564
+ const referrer = request.headers.get("referer");
565
+ if (referrer) {
566
+ const location = response?.headers.get("Location");
567
+ const target = location ? new URL(location, request.url) : new URL(referrer);
568
+ if (target.origin === new URL(request.url).origin) outcome.targetUrl = target.toString();
569
+ }
570
+ } catch {
571
+ }
572
+ }
573
+ function parseSetCookie(setCookie) {
574
+ const [pair, ...attributes] = setCookie.split(";");
575
+ const eq = pair.indexOf("=");
576
+ if (eq < 0) return undefined;
577
+ const parsed = {
578
+ name: pair.slice(0, eq).trim(),
579
+ value: pair.slice(eq + 1).trim()
580
+ };
581
+ for (const attribute of attributes) {
582
+ const attrEq = attribute.indexOf("=");
583
+ const key = (attrEq < 0 ? attribute : attribute.slice(0, attrEq)).trim().toLowerCase();
584
+ const value = attrEq < 0 ? "" : attribute.slice(attrEq + 1).trim();
585
+ if (key === "max-age") parsed.maxAge = Number(value);else if (key === "expires") parsed.expires = new Date(value);
586
+ }
587
+ return parsed;
588
+ }
589
+ function foldSetCookies(headers, setCookies) {
590
+ const folded = new Headers(headers);
591
+ if (!setCookies.length) return folded;
592
+ const cookies = {};
593
+ for (const pair of folded.get("cookie")?.split(";") ?? []) {
594
+ const eq = pair.indexOf("=");
595
+ if (eq > -1) cookies[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
596
+ }
597
+ for (const setCookie of setCookies) {
598
+ const parsed = parseSetCookie(setCookie);
599
+ if (!parsed) continue;
600
+ if (parsed.maxAge != null && parsed.maxAge <= 0 || parsed.expires != null && parsed.expires.getTime() <= Date.now()) {
601
+ delete cookies[parsed.name];
602
+ } else {
603
+ cookies[parsed.name] = parsed.value;
604
+ }
605
+ }
606
+ folded.delete("cookie");
607
+ const serialized = Object.entries(cookies).map(([name, value]) => `${name}=${value}`).join("; ");
608
+ if (serialized) folded.set("cookie", serialized);
609
+ return folded;
610
+ }
611
+ const validRedirectStatuses = new Set([301, 302, 303, 307, 308]);
612
+ function createNoJSHandler({
613
+ base = ""
614
+ } = {}) {
615
+ return function handleNoJS(result, request, args, thrown) {
616
+ const url = new URL(request.url);
617
+ let back = new URL(base || "/", url.origin).toString();
618
+ try {
619
+ const referer = request.headers.get("referer");
620
+ if (referer) back = new URL(referer).toString();
621
+ } catch {}
622
+ let status = 303;
623
+ let headers;
624
+ if (result instanceof Response) {
625
+ headers = new Headers(result.headers);
626
+ if (result.headers.has("Location")) {
627
+ headers.set("Location", new URL(result.headers.get("Location"), url.origin + base).toString());
628
+ if (validRedirectStatuses.has(result.status)) status = result.status;
629
+ } else {
630
+ headers.set("Location", back);
631
+ }
632
+ headers.delete("Content-Type");
633
+ headers.delete("Content-Length");
634
+ } else {
635
+ headers = new Headers({
636
+ Location: back
637
+ });
638
+ }
639
+ if (result && !(result instanceof Response)) {
640
+ headers.append("Set-Cookie", encodeFlashCookie(url.pathname + url.search, result, args, thrown));
641
+ }
642
+ return new Response(null, {
643
+ status,
644
+ headers
645
+ });
646
+ };
647
+ }
648
+ let defaultNoJSHandler;
649
+ function isFormPost(request) {
650
+ if (request.method !== "POST" || request.headers.has(BODY_FORMAT_HEADER)) return false;
651
+ const type = request.headers.get("content-type") || "";
652
+ return type.startsWith("application/x-www-form-urlencoded") || type.startsWith("multipart/form-data");
653
+ }
474
654
  function serializedResponse(value, headers, codec) {
475
655
  headers.set(BODY_FORMAT_HEADER, BodyFormat.Serialized);
476
656
  headers.set("Content-Type", "text/plain");
@@ -528,6 +708,7 @@ async function handleServerFunctionRequest(request, options = {}) {
528
708
  const provide = options.provideEvent || provideEvent;
529
709
  const flightHook = options.collectFlightData !== undefined ? options.collectFlightData : config.collectFlightData;
530
710
  const transformResult = options.transformResult !== undefined ? options.transformResult : config.transformResult;
711
+ const handleNoJS = options.handleNoJS !== undefined ? options.handleNoJS : config.handleNoJS !== undefined ? config.handleNoJS : isFormPost(request) ? defaultNoJSHandler || (defaultNoJSHandler = createNoJSHandler()) : undefined;
531
712
  const collectsFlight = !!(flightHook && instance && request.headers.has(SINGLE_FLIGHT_HEADER));
532
713
  const parsed = await parseArguments(request, url, instance, codec);
533
714
  const headers = new Headers();
@@ -551,7 +732,7 @@ async function handleServerFunctionRequest(request, options = {}) {
551
732
  response,
552
733
  value
553
734
  } = result;
554
- if (!instance && !options.handleNoJS && response && response.body) {
735
+ if (!instance && !handleNoJS && response && response.body) {
555
736
  return response;
556
737
  }
557
738
  if (response && response.headers) {
@@ -587,7 +768,7 @@ async function handleServerFunctionRequest(request, options = {}) {
587
768
  });
588
769
  }
589
770
  if (!instance) {
590
- if (options.handleNoJS) return options.handleNoJS(result, request, parsed);
771
+ if (handleNoJS) return handleNoJS(result, request, parsed);
591
772
  if (result instanceof Response) return result;
592
773
  return encodeResult(result, headers, 200, codec);
593
774
  }
@@ -639,13 +820,13 @@ async function handleServerFunctionRequest(request, options = {}) {
639
820
  }
640
821
  headers.set(ERROR_HEADER, "true");
641
822
  if (!instance) {
642
- if (options.handleNoJS) return options.handleNoJS(x, request, parsed, true);
823
+ if (handleNoJS) return handleNoJS(x ?? metadata, request, parsed, true);
643
824
  if (x instanceof Response) return x;
644
825
  }
645
826
  return encodeResult(x, headers, status, codec);
646
827
  }
647
828
  if (!instance) {
648
- if (options.handleNoJS) return options.handleNoJS(x, request, parsed, true);
829
+ if (handleNoJS) return handleNoJS(x, request, parsed, true);
649
830
  const message = x instanceof Error ? x.message : String(x);
650
831
  return new Response(process.env.NODE_ENV === "development" ? message : null, {
651
832
  status: 500
@@ -657,4 +838,4 @@ async function handleServerFunctionRequest(request, options = {}) {
657
838
  }
658
839
  }
659
840
 
660
- export { ERROR_HEADER, FUNCTION_HEADER, GET, INSTANCE_HEADER, SINGLE_FLIGHT_HEADER, configureServerFunctionsServer, createServerReference, decodeErrorHeaderValue, decodeResponse, encodeErrorHeaderValue, getServerFunction, getServerFunctionMeta, getServerFunctionMetadata, handleServerFunctionRequest, isServerFunction, registerServerFunction, registerServerReference, subscribeFlightData, withMeta };
841
+ export { ERROR_HEADER, FLASH_COOKIE, FUNCTION_HEADER, GET, INSTANCE_HEADER, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsServer, createNoJSHandler, createServerReference, decodeErrorHeaderValue, decodeFlashCookie, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, encodeFlashCookie, foldSetCookies, getServerFunction, getServerFunctionMeta, getServerFunctionMetadata, handleServerFunctionRequest, hasFlashCookie, isServerFunction, registerServerFunction, registerServerReference, subscribeFlightData, withMeta };
package/types/client.d.ts CHANGED
@@ -109,9 +109,9 @@ export function style(
109
109
  export function getOwner(): unknown;
110
110
  export function mergeProps(...sources: unknown[]): unknown;
111
111
  export function dynamicProperty(props: unknown, key: string): unknown;
112
- export function applyRef(
113
- r: ((element: Element) => void) | ((element: Element) => void)[],
114
- element: Element
112
+ export function applyRef<T extends Element = Element>(
113
+ r: ((element: NoInfer<T>) => void) | ((element: NoInfer<T>) => void)[],
114
+ element: T
115
115
  ): void;
116
116
  export function ref(
117
117
  fn: () => ((element: Element) => void) | ((element: Element) => void)[],