create-monolayer-app 0.5.0 → 0.6.1
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/bin/index.js +91 -59
- package/package.json +19 -16
- package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/SKILL.md +2 -2
- package/template/.agents/skills/application-permissions/agents/openai.yaml +4 -0
- package/template/.agents/skills/kysely/SKILL.md +48 -0
- package/template/.agents/skills/kysely/agents/openai.yaml +4 -0
- package/template/.agents/skills/kysely/references/execution-plugins-and-runtime.md +67 -0
- package/template/.agents/skills/kysely/references/expressions-and-helpers.md +102 -0
- package/template/.agents/skills/kysely/references/migrations.md +43 -0
- package/template/.agents/skills/kysely/references/official-docs-map.md +97 -0
- package/template/.agents/skills/kysely/references/query-building.md +119 -0
- package/template/.agents/skills/kysely/references/transactions.md +47 -0
- package/template/.agents/skills/kysely/references/troubleshooting-and-boundaries.md +67 -0
- package/template/.agents/skills/kysely/references/types.md +58 -0
- package/template/.agents/skills/persistent-app-data/SKILL.md +74 -0
- package/template/.agents/skills/persistent-app-data/agents/openai.yaml +4 -0
- package/template/.agents/skills/persistent-app-data/references/generated-types.md +23 -0
- package/template/.agents/skills/persistent-app-data/references/migrations.md +68 -0
- package/template/.agents/skills/vitest/GENERATION.md +25 -0
- package/template/.agents/skills/vitest/SKILL.md +59 -0
- package/template/.agents/skills/vitest/references/advanced-environments.md +26 -0
- package/template/.agents/skills/vitest/references/advanced-projects.md +31 -0
- package/template/.agents/skills/vitest/references/advanced-type-testing.md +30 -0
- package/template/.agents/skills/vitest/references/advanced-vi.md +23 -0
- package/template/.agents/skills/vitest/references/core-cli.md +23 -0
- package/template/.agents/skills/vitest/references/core-config.md +24 -0
- package/template/.agents/skills/vitest/references/core-describe.md +24 -0
- package/template/.agents/skills/vitest/references/core-expect.md +35 -0
- package/template/.agents/skills/vitest/references/core-hooks.md +26 -0
- package/template/.agents/skills/vitest/references/core-test-api.md +29 -0
- package/template/.agents/skills/vitest/references/features-benchmarking.md +25 -0
- package/template/.agents/skills/vitest/references/features-concurrency.md +20 -0
- package/template/.agents/skills/vitest/references/features-context.md +27 -0
- package/template/.agents/skills/vitest/references/features-coverage.md +17 -0
- package/template/.agents/skills/vitest/references/features-filtering.md +24 -0
- package/template/.agents/skills/vitest/references/features-mocking.md +32 -0
- package/template/.agents/skills/vitest/references/features-reporters.md +22 -0
- package/template/.agents/skills/vitest/references/features-snapshots.md +23 -0
- package/template/.agents/skills/vitest/references/features-test-tags.md +30 -0
- package/template/.agents/skills/vitest/references/version-compatibility.md +19 -0
- package/template/.claude/CLAUDE.md +2 -2
- package/template/.react-router/types/+routes.ts +41 -0
- package/template/.react-router/types/+server-build.d.ts +18 -0
- package/template/.react-router/types/app/+types/root.ts +68 -0
- package/template/.react-router/types/app/routes/+types/files.ts +71 -0
- package/template/.react-router/types/app/routes/+types/home.ts +71 -0
- package/template/AGENTS.md +6 -2
- package/template/app/routes/files.ts +3 -3
- package/template/build/client/assets/entry.client-Bc58m2-m.js +9 -0
- package/template/build/client/assets/errorBoundaries-CQxPIuVU.js +16 -0
- package/template/build/client/assets/files-BvRk9kiK.js +0 -0
- package/template/build/client/assets/home-CH2INc5e.js +1 -0
- package/template/build/client/assets/jsx-runtime-eLCqVKHJ.js +1 -0
- package/template/build/client/assets/logo-dark-pX2395Y0.svg +23 -0
- package/template/build/client/assets/logo-light-CVbx2LBR.svg +23 -0
- package/template/build/client/assets/manifest-2d65dd0f.js +1 -0
- package/template/build/client/assets/root-BeNS6njP.css +2 -0
- package/template/build/client/assets/root-C1tmjS9o.js +1 -0
- package/template/build/client/favicon.ico +0 -0
- package/template/build/server/index.js +1096 -0
- package/template/lib/db/client.ts +1 -27
- package/template/package-lock.json +8221 -0
- package/template/package.json +7 -3
- package/template/vendor/file-bucket/{client.ts → index.client.ts} +11 -9
- package/template/vendor/file-bucket/{server.ts → index.server.ts} +13 -8
- package/template/vendor/file-bucket/{index.ts → lib/file-bucket.ts} +2 -1
- package/template/vendor/file-bucket/{react-router.ts → react-router.server.ts} +15 -13
- package/template/vendor/kysely-dsql/dsql/dsql-kysely.ts +31 -3
- package/template/vendor/kysely-dsql/generator/migration-file.ts +3 -3
- package/template/vendor/kysely-dsql/plugins/index.ts +0 -2
- package/template/vendor/package-app/package-app.mjs +2 -2
- package/template/vite.config.ts +3 -1
- package/template/vitest.config.ts +1 -1
- package/template/.agents/skills/monolayer-application-permissions/agents/openai.yaml +0 -4
- package/template/vendor/kysely-dsql/plugins/soft-delete.plugin.ts +0 -217
- /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/development-and-verification.md +0 -0
- /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/interface-examples.md +0 -0
- /package/template/.agents/skills/{monolayer-application-permissions → application-permissions}/references/server-examples.md +0 -0
- /package/template/lib/{db/user-context.server.ts → user-context.server.ts} +0 -0
- /package/template/vendor/file-bucket/{errors.ts → lib/errors.server.ts} +0 -0
- /package/template/vendor/file-bucket/{multipart.ts → lib/multipart.ts} +0 -0
- /package/template/vendor/file-bucket/{protocol.ts → lib/protocol.ts} +0 -0
- /package/template/vendor/file-bucket/{transport.ts → lib/transport.client.ts} +0 -0
- /package/template/vendor/file-bucket/{types.ts → lib/types.ts} +0 -0
- /package/template/vendor/file-bucket/{transfers.ts → lib/upload-signed-url.ts} +0 -0
- /package/template/vendor/file-bucket/{xml.ts → lib/xml.ts} +0 -0
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
import { createReadableStreamFromReadable } from "@react-router/node";
|
|
3
|
+
import { Links, Meta, Outlet, Scripts, ScrollRestoration, ServerRouter, UNSAFE_withComponentProps, UNSAFE_withErrorBoundaryProps, createContext, isRouteErrorResponse } from "react-router";
|
|
4
|
+
import { isbot } from "isbot";
|
|
5
|
+
import { renderToPipeableStream } from "react-dom/server";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
10
|
+
import "@aws-sdk/lib-storage";
|
|
11
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
12
|
+
//#region \0rolldown/runtime.js
|
|
13
|
+
var __defProp = Object.defineProperty;
|
|
14
|
+
var __exportAll = (all, no_symbols) => {
|
|
15
|
+
let target = {};
|
|
16
|
+
for (var name in all) __defProp(target, name, {
|
|
17
|
+
get: all[name],
|
|
18
|
+
enumerable: true
|
|
19
|
+
});
|
|
20
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
21
|
+
return target;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region node_modules/@react-router/dev/dist/config/defaults/entry.server.node.tsx
|
|
25
|
+
var entry_server_node_exports = /* @__PURE__ */ __exportAll({
|
|
26
|
+
default: () => handleRequest,
|
|
27
|
+
streamTimeout: () => streamTimeout
|
|
28
|
+
});
|
|
29
|
+
var streamTimeout = 5e3;
|
|
30
|
+
function handleRequest(request, responseStatusCode, responseHeaders, routerContext, loadContext) {
|
|
31
|
+
if (request.method.toUpperCase() === "HEAD") return new Response(null, {
|
|
32
|
+
status: responseStatusCode,
|
|
33
|
+
headers: responseHeaders
|
|
34
|
+
});
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let shellRendered = false;
|
|
37
|
+
let userAgent = request.headers.get("user-agent");
|
|
38
|
+
let readyOption = userAgent && isbot(userAgent) || routerContext.isSpaMode ? "onAllReady" : "onShellReady";
|
|
39
|
+
let timeoutId = setTimeout(() => abort(), 6e3);
|
|
40
|
+
const { pipe, abort } = renderToPipeableStream(/* @__PURE__ */ jsx(ServerRouter, {
|
|
41
|
+
context: routerContext,
|
|
42
|
+
url: request.url
|
|
43
|
+
}), {
|
|
44
|
+
[readyOption]() {
|
|
45
|
+
shellRendered = true;
|
|
46
|
+
const body = new PassThrough({ final(callback) {
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
timeoutId = void 0;
|
|
49
|
+
callback();
|
|
50
|
+
} });
|
|
51
|
+
const stream = createReadableStreamFromReadable(body);
|
|
52
|
+
responseHeaders.set("Content-Type", "text/html");
|
|
53
|
+
pipe(body);
|
|
54
|
+
resolve(new Response(stream, {
|
|
55
|
+
headers: responseHeaders,
|
|
56
|
+
status: responseStatusCode
|
|
57
|
+
}));
|
|
58
|
+
},
|
|
59
|
+
onShellError(error) {
|
|
60
|
+
reject(error);
|
|
61
|
+
},
|
|
62
|
+
onError(error) {
|
|
63
|
+
responseStatusCode = 500;
|
|
64
|
+
if (shellRendered) console.error(error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region lib/user-context.server.ts
|
|
71
|
+
var userContextMiddleware = async ({ request, context }, next) => {
|
|
72
|
+
const result = userSchema.safeParse({
|
|
73
|
+
email: request.headers.get("x-user-email"),
|
|
74
|
+
role: request.headers.get("x-user-authorization")
|
|
75
|
+
});
|
|
76
|
+
if (!result.success) throw new Response("Unauthorized", { status: 401 });
|
|
77
|
+
context.set(userContext, result.data);
|
|
78
|
+
return next();
|
|
79
|
+
};
|
|
80
|
+
var userSchema = z.object({
|
|
81
|
+
email: z.email(),
|
|
82
|
+
role: z.enum([
|
|
83
|
+
"owner",
|
|
84
|
+
"editor",
|
|
85
|
+
"viewer"
|
|
86
|
+
])
|
|
87
|
+
});
|
|
88
|
+
var userContext = createContext();
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region app/root.tsx
|
|
91
|
+
var root_exports = /* @__PURE__ */ __exportAll({
|
|
92
|
+
ErrorBoundary: () => ErrorBoundary,
|
|
93
|
+
Layout: () => Layout,
|
|
94
|
+
default: () => root_default,
|
|
95
|
+
links: () => links,
|
|
96
|
+
middleware: () => middleware
|
|
97
|
+
});
|
|
98
|
+
var links = () => [
|
|
99
|
+
{
|
|
100
|
+
rel: "preconnect",
|
|
101
|
+
href: "https://fonts.googleapis.com"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
rel: "preconnect",
|
|
105
|
+
href: "https://fonts.gstatic.com",
|
|
106
|
+
crossOrigin: "anonymous"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
rel: "stylesheet",
|
|
110
|
+
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
function Layout({ children }) {
|
|
114
|
+
return /* @__PURE__ */ jsxs("html", {
|
|
115
|
+
lang: "en",
|
|
116
|
+
children: [/* @__PURE__ */ jsxs("head", { children: [
|
|
117
|
+
/* @__PURE__ */ jsx("meta", { charSet: "utf-8" }),
|
|
118
|
+
/* @__PURE__ */ jsx("meta", {
|
|
119
|
+
name: "viewport",
|
|
120
|
+
content: "width=device-width, initial-scale=1"
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ jsx(Meta, {}),
|
|
123
|
+
/* @__PURE__ */ jsx(Links, {})
|
|
124
|
+
] }), /* @__PURE__ */ jsxs("body", { children: [
|
|
125
|
+
children,
|
|
126
|
+
/* @__PURE__ */ jsx(ScrollRestoration, {}),
|
|
127
|
+
/* @__PURE__ */ jsx(Scripts, {})
|
|
128
|
+
] })]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
var root_default = UNSAFE_withComponentProps(function App() {
|
|
132
|
+
return /* @__PURE__ */ jsx(Outlet, {});
|
|
133
|
+
});
|
|
134
|
+
var ErrorBoundary = UNSAFE_withErrorBoundaryProps(function ErrorBoundary({ error }) {
|
|
135
|
+
let message = "Oops!";
|
|
136
|
+
let details = "An unexpected error occurred.";
|
|
137
|
+
let stack;
|
|
138
|
+
if (isRouteErrorResponse(error)) {
|
|
139
|
+
message = error.status === 404 ? "404" : "Error";
|
|
140
|
+
details = error.status === 404 ? "The requested page could not be found." : error.statusText || details;
|
|
141
|
+
}
|
|
142
|
+
return /* @__PURE__ */ jsxs("main", {
|
|
143
|
+
className: "container mx-auto p-4 pt-16",
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ jsx("h1", { children: message }),
|
|
146
|
+
/* @__PURE__ */ jsx("p", { children: details }),
|
|
147
|
+
stack
|
|
148
|
+
]
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
var middleware = [userContextMiddleware];
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region app/welcome/logo-dark.svg
|
|
154
|
+
var logo_dark_default = "/assets/logo-dark-pX2395Y0.svg";
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region app/welcome/logo-light.svg
|
|
157
|
+
var logo_light_default = "/assets/logo-light-CVbx2LBR.svg";
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region app/welcome/welcome.tsx
|
|
160
|
+
function Welcome() {
|
|
161
|
+
return /* @__PURE__ */ jsx("main", {
|
|
162
|
+
className: "flex items-center justify-center pt-16 pb-4",
|
|
163
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
164
|
+
className: "flex min-h-0 flex-1 flex-col items-center gap-16",
|
|
165
|
+
children: [/* @__PURE__ */ jsx("header", {
|
|
166
|
+
className: "flex flex-col items-center gap-9",
|
|
167
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
168
|
+
className: "w-[500px] max-w-[100vw] p-4",
|
|
169
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
170
|
+
src: logo_light_default,
|
|
171
|
+
alt: "React Router",
|
|
172
|
+
className: "block w-full dark:hidden"
|
|
173
|
+
}), /* @__PURE__ */ jsx("img", {
|
|
174
|
+
src: logo_dark_default,
|
|
175
|
+
alt: "React Router",
|
|
176
|
+
className: "hidden w-full dark:block"
|
|
177
|
+
})]
|
|
178
|
+
})
|
|
179
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
180
|
+
className: "w-full max-w-[300px] space-y-6 px-4",
|
|
181
|
+
children: /* @__PURE__ */ jsxs("nav", {
|
|
182
|
+
className: "space-y-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700",
|
|
183
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
184
|
+
className: "text-center leading-6 text-gray-700 dark:text-gray-200",
|
|
185
|
+
children: "What's next?"
|
|
186
|
+
}), /* @__PURE__ */ jsx("ul", { children: resources.map(({ href, text, icon }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
|
|
187
|
+
className: "group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500",
|
|
188
|
+
href,
|
|
189
|
+
target: "_blank",
|
|
190
|
+
rel: "noreferrer",
|
|
191
|
+
children: [icon, text]
|
|
192
|
+
}) }, href)) })]
|
|
193
|
+
})
|
|
194
|
+
})]
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
var resources = [{
|
|
199
|
+
href: "https://reactrouter.com/docs",
|
|
200
|
+
text: "React Router Docs",
|
|
201
|
+
icon: /* @__PURE__ */ jsx("svg", {
|
|
202
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
203
|
+
width: "24",
|
|
204
|
+
height: "20",
|
|
205
|
+
viewBox: "0 0 20 20",
|
|
206
|
+
fill: "none",
|
|
207
|
+
className: "stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300",
|
|
208
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
209
|
+
d: "M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z",
|
|
210
|
+
strokeWidth: "1.5",
|
|
211
|
+
strokeLinecap: "round"
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
}, {
|
|
215
|
+
href: "https://rmx.as/discord",
|
|
216
|
+
text: "Join Discord",
|
|
217
|
+
icon: /* @__PURE__ */ jsx("svg", {
|
|
218
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
219
|
+
width: "24",
|
|
220
|
+
height: "20",
|
|
221
|
+
viewBox: "0 0 24 20",
|
|
222
|
+
fill: "none",
|
|
223
|
+
className: "stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300",
|
|
224
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
225
|
+
d: "M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z",
|
|
226
|
+
strokeWidth: "1.5"
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
}];
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region app/routes/home.tsx
|
|
232
|
+
var home_exports = /* @__PURE__ */ __exportAll({
|
|
233
|
+
default: () => home_default,
|
|
234
|
+
meta: () => meta
|
|
235
|
+
});
|
|
236
|
+
function meta({}) {
|
|
237
|
+
return [{ title: "New React Router App" }, {
|
|
238
|
+
name: "description",
|
|
239
|
+
content: "Welcome to React Router!"
|
|
240
|
+
}];
|
|
241
|
+
}
|
|
242
|
+
var home_default = UNSAFE_withComponentProps(function Home() {
|
|
243
|
+
return /* @__PURE__ */ jsx(Welcome, {});
|
|
244
|
+
});
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region vendor/file-bucket/lib/xml.ts
|
|
247
|
+
var XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
|
|
248
|
+
var ParseError = class extends Error {
|
|
249
|
+
constructor(message) {
|
|
250
|
+
super(message);
|
|
251
|
+
this.name = "ParseError";
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
function escapeXml(str) {
|
|
255
|
+
if (str === null || str === void 0) return "";
|
|
256
|
+
return String(str).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
257
|
+
}
|
|
258
|
+
function unescapeXml(str) {
|
|
259
|
+
return str.replaceAll(""", "\"").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&").replaceAll(/&#(\d+);/gu, (_, dec) => String.fromCodePoint(Math.trunc(Number(dec)))).replaceAll(/&#x([0-9a-fA-F]+);/gu, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Strips namespace prefix or Clark notation from a tag name.
|
|
263
|
+
* e.g. "{http://...}Rule" -> "Rule", "s3:Rule" -> "Rule", "Rule" -> "Rule"
|
|
264
|
+
*/
|
|
265
|
+
function localTagName(tag) {
|
|
266
|
+
if (tag.startsWith("{")) {
|
|
267
|
+
const idx = tag.indexOf("}");
|
|
268
|
+
if (idx !== -1) return tag.slice(idx + 1);
|
|
269
|
+
}
|
|
270
|
+
const colonIdx = tag.indexOf(":");
|
|
271
|
+
if (colonIdx !== -1) return tag.slice(colonIdx + 1);
|
|
272
|
+
return tag;
|
|
273
|
+
}
|
|
274
|
+
function isString(value) {
|
|
275
|
+
return typeof value === "string";
|
|
276
|
+
}
|
|
277
|
+
var XmlElement = class XmlElement {
|
|
278
|
+
tag;
|
|
279
|
+
attrs;
|
|
280
|
+
text;
|
|
281
|
+
children;
|
|
282
|
+
constructor(tag, attrs = {}, text) {
|
|
283
|
+
this.tag = tag;
|
|
284
|
+
this.attrs = { ...attrs };
|
|
285
|
+
this.text = text;
|
|
286
|
+
this.children = [];
|
|
287
|
+
}
|
|
288
|
+
localName() {
|
|
289
|
+
return localTagName(this.tag);
|
|
290
|
+
}
|
|
291
|
+
subElement(tag, textOrAttrs, text) {
|
|
292
|
+
const el = isString(textOrAttrs) ? new XmlElement(tag, {}, textOrAttrs) : new XmlElement(tag, textOrAttrs || {}, text);
|
|
293
|
+
this.children.push(el);
|
|
294
|
+
return el;
|
|
295
|
+
}
|
|
296
|
+
find(tag) {
|
|
297
|
+
const targetLocal = localTagName(tag);
|
|
298
|
+
return this.children.find((c) => c.tag === tag || c.localName() === targetLocal);
|
|
299
|
+
}
|
|
300
|
+
findAll(tag) {
|
|
301
|
+
const targetLocal = localTagName(tag);
|
|
302
|
+
return this.children.filter((c) => c.tag === tag || c.localName() === targetLocal);
|
|
303
|
+
}
|
|
304
|
+
findText(tag) {
|
|
305
|
+
return this.find(tag)?.text;
|
|
306
|
+
}
|
|
307
|
+
findDescendant(tag) {
|
|
308
|
+
const targetLocal = localTagName(tag);
|
|
309
|
+
for (const c of this.children) {
|
|
310
|
+
if (c.tag === tag || c.localName() === targetLocal) return c;
|
|
311
|
+
const deep = c.findDescendant(tag);
|
|
312
|
+
if (deep) return deep;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
findAllDescendants(tag) {
|
|
316
|
+
const targetLocal = localTagName(tag);
|
|
317
|
+
const result = [];
|
|
318
|
+
for (const c of this.children) {
|
|
319
|
+
if (c.tag === tag || c.localName() === targetLocal) result.push(c);
|
|
320
|
+
result.push(...c.findAllDescendants(tag));
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
iter() {
|
|
325
|
+
const result = [this];
|
|
326
|
+
for (const child of this.children) result.push(...child.iter());
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
toXml(includeDeclaration = false) {
|
|
330
|
+
let s = includeDeclaration ? `${XML_DECL}\n` : "";
|
|
331
|
+
s += `<${this.tag}`;
|
|
332
|
+
for (const [k, v] of Object.entries(this.attrs)) s += ` ${k}="${escapeXml(v)}"`;
|
|
333
|
+
if (this.children.length === 0 && this.text === void 0) {
|
|
334
|
+
s += "/>";
|
|
335
|
+
return s;
|
|
336
|
+
}
|
|
337
|
+
s += ">";
|
|
338
|
+
if (this.text !== void 0) s += escapeXml(this.text);
|
|
339
|
+
for (const child of this.children) s += child.toXml(false);
|
|
340
|
+
s += `</${this.tag}>`;
|
|
341
|
+
return s;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function parseXml(xml) {
|
|
345
|
+
let str = (typeof xml === "string" ? xml : xml.toString("utf-8")).trim();
|
|
346
|
+
if (!str) throw new ParseError("Empty XML document");
|
|
347
|
+
if (str.startsWith("<?xml")) {
|
|
348
|
+
const end = str.indexOf("?>");
|
|
349
|
+
if (end !== -1) str = str.slice(end + 2).trim();
|
|
350
|
+
}
|
|
351
|
+
let pos = 0;
|
|
352
|
+
const len = str.length;
|
|
353
|
+
let root = null;
|
|
354
|
+
const stack = [];
|
|
355
|
+
while (pos < len) {
|
|
356
|
+
const lt = str.indexOf("<", pos);
|
|
357
|
+
if (lt === -1) {
|
|
358
|
+
if (stack.length > 0) {
|
|
359
|
+
const text = unescapeXml(str.slice(pos).trim());
|
|
360
|
+
const top = stack.at(-1);
|
|
361
|
+
if (text && top) top.text = (top.text || "") + text;
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (lt > pos && stack.length > 0) {
|
|
366
|
+
const text = unescapeXml(str.slice(pos, lt).trim());
|
|
367
|
+
const top = stack.at(-1);
|
|
368
|
+
if (text && top) top.text = (top.text || "") + text;
|
|
369
|
+
}
|
|
370
|
+
pos = lt + 1;
|
|
371
|
+
if (str.startsWith("!--", pos)) {
|
|
372
|
+
const end = str.indexOf("-->", pos + 3);
|
|
373
|
+
pos = end === -1 ? len : end + 3;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (str.startsWith("![CDATA[", pos)) {
|
|
377
|
+
const end = str.indexOf("]]>", pos + 8);
|
|
378
|
+
const cdata = end === -1 ? str.slice(pos + 8) : str.slice(pos + 8, end);
|
|
379
|
+
const top = stack.at(-1);
|
|
380
|
+
if (top) top.text = (top.text || "") + cdata;
|
|
381
|
+
pos = end === -1 ? len : end + 3;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (str.startsWith("?", pos) || str.startsWith("!", pos)) {
|
|
385
|
+
const gt = str.indexOf(">", pos);
|
|
386
|
+
pos = gt === -1 ? len : gt + 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (str[pos] === "/") {
|
|
390
|
+
const gt = str.indexOf(">", pos);
|
|
391
|
+
if (gt === -1) throw new ParseError("Unclosed closing tag");
|
|
392
|
+
const closeTag = str.slice(pos + 1, gt).trim();
|
|
393
|
+
const open = stack.pop();
|
|
394
|
+
if (!open) throw new ParseError(`Unexpected closing tag: </${closeTag}>`);
|
|
395
|
+
if (localTagName(open.tag) !== localTagName(closeTag) && open.tag !== closeTag) throw new ParseError(`Mismatched closing tag: expected </${open.tag}>, got </${closeTag}>`);
|
|
396
|
+
pos = gt + 1;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const gt = str.indexOf(">", pos);
|
|
400
|
+
if (gt === -1) throw new ParseError("Unclosed opening tag");
|
|
401
|
+
let tagBody = str.slice(pos, gt).trim();
|
|
402
|
+
const selfClosing = tagBody.endsWith("/");
|
|
403
|
+
if (selfClosing) tagBody = tagBody.slice(0, -1).trim();
|
|
404
|
+
const spaceIdx = tagBody.search(/\s/u);
|
|
405
|
+
const tagName = spaceIdx === -1 ? tagBody : tagBody.slice(0, spaceIdx);
|
|
406
|
+
const attrs = {};
|
|
407
|
+
if (spaceIdx !== -1) {
|
|
408
|
+
const attrStr = tagBody.slice(spaceIdx).trim();
|
|
409
|
+
const attrRegex = /([a-zA-Z0-9_:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gu;
|
|
410
|
+
let m;
|
|
411
|
+
while ((m = attrRegex.exec(attrStr)) !== null) attrs[m[1]] = unescapeXml(m[2] ?? m[3] ?? m[4] ?? "");
|
|
412
|
+
}
|
|
413
|
+
const el = new XmlElement(tagName, attrs);
|
|
414
|
+
const top = stack.at(-1);
|
|
415
|
+
if (top) top.children.push(el);
|
|
416
|
+
else {
|
|
417
|
+
if (root) throw new ParseError("Multiple root elements found in XML document");
|
|
418
|
+
root = el;
|
|
419
|
+
}
|
|
420
|
+
if (!selfClosing) stack.push(el);
|
|
421
|
+
pos = gt + 1;
|
|
422
|
+
}
|
|
423
|
+
const lastUnclosed = stack.at(-1);
|
|
424
|
+
if (lastUnclosed) throw new ParseError(`Unclosed element: <${lastUnclosed.tag}>`);
|
|
425
|
+
if (!root) throw new ParseError("Empty XML document");
|
|
426
|
+
return root;
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region vendor/file-bucket/lib/multipart.ts
|
|
430
|
+
function uploadError(name, message, cause) {
|
|
431
|
+
return Object.assign(new Error(message, { cause }), { name });
|
|
432
|
+
}
|
|
433
|
+
function delay(ms, signal) {
|
|
434
|
+
signal.throwIfAborted();
|
|
435
|
+
return new Promise((resolve, reject) => {
|
|
436
|
+
const abort = () => {
|
|
437
|
+
clearTimeout(timer);
|
|
438
|
+
reject(signal.reason);
|
|
439
|
+
};
|
|
440
|
+
const timer = setTimeout(() => {
|
|
441
|
+
signal.removeEventListener("abort", abort);
|
|
442
|
+
resolve();
|
|
443
|
+
}, ms);
|
|
444
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async function responseError(response) {
|
|
448
|
+
let code = "UploadFailed";
|
|
449
|
+
try {
|
|
450
|
+
const error = parseXml(await response.text());
|
|
451
|
+
code = error.findText("Code") || code;
|
|
452
|
+
if (code === "AccessDenied" && /expired/iu.test(error.findText("Message") || "")) code = "UploadExpired";
|
|
453
|
+
} catch {}
|
|
454
|
+
if (code === "RequestExpired" || code === "ExpiredToken") code = "UploadExpired";
|
|
455
|
+
return uploadError(code, `S3 upload request failed (${response.status}, ${code}).`);
|
|
456
|
+
}
|
|
457
|
+
/** Hydrate the wire descriptor without Node.js or AWS SDK runtime dependencies. */
|
|
458
|
+
function createMultipartUploadClient(descriptor) {
|
|
459
|
+
const controller = new AbortController();
|
|
460
|
+
let state = "ready";
|
|
461
|
+
let active;
|
|
462
|
+
let cleaned = false;
|
|
463
|
+
let cleaning;
|
|
464
|
+
function checkExpiry() {
|
|
465
|
+
if (Date.now() >= descriptor.expiresAt) throw uploadError("UploadExpired", "The multipart upload URLs have expired. Create a new handle.");
|
|
466
|
+
}
|
|
467
|
+
async function sendPart(url, body, signal) {
|
|
468
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
469
|
+
signal.throwIfAborted();
|
|
470
|
+
checkExpiry();
|
|
471
|
+
let response;
|
|
472
|
+
try {
|
|
473
|
+
response = await fetch(url, {
|
|
474
|
+
method: "PUT",
|
|
475
|
+
body,
|
|
476
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(12e4)])
|
|
477
|
+
});
|
|
478
|
+
} catch (error) {
|
|
479
|
+
signal.throwIfAborted();
|
|
480
|
+
if (attempt === 2) throw error;
|
|
481
|
+
await delay(200 * 2 ** attempt, signal);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (response.ok) {
|
|
485
|
+
const etag = response.headers.get("etag");
|
|
486
|
+
await response.arrayBuffer();
|
|
487
|
+
if (!etag) throw uploadError("MissingETag", "S3 did not expose the part ETag. Check the bucket CORS configuration.");
|
|
488
|
+
return etag;
|
|
489
|
+
}
|
|
490
|
+
const error = await responseError(response);
|
|
491
|
+
if (attempt === 2 || response.status !== 429 && response.status < 500) throw error;
|
|
492
|
+
await delay(200 * 2 ** attempt, signal);
|
|
493
|
+
}
|
|
494
|
+
throw new Error("Part upload retry limit reached.");
|
|
495
|
+
}
|
|
496
|
+
async function cleanUp() {
|
|
497
|
+
if (cleaned) return;
|
|
498
|
+
if (cleaning) return cleaning;
|
|
499
|
+
cleaning = (async () => {
|
|
500
|
+
const response = await fetch(descriptor.abortUrl, {
|
|
501
|
+
method: "DELETE",
|
|
502
|
+
signal: AbortSignal.timeout(15e3)
|
|
503
|
+
});
|
|
504
|
+
if (!response.ok && response.status !== 404) throw await responseError(response);
|
|
505
|
+
await response.arrayBuffer();
|
|
506
|
+
cleaned = true;
|
|
507
|
+
})();
|
|
508
|
+
try {
|
|
509
|
+
await cleaning;
|
|
510
|
+
} finally {
|
|
511
|
+
cleaning = void 0;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function execute(body, options) {
|
|
515
|
+
const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
516
|
+
try {
|
|
517
|
+
signal.throwIfAborted();
|
|
518
|
+
checkExpiry();
|
|
519
|
+
const file = body instanceof Blob ? body : new Blob([body instanceof Uint8Array ? new Uint8Array(body).buffer : body]);
|
|
520
|
+
if (file.size !== descriptor.size) throw new RangeError("File size does not match the multipart upload handle.");
|
|
521
|
+
let nextPart = 0;
|
|
522
|
+
let uploadedBytes = 0;
|
|
523
|
+
const etags = Array.from({ length: descriptor.partUrls.length }, () => "");
|
|
524
|
+
const worker = async () => {
|
|
525
|
+
try {
|
|
526
|
+
while (nextPart < descriptor.partUrls.length) {
|
|
527
|
+
signal.throwIfAborted();
|
|
528
|
+
const index = nextPart;
|
|
529
|
+
nextPart += 1;
|
|
530
|
+
const start = index * descriptor.partSize;
|
|
531
|
+
const part = file.slice(start, Math.min(file.size, start + descriptor.partSize));
|
|
532
|
+
etags[index] = await sendPart(descriptor.partUrls[index], part, signal);
|
|
533
|
+
uploadedBytes += part.size;
|
|
534
|
+
options.onProgress?.({
|
|
535
|
+
uploadedBytes,
|
|
536
|
+
totalBytes: descriptor.size
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
} catch (error) {
|
|
540
|
+
controller.abort(error);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const failure = (await Promise.allSettled(Array.from({ length: Math.min(4, etags.length) }, worker))).find((result) => result.status === "rejected");
|
|
545
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
546
|
+
signal.throwIfAborted();
|
|
547
|
+
checkExpiry();
|
|
548
|
+
const xml = `<CompleteMultipartUpload>${etags.map((etag, index) => `<Part><PartNumber>${index + 1}</PartNumber><ETag>${escapeXml(etag)}</ETag></Part>`).join("")}</CompleteMultipartUpload>`;
|
|
549
|
+
let response;
|
|
550
|
+
let result;
|
|
551
|
+
try {
|
|
552
|
+
response = await fetch(descriptor.completeUrl, {
|
|
553
|
+
method: "POST",
|
|
554
|
+
body: xml,
|
|
555
|
+
headers: { "Content-Type": "application/xml" },
|
|
556
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3e5)])
|
|
557
|
+
});
|
|
558
|
+
result = parseXml(await response.text());
|
|
559
|
+
} catch (error) {
|
|
560
|
+
state = "unknown";
|
|
561
|
+
throw uploadError("UploadCompletionUnknown", "The completion result is unknown. Check the object before retrying.", error);
|
|
562
|
+
}
|
|
563
|
+
if (result.localName() === "Error") throw uploadError(result.findText("Code") || "UploadFailed", result.findText("Message") || "S3 rejected multipart completion.");
|
|
564
|
+
if (!response.ok || result.localName() !== "CompleteMultipartUploadResult" || !result.findText("ETag")) {
|
|
565
|
+
state = "unknown";
|
|
566
|
+
throw uploadError("UploadCompletionUnknown", "S3 did not return a valid completion result. Check the object before retrying.");
|
|
567
|
+
}
|
|
568
|
+
state = "completed";
|
|
569
|
+
} catch (error) {
|
|
570
|
+
controller.abort(error);
|
|
571
|
+
if (state !== "unknown") {
|
|
572
|
+
state = signal.aborted ? "aborted" : "failed";
|
|
573
|
+
try {
|
|
574
|
+
await cleanUp();
|
|
575
|
+
} catch (cleanupError) {
|
|
576
|
+
throw uploadError(error instanceof Error ? error.name : "UploadFailed", "Upload failed and its session could not be cleaned up.", new AggregateError([error, cleanupError], "Upload and cleanup failed."));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const isCompleted = () => state === "completed";
|
|
583
|
+
return {
|
|
584
|
+
async upload(body, options = {}) {
|
|
585
|
+
if (state !== "ready") throw uploadError("InvalidUploadState", "This multipart upload handle has already been used.");
|
|
586
|
+
state = "uploading";
|
|
587
|
+
active = execute(body, options);
|
|
588
|
+
await active;
|
|
589
|
+
},
|
|
590
|
+
async abort() {
|
|
591
|
+
if (isCompleted()) return;
|
|
592
|
+
controller.abort(new DOMException("Upload aborted.", "AbortError"));
|
|
593
|
+
if (active) try {
|
|
594
|
+
await active;
|
|
595
|
+
} catch {}
|
|
596
|
+
if (isCompleted()) return;
|
|
597
|
+
await cleanUp();
|
|
598
|
+
state = "aborted";
|
|
599
|
+
},
|
|
600
|
+
toJSON: () => descriptor
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
var DEFAULT_PART_SIZE = 8388608;
|
|
604
|
+
var MAX_PARTS = 1e4;
|
|
605
|
+
var MAX_PART_SIZE = 5368709120;
|
|
606
|
+
function multipartPartSize(size) {
|
|
607
|
+
if (size === void 0) return DEFAULT_PART_SIZE;
|
|
608
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new RangeError("Upload size must be a nonnegative safe integer.");
|
|
609
|
+
const partSize = Math.max(DEFAULT_PART_SIZE, Math.ceil(size / MAX_PARTS));
|
|
610
|
+
if (partSize > MAX_PART_SIZE) throw new RangeError("Upload exceeds the multipart size limit.");
|
|
611
|
+
return partSize;
|
|
612
|
+
}
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region vendor/file-bucket/lib/file-bucket.ts
|
|
615
|
+
/** Shared file bucket definition for server and browser code. */
|
|
616
|
+
var FileBucket = class {
|
|
617
|
+
name;
|
|
618
|
+
constructor(name) {
|
|
619
|
+
this.name = name;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region vendor/file-bucket/index.server.ts
|
|
624
|
+
var clients = /* @__PURE__ */ new WeakMap();
|
|
625
|
+
function getClient(bucket) {
|
|
626
|
+
const client = clients.get(bucket) ?? new S3Client(process.env.NODE_ENV === "production" ? { requestChecksumCalculation: "WHEN_REQUIRED" } : {
|
|
627
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
628
|
+
endpoint: process.env.S3_EMULATOR_ENDPOINT || "http://127.0.0.1:4566",
|
|
629
|
+
region: "us-east-1",
|
|
630
|
+
forcePathStyle: true,
|
|
631
|
+
credentials: {
|
|
632
|
+
accessKeyId: "test",
|
|
633
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "test"
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
clients.set(bucket, client);
|
|
637
|
+
return client;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Delete an object through S3. Deleting a missing key succeeds.
|
|
641
|
+
*
|
|
642
|
+
* @param bucket - Shared definition whose ID is resolved to the configured physical bucket.
|
|
643
|
+
* @param path - Object key to delete.
|
|
644
|
+
* @returns A promise that resolves after S3 confirms deletion.
|
|
645
|
+
*/
|
|
646
|
+
async function deleteFile(bucket, path) {
|
|
647
|
+
const bucketName = resolveBucketName(bucket);
|
|
648
|
+
await getClient(bucket).send(new DeleteObjectCommand({
|
|
649
|
+
Bucket: bucketName,
|
|
650
|
+
Key: path
|
|
651
|
+
}));
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Delete objects in sequential batches of up to 1,000 keys.
|
|
655
|
+
*
|
|
656
|
+
* An empty list sends no S3 request. Deletion is not atomic: a rejected batch may
|
|
657
|
+
* already have deleted some objects, and later batches are not attempted.
|
|
658
|
+
*
|
|
659
|
+
* @param bucket - Shared definition whose ID is resolved to the configured physical bucket.
|
|
660
|
+
* @param paths - Object keys to delete.
|
|
661
|
+
* @returns A promise that resolves after all batches complete.
|
|
662
|
+
* @throws {AggregateError} If S3 reports per-object deletion errors.
|
|
663
|
+
*/
|
|
664
|
+
async function deleteBatch(bucket, paths) {
|
|
665
|
+
if (paths.length === 0) return;
|
|
666
|
+
const bucketName = resolveBucketName(bucket);
|
|
667
|
+
const client = getClient(bucket);
|
|
668
|
+
const CHUNK_SIZE = 1e3;
|
|
669
|
+
for (let i = 0; i < paths.length; i += CHUNK_SIZE) {
|
|
670
|
+
const chunk = paths.slice(i, i + CHUNK_SIZE);
|
|
671
|
+
const result = await client.send(new DeleteObjectsCommand({
|
|
672
|
+
Bucket: bucketName,
|
|
673
|
+
Delete: {
|
|
674
|
+
Objects: chunk.map((Key) => ({ Key })),
|
|
675
|
+
Quiet: true
|
|
676
|
+
}
|
|
677
|
+
}));
|
|
678
|
+
if (result.Errors?.length) throw new AggregateError(result.Errors.map((error) => /* @__PURE__ */ new Error(`Failed to delete ${error.Key}: ${error.Code} ${error.Message ?? ""}`)), "Some files could not be deleted.");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Sign a GET URL on the server without reading the object or checking that it exists.
|
|
683
|
+
*
|
|
684
|
+
* @param bucket - Shared definition whose ID is resolved to the configured physical bucket.
|
|
685
|
+
* @param path - Object key to access.
|
|
686
|
+
* @param options - URL lifetime in seconds (default 3600) and optional response content disposition.
|
|
687
|
+
* @returns A signed URL; requesting it performs the download.
|
|
688
|
+
*/
|
|
689
|
+
function getUrl(bucket, path, options) {
|
|
690
|
+
const bucketName = resolveBucketName(bucket);
|
|
691
|
+
return getSignedUrl(getClient(bucket), new GetObjectCommand({
|
|
692
|
+
Bucket: bucketName,
|
|
693
|
+
Key: path,
|
|
694
|
+
ResponseContentDisposition: options?.contentDisposition
|
|
695
|
+
}), { expiresIn: options?.expiresIn ?? 3600 });
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Sign a URL for one PUT without uploading bytes or creating a multipart session.
|
|
699
|
+
*
|
|
700
|
+
* Send the raw body with the content-type and cache-control headers used when
|
|
701
|
+
* signing. Metadata is carried in the signed query. Uploading replaces an existing key.
|
|
702
|
+
*
|
|
703
|
+
* @param bucket - Shared definition whose ID is resolved to the configured physical bucket.
|
|
704
|
+
* @param path - Destination object key.
|
|
705
|
+
* @param options - Object metadata, content headers, and URL lifetime in seconds (default 3600).
|
|
706
|
+
* @returns A signed URL for a single PUT.
|
|
707
|
+
*/
|
|
708
|
+
function putUrl(bucket, path, options) {
|
|
709
|
+
const bucketName = resolveBucketName(bucket);
|
|
710
|
+
return getSignedUrl(getClient(bucket), new PutObjectCommand({
|
|
711
|
+
Bucket: bucketName,
|
|
712
|
+
Key: path,
|
|
713
|
+
ContentType: options?.contentType,
|
|
714
|
+
Metadata: options?.metadata,
|
|
715
|
+
CacheControl: options?.cacheControl
|
|
716
|
+
}), { expiresIn: options?.expiresIn ?? 3600 });
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Create an S3 multipart session and sign its part, completion, and abort URLs.
|
|
720
|
+
*
|
|
721
|
+
* The returned handle uploads once, supports cancellation and progress, and can
|
|
722
|
+
* abort an unused session. Serialize with `toJSON()` and hydrate in the browser
|
|
723
|
+
* with `createMultipartUploadClient`. Creation sends no file bytes.
|
|
724
|
+
*
|
|
725
|
+
* @param bucket - Shared definition whose ID is resolved to the configured physical bucket.
|
|
726
|
+
* @param path - Destination object key.
|
|
727
|
+
* @param options - Expected byte size, object metadata, content headers, and URL lifetime (default 3600 seconds).
|
|
728
|
+
* @returns A multipart handle tied to the newly created session.
|
|
729
|
+
* @throws {RangeError} For an unsupported size or URL lifetime.
|
|
730
|
+
*/
|
|
731
|
+
async function createMultipartUploadHandle(bucket, path, options) {
|
|
732
|
+
const bucketName = resolveBucketName(bucket);
|
|
733
|
+
const partSize = multipartPartSize(options.size);
|
|
734
|
+
const expiresIn = options.expiresIn ?? 3600;
|
|
735
|
+
if (!Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > 604800) throw new RangeError("URL lifetime must be between 1 and 604800 seconds.");
|
|
736
|
+
const client = getClient(bucket);
|
|
737
|
+
const target = {
|
|
738
|
+
Bucket: bucketName,
|
|
739
|
+
Key: path
|
|
740
|
+
};
|
|
741
|
+
const { UploadId } = await client.send(new CreateMultipartUploadCommand({
|
|
742
|
+
...target,
|
|
743
|
+
ContentType: options.contentType,
|
|
744
|
+
Metadata: options.metadata,
|
|
745
|
+
CacheControl: options.cacheControl
|
|
746
|
+
}));
|
|
747
|
+
if (!UploadId) throw new Error("S3 did not return an upload ID.");
|
|
748
|
+
const session = {
|
|
749
|
+
...target,
|
|
750
|
+
UploadId
|
|
751
|
+
};
|
|
752
|
+
const expiresAt = Math.floor(Date.now() / 1e3) * 1e3 + expiresIn * 1e3;
|
|
753
|
+
try {
|
|
754
|
+
const partUrls = [];
|
|
755
|
+
const count = Math.max(1, Math.ceil(options.size / partSize));
|
|
756
|
+
for (let partNumber = 1; partNumber <= count; partNumber += 1) partUrls.push(await getSignedUrl(client, new UploadPartCommand({
|
|
757
|
+
...session,
|
|
758
|
+
PartNumber: partNumber
|
|
759
|
+
}), { expiresIn }));
|
|
760
|
+
const completeUrl = await getSignedUrl(client, new CompleteMultipartUploadCommand({
|
|
761
|
+
...session,
|
|
762
|
+
MultipartUpload: { Parts: [] }
|
|
763
|
+
}), {
|
|
764
|
+
expiresIn,
|
|
765
|
+
unsignableHeaders: /* @__PURE__ */ new Set(["content-length"])
|
|
766
|
+
});
|
|
767
|
+
const abortUrl = await getSignedUrl(client, new AbortMultipartUploadCommand(session), { expiresIn });
|
|
768
|
+
return createMultipartUploadClient({
|
|
769
|
+
__blocks: "file-bucket/multipart-upload",
|
|
770
|
+
uploadId: UploadId,
|
|
771
|
+
size: options.size,
|
|
772
|
+
partSize,
|
|
773
|
+
expiresAt,
|
|
774
|
+
partUrls,
|
|
775
|
+
completeUrl,
|
|
776
|
+
abortUrl
|
|
777
|
+
});
|
|
778
|
+
} catch (error) {
|
|
779
|
+
try {
|
|
780
|
+
await client.send(new AbortMultipartUploadCommand(session));
|
|
781
|
+
} catch (cleanupError) {
|
|
782
|
+
throw new AggregateError([error, cleanupError], "Failed to create and clean up multipart upload.", { cause: cleanupError });
|
|
783
|
+
}
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function resolveBucketName(bucket) {
|
|
788
|
+
const hash = createHash("sha256").update(bucket.name).digest("hex").slice(0, 8);
|
|
789
|
+
return process.env[`FILE_BUCKET_${hash}`] || bucket.name;
|
|
790
|
+
}
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region vendor/file-bucket/lib/protocol.ts
|
|
793
|
+
var MAX_KEY_BYTES = 1024;
|
|
794
|
+
var MAX_METADATA_ENTRIES = 50;
|
|
795
|
+
var MAX_METADATA_KEY_LENGTH = 128;
|
|
796
|
+
var MAX_METADATA_VALUE_LENGTH = 2048;
|
|
797
|
+
function isRecord(value) {
|
|
798
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
799
|
+
}
|
|
800
|
+
function textByteLength(value) {
|
|
801
|
+
return new TextEncoder().encode(value).length;
|
|
802
|
+
}
|
|
803
|
+
function isKey(value) {
|
|
804
|
+
return typeof value === "string" && value.length > 0 && textByteLength(value) <= MAX_KEY_BYTES;
|
|
805
|
+
}
|
|
806
|
+
function isOptionalText(value, maxLen = 1024) {
|
|
807
|
+
return value === void 0 || typeof value === "string" && textByteLength(value) <= maxLen;
|
|
808
|
+
}
|
|
809
|
+
function isStringRecord(value) {
|
|
810
|
+
if (!isRecord(value)) return false;
|
|
811
|
+
const entries = Object.entries(value);
|
|
812
|
+
if (entries.length > MAX_METADATA_ENTRIES) return false;
|
|
813
|
+
return entries.every(([k, v]) => typeof v === "string" && k.length <= MAX_METADATA_KEY_LENGTH && v.length <= MAX_METADATA_VALUE_LENGTH);
|
|
814
|
+
}
|
|
815
|
+
function isSize(value) {
|
|
816
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
817
|
+
}
|
|
818
|
+
function isMetadata(input) {
|
|
819
|
+
return isOptionalText(input.contentType, 512) && isOptionalText(input.cacheControl, 512) && (input.metadata === void 0 || isStringRecord(input.metadata));
|
|
820
|
+
}
|
|
821
|
+
function isSigningOptions(input) {
|
|
822
|
+
return isMetadata(input) && isOptionalText(input.contentDisposition, 512) && (input.expiresIn === void 0 || isSize(input.expiresIn) && input.expiresIn >= 1 && input.expiresIn <= 604800);
|
|
823
|
+
}
|
|
824
|
+
function deepFreeze(object) {
|
|
825
|
+
if (object === null || typeof object !== "object") return object;
|
|
826
|
+
Object.freeze(object);
|
|
827
|
+
for (const key of Object.keys(object)) {
|
|
828
|
+
const val = object[key];
|
|
829
|
+
if (val !== null && typeof val === "object" && !Object.isFrozen(val)) deepFreeze(val);
|
|
830
|
+
}
|
|
831
|
+
return object;
|
|
832
|
+
}
|
|
833
|
+
function isRequest(input, maxBatchKeys = 1e3) {
|
|
834
|
+
if (!isKey(input.bucketId)) return false;
|
|
835
|
+
if (input.operation === "deleteBatch") return Array.isArray(input.paths) && input.paths.length > 0 && input.paths.length <= maxBatchKeys && input.paths.every(isKey);
|
|
836
|
+
if (!isKey(input.path)) return false;
|
|
837
|
+
switch (input.operation) {
|
|
838
|
+
case "get":
|
|
839
|
+
case "delete": return true;
|
|
840
|
+
case "getUrl":
|
|
841
|
+
case "putUrl": return isSigningOptions(input);
|
|
842
|
+
case "put": return isSize(input.size) && isMetadata(input);
|
|
843
|
+
default: return false;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function parseFileBucketRequest(value, options) {
|
|
847
|
+
if (!isRecord(value)) throw new Error("Invalid file request");
|
|
848
|
+
const input = {
|
|
849
|
+
...value,
|
|
850
|
+
operation: value.operation === void 0 ? "put" : value.operation
|
|
851
|
+
};
|
|
852
|
+
if (!isRequest(input, options?.maxBatchKeys)) throw new Error("Invalid file request");
|
|
853
|
+
return deepFreeze(input);
|
|
854
|
+
}
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region vendor/file-bucket/react-router.server.ts
|
|
857
|
+
var MULTIPART_THRESHOLD = 8388608;
|
|
858
|
+
var MAX_REQUEST_BYTES = 65536;
|
|
859
|
+
var MAX_BATCH_KEYS = 1e3;
|
|
860
|
+
async function readLimitedJson(request, maxBytes) {
|
|
861
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) throw new RangeError("maxBytes must be a positive safe integer.");
|
|
862
|
+
const reader = request.body?.getReader();
|
|
863
|
+
if (!reader) throw new Response("Invalid JSON", { status: 400 });
|
|
864
|
+
const chunks = [];
|
|
865
|
+
let length = 0;
|
|
866
|
+
try {
|
|
867
|
+
while (true) {
|
|
868
|
+
const { done, value } = await reader.read();
|
|
869
|
+
if (done) break;
|
|
870
|
+
length += value.byteLength;
|
|
871
|
+
if (length > maxBytes) {
|
|
872
|
+
try {
|
|
873
|
+
await reader.cancel();
|
|
874
|
+
} catch {}
|
|
875
|
+
throw new Response("Request too large", { status: 413 });
|
|
876
|
+
}
|
|
877
|
+
chunks.push(value);
|
|
878
|
+
}
|
|
879
|
+
} finally {
|
|
880
|
+
reader.releaseLock();
|
|
881
|
+
}
|
|
882
|
+
const bytes = new Uint8Array(length);
|
|
883
|
+
let offset = 0;
|
|
884
|
+
for (const chunk of chunks) {
|
|
885
|
+
bytes.set(chunk, offset);
|
|
886
|
+
offset += chunk.byteLength;
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
890
|
+
} catch {
|
|
891
|
+
throw new Response("Invalid JSON", { status: 400 });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function uploadGrant(bucket, input) {
|
|
895
|
+
const options = {
|
|
896
|
+
contentType: input.contentType,
|
|
897
|
+
cacheControl: input.cacheControl,
|
|
898
|
+
metadata: input.metadata
|
|
899
|
+
};
|
|
900
|
+
if (input.size >= MULTIPART_THRESHOLD) return {
|
|
901
|
+
type: "multipart",
|
|
902
|
+
descriptor: (await createMultipartUploadHandle(bucket, input.path, {
|
|
903
|
+
...options,
|
|
904
|
+
size: input.size
|
|
905
|
+
})).toJSON()
|
|
906
|
+
};
|
|
907
|
+
const url = await putUrl(bucket, input.path, options);
|
|
908
|
+
const headers = {};
|
|
909
|
+
if (options.contentType) headers["Content-Type"] = options.contentType;
|
|
910
|
+
if (options.cacheControl) headers["Cache-Control"] = options.cacheControl;
|
|
911
|
+
return {
|
|
912
|
+
type: "single",
|
|
913
|
+
url,
|
|
914
|
+
headers
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
async function dispatch(bucket, input) {
|
|
918
|
+
switch (input.operation) {
|
|
919
|
+
case "delete":
|
|
920
|
+
await deleteFile(bucket, input.path);
|
|
921
|
+
return;
|
|
922
|
+
case "deleteBatch":
|
|
923
|
+
await deleteBatch(bucket, input.paths);
|
|
924
|
+
return;
|
|
925
|
+
case "get": return { url: await getUrl(bucket, input.path, { contentDisposition: "attachment" }) };
|
|
926
|
+
case "getUrl": return { url: await getUrl(bucket, input.path, {
|
|
927
|
+
expiresIn: input.expiresIn,
|
|
928
|
+
contentDisposition: input.contentDisposition
|
|
929
|
+
}) };
|
|
930
|
+
case "putUrl": return { url: await putUrl(bucket, input.path, {
|
|
931
|
+
expiresIn: input.expiresIn,
|
|
932
|
+
contentType: input.contentType,
|
|
933
|
+
metadata: input.metadata,
|
|
934
|
+
cacheControl: input.cacheControl
|
|
935
|
+
}) };
|
|
936
|
+
case "put": return uploadGrant(bucket, input);
|
|
937
|
+
default: throw new Error("Unsupported file operation");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region app/routes/files.ts
|
|
942
|
+
var files_exports = /* @__PURE__ */ __exportAll({ action: () => action });
|
|
943
|
+
var action = async ({ request }) => {
|
|
944
|
+
if (request.method !== "POST") return new Response("Method not allowed", {
|
|
945
|
+
status: 405,
|
|
946
|
+
headers: { Allow: "POST" }
|
|
947
|
+
});
|
|
948
|
+
if (request.headers.get("content-type")?.toLowerCase().split(";")[0]?.trim() !== "application/json") return new Response("Unsupported Media Type", { status: 415 });
|
|
949
|
+
const contentLengthHeader = request.headers.get("content-length");
|
|
950
|
+
if (contentLengthHeader !== null) {
|
|
951
|
+
const declaredLength = Number(contentLengthHeader);
|
|
952
|
+
if (!Number.isNaN(declaredLength) && declaredLength > 65536) return new Response("Request too large", { status: 413 });
|
|
953
|
+
}
|
|
954
|
+
let value;
|
|
955
|
+
try {
|
|
956
|
+
value = await readLimitedJson(request, MAX_REQUEST_BYTES);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
if (error instanceof Response) return error;
|
|
959
|
+
return new Response("Invalid JSON", { status: 400 });
|
|
960
|
+
}
|
|
961
|
+
let input;
|
|
962
|
+
try {
|
|
963
|
+
input = parseFileBucketRequest(value, { maxBatchKeys: MAX_BATCH_KEYS });
|
|
964
|
+
} catch {
|
|
965
|
+
return new Response("Invalid file request", { status: 400 });
|
|
966
|
+
}
|
|
967
|
+
const result = await dispatch(new FileBucket(input.bucketId), input);
|
|
968
|
+
if (result === void 0) return new Response(null, {
|
|
969
|
+
status: 204,
|
|
970
|
+
headers: { "Cache-Control": "no-store" }
|
|
971
|
+
});
|
|
972
|
+
return Response.json(result, { headers: { "Cache-Control": "private, no-store" } });
|
|
973
|
+
};
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region \0virtual:react-router/server-manifest
|
|
976
|
+
var server_manifest_default = {
|
|
977
|
+
"entry": {
|
|
978
|
+
"module": "/assets/entry.client-Bc58m2-m.js",
|
|
979
|
+
"imports": ["/assets/jsx-runtime-eLCqVKHJ.js", "/assets/errorBoundaries-CQxPIuVU.js"],
|
|
980
|
+
"css": []
|
|
981
|
+
},
|
|
982
|
+
"routes": {
|
|
983
|
+
"root": {
|
|
984
|
+
"id": "root",
|
|
985
|
+
"parentId": void 0,
|
|
986
|
+
"path": "",
|
|
987
|
+
"index": void 0,
|
|
988
|
+
"caseSensitive": void 0,
|
|
989
|
+
"hasAction": false,
|
|
990
|
+
"hasLoader": false,
|
|
991
|
+
"hasClientAction": false,
|
|
992
|
+
"hasClientLoader": false,
|
|
993
|
+
"hasClientMiddleware": false,
|
|
994
|
+
"hasDefaultExport": true,
|
|
995
|
+
"hasErrorBoundary": true,
|
|
996
|
+
"module": "/assets/root-C1tmjS9o.js",
|
|
997
|
+
"imports": ["/assets/jsx-runtime-eLCqVKHJ.js", "/assets/errorBoundaries-CQxPIuVU.js"],
|
|
998
|
+
"css": ["/assets/root-BeNS6njP.css"],
|
|
999
|
+
"clientActionModule": void 0,
|
|
1000
|
+
"clientLoaderModule": void 0,
|
|
1001
|
+
"clientMiddlewareModule": void 0,
|
|
1002
|
+
"hydrateFallbackModule": void 0
|
|
1003
|
+
},
|
|
1004
|
+
"routes/home": {
|
|
1005
|
+
"id": "routes/home",
|
|
1006
|
+
"parentId": "root",
|
|
1007
|
+
"path": void 0,
|
|
1008
|
+
"index": true,
|
|
1009
|
+
"caseSensitive": void 0,
|
|
1010
|
+
"hasAction": false,
|
|
1011
|
+
"hasLoader": false,
|
|
1012
|
+
"hasClientAction": false,
|
|
1013
|
+
"hasClientLoader": false,
|
|
1014
|
+
"hasClientMiddleware": false,
|
|
1015
|
+
"hasDefaultExport": true,
|
|
1016
|
+
"hasErrorBoundary": false,
|
|
1017
|
+
"module": "/assets/home-CH2INc5e.js",
|
|
1018
|
+
"imports": ["/assets/jsx-runtime-eLCqVKHJ.js"],
|
|
1019
|
+
"css": [],
|
|
1020
|
+
"clientActionModule": void 0,
|
|
1021
|
+
"clientLoaderModule": void 0,
|
|
1022
|
+
"clientMiddlewareModule": void 0,
|
|
1023
|
+
"hydrateFallbackModule": void 0
|
|
1024
|
+
},
|
|
1025
|
+
"routes/files": {
|
|
1026
|
+
"id": "routes/files",
|
|
1027
|
+
"parentId": "root",
|
|
1028
|
+
"path": "resources/files",
|
|
1029
|
+
"index": void 0,
|
|
1030
|
+
"caseSensitive": void 0,
|
|
1031
|
+
"hasAction": true,
|
|
1032
|
+
"hasLoader": false,
|
|
1033
|
+
"hasClientAction": false,
|
|
1034
|
+
"hasClientLoader": false,
|
|
1035
|
+
"hasClientMiddleware": false,
|
|
1036
|
+
"hasDefaultExport": false,
|
|
1037
|
+
"hasErrorBoundary": false,
|
|
1038
|
+
"module": "/assets/files-BvRk9kiK.js",
|
|
1039
|
+
"imports": [],
|
|
1040
|
+
"css": [],
|
|
1041
|
+
"clientActionModule": void 0,
|
|
1042
|
+
"clientLoaderModule": void 0,
|
|
1043
|
+
"clientMiddlewareModule": void 0,
|
|
1044
|
+
"hydrateFallbackModule": void 0
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
"url": "/assets/manifest-2d65dd0f.js",
|
|
1048
|
+
"version": "2d65dd0f",
|
|
1049
|
+
"sri": void 0
|
|
1050
|
+
};
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region \0virtual:react-router/server-build
|
|
1053
|
+
var assetsBuildDirectory = "build/client";
|
|
1054
|
+
var basename = "/";
|
|
1055
|
+
var future = {
|
|
1056
|
+
"unstable_enableNodeReadableStream": false,
|
|
1057
|
+
"unstable_optimizeDeps": false
|
|
1058
|
+
};
|
|
1059
|
+
var ssr = true;
|
|
1060
|
+
var isSpaMode = false;
|
|
1061
|
+
var prerender = [];
|
|
1062
|
+
var routeDiscovery = {
|
|
1063
|
+
"mode": "lazy",
|
|
1064
|
+
"manifestPath": "/__manifest"
|
|
1065
|
+
};
|
|
1066
|
+
var publicPath = "/";
|
|
1067
|
+
var entry = { module: entry_server_node_exports };
|
|
1068
|
+
var routes = {
|
|
1069
|
+
"root": {
|
|
1070
|
+
id: "root",
|
|
1071
|
+
parentId: void 0,
|
|
1072
|
+
path: "",
|
|
1073
|
+
index: void 0,
|
|
1074
|
+
caseSensitive: void 0,
|
|
1075
|
+
module: root_exports
|
|
1076
|
+
},
|
|
1077
|
+
"routes/home": {
|
|
1078
|
+
id: "routes/home",
|
|
1079
|
+
parentId: "root",
|
|
1080
|
+
path: void 0,
|
|
1081
|
+
index: true,
|
|
1082
|
+
caseSensitive: void 0,
|
|
1083
|
+
module: home_exports
|
|
1084
|
+
},
|
|
1085
|
+
"routes/files": {
|
|
1086
|
+
id: "routes/files",
|
|
1087
|
+
parentId: "root",
|
|
1088
|
+
path: "resources/files",
|
|
1089
|
+
index: void 0,
|
|
1090
|
+
caseSensitive: void 0,
|
|
1091
|
+
module: files_exports
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
var allowedActionOrigins = false;
|
|
1095
|
+
//#endregion
|
|
1096
|
+
export { allowedActionOrigins, server_manifest_default as assets, assetsBuildDirectory, basename, entry, future, isSpaMode, prerender, publicPath, routeDiscovery, routes, ssr };
|