@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.
- package/README.md +31 -6
- package/dist/index.d.ts +9 -139
- package/dist/index.js +26 -162
- package/dist/kit2/index.d.ts +149 -0
- package/dist/kit2/index.js +181 -0
- package/dist/kit3/index.d.ts +80 -0
- package/dist/kit3/index.js +353 -0
- package/dist/kit3/templates/env.ts +133 -0
- package/dist/kit3/templates/handler.ts +135 -0
- package/dist/kit3/templates/healthcheck.ts +31 -0
- package/dist/kit3/templates/index.ts +114 -0
- package/dist/kit3/templates/routes-util.ts +207 -0
- package/package.json +5 -14
- /package/dist/{templates → kit2/templates}/env.ts +0 -0
- /package/dist/{templates → kit2/templates}/handler.ts +0 -0
- /package/dist/{templates → kit2/templates}/healthcheck.ts +0 -0
- /package/dist/{templates → kit2/templates}/index.ts +0 -0
|
@@ -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
|
+
}
|