@solidjs/router 2.0.0-next.33 → 2.0.0-next.34
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/data/action.js +34 -4
- package/dist/index.js +74 -31
- package/dist/routing.js +33 -28
- package/package.json +1 -1
package/dist/data/action.js
CHANGED
|
@@ -52,13 +52,40 @@ export function handleFormAction(evt, router, actionBase) {
|
|
|
52
52
|
// stays a no-JS fallback.
|
|
53
53
|
// Client-only actions (`https://action/`) are their module's JS by
|
|
54
54
|
// definition, so a miss there falls through to native submission.
|
|
55
|
-
const handler =
|
|
55
|
+
const handler = findAction(actionRef) || (serverAction && createServerFormAction(actionRef));
|
|
56
56
|
if (handler) {
|
|
57
57
|
evt.preventDefault();
|
|
58
58
|
const data = new FormData(form, evt.submitter);
|
|
59
59
|
handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Looks a rendered action url up in the registry. A server-rendered
|
|
64
|
+
* `.with()` url is only registered if this client made the same binding
|
|
65
|
+
* itself; otherwise its base action (the url without `?args`) is rebound to
|
|
66
|
+
* the rendered arguments, so the submission runs through that action's
|
|
67
|
+
* submit and settled hooks and is recorded under its base.
|
|
68
|
+
*/
|
|
69
|
+
function findAction(url) {
|
|
70
|
+
const handler = actions.get(url);
|
|
71
|
+
if (handler)
|
|
72
|
+
return handler;
|
|
73
|
+
const query = url.indexOf("?");
|
|
74
|
+
if (query < 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
const base = actions.get(url.slice(0, query));
|
|
77
|
+
const args = new URLSearchParams(url.slice(query)).get("args");
|
|
78
|
+
if (!base || args === null)
|
|
79
|
+
return undefined;
|
|
80
|
+
let bound;
|
|
81
|
+
try {
|
|
82
|
+
bound = JSON.parse(args);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return Array.isArray(bound) ? base.with(...bound) : undefined;
|
|
88
|
+
}
|
|
62
89
|
/**
|
|
63
90
|
* Synthesizes a router action for a server-rendered action url. The url
|
|
64
91
|
* carries everything an invocation needs — the function id in the path
|
|
@@ -90,7 +117,7 @@ function createServerFormAction(url) {
|
|
|
90
117
|
* the enctype conversion and the generic invocation happen here.
|
|
91
118
|
*/
|
|
92
119
|
export function submitServerForm(router, url, form, data) {
|
|
93
|
-
const handler =
|
|
120
|
+
const handler = findAction(url) || createServerFormAction(url);
|
|
94
121
|
// not an address (`<endpoint>/<id>`) — not the server function convention;
|
|
95
122
|
// nothing can run it, resubmit natively (submit() bypasses the delegated
|
|
96
123
|
// handler)
|
|
@@ -244,9 +271,12 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
244
271
|
return url;
|
|
245
272
|
};
|
|
246
273
|
fn.with = function (...args) {
|
|
274
|
+
const bound = [...boundArgs, ...args];
|
|
247
275
|
const uri = new URL(url, mockBase);
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
// the server prepends `args` to the submitted arguments, so it must carry
|
|
277
|
+
// the whole binding, not just this call's part of a chain
|
|
278
|
+
uri.searchParams.set("args", hashKey(bound));
|
|
279
|
+
return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, bound, base, submitHooks, settledHooks);
|
|
250
280
|
};
|
|
251
281
|
fn.onSubmit = function (hook) {
|
|
252
282
|
const id = Symbol("actionOnSubmitHook");
|
package/dist/index.js
CHANGED
|
@@ -1190,6 +1190,17 @@ let flashDecoder;
|
|
|
1190
1190
|
function provideFlashDecoder(decoder) {
|
|
1191
1191
|
flashDecoder || (flashDecoder = decoder);
|
|
1192
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* A request's flash cookie and its decode, shared by every router created
|
|
1196
|
+
* for that request. A server render can re-create the router — a suspension
|
|
1197
|
+
* under `<Errored>` retries from the nearest hole above it, which may sit
|
|
1198
|
+
* above the router — so state kept on one router instance would restart the
|
|
1199
|
+
* decode with every re-creation and the seeding read would never settle.
|
|
1200
|
+
* Keyed by the request event, so it lives exactly as long as the request.
|
|
1201
|
+
*/
|
|
1202
|
+
|
|
1203
|
+
const flashStates = new WeakMap();
|
|
1193
1204
|
let preloadIntent;
|
|
1194
1205
|
function getIntent() {
|
|
1195
1206
|
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
@@ -1228,63 +1239,68 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1228
1239
|
// unread outcome must not haunt a later request's render. Only detection
|
|
1229
1240
|
// and clearing happen here (the runtime's isomorphic half); the raw header
|
|
1230
1241
|
// is stashed and decoding waits for the action-provided codec, read when
|
|
1231
|
-
// the lazily allocated submissions signal below first initializes.
|
|
1232
|
-
|
|
1242
|
+
// the lazily allocated submissions signal below first initializes. Both
|
|
1243
|
+
// happen once per request: a router re-created for the same request finds
|
|
1244
|
+
// the request's flash state (see `flashStates`) and neither clears again
|
|
1245
|
+
// nor decodes again.
|
|
1246
|
+
let flash;
|
|
1233
1247
|
if (isServer) {
|
|
1234
1248
|
const e = getRequestEvent();
|
|
1235
1249
|
if (e && !(e.router && e.router.submission)) {
|
|
1236
|
-
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1250
|
+
flash = flashStates.get(e);
|
|
1251
|
+
if (!flash) {
|
|
1252
|
+
const cookieHeader = e.request.headers.get("cookie");
|
|
1253
|
+
if (hasFlashCookie(cookieHeader)) {
|
|
1254
|
+
flashStates.set(e, flash = {
|
|
1255
|
+
cookieHeader
|
|
1256
|
+
});
|
|
1257
|
+
// one-shot: clear it even when unreadable so it can't haunt later renders
|
|
1258
|
+
if (e.response && e.response.headers) e.response.headers.append("Set-Cookie", clearFlashCookie());
|
|
1259
|
+
}
|
|
1241
1260
|
}
|
|
1242
1261
|
}
|
|
1243
1262
|
}
|
|
1244
1263
|
|
|
1245
|
-
// The decode, at most once per request: the decoder may answer with a
|
|
1246
|
-
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
1247
|
-
// async), and this cache is what keeps the parked read's rerun from
|
|
1248
|
-
// restarting it — resumption finds the settled outcome and just reads it.
|
|
1249
|
-
// A decoder that rejects reads as "no flash", matching the runtime's own
|
|
1250
|
-
// malformed-cookie semantics.
|
|
1251
|
-
let flashDecode;
|
|
1252
|
-
|
|
1253
1264
|
// The seeding read, as a memo: NotReadyError must surface from a reactive
|
|
1254
1265
|
// node the graph can park and retry — never from router setup, which no
|
|
1255
|
-
// boundary guards — and the memo bounds the recompute to this function
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1258
|
-
//
|
|
1266
|
+
// boundary guards — and the memo bounds the recompute to this function.
|
|
1267
|
+
// The decode runs at most once per request: the decoder may answer with a
|
|
1268
|
+
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
1269
|
+
// async), and the request's cached state is what keeps a parked read's
|
|
1270
|
+
// rerun — or a re-created router's first read — from restarting it. A
|
|
1271
|
+
// decoder that rejects reads as "no flash", matching the runtime's own
|
|
1272
|
+
// malformed-cookie semantics. Created only when a flash cookie actually
|
|
1273
|
+
// arrived (server-only by construction: `flash` is only ever set there), and
|
|
1259
1274
|
// - `lazy`: server memos compute eagerly by default — deferred to first
|
|
1260
1275
|
// read, a request whose submissions are never read never decodes;
|
|
1261
1276
|
// - `transparent`: the memo exists on the server only, so its owner
|
|
1262
1277
|
// must not consume a hydration-id slot — the client, which seeds
|
|
1263
1278
|
// submissions as [] without ever creating this memo, would miss it
|
|
1264
1279
|
// and every sibling id would shift.
|
|
1265
|
-
const flashSubmission =
|
|
1280
|
+
const flashSubmission = flash !== undefined ? createMemo(() => {
|
|
1281
|
+
const state = flash;
|
|
1266
1282
|
if (!flashDecoder) return undefined;
|
|
1267
|
-
if (!
|
|
1268
|
-
const promise = flashDecoder(
|
|
1269
|
-
|
|
1283
|
+
if (!state.decode) {
|
|
1284
|
+
const promise = flashDecoder(state.cookieHeader).then(value => {
|
|
1285
|
+
state.decode = {
|
|
1270
1286
|
done: true,
|
|
1271
1287
|
value
|
|
1272
1288
|
};
|
|
1273
1289
|
}, () => {
|
|
1274
|
-
|
|
1290
|
+
state.decode = {
|
|
1275
1291
|
done: true,
|
|
1276
1292
|
value: undefined
|
|
1277
1293
|
};
|
|
1278
1294
|
});
|
|
1279
|
-
|
|
1295
|
+
state.decode = {
|
|
1280
1296
|
done: false,
|
|
1281
1297
|
promise
|
|
1282
1298
|
};
|
|
1283
1299
|
}
|
|
1284
1300
|
// SSR carries the Promise through NotReadyError so the parked
|
|
1285
1301
|
// reader can resume, exactly like the lazy matches above.
|
|
1286
|
-
if (!
|
|
1287
|
-
return
|
|
1302
|
+
if (!state.decode.done) throw new NotReadyError(state.decode.promise);
|
|
1303
|
+
return state.decode.value;
|
|
1288
1304
|
}, {
|
|
1289
1305
|
lazy: true,
|
|
1290
1306
|
transparent: true
|
|
@@ -3086,7 +3102,7 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
3086
3102
|
// stays a no-JS fallback.
|
|
3087
3103
|
// Client-only actions (`https://action/`) are their module's JS by
|
|
3088
3104
|
// definition, so a miss there falls through to native submission.
|
|
3089
|
-
const handler =
|
|
3105
|
+
const handler = findAction(actionRef) || serverAction && createServerFormAction(actionRef);
|
|
3090
3106
|
if (handler) {
|
|
3091
3107
|
evt.preventDefault();
|
|
3092
3108
|
const data = new FormData(form, evt.submitter);
|
|
@@ -3097,6 +3113,30 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
3097
3113
|
}
|
|
3098
3114
|
}
|
|
3099
3115
|
|
|
3116
|
+
/**
|
|
3117
|
+
* Looks a rendered action url up in the registry. A server-rendered
|
|
3118
|
+
* `.with()` url is only registered if this client made the same binding
|
|
3119
|
+
* itself; otherwise its base action (the url without `?args`) is rebound to
|
|
3120
|
+
* the rendered arguments, so the submission runs through that action's
|
|
3121
|
+
* submit and settled hooks and is recorded under its base.
|
|
3122
|
+
*/
|
|
3123
|
+
function findAction(url) {
|
|
3124
|
+
const handler = actions.get(url);
|
|
3125
|
+
if (handler) return handler;
|
|
3126
|
+
const query = url.indexOf("?");
|
|
3127
|
+
if (query < 0) return undefined;
|
|
3128
|
+
const base = actions.get(url.slice(0, query));
|
|
3129
|
+
const args = new URLSearchParams(url.slice(query)).get("args");
|
|
3130
|
+
if (!base || args === null) return undefined;
|
|
3131
|
+
let bound;
|
|
3132
|
+
try {
|
|
3133
|
+
bound = JSON.parse(args);
|
|
3134
|
+
} catch {
|
|
3135
|
+
return undefined;
|
|
3136
|
+
}
|
|
3137
|
+
return Array.isArray(bound) ? base.with(...bound) : undefined;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3100
3140
|
/**
|
|
3101
3141
|
* Synthesizes a router action for a server-rendered action url. The url
|
|
3102
3142
|
* carries everything an invocation needs — the function id in the path
|
|
@@ -3130,7 +3170,7 @@ function createServerFormAction(url) {
|
|
|
3130
3170
|
* the enctype conversion and the generic invocation happen here.
|
|
3131
3171
|
*/
|
|
3132
3172
|
function submitServerForm(router, url, form, data) {
|
|
3133
|
-
const handler =
|
|
3173
|
+
const handler = findAction(url) || createServerFormAction(url);
|
|
3134
3174
|
// not an address (`<endpoint>/<id>`) — not the server function convention;
|
|
3135
3175
|
// nothing can run it, resubmit natively (submit() bypasses the delegated
|
|
3136
3176
|
// handler)
|
|
@@ -3282,9 +3322,12 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
3282
3322
|
return url;
|
|
3283
3323
|
};
|
|
3284
3324
|
fn.with = function (...args) {
|
|
3325
|
+
const bound = [...boundArgs, ...args];
|
|
3285
3326
|
const uri = new URL(url, mockBase);
|
|
3286
|
-
|
|
3287
|
-
|
|
3327
|
+
// the server prepends `args` to the submitted arguments, so it must carry
|
|
3328
|
+
// the whole binding, not just this call's part of a chain
|
|
3329
|
+
uri.searchParams.set("args", hashKey(bound));
|
|
3330
|
+
return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, bound, base, submitHooks, settledHooks);
|
|
3288
3331
|
};
|
|
3289
3332
|
fn.onSubmit = function (hook) {
|
|
3290
3333
|
const id = Symbol("actionOnSubmitHook");
|
package/dist/routing.js
CHANGED
|
@@ -556,6 +556,7 @@ let flashDecoder;
|
|
|
556
556
|
export function provideFlashDecoder(decoder) {
|
|
557
557
|
flashDecoder || (flashDecoder = decoder);
|
|
558
558
|
}
|
|
559
|
+
const flashStates = new WeakMap();
|
|
559
560
|
let preloadIntent;
|
|
560
561
|
export function getIntent() {
|
|
561
562
|
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
@@ -592,56 +593,60 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
592
593
|
// unread outcome must not haunt a later request's render. Only detection
|
|
593
594
|
// and clearing happen here (the runtime's isomorphic half); the raw header
|
|
594
595
|
// is stashed and decoding waits for the action-provided codec, read when
|
|
595
|
-
// the lazily allocated submissions signal below first initializes.
|
|
596
|
-
|
|
596
|
+
// the lazily allocated submissions signal below first initializes. Both
|
|
597
|
+
// happen once per request: a router re-created for the same request finds
|
|
598
|
+
// the request's flash state (see `flashStates`) and neither clears again
|
|
599
|
+
// nor decodes again.
|
|
600
|
+
let flash;
|
|
597
601
|
if (isServer) {
|
|
598
602
|
const e = getRequestEvent();
|
|
599
603
|
if (e && !(e.router && e.router.submission)) {
|
|
600
|
-
|
|
601
|
-
if (
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
flash = flashStates.get(e);
|
|
605
|
+
if (!flash) {
|
|
606
|
+
const cookieHeader = e.request.headers.get("cookie");
|
|
607
|
+
if (hasFlashCookie(cookieHeader)) {
|
|
608
|
+
flashStates.set(e, (flash = { cookieHeader }));
|
|
609
|
+
// one-shot: clear it even when unreadable so it can't haunt later renders
|
|
610
|
+
if (e.response && e.response.headers)
|
|
611
|
+
e.response.headers.append("Set-Cookie", clearFlashCookie());
|
|
612
|
+
}
|
|
606
613
|
}
|
|
607
614
|
}
|
|
608
615
|
}
|
|
609
|
-
// The decode, at most once per request: the decoder may answer with a
|
|
610
|
-
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
611
|
-
// async), and this cache is what keeps the parked read's rerun from
|
|
612
|
-
// restarting it — resumption finds the settled outcome and just reads it.
|
|
613
|
-
// A decoder that rejects reads as "no flash", matching the runtime's own
|
|
614
|
-
// malformed-cookie semantics.
|
|
615
|
-
let flashDecode;
|
|
616
616
|
// The seeding read, as a memo: NotReadyError must surface from a reactive
|
|
617
617
|
// node the graph can park and retry — never from router setup, which no
|
|
618
|
-
// boundary guards — and the memo bounds the recompute to this function
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
618
|
+
// boundary guards — and the memo bounds the recompute to this function.
|
|
619
|
+
// The decode runs at most once per request: the decoder may answer with a
|
|
620
|
+
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
621
|
+
// async), and the request's cached state is what keeps a parked read's
|
|
622
|
+
// rerun — or a re-created router's first read — from restarting it. A
|
|
623
|
+
// decoder that rejects reads as "no flash", matching the runtime's own
|
|
624
|
+
// malformed-cookie semantics. Created only when a flash cookie actually
|
|
625
|
+
// arrived (server-only by construction: `flash` is only ever set there), and
|
|
622
626
|
// - `lazy`: server memos compute eagerly by default — deferred to first
|
|
623
627
|
// read, a request whose submissions are never read never decodes;
|
|
624
628
|
// - `transparent`: the memo exists on the server only, so its owner
|
|
625
629
|
// must not consume a hydration-id slot — the client, which seeds
|
|
626
630
|
// submissions as [] without ever creating this memo, would miss it
|
|
627
631
|
// and every sibling id would shift.
|
|
628
|
-
const flashSubmission =
|
|
632
|
+
const flashSubmission = flash !== undefined
|
|
629
633
|
? createMemo(() => {
|
|
634
|
+
const state = flash;
|
|
630
635
|
if (!flashDecoder)
|
|
631
636
|
return undefined;
|
|
632
|
-
if (!
|
|
633
|
-
const promise = flashDecoder(
|
|
634
|
-
|
|
637
|
+
if (!state.decode) {
|
|
638
|
+
const promise = flashDecoder(state.cookieHeader).then(value => {
|
|
639
|
+
state.decode = { done: true, value };
|
|
635
640
|
}, () => {
|
|
636
|
-
|
|
641
|
+
state.decode = { done: true, value: undefined };
|
|
637
642
|
});
|
|
638
|
-
|
|
643
|
+
state.decode = { done: false, promise };
|
|
639
644
|
}
|
|
640
645
|
// SSR carries the Promise through NotReadyError so the parked
|
|
641
646
|
// reader can resume, exactly like the lazy matches above.
|
|
642
|
-
if (!
|
|
643
|
-
throw new NotReadyError(
|
|
644
|
-
return
|
|
647
|
+
if (!state.decode.done)
|
|
648
|
+
throw new NotReadyError(state.decode.promise);
|
|
649
|
+
return state.decode.value;
|
|
645
650
|
}, { lazy: true, transparent: true })
|
|
646
651
|
: undefined;
|
|
647
652
|
let submissions;
|