@reckona/mreact-router 0.0.149 → 0.0.150
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/README.md +1 -0
- package/dist/actions.d.ts +5 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +53 -1
- package/dist/actions.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +110 -0
- package/dist/client.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +24 -0
- package/dist/render.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +83 -1
- package/src/client.ts +110 -0
- package/src/render.ts +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reckona/mreact-router",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.150",
|
|
4
4
|
"description": "File-system app router, SSR, actions, and deployment adapters for mreact.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jsx",
|
|
@@ -105,20 +105,20 @@
|
|
|
105
105
|
},
|
|
106
106
|
"dependencies": {
|
|
107
107
|
"typescript": "^6.0.3",
|
|
108
|
-
"@reckona/mreact": "0.0.
|
|
109
|
-
"@reckona/mreact-compiler": "0.0.
|
|
110
|
-
"@reckona/mreact-
|
|
111
|
-
"@reckona/mreact-
|
|
112
|
-
"@reckona/mreact-
|
|
113
|
-
"@reckona/mreact-
|
|
114
|
-
"@reckona/mreact-
|
|
115
|
-
"@reckona/mreact-
|
|
116
|
-
"@reckona/mreact-
|
|
108
|
+
"@reckona/mreact": "0.0.150",
|
|
109
|
+
"@reckona/mreact-compiler": "0.0.150",
|
|
110
|
+
"@reckona/mreact-devtools": "0.0.150",
|
|
111
|
+
"@reckona/mreact-query": "0.0.150",
|
|
112
|
+
"@reckona/mreact-reactive-core": "0.0.150",
|
|
113
|
+
"@reckona/mreact-compat": "0.0.150",
|
|
114
|
+
"@reckona/mreact-reactive-dom": "0.0.150",
|
|
115
|
+
"@reckona/mreact-server": "0.0.150",
|
|
116
|
+
"@reckona/mreact-shared": "0.0.150"
|
|
117
117
|
},
|
|
118
118
|
"peerDependencies": {
|
|
119
119
|
"vite": ">=8 <9"
|
|
120
120
|
},
|
|
121
121
|
"optionalDependencies": {
|
|
122
|
-
"@reckona/mreact-router-native": "0.0.
|
|
122
|
+
"@reckona/mreact-router-native": "0.0.150"
|
|
123
123
|
}
|
|
124
124
|
}
|
package/src/actions.ts
CHANGED
|
@@ -286,6 +286,11 @@ export async function dispatchServerActionRequest(options: {
|
|
|
286
286
|
appDir: string;
|
|
287
287
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
288
288
|
request: Request;
|
|
289
|
+
renderSingleFlightNavigation?: ((options: {
|
|
290
|
+
path: string;
|
|
291
|
+
request: Request;
|
|
292
|
+
revalidatedPaths: readonly string[];
|
|
293
|
+
}) => Promise<Response | undefined>) | undefined;
|
|
289
294
|
routeCache?: AppRouterCache | undefined;
|
|
290
295
|
serverActionCacheVersion?: string | undefined;
|
|
291
296
|
serverActions?: AppRouterServerActionOptions | undefined;
|
|
@@ -293,8 +298,14 @@ export async function dispatchServerActionRequest(options: {
|
|
|
293
298
|
const { revalidatedPaths, value } = await withRouteCacheContext(options.routeCache, () =>
|
|
294
299
|
dispatchServerActionRequestWithoutCacheContext(options),
|
|
295
300
|
);
|
|
301
|
+
const singleFlight = await renderSingleFlightActionResponse({
|
|
302
|
+
render: options.renderSingleFlightNavigation,
|
|
303
|
+
request: options.request,
|
|
304
|
+
response: value,
|
|
305
|
+
revalidatedPaths,
|
|
306
|
+
});
|
|
296
307
|
|
|
297
|
-
return withRevalidationHeader(value, revalidatedPaths);
|
|
308
|
+
return withRevalidationHeader(singleFlight ?? value, revalidatedPaths);
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
async function dispatchServerActionRequestWithoutCacheContext(options: {
|
|
@@ -568,6 +579,70 @@ function redirectToFormReferer(request: Request): Response {
|
|
|
568
579
|
});
|
|
569
580
|
}
|
|
570
581
|
|
|
582
|
+
async function renderSingleFlightActionResponse(options: {
|
|
583
|
+
render:
|
|
584
|
+
| ((options: {
|
|
585
|
+
path: string;
|
|
586
|
+
request: Request;
|
|
587
|
+
revalidatedPaths: readonly string[];
|
|
588
|
+
}) => Promise<Response | undefined>)
|
|
589
|
+
| undefined;
|
|
590
|
+
request: Request;
|
|
591
|
+
response: Response;
|
|
592
|
+
revalidatedPaths: readonly string[];
|
|
593
|
+
}): Promise<Response | undefined> {
|
|
594
|
+
if (
|
|
595
|
+
options.render === undefined ||
|
|
596
|
+
options.revalidatedPaths.length === 0 ||
|
|
597
|
+
options.request.headers.get("x-mreact-action-single-flight") !== "1" ||
|
|
598
|
+
!isFormActionRedirectResponse(options.response)
|
|
599
|
+
) {
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const path = sameOriginResponseLocationPath(options.request, options.response);
|
|
604
|
+
|
|
605
|
+
if (path === undefined || !options.revalidatedPaths.includes(normalizeActionPath(path))) {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const rendered = await options.render({
|
|
610
|
+
path,
|
|
611
|
+
request: options.request,
|
|
612
|
+
revalidatedPaths: options.revalidatedPaths,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (rendered === undefined) {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
rendered.headers.set("x-mreact-action-single-flight", "1");
|
|
620
|
+
return rendered;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isFormActionRedirectResponse(response: Response): boolean {
|
|
624
|
+
return response.status >= 300 && response.status < 400 && response.headers.has("location");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function sameOriginResponseLocationPath(request: Request, response: Response): string | undefined {
|
|
628
|
+
const location = response.headers.get("location");
|
|
629
|
+
|
|
630
|
+
if (location === null) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
const requestUrl = new URL(request.url);
|
|
636
|
+
const locationUrl = new URL(location, requestUrl);
|
|
637
|
+
|
|
638
|
+
return locationUrl.origin === requestUrl.origin
|
|
639
|
+
? `${locationUrl.pathname}${locationUrl.search}`
|
|
640
|
+
: undefined;
|
|
641
|
+
} catch {
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
571
646
|
function sameOriginRefererPath(request: Request): string | undefined {
|
|
572
647
|
const referer = request.headers.get("referer");
|
|
573
648
|
|
|
@@ -587,6 +662,13 @@ function sameOriginRefererPath(request: Request): string | undefined {
|
|
|
587
662
|
}
|
|
588
663
|
}
|
|
589
664
|
|
|
665
|
+
function normalizeActionPath(path: string): string {
|
|
666
|
+
const pathname = path.startsWith("/") ? path : `/${path}`;
|
|
667
|
+
const withoutTrailing = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
668
|
+
|
|
669
|
+
return withoutTrailing === "" ? "/" : withoutTrailing;
|
|
670
|
+
}
|
|
671
|
+
|
|
590
672
|
function withRevalidationHeader(response: Response, paths: string[]): Response {
|
|
591
673
|
if (paths.length > 0) {
|
|
592
674
|
response.headers.set("x-mreact-revalidate", paths.join(","));
|
package/src/client.ts
CHANGED
|
@@ -3512,6 +3512,22 @@ function __mreactInstallNavigation() {
|
|
|
3512
3512
|
}
|
|
3513
3513
|
});
|
|
3514
3514
|
__mreactObserveViewportPrefetchAnchors(document);
|
|
3515
|
+
document.addEventListener("submit", (event) => {
|
|
3516
|
+
if (event.defaultPrevented || typeof fetch !== "function") {
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
const submission = __mreactServerActionSubmissionFromEvent(event);
|
|
3521
|
+
|
|
3522
|
+
if (submission === null) {
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
event.preventDefault();
|
|
3527
|
+
void __mreactSubmitServerActionForm(submission).catch(() => {
|
|
3528
|
+
location.href = submission.action;
|
|
3529
|
+
});
|
|
3530
|
+
});
|
|
3515
3531
|
document.addEventListener("click", (event) => {
|
|
3516
3532
|
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
3517
3533
|
return;
|
|
@@ -3562,6 +3578,100 @@ function __mreactInstallNavigation() {
|
|
|
3562
3578
|
});
|
|
3563
3579
|
}
|
|
3564
3580
|
|
|
3581
|
+
function __mreactServerActionSubmissionFromEvent(event) {
|
|
3582
|
+
const target = event.target;
|
|
3583
|
+
const form = target instanceof Element ? target.closest("form") : null;
|
|
3584
|
+
const submitter = __mreactFormSubmitterFromEvent(event);
|
|
3585
|
+
|
|
3586
|
+
if (!(form instanceof HTMLFormElement)) {
|
|
3587
|
+
return null;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
const action = submitter?.getAttribute("formaction") ?? form.action;
|
|
3591
|
+
const method = submitter?.getAttribute("formmethod") ?? form.method;
|
|
3592
|
+
const targetName = submitter?.getAttribute("formtarget") ?? form.target;
|
|
3593
|
+
const actionUrl = new URL(action, location.href);
|
|
3594
|
+
|
|
3595
|
+
if (
|
|
3596
|
+
targetName !== "" ||
|
|
3597
|
+
actionUrl.origin !== location.origin ||
|
|
3598
|
+
actionUrl.pathname !== "/_mreact/actions" ||
|
|
3599
|
+
method.toUpperCase() !== "POST"
|
|
3600
|
+
) {
|
|
3601
|
+
return null;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
return { action: actionUrl.href, form, submitter };
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
function __mreactFormSubmitterFromEvent(event) {
|
|
3608
|
+
const submitter = event.submitter;
|
|
3609
|
+
|
|
3610
|
+
return submitter instanceof HTMLElement ? submitter : null;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
async function __mreactSubmitServerActionForm(submission) {
|
|
3614
|
+
const response = await fetch(submission.action, {
|
|
3615
|
+
body: __mreactServerActionFormData(submission.form, submission.submitter),
|
|
3616
|
+
headers: { "x-mreact-action-single-flight": "1" },
|
|
3617
|
+
method: "POST",
|
|
3618
|
+
});
|
|
3619
|
+
__mreactApplyRevalidationHeader(response);
|
|
3620
|
+
|
|
3621
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3622
|
+
const singleFlight = response.headers.get("x-mreact-action-single-flight") === "1";
|
|
3623
|
+
if (
|
|
3624
|
+
!singleFlight &&
|
|
3625
|
+
!contentType.includes("text/html")
|
|
3626
|
+
) {
|
|
3627
|
+
location.href = response.url || submission.action;
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
const html = await response.text();
|
|
3632
|
+
const applied = __mreactApplyServerActionHtml(html, singleFlight);
|
|
3633
|
+
|
|
3634
|
+
if (!applied) {
|
|
3635
|
+
location.href = response.url || submission.action;
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
function __mreactServerActionFormData(form, submitter) {
|
|
3640
|
+
if (submitter !== null) {
|
|
3641
|
+
try {
|
|
3642
|
+
return new FormData(form, submitter);
|
|
3643
|
+
} catch {
|
|
3644
|
+
// Older DOM implementations only accept the form argument.
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
return new FormData(form);
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
function __mreactApplyServerActionHtml(html, singleFlight) {
|
|
3652
|
+
__mreactSaveCurrentHistoryState();
|
|
3653
|
+
const applied = __mreactApplyNavigationHtml(html, location.href);
|
|
3654
|
+
|
|
3655
|
+
if (!applied) {
|
|
3656
|
+
if (
|
|
3657
|
+
!singleFlight ||
|
|
3658
|
+
typeof document === "undefined" ||
|
|
3659
|
+
typeof document.open !== "function"
|
|
3660
|
+
) {
|
|
3661
|
+
return false;
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
document.open();
|
|
3665
|
+
document.write(html);
|
|
3666
|
+
document.close();
|
|
3667
|
+
return true;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
__mreactResetNavigationFocus();
|
|
3671
|
+
__mreactAnnounceNavigation();
|
|
3672
|
+
return true;
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3565
3675
|
function __mreactInstallNavigationFetchRevalidation() {
|
|
3566
3676
|
if (
|
|
3567
3677
|
__mreactNavigationState.fetchRevalidationInstalled ||
|
package/src/render.ts
CHANGED
|
@@ -812,6 +812,16 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
812
812
|
appDir: options.appDir,
|
|
813
813
|
importPolicy: options.importPolicy,
|
|
814
814
|
request: options.request,
|
|
815
|
+
renderSingleFlightNavigation: async (singleFlight) =>
|
|
816
|
+
renderAppRequestInternal({
|
|
817
|
+
...options,
|
|
818
|
+
matchedRoute: undefined,
|
|
819
|
+
request: singleFlightNavigationRequest({
|
|
820
|
+
path: singleFlight.path,
|
|
821
|
+
request: singleFlight.request,
|
|
822
|
+
}),
|
|
823
|
+
requestUrl: undefined,
|
|
824
|
+
}),
|
|
815
825
|
routeCache: options.routeCache,
|
|
816
826
|
...(options.serverModuleCacheVersion === undefined
|
|
817
827
|
? {}
|
|
@@ -1672,6 +1682,25 @@ function isNavigationRouteCacheReloadRequest(request: Request): boolean {
|
|
|
1672
1682
|
);
|
|
1673
1683
|
}
|
|
1674
1684
|
|
|
1685
|
+
function singleFlightNavigationRequest(options: { path: string; request: Request }): Request {
|
|
1686
|
+
const actionUrl = new URL(options.request.url);
|
|
1687
|
+
const targetUrl = new URL(options.path, actionUrl);
|
|
1688
|
+
const headers = new Headers();
|
|
1689
|
+
const cookie = options.request.headers.get("cookie");
|
|
1690
|
+
|
|
1691
|
+
if (cookie !== null) {
|
|
1692
|
+
headers.set("cookie", cookie);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
headers.set("x-mreact-navigation", "1");
|
|
1696
|
+
headers.set("x-mreact-navigation-cache", "reload");
|
|
1697
|
+
|
|
1698
|
+
return new Request(targetUrl, {
|
|
1699
|
+
headers,
|
|
1700
|
+
method: "GET",
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1675
1704
|
async function nearestBoundaryFileForPage(options: {
|
|
1676
1705
|
appDir: string;
|
|
1677
1706
|
filename: string;
|