@orochibraru/svelte-smol 1.8.1 → 1.8.2

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.
@@ -0,0 +1,181 @@
1
+ import { fileURLToPath } from "node:url";
2
+ // `node:url`, not `Bun.fileURLToPath`: this module is loaded at config-parse
3
+ // time by Node-based tooling too (svelte-check, the Svelte language server,
4
+ // `svelte-kit sync`), where `Bun` is undefined. Everything that actually needs
5
+ // the Bun runtime lives inside `adapt()`.
6
+ const templates = fileURLToPath(new URL("./templates", import.meta.url));
7
+ /**
8
+ * SvelteKit adapter that compiles the app into a single standalone executable
9
+ * with `bun build --compile`.
10
+ *
11
+ * `adapt()` writes the SvelteKit server and this package's entrypoint templates
12
+ * into a temp directory, then compiles them — `@sveltejs/kit` and every other
13
+ * pure-JS dependency bundled in — into one binary. The build must run under the
14
+ * Bun runtime (`bun --bun vite build`, or a `bunfig.toml` with `[run] bun =
15
+ * true`); loading the config alone works under Node too.
16
+ *
17
+ * With {@link AdapterOptions.compile | `compile: false`} it emits a plain
18
+ * `build/index.js` bundle instead (run with `bun`), the only way to ship a
19
+ * native (`.node`) addon.
20
+ *
21
+ * Output, all written to {@link AdapterOptions.out | `out`}:
22
+ *
23
+ * ```text
24
+ * build/
25
+ * ├── server the executable (rename with `name`)
26
+ * ├── client/ static assets, served by the executable
27
+ * └── prerendered/ prerendered pages, served by the executable
28
+ * ```
29
+ *
30
+ * The executable resolves `client/` and `prerendered/` from its own location
31
+ * (`dirname(process.execPath)`, overridable with the `ASSETS_DIR` env var), so
32
+ * it can run from any working directory. Deploy the whole `out` directory, or
33
+ * just the binary when {@link AdapterOptions.serveAssets | `serveAssets`} is
34
+ * off and a proxy/CDN serves the assets.
35
+ *
36
+ * Runtime configuration (`HOST`, `PORT`, `ORIGIN`, `BODY_SIZE_LIMIT`, …) is
37
+ * read from environment variables, optionally namespaced by
38
+ * {@link AdapterOptions.envPrefix | `envPrefix`}.
39
+ *
40
+ * @param options - see {@link AdapterOptions}
41
+ * @returns the configured SvelteKit {@link Adapter}
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // svelte.config.js
46
+ * import adapter from "@orochibraru/svelte-smol";
47
+ *
48
+ * export default {
49
+ * kit: {
50
+ * adapter: adapter({
51
+ * // cross-compile for an Alpine container from any host
52
+ * target: "bun-linux-x64-musl",
53
+ * bytecode: true,
54
+ * }),
55
+ * },
56
+ * };
57
+ * ```
58
+ *
59
+ * @see {@link https://bun.com/docs/bundler/executables | Bun — Single-file executables}
60
+ */
61
+ export default function adapter(options = {}) {
62
+ const { out = "build", name = "server", compile = true, target, bytecode = false, minify = false, sourcemap = false, precompress = false, healthcheck = true, envPrefix = "", serveAssets = true, serveOptions = {}, } = options;
63
+ const healthcheckConfig = healthcheck === false
64
+ ? false
65
+ : {
66
+ path: (healthcheck === true ? undefined : healthcheck.path) ?? "/_health",
67
+ };
68
+ return {
69
+ name: "@orochibraru/svelte-smol",
70
+ supports: {
71
+ instrumentation: () => true,
72
+ read: () => true,
73
+ },
74
+ async adapt(builder) {
75
+ const { base } = builder.config.kit.paths;
76
+ const tmp = builder.getBuildDirectory("adapter-bun");
77
+ builder.rimraf(out);
78
+ builder.rimraf(tmp);
79
+ builder.mkdirp(`${out}/`);
80
+ builder.mkdirp(tmp);
81
+ builder.log.minor("Copying assets");
82
+ builder.writeClient(`${out}/client${base}`);
83
+ builder.writePrerendered(`${out}/prerendered${base}`);
84
+ if (precompress) {
85
+ builder.log.minor("Compressing assets");
86
+ await Promise.all([
87
+ builder.compress(`${out}/client`),
88
+ builder.compress(`${out}/prerendered`),
89
+ ]);
90
+ }
91
+ builder.log.minor("Building server");
92
+ builder.writeServer(`${tmp}/server`);
93
+ await Bun.write(`${tmp}/server/manifest.js`, [
94
+ `export const manifest = ${builder.generateManifest({ relativePath: "./" })};`,
95
+ `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});`,
96
+ `export const base = ${JSON.stringify(base)};`,
97
+ ].join("\n\n"));
98
+ // Entry templates ship as raw `.ts` — `bun build --compile` runs them
99
+ // straight through. The virtual specifiers (`ENV`, `MANIFEST`,
100
+ // `SERVER`, `HANDLER`) and the `ENV_PREFIX` / `BUILD_OPTIONS` /
101
+ // `SERVE_OPTIONS` tokens are resolved by this raw word-boundary token
102
+ // swap over the copied source.
103
+ builder.log.minor("Copying entrypoint");
104
+ builder.copy(templates, tmp, {
105
+ replace: {
106
+ BUILD_OPTIONS: JSON.stringify({
107
+ serveAssets,
108
+ precompress,
109
+ healthcheck: healthcheckConfig,
110
+ compiled: compile,
111
+ }),
112
+ ENV: "./env.ts",
113
+ ENV_PREFIX: JSON.stringify(envPrefix),
114
+ HANDLER: "./handler.ts",
115
+ MANIFEST: "./server/manifest.js",
116
+ SERVE_OPTIONS: JSON.stringify(serveOptions),
117
+ SERVER: "./server/index.js",
118
+ },
119
+ });
120
+ const entry = `${tmp}/index.ts`;
121
+ if (builder.hasServerInstrumentationFile?.()) {
122
+ // Instrumentation (OpenTelemetry &c.) has to load before anything
123
+ // else in the bundle. A compiled binary has no post-build
124
+ // entrypoint to rewrite, so prepend the import to the compile
125
+ // entrypoint instead.
126
+ await Bun.write(entry, `import "./server/instrumentation.server.js";\n${await Bun.file(entry).text()}`);
127
+ }
128
+ const check = (result, label) => {
129
+ if (!result.success) {
130
+ for (const message of result.logs) {
131
+ builder.log.error(String(message));
132
+ }
133
+ throw new Error(`\`bun build\` failed for ${label}`);
134
+ }
135
+ };
136
+ const compileBinary = async (entrypoint, outName) => {
137
+ builder.log.minor(target ? `Compiling ${outName} (${target})` : `Compiling ${outName}`);
138
+ check(await Bun.build({
139
+ entrypoints: [entrypoint],
140
+ target: "bun",
141
+ minify,
142
+ bytecode,
143
+ sourcemap: sourcemap ? "linked" : "none",
144
+ compile: {
145
+ outfile: `${out}/${outName}`,
146
+ ...(target ? { target } : {}),
147
+ },
148
+ }), outName);
149
+ builder.log.success(`Compiled ${out}/${outName}`);
150
+ };
151
+ const bundleServer = async (entrypoint) => {
152
+ builder.log.minor("Bundling index.js");
153
+ // Same bundle as the compiled binary, just emitted as a file. Every
154
+ // pure-JS dependency is inlined; a native (`.node`) addon can't be,
155
+ // so its `require` stays in the output and resolves from
156
+ // `node_modules` at runtime — which is exactly what a plain `bun`
157
+ // process (unlike a compiled binary) can do. Ship `node_modules`
158
+ // for those; anything fully bundled needn't be installed.
159
+ check(await Bun.build({
160
+ entrypoints: [entrypoint],
161
+ target: "bun",
162
+ format: "esm",
163
+ minify,
164
+ sourcemap: sourcemap ? "linked" : "none",
165
+ outdir: out,
166
+ naming: "index.js",
167
+ }), "index.js");
168
+ builder.log.success(`Bundled ${out}/index.js`);
169
+ };
170
+ if (compile) {
171
+ await compileBinary(entry, name);
172
+ }
173
+ else {
174
+ await bundleServer(entry);
175
+ }
176
+ if (healthcheckConfig) {
177
+ await compileBinary(`${tmp}/healthcheck.ts`, "healthcheck");
178
+ }
179
+ },
180
+ };
181
+ }
@@ -0,0 +1,80 @@
1
+ import type { Adapter } from "@sveltejs/kit";
2
+ import type { BuildConfig, Serve, Server } from "bun";
3
+ declare global {
4
+ namespace App {
5
+ interface Platform {
6
+ /** The Bun HTTP server handling the request. */
7
+ server: Server<undefined>;
8
+ }
9
+ }
10
+ }
11
+ export interface AdapterOptions {
12
+ /**
13
+ * The directory to build the server to.
14
+ * @default "build"
15
+ */
16
+ out?: string;
17
+ /**
18
+ * Generate `.br` and `.gz` variants of client and prerendered assets during
19
+ * the build, negotiated per request (brotli preferred). Ignored when
20
+ * compiling, because embedded assets are imported by identity path.
21
+ * @default false
22
+ */
23
+ precompress?: boolean;
24
+ /**
25
+ * Prefix for this adapter's runtime env vars (`MY_APP_` → `MY_APP_PORT`).
26
+ * @default ""
27
+ */
28
+ envPrefix?: string;
29
+ /**
30
+ * Default options passed to `Bun.serve`. Environment variables take
31
+ * precedence. Must be JSON-serializable.
32
+ * @default {}
33
+ */
34
+ serverOptions?: Pick<Serve.Options<never>, "development" | "hostname" | "port" | "idleTimeout" | "maxRequestBodySize" | "reusePort" | "unix" | "ipv6Only">;
35
+ /**
36
+ * Bun build options. The entrypoint, output directory, top-level target and
37
+ * module format are reserved. `compile` defaults to `true`: the server is a
38
+ * single executable at `<out>/server` with client and prerendered assets
39
+ * embedded. Pass a target string (`"bun-linux-x64-musl"`) to cross-compile,
40
+ * or `false` to emit a plain `<out>/index.js` bundle run with `bun` — the
41
+ * only way to ship native (`.node`) addons.
42
+ * @default { compile: true }
43
+ */
44
+ buildOptions?: Pick<BuildConfig, "sourcemap" | "minify" | "bytecode" | "banner" | "footer" | "drop" | "features" | "optimizeImports" | "splitting" | "compile">;
45
+ /**
46
+ * Compile a tiny `healthcheck` executable next to the server and expose a
47
+ * matching `GET` endpoint. The binary exits `0` when the server answers,
48
+ * `1` otherwise — ready for a Docker `HEALTHCHECK`. Pass an object to
49
+ * change the endpoint path.
50
+ * @default true
51
+ */
52
+ healthcheck?: boolean | {
53
+ path?: string;
54
+ };
55
+ }
56
+ /**
57
+ * SvelteKit adapter for Bun, based on `@sveltejs/adapter-bun`, that compiles
58
+ * the app into a single standalone executable by default and adds a
59
+ * `healthcheck` binary.
60
+ *
61
+ * The build must run under the Bun runtime (`bun --bun vite build`, or a
62
+ * `bunfig.toml` with `[run] bun = true`); loading the config works under Node.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * // vite.config.js
67
+ * import { sveltekit } from "@sveltejs/kit/vite";
68
+ * import { defineConfig } from "vite";
69
+ * import adapter from "@orochibraru/svelte-smol";
70
+ *
71
+ * export default defineConfig({
72
+ * plugins: [
73
+ * sveltekit({
74
+ * adapter: adapter({ buildOptions: { compile: "bun-linux-x64-musl" } }),
75
+ * }),
76
+ * ],
77
+ * });
78
+ * ```
79
+ */
80
+ export default function adapter(options?: AdapterOptions): Adapter;
@@ -0,0 +1,353 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ // `node:url`, not `import.meta.dir`: this module is loaded at config-parse time
5
+ // by Node-based tooling too (svelte-check, `svelte-kit sync`).
6
+ const templates = fileURLToPath(new URL("./templates", import.meta.url));
7
+ const NOT_BUN = "svelte-smol requires running the SvelteKit build with Bun. Use `bun run --bun build`.";
8
+ function read_files_recursive(dir) {
9
+ if (!fs.existsSync(dir))
10
+ return [];
11
+ return fs
12
+ .readdirSync(dir, { recursive: true, withFileTypes: true })
13
+ .filter((entry) => entry.isFile())
14
+ .map((entry) => {
15
+ const abs = path.resolve(entry.parentPath, entry.name);
16
+ return { abs, rel: posixify(path.relative(dir, abs)) };
17
+ })
18
+ .filter(({ rel }) => rel.split("/").every((segment) => segment !== ".vite"));
19
+ }
20
+ /** Dotfiles are not served, except the `.well-known` directory (like sirv). */
21
+ function is_dotfile(file) {
22
+ return file
23
+ .split("/")
24
+ .some((segment, i) => segment.startsWith(".") && !(i === 0 && segment === ".well-known"));
25
+ }
26
+ // bounds open file handles while every asset hashes concurrently
27
+ const MAX_OPEN_FILES = 64;
28
+ let open_files = 0;
29
+ const file_waiters = [];
30
+ async function hash_file(file) {
31
+ if (open_files === MAX_OPEN_FILES) {
32
+ await new Promise((resolve) => file_waiters.push(resolve));
33
+ }
34
+ open_files++;
35
+ try {
36
+ const hasher = new Bun.CryptoHasher("blake2b256");
37
+ for await (const chunk of Bun.file(file).stream()) {
38
+ hasher.update(chunk);
39
+ }
40
+ return hasher.digest("hex").slice(0, 16);
41
+ }
42
+ finally {
43
+ open_files--;
44
+ file_waiters.shift()?.();
45
+ }
46
+ }
47
+ /** Bun only generates ETags for in-memory static routes, so ship our own. */
48
+ async function asset_meta(file, precompress = false) {
49
+ const meta = {
50
+ hash: await hash_file(file),
51
+ mtime: Bun.file(file).lastModified,
52
+ };
53
+ if (precompress) {
54
+ if (fs.existsSync(`${file}.br`))
55
+ meta.br = true;
56
+ if (fs.existsSync(`${file}.gz`))
57
+ meta.gz = true;
58
+ }
59
+ return meta;
60
+ }
61
+ function validate_file_paths(files) {
62
+ for (const file of files) {
63
+ if (file.includes("*")) {
64
+ throw new Error(`Cannot build with ${JSON.stringify(file)} because Bun treats literal \`*\` characters in route paths as wildcards. Rename the file or route to remove the \`*\` character.`);
65
+ }
66
+ // a leading ':' would need percent-encoding, but browsers request the colon raw
67
+ if (file.split("/").some((segment) => segment.startsWith(":"))) {
68
+ throw new Error(`Cannot build with ${JSON.stringify(file)} because Bun treats a route segment starting with \`:\` as a parameter. Rename the file or route so no segment starts with \`:\`.`);
69
+ }
70
+ }
71
+ }
72
+ /**
73
+ * SvelteKit adapter for Bun, based on `@sveltejs/adapter-bun`, that compiles
74
+ * the app into a single standalone executable by default and adds a
75
+ * `healthcheck` binary.
76
+ *
77
+ * The build must run under the Bun runtime (`bun --bun vite build`, or a
78
+ * `bunfig.toml` with `[run] bun = true`); loading the config works under Node.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * // vite.config.js
83
+ * import { sveltekit } from "@sveltejs/kit/vite";
84
+ * import { defineConfig } from "vite";
85
+ * import adapter from "@orochibraru/svelte-smol";
86
+ *
87
+ * export default defineConfig({
88
+ * plugins: [
89
+ * sveltekit({
90
+ * adapter: adapter({ buildOptions: { compile: "bun-linux-x64-musl" } }),
91
+ * }),
92
+ * ],
93
+ * });
94
+ * ```
95
+ */
96
+ export default function adapter(options = {}) {
97
+ const { out = "build", envPrefix = "", precompress = false, serverOptions = {}, healthcheck = true, } = options;
98
+ const buildOptions = { compile: true, ...options.buildOptions };
99
+ const embed = !!buildOptions.compile;
100
+ const healthcheck_path = healthcheck === false
101
+ ? false
102
+ : ((healthcheck === true ? undefined : healthcheck.path) ?? "/_health");
103
+ return {
104
+ name: "@orochibraru/svelte-smol",
105
+ async adapt(builder) {
106
+ if (typeof Bun === "undefined")
107
+ throw new Error(NOT_BUN);
108
+ fs.rmSync(out, { recursive: true, force: true });
109
+ builder.log.minor("Building server");
110
+ if (precompress && embed) {
111
+ builder.log.warn("precompress is ignored when compiling: embedded assets are imported by identity path");
112
+ }
113
+ const server = builder.getServerDirectory();
114
+ const index_file = path.resolve(templates, "index.ts");
115
+ const routes_file = path.resolve(templates, "routes.ts");
116
+ const manifest_file = path.resolve(server, "manifest.js");
117
+ const server_options_file = path.resolve(templates, "options.ts");
118
+ const tmp = builder.getBuildDirectory("bun-tmp");
119
+ fs.mkdirSync(tmp, { recursive: true });
120
+ builder.generateServerInstance(`${tmp}/server.js`);
121
+ const virtual_files = {
122
+ [manifest_file]: [
123
+ `export const app_dir = ${JSON.stringify(builder.config.appDir)};`,
124
+ `export const base = ${JSON.stringify(builder.config.paths.base || "/")};`,
125
+ `export const embed = ${JSON.stringify(embed)};`,
126
+ `export const env_prefix = ${JSON.stringify(envPrefix)};`,
127
+ `export const origin = ${JSON.stringify(builder.config.paths.origin) ?? "undefined"};`,
128
+ `export const healthcheck_path = ${JSON.stringify(healthcheck_path)};`,
129
+ ].join("\n"),
130
+ [server_options_file]: `export default ${JSON.stringify(serverOptions)};`,
131
+ [routes_file]: await create_routes({
132
+ builder,
133
+ out,
134
+ embed,
135
+ precompress: precompress && !embed,
136
+ }),
137
+ };
138
+ const entrypoints = [index_file];
139
+ if (builder.hasServerInstrumentationFile()) {
140
+ const start_file = path.resolve(templates, "start.ts"); // virtual only
141
+ virtual_files[start_file] = await Bun.file(index_file).text();
142
+ virtual_files[index_file] = [
143
+ `import ${JSON.stringify(`${server}/instrumentation.server.js`)};`,
144
+ `await import(${JSON.stringify(start_file)});`,
145
+ ].join("\n");
146
+ // as a split chunk, start.js would resolve assets from server/chunks/ instead of the output root
147
+ if (!embed)
148
+ entrypoints.push(start_file);
149
+ }
150
+ // Side-effect-only chunks (e.g. Svelte's events.js) compile to identical
151
+ // stubs whose content hashes collide on one output path, failing the build
152
+ // with "Multiple files share the same output path" (oven-sh/bun#37576).
153
+ // Resolving a distinct identity per importer keeps every copy unique;
154
+ // delete this once the Bun fix ships.
155
+ const side_effect_sources = new Map();
156
+ for (const { abs } of read_files_recursive(`${server}/chunks`)) {
157
+ const source = fs.readFileSync(abs, "utf8");
158
+ if (/^import\s+["'][^"']+["'];\s*export\s*\{\s*\};?\s*$/.test(source)) {
159
+ side_effect_sources.set(abs, source);
160
+ }
161
+ }
162
+ // only the stubs above, because a hook that matches without resolving sends
163
+ // Bun back to the filesystem, where the virtual entrypoints do not exist
164
+ const side_effect_filter = new RegExp(`/(?:${[...side_effect_sources.keys()]
165
+ .map((file) => path.basename(file).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
166
+ .join("|")})$`);
167
+ const adapter_plugin = {
168
+ name: "svelte-smol",
169
+ setup(build) {
170
+ const virtual = {
171
+ SERVER: `${tmp}/server.js`,
172
+ MANIFEST: manifest_file,
173
+ ROUTES: routes_file,
174
+ SERVER_OPTIONS: server_options_file,
175
+ };
176
+ build.onResolve({ filter: /^(SERVER|MANIFEST|ROUTES|SERVER_OPTIONS)$/ }, (args) => ({ path: virtual[args.path] }));
177
+ if (side_effect_sources.size === 0)
178
+ return;
179
+ build.onResolve({ filter: side_effect_filter }, (args) => {
180
+ const file = path.resolve(args.resolveDir, args.path);
181
+ if (!side_effect_sources.has(file))
182
+ return virtual_files[file] ? { path: file } : undefined;
183
+ // The `?` suffix keeps dirname(path) inside chunks/ — Bun resolves the
184
+ // synthetic module's relative imports against that, ignoring onLoad's resolveDir.
185
+ return {
186
+ path: `${file}?${Bun.hash(args.importer).toString(16)}`,
187
+ namespace: "svelte-smol-side-effect",
188
+ };
189
+ });
190
+ build.onLoad({ filter: /.*/, namespace: "svelte-smol-side-effect" }, (args) => ({
191
+ loader: "js",
192
+ contents: `${side_effect_sources.get(args.path.slice(0, args.path.indexOf("?")))}\nSymbol.for('svelte-smol:${Bun.hash(args.path).toString(16)}');`,
193
+ }));
194
+ },
195
+ };
196
+ const compile = (outfile) => embed
197
+ ? {
198
+ outfile,
199
+ ...(typeof buildOptions.compile === "string"
200
+ ? { target: buildOptions.compile }
201
+ : {}),
202
+ ...(typeof buildOptions.compile === "object"
203
+ ? buildOptions.compile
204
+ : {}),
205
+ }
206
+ : false;
207
+ await bun_build(builder, {
208
+ ...buildOptions,
209
+ splitting: buildOptions.splitting ?? true,
210
+ sourcemap: buildOptions.sourcemap ?? (embed ? "none" : "external"),
211
+ entrypoints,
212
+ target: "bun",
213
+ format: "esm",
214
+ naming: {
215
+ entry: "[name].[ext]",
216
+ chunk: "server/chunks/[name]-[hash].[ext]",
217
+ asset: "server/assets/[name]-[hash].[ext]",
218
+ },
219
+ plugins: [adapter_plugin],
220
+ conditions: ["bun", "node"],
221
+ files: virtual_files,
222
+ outdir: out,
223
+ compile: compile("server"),
224
+ });
225
+ if (healthcheck_path) {
226
+ builder.log.minor("Compiling healthcheck");
227
+ const { compile: c } = buildOptions;
228
+ const target = typeof c === "object" ? c.target : c;
229
+ await bun_build(builder, {
230
+ entrypoints: [path.resolve(templates, "healthcheck.ts")],
231
+ target: "bun",
232
+ format: "esm",
233
+ minify: true,
234
+ plugins: [adapter_plugin],
235
+ files: virtual_files,
236
+ outdir: out,
237
+ compile: {
238
+ outfile: "healthcheck",
239
+ ...(typeof target === "string" ? { target } : {}),
240
+ },
241
+ });
242
+ }
243
+ },
244
+ supports: {
245
+ read: () => true,
246
+ instrumentation: () => true,
247
+ },
248
+ };
249
+ }
250
+ async function bun_build(builder, config) {
251
+ const result = await Bun.build({ ...config, throw: false });
252
+ if (!result.success) {
253
+ for (const log of result.logs) {
254
+ // BuildMessage properties are not enumerable, so console.error(log) prints `{}`
255
+ const message = log.message ?? String(log);
256
+ if (log.level === "error")
257
+ builder.log.error(message);
258
+ else if (log.level === "warning")
259
+ builder.log.warn(message);
260
+ else
261
+ builder.log.info(message);
262
+ }
263
+ throw new AggregateError(result.logs, "`bun build` failed");
264
+ }
265
+ }
266
+ async function get_embed_entries(builder, server_assets) {
267
+ const built_files = `${builder.config.outDir}/output`;
268
+ const cl_files = read_files_recursive(`${built_files}/client`).filter(({ rel }) => !is_dotfile(rel));
269
+ const pr_pages = read_files_recursive(`${built_files}/prerendered/pages`);
270
+ const pr_deps = read_files_recursive(`${built_files}/prerendered/dependencies`);
271
+ const pr_data = read_files_recursive(`${built_files}/prerendered/data`);
272
+ const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
273
+ validate_file_paths(assets.map(({ rel }) => rel));
274
+ // keyed by identity: client and prerendered trees can contain the same relative path
275
+ const asset_index = new Map(assets.map((file, i) => [file, i]));
276
+ const imports = assets.map(({ abs }, i) => `import asset_${i} from ${JSON.stringify(abs)} with { type: 'file' };`);
277
+ const entry = async (file, helper, url = file.rel) => `...${helper}(${JSON.stringify(url)}, asset_${asset_index.get(file)}, ${JSON.stringify(await asset_meta(file.abs))})`;
278
+ const page_files = new Map(pr_pages.map((file) => [file.rel, file]));
279
+ const page_rels = new Set([...builder.prerendered.pages].map(([, { file }]) => file));
280
+ const entries = await Promise.all([
281
+ ...cl_files.map((file) => entry(file, "client_asset")),
282
+ ...[...builder.prerendered.pages].map(([pathname, { file }]) => {
283
+ const page = page_files.get(file);
284
+ if (page === undefined)
285
+ throw new Error(`Could not find prerendered page ${file} for route ${pathname}`);
286
+ return entry(page, "prerendered_page", pathname);
287
+ }),
288
+ ...pr_pages
289
+ .filter(({ rel }) => !page_rels.has(rel))
290
+ .map((file) => entry(file, "prerendered_asset")),
291
+ ...[...pr_deps, ...pr_data].map((file) => entry(file, "prerendered_asset")),
292
+ ]);
293
+ const index_by_rel = new Map(assets.map(({ rel }, i) => [rel, i]).reverse());
294
+ return {
295
+ imports,
296
+ entries,
297
+ server_assets: server_assets.map((file) => {
298
+ const idx = index_by_rel.get(file);
299
+ if (idx === undefined)
300
+ throw new Error(`Could not find server asset ${file}`);
301
+ return `[${JSON.stringify(file)}, server_asset(${JSON.stringify(file)}, asset_${idx})]`;
302
+ }),
303
+ };
304
+ }
305
+ async function get_no_embed_entries(builder, server_assets, out, precompress) {
306
+ const client_files = builder
307
+ .writeClient(`${out}/client`)
308
+ .filter((file) => !is_dotfile(file));
309
+ const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
310
+ validate_file_paths([...client_files, ...prerendered_files]);
311
+ if (precompress) {
312
+ await Promise.all([
313
+ builder.compress(`${out}/client`),
314
+ builder.compress(`${out}/prerendered`),
315
+ ]);
316
+ }
317
+ const entry = async (helper, url, dir, filename) => `...${helper}(${JSON.stringify(url)}, ${JSON.stringify(filename)}, ${JSON.stringify(await asset_meta(`${out}/${dir}/${filename ?? url}`, precompress))})`;
318
+ const pages = [...builder.prerendered.pages];
319
+ const page_files = new Set(pages.map(([, { file }]) => file));
320
+ return {
321
+ imports: [],
322
+ entries: await Promise.all([
323
+ ...client_files.map((file) => entry("client_asset", file, "client")),
324
+ ...pages.map(([pathname, { file }]) => entry("prerendered_page", pathname, "prerendered", file)),
325
+ ...prerendered_files
326
+ .filter((file) => !page_files.has(file))
327
+ .map((file) => entry("prerendered_asset", file, "prerendered")),
328
+ ]),
329
+ server_assets: server_assets.map((file) => `[${JSON.stringify(file)}, server_asset(${JSON.stringify(file)})]`),
330
+ };
331
+ }
332
+ async function create_routes({ builder, out, embed, precompress, }) {
333
+ validate_file_paths([
334
+ ...builder.prerendered.pages.keys(),
335
+ ...builder.prerendered.redirects.keys(),
336
+ ]);
337
+ const server_assets = builder.findServerAssets(builder.routes.filter((route) => route.prerender !== true));
338
+ const { imports, entries, ...rest } = embed
339
+ ? await get_embed_entries(builder, server_assets)
340
+ : await get_no_embed_entries(builder, server_assets, out, precompress);
341
+ const redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`);
342
+ return [
343
+ `import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect, server_asset } from './routes-util.ts';`,
344
+ ...imports,
345
+ // reversed because Object.fromEntries keeps the last duplicate: the first generated
346
+ // entry for a path must win so exact files beat aliases, like sirv's lookup order
347
+ `export const routes = Object.fromEntries([${[...entries, ...redirects].join(",\n")}].reverse());`,
348
+ `export const server_assets = new Map([${rest.server_assets.join(",\n")}]);`,
349
+ ].join("\n");
350
+ }
351
+ function posixify(p) {
352
+ return p.replace(/\\/g, "/");
353
+ }