@bleedingdev/modern-js-app-tools 3.2.0-ultramodern.9 → 3.2.0-ultramodern.91
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/baseline.js +43 -1
- package/dist/cjs/builder/generator/getBuilderEnvironments.js +176 -8
- package/dist/cjs/builder/shared/builderPlugins/adapterBasic.js +41 -5
- package/dist/cjs/builder/shared/builderPlugins/adapterSSR.js +3 -1
- package/dist/cjs/plugins/deploy/index.js +17 -6
- package/dist/cjs/plugins/deploy/platforms/cloudflare.js +222 -0
- package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-entry.mjs +438 -0
- package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-worker-fs-promises.mjs +7 -0
- package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-worker-loadable-server.mjs +185 -0
- package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-worker-path.mjs +59 -0
- package/dist/cjs/rsbuild.js +3 -0
- package/dist/esm/baseline.mjs +33 -1
- package/dist/esm/builder/generator/getBuilderEnvironments.mjs +165 -8
- package/dist/esm/builder/shared/builderPlugins/adapterBasic.mjs +41 -5
- package/dist/esm/builder/shared/builderPlugins/adapterSSR.mjs +3 -1
- package/dist/esm/plugins/deploy/index.mjs +10 -4
- package/dist/esm/plugins/deploy/platforms/cloudflare.mjs +178 -0
- package/dist/esm/plugins/deploy/platforms/templates/cloudflare-entry.mjs +438 -0
- package/dist/esm/plugins/deploy/platforms/templates/cloudflare-worker-fs-promises.mjs +7 -0
- package/dist/esm/plugins/deploy/platforms/templates/cloudflare-worker-loadable-server.mjs +185 -0
- package/dist/esm/plugins/deploy/platforms/templates/cloudflare-worker-path.mjs +59 -0
- package/dist/esm/rsbuild.mjs +5 -2
- package/dist/esm-node/baseline.mjs +33 -1
- package/dist/esm-node/builder/generator/getBuilderEnvironments.mjs +170 -9
- package/dist/esm-node/builder/shared/builderPlugins/adapterBasic.mjs +41 -5
- package/dist/esm-node/builder/shared/builderPlugins/adapterSSR.mjs +3 -1
- package/dist/esm-node/plugins/deploy/index.mjs +10 -4
- package/dist/esm-node/plugins/deploy/platforms/cloudflare.mjs +179 -0
- package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-entry.mjs +438 -0
- package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-worker-fs-promises.mjs +7 -0
- package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-worker-loadable-server.mjs +185 -0
- package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-worker-path.mjs +59 -0
- package/dist/esm-node/rsbuild.mjs +5 -2
- package/dist/types/locale/en.d.ts +1 -1
- package/dist/types/locale/zh.d.ts +1 -1
- package/dist/types/plugins/deploy/index.d.ts +4 -1
- package/dist/types/plugins/deploy/platforms/cloudflare.d.ts +2 -0
- package/dist/types/plugins/deploy/platforms/templates/cloudflare-entry.d.mts +4 -0
- package/dist/types/plugins/deploy/platforms/templates/cloudflare-worker-fs-promises.d.mts +5 -0
- package/dist/types/plugins/deploy/platforms/templates/cloudflare-worker-loadable-server.d.mts +48 -0
- package/dist/types/plugins/deploy/platforms/templates/cloudflare-worker-path.d.mts +21 -0
- package/dist/types/types/config/deploy.d.ts +8 -0
- package/package.json +18 -17
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
const ASSETS_BINDING = 'ASSETS';
|
|
2
|
+
const MODERN_WORKER_MANIFEST = p_workerManifest;
|
|
3
|
+
const WORKER_MODULE_LOADERS = p_workerModuleLoaders;
|
|
4
|
+
const workerModulePromises = new Map();
|
|
5
|
+
const remoteJsonPromises = new Map();
|
|
6
|
+
const CORS_HEADERS = {
|
|
7
|
+
'access-control-allow-headers': '*',
|
|
8
|
+
'access-control-allow-methods': 'GET, HEAD, OPTIONS',
|
|
9
|
+
'access-control-allow-origin': '*'
|
|
10
|
+
};
|
|
11
|
+
globalThis.__dirname ??= '/';
|
|
12
|
+
globalThis.__filename ??= '/index.js';
|
|
13
|
+
function withCorsHeaders(response) {
|
|
14
|
+
const headers = new Headers(response.headers);
|
|
15
|
+
for (const [name, value] of Object.entries(CORS_HEADERS))if (!headers.has(name)) headers.set(name, value);
|
|
16
|
+
return new Response(response.body, {
|
|
17
|
+
headers,
|
|
18
|
+
status: response.status,
|
|
19
|
+
statusText: response.statusText
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isFingerprintedAssetPathname(pathname) {
|
|
23
|
+
return /(?:^|\/)[^/]+\.[a-f0-9]{8,}\.(?:css|js|mjs|json|svg|png|jpe?g|webp|avif|gif|woff2?|ttf)$/iu.test(pathname);
|
|
24
|
+
}
|
|
25
|
+
function withAssetHeaders(response, request) {
|
|
26
|
+
const corsResponse = withCorsHeaders(response);
|
|
27
|
+
const headers = new Headers(corsResponse.headers);
|
|
28
|
+
const { pathname } = new URL(request.url);
|
|
29
|
+
if (isFingerprintedAssetPathname(pathname)) headers.set('cache-control', 'public, max-age=31536000, immutable');
|
|
30
|
+
return new Response(corsResponse.body, {
|
|
31
|
+
headers,
|
|
32
|
+
status: corsResponse.status,
|
|
33
|
+
statusText: corsResponse.statusText
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function createCorsPreflightResponse(request) {
|
|
37
|
+
if ('OPTIONS' !== request.method) return null;
|
|
38
|
+
return new Response(null, {
|
|
39
|
+
headers: CORS_HEADERS,
|
|
40
|
+
status: 204
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function fetchAsset(request, env) {
|
|
44
|
+
const assets = env?.[ASSETS_BINDING];
|
|
45
|
+
if (!assets || 'function' != typeof assets.fetch) return null;
|
|
46
|
+
const response = await assets.fetch(request);
|
|
47
|
+
if (404 === response.status) return null;
|
|
48
|
+
return withAssetHeaders(response, request);
|
|
49
|
+
}
|
|
50
|
+
async function fetchAssetByPath(pathname, request, env) {
|
|
51
|
+
const url = new URL(request.url);
|
|
52
|
+
url.pathname = `/${pathname.replace(/^\/+/u, '')}`;
|
|
53
|
+
return fetchAsset(new Request(url, request), env);
|
|
54
|
+
}
|
|
55
|
+
async function fetchAssetByPathFollowingRedirects(pathname, request, env, visited = new Set()) {
|
|
56
|
+
const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
57
|
+
if (visited.has(normalizedPathname)) return null;
|
|
58
|
+
visited.add(normalizedPathname);
|
|
59
|
+
const response = await fetchAssetByPath(normalizedPathname, request, env);
|
|
60
|
+
if (response && response.status >= 300 && response.status < 400 && response.headers.has('location')) {
|
|
61
|
+
const location = response.headers.get('location');
|
|
62
|
+
if (location) {
|
|
63
|
+
const nextUrl = new URL(location, request.url);
|
|
64
|
+
const currentUrl = new URL(request.url);
|
|
65
|
+
if (nextUrl.origin === currentUrl.origin) return fetchAssetByPathFollowingRedirects(nextUrl.pathname, request, env, visited);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
async function readAssetText(pathname, request, env) {
|
|
71
|
+
const response = await fetchAssetByPathFollowingRedirects(pathname, request, env);
|
|
72
|
+
if (!response || !response.ok) return;
|
|
73
|
+
return response.text();
|
|
74
|
+
}
|
|
75
|
+
async function readAssetJson(pathname, request, env) {
|
|
76
|
+
const text = await readAssetText(pathname, request, env);
|
|
77
|
+
if (!text) return {};
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
}
|
|
80
|
+
function normalizeRoutePath(pathname) {
|
|
81
|
+
if ('/' === pathname) return pathname;
|
|
82
|
+
return pathname.replace(/\/+$/u, '');
|
|
83
|
+
}
|
|
84
|
+
function getPathExtension(pathname) {
|
|
85
|
+
const lastSegment = pathname.split('/').pop() || '';
|
|
86
|
+
const dotIndex = lastSegment.lastIndexOf('.');
|
|
87
|
+
if (dotIndex <= 0 || dotIndex === lastSegment.length - 1) return '';
|
|
88
|
+
return lastSegment.slice(dotIndex).toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
function isAssetLikePathname(pathname) {
|
|
91
|
+
const extension = getPathExtension(pathname);
|
|
92
|
+
return '' !== extension && '.html' !== extension && '.htm' !== extension;
|
|
93
|
+
}
|
|
94
|
+
function routeMatchesExactly(route, pathname) {
|
|
95
|
+
if ('string' != typeof route?.urlPath) return false;
|
|
96
|
+
return normalizeRoutePath(route.urlPath) === normalizeRoutePath(pathname);
|
|
97
|
+
}
|
|
98
|
+
function routeMatches(route, pathname) {
|
|
99
|
+
if ('string' != typeof route.urlPath) return false;
|
|
100
|
+
const routePath = normalizeRoutePath(route.urlPath);
|
|
101
|
+
const requestPath = normalizeRoutePath(pathname);
|
|
102
|
+
return routePath === requestPath || '/' === routePath && route.isSSR || requestPath.startsWith(`${routePath}/`);
|
|
103
|
+
}
|
|
104
|
+
function findRoute(request) {
|
|
105
|
+
const { pathname } = new URL(request.url);
|
|
106
|
+
const routes = MODERN_WORKER_MANIFEST.routeSpec.routes;
|
|
107
|
+
return [
|
|
108
|
+
...routes
|
|
109
|
+
].sort((left, right)=>{
|
|
110
|
+
const leftLength = left.urlPath?.length || 0;
|
|
111
|
+
const rightLength = right.urlPath?.length || 0;
|
|
112
|
+
return rightLength - leftLength;
|
|
113
|
+
}).find((route)=>routeMatches(route, pathname));
|
|
114
|
+
}
|
|
115
|
+
async function fetchRouteHtml(route, request, env) {
|
|
116
|
+
if (!route?.entryPath) return null;
|
|
117
|
+
return fetchAssetByPath(route.entryPath, request, env);
|
|
118
|
+
}
|
|
119
|
+
function createNoopMonitors() {
|
|
120
|
+
const noop = ()=>{};
|
|
121
|
+
return {
|
|
122
|
+
debug: noop,
|
|
123
|
+
error: noop,
|
|
124
|
+
info: noop,
|
|
125
|
+
warn: noop
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function createRequestHandlerOptions({ route, htmlTemplate, routeManifest, loadableStats }) {
|
|
129
|
+
const monitors = createNoopMonitors();
|
|
130
|
+
return {
|
|
131
|
+
resource: {
|
|
132
|
+
route,
|
|
133
|
+
routeManifest,
|
|
134
|
+
loadableStats,
|
|
135
|
+
htmlTemplate,
|
|
136
|
+
entryName: route.entryName
|
|
137
|
+
},
|
|
138
|
+
params: {},
|
|
139
|
+
loaderContext: {},
|
|
140
|
+
config: {},
|
|
141
|
+
locals: {},
|
|
142
|
+
staticGenerate: false,
|
|
143
|
+
monitors,
|
|
144
|
+
onError (error) {
|
|
145
|
+
monitors.error(error);
|
|
146
|
+
},
|
|
147
|
+
onTiming () {},
|
|
148
|
+
reporter: {
|
|
149
|
+
reportTiming: ()=>{}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function collectRouteCssAssets(route, routeManifest) {
|
|
154
|
+
const routeAssets = routeManifest?.routeAssets || {};
|
|
155
|
+
const candidateKeys = [
|
|
156
|
+
route.entryName,
|
|
157
|
+
`async-${route.entryName}`
|
|
158
|
+
].filter(Boolean);
|
|
159
|
+
const assets = new Set();
|
|
160
|
+
for (const key of candidateKeys){
|
|
161
|
+
const routeAsset = routeAssets[key];
|
|
162
|
+
const cssAssets = [
|
|
163
|
+
...Array.isArray(routeAsset?.referenceCssAssets) ? routeAsset.referenceCssAssets : [],
|
|
164
|
+
...Array.isArray(routeAsset?.assets) ? routeAsset.assets : []
|
|
165
|
+
];
|
|
166
|
+
for (const asset of cssAssets)if ('string' == typeof asset && asset.endsWith('.css')) assets.add(asset);
|
|
167
|
+
}
|
|
168
|
+
return [
|
|
169
|
+
...assets
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
function collectRenderedFederatedExposes(html) {
|
|
173
|
+
const renderedExposes = [];
|
|
174
|
+
const tagPattern = /<[^>]*data-modern-(?:boundary-id|mf-expose)=["'][^"']+["'][^>]*>/g;
|
|
175
|
+
const attributePattern = /\s(data-modern-(?:boundary-id|mf-expose))=["']([^"']+)["']/g;
|
|
176
|
+
for (const [tag] of html.matchAll(tagPattern)){
|
|
177
|
+
const attributes = {};
|
|
178
|
+
for (const [, name, value] of tag.matchAll(attributePattern))attributes[name] = value;
|
|
179
|
+
const boundaryId = attributes['data-modern-boundary-id'];
|
|
180
|
+
const expose = attributes['data-modern-mf-expose'];
|
|
181
|
+
if (boundaryId && expose) renderedExposes.push({
|
|
182
|
+
boundaryId,
|
|
183
|
+
expose
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return renderedExposes;
|
|
187
|
+
}
|
|
188
|
+
function getRemoteManifestUrl(remote, request) {
|
|
189
|
+
const entry = remote?.entry;
|
|
190
|
+
if ('string' != typeof entry || 0 === entry.length) return;
|
|
191
|
+
return new URL(entry, request.url).toString();
|
|
192
|
+
}
|
|
193
|
+
async function fetchRemoteJson(jsonUrl) {
|
|
194
|
+
if (!remoteJsonPromises.has(jsonUrl)) remoteJsonPromises.set(jsonUrl, fetch(jsonUrl).then((response)=>{
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
remoteJsonPromises.delete(jsonUrl);
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
return response.json().catch(()=>{
|
|
200
|
+
remoteJsonPromises.delete(jsonUrl);
|
|
201
|
+
return {};
|
|
202
|
+
});
|
|
203
|
+
}).catch(()=>{
|
|
204
|
+
remoteJsonPromises.delete(jsonUrl);
|
|
205
|
+
return {};
|
|
206
|
+
}));
|
|
207
|
+
return remoteJsonPromises.get(jsonUrl);
|
|
208
|
+
}
|
|
209
|
+
function findRemoteExpose(remoteManifest, exposePath) {
|
|
210
|
+
const exposes = Array.isArray(remoteManifest?.exposes) ? remoteManifest.exposes : [];
|
|
211
|
+
const normalizedExpose = exposePath.replace(/^\.\//u, '');
|
|
212
|
+
return exposes.find((expose)=>{
|
|
213
|
+
if (!expose || 'object' != typeof expose) return false;
|
|
214
|
+
return expose.path === exposePath || expose.path === `./${normalizedExpose}` || expose.name === normalizedExpose;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function collectCssAssetEntries(assets) {
|
|
218
|
+
const cssAssets = assets?.css;
|
|
219
|
+
return [
|
|
220
|
+
...Array.isArray(cssAssets?.sync) ? cssAssets.sync : [],
|
|
221
|
+
...Array.isArray(cssAssets?.async) ? cssAssets.async : []
|
|
222
|
+
].filter((asset)=>'string' == typeof asset && asset.endsWith('.css'));
|
|
223
|
+
}
|
|
224
|
+
function collectRouteManifestCssAssets(routeManifest) {
|
|
225
|
+
const routeAssets = routeManifest?.routeAssets || {};
|
|
226
|
+
const assets = new Set();
|
|
227
|
+
for (const routeAsset of Object.values(routeAssets)){
|
|
228
|
+
const cssAssets = [
|
|
229
|
+
...Array.isArray(routeAsset?.referenceCssAssets) ? routeAsset.referenceCssAssets : [],
|
|
230
|
+
...Array.isArray(routeAsset?.assets) ? routeAsset.assets : []
|
|
231
|
+
];
|
|
232
|
+
for (const asset of cssAssets)if ('string' == typeof asset && asset.endsWith('.css')) assets.add(asset);
|
|
233
|
+
}
|
|
234
|
+
return [
|
|
235
|
+
...assets
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
async function collectRenderedRemoteCssHrefs(html, request, env) {
|
|
239
|
+
const renderedExposes = collectRenderedFederatedExposes(html);
|
|
240
|
+
if (0 === renderedExposes.length) return [];
|
|
241
|
+
const hostManifest = await readAssetJson('mf-manifest.json', request, env);
|
|
242
|
+
const remotes = Array.isArray(hostManifest?.remotes) ? hostManifest.remotes : [];
|
|
243
|
+
const remoteByBoundary = new Map();
|
|
244
|
+
const hrefs = new Set();
|
|
245
|
+
for (const remote of remotes){
|
|
246
|
+
if ('string' == typeof remote?.alias) remoteByBoundary.set(remote.alias, remote);
|
|
247
|
+
if ('string' == typeof remote?.federationContainerName) remoteByBoundary.set(remote.federationContainerName, remote);
|
|
248
|
+
}
|
|
249
|
+
await Promise.all(renderedExposes.map(async ({ boundaryId, expose })=>{
|
|
250
|
+
const remote = remoteByBoundary.get(boundaryId);
|
|
251
|
+
const manifestUrl = remote ? getRemoteManifestUrl(remote, request) : void 0;
|
|
252
|
+
if (!manifestUrl) return;
|
|
253
|
+
const remoteManifest = await fetchRemoteJson(manifestUrl);
|
|
254
|
+
const remoteExpose = findRemoteExpose(remoteManifest, expose);
|
|
255
|
+
const publicPath = 'string' == typeof remoteManifest?.metaData?.publicPath ? remoteManifest.metaData.publicPath : manifestUrl;
|
|
256
|
+
const remoteRouteManifest = await fetchRemoteJson(new URL('routes-manifest.json', publicPath).toString());
|
|
257
|
+
for (const asset of collectCssAssetEntries(remoteExpose?.assets))hrefs.add(new URL(asset, publicPath).toString());
|
|
258
|
+
for (const asset of collectRouteManifestCssAssets(remoteRouteManifest))hrefs.add(new URL(asset, publicPath).toString());
|
|
259
|
+
}));
|
|
260
|
+
return [
|
|
261
|
+
...hrefs
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
function escapeAttribute(value) {
|
|
265
|
+
return String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
266
|
+
}
|
|
267
|
+
async function withRouteCssLinks(response, route, routeManifest, request, env) {
|
|
268
|
+
const contentType = response.headers.get('content-type') || '';
|
|
269
|
+
if (!contentType.includes('text/html')) return response;
|
|
270
|
+
const html = await response.text();
|
|
271
|
+
const cssHrefs = [
|
|
272
|
+
...collectRouteCssAssets(route, routeManifest).map((asset)=>new URL(asset, request.url).toString()),
|
|
273
|
+
...await collectRenderedRemoteCssHrefs(html, request, env)
|
|
274
|
+
];
|
|
275
|
+
if (0 === cssHrefs.length) return response;
|
|
276
|
+
const uniqueCssHrefs = [
|
|
277
|
+
...new Set(cssHrefs)
|
|
278
|
+
];
|
|
279
|
+
const headers = new Headers(response.headers);
|
|
280
|
+
for (const href of uniqueCssHrefs)headers.append('link', `<${href}>; rel=preload; as=style`);
|
|
281
|
+
const links = uniqueCssHrefs.filter((href)=>!html.includes(href)).map((href)=>`<link rel="stylesheet" href="${escapeAttribute(href)}">`);
|
|
282
|
+
if (0 === links.length || !html.includes('</head>')) return new Response(html, {
|
|
283
|
+
headers,
|
|
284
|
+
status: response.status,
|
|
285
|
+
statusText: response.statusText
|
|
286
|
+
});
|
|
287
|
+
return new Response(html.replace('</head>', `${links.join('')}</head>`), {
|
|
288
|
+
headers,
|
|
289
|
+
status: response.status,
|
|
290
|
+
statusText: response.statusText
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async function getRequestHandlerOptions(route, request, env) {
|
|
294
|
+
const [htmlTemplate, routeManifest, loadableStats] = await Promise.all([
|
|
295
|
+
readAssetText(route.entryPath, request, env),
|
|
296
|
+
readAssetJson(MODERN_WORKER_MANIFEST.resources.routeManifest, request, env),
|
|
297
|
+
readAssetJson(MODERN_WORKER_MANIFEST.resources.loadableStats, request, env)
|
|
298
|
+
]);
|
|
299
|
+
return createRequestHandlerOptions({
|
|
300
|
+
route,
|
|
301
|
+
htmlTemplate: htmlTemplate || '',
|
|
302
|
+
routeManifest,
|
|
303
|
+
loadableStats
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async function loadWorkerModule(workerPath) {
|
|
307
|
+
const loader = WORKER_MODULE_LOADERS[workerPath];
|
|
308
|
+
if (!loader) return;
|
|
309
|
+
if (!workerModulePromises.has(workerPath)) workerModulePromises.set(workerPath, loader());
|
|
310
|
+
return workerModulePromises.get(workerPath);
|
|
311
|
+
}
|
|
312
|
+
function getRuntimeModule(workerModule) {
|
|
313
|
+
const defaultExport = workerModule.default;
|
|
314
|
+
const nestedDefaultExport = defaultExport && 'object' == typeof defaultExport ? defaultExport.default : void 0;
|
|
315
|
+
return defaultExport && 'object' == typeof defaultExport ? {
|
|
316
|
+
...workerModule,
|
|
317
|
+
...defaultExport,
|
|
318
|
+
...nestedDefaultExport && 'object' == typeof nestedDefaultExport ? nestedDefaultExport : {}
|
|
319
|
+
} : workerModule;
|
|
320
|
+
}
|
|
321
|
+
function getFetchHandler(workerModule) {
|
|
322
|
+
const defaultExport = workerModule.default;
|
|
323
|
+
const runtime = getRuntimeModule(workerModule);
|
|
324
|
+
return 'function' == typeof runtime.fetch && runtime.fetch.bind(runtime) || 'function' == typeof defaultExport && defaultExport.fetch?.bind?.(defaultExport);
|
|
325
|
+
}
|
|
326
|
+
async function getRequestHandler(workerModule) {
|
|
327
|
+
const defaultExport = workerModule.default;
|
|
328
|
+
const runtime = getRuntimeModule(workerModule);
|
|
329
|
+
return await workerModule.requestHandler || await runtime.requestHandler || ('function' == typeof defaultExport ? defaultExport : void 0);
|
|
330
|
+
}
|
|
331
|
+
async function dispatchRouteWorker(route, request, env, ctx) {
|
|
332
|
+
const workerPath = route.worker;
|
|
333
|
+
if (!workerPath) return new Response('Worker bundle not configured for SSR route', {
|
|
334
|
+
status: 500,
|
|
335
|
+
headers: {
|
|
336
|
+
'content-type': 'text/plain; charset=utf-8'
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
const workerModule = await loadWorkerModule(workerPath);
|
|
340
|
+
if (!workerModule) return new Response(`Worker bundle not found: ${workerPath}`, {
|
|
341
|
+
status: 500,
|
|
342
|
+
headers: {
|
|
343
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
344
|
+
'x-modern-js-route-worker': workerPath
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const fetchHandler = getFetchHandler(workerModule);
|
|
348
|
+
if (fetchHandler) return fetchHandler(request, env, ctx);
|
|
349
|
+
const requestHandler = await getRequestHandler(workerModule);
|
|
350
|
+
if ('function' == typeof requestHandler) {
|
|
351
|
+
const requestHandlerOptions = await getRequestHandlerOptions(route, request, env);
|
|
352
|
+
return withRouteCssLinks(await requestHandler(request, requestHandlerOptions), route, requestHandlerOptions.resource.routeManifest, request, env);
|
|
353
|
+
}
|
|
354
|
+
return new Response(`Worker bundle has no fetch or requestHandler export: ${workerPath}`, {
|
|
355
|
+
status: 500,
|
|
356
|
+
headers: {
|
|
357
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
358
|
+
'x-modern-js-route-worker': workerPath
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function matchesPrefix(pathname, prefix) {
|
|
363
|
+
if (!prefix || '/' === prefix) return true;
|
|
364
|
+
const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
365
|
+
return pathname === normalized || pathname.startsWith(`${normalized}/`);
|
|
366
|
+
}
|
|
367
|
+
function createRequestForMountedPrefix(request, prefix) {
|
|
368
|
+
if (!prefix || '/' === prefix) return request;
|
|
369
|
+
const url = new URL(request.url);
|
|
370
|
+
const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
371
|
+
if (!matchesPrefix(url.pathname, normalized)) return request;
|
|
372
|
+
const nextPath = url.pathname.slice(normalized.length) || '/';
|
|
373
|
+
url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
|
|
374
|
+
return new Request(url, request);
|
|
375
|
+
}
|
|
376
|
+
function createEffectContext(originalRequest, mountedRequest, env) {
|
|
377
|
+
const url = new URL(originalRequest.url);
|
|
378
|
+
return {
|
|
379
|
+
request: mountedRequest,
|
|
380
|
+
env: env || {},
|
|
381
|
+
path: url.pathname,
|
|
382
|
+
method: originalRequest.method,
|
|
383
|
+
operationContext: {
|
|
384
|
+
request: mountedRequest,
|
|
385
|
+
env: env || {},
|
|
386
|
+
path: url.pathname,
|
|
387
|
+
method: originalRequest.method
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function dispatchBffRequest(request, env) {
|
|
392
|
+
const bff = MODERN_WORKER_MANIFEST.bff;
|
|
393
|
+
if (!bff?.worker || !matchesPrefix(new URL(request.url).pathname, bff.prefix)) return null;
|
|
394
|
+
const workerModule = await loadWorkerModule(bff.worker);
|
|
395
|
+
if (!workerModule) return new Response(`BFF worker bundle not found: ${bff.worker}`, {
|
|
396
|
+
status: 500,
|
|
397
|
+
headers: {
|
|
398
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
399
|
+
'x-modern-js-bff-worker': bff.worker
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
const mountedRequest = createRequestForMountedPrefix(request, bff.prefix);
|
|
403
|
+
const effectContext = createEffectContext(request, mountedRequest, env);
|
|
404
|
+
const defaultExport = workerModule.default;
|
|
405
|
+
const runtime = getRuntimeModule(workerModule);
|
|
406
|
+
const directHandler = 'function' == typeof runtime.handler && runtime.handler || 'function' == typeof defaultExport && defaultExport;
|
|
407
|
+
const createdHandler = 'function' == typeof runtime.createHandler ? runtime.createHandler().handler : void 0;
|
|
408
|
+
const handler = directHandler || createdHandler;
|
|
409
|
+
if ('function' != typeof handler) return new Response(`BFF worker bundle has no handler export: ${bff.worker}`, {
|
|
410
|
+
status: 500,
|
|
411
|
+
headers: {
|
|
412
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
413
|
+
'x-modern-js-bff-worker': bff.worker
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
return handler.length > 1 ? handler(mountedRequest, effectContext) : handler(mountedRequest);
|
|
417
|
+
}
|
|
418
|
+
export default {
|
|
419
|
+
async fetch (request, env, ctx) {
|
|
420
|
+
const corsPreflightResponse = createCorsPreflightResponse(request);
|
|
421
|
+
if (corsPreflightResponse) return corsPreflightResponse;
|
|
422
|
+
const assetResponse = await fetchAsset(request, env);
|
|
423
|
+
if (assetResponse) return assetResponse;
|
|
424
|
+
const bffResponse = await dispatchBffRequest(request, env);
|
|
425
|
+
if (bffResponse) return withCorsHeaders(bffResponse);
|
|
426
|
+
const route = findRoute(request);
|
|
427
|
+
const { pathname } = new URL(request.url);
|
|
428
|
+
if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname)) return withCorsHeaders(new Response('Not found', {
|
|
429
|
+
status: 404
|
|
430
|
+
}));
|
|
431
|
+
if (route?.worker) return withCorsHeaders(await dispatchRouteWorker(route, request, env, ctx));
|
|
432
|
+
const htmlResponse = await fetchRouteHtml(route, request, env);
|
|
433
|
+
if (htmlResponse) return htmlResponse;
|
|
434
|
+
return withCorsHeaders(new Response('Not found', {
|
|
435
|
+
status: 404
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import loadableComponent from '@loadable/component';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
const internals = loadableComponent?.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED || {};
|
|
4
|
+
const LoadableContext = internals.Context || React.createContext(null);
|
|
5
|
+
const getRequiredChunkKey = internals.getRequiredChunkKey || ((namespace)=>`${namespace}__LOADABLE_REQUIRED_CHUNKS__`);
|
|
6
|
+
const scriptExtensions = new Set([
|
|
7
|
+
'.js',
|
|
8
|
+
'.mjs'
|
|
9
|
+
]);
|
|
10
|
+
const styleExtensions = new Set([
|
|
11
|
+
'.css'
|
|
12
|
+
]);
|
|
13
|
+
function uniqByUrl(assets) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
return assets.filter((asset)=>{
|
|
16
|
+
if (seen.has(asset.url)) return false;
|
|
17
|
+
seen.add(asset.url);
|
|
18
|
+
return true;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function extname(filePath) {
|
|
22
|
+
const basename = String(filePath).split('/').pop() || '';
|
|
23
|
+
const index = basename.lastIndexOf('.');
|
|
24
|
+
return index > 0 ? basename.slice(index) : '';
|
|
25
|
+
}
|
|
26
|
+
function joinUrl(publicPath, filename) {
|
|
27
|
+
const base = publicPath || '/';
|
|
28
|
+
return `${base.replace(/\/+$/u, '')}/${String(filename).replace(/^\/+/u, '')}`;
|
|
29
|
+
}
|
|
30
|
+
function extraPropsToString(extraProps = {}) {
|
|
31
|
+
return Object.entries(extraProps).filter(([, value])=>void 0 !== value && false !== value).map(([key, value])=>true === value ? ` ${key}` : ` ${key}="${value}"`).join('');
|
|
32
|
+
}
|
|
33
|
+
function assetScriptType(filename) {
|
|
34
|
+
const extension = extname(filename);
|
|
35
|
+
if (scriptExtensions.has(extension)) return "script";
|
|
36
|
+
if (styleExtensions.has(extension)) return 'style';
|
|
37
|
+
}
|
|
38
|
+
function getAssetName(asset) {
|
|
39
|
+
return 'object' == typeof asset && asset?.name ? asset.name : asset;
|
|
40
|
+
}
|
|
41
|
+
function getAssetIntegrity(asset) {
|
|
42
|
+
return 'object' == typeof asset && asset?.integrity ? asset.integrity : null;
|
|
43
|
+
}
|
|
44
|
+
function getChunkGroupAssets(chunkGroup) {
|
|
45
|
+
const assets = chunkGroup?.assets;
|
|
46
|
+
if (Array.isArray(assets)) return assets;
|
|
47
|
+
if (assets && 'object' == typeof assets) return [
|
|
48
|
+
...assets.js || [],
|
|
49
|
+
...assets.css || []
|
|
50
|
+
];
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function getChunkGroupChildAssets(chunkGroup, type) {
|
|
54
|
+
const childAssets = chunkGroup?.childAssets?.[type];
|
|
55
|
+
return Array.isArray(childAssets) ? childAssets : [];
|
|
56
|
+
}
|
|
57
|
+
function chunkIncludesJs(chunkInfo) {
|
|
58
|
+
return (chunkInfo?.files || []).some((file)=>scriptExtensions.has(extname(file)));
|
|
59
|
+
}
|
|
60
|
+
export function ChunkExtractorManager({ extractor, children }) {
|
|
61
|
+
return React.createElement(LoadableContext.Provider, {
|
|
62
|
+
value: extractor
|
|
63
|
+
}, children);
|
|
64
|
+
}
|
|
65
|
+
export class ChunkExtractor {
|
|
66
|
+
constructor({ stats, entrypoints = [
|
|
67
|
+
'main'
|
|
68
|
+
], namespace = '', outputPath = '/', publicPath } = {}){
|
|
69
|
+
this.namespace = namespace;
|
|
70
|
+
this.stats = stats || {};
|
|
71
|
+
this.publicPath = publicPath || this.stats.publicPath || '/';
|
|
72
|
+
this.outputPath = outputPath || this.stats.outputPath || '/';
|
|
73
|
+
this.entrypoints = Array.isArray(entrypoints) ? entrypoints : [
|
|
74
|
+
entrypoints
|
|
75
|
+
];
|
|
76
|
+
this.chunks = [];
|
|
77
|
+
}
|
|
78
|
+
addChunk(chunk) {
|
|
79
|
+
if (!this.chunks.includes(chunk)) this.chunks.push(chunk);
|
|
80
|
+
}
|
|
81
|
+
collectChunks(app) {
|
|
82
|
+
return React.createElement(ChunkExtractorManager, {
|
|
83
|
+
extractor: this
|
|
84
|
+
}, app);
|
|
85
|
+
}
|
|
86
|
+
getChunkGroup(chunk) {
|
|
87
|
+
return this.stats.namedChunkGroups?.[chunk] || {
|
|
88
|
+
assets: [],
|
|
89
|
+
childAssets: {},
|
|
90
|
+
chunks: []
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
getChunkInfo(chunkId) {
|
|
94
|
+
return (this.stats.chunks || []).find((chunk)=>chunk.id === chunkId);
|
|
95
|
+
}
|
|
96
|
+
resolvePublicUrl(filename) {
|
|
97
|
+
return joinUrl(this.publicPath, filename);
|
|
98
|
+
}
|
|
99
|
+
createChunkAsset({ filename, chunk, type, linkType }) {
|
|
100
|
+
const resolvedFilename = getAssetName(filename);
|
|
101
|
+
const scriptType = assetScriptType(resolvedFilename);
|
|
102
|
+
if (!scriptType) return;
|
|
103
|
+
return {
|
|
104
|
+
filename: resolvedFilename,
|
|
105
|
+
integrity: getAssetIntegrity(filename),
|
|
106
|
+
scriptType,
|
|
107
|
+
chunk,
|
|
108
|
+
url: this.resolvePublicUrl(resolvedFilename),
|
|
109
|
+
path: String(resolvedFilename).replace(/^\/+/u, ''),
|
|
110
|
+
type,
|
|
111
|
+
linkType
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
getChunkAssets(chunks) {
|
|
115
|
+
const one = (chunk)=>getChunkGroupAssets(this.getChunkGroup(chunk)).map((filename)=>this.createChunkAsset({
|
|
116
|
+
filename,
|
|
117
|
+
chunk,
|
|
118
|
+
type: 'mainAsset',
|
|
119
|
+
linkType: 'preload'
|
|
120
|
+
})).filter(Boolean);
|
|
121
|
+
return Array.isArray(chunks) ? uniqByUrl(chunks.flatMap(one)) : one(chunks);
|
|
122
|
+
}
|
|
123
|
+
getChunkChildAssets(chunks, type) {
|
|
124
|
+
const one = (chunk)=>getChunkGroupChildAssets(this.getChunkGroup(chunk), type).map((filename)=>this.createChunkAsset({
|
|
125
|
+
filename,
|
|
126
|
+
chunk,
|
|
127
|
+
type: 'childAsset',
|
|
128
|
+
linkType: type
|
|
129
|
+
})).filter(Boolean);
|
|
130
|
+
return Array.isArray(chunks) ? uniqByUrl(chunks.flatMap(one)) : one(chunks);
|
|
131
|
+
}
|
|
132
|
+
getChunkDependencies(chunks) {
|
|
133
|
+
const one = (chunk)=>(this.getChunkGroup(chunk).chunks || []).filter((chunkId)=>chunkIncludesJs(this.getChunkInfo(chunkId)));
|
|
134
|
+
return Array.isArray(chunks) ? [
|
|
135
|
+
...new Set(chunks.flatMap(one))
|
|
136
|
+
] : one(chunks);
|
|
137
|
+
}
|
|
138
|
+
getRequiredChunksScriptContent() {
|
|
139
|
+
return JSON.stringify(this.getChunkDependencies(this.chunks));
|
|
140
|
+
}
|
|
141
|
+
getRequiredChunksNamesScriptContent() {
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
namedChunks: this.chunks
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
getRequiredChunksScriptTag(extraProps = {}) {
|
|
147
|
+
const id = getRequiredChunkKey(this.namespace);
|
|
148
|
+
const props = `type="application/json"${extraPropsToString(extraProps)}`;
|
|
149
|
+
return [
|
|
150
|
+
`<script id="${id}" ${props}>${this.getRequiredChunksScriptContent()}</script>`,
|
|
151
|
+
`<script id="${id}_ext" ${props}>${this.getRequiredChunksNamesScriptContent()}</script>`
|
|
152
|
+
].join('');
|
|
153
|
+
}
|
|
154
|
+
getMainAssets(scriptType) {
|
|
155
|
+
const assets = this.getChunkAssets([
|
|
156
|
+
...this.entrypoints,
|
|
157
|
+
...this.chunks
|
|
158
|
+
]);
|
|
159
|
+
return scriptType ? assets.filter((asset)=>asset.scriptType === scriptType) : assets;
|
|
160
|
+
}
|
|
161
|
+
getScriptTags(extraProps = {}) {
|
|
162
|
+
const scripts = this.getMainAssets("script").map((asset)=>`<script async data-chunk="${asset.chunk}" src="${asset.url}"${extraPropsToString(extraProps)}></script>`);
|
|
163
|
+
return [
|
|
164
|
+
this.getRequiredChunksScriptTag(extraProps),
|
|
165
|
+
...scripts
|
|
166
|
+
].join('');
|
|
167
|
+
}
|
|
168
|
+
getStyleTags(extraProps = {}) {
|
|
169
|
+
return this.getMainAssets('style').map((asset)=>`<link data-chunk="${asset.chunk}" rel="stylesheet" href="${asset.url}"${extraPropsToString(extraProps)}>`).join('');
|
|
170
|
+
}
|
|
171
|
+
getLinkTags(extraProps = {}) {
|
|
172
|
+
const assets = [
|
|
173
|
+
...this.getMainAssets(),
|
|
174
|
+
...this.getChunkChildAssets([
|
|
175
|
+
...this.entrypoints,
|
|
176
|
+
...this.chunks
|
|
177
|
+
], 'preload'),
|
|
178
|
+
...this.getChunkChildAssets([
|
|
179
|
+
...this.entrypoints,
|
|
180
|
+
...this.chunks
|
|
181
|
+
], 'prefetch')
|
|
182
|
+
];
|
|
183
|
+
return uniqByUrl(assets).map((asset)=>`<link data-chunk="${asset.chunk}" rel="${asset.linkType}" as="${asset.scriptType}" href="${asset.url}"${extraPropsToString(extraProps)}>`).join('');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const trimSlashes = (value)=>String(value).replace(/^\/+|\/+$/gu, '');
|
|
2
|
+
const normalizeSeparators = (value)=>String(value).replace(/\\+/gu, '/');
|
|
3
|
+
export const sep = '/';
|
|
4
|
+
export const delimiter = ':';
|
|
5
|
+
export function isAbsolute(filePath) {
|
|
6
|
+
return normalizeSeparators(filePath).startsWith('/');
|
|
7
|
+
}
|
|
8
|
+
export function normalize(filePath) {
|
|
9
|
+
const normalized = normalizeSeparators(filePath);
|
|
10
|
+
const absolute = isAbsolute(normalized);
|
|
11
|
+
const parts = [];
|
|
12
|
+
for (const part of normalized.split('/'))if (part && '.' !== part) {
|
|
13
|
+
if ('..' === part) {
|
|
14
|
+
if (parts.length > 0 && '..' !== parts[parts.length - 1]) parts.pop();
|
|
15
|
+
else if (!absolute) parts.push(part);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
parts.push(part);
|
|
19
|
+
}
|
|
20
|
+
const joined = parts.join('/');
|
|
21
|
+
if (absolute) return joined ? `/${joined}` : '/';
|
|
22
|
+
return joined || '.';
|
|
23
|
+
}
|
|
24
|
+
export function join(...segments) {
|
|
25
|
+
const joined = segments.map(normalizeSeparators).filter(Boolean).map((segment, index)=>0 === index ? segment : trimSlashes(segment)).filter(Boolean).join('/');
|
|
26
|
+
return joined ? normalize(joined) : '.';
|
|
27
|
+
}
|
|
28
|
+
export function resolve(...segments) {
|
|
29
|
+
const joined = join(...segments);
|
|
30
|
+
return joined.startsWith('/') ? joined : `/${joined}`;
|
|
31
|
+
}
|
|
32
|
+
export function dirname(filePath) {
|
|
33
|
+
const normalized = normalizeSeparators(filePath).replace(/\/+$/u, '');
|
|
34
|
+
const index = normalized.lastIndexOf('/');
|
|
35
|
+
if (index <= 0) return '/';
|
|
36
|
+
return normalized.slice(0, index);
|
|
37
|
+
}
|
|
38
|
+
export function basename(filePath, suffix = '') {
|
|
39
|
+
const normalized = normalizeSeparators(filePath).replace(/\/+$/u, '');
|
|
40
|
+
const base = normalized.slice(normalized.lastIndexOf('/') + 1);
|
|
41
|
+
return suffix && base.endsWith(suffix) ? base.slice(0, -suffix.length) : base;
|
|
42
|
+
}
|
|
43
|
+
export function extname(filePath) {
|
|
44
|
+
const base = basename(filePath);
|
|
45
|
+
const index = base.lastIndexOf('.');
|
|
46
|
+
if (index <= 0) return '';
|
|
47
|
+
return base.slice(index);
|
|
48
|
+
}
|
|
49
|
+
export default {
|
|
50
|
+
basename,
|
|
51
|
+
delimiter,
|
|
52
|
+
dirname,
|
|
53
|
+
extname,
|
|
54
|
+
isAbsolute,
|
|
55
|
+
join,
|
|
56
|
+
normalize,
|
|
57
|
+
resolve,
|
|
58
|
+
sep
|
|
59
|
+
};
|