@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.2 → 3.9.0-ultramodern.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.
- package/dist/cjs/backend-federation-build/codegen.js +267 -0
- package/dist/cjs/backend-federation-build/config.js +239 -0
- package/dist/cjs/backend-federation-build/emit.js +154 -0
- package/dist/cjs/backend-federation-build/index.js +53 -0
- package/dist/cjs/build-config/build-environment.js +129 -0
- package/dist/cjs/build-config/precompress/plugin.js +124 -0
- package/dist/cjs/build-config/precompress/types.js +18 -0
- package/dist/cjs/build-config/public.js +43 -0
- package/dist/cjs/build-config/remote-address.js +58 -0
- package/dist/cjs/cloudflare/artifacts.js +8 -19
- package/dist/cjs/cloudflare/constants.js +1 -1
- package/dist/cjs/cloudflare/delivery-unit.js +23 -42
- package/dist/cjs/cloudflare/index.js +10 -2
- package/dist/cjs/cloudflare/worker-manifest.js +13 -4
- package/dist/cjs/cloudflare/wrangler-config.js +35 -7
- package/dist/cjs/cloudflare-builder.js +252 -75
- package/dist/cjs/cloudflare-output-contract.js +69 -3
- package/dist/cjs/cloudflare-output-verifier/identity.js +29 -27
- package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
- package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
- package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
- package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +14 -6
- package/dist/cjs/config.js +34 -1
- package/dist/cjs/deploy-output/npmAliases.js +251 -0
- package/dist/cjs/deploy-output/plugin.js +97 -0
- package/dist/cjs/deploy-output/public-assets.js +146 -0
- package/dist/cjs/deploy-output/target.js +104 -0
- package/dist/cjs/deploy-output/trace-files.js +90 -0
- package/dist/cjs/native-type-checker.js +193 -0
- package/dist/cjs/policy-defaults.js +101 -0
- package/dist/cjs/release-envelope/framework-output.js +153 -79
- package/dist/cjs/release-envelope/index.js +10 -4
- package/dist/cjs/release-envelope/plugin.js +18 -13
- package/dist/cjs/release-identity.js +5 -6
- package/dist/cjs/runtime-package-resolution.js +118 -0
- package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
- package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
- package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
- package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
- package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/esm/backend-federation-build/codegen.mjs +216 -0
- package/dist/esm/backend-federation-build/config.mjs +163 -0
- package/dist/esm/backend-federation-build/emit.mjs +103 -0
- package/dist/esm/backend-federation-build/index.mjs +1 -0
- package/dist/esm/build-config/build-environment.mjs +88 -0
- package/dist/esm/build-config/precompress/plugin.mjs +76 -0
- package/dist/esm/build-config/precompress/types.mjs +0 -0
- package/dist/esm/build-config/public.mjs +1 -0
- package/dist/esm/build-config/remote-address.mjs +20 -0
- package/dist/esm/cloudflare/artifacts.mjs +8 -19
- package/dist/esm/cloudflare/constants.mjs +1 -1
- package/dist/esm/cloudflare/delivery-unit.mjs +17 -36
- package/dist/esm/cloudflare/index.mjs +10 -2
- package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
- package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
- package/dist/esm/cloudflare-builder.mjs +246 -75
- package/dist/esm/cloudflare-output-contract.mjs +59 -2
- package/dist/esm/cloudflare-output-verifier/identity.mjs +27 -25
- package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
- package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
- package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
- package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +15 -7
- package/dist/esm/config.mjs +3 -0
- package/dist/esm/deploy-output/npmAliases.mjs +199 -0
- package/dist/esm/deploy-output/plugin.mjs +46 -0
- package/dist/esm/deploy-output/public-assets.mjs +82 -0
- package/dist/esm/deploy-output/target.mjs +57 -0
- package/dist/esm/deploy-output/trace-files.mjs +38 -0
- package/dist/esm/native-type-checker.mjs +142 -0
- package/dist/esm/policy-defaults.mjs +45 -0
- package/dist/esm/release-envelope/framework-output.mjs +130 -56
- package/dist/esm/release-envelope/index.mjs +10 -4
- package/dist/esm/release-envelope/plugin.mjs +18 -13
- package/dist/esm/release-identity.mjs +5 -6
- package/dist/esm/runtime-package-resolution.mjs +60 -0
- package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
- package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
- package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
- package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
- package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/esm-node/backend-federation-build/codegen.mjs +217 -0
- package/dist/esm-node/backend-federation-build/config.mjs +164 -0
- package/dist/esm-node/backend-federation-build/emit.mjs +104 -0
- package/dist/esm-node/backend-federation-build/index.mjs +2 -0
- package/dist/esm-node/build-config/build-environment.mjs +89 -0
- package/dist/esm-node/build-config/precompress/plugin.mjs +77 -0
- package/dist/esm-node/build-config/precompress/types.mjs +1 -0
- package/dist/esm-node/build-config/public.mjs +2 -0
- package/dist/esm-node/build-config/remote-address.mjs +21 -0
- package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
- package/dist/esm-node/cloudflare/constants.mjs +1 -1
- package/dist/esm-node/cloudflare/delivery-unit.mjs +17 -36
- package/dist/esm-node/cloudflare/index.mjs +10 -2
- package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
- package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
- package/dist/esm-node/cloudflare-builder.mjs +246 -75
- package/dist/esm-node/cloudflare-output-contract.mjs +59 -2
- package/dist/esm-node/cloudflare-output-verifier/identity.mjs +27 -25
- package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
- package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
- package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
- package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +15 -7
- package/dist/esm-node/config.mjs +3 -0
- package/dist/esm-node/deploy-output/npmAliases.mjs +200 -0
- package/dist/esm-node/deploy-output/plugin.mjs +49 -0
- package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
- package/dist/esm-node/deploy-output/target.mjs +58 -0
- package/dist/esm-node/deploy-output/trace-files.mjs +39 -0
- package/dist/esm-node/native-type-checker.mjs +143 -0
- package/dist/esm-node/policy-defaults.mjs +46 -0
- package/dist/esm-node/release-envelope/framework-output.mjs +130 -56
- package/dist/esm-node/release-envelope/index.mjs +10 -4
- package/dist/esm-node/release-envelope/plugin.mjs +18 -13
- package/dist/esm-node/release-identity.mjs +5 -6
- package/dist/esm-node/runtime-package-resolution.mjs +61 -0
- package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
- package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
- package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
- package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
- package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/types/backend-federation-build/codegen.d.ts +116 -0
- package/dist/types/backend-federation-build/config.d.ts +121 -0
- package/dist/types/backend-federation-build/emit.d.ts +21 -0
- package/dist/types/backend-federation-build/index.d.ts +2 -0
- package/dist/types/build-config/build-environment.d.ts +14 -0
- package/dist/types/build-config/precompress/plugin.d.ts +4 -0
- package/dist/types/build-config/precompress/types.d.ts +19 -0
- package/dist/types/build-config/public.d.ts +2 -0
- package/dist/types/build-config/remote-address.d.ts +3 -0
- package/dist/types/cloudflare/artifacts.d.ts +7 -6
- package/dist/types/cloudflare/constants.d.ts +1 -1
- package/dist/types/cloudflare/delivery-unit.d.ts +3 -3
- package/dist/types/cloudflare/worker-manifest.d.ts +2 -2
- package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
- package/dist/types/cloudflare-builder.d.ts +49 -9
- package/dist/types/cloudflare-output-contract.d.ts +4 -1
- package/dist/types/cloudflare-output-verifier/identity.d.ts +1 -1
- package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
- package/dist/types/config.d.ts +29 -1
- package/dist/types/deploy-output/npmAliases.d.ts +14 -0
- package/dist/types/deploy-output/plugin.d.ts +35 -0
- package/dist/types/deploy-output/public-assets.d.ts +51 -0
- package/dist/types/deploy-output/target.d.ts +29 -0
- package/dist/types/deploy-output/trace-files.d.ts +17 -0
- package/dist/types/native-type-checker.d.ts +15 -0
- package/dist/types/policy-defaults.d.ts +46 -0
- package/dist/types/precompress/LICENSE +20 -0
- package/dist/types/precompress/index.d.ts +76 -0
- package/dist/types/release-envelope/framework-output.d.ts +8 -3
- package/dist/types/release-envelope/plugin.d.ts +7 -7
- package/dist/types/runtime-package-resolution.d.ts +28 -0
- package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
- package/package.json +97 -15
- package/src/backend-federation-build/codegen.ts +254 -0
- package/src/backend-federation-build/config.ts +492 -0
- package/src/backend-federation-build/emit.ts +213 -0
- package/src/backend-federation-build/index.ts +2 -0
- package/src/build-config/build-environment.ts +160 -0
- package/src/build-config/precompress/plugin.ts +140 -0
- package/src/build-config/precompress/types.ts +26 -0
- package/src/build-config/public.ts +5 -0
- package/src/build-config/remote-address.ts +33 -0
- package/src/cloudflare/artifacts.ts +22 -43
- package/src/cloudflare/constants.ts +1 -1
- package/src/cloudflare/delivery-unit.ts +42 -53
- package/src/cloudflare/index.ts +24 -11
- package/src/cloudflare/worker-manifest.ts +20 -3
- package/src/cloudflare/wrangler-config.ts +86 -10
- package/src/cloudflare-builder.ts +475 -151
- package/src/cloudflare-output-contract.ts +70 -0
- package/src/cloudflare-output-verifier/identity.ts +5 -1
- package/src/cloudflare-output-verifier/index.ts +20 -0
- package/src/cloudflare-output-verifier/manifest.ts +77 -0
- package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
- package/src/cloudflare-output-verifier/worker-bundles.ts +32 -14
- package/src/config.ts +34 -6
- package/src/deploy-output/npmAliases.ts +382 -0
- package/src/deploy-output/plugin.ts +86 -0
- package/src/deploy-output/public-assets.ts +224 -0
- package/src/deploy-output/target.ts +106 -0
- package/src/deploy-output/trace-files.ts +67 -0
- package/src/native-type-checker.ts +183 -0
- package/src/policy-defaults.ts +127 -0
- package/src/release-envelope/framework-output.ts +346 -94
- package/src/release-envelope/index.ts +26 -5
- package/src/release-envelope/plugin.ts +43 -40
- package/src/release-identity.ts +13 -2
- package/src/runtime-package-resolution.ts +115 -0
- package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
- package/src/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
- package/src/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
- package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
- package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/dist/cjs/templates/cloudflare-worker-path.mjs +0 -98
- package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/dist/esm/templates/cloudflare-worker-path.mjs +0 -98
- package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/dist/esm-node/templates/cloudflare-worker-path.mjs +0 -98
- package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +0 -5
- package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +0 -48
- package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +0 -13
- package/dist/types/templates/cloudflare-worker-path.d.mts +0 -21
- package/src/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/src/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/src/templates/cloudflare-worker-path.mjs +0 -98
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare worker route data handler.
|
|
3
|
+
*
|
|
4
|
+
* Answers Modern.js route data requests (`?__loader=`) inside the worker with
|
|
5
|
+
* the entry's route loaders, the way the Node server's data handler
|
|
6
|
+
* (`@modern-js/plugin-data-loader/runtime` `handleRequest`) does: same route
|
|
7
|
+
* IDs and localized route identity, same loader and action arguments, same
|
|
8
|
+
* request context, same response encoding.
|
|
9
|
+
*
|
|
10
|
+
* It does not import react-router. The Node handler matches routes with
|
|
11
|
+
* react-router's static handler, and react-router's main entry keeps
|
|
12
|
+
* `Link -> PrefetchPageLinks -> import(route.module)` alive in minified worker
|
|
13
|
+
* bundles, which the Cloudflare output verifier rejects. Route matching and
|
|
14
|
+
* the static handler's route request semantics are ported below instead.
|
|
15
|
+
*
|
|
16
|
+
* Route matching, redirect normalization and route request handling are
|
|
17
|
+
* modified from React Router 7.18.4
|
|
18
|
+
* (https://github.com/remix-run/react-router/tree/react-router%407.18.4/packages/react-router/lib/router),
|
|
19
|
+
* MIT Licensed, Copyright (c) Remix Software Inc. and React Training LLC.
|
|
20
|
+
* Response encoding is modified from `@modern-js/plugin-data-loader/runtime`,
|
|
21
|
+
* which is based on
|
|
22
|
+
* https://github.com/remix-run/remix/blob/2b5e1a72fc628d0408e27cf4d72e537762f1dc5b/packages/remix-server-runtime/responses.ts,
|
|
23
|
+
* MIT Licensed, Copyright 2021 Remix Software Inc.
|
|
24
|
+
*/
|
|
25
|
+
import { DeferredData } from '@modern-js/runtime-utils/browser';
|
|
26
|
+
import {
|
|
27
|
+
createRequestContext,
|
|
28
|
+
reporterCtx,
|
|
29
|
+
serializeJson,
|
|
30
|
+
storage,
|
|
31
|
+
} from '@modern-js/runtime-utils/node';
|
|
32
|
+
import { time } from '@modern-js/runtime-utils/time';
|
|
33
|
+
import { parseHeaders } from '@modern-js/runtime-utils/universal/request';
|
|
34
|
+
import {
|
|
35
|
+
expandLocalisedLoaderRoutes,
|
|
36
|
+
resolveLocalisedLoaderRoute,
|
|
37
|
+
} from '@modern-js/server-runtime-extensions/localised-loader';
|
|
38
|
+
import { LOADER_REPORTER_NAME } from '@modern-js/utils/universal/constants';
|
|
39
|
+
|
|
40
|
+
const LOADER_ID_PARAM = '__loader';
|
|
41
|
+
const CONTENT_TYPE_DEFERRED = 'text/modernjs-deferred';
|
|
42
|
+
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = '__deferred_promise:';
|
|
43
|
+
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
|
|
44
|
+
const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
45
|
+
const REQUEST_METHODS = new Set(['GET', ...MUTATION_METHODS]);
|
|
46
|
+
const ABSOLUTE_URL_PATTERN = /^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i;
|
|
47
|
+
const PARAM_SEGMENT_PATTERN = /^:[\w-]+$/;
|
|
48
|
+
|
|
49
|
+
function invariant(value, message) {
|
|
50
|
+
if (value === false || value === null || typeof value === 'undefined') {
|
|
51
|
+
throw new Error(message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Route matching (react-router matchRoutes) -----------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Give loader routes the IDs and shape react-router gives the routes the
|
|
59
|
+
* data loader runtime renders: a route's own `id`, or its position in the
|
|
60
|
+
* tree, and no children on index routes.
|
|
61
|
+
*/
|
|
62
|
+
function createRouteTree(routes, parentPath = [], ids = new Set()) {
|
|
63
|
+
return routes.map((loaderRoute, position) => {
|
|
64
|
+
const treePath = [...parentPath, position];
|
|
65
|
+
const id = loaderRoute.id || treePath.join('-');
|
|
66
|
+
invariant(
|
|
67
|
+
!ids.has(id),
|
|
68
|
+
`Found a route id collision on id "${id}". Route id's must be globally unique within Data Router usages`,
|
|
69
|
+
);
|
|
70
|
+
ids.add(id);
|
|
71
|
+
const route = {
|
|
72
|
+
id,
|
|
73
|
+
caseSensitive: loaderRoute.caseSensitive,
|
|
74
|
+
index: Boolean(loaderRoute.index),
|
|
75
|
+
path: loaderRoute.path,
|
|
76
|
+
loaderRoute,
|
|
77
|
+
};
|
|
78
|
+
if (!route.index && loaderRoute.children) {
|
|
79
|
+
route.children = createRouteTree(loaderRoute.children, treePath, ids);
|
|
80
|
+
}
|
|
81
|
+
return route;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const removeDoubleSlashes = path => path.replace(/[\\/]{2,}/g, '/');
|
|
86
|
+
const joinPaths = paths => removeDoubleSlashes(paths.join('/'));
|
|
87
|
+
|
|
88
|
+
function removeTrailingSlash(path, minLength = 0) {
|
|
89
|
+
let end = path.length;
|
|
90
|
+
while (end > minLength && path.charCodeAt(end - 1) === 47) {
|
|
91
|
+
end--;
|
|
92
|
+
}
|
|
93
|
+
return end === path.length ? path : path.slice(0, end);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const normalizePathname = pathname =>
|
|
97
|
+
removeTrailingSlash(pathname).replace(/^\/*/, '/');
|
|
98
|
+
|
|
99
|
+
function explodeOptionalSegments(path) {
|
|
100
|
+
const segments = path.split('/');
|
|
101
|
+
if (segments.length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const [first, ...rest] = segments;
|
|
105
|
+
const isOptional = first.endsWith('?');
|
|
106
|
+
const required = first.replace(/\?$/, '');
|
|
107
|
+
if (rest.length === 0) {
|
|
108
|
+
return isOptional ? [required, ''] : [required];
|
|
109
|
+
}
|
|
110
|
+
const restExploded = explodeOptionalSegments(rest.join('/'));
|
|
111
|
+
const result = restExploded.map(subpath =>
|
|
112
|
+
subpath === '' ? required : [required, subpath].join('/'),
|
|
113
|
+
);
|
|
114
|
+
if (isOptional) {
|
|
115
|
+
result.push(...restExploded);
|
|
116
|
+
}
|
|
117
|
+
return result.map(exploded =>
|
|
118
|
+
path.startsWith('/') && exploded === '' ? '/' : exploded,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function computeScore(path, index) {
|
|
123
|
+
const segments = path.split('/');
|
|
124
|
+
let initialScore = segments.length;
|
|
125
|
+
if (segments.some(segment => segment === '*')) {
|
|
126
|
+
initialScore += -2;
|
|
127
|
+
}
|
|
128
|
+
if (index) {
|
|
129
|
+
initialScore += 2;
|
|
130
|
+
}
|
|
131
|
+
return segments
|
|
132
|
+
.filter(segment => segment !== '*')
|
|
133
|
+
.reduce(
|
|
134
|
+
(score, segment) =>
|
|
135
|
+
score +
|
|
136
|
+
(PARAM_SEGMENT_PATTERN.test(segment) ? 3 : segment === '' ? 1 : 10),
|
|
137
|
+
initialScore,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function compareIndexes(a, b) {
|
|
142
|
+
const siblings =
|
|
143
|
+
a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
|
|
144
|
+
// Siblings keep their declaration order; other branches rank equally.
|
|
145
|
+
return siblings ? a[a.length - 1] - b[b.length - 1] : 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function compilePath(path, caseSensitive = false, end = true) {
|
|
149
|
+
const params = [];
|
|
150
|
+
let regexpSource = `^${path
|
|
151
|
+
.replace(/\/*\*?$/, '')
|
|
152
|
+
.replace(/^\/*/, '/')
|
|
153
|
+
.replace(/[\\.*+^${}|()[\]]/g, '\\$&')
|
|
154
|
+
.replace(
|
|
155
|
+
/\/:([\w-]+)(\?)?/g,
|
|
156
|
+
(match, paramName, isOptional, index, str) => {
|
|
157
|
+
params.push({ paramName, isOptional: isOptional != null });
|
|
158
|
+
if (isOptional) {
|
|
159
|
+
const nextChar = str.charAt(index + match.length);
|
|
160
|
+
if (nextChar && nextChar !== '/') {
|
|
161
|
+
return '/([^\\/]*)';
|
|
162
|
+
}
|
|
163
|
+
return '(?:/([^\\/]*))?';
|
|
164
|
+
}
|
|
165
|
+
return '/([^\\/]+)';
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
.replace(/\/([\w-]+)\?(\/|$)/g, '(/$1)?$2')}`;
|
|
169
|
+
if (path.endsWith('*')) {
|
|
170
|
+
params.push({ paramName: '*' });
|
|
171
|
+
regexpSource +=
|
|
172
|
+
path === '*' || path === '/*' ? '(.*)$' : '(?:\\/(.+)|\\/*)$';
|
|
173
|
+
} else if (end) {
|
|
174
|
+
regexpSource += '\\/*$';
|
|
175
|
+
} else if (path !== '' && path !== '/') {
|
|
176
|
+
regexpSource += '(?:(?=\\/|$))';
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
matcher: new RegExp(regexpSource, caseSensitive ? undefined : 'i'),
|
|
180
|
+
compiledParams: params,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function flattenRoutes(
|
|
185
|
+
routes,
|
|
186
|
+
branches = [],
|
|
187
|
+
parentsMeta = [],
|
|
188
|
+
parentPath = '',
|
|
189
|
+
parentHasOptionalSegments = false,
|
|
190
|
+
) {
|
|
191
|
+
const flattenRoute = (
|
|
192
|
+
route,
|
|
193
|
+
childrenIndex,
|
|
194
|
+
hasOptionalSegments = parentHasOptionalSegments,
|
|
195
|
+
relativePath = undefined,
|
|
196
|
+
) => {
|
|
197
|
+
const meta = {
|
|
198
|
+
relativePath:
|
|
199
|
+
relativePath === undefined ? route.path || '' : relativePath,
|
|
200
|
+
caseSensitive: route.caseSensitive === true,
|
|
201
|
+
childrenIndex,
|
|
202
|
+
route,
|
|
203
|
+
};
|
|
204
|
+
if (meta.relativePath.startsWith('/')) {
|
|
205
|
+
if (!meta.relativePath.startsWith(parentPath) && hasOptionalSegments) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
invariant(
|
|
209
|
+
meta.relativePath.startsWith(parentPath),
|
|
210
|
+
`Absolute route path "${meta.relativePath}" nested under path "${parentPath}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`,
|
|
211
|
+
);
|
|
212
|
+
meta.relativePath = meta.relativePath.slice(parentPath.length);
|
|
213
|
+
}
|
|
214
|
+
const path = joinPaths([parentPath, meta.relativePath]);
|
|
215
|
+
const routesMeta = parentsMeta.concat(meta);
|
|
216
|
+
if (route.children && route.children.length > 0) {
|
|
217
|
+
flattenRoutes(
|
|
218
|
+
route.children,
|
|
219
|
+
branches,
|
|
220
|
+
routesMeta,
|
|
221
|
+
path,
|
|
222
|
+
hasOptionalSegments,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (route.path == null && !route.index) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
branches.push({
|
|
229
|
+
path,
|
|
230
|
+
score: computeScore(path, route.index),
|
|
231
|
+
routesMeta: routesMeta.map((routeMeta, i) => ({
|
|
232
|
+
...routeMeta,
|
|
233
|
+
...compilePath(
|
|
234
|
+
routeMeta.relativePath,
|
|
235
|
+
routeMeta.caseSensitive,
|
|
236
|
+
i === routesMeta.length - 1,
|
|
237
|
+
),
|
|
238
|
+
})),
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
routes.forEach((route, childrenIndex) => {
|
|
242
|
+
if (route.path === '' || !route.path?.includes('?')) {
|
|
243
|
+
flattenRoute(route, childrenIndex);
|
|
244
|
+
} else {
|
|
245
|
+
for (const exploded of explodeOptionalSegments(route.path)) {
|
|
246
|
+
flattenRoute(route, childrenIndex, true, exploded);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return branches;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function flattenAndRankRoutes(routes) {
|
|
254
|
+
return flattenRoutes(routes).sort((a, b) =>
|
|
255
|
+
a.score !== b.score
|
|
256
|
+
? b.score - a.score
|
|
257
|
+
: compareIndexes(
|
|
258
|
+
a.routesMeta.map(meta => meta.childrenIndex),
|
|
259
|
+
b.routesMeta.map(meta => meta.childrenIndex),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function matchPathImpl(pathname, matcher, compiledParams) {
|
|
265
|
+
const match = pathname.match(matcher);
|
|
266
|
+
if (!match) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const matchedPathname = match[0];
|
|
270
|
+
let pathnameBase = removeTrailingSlash(matchedPathname, 1);
|
|
271
|
+
const captureGroups = match.slice(1);
|
|
272
|
+
const params = compiledParams.reduce(
|
|
273
|
+
(memo, { paramName, isOptional }, index) => {
|
|
274
|
+
if (paramName === '*') {
|
|
275
|
+
const splatValue = captureGroups[index] || '';
|
|
276
|
+
pathnameBase = removeTrailingSlash(
|
|
277
|
+
matchedPathname.slice(0, matchedPathname.length - splatValue.length),
|
|
278
|
+
1,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const value = captureGroups[index];
|
|
282
|
+
memo[paramName] =
|
|
283
|
+
isOptional && !value ? undefined : (value || '').replace(/%2F/g, '/');
|
|
284
|
+
return memo;
|
|
285
|
+
},
|
|
286
|
+
{},
|
|
287
|
+
);
|
|
288
|
+
return { params, pathname: matchedPathname, pathnameBase };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function matchRouteBranch(branch, pathname) {
|
|
292
|
+
// Every match of a branch shares the params of the whole branch.
|
|
293
|
+
const matchedParams = {};
|
|
294
|
+
let matchedPathname = '/';
|
|
295
|
+
const matches = [];
|
|
296
|
+
for (const meta of branch.routesMeta) {
|
|
297
|
+
const remainingPathname =
|
|
298
|
+
matchedPathname === '/'
|
|
299
|
+
? pathname
|
|
300
|
+
: pathname.slice(matchedPathname.length) || '/';
|
|
301
|
+
const match = matchPathImpl(
|
|
302
|
+
remainingPathname,
|
|
303
|
+
meta.matcher,
|
|
304
|
+
meta.compiledParams,
|
|
305
|
+
);
|
|
306
|
+
if (!match) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
Object.assign(matchedParams, match.params);
|
|
310
|
+
matches.push({
|
|
311
|
+
params: matchedParams,
|
|
312
|
+
pathname: joinPaths([matchedPathname, match.pathname]),
|
|
313
|
+
pathnameBase: normalizePathname(
|
|
314
|
+
joinPaths([matchedPathname, match.pathnameBase]),
|
|
315
|
+
),
|
|
316
|
+
route: meta.route,
|
|
317
|
+
});
|
|
318
|
+
if (match.pathnameBase !== '/') {
|
|
319
|
+
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return matches;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function decodePath(value) {
|
|
326
|
+
try {
|
|
327
|
+
return value
|
|
328
|
+
.split('/')
|
|
329
|
+
.map(segment => decodeURIComponent(segment).replace(/\//g, '%2F'))
|
|
330
|
+
.join('/');
|
|
331
|
+
} catch {
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function stripBasename(pathname, basename) {
|
|
337
|
+
if (basename === '/') {
|
|
338
|
+
return pathname;
|
|
339
|
+
}
|
|
340
|
+
if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const startIndex = basename.endsWith('/')
|
|
344
|
+
? basename.length - 1
|
|
345
|
+
: basename.length;
|
|
346
|
+
const nextChar = pathname.charAt(startIndex);
|
|
347
|
+
if (nextChar && nextChar !== '/') {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return pathname.slice(startIndex) || '/';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function matchRoutes(branches, pathname, basename) {
|
|
354
|
+
const strippedPathname = stripBasename(pathname || '/', basename);
|
|
355
|
+
if (strippedPathname == null) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const decodedPathname = decodePath(strippedPathname);
|
|
359
|
+
for (const branch of branches) {
|
|
360
|
+
const matches = matchRouteBranch(branch, decodedPathname);
|
|
361
|
+
if (matches) {
|
|
362
|
+
return matches;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// --- Route requests (react-router createStaticHandler().queryRoute) -------
|
|
369
|
+
|
|
370
|
+
class ErrorResponseImpl {
|
|
371
|
+
constructor(status, statusText, data, internal = false) {
|
|
372
|
+
this.status = status;
|
|
373
|
+
this.statusText = statusText || '';
|
|
374
|
+
this.internal = internal;
|
|
375
|
+
if (data instanceof Error) {
|
|
376
|
+
this.data = data.toString();
|
|
377
|
+
this.error = data;
|
|
378
|
+
} else {
|
|
379
|
+
this.data = data;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isRouteErrorResponse(error) {
|
|
385
|
+
return (
|
|
386
|
+
error != null &&
|
|
387
|
+
typeof error.status === 'number' &&
|
|
388
|
+
typeof error.statusText === 'string' &&
|
|
389
|
+
typeof error.internal === 'boolean' &&
|
|
390
|
+
'data' in error
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getInternalRouterError(status, { pathname, routeId, method } = {}) {
|
|
395
|
+
let statusText = 'Unknown Server Error';
|
|
396
|
+
let errorMessage = 'Unknown @remix-run/router error';
|
|
397
|
+
if (status === 403) {
|
|
398
|
+
statusText = 'Forbidden';
|
|
399
|
+
errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
|
|
400
|
+
} else if (status === 404) {
|
|
401
|
+
statusText = 'Not Found';
|
|
402
|
+
errorMessage = `No route matches URL "${pathname}"`;
|
|
403
|
+
} else if (status === 405) {
|
|
404
|
+
statusText = 'Method Not Allowed';
|
|
405
|
+
if (method && pathname && routeId) {
|
|
406
|
+
errorMessage = `You made a ${method.toUpperCase()} request to "${pathname}" but did not provide an \`action\` for route "${routeId}", so there is no way to handle the request.`;
|
|
407
|
+
} else if (method) {
|
|
408
|
+
errorMessage = `Invalid request method "${method.toUpperCase()}"`;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return new ErrorResponseImpl(
|
|
412
|
+
status || 500,
|
|
413
|
+
statusText,
|
|
414
|
+
new Error(errorMessage),
|
|
415
|
+
true,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isResponse(value) {
|
|
420
|
+
return (
|
|
421
|
+
value != null &&
|
|
422
|
+
typeof value.status === 'number' &&
|
|
423
|
+
typeof value.statusText === 'string' &&
|
|
424
|
+
typeof value.headers === 'object' &&
|
|
425
|
+
typeof value.body !== 'undefined'
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const isRedirectStatus = status => REDIRECT_STATUS_CODES.has(status);
|
|
430
|
+
|
|
431
|
+
function isRouterRedirectResponse(value) {
|
|
432
|
+
return (
|
|
433
|
+
isResponse(value) &&
|
|
434
|
+
isRedirectStatus(value.status) &&
|
|
435
|
+
value.headers.has('Location')
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isDataWithResponseInit(value) {
|
|
440
|
+
return (
|
|
441
|
+
typeof value === 'object' &&
|
|
442
|
+
value != null &&
|
|
443
|
+
'type' in value &&
|
|
444
|
+
'data' in value &&
|
|
445
|
+
'init' in value &&
|
|
446
|
+
value.type === 'DataWithResponseInit'
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isHandlerResult(value) {
|
|
451
|
+
return (
|
|
452
|
+
value != null &&
|
|
453
|
+
typeof value === 'object' &&
|
|
454
|
+
'type' in value &&
|
|
455
|
+
'result' in value &&
|
|
456
|
+
(value.type === 'data' || value.type === 'error')
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function parsePath(path) {
|
|
461
|
+
const parsedPath = {};
|
|
462
|
+
if (path) {
|
|
463
|
+
let rest = path;
|
|
464
|
+
const hashIndex = rest.indexOf('#');
|
|
465
|
+
if (hashIndex >= 0) {
|
|
466
|
+
parsedPath.hash = rest.substring(hashIndex);
|
|
467
|
+
rest = rest.substring(0, hashIndex);
|
|
468
|
+
}
|
|
469
|
+
const searchIndex = rest.indexOf('?');
|
|
470
|
+
if (searchIndex >= 0) {
|
|
471
|
+
parsedPath.search = rest.substring(searchIndex);
|
|
472
|
+
rest = rest.substring(0, searchIndex);
|
|
473
|
+
}
|
|
474
|
+
if (rest) {
|
|
475
|
+
parsedPath.pathname = rest;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return parsedPath;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function createPath({ pathname = '/', search = '', hash = '' }) {
|
|
482
|
+
let path = pathname;
|
|
483
|
+
if (search && search !== '?') {
|
|
484
|
+
path += search.charAt(0) === '?' ? search : `?${search}`;
|
|
485
|
+
}
|
|
486
|
+
if (hash && hash !== '#') {
|
|
487
|
+
path += hash.charAt(0) === '#' ? hash : `#${hash}`;
|
|
488
|
+
}
|
|
489
|
+
return path;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resolvePathname(relativePath, fromPathname) {
|
|
493
|
+
const segments = removeTrailingSlash(fromPathname).split('/');
|
|
494
|
+
for (const segment of relativePath.split('/')) {
|
|
495
|
+
if (segment === '..') {
|
|
496
|
+
if (segments.length > 1) {
|
|
497
|
+
segments.pop();
|
|
498
|
+
}
|
|
499
|
+
} else if (segment !== '.') {
|
|
500
|
+
segments.push(segment);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return segments.length > 1 ? segments.join('/') : '/';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const normalizeSearch = search =>
|
|
507
|
+
!search || search === '?'
|
|
508
|
+
? ''
|
|
509
|
+
: search.startsWith('?')
|
|
510
|
+
? search
|
|
511
|
+
: `?${search}`;
|
|
512
|
+
const normalizeHash = hash =>
|
|
513
|
+
!hash || hash === '#' ? '' : hash.startsWith('#') ? hash : `#${hash}`;
|
|
514
|
+
|
|
515
|
+
function resolvePath(to, fromPathname = '/') {
|
|
516
|
+
const { pathname: toPathname, search = '', hash = '' } = to;
|
|
517
|
+
let pathname = fromPathname;
|
|
518
|
+
if (toPathname) {
|
|
519
|
+
const normalizedPathname = removeDoubleSlashes(toPathname);
|
|
520
|
+
pathname =
|
|
521
|
+
normalizedPathname.startsWith('/') || normalizedPathname.startsWith('\\')
|
|
522
|
+
? resolvePathname(normalizedPathname.substring(1), '/')
|
|
523
|
+
: resolvePathname(normalizedPathname, fromPathname);
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
pathname,
|
|
527
|
+
search: normalizeSearch(search),
|
|
528
|
+
hash: normalizeHash(hash),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function resolveTo(toArg, routePathnames, locationPathname) {
|
|
533
|
+
const to = parsePath(toArg);
|
|
534
|
+
const isEmptyPath = toArg === '' || to.pathname === '';
|
|
535
|
+
const toPathname = isEmptyPath ? '/' : to.pathname;
|
|
536
|
+
let from;
|
|
537
|
+
if (toPathname == null) {
|
|
538
|
+
from = locationPathname;
|
|
539
|
+
} else {
|
|
540
|
+
let routePathnameIndex = routePathnames.length - 1;
|
|
541
|
+
if (toPathname.startsWith('..')) {
|
|
542
|
+
const toSegments = toPathname.split('/');
|
|
543
|
+
while (toSegments[0] === '..') {
|
|
544
|
+
toSegments.shift();
|
|
545
|
+
routePathnameIndex -= 1;
|
|
546
|
+
}
|
|
547
|
+
to.pathname = toSegments.join('/');
|
|
548
|
+
}
|
|
549
|
+
from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : '/';
|
|
550
|
+
}
|
|
551
|
+
const path = resolvePath(to, from);
|
|
552
|
+
const hasExplicitTrailingSlash =
|
|
553
|
+
toPathname && toPathname !== '/' && toPathname.endsWith('/');
|
|
554
|
+
const hasCurrentTrailingSlash =
|
|
555
|
+
(isEmptyPath || toPathname === '.') && locationPathname.endsWith('/');
|
|
556
|
+
if (
|
|
557
|
+
!path.pathname.endsWith('/') &&
|
|
558
|
+
(hasExplicitTrailingSlash || hasCurrentTrailingSlash)
|
|
559
|
+
) {
|
|
560
|
+
path.pathname += '/';
|
|
561
|
+
}
|
|
562
|
+
return path;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const hasNakedIndexQuery = search =>
|
|
566
|
+
new URLSearchParams(search).getAll('index').some(value => value === '');
|
|
567
|
+
|
|
568
|
+
/** Resolve a relative redirect `Location` against the matched route. */
|
|
569
|
+
function normalizeRedirectLocation(requestUrl, matches, basename, to) {
|
|
570
|
+
const pathMatches = matches.filter(
|
|
571
|
+
(match, index) =>
|
|
572
|
+
index === 0 || (match.route.path && match.route.path.length > 0),
|
|
573
|
+
);
|
|
574
|
+
const routePathnames = pathMatches.map((match, index) =>
|
|
575
|
+
index === pathMatches.length - 1 ? match.pathname : match.pathnameBase,
|
|
576
|
+
);
|
|
577
|
+
const activeRouteMatch = matches[matches.length - 1];
|
|
578
|
+
const path = resolveTo(
|
|
579
|
+
to,
|
|
580
|
+
routePathnames,
|
|
581
|
+
stripBasename(requestUrl.pathname, basename) || requestUrl.pathname,
|
|
582
|
+
);
|
|
583
|
+
if (to === '.' && activeRouteMatch) {
|
|
584
|
+
const nakedIndex = hasNakedIndexQuery(path.search);
|
|
585
|
+
if (activeRouteMatch.route.index && !nakedIndex) {
|
|
586
|
+
path.search = path.search
|
|
587
|
+
? path.search.replace(/^\?/, '?index&')
|
|
588
|
+
: '?index';
|
|
589
|
+
} else if (!activeRouteMatch.route.index && nakedIndex) {
|
|
590
|
+
const params = new URLSearchParams(path.search);
|
|
591
|
+
const indexValues = params.getAll('index');
|
|
592
|
+
params.delete('index');
|
|
593
|
+
for (const value of indexValues.filter(Boolean)) {
|
|
594
|
+
params.append('index', value);
|
|
595
|
+
}
|
|
596
|
+
const qs = params.toString();
|
|
597
|
+
path.search = qs ? `?${qs}` : '';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (basename !== '/') {
|
|
601
|
+
path.pathname =
|
|
602
|
+
path.pathname === '/' ? basename : joinPaths([basename, path.pathname]);
|
|
603
|
+
}
|
|
604
|
+
return createPath(path);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function normalizeRelativeRedirectResponse(
|
|
608
|
+
response,
|
|
609
|
+
request,
|
|
610
|
+
routeId,
|
|
611
|
+
matches,
|
|
612
|
+
basename,
|
|
613
|
+
) {
|
|
614
|
+
const location = response.headers.get('Location');
|
|
615
|
+
invariant(
|
|
616
|
+
location,
|
|
617
|
+
'Redirects returned/thrown from loaders/actions must have a Location header',
|
|
618
|
+
);
|
|
619
|
+
if (!ABSOLUTE_URL_PATTERN.test(location)) {
|
|
620
|
+
response.headers.set(
|
|
621
|
+
'Location',
|
|
622
|
+
normalizeRedirectLocation(
|
|
623
|
+
new URL(request.url),
|
|
624
|
+
matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1),
|
|
625
|
+
basename,
|
|
626
|
+
location,
|
|
627
|
+
),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return response;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function createDataFunctionUrl(request, location) {
|
|
634
|
+
const url = new URL(request.url);
|
|
635
|
+
url.pathname = location.pathname || '/';
|
|
636
|
+
if (location.search) {
|
|
637
|
+
const searchParams = new URLSearchParams(location.search);
|
|
638
|
+
const indexValues = searchParams.getAll('index');
|
|
639
|
+
searchParams.delete('index');
|
|
640
|
+
for (const value of indexValues.filter(Boolean)) {
|
|
641
|
+
searchParams.append('index', value);
|
|
642
|
+
}
|
|
643
|
+
url.search = searchParams.size ? `?${searchParams.toString()}` : '';
|
|
644
|
+
} else {
|
|
645
|
+
url.search = '';
|
|
646
|
+
}
|
|
647
|
+
url.hash = location.hash || '';
|
|
648
|
+
return url;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function throwAbortedError(request) {
|
|
652
|
+
if (request.signal.reason !== undefined) {
|
|
653
|
+
throw request.signal.reason;
|
|
654
|
+
}
|
|
655
|
+
throw new Error(
|
|
656
|
+
`queryRoute() call aborted without an \`AbortSignal.reason\`: ${request.method} ${request.url}`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Run a route loader or action, settling as a `{ type, result }` handler
|
|
662
|
+
* result. An aborted request settles it as an error without a value.
|
|
663
|
+
*/
|
|
664
|
+
async function callRouteHandler(handler, args, request, match, isAction) {
|
|
665
|
+
let onAbort;
|
|
666
|
+
try {
|
|
667
|
+
const abortPromise = new Promise((_, reject) => {
|
|
668
|
+
onAbort = () => reject();
|
|
669
|
+
});
|
|
670
|
+
request.signal.addEventListener('abort', onAbort);
|
|
671
|
+
const handlerPromise = (async () => {
|
|
672
|
+
try {
|
|
673
|
+
if (typeof handler !== 'function') {
|
|
674
|
+
throw new Error(
|
|
675
|
+
`You cannot call the handler for a route which defines a boolean "${
|
|
676
|
+
isAction ? 'action' : 'loader'
|
|
677
|
+
}" [routeId: ${match.route.id}]`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return { type: 'data', result: await handler(args) };
|
|
681
|
+
} catch (error) {
|
|
682
|
+
return { type: 'error', result: error };
|
|
683
|
+
}
|
|
684
|
+
})();
|
|
685
|
+
return await Promise.race([handlerPromise, abortPromise]);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
return { type: 'error', result: error };
|
|
688
|
+
} finally {
|
|
689
|
+
if (onAbort) {
|
|
690
|
+
request.signal.removeEventListener('abort', onAbort);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Settle a route request like react-router's static handler `queryRoute()`.
|
|
697
|
+
* Resolves with the loader or action value, or with a Response that the
|
|
698
|
+
* route returned or that redirects; rejects with what the route threw.
|
|
699
|
+
*/
|
|
700
|
+
async function queryRoute(request, { branches, basename, routeId, context }) {
|
|
701
|
+
const { method } = request;
|
|
702
|
+
const url = new URL(request.url);
|
|
703
|
+
const location = {
|
|
704
|
+
pathname: url.pathname,
|
|
705
|
+
search: url.search,
|
|
706
|
+
hash: url.hash,
|
|
707
|
+
};
|
|
708
|
+
const matches = matchRoutes(branches, location.pathname, basename);
|
|
709
|
+
if (
|
|
710
|
+
!REQUEST_METHODS.has(method.toUpperCase()) &&
|
|
711
|
+
method !== 'HEAD' &&
|
|
712
|
+
method !== 'OPTIONS'
|
|
713
|
+
) {
|
|
714
|
+
throw getInternalRouterError(405, { method });
|
|
715
|
+
}
|
|
716
|
+
if (!matches) {
|
|
717
|
+
throw getInternalRouterError(404, { pathname: location.pathname });
|
|
718
|
+
}
|
|
719
|
+
const match = matches.find(m => m.route.id === routeId);
|
|
720
|
+
if (!match) {
|
|
721
|
+
throw getInternalRouterError(403, {
|
|
722
|
+
pathname: location.pathname,
|
|
723
|
+
routeId,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const isAction = MUTATION_METHODS.has(method.toUpperCase());
|
|
728
|
+
let result;
|
|
729
|
+
try {
|
|
730
|
+
const { loaderRoute } = match.route;
|
|
731
|
+
if (isAction && !loaderRoute.action) {
|
|
732
|
+
throw getInternalRouterError(405, {
|
|
733
|
+
method,
|
|
734
|
+
pathname: url.pathname,
|
|
735
|
+
routeId: match.route.id,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const handlerResult = await callRouteHandler(
|
|
739
|
+
isAction
|
|
740
|
+
? loaderRoute.action
|
|
741
|
+
: args => context.loadRoute(loaderRoute, args),
|
|
742
|
+
{
|
|
743
|
+
request,
|
|
744
|
+
url: createDataFunctionUrl(request, location),
|
|
745
|
+
pattern:
|
|
746
|
+
joinPaths(matches.map(m => m.route.path).filter(Boolean)) || '/',
|
|
747
|
+
params: match.params,
|
|
748
|
+
context: context.requestContext,
|
|
749
|
+
},
|
|
750
|
+
request,
|
|
751
|
+
match,
|
|
752
|
+
isAction,
|
|
753
|
+
);
|
|
754
|
+
const value = handlerResult.result;
|
|
755
|
+
if (isResponse(value) && isRedirectStatus(value.status)) {
|
|
756
|
+
throw normalizeRelativeRedirectResponse(
|
|
757
|
+
value,
|
|
758
|
+
request,
|
|
759
|
+
match.route.id,
|
|
760
|
+
matches,
|
|
761
|
+
basename,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (isResponse(value)) {
|
|
765
|
+
throw handlerResult;
|
|
766
|
+
}
|
|
767
|
+
if (isDataWithResponseInit(value)) {
|
|
768
|
+
throw Response.json(value.data, value.init ?? undefined);
|
|
769
|
+
}
|
|
770
|
+
if (request.signal.aborted) {
|
|
771
|
+
throwAbortedError(request);
|
|
772
|
+
}
|
|
773
|
+
if (isAction && handlerResult.type === 'error') {
|
|
774
|
+
throw value;
|
|
775
|
+
}
|
|
776
|
+
result = handlerResult;
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (isHandlerResult(error) && isResponse(error.result)) {
|
|
779
|
+
if (error.type === 'error') {
|
|
780
|
+
throw error.result;
|
|
781
|
+
}
|
|
782
|
+
return error.result;
|
|
783
|
+
}
|
|
784
|
+
if (isRouterRedirectResponse(error)) {
|
|
785
|
+
return error;
|
|
786
|
+
}
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
// A loader that threw `undefined` has no error to report and no data.
|
|
790
|
+
if (result.type === 'error' && result.result !== undefined) {
|
|
791
|
+
throw result.result;
|
|
792
|
+
}
|
|
793
|
+
return result.type === 'error' ? undefined : result.result;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// --- Response encoding (@modern-js/plugin-data-loader/runtime) ------------
|
|
797
|
+
|
|
798
|
+
const isProductionErrorMode = () =>
|
|
799
|
+
process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test';
|
|
800
|
+
|
|
801
|
+
function serializeError(error) {
|
|
802
|
+
const sanitized =
|
|
803
|
+
error instanceof Error && isProductionErrorMode()
|
|
804
|
+
? Object.assign(new Error('Unexpected Server Error'), {
|
|
805
|
+
stack: undefined,
|
|
806
|
+
})
|
|
807
|
+
: error;
|
|
808
|
+
return { message: sanitized.message, stack: sanitized.stack };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function errorResponseToJson(errorResponse) {
|
|
812
|
+
return Response.json(
|
|
813
|
+
serializeError(errorResponse.error || new Error('Unexpected Server Error')),
|
|
814
|
+
{
|
|
815
|
+
status: errorResponse.status,
|
|
816
|
+
statusText:
|
|
817
|
+
errorResponse.status >= 500 && isProductionErrorMode()
|
|
818
|
+
? 'Internal Server Error'
|
|
819
|
+
: errorResponse.statusText,
|
|
820
|
+
headers: { 'X-Modernjs-Error': 'yes' },
|
|
821
|
+
},
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function convertModernRedirectResponse(headers, basename) {
|
|
826
|
+
const newHeaders = new Headers(headers);
|
|
827
|
+
let redirectUrl = headers.get('Location');
|
|
828
|
+
// The client loader handles the basename.
|
|
829
|
+
if (basename !== '/') {
|
|
830
|
+
redirectUrl = redirectUrl.replace(basename, '');
|
|
831
|
+
}
|
|
832
|
+
newHeaders.set('X-Modernjs-Redirect', redirectUrl);
|
|
833
|
+
newHeaders.delete('Location');
|
|
834
|
+
return new Response(null, { status: 204, headers: newHeaders });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function serializeDeferredError(error) {
|
|
838
|
+
return isProductionErrorMode()
|
|
839
|
+
? { message: 'Unexpected Server Error', stack: undefined }
|
|
840
|
+
: { message: error.message, stack: error.stack };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function isTrackedPromise(value) {
|
|
844
|
+
return (
|
|
845
|
+
value != null && typeof value.then === 'function' && value._tracked === true
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function enqueueTrackedPromise(controller, encoder, settledKey, promise) {
|
|
850
|
+
if ('_error' in promise) {
|
|
851
|
+
controller.enqueue(
|
|
852
|
+
encoder.encode(
|
|
853
|
+
`error:${serializeJson({
|
|
854
|
+
[settledKey]: serializeDeferredError(promise._error),
|
|
855
|
+
})}\n\n`,
|
|
856
|
+
),
|
|
857
|
+
);
|
|
858
|
+
} else {
|
|
859
|
+
controller.enqueue(
|
|
860
|
+
encoder.encode(
|
|
861
|
+
`data:${JSON.stringify({ [settledKey]: promise._data ?? null })}\n\n`,
|
|
862
|
+
),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function createDeferredReadableStream(deferredData, signal) {
|
|
868
|
+
const encoder = new TextEncoder();
|
|
869
|
+
return new ReadableStream({
|
|
870
|
+
async start(controller) {
|
|
871
|
+
const criticalData = {};
|
|
872
|
+
const preresolvedKeys = [];
|
|
873
|
+
for (const [key, value] of Object.entries(deferredData.data)) {
|
|
874
|
+
if (isTrackedPromise(value)) {
|
|
875
|
+
criticalData[key] = `${DEFERRED_VALUE_PLACEHOLDER_PREFIX}${key}`;
|
|
876
|
+
if (
|
|
877
|
+
typeof value._data !== 'undefined' ||
|
|
878
|
+
typeof value._error !== 'undefined'
|
|
879
|
+
) {
|
|
880
|
+
preresolvedKeys.push(key);
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
criticalData[key] = value;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
controller.enqueue(encoder.encode(`${JSON.stringify(criticalData)}\n\n`));
|
|
887
|
+
for (const preresolvedKey of preresolvedKeys) {
|
|
888
|
+
enqueueTrackedPromise(
|
|
889
|
+
controller,
|
|
890
|
+
encoder,
|
|
891
|
+
preresolvedKey,
|
|
892
|
+
deferredData.data[preresolvedKey],
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
const unsubscribe = deferredData.subscribe((_aborted, settledKey) => {
|
|
896
|
+
if (settledKey) {
|
|
897
|
+
enqueueTrackedPromise(
|
|
898
|
+
controller,
|
|
899
|
+
encoder,
|
|
900
|
+
settledKey,
|
|
901
|
+
deferredData.data[settledKey],
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
await deferredData.resolveData(signal);
|
|
906
|
+
unsubscribe();
|
|
907
|
+
controller.close();
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const isObjectLiteral = value =>
|
|
913
|
+
value != null &&
|
|
914
|
+
typeof value === 'object' &&
|
|
915
|
+
Object.getPrototypeOf(value) === Object.prototype;
|
|
916
|
+
|
|
917
|
+
function hasFileExtension(pathname) {
|
|
918
|
+
const lastSegment = pathname.split('/').pop() || '';
|
|
919
|
+
const dotIndex = lastSegment.lastIndexOf('.');
|
|
920
|
+
return (
|
|
921
|
+
dotIndex !== -1 && lastSegment.substring(dotIndex).toLowerCase() !== '.html'
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const matchEntry = (pathname, serverRoutes) =>
|
|
926
|
+
[...serverRoutes]
|
|
927
|
+
.sort((a, b) => b.urlPath.length - a.urlPath.length)
|
|
928
|
+
.find(entry => pathname.startsWith(entry.urlPath));
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Create the worker route data request handler for an entry's route loaders
|
|
932
|
+
* (the `routes` export of its generated `route-server-loaders.js`).
|
|
933
|
+
*
|
|
934
|
+
* The handler takes the Node data handler's options and resolves to
|
|
935
|
+
* `undefined` for requests that are not route data requests of a known entry,
|
|
936
|
+
* so the caller renders the page instead.
|
|
937
|
+
*/
|
|
938
|
+
export function createRouteDataRequestHandler(routes) {
|
|
939
|
+
let routeTable;
|
|
940
|
+
const getRouteTable = () => {
|
|
941
|
+
if (!routeTable) {
|
|
942
|
+
const loaderRoutes = expandLocalisedLoaderRoutes(routes);
|
|
943
|
+
invariant(
|
|
944
|
+
loaderRoutes.length > 0,
|
|
945
|
+
'You must provide a non-empty routes array to createStaticHandler',
|
|
946
|
+
);
|
|
947
|
+
routeTable = {
|
|
948
|
+
loaderRoutes,
|
|
949
|
+
branches: flattenAndRankRoutes(createRouteTree(loaderRoutes)),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
return routeTable;
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
return async ({ request, serverRoutes, context = {}, onTiming }) => {
|
|
956
|
+
const url = new URL(request.url);
|
|
957
|
+
const requestedRouteId = url.searchParams.get(LOADER_ID_PARAM);
|
|
958
|
+
if (hasFileExtension(url.pathname)) {
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
const entry = matchEntry(url.pathname, serverRoutes);
|
|
962
|
+
if (!requestedRouteId || !entry) {
|
|
963
|
+
return undefined;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const basename = entry.urlPath;
|
|
967
|
+
const end = time();
|
|
968
|
+
const { reporter, loaderContext, monitors } = context;
|
|
969
|
+
const activeDeferreds = new Map();
|
|
970
|
+
|
|
971
|
+
return storage.run(
|
|
972
|
+
{
|
|
973
|
+
headers: parseHeaders(request),
|
|
974
|
+
monitors,
|
|
975
|
+
request,
|
|
976
|
+
activeDeferreds,
|
|
977
|
+
},
|
|
978
|
+
async () => {
|
|
979
|
+
const { loaderRoutes, branches } = getRouteTable();
|
|
980
|
+
const routeId = resolveLocalisedLoaderRoute(requestedRouteId, {
|
|
981
|
+
routes: loaderRoutes,
|
|
982
|
+
matchedRouteIds:
|
|
983
|
+
matchRoutes(branches, url.pathname, basename)?.map(
|
|
984
|
+
match => match.route.id,
|
|
985
|
+
) ?? [],
|
|
986
|
+
});
|
|
987
|
+
const requestContext = createRequestContext(loaderContext);
|
|
988
|
+
requestContext.set(reporterCtx, reporter);
|
|
989
|
+
|
|
990
|
+
// Route loaders run like the data loader runtime's nested route
|
|
991
|
+
// loaders: plain object data is served as deferred data.
|
|
992
|
+
const loadRoute = async (loaderRoute, args) => {
|
|
993
|
+
if (!loaderRoute.loader) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
const endLoader = time();
|
|
997
|
+
const data = await loaderRoute.loader(args);
|
|
998
|
+
if (isObjectLiteral(data)) {
|
|
999
|
+
activeDeferreds.set(loaderRoute.id, new DeferredData(data));
|
|
1000
|
+
}
|
|
1001
|
+
monitors?.timing(
|
|
1002
|
+
`${LOADER_REPORTER_NAME}-${loaderRoute.id?.replace(/\//g, '_')}`,
|
|
1003
|
+
endLoader(),
|
|
1004
|
+
);
|
|
1005
|
+
return data;
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
let response;
|
|
1009
|
+
try {
|
|
1010
|
+
response = await queryRoute(request, {
|
|
1011
|
+
basename: basename || '/',
|
|
1012
|
+
branches,
|
|
1013
|
+
context: { loadRoute, requestContext },
|
|
1014
|
+
routeId,
|
|
1015
|
+
});
|
|
1016
|
+
// Only a loader's object literal data is registered as deferred
|
|
1017
|
+
// data, and it is the value the route request resolved with.
|
|
1018
|
+
const deferredData = activeDeferreds.get(routeId);
|
|
1019
|
+
if (isResponse(response) && isRedirectStatus(response.status)) {
|
|
1020
|
+
response = convertModernRedirectResponse(
|
|
1021
|
+
response.headers,
|
|
1022
|
+
basename,
|
|
1023
|
+
);
|
|
1024
|
+
} else if (deferredData) {
|
|
1025
|
+
response = new Response(
|
|
1026
|
+
createDeferredReadableStream(deferredData, request.signal),
|
|
1027
|
+
{
|
|
1028
|
+
headers: {
|
|
1029
|
+
'Content-Type': `${CONTENT_TYPE_DEFERRED}; charset=UTF-8`,
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
);
|
|
1033
|
+
} else if (!isResponse(response)) {
|
|
1034
|
+
response = new Response(JSON.stringify(response), {
|
|
1035
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
const cost = end();
|
|
1039
|
+
// Tells the client the response comes from the Modern.js server.
|
|
1040
|
+
response.headers.set('X-Modernjs-Response', 'yes');
|
|
1041
|
+
onTiming?.(`${LOADER_REPORTER_NAME}-navigation`, cost);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (isResponse(error)) {
|
|
1044
|
+
error.headers.set('X-Modernjs-Catch', 'yes');
|
|
1045
|
+
response = error;
|
|
1046
|
+
} else if (isRouteErrorResponse(error)) {
|
|
1047
|
+
response = errorResponseToJson(error);
|
|
1048
|
+
} else {
|
|
1049
|
+
const errorInstance =
|
|
1050
|
+
error instanceof Error || error instanceof DOMException
|
|
1051
|
+
? error
|
|
1052
|
+
: new Error('Unexpected Server Error');
|
|
1053
|
+
response = new Response(
|
|
1054
|
+
JSON.stringify(serializeError(errorInstance)),
|
|
1055
|
+
{
|
|
1056
|
+
status: 500,
|
|
1057
|
+
headers: {
|
|
1058
|
+
'X-Modernjs-Error': 'yes',
|
|
1059
|
+
'Content-Type': 'application/json',
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return response;
|
|
1066
|
+
},
|
|
1067
|
+
);
|
|
1068
|
+
};
|
|
1069
|
+
}
|