@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.13 → 3.9.0-ultramodern.14
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/cloudflare/worker-manifest.js +13 -4
- package/dist/cjs/cloudflare/wrangler-config.js +35 -7
- package/dist/cjs/cloudflare-builder.js +235 -14
- package/dist/cjs/cloudflare-output-contract.js +31 -2
- package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
- package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- 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/cloudflare/worker-manifest.mjs +13 -4
- package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
- package/dist/esm/cloudflare-builder.mjs +229 -17
- package/dist/esm/cloudflare-output-contract.mjs +24 -1
- package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
- package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- 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/cloudflare/worker-manifest.mjs +13 -4
- package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
- package/dist/esm-node/cloudflare-builder.mjs +229 -17
- package/dist/esm-node/cloudflare-output-contract.mjs +24 -1
- package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
- package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- 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/cloudflare/wrangler-config.d.ts +6 -0
- package/dist/types/cloudflare-builder.d.ts +44 -1
- package/dist/types/cloudflare-output-contract.d.ts +3 -1
- package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
- package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
- package/package.json +17 -13
- package/src/cloudflare/worker-manifest.ts +19 -3
- package/src/cloudflare/wrangler-config.ts +86 -10
- package/src/cloudflare-builder.ts +447 -12
- package/src/cloudflare-output-contract.ts +34 -2
- package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
- package/src/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import node_path from "node:path";
|
|
2
2
|
import { fs } from "@modern-js/utils";
|
|
3
|
+
import { getCloudflareWorkerRouteDataEntryName } from "../cloudflare-output-contract.mjs";
|
|
3
4
|
import { readRouteSpec } from "./artifacts.mjs";
|
|
4
5
|
import { ASSETS_BINDING, BFF_EFFECT_WORKER_DISPATCHER_EXPORT, BFF_EFFECT_WORKER_ENTRY, CLOUDFLARE_RUNTIME_TYPE, CLOUDFLARE_WORKER_BUNDLE_FORMAT, DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES, EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE, LOADABLE_STATS_FILE, PUBLIC_ASSETS_DIRECTORY, ROUTE_MANIFEST_FILE, ROUTE_SPEC_FILE, ROUTE_SPEC_OUTPUT, SERVER_BUNDLE_DIRECTORY, WORKER_BUNDLE_DIRECTORY, WORKER_ENTRY } from "./constants.mjs";
|
|
5
6
|
import { createI18nWorkerManifest } from "./i18n-worker.mjs";
|
|
@@ -65,13 +66,18 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, d
|
|
|
65
66
|
const routeSpec = await readRouteSpec(outputDirectory);
|
|
66
67
|
const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
|
|
67
68
|
const worker = 'string' == typeof route.worker ? route.worker : void 0;
|
|
69
|
+
const routeDataWorker = route.isSSR && 'string' == typeof route.entryName ? `${WORKER_BUNDLE_DIRECTORY}/${getCloudflareWorkerRouteDataEntryName(route.entryName)}.js` : void 0;
|
|
70
|
+
const hasRouteDataWorker = routeDataWorker ? await fs.pathExists(node_path.join(outputDirectory, routeDataWorker)) : false;
|
|
68
71
|
return {
|
|
69
72
|
urlPath: route.urlPath,
|
|
70
73
|
entryName: route.entryName,
|
|
71
74
|
entryPath: route.entryPath,
|
|
72
75
|
isSSR: Boolean(route.isSSR),
|
|
73
76
|
worker,
|
|
74
|
-
workerExists: worker ? await fs.pathExists(node_path.join(outputDirectory, worker)) : false
|
|
77
|
+
workerExists: worker ? await fs.pathExists(node_path.join(outputDirectory, worker)) : false,
|
|
78
|
+
...hasRouteDataWorker ? {
|
|
79
|
+
routeDataWorker
|
|
80
|
+
} : {}
|
|
75
81
|
};
|
|
76
82
|
}));
|
|
77
83
|
const bffPrefix = modernConfig.bff?.prefix;
|
|
@@ -131,9 +137,12 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, d
|
|
|
131
137
|
};
|
|
132
138
|
const createWorkerModuleLoaders = (manifest)=>{
|
|
133
139
|
const imports = new Map();
|
|
134
|
-
for (const route of manifest.routeSpec.routes)
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
for (const route of manifest.routeSpec.routes)for (const worker of [
|
|
141
|
+
route.workerExists ? route.worker : void 0,
|
|
142
|
+
route.routeDataWorker
|
|
143
|
+
])if (worker) {
|
|
144
|
+
const importPath = `../${String(worker).replace(/^\/+/u, '')}`;
|
|
145
|
+
imports.set(worker, `() => import(${JSON.stringify(importPath)})`);
|
|
137
146
|
}
|
|
138
147
|
if (manifest.bff?.worker) {
|
|
139
148
|
const importPath = `../${String(manifest.bff.worker).replace(/^\/+/u, '')}`;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import node_path from "node:path";
|
|
2
2
|
import { ASSETS_BINDING, COMPATIBILITY_DATE_PATTERN, DEFAULT_COMPATIBILITY_DATE, PUBLIC_ASSETS_DIRECTORY, REQUIRED_COMPATIBILITY_FLAGS, WORKER_ENTRY } from "./constants.mjs";
|
|
3
3
|
import { isJsonRecord, normalizeRelativePath } from "./utils.mjs";
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (
|
|
4
|
+
const assertSupportedCompatibilityDate = (compatibilityDate, label)=>{
|
|
5
|
+
if ('string' != typeof compatibilityDate) throw new Error(`${label} must be a YYYY-MM-DD string.`);
|
|
6
|
+
if (!COMPATIBILITY_DATE_PATTERN.test(compatibilityDate)) throw new Error(`${label} must use YYYY-MM-DD, received ${JSON.stringify(compatibilityDate)}.`);
|
|
7
|
+
if (compatibilityDate < DEFAULT_COMPATIBILITY_DATE) throw new Error(`${label} must be ${DEFAULT_COMPATIBILITY_DATE} or later, the date the Cloudflare worker Node.js built-in contract is verified against; received ${JSON.stringify(compatibilityDate)}.`);
|
|
8
8
|
return compatibilityDate;
|
|
9
9
|
};
|
|
10
|
+
const getCompatibilityDate = (modernConfig, wranglerCompatibilityDate)=>{
|
|
11
|
+
if (void 0 !== wranglerCompatibilityDate) return assertSupportedCompatibilityDate(wranglerCompatibilityDate, 'deploy.worker.wrangler.compatibility_date');
|
|
12
|
+
return assertSupportedCompatibilityDate(modernConfig.deploy?.worker?.compatibilityDate?.trim() || DEFAULT_COMPATIBILITY_DATE, 'deploy.worker.compatibilityDate');
|
|
13
|
+
};
|
|
10
14
|
const getWorkerName = (appDirectory)=>{
|
|
11
15
|
const basename = node_path.basename(appDirectory);
|
|
12
16
|
return basename.replace(/[^a-zA-Z0-9-_]/g, '-') || 'modern-cloudflare-worker';
|
|
@@ -21,11 +25,11 @@ const getConfiguredWrangler = (modernConfig)=>{
|
|
|
21
25
|
if (!isJsonRecord(wrangler)) throw new Error('deploy.worker.wrangler must be a JSON object.');
|
|
22
26
|
return wrangler;
|
|
23
27
|
};
|
|
24
|
-
const createWranglerCompatibilityFlags = (configuredFlags)=>{
|
|
28
|
+
const createWranglerCompatibilityFlags = (configuredFlags, label = 'deploy.worker.wrangler.compatibility_flags')=>{
|
|
25
29
|
if (void 0 === configuredFlags) return [
|
|
26
30
|
...REQUIRED_COMPATIBILITY_FLAGS
|
|
27
31
|
];
|
|
28
|
-
if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error(
|
|
32
|
+
if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error(`${label} must be an array of strings.`);
|
|
29
33
|
return [
|
|
30
34
|
...new Set([
|
|
31
35
|
...configuredFlags,
|
|
@@ -33,6 +37,26 @@ const createWranglerCompatibilityFlags = (configuredFlags)=>{
|
|
|
33
37
|
])
|
|
34
38
|
];
|
|
35
39
|
};
|
|
40
|
+
const createWranglerEnvironments = (wranglerEnv)=>{
|
|
41
|
+
if (void 0 === wranglerEnv) return;
|
|
42
|
+
if (!isJsonRecord(wranglerEnv)) throw new Error('deploy.worker.wrangler.env must be an object.');
|
|
43
|
+
return Object.fromEntries(Object.entries(wranglerEnv).map(([name, environment])=>{
|
|
44
|
+
const label = `deploy.worker.wrangler.env.${name}`;
|
|
45
|
+
if (!isJsonRecord(environment)) throw new Error(`${label} must be an object.`);
|
|
46
|
+
return [
|
|
47
|
+
name,
|
|
48
|
+
{
|
|
49
|
+
...environment,
|
|
50
|
+
...void 0 === environment.compatibility_date ? {} : {
|
|
51
|
+
compatibility_date: assertSupportedCompatibilityDate(environment.compatibility_date, `${label}.compatibility_date`)
|
|
52
|
+
},
|
|
53
|
+
...void 0 === environment.compatibility_flags ? {} : {
|
|
54
|
+
compatibility_flags: createWranglerCompatibilityFlags(environment.compatibility_flags, `${label}.compatibility_flags`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
}));
|
|
59
|
+
};
|
|
36
60
|
const createWranglerAssetsConfig = (configuredAssets)=>{
|
|
37
61
|
if (void 0 !== configuredAssets && !isJsonRecord(configuredAssets)) throw new Error('deploy.worker.wrangler.assets must be an object.');
|
|
38
62
|
return {
|
|
@@ -129,17 +153,21 @@ const createWorkerManifestServiceBindings = (serviceBindings)=>{
|
|
|
129
153
|
};
|
|
130
154
|
const createWranglerConfig = (appDirectory, modernConfig)=>{
|
|
131
155
|
const wrangler = getConfiguredWrangler(modernConfig);
|
|
156
|
+
const environments = createWranglerEnvironments(wrangler.env);
|
|
132
157
|
const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
|
|
133
158
|
const serviceBindings = createWorkerServiceBindings(modernConfig, wrangler.services);
|
|
134
159
|
const wranglerServices = createWranglerServices(serviceBindings);
|
|
135
160
|
return {
|
|
136
161
|
$schema: 'node_modules/wrangler/config-schema.json',
|
|
137
162
|
name: getConfiguredWorkerName(appDirectory, modernConfig),
|
|
138
|
-
compatibility_date: getCompatibilityDate(modernConfig),
|
|
139
163
|
...wrangler,
|
|
164
|
+
compatibility_date: getCompatibilityDate(modernConfig, wrangler.compatibility_date),
|
|
140
165
|
main: WORKER_ENTRY,
|
|
141
166
|
compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
|
|
142
167
|
assets: createWranglerAssetsConfig(wrangler.assets),
|
|
168
|
+
...void 0 === environments ? {} : {
|
|
169
|
+
env: environments
|
|
170
|
+
},
|
|
143
171
|
...void 0 === d1Databases ? {} : {
|
|
144
172
|
d1_databases: d1Databases
|
|
145
173
|
},
|
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
import node_fs from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
2
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
3
3
|
import node_path from "node:path";
|
|
4
4
|
import { SERVICE_WORKER_ENVIRONMENT_NAME } from "@modern-js/builder";
|
|
5
5
|
import { isProd } from "@modern-js/utils";
|
|
6
6
|
import { provider } from "std-env";
|
|
7
|
-
import { CLOUDFLARE_WORKER_NODE_BUILTINS } from "./cloudflare-output-contract.mjs";
|
|
7
|
+
import { CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES, getCloudflareWorkerRouteDataEntryName } from "./cloudflare-output-contract.mjs";
|
|
8
8
|
import { getTemplatePath } from "./read-template.mjs";
|
|
9
9
|
const moduleRequire = createRequire(import.meta.url);
|
|
10
10
|
const BFF_EFFECT_WORKER_ENTRY_NAME = '__modern_bff_effect';
|
|
11
11
|
const BFF_EFFECT_WORKER_RUNTIME_QUERY = 'modern-bff-runtime';
|
|
12
|
+
const SERVER_LOADER_COMBINED_FILE = 'server-loader-combined.js';
|
|
13
|
+
const ROUTE_SERVER_LOADERS_FILE = 'route-server-loaders.js';
|
|
14
|
+
const WORKER_ROUTE_DATA_FILE = 'cloudflare-worker-route-data.js';
|
|
15
|
+
const WORKER_ROUTE_DATA_TEMPLATE = 'cloudflare-worker-route-data.mjs';
|
|
16
|
+
const DATA_LOADER_CLIENT_TRANSFORM = /plugin-data-loader[\\/](?:.+[\\/])?loader\.[cm]?js$/u;
|
|
12
17
|
const MF_SSR_DATA_FETCH_RUNTIME_PLUGIN = '@module-federation/modern-js-v3/ssr-inject-data-fetch-function-plugin';
|
|
13
18
|
const MF_SSR_DEV_RUNTIME_PLUGIN = '@module-federation/modern-js-v3/ssr-dev-plugin';
|
|
19
|
+
const WORKER_DEPENDENCY_CONDITIONS = [
|
|
20
|
+
[
|
|
21
|
+
'esm',
|
|
22
|
+
'import'
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
'wasm',
|
|
26
|
+
'import'
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
'loaderImport',
|
|
30
|
+
'import'
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'worker',
|
|
34
|
+
'import'
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
'commonjs',
|
|
38
|
+
'require'
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
'amd',
|
|
42
|
+
'require'
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
'loader',
|
|
46
|
+
'require'
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
'unknown',
|
|
50
|
+
'require'
|
|
51
|
+
]
|
|
52
|
+
];
|
|
14
53
|
const JS_OR_TS_EXTENSIONS = new Set([
|
|
15
54
|
'.js',
|
|
16
55
|
'.jsx',
|
|
@@ -70,19 +109,154 @@ const resolvePackageFile = (packageName, filePath, paths)=>{
|
|
|
70
109
|
const setAliasIfPresent = (alias, name, value)=>{
|
|
71
110
|
if (value) alias.set(name, value);
|
|
72
111
|
};
|
|
73
|
-
const getCloudflareWorkerNodeExternals = ()=>Object.fromEntries(
|
|
74
|
-
|
|
112
|
+
const getCloudflareWorkerNodeExternals = ()=>Object.fromEntries([
|
|
113
|
+
...CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap((builtin)=>{
|
|
114
|
+
const nodeBuiltin = `node:${builtin}`;
|
|
115
|
+
const external = `module-import ${nodeBuiltin}`;
|
|
116
|
+
return isBuiltin(builtin) ? [
|
|
117
|
+
[
|
|
118
|
+
builtin,
|
|
119
|
+
external
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
nodeBuiltin,
|
|
123
|
+
external
|
|
124
|
+
]
|
|
125
|
+
] : [
|
|
126
|
+
[
|
|
127
|
+
nodeBuiltin,
|
|
128
|
+
external
|
|
129
|
+
]
|
|
130
|
+
];
|
|
131
|
+
}),
|
|
132
|
+
...CLOUDFLARE_WORKER_PLATFORM_MODULES.map((platformModule)=>[
|
|
133
|
+
platformModule,
|
|
134
|
+
`module-import ${platformModule}`
|
|
135
|
+
])
|
|
136
|
+
]);
|
|
137
|
+
const getPackageNameFromRequest = (request)=>{
|
|
138
|
+
if (request.startsWith('.') || request.startsWith('/') || request.includes(':') || node_path.isAbsolute(request)) return;
|
|
139
|
+
const segments = request.split('/');
|
|
140
|
+
return request.startsWith('@') ? segments.length > 1 ? `${segments[0]}/${segments[1]}` : void 0 : segments[0];
|
|
141
|
+
};
|
|
142
|
+
const isRedirectedByAliasField = (manifest, aliasFields, request)=>aliasFields.some((field)=>{
|
|
143
|
+
let value = manifest;
|
|
144
|
+
for (const key of 'string' == typeof field ? [
|
|
145
|
+
field
|
|
146
|
+
] : field)value = value && 'object' == typeof value ? value[key] : void 0;
|
|
147
|
+
return 'object' == typeof value && null !== value && Object.hasOwn(value, request);
|
|
148
|
+
});
|
|
149
|
+
const findOwningPackageManifest = (directory, cache)=>{
|
|
150
|
+
if (cache.has(directory)) return cache.get(directory);
|
|
151
|
+
let manifest;
|
|
152
|
+
const packageJsonPath = node_path.join(directory, 'package.json');
|
|
153
|
+
if (node_fs.existsSync(packageJsonPath)) try {
|
|
154
|
+
manifest = JSON.parse(node_fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
155
|
+
} catch {
|
|
156
|
+
manifest = void 0;
|
|
157
|
+
}
|
|
158
|
+
else if (node_path.dirname(directory) !== directory) manifest = findOwningPackageManifest(node_path.dirname(directory), cache);
|
|
159
|
+
cache.set(directory, manifest);
|
|
160
|
+
return manifest;
|
|
161
|
+
};
|
|
162
|
+
const isPackageInstalled = (packageName, directory, moduleDirectories)=>moduleDirectories.some((moduleDirectory)=>{
|
|
163
|
+
if (node_path.isAbsolute(moduleDirectory)) return node_fs.existsSync(node_path.join(moduleDirectory, packageName));
|
|
164
|
+
let current = directory;
|
|
165
|
+
while(true){
|
|
166
|
+
if (node_fs.existsSync(node_path.join(current, moduleDirectory, packageName))) return true;
|
|
167
|
+
const parent = node_path.dirname(current);
|
|
168
|
+
if (parent === current) return false;
|
|
169
|
+
current = parent;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const createAbsentOptionalDependencyFilter = (isRedirected = ()=>false, moduleDirectories = [
|
|
173
|
+
'node_modules'
|
|
174
|
+
], aliasFields = [])=>{
|
|
175
|
+
const manifests = new Map();
|
|
176
|
+
return (request, context)=>{
|
|
177
|
+
const packageName = getPackageNameFromRequest(request);
|
|
178
|
+
if (!packageName || !context || isRedirected(request)) return false;
|
|
179
|
+
const manifest = findOwningPackageManifest(context, manifests);
|
|
180
|
+
if (!manifest) return false;
|
|
181
|
+
const optional = manifest.peerDependenciesMeta?.[packageName]?.optional === true || Object.hasOwn(manifest.optionalDependencies ?? {}, packageName);
|
|
182
|
+
return optional && !isRedirectedByAliasField(manifest, aliasFields, request) && !isPackageInstalled(packageName, context, moduleDirectories);
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
const getAliasNames = (alias)=>alias ? Array.isArray(alias) ? alias.map((entry)=>entry.onlyModule ? `${entry.name}$` : entry.name) : Object.keys(alias) : [];
|
|
186
|
+
const getExternalMatchers = (externals)=>{
|
|
187
|
+
if (Array.isArray(externals)) return externals.flatMap(getExternalMatchers);
|
|
188
|
+
if ('string' == typeof externals) return [
|
|
189
|
+
(request)=>request === externals
|
|
190
|
+
];
|
|
191
|
+
if (externals instanceof RegExp) return [
|
|
192
|
+
(request)=>{
|
|
193
|
+
externals.lastIndex = 0;
|
|
194
|
+
return externals.test(request);
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
if (externals && 'object' == typeof externals) {
|
|
198
|
+
const names = new Set(Object.entries(externals).filter(([, value])=>false !== value).map(([name])=>name));
|
|
75
199
|
return [
|
|
76
|
-
|
|
77
|
-
builtin,
|
|
78
|
-
`module-import ${nodeBuiltin}`
|
|
79
|
-
],
|
|
80
|
-
[
|
|
81
|
-
nodeBuiltin,
|
|
82
|
-
`module-import ${nodeBuiltin}`
|
|
83
|
-
]
|
|
200
|
+
(request)=>names.has(request)
|
|
84
201
|
];
|
|
85
|
-
}
|
|
202
|
+
}
|
|
203
|
+
return [];
|
|
204
|
+
};
|
|
205
|
+
const createRequestRedirectMatcher = (options)=>{
|
|
206
|
+
const aliasNames = [
|
|
207
|
+
...getAliasNames(options.resolve?.alias),
|
|
208
|
+
...getAliasNames(options.resolve?.fallback)
|
|
209
|
+
];
|
|
210
|
+
const externalMatchers = getExternalMatchers(options.externals);
|
|
211
|
+
return (request)=>externalMatchers.some((matches)=>matches(request)) || aliasNames.some((name)=>name.endsWith('$') ? request === name.slice(0, -1) : request === name || request.startsWith(`${name}/`));
|
|
212
|
+
};
|
|
213
|
+
const getFunctionExternals = (externals)=>Array.isArray(externals) ? externals.flatMap(getFunctionExternals) : 'function' == typeof externals ? [
|
|
214
|
+
externals
|
|
215
|
+
] : [];
|
|
216
|
+
const callFunctionExternal = (external, data)=>new Promise((resolve, reject)=>{
|
|
217
|
+
const returned = external(data, (error, result)=>error ? reject(error) : resolve(result));
|
|
218
|
+
if (null !== returned && 'object' == typeof returned && 'function' == typeof returned.then) returned.then(resolve, reject);
|
|
219
|
+
else if (1 === external.length) resolve(returned);
|
|
220
|
+
});
|
|
221
|
+
const claimsByFunctionExternal = async (externals, data)=>{
|
|
222
|
+
for (const external of externals)try {
|
|
223
|
+
const result = await callFunctionExternal(external, data);
|
|
224
|
+
if (null != result && false !== result) return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
};
|
|
230
|
+
const PLUGIN_NAME = 'AbsentOptionalDependencyPlugin';
|
|
231
|
+
class AbsentOptionalDependencyPlugin {
|
|
232
|
+
apply(compiler) {
|
|
233
|
+
const isAbsent = createAbsentOptionalDependencyFilter(createRequestRedirectMatcher(compiler.options), compiler.options.resolve.modules, compiler.options.resolve.aliasFields);
|
|
234
|
+
const functionExternals = getFunctionExternals(compiler.options.externals);
|
|
235
|
+
compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (factory)=>{
|
|
236
|
+
factory.hooks.beforeResolve.tapPromise(PLUGIN_NAME, async (data)=>{
|
|
237
|
+
if (!isAbsent(data.request, data.context)) return;
|
|
238
|
+
const claimed = await claimsByFunctionExternal(functionExternals, {
|
|
239
|
+
context: data.context,
|
|
240
|
+
contextInfo: {
|
|
241
|
+
issuer: data.contextInfo.issuer,
|
|
242
|
+
issuerLayer: data.contextInfo.issuerLayer ?? null
|
|
243
|
+
},
|
|
244
|
+
getResolve: (options)=>{
|
|
245
|
+
const resolver = factory.getResolver('normal', options ?? {});
|
|
246
|
+
return (context, request, callback)=>{
|
|
247
|
+
if (callback) return void resolver.resolve({}, context, request, {}, callback);
|
|
248
|
+
return new Promise((resolve, reject)=>{
|
|
249
|
+
resolver.resolve({}, context, request, {}, (error, result)=>error ? reject(error) : resolve(result || void 0));
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
request: data.request
|
|
254
|
+
});
|
|
255
|
+
return claimed ? void 0 : false;
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
86
260
|
const normalizeWorkerOutputName = (name)=>node_path.posix.normalize(name.replace(/\\/gu, '/')).toLowerCase();
|
|
87
261
|
const reserveWorkerChunkName = (baseName, reservedOutputNames)=>{
|
|
88
262
|
let name = baseName;
|
|
@@ -132,6 +306,9 @@ function applyCloudflareWorkerRspackConfig(chain, workerEntryNames) {
|
|
|
132
306
|
chain.optimization.runtimeChunk(config.optimization.runtimeChunk);
|
|
133
307
|
chain.optimization.splitChunks(config.optimization.splitChunks);
|
|
134
308
|
}
|
|
309
|
+
function removeCloudflareWorkerDataLoaderClientTransform(chain) {
|
|
310
|
+
for (const [name, rule] of Object.entries(chain.module.rules.entries() ?? {}))if (rule.uses.values().some((use)=>DATA_LOADER_CLIENT_TRANSFORM.test(String(use.get('loader'))))) chain.module.rules.delete(name);
|
|
311
|
+
}
|
|
135
312
|
function applyCloudflareWorkerMfRuntimeBoundary(chain) {
|
|
136
313
|
const runtimePlugin = getTemplatePath('cloudflare-worker-mf-ssr-runtime-plugin.mjs');
|
|
137
314
|
chain.plugins.delete('plugin-module-federation');
|
|
@@ -203,11 +380,40 @@ const prependBundlerChain = (environment, handler)=>{
|
|
|
203
380
|
}
|
|
204
381
|
};
|
|
205
382
|
};
|
|
383
|
+
const createWorkerRouteDataModule = ()=>[
|
|
384
|
+
`import { routes } from './${ROUTE_SERVER_LOADERS_FILE}';`,
|
|
385
|
+
`import { createRouteDataRequestHandler } from ${JSON.stringify(getTemplatePath(WORKER_ROUTE_DATA_TEMPLATE))};`,
|
|
386
|
+
'',
|
|
387
|
+
`export const ${CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT} =`,
|
|
388
|
+
' createRouteDataRequestHandler(routes);',
|
|
389
|
+
''
|
|
390
|
+
].join('\n');
|
|
391
|
+
const getWorkerRouteDataEntries = (appContext)=>{
|
|
392
|
+
const { entrypoints, internalDirectory } = appContext;
|
|
393
|
+
if (!entrypoints || !internalDirectory) return {};
|
|
394
|
+
return Object.fromEntries(entrypoints.flatMap(({ entryName })=>{
|
|
395
|
+
const entryDirectory = node_path.join(internalDirectory, entryName);
|
|
396
|
+
if (!node_fs.existsSync(node_path.join(entryDirectory, SERVER_LOADER_COMBINED_FILE)) || !node_fs.existsSync(node_path.join(entryDirectory, ROUTE_SERVER_LOADERS_FILE))) return [];
|
|
397
|
+
const routeDataFile = node_path.join(entryDirectory, WORKER_ROUTE_DATA_FILE);
|
|
398
|
+
node_fs.writeFileSync(routeDataFile, createWorkerRouteDataModule());
|
|
399
|
+
return [
|
|
400
|
+
[
|
|
401
|
+
getCloudflareWorkerRouteDataEntryName(entryName),
|
|
402
|
+
[
|
|
403
|
+
routeDataFile
|
|
404
|
+
]
|
|
405
|
+
]
|
|
406
|
+
];
|
|
407
|
+
}));
|
|
408
|
+
};
|
|
206
409
|
const getWorkerEntries = (environment, normalizedConfig, appContext)=>{
|
|
207
410
|
const configuredEntries = environment.source?.entry;
|
|
208
411
|
const effectApiEntry = getEffectBffEntry(normalizedConfig, appContext);
|
|
209
412
|
if (!configuredEntries && !effectApiEntry) return;
|
|
210
|
-
const entries = configuredEntries ?
|
|
413
|
+
const entries = configuredEntries ? {
|
|
414
|
+
...rewriteWorkerEntries(configuredEntries),
|
|
415
|
+
...getWorkerRouteDataEntries(appContext)
|
|
416
|
+
} : {};
|
|
211
417
|
return effectApiEntry ? {
|
|
212
418
|
...entries,
|
|
213
419
|
[BFF_EFFECT_WORKER_ENTRY_NAME]: [
|
|
@@ -240,6 +446,7 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
|
|
|
240
446
|
];
|
|
241
447
|
return (chain)=>{
|
|
242
448
|
applyCloudflareWorkerRspackConfig(chain, entryNames);
|
|
449
|
+
removeCloudflareWorkerDataLoaderClientTransform(chain);
|
|
243
450
|
chain.output.module(true).library({
|
|
244
451
|
type: 'module'
|
|
245
452
|
}).publicPath('/').chunkFormat('module').chunkLoading('import').workerChunkLoading('import');
|
|
@@ -248,10 +455,15 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
|
|
|
248
455
|
'worker',
|
|
249
456
|
'webpack',
|
|
250
457
|
isProd() ? 'production' : 'development',
|
|
251
|
-
'import',
|
|
252
|
-
'require',
|
|
253
458
|
'module'
|
|
254
459
|
])chain.resolve.conditionNames.add(condition);
|
|
460
|
+
for (const [dependencyType, condition] of WORKER_DEPENDENCY_CONDITIONS)chain.resolve.byDependency.set(dependencyType, {
|
|
461
|
+
conditionNames: [
|
|
462
|
+
condition,
|
|
463
|
+
'...'
|
|
464
|
+
]
|
|
465
|
+
});
|
|
466
|
+
chain.plugin('cloudflare-worker-absent-optional-dependencies').use(AbsentOptionalDependencyPlugin);
|
|
255
467
|
applyCloudflareWorkerMfRuntimeBoundary(chain);
|
|
256
468
|
if (tanstackRouterSsrServerFile) {
|
|
257
469
|
chain.resolve.alias.set('@tanstack/router-core/ssr/server$', tanstackRouterSsrServerFile);
|
|
@@ -334,4 +546,4 @@ const createCloudflareBuilderPlugin = (resolveDeployProvider = resolveStdEnvDepl
|
|
|
334
546
|
}));
|
|
335
547
|
}
|
|
336
548
|
});
|
|
337
|
-
export { applyCloudflareBuilderEnvironments, applyCloudflareWorkerMfRuntimeBoundary, applyCloudflareWorkerRspackConfig, createCloudflareBuilderPlugin, getCloudflareBuilderEnvironments, getCloudflareWorkerRspackConfig };
|
|
549
|
+
export { applyCloudflareBuilderEnvironments, applyCloudflareWorkerMfRuntimeBoundary, applyCloudflareWorkerRspackConfig, claimsByFunctionExternal, createAbsentOptionalDependencyFilter, createCloudflareBuilderPlugin, createRequestRedirectMatcher, getCloudflareBuilderEnvironments, getCloudflareWorkerRspackConfig };
|
|
@@ -10,15 +10,30 @@ const CLOUDFLARE_RUNTIME_TYPE = 'cloudflare-module-worker';
|
|
|
10
10
|
const CLOUDFLARE_OUTPUT_PACKAGE_TYPE = 'module';
|
|
11
11
|
const CLOUDFLARE_WORKER_PACKAGE_TYPE = 'commonjs';
|
|
12
12
|
const CLOUDFLARE_WORKER_BUNDLE_FORMAT = 'commonjs';
|
|
13
|
+
const CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT = 'handleRouteDataRequest';
|
|
14
|
+
const getCloudflareWorkerRouteDataEntryName = (entryName)=>`${entryName}-server-loaders`;
|
|
13
15
|
const CLOUDFLARE_WORKER_NODE_BUILTINS = [
|
|
16
|
+
'_http_agent',
|
|
17
|
+
'_http_client',
|
|
18
|
+
'_http_common',
|
|
19
|
+
'_http_incoming',
|
|
20
|
+
'_http_outgoing',
|
|
21
|
+
'_http_server',
|
|
22
|
+
'_tls_common',
|
|
23
|
+
'_tls_wrap',
|
|
14
24
|
'assert',
|
|
15
25
|
'assert/strict',
|
|
16
26
|
'async_hooks',
|
|
17
27
|
'buffer',
|
|
18
28
|
'child_process',
|
|
29
|
+
'cluster',
|
|
30
|
+
'console',
|
|
31
|
+
'constants',
|
|
19
32
|
'crypto',
|
|
20
33
|
'dgram',
|
|
34
|
+
'diagnostics_channel',
|
|
21
35
|
'dns',
|
|
36
|
+
'dns/promises',
|
|
22
37
|
'domain',
|
|
23
38
|
'events',
|
|
24
39
|
'fs',
|
|
@@ -27,11 +42,17 @@ const CLOUDFLARE_WORKER_NODE_BUILTINS = [
|
|
|
27
42
|
'http2',
|
|
28
43
|
'https',
|
|
29
44
|
'inspector',
|
|
45
|
+
'inspector/promises',
|
|
30
46
|
'module',
|
|
31
47
|
'net',
|
|
32
48
|
'os',
|
|
33
49
|
'path',
|
|
50
|
+
'path/posix',
|
|
51
|
+
'path/win32',
|
|
52
|
+
'perf_hooks',
|
|
34
53
|
'process',
|
|
54
|
+
'punycode',
|
|
55
|
+
'querystring',
|
|
35
56
|
'readline',
|
|
36
57
|
'readline/promises',
|
|
37
58
|
'repl',
|
|
@@ -41,6 +62,8 @@ const CLOUDFLARE_WORKER_NODE_BUILTINS = [
|
|
|
41
62
|
'stream/promises',
|
|
42
63
|
'stream/web',
|
|
43
64
|
'string_decoder',
|
|
65
|
+
'sys',
|
|
66
|
+
'test',
|
|
44
67
|
'timers',
|
|
45
68
|
'timers/promises',
|
|
46
69
|
'tls',
|
|
@@ -62,4 +85,4 @@ const CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS = [
|
|
|
62
85
|
'nodejs_compat',
|
|
63
86
|
'global_fetch_strictly_public'
|
|
64
87
|
];
|
|
65
|
-
export { CLOUDFLARE_ASSETS_BINDING, CLOUDFLARE_OUTPUT_PACKAGE_FILE, CLOUDFLARE_OUTPUT_PACKAGE_TYPE, CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY, CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS, CLOUDFLARE_RUNTIME_TYPE, CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_BUNDLE_FORMAT, CLOUDFLARE_WORKER_ENTRY, CLOUDFLARE_WORKER_MANIFEST, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PACKAGE_FILE, CLOUDFLARE_WORKER_PACKAGE_TYPE, CLOUDFLARE_WORKER_PLATFORM_MODULES, CLOUDFLARE_WRANGLER_CONFIG_FILE };
|
|
88
|
+
export { CLOUDFLARE_ASSETS_BINDING, CLOUDFLARE_OUTPUT_PACKAGE_FILE, CLOUDFLARE_OUTPUT_PACKAGE_TYPE, CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY, CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS, CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_RUNTIME_TYPE, CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_BUNDLE_FORMAT, CLOUDFLARE_WORKER_ENTRY, CLOUDFLARE_WORKER_MANIFEST, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PACKAGE_FILE, CLOUDFLARE_WORKER_PACKAGE_TYPE, CLOUDFLARE_WORKER_PLATFORM_MODULES, CLOUDFLARE_WRANGLER_CONFIG_FILE, getCloudflareWorkerRouteDataEntryName };
|
|
@@ -4,9 +4,9 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { parse } from "@babel/parser";
|
|
6
6
|
import traverse from "@babel/traverse";
|
|
7
|
-
import { CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES } from "../cloudflare-output-contract.mjs";
|
|
7
|
+
import { CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES } from "../cloudflare-output-contract.mjs";
|
|
8
8
|
import { addIssue } from "./issues.mjs";
|
|
9
|
-
const getReferencedRouteWorkers = (manifest)=>Array.isArray(manifest?.routeSpec?.routes) ? manifest.routeSpec.routes.map((route)=>'string' == typeof route?.
|
|
9
|
+
const getReferencedRouteWorkers = (manifest, field)=>Array.isArray(manifest?.routeSpec?.routes) ? manifest.routeSpec.routes.map((route)=>'string' == typeof route?.[field] && route[field].length > 0 ? route[field] : void 0).filter((worker)=>'string' == typeof worker) : [];
|
|
10
10
|
const getEffectBffWorker = (manifest)=>manifest?.bff?.runtimeFramework === 'effect' && 'string' == typeof manifest.bff.worker ? manifest.bff.worker : void 0;
|
|
11
11
|
const getWorkerBundleReferences = (manifest)=>{
|
|
12
12
|
const effectBffWorker = getEffectBffWorker(manifest);
|
|
@@ -18,9 +18,14 @@ const getWorkerBundleReferences = (manifest)=>{
|
|
|
18
18
|
reference: effectBffWorker
|
|
19
19
|
}
|
|
20
20
|
] : [],
|
|
21
|
-
...getReferencedRouteWorkers(manifest).map((reference)=>({
|
|
21
|
+
...getReferencedRouteWorkers(manifest, 'worker').map((reference)=>({
|
|
22
22
|
kind: 'route',
|
|
23
23
|
reference
|
|
24
|
+
})),
|
|
25
|
+
...getReferencedRouteWorkers(manifest, 'routeDataWorker').map((reference)=>({
|
|
26
|
+
dispatcherExport: CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT,
|
|
27
|
+
kind: 'route-data',
|
|
28
|
+
reference
|
|
24
29
|
}))
|
|
25
30
|
];
|
|
26
31
|
};
|
|
@@ -41,7 +46,7 @@ const resolveWorkerBundleReference = (issues, outputDirectory, reference, manife
|
|
|
41
46
|
path: workerPath
|
|
42
47
|
};
|
|
43
48
|
};
|
|
44
|
-
const missingWorkerBundleMessage = (reference)=>'effect-bff' === reference.kind ? 'Cloudflare Effect BFF manifest points to a missing worker bundle.' : 'Cloudflare route worker manifest points to a missing worker bundle.';
|
|
49
|
+
const missingWorkerBundleMessage = (reference)=>'effect-bff' === reference.kind ? 'Cloudflare Effect BFF manifest points to a missing worker bundle.' : 'route-data' === reference.kind ? 'Cloudflare route data worker manifest points to a missing worker bundle.' : 'Cloudflare route worker manifest points to a missing worker bundle.';
|
|
45
50
|
const isAstNode = (value)=>null !== value && 'object' == typeof value && 'type' in value && 'string' == typeof value.type;
|
|
46
51
|
const getNodeName = (node)=>{
|
|
47
52
|
if (!isAstNode(node)) return;
|
|
@@ -285,9 +290,9 @@ const verifyWorkerModuleClosure = async (issues, entryPath, entrySource, contrac
|
|
|
285
290
|
};
|
|
286
291
|
const verifyWorkerBundleReferences = async (issues, worker, source, contract)=>{
|
|
287
292
|
const entryAnalysis = await verifyWorkerModuleClosure(issues, worker.path, source, contract);
|
|
288
|
-
if ('
|
|
293
|
+
if ('string' == typeof worker.dispatcherExport && !entryAnalysis?.exports.has(worker.dispatcherExport)) addIssue(issues, {
|
|
289
294
|
code: 'invalid-worker-bundle',
|
|
290
|
-
message: 'Cloudflare Effect BFF worker bundle must expose its manifest dispatcherExport.',
|
|
295
|
+
message: 'route-data' === worker.kind ? `Cloudflare route data worker bundle must expose ${worker.dispatcherExport}.` : 'Cloudflare Effect BFF worker bundle must expose its manifest dispatcherExport.',
|
|
291
296
|
path: worker.path
|
|
292
297
|
});
|
|
293
298
|
};
|
|
@@ -52,6 +52,52 @@ async function getRequestHandler(workerModule) {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const ROUTE_DATA_REQUEST_PARAM = '__loader';
|
|
56
|
+
|
|
57
|
+
// Route data requests (`?__loader=`) are answered by the entry's route data
|
|
58
|
+
// worker, like the Node server's data handler. Without a match the request
|
|
59
|
+
// falls through to page rendering, as it does on Node.
|
|
60
|
+
async function dispatchRouteDataRequest(route, request) {
|
|
61
|
+
const routeDataWorkerPath = route.routeDataWorker;
|
|
62
|
+
if (
|
|
63
|
+
!routeDataWorkerPath ||
|
|
64
|
+
!new URL(request.url).searchParams.has(ROUTE_DATA_REQUEST_PARAM)
|
|
65
|
+
) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const routeDataWorkerModule = await loadWorkerModule(routeDataWorkerPath);
|
|
70
|
+
const handleRouteDataRequest = routeDataWorkerModule
|
|
71
|
+
? getRuntimeModule(routeDataWorkerModule).handleRouteDataRequest
|
|
72
|
+
: undefined;
|
|
73
|
+
|
|
74
|
+
if (typeof handleRouteDataRequest !== 'function') {
|
|
75
|
+
return new Response(
|
|
76
|
+
`Route data worker bundle has no handleRouteDataRequest export: ${routeDataWorkerPath}`,
|
|
77
|
+
{
|
|
78
|
+
status: 500,
|
|
79
|
+
headers: {
|
|
80
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
81
|
+
'x-modern-js-route-data-worker': routeDataWorkerPath,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return handleRouteDataRequest({
|
|
88
|
+
request,
|
|
89
|
+
serverRoutes: MODERN_WORKER_MANIFEST.routeSpec.routes,
|
|
90
|
+
context: {
|
|
91
|
+
loaderContext: new Map(),
|
|
92
|
+
monitors: createNoopMonitors(),
|
|
93
|
+
reporter: {
|
|
94
|
+
reportTiming: () => {},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
onTiming() {},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
55
101
|
async function dispatchRouteWorker(route, request, env, ctx) {
|
|
56
102
|
const workerPath = route.worker;
|
|
57
103
|
if (!workerPath) {
|
|
@@ -62,11 +62,18 @@ export default {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (route?.worker) {
|
|
65
|
-
|
|
65
|
+
// Route data requests keep their method, as on the Node server.
|
|
66
|
+
const routeDataResponse = await dispatchRouteDataRequest(route, request);
|
|
66
67
|
|
|
67
68
|
return finalizeResponseForRequest(
|
|
68
69
|
withAppCorsHeaders(
|
|
69
|
-
|
|
70
|
+
routeDataResponse ??
|
|
71
|
+
(await dispatchRouteWorker(
|
|
72
|
+
route,
|
|
73
|
+
createRenderableRequest(request),
|
|
74
|
+
env,
|
|
75
|
+
ctx,
|
|
76
|
+
)),
|
|
70
77
|
request,
|
|
71
78
|
),
|
|
72
79
|
request,
|