@solidjs/router 2.0.0-next.20 → 2.0.0-next.21

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.
@@ -1,6 +1,6 @@
1
1
  import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
2
2
  import { isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
3
- import { createServerReference, decodeResponsePayload, parseServerFunctionUrl, subscribeFlightData } from "@solidjs/web/server-functions";
3
+ import { createServerReference, decodeRedirectHeaderValue, decodeResponsePayload, parseServerFunctionUrl, REDIRECT_HEADER, subscribeFlightData } from "@solidjs/web/server-functions";
4
4
  // The explicit /server specifier is safe here: the only call site is
5
5
  // server-guarded, so client builds tree-shake the codec away.
6
6
  import { decodeFlashCookie } from "@solidjs/web/server-functions/server";
@@ -68,11 +68,13 @@ export function handleFormAction(evt, router, actionBase) {
68
68
  * carries everything an invocation needs — the function id in the path
69
69
  * (`<endpoint>/<id>`) and any bound `.with()` arguments (plain JSON in
70
70
  * `?args`, which the server prepends for natural-encoding bodies exactly as
71
- * it does for no-JS posts) — so the FormData is posted to it verbatim
72
- * through the server-function transport:
73
- * submissions, `aria-busy`, redirects, revalidation, and single-flight all
74
- * flow through the normal action machinery. Registered under the url, so
75
- * repeat submits reuse it (and a later real registration overrides it).
71
+ * it does for no-JS posts) — so the FormData is posted through the
72
+ * server-function transport, which addresses its own scripted calls at the
73
+ * url's data sibling (`<endpoint>/data/<id>`, solidjs/solid#3094) with the
74
+ * bound query intact: submissions, `aria-busy`, redirects, revalidation,
75
+ * and single-flight all flow through the normal action machinery.
76
+ * Registered under the rendered url, so repeat submits reuse it (and a
77
+ * later real registration overrides it).
76
78
  */
77
79
  function createServerFormAction(url) {
78
80
  const id = parseServerFunctionUrl(url);
@@ -290,6 +292,10 @@ async function settleActionResult(result) {
290
292
  // response's metadata (and so the default revalidation pass must not run
291
293
  // again and wipe the freshly seeded cache).
292
294
  let flightApplications = 0;
295
+ // The statuses fetch follows (Fetch §2.2.3) — the set the server masks into
296
+ // the redirect carrier for scripted calls, and the set a locally-produced
297
+ // redirect() envelope wears for real.
298
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
293
299
  /**
294
300
  * Registers the router as the single-flight consumer of the server function
295
301
  * transport. Subscribing is the opt-in: while registered, the transport
@@ -309,23 +315,43 @@ export function setupFlightDataConsumer(router) {
309
315
  }
310
316
  /**
311
317
  * Applies a server function response's integration metadata: `X-Revalidate`
312
- * keys invalidate, `Location` navigates (hard for absolute urls), flight
313
- * data seeds the query cache, and matching entries revalidate. Shared by
314
- * the flight-data consumer and the action response path (which still sees
315
- * metadata-bearing responses when no flight data was collected).
318
+ * keys invalidate, the redirect carrier navigates (soft when same-origin,
319
+ * hard otherwise), flight data seeds the query cache, and matching entries
320
+ * revalidate. Shared by the flight-data consumer and the action response
321
+ * path (which still sees metadata-bearing responses when no flight data was
322
+ * collected).
316
323
  */
317
324
  function applyResponseMetadata(metadata, navigate, flightData) {
318
325
  let keys;
319
326
  if (metadata) {
320
327
  if (metadata.headers.has(REVALIDATE_HEADER))
321
328
  keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
322
- if (metadata.headers.has("Location")) {
323
- const locationUrl = metadata.headers.get("Location") || "/";
324
- if (locationUrl.startsWith("http")) {
325
- window.location.href = locationUrl;
329
+ // The carrier delivers the target RESOLVED to an absolute url
330
+ // (solidjs/solid#3102), so the soft/hard split is a real origin
331
+ // comparison — never a guess from how the author spelled the target,
332
+ // which sent `redirect("/")` and `redirect(new URL("/", url).href)`
333
+ // down different navigation paths (solidjs/solid#3107). A redirect
334
+ // produced locally (a client-side action's `redirect()`) never crossed
335
+ // the wire, so no carrier was attached: it is the real 3xx with its
336
+ // Location, resolved against the page it runs in. A `Location` on any
337
+ // other status is the author's data (a 201's created-at) and never
338
+ // navigates. Same-origin targets navigate softly under the router;
339
+ // anything else leaves the app, so the document goes with it.
340
+ // `replace` matches what HTTP gives a form post: the target takes the
341
+ // submission's place in history rather than stacking on it.
342
+ const carried = decodeRedirectHeaderValue(metadata.headers.get(REDIRECT_HEADER));
343
+ const local = !carried && REDIRECT_STATUSES.has(metadata.status) && metadata.headers.get("Location");
344
+ const target = carried
345
+ ? new URL(carried.url)
346
+ : local
347
+ ? new URL(local, window.location.href)
348
+ : undefined;
349
+ if (target) {
350
+ if (target.origin === window.location.origin) {
351
+ navigate(target.pathname + target.search + target.hash, { replace: true });
326
352
  }
327
353
  else {
328
- navigate(locationUrl);
354
+ window.location.href = target.href;
329
355
  }
330
356
  }
331
357
  }
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
2
  import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
- import { subscribeFlightData, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
3
+ import { subscribeFlightData, decodeRedirectHeaderValue, REDIRECT_HEADER, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
4
4
  import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
5
5
 
6
6
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
@@ -2562,11 +2562,13 @@ function handleFormAction(evt, router, actionBase) {
2562
2562
  * carries everything an invocation needs — the function id in the path
2563
2563
  * (`<endpoint>/<id>`) and any bound `.with()` arguments (plain JSON in
2564
2564
  * `?args`, which the server prepends for natural-encoding bodies exactly as
2565
- * it does for no-JS posts) — so the FormData is posted to it verbatim
2566
- * through the server-function transport:
2567
- * submissions, `aria-busy`, redirects, revalidation, and single-flight all
2568
- * flow through the normal action machinery. Registered under the url, so
2569
- * repeat submits reuse it (and a later real registration overrides it).
2565
+ * it does for no-JS posts) — so the FormData is posted through the
2566
+ * server-function transport, which addresses its own scripted calls at the
2567
+ * url's data sibling (`<endpoint>/data/<id>`, solidjs/solid#3094) with the
2568
+ * bound query intact: submissions, `aria-busy`, redirects, revalidation,
2569
+ * and single-flight all flow through the normal action machinery.
2570
+ * Registered under the rendered url, so repeat submits reuse it (and a
2571
+ * later real registration overrides it).
2570
2572
  */
2571
2573
  function createServerFormAction(url) {
2572
2574
  const id = parseServerFunctionUrl(url);
@@ -2791,6 +2793,11 @@ async function settleActionResult(result) {
2791
2793
  // again and wipe the freshly seeded cache).
2792
2794
  let flightApplications = 0;
2793
2795
 
2796
+ // The statuses fetch follows (Fetch §2.2.3) — the set the server masks into
2797
+ // the redirect carrier for scripted calls, and the set a locally-produced
2798
+ // redirect() envelope wears for real.
2799
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
2800
+
2794
2801
  /**
2795
2802
  * Registers the router as the single-flight consumer of the server function
2796
2803
  * transport. Subscribing is the opt-in: while registered, the transport
@@ -2813,21 +2820,39 @@ function setupFlightDataConsumer(router) {
2813
2820
 
2814
2821
  /**
2815
2822
  * Applies a server function response's integration metadata: `X-Revalidate`
2816
- * keys invalidate, `Location` navigates (hard for absolute urls), flight
2817
- * data seeds the query cache, and matching entries revalidate. Shared by
2818
- * the flight-data consumer and the action response path (which still sees
2819
- * metadata-bearing responses when no flight data was collected).
2823
+ * keys invalidate, the redirect carrier navigates (soft when same-origin,
2824
+ * hard otherwise), flight data seeds the query cache, and matching entries
2825
+ * revalidate. Shared by the flight-data consumer and the action response
2826
+ * path (which still sees metadata-bearing responses when no flight data was
2827
+ * collected).
2820
2828
  */
2821
2829
  function applyResponseMetadata(metadata, navigate, flightData) {
2822
2830
  let keys;
2823
2831
  if (metadata) {
2824
2832
  if (metadata.headers.has(REVALIDATE_HEADER)) keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
2825
- if (metadata.headers.has("Location")) {
2826
- const locationUrl = metadata.headers.get("Location") || "/";
2827
- if (locationUrl.startsWith("http")) {
2828
- window.location.href = locationUrl;
2833
+ // The carrier delivers the target RESOLVED to an absolute url
2834
+ // (solidjs/solid#3102), so the soft/hard split is a real origin
2835
+ // comparison — never a guess from how the author spelled the target,
2836
+ // which sent `redirect("/")` and `redirect(new URL("/", url).href)`
2837
+ // down different navigation paths (solidjs/solid#3107). A redirect
2838
+ // produced locally (a client-side action's `redirect()`) never crossed
2839
+ // the wire, so no carrier was attached: it is the real 3xx with its
2840
+ // Location, resolved against the page it runs in. A `Location` on any
2841
+ // other status is the author's data (a 201's created-at) and never
2842
+ // navigates. Same-origin targets navigate softly under the router;
2843
+ // anything else leaves the app, so the document goes with it.
2844
+ // `replace` matches what HTTP gives a form post: the target takes the
2845
+ // submission's place in history rather than stacking on it.
2846
+ const carried = decodeRedirectHeaderValue(metadata.headers.get(REDIRECT_HEADER));
2847
+ const local = !carried && REDIRECT_STATUSES.has(metadata.status) && metadata.headers.get("Location");
2848
+ const target = carried ? new URL(carried.url) : local ? new URL(local, window.location.href) : undefined;
2849
+ if (target) {
2850
+ if (target.origin === window.location.origin) {
2851
+ navigate(target.pathname + target.search + target.hash, {
2852
+ replace: true
2853
+ });
2829
2854
  } else {
2830
- navigate(locationUrl);
2855
+ window.location.href = target.href;
2831
2856
  }
2832
2857
  }
2833
2858
  }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.20",
9
+ "version": "2.0.0-next.21",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -38,21 +38,21 @@
38
38
  "@rollup/plugin-node-resolve": "15.3.0",
39
39
  "@rollup/plugin-terser": "0.4.4",
40
40
  "@solidjs/vite-plugin": "3.0.0-next.35",
41
- "@solidjs/web": "^2.0.0-rc.4",
41
+ "@solidjs/web": "^2.0.0-rc.5",
42
42
  "@types/jest": "^29.5.14",
43
43
  "@types/node": "^22.10.0",
44
44
  "babel-preset-solid": "^2.0.0-rc.2",
45
45
  "jsdom": "^25.0.1",
46
46
  "prettier": "^3.4.1",
47
47
  "rollup": "^4.27.4",
48
- "solid-js": "^2.0.0-rc.4",
48
+ "solid-js": "^2.0.0-rc.5",
49
49
  "typescript": "^5.7.2",
50
50
  "vite": "^8.2.2",
51
51
  "vitest": "^4.1.11"
52
52
  },
53
53
  "peerDependencies": {
54
- "@solidjs/web": "^2.0.0-rc.4",
55
- "solid-js": "^2.0.0-rc.4"
54
+ "@solidjs/web": "^2.0.0-rc.5",
55
+ "solid-js": "^2.0.0-rc.5"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsc && rollup -c",