@tschk/moonshine-deploy-vercel 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/package.json +15 -8
- package/src/index.ts +204 -116
package/README.md
CHANGED
|
@@ -18,3 +18,17 @@ route becomes a separate function; `revalidation` is not supported.
|
|
|
18
18
|
|
|
19
19
|
See [docs/DEPLOYMENT.md](../../docs/DEPLOYMENT.md) and
|
|
20
20
|
[docs/MANIFEST.md](../../docs/MANIFEST.md).
|
|
21
|
+
|
|
22
|
+
Native manifests support only the Node runtime. The adapter bundles one standalone
|
|
23
|
+
Node 22 function (`index.mjs`) containing the native renderer and route modules.
|
|
24
|
+
Build Output filesystem routing serves public client assets first, then sends
|
|
25
|
+
requests through the complete loader/action/middleware pipeline, including routes
|
|
26
|
+
marked static. No React or Solid runtime is included. For native builds, only files within the build's `public` directory can enter
|
|
27
|
+
Vercel static output. Legacy explicit external asset registrations remain supported,
|
|
28
|
+
while the build's `dist` directory and registered server entry are excluded.
|
|
29
|
+
|
|
30
|
+
Native Edge, request context, islands/hydration, and cache/revalidation options are
|
|
31
|
+
rejected. Local acceptance runs the relocated function under actual Node HTTP with
|
|
32
|
+
a small filesystem-routing harness; this does not emulate Vercel infrastructure
|
|
33
|
+
or establish a public deployment. Legacy rendering requires the optional
|
|
34
|
+
`@tschk/moonshine-react` peer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tschk/moonshine-deploy-vercel",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -15,18 +15,25 @@
|
|
|
15
15
|
"test": "bun test"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@tschk/moonshine-
|
|
19
|
-
"@tschk/moonshine-
|
|
20
|
-
"@tschk/moonshine-
|
|
21
|
-
"@tschk/moonshine-
|
|
22
|
-
"@tschk/moonshine-server": "^0.3.6"
|
|
18
|
+
"@tschk/moonshine-framework": "^0.4.4",
|
|
19
|
+
"@tschk/moonshine-router": "^0.4.4",
|
|
20
|
+
"@tschk/moonshine-server": "^0.4.4",
|
|
21
|
+
"@tschk/moonshine-deploy-node": "^0.4.4"
|
|
23
22
|
},
|
|
24
23
|
"devDependencies": {
|
|
25
24
|
"@types/bun": "1.3.14",
|
|
26
25
|
"typescript": "7.0.2",
|
|
27
|
-
"@tschk/moonshine-adapter-conformance": "^0.
|
|
26
|
+
"@tschk/moonshine-adapter-conformance": "^0.4.4"
|
|
28
27
|
},
|
|
29
28
|
"files": [
|
|
30
29
|
"src"
|
|
31
|
-
]
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@tschk/moonshine-react": "^0.4.4"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"@tschk/moonshine-react": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
32
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
1
|
+
import { copyFile, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { tryServeStatic } from "@tschk/moonshine-server";
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
DeploymentAdapter,
|
|
7
|
-
MoonshineManifest,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type DeploymentAdapter,
|
|
8
|
+
type MoonshineManifest,
|
|
9
|
+
type RenderContext,
|
|
10
|
+
type RouteArtifact,
|
|
11
|
+
type Harness,
|
|
12
|
+
type HarnessFactory,
|
|
13
|
+
resolveManifestFiles,
|
|
14
|
+
copyAssets,
|
|
15
|
+
validateRuntime,
|
|
16
|
+
validateCapabilities,
|
|
11
17
|
} from "@tschk/moonshine-framework";
|
|
12
18
|
|
|
13
19
|
type VercelRoute = {
|
|
@@ -20,92 +26,6 @@ type VercelConfig = {
|
|
|
20
26
|
routes: VercelRoute[];
|
|
21
27
|
};
|
|
22
28
|
|
|
23
|
-
function resolveManifestFiles(
|
|
24
|
-
manifest: MoonshineManifest,
|
|
25
|
-
projectRoot: string,
|
|
26
|
-
): MoonshineManifest {
|
|
27
|
-
return {
|
|
28
|
-
...manifest,
|
|
29
|
-
routes: manifest.routes.map((route) => ({
|
|
30
|
-
...route,
|
|
31
|
-
file: isAbsolute(route.file)
|
|
32
|
-
? route.file
|
|
33
|
-
: resolve(projectRoot, route.file),
|
|
34
|
-
...(route.dataFile && {
|
|
35
|
-
dataFile: isAbsolute(route.dataFile)
|
|
36
|
-
? route.dataFile
|
|
37
|
-
: resolve(projectRoot, route.dataFile),
|
|
38
|
-
}),
|
|
39
|
-
...(route.layouts && {
|
|
40
|
-
layouts: route.layouts.map((layout) =>
|
|
41
|
-
isAbsolute(layout) ? layout : resolve(projectRoot, layout),
|
|
42
|
-
),
|
|
43
|
-
}),
|
|
44
|
-
...(route.middleware && {
|
|
45
|
-
middleware: route.middleware.map((middleware) =>
|
|
46
|
-
isAbsolute(middleware)
|
|
47
|
-
? middleware
|
|
48
|
-
: resolve(projectRoot, middleware),
|
|
49
|
-
),
|
|
50
|
-
}),
|
|
51
|
-
...(route.errorBoundary && {
|
|
52
|
-
errorBoundary: isAbsolute(route.errorBoundary)
|
|
53
|
-
? route.errorBoundary
|
|
54
|
-
: resolve(projectRoot, route.errorBoundary),
|
|
55
|
-
}),
|
|
56
|
-
})),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function copyAssets(
|
|
61
|
-
assets: MoonshineManifest["assets"],
|
|
62
|
-
outDir: string,
|
|
63
|
-
): Promise<void> {
|
|
64
|
-
await Promise.all(
|
|
65
|
-
assets.map(async (asset) => {
|
|
66
|
-
const source = isAbsolute(asset.file)
|
|
67
|
-
? asset.file
|
|
68
|
-
: resolve(outDir, asset.file);
|
|
69
|
-
const target = resolve(
|
|
70
|
-
outDir,
|
|
71
|
-
(asset.path ?? asset.file).replace(/^\/+/, ""),
|
|
72
|
-
);
|
|
73
|
-
await mkdir(dirname(target), { recursive: true });
|
|
74
|
-
if (source !== target) {
|
|
75
|
-
await copyFile(source, target);
|
|
76
|
-
}
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function validateRuntime(
|
|
82
|
-
adapter: DeploymentAdapter,
|
|
83
|
-
manifest: MoonshineManifest,
|
|
84
|
-
): void {
|
|
85
|
-
const errors: string[] = [];
|
|
86
|
-
for (const route of manifest.routes) {
|
|
87
|
-
if (!adapter.runtimes.includes(route.runtime)) {
|
|
88
|
-
errors.push(
|
|
89
|
-
`${adapter.name} adapter does not support route "${route.path}" with runtime "${route.runtime}"; supported runtimes: ${adapter.runtimes.join(", ")}`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (errors.length) throw new Error(errors.join("\n"));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function validateCapabilities(
|
|
97
|
-
adapter: DeploymentAdapter,
|
|
98
|
-
manifest: MoonshineManifest,
|
|
99
|
-
): void {
|
|
100
|
-
for (const capability of manifest.capabilities) {
|
|
101
|
-
if (!adapter.capabilities.includes(capability)) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`${adapter.name} adapter does not support capability "${capability}"; supported capabilities: ${adapter.capabilities.join(", ")}`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
29
|
function routeToRegex(path: string): string {
|
|
110
30
|
return `^${path
|
|
111
31
|
.replace(/\//g, "\\/")
|
|
@@ -132,7 +52,7 @@ async function mapConcurrent<T, R>(
|
|
|
132
52
|
fn: (item: T) => Promise<R>,
|
|
133
53
|
concurrency: number,
|
|
134
54
|
): Promise<R[]> {
|
|
135
|
-
const results =
|
|
55
|
+
const results: R[] = Array.from({ length: items.length });
|
|
136
56
|
const errors: unknown[] = [];
|
|
137
57
|
let index = 0;
|
|
138
58
|
|
|
@@ -163,18 +83,189 @@ async function prerenderRoute(route: RouteArtifact): Promise<string> {
|
|
|
163
83
|
data: {},
|
|
164
84
|
signal: new AbortController().signal,
|
|
165
85
|
};
|
|
86
|
+
const { reactRenderer } = await import("@tschk/moonshine-react");
|
|
166
87
|
return reactRenderer.prerender(context);
|
|
167
88
|
}
|
|
168
89
|
|
|
90
|
+
function validateAssetPath(path: string): void {
|
|
91
|
+
let decoded = path;
|
|
92
|
+
try {
|
|
93
|
+
decoded = decodeURIComponent(path);
|
|
94
|
+
} catch {
|
|
95
|
+
/* Literal percent filenames are valid. */
|
|
96
|
+
}
|
|
97
|
+
for (const value of [path, decoded]) {
|
|
98
|
+
if (
|
|
99
|
+
!value.startsWith("/") ||
|
|
100
|
+
value.includes("\\") ||
|
|
101
|
+
Array.from(value).some(
|
|
102
|
+
(char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127,
|
|
103
|
+
) ||
|
|
104
|
+
value.split("/").includes("..")
|
|
105
|
+
)
|
|
106
|
+
throw new Error(`Invalid public asset path: ${path}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Native output exposes only public files; legacy registries exclude server bundles. */
|
|
111
|
+
async function copyPublicAssets(
|
|
112
|
+
manifest: MoonshineManifest,
|
|
113
|
+
outDir: string,
|
|
114
|
+
staticDir: string,
|
|
115
|
+
) {
|
|
116
|
+
await Promise.all(
|
|
117
|
+
manifest.assets.map(async (asset) => {
|
|
118
|
+
const source = await realpath(
|
|
119
|
+
isAbsolute(asset.file) ? asset.file : resolve(outDir, asset.file),
|
|
120
|
+
);
|
|
121
|
+
const directory = resolve(
|
|
122
|
+
outDir,
|
|
123
|
+
manifest.renderer === "native" ? "public" : "dist",
|
|
124
|
+
);
|
|
125
|
+
const canonicalDirectory = await realpath(directory).catch(
|
|
126
|
+
() => directory,
|
|
127
|
+
);
|
|
128
|
+
const sourcePath = relative(canonicalDirectory, source);
|
|
129
|
+
const withinDirectory =
|
|
130
|
+
sourcePath !== ".." &&
|
|
131
|
+
!sourcePath.startsWith(`..${sep}`) &&
|
|
132
|
+
!isAbsolute(sourcePath);
|
|
133
|
+
if (manifest.renderer === "native" ? !withinDirectory : withinDirectory)
|
|
134
|
+
return;
|
|
135
|
+
if (manifest.renderer !== "native" && manifest.entries.server) {
|
|
136
|
+
const entry = resolve(outDir, manifest.entries.server);
|
|
137
|
+
if (source === (await realpath(entry).catch(() => entry))) return;
|
|
138
|
+
}
|
|
139
|
+
validateAssetPath(asset.path);
|
|
140
|
+
const publicDirectory = resolve(outDir, "public");
|
|
141
|
+
const canonicalPublic = await realpath(publicDirectory).catch(
|
|
142
|
+
() => publicDirectory,
|
|
143
|
+
);
|
|
144
|
+
const publicPath = relative(canonicalPublic, source);
|
|
145
|
+
const withinPublic =
|
|
146
|
+
publicPath !== ".." &&
|
|
147
|
+
!publicPath.startsWith(`..${sep}`) &&
|
|
148
|
+
!isAbsolute(publicPath);
|
|
149
|
+
const canonicalUrl =
|
|
150
|
+
"/" + publicPath.split(sep).map(encodeURIComponent).join("/");
|
|
151
|
+
const preservePublicFilename =
|
|
152
|
+
withinPublic &&
|
|
153
|
+
(manifest.renderer === "native" || asset.path === canonicalUrl);
|
|
154
|
+
const target = resolve(
|
|
155
|
+
staticDir,
|
|
156
|
+
preservePublicFilename ? publicPath : asset.path.replace(/^\/+/, ""),
|
|
157
|
+
);
|
|
158
|
+
const targetPath = relative(staticDir, target);
|
|
159
|
+
if (
|
|
160
|
+
!targetPath ||
|
|
161
|
+
targetPath === ".." ||
|
|
162
|
+
targetPath.startsWith(`..${sep}`) ||
|
|
163
|
+
isAbsolute(targetPath)
|
|
164
|
+
)
|
|
165
|
+
throw new Error(`Invalid public asset path: ${asset.path}`);
|
|
166
|
+
await mkdir(dirname(target), { recursive: true });
|
|
167
|
+
if (source !== target) await copyFile(source, target);
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function buildNativeFunction(
|
|
173
|
+
manifest: MoonshineManifest,
|
|
174
|
+
outDir: string,
|
|
175
|
+
) {
|
|
176
|
+
const output = resolve(outDir, ".vercel/output");
|
|
177
|
+
const funcDir = resolve(output, "functions/moonshine.func");
|
|
178
|
+
const staticDir = resolve(output, "static");
|
|
179
|
+
await mkdir(funcDir, { recursive: true });
|
|
180
|
+
await mkdir(staticDir, { recursive: true });
|
|
181
|
+
await writeFile(
|
|
182
|
+
resolve(outDir, "manifest.json"),
|
|
183
|
+
JSON.stringify(manifest) + "\n",
|
|
184
|
+
);
|
|
185
|
+
await copyPublicAssets(manifest, outDir, staticDir);
|
|
186
|
+
const entryPath = resolve(outDir, "vercel-native.ts");
|
|
187
|
+
await writeFile(
|
|
188
|
+
entryPath,
|
|
189
|
+
`import { createRequestHandler } from "@tschk/moonshine-server";
|
|
190
|
+
import { createNodeHandler } from ${JSON.stringify(fileURLToPath(import.meta.resolve("@tschk/moonshine-deploy-node")))};
|
|
191
|
+
import { modules, renderer } from "./dist/server.js";
|
|
192
|
+
import manifest from "./manifest.json" with { type: "json" };
|
|
193
|
+
const fetch = createRequestHandler({ manifest, modules, renderer });
|
|
194
|
+
export default createNodeHandler({ fetch });
|
|
195
|
+
`,
|
|
196
|
+
);
|
|
197
|
+
const result = await Bun.build({
|
|
198
|
+
entrypoints: [entryPath],
|
|
199
|
+
outdir: funcDir,
|
|
200
|
+
naming: "index.mjs",
|
|
201
|
+
target: "node",
|
|
202
|
+
packages: "bundle",
|
|
203
|
+
});
|
|
204
|
+
if (!result.success)
|
|
205
|
+
throw new AggregateError(
|
|
206
|
+
result.logs,
|
|
207
|
+
"Native Vercel Node function bundle failed",
|
|
208
|
+
);
|
|
209
|
+
await writeFile(
|
|
210
|
+
resolve(funcDir, ".vc-config.json"),
|
|
211
|
+
JSON.stringify(
|
|
212
|
+
{
|
|
213
|
+
runtime: "nodejs22.x",
|
|
214
|
+
handler: "index.mjs",
|
|
215
|
+
launcherType: "Nodejs",
|
|
216
|
+
supportsResponseStreaming: true,
|
|
217
|
+
},
|
|
218
|
+
null,
|
|
219
|
+
2,
|
|
220
|
+
) + "\n",
|
|
221
|
+
);
|
|
222
|
+
await writeFile(
|
|
223
|
+
resolve(output, "config.json"),
|
|
224
|
+
JSON.stringify(
|
|
225
|
+
{
|
|
226
|
+
version: 3,
|
|
227
|
+
routes: [{ handle: "filesystem" }, { src: "/.*", dest: "/moonshine" }],
|
|
228
|
+
},
|
|
229
|
+
null,
|
|
230
|
+
2,
|
|
231
|
+
) + "\n",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
169
235
|
export const vercelAdapter: DeploymentAdapter = {
|
|
170
236
|
name: "vercel",
|
|
171
237
|
runtimes: ["node", "vercel-edge"],
|
|
172
238
|
capabilities: ["streaming", "islands", "edge"],
|
|
173
239
|
build: async (manifest, outDir) => {
|
|
240
|
+
if (manifest.renderer === "native") {
|
|
241
|
+
if (manifest.routes.some((route) => route.runtime !== "node"))
|
|
242
|
+
throw new Error("Native Vercel supports only the Node runtime");
|
|
243
|
+
if (manifest.requestContext)
|
|
244
|
+
throw new Error("Native Vercel request context is not supported");
|
|
245
|
+
if (manifest.routes.some((route) => route.mode === "island"))
|
|
246
|
+
throw new Error("Native island hydration is not implemented");
|
|
247
|
+
if (
|
|
248
|
+
manifest.routes.some((route) => route.cache) ||
|
|
249
|
+
manifest.capabilities.includes("revalidation")
|
|
250
|
+
)
|
|
251
|
+
throw new Error(
|
|
252
|
+
"Native Vercel caching/revalidation is not implemented",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
174
255
|
validateRuntime(vercelAdapter, manifest);
|
|
175
256
|
validateCapabilities(vercelAdapter, manifest);
|
|
176
257
|
|
|
177
258
|
await mkdir(outDir, { recursive: true });
|
|
259
|
+
await rm(resolve(outDir, ".vercel/output"), {
|
|
260
|
+
recursive: true,
|
|
261
|
+
force: true,
|
|
262
|
+
});
|
|
263
|
+
if (manifest.renderer === "native") {
|
|
264
|
+
await buildNativeFunction(manifest, outDir);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
for (const asset of manifest.assets) validateAssetPath(asset.path);
|
|
268
|
+
await copyAssets(manifest.assets, outDir);
|
|
178
269
|
const projectRoot = resolve(outDir, "..");
|
|
179
270
|
const resolvedManifest = resolveManifestFiles(manifest, projectRoot);
|
|
180
271
|
|
|
@@ -182,7 +273,6 @@ export const vercelAdapter: DeploymentAdapter = {
|
|
|
182
273
|
resolve(outDir, "manifest.json"),
|
|
183
274
|
JSON.stringify(resolvedManifest, null, 2) + "\n",
|
|
184
275
|
);
|
|
185
|
-
await copyAssets(manifest.assets, outDir);
|
|
186
276
|
|
|
187
277
|
const vercelOutput = resolve(outDir, ".vercel/output");
|
|
188
278
|
const staticDir = resolve(vercelOutput, "static");
|
|
@@ -196,9 +286,15 @@ export const vercelAdapter: DeploymentAdapter = {
|
|
|
196
286
|
const routesConfig = await mapConcurrent(
|
|
197
287
|
resolvedManifest.routes,
|
|
198
288
|
async (route) => {
|
|
199
|
-
if (
|
|
200
|
-
|
|
289
|
+
if (
|
|
290
|
+
route.mode === "static" &&
|
|
291
|
+
!route.path.includes("*") &&
|
|
292
|
+
!route.path.includes(":")
|
|
293
|
+
) {
|
|
294
|
+
// Allocate the output slug before awaiting so concurrent workers
|
|
295
|
+
// cannot race on `usedSlugs`.
|
|
201
296
|
const slug = staticSlug(route.path, usedSlugs);
|
|
297
|
+
const html = await prerenderRoute(route);
|
|
202
298
|
await writeFile(resolve(staticDir, `${slug}.html`), html);
|
|
203
299
|
return {
|
|
204
300
|
src: routeToRegex(route.path),
|
|
@@ -242,21 +338,7 @@ export default { fetch };
|
|
|
242
338
|
);
|
|
243
339
|
config.routes.push(...routesConfig);
|
|
244
340
|
|
|
245
|
-
await
|
|
246
|
-
resolvedManifest.assets.map(async (asset) => {
|
|
247
|
-
const source = isAbsolute(asset.file)
|
|
248
|
-
? asset.file
|
|
249
|
-
: resolve(outDir, asset.file);
|
|
250
|
-
const target = resolve(
|
|
251
|
-
staticDir,
|
|
252
|
-
(asset.path ?? asset.file).replace(/^\/+/, ""),
|
|
253
|
-
);
|
|
254
|
-
await mkdir(dirname(target), { recursive: true });
|
|
255
|
-
if (source !== target) {
|
|
256
|
-
await copyFile(source, target);
|
|
257
|
-
}
|
|
258
|
-
}),
|
|
259
|
-
);
|
|
341
|
+
await copyPublicAssets(resolvedManifest, outDir, staticDir);
|
|
260
342
|
|
|
261
343
|
await writeFile(
|
|
262
344
|
resolve(vercelOutput, "manifest.json"),
|
|
@@ -278,7 +360,8 @@ const fetch = createRequestHandler({
|
|
|
278
360
|
staticDir: join(import.meta.dir, ".vercel/output/static"),
|
|
279
361
|
});
|
|
280
362
|
|
|
281
|
-
|
|
363
|
+
const envPort = Number(process.env.PORT);
|
|
364
|
+
Bun.serve({ port: Number.isInteger(envPort) && envPort >= 0 && envPort <= 65535 ? envPort : 0, fetch });
|
|
282
365
|
`;
|
|
283
366
|
await writeFile(resolve(outDir, "server.ts"), serverEntry);
|
|
284
367
|
},
|
|
@@ -294,7 +377,12 @@ export const vercelHarness: HarnessFactory = (fetch, options) => {
|
|
|
294
377
|
(request.method === "GET" || request.method === "HEAD") &&
|
|
295
378
|
pathname !== "/"
|
|
296
379
|
) {
|
|
297
|
-
const res = await tryServeStatic(
|
|
380
|
+
const res = await tryServeStatic(
|
|
381
|
+
options.staticDir,
|
|
382
|
+
pathname,
|
|
383
|
+
undefined,
|
|
384
|
+
request.method,
|
|
385
|
+
);
|
|
298
386
|
if (res) return res;
|
|
299
387
|
}
|
|
300
388
|
return fetch(request);
|