next-bun-compile 0.7.1 → 0.9.0
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 +10 -23
- package/dist/cli.js +2 -6
- package/dist/{index-jevfyh6f.js → index-45tzedws.js} +84 -45
- package/dist/index.js +1 -39
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# next-bun-compile
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Compile your Next.js app into a single-file [Bun](https://bun.sh) executable.
|
|
4
4
|
|
|
5
5
|
One command. One binary. No runtime dependencies.
|
|
6
6
|
|
|
@@ -23,30 +23,18 @@ bun add -D next-bun-compile
|
|
|
23
23
|
|
|
24
24
|
## Setup
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Enable Next.js standalone output in your `next.config.ts`:
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
29
|
import type { NextConfig } from "next";
|
|
30
30
|
|
|
31
31
|
const nextConfig: NextConfig = {
|
|
32
|
-
|
|
32
|
+
output: "standalone",
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export default nextConfig;
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
<details>
|
|
39
|
-
<summary>Using Next.js 16.1? Use the experimental config instead</summary>
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
const nextConfig: NextConfig = {
|
|
43
|
-
experimental: {
|
|
44
|
-
adapterPath: import.meta.resolve("next-bun-compile"),
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
```
|
|
48
|
-
</details>
|
|
49
|
-
|
|
50
38
|
Update your build script in `package.json`:
|
|
51
39
|
|
|
52
40
|
```json
|
|
@@ -88,12 +76,12 @@ See the [Bun cross-compilation docs](https://bun.sh/docs/bundler/executables#cro
|
|
|
88
76
|
|
|
89
77
|
### CDN / `assetPrefix`
|
|
90
78
|
|
|
91
|
-
If you configure `assetPrefix` in your `next.config.ts`, static assets (`/_next/static/`) are served from your CDN instead of the origin server.
|
|
79
|
+
If you configure `assetPrefix` in your `next.config.ts`, static assets (`/_next/static/`) are served from your CDN instead of the origin server. `next-bun-compile` detects this from the build output and skips embedding static assets in the binary — only public files are embedded. This results in a smaller binary.
|
|
92
80
|
|
|
93
81
|
```ts
|
|
94
82
|
const nextConfig: NextConfig = {
|
|
83
|
+
output: "standalone",
|
|
95
84
|
assetPrefix: "https://cdn.example.com",
|
|
96
|
-
adapterPath: import.meta.resolve("next-bun-compile"),
|
|
97
85
|
};
|
|
98
86
|
```
|
|
99
87
|
|
|
@@ -115,18 +103,17 @@ Startup improvements scale with codebase size — larger applications benefit mo
|
|
|
115
103
|
|
|
116
104
|
## How It Works
|
|
117
105
|
|
|
118
|
-
1. **
|
|
119
|
-
2. **
|
|
120
|
-
3. **Code generation** — Creates a `server-entry.js` that:
|
|
106
|
+
1. **Asset discovery** — Scans `.next/static/` and `public/` for all static files
|
|
107
|
+
2. **Code generation** — Creates a `server-entry.js` that:
|
|
121
108
|
- Embeds all assets into the binary via Bun's `import ... with { type: "file" }`
|
|
122
109
|
- Extracts them to disk on first run
|
|
123
110
|
- Fixes `__dirname` for compiled binary context
|
|
124
111
|
- Starts the Next.js server
|
|
125
|
-
|
|
112
|
+
3. **Compilation** — Runs `bun build --compile` with `--define` flags to eliminate dead code branches (dev-only modules, non-turbo runtimes)
|
|
126
113
|
|
|
127
114
|
### Module Stubs
|
|
128
115
|
|
|
129
|
-
Some modules can't be resolved at compile time but are never reached in production (dev servers, optional dependencies).
|
|
116
|
+
Some modules can't be resolved at compile time but are never reached in production (dev servers, optional dependencies). `next-bun-compile` creates no-op stubs for these **only if** the real module isn't installed. If you actually use `@opentelemetry/api` or `critters`, the real package gets bundled instead.
|
|
130
117
|
|
|
131
118
|
## Troubleshooting
|
|
132
119
|
|
|
@@ -152,8 +139,8 @@ Failed to load external module pino-142500b1eb3f4baf: Cannot find package ...
|
|
|
152
139
|
|
|
153
140
|
```ts
|
|
154
141
|
const nextConfig: NextConfig = {
|
|
142
|
+
output: "standalone",
|
|
155
143
|
transpilePackages: ["pino", "pino-pretty"],
|
|
156
|
-
adapterPath: import.meta.resolve("next-bun-compile"),
|
|
157
144
|
};
|
|
158
145
|
```
|
|
159
146
|
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
compile,
|
|
4
4
|
generateEntryPoint
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-45tzedws.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
@@ -12,12 +12,8 @@ var projectDir = resolve(".");
|
|
|
12
12
|
var distDir = join(projectDir, ".next");
|
|
13
13
|
var standaloneDir = join(distDir, "standalone");
|
|
14
14
|
if (!existsSync(standaloneDir)) {
|
|
15
|
-
console.error('next-bun-compile: No standalone output found.
|
|
15
|
+
console.error('next-bun-compile: No standalone output found. Add `output: "standalone"` to next.config.ts and re-run `next build`.');
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
|
-
var ctxPath = join(distDir, "bun-compile-ctx.json");
|
|
19
|
-
if (existsSync(ctxPath)) {
|
|
20
|
-
console.log("next-bun-compile: Using build context from adapter");
|
|
21
|
-
}
|
|
22
18
|
var serverDir = generateEntryPoint({ standaloneDir, distDir, projectDir });
|
|
23
19
|
compile({ serverDir, outfile: join(projectDir, "server"), extraArgs });
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
-
|
|
4
1
|
// src/generate.ts
|
|
5
2
|
import {
|
|
6
3
|
writeFileSync,
|
|
@@ -10,10 +7,18 @@ import {
|
|
|
10
7
|
statSync,
|
|
11
8
|
lstatSync,
|
|
12
9
|
realpathSync,
|
|
13
|
-
mkdirSync
|
|
10
|
+
mkdirSync,
|
|
11
|
+
copyFileSync
|
|
14
12
|
} from "node:fs";
|
|
15
13
|
import { join, relative, basename } from "node:path";
|
|
16
14
|
import { createHash } from "node:crypto";
|
|
15
|
+
function readAssetPrefix(distDir) {
|
|
16
|
+
const rsfPath = join(distDir, "required-server-files.json");
|
|
17
|
+
if (!existsSync(rsfPath))
|
|
18
|
+
return "";
|
|
19
|
+
const rsf = JSON.parse(readFileSync(rsfPath, "utf-8"));
|
|
20
|
+
return rsf.config?.assetPrefix ?? "";
|
|
21
|
+
}
|
|
17
22
|
function tryStat(p) {
|
|
18
23
|
try {
|
|
19
24
|
return statSync(p);
|
|
@@ -42,7 +47,7 @@ function walkDir(dir, base = dir) {
|
|
|
42
47
|
return results;
|
|
43
48
|
}
|
|
44
49
|
function toVarName(filePath) {
|
|
45
|
-
const hash = createHash("
|
|
50
|
+
const hash = createHash("sha256").update(filePath).digest("hex").slice(0, 6);
|
|
46
51
|
const safe = filePath.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
|
|
47
52
|
return `asset_${safe}_${hash}`;
|
|
48
53
|
}
|
|
@@ -298,14 +303,14 @@ function validateAliasResolutions(aliases, resolutions) {
|
|
|
298
303
|
return unresolved;
|
|
299
304
|
}
|
|
300
305
|
function rewriteTurbopackAliases(standaloneNextDir, aliases, resolutions) {
|
|
306
|
+
const rewrittenPaths = [];
|
|
301
307
|
if (aliases.length === 0 || resolutions.size === 0)
|
|
302
|
-
return;
|
|
308
|
+
return rewrittenPaths;
|
|
303
309
|
const serverDir = join(standaloneNextDir, "server");
|
|
304
310
|
if (!existsSync(serverDir))
|
|
305
|
-
return;
|
|
311
|
+
return rewrittenPaths;
|
|
306
312
|
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
307
313
|
const pattern = new RegExp(`(["'])((?:` + aliases.map((a) => escape(a.alias)).join("|") + `)(?:/[^"']+)?)\\1`, "g");
|
|
308
|
-
let rewritten = 0;
|
|
309
314
|
for (const f of walkDir(serverDir)) {
|
|
310
315
|
if (!f.absolutePath.endsWith(".js"))
|
|
311
316
|
continue;
|
|
@@ -323,12 +328,13 @@ function rewriteTurbopackAliases(standaloneNextDir, aliases, resolutions) {
|
|
|
323
328
|
});
|
|
324
329
|
if (next !== content) {
|
|
325
330
|
writeFileSync(f.absolutePath, next);
|
|
326
|
-
|
|
331
|
+
rewrittenPaths.push(`.next/server/${f.relativePath.replace(/\\/g, "/")}`);
|
|
327
332
|
}
|
|
328
333
|
}
|
|
329
|
-
if (
|
|
330
|
-
console.log(`next-bun-compile: Rewrote turbopack-mangled aliases in ${
|
|
334
|
+
if (rewrittenPaths.length > 0) {
|
|
335
|
+
console.log(`next-bun-compile: Rewrote turbopack-mangled aliases in ${rewrittenPaths.length} server chunks`);
|
|
331
336
|
}
|
|
337
|
+
return rewrittenPaths;
|
|
332
338
|
}
|
|
333
339
|
function findServerDir(standaloneDir) {
|
|
334
340
|
if (existsSync(join(standaloneDir, "server.js"))) {
|
|
@@ -465,7 +471,12 @@ function fixModuleResolution(standaloneDir) {
|
|
|
465
471
|
const indexPath = join(dir, "index.js");
|
|
466
472
|
if (!existsSync(pkgJsonPath) || existsSync(indexPath))
|
|
467
473
|
continue;
|
|
468
|
-
|
|
474
|
+
let pkg;
|
|
475
|
+
try {
|
|
476
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
477
|
+
} catch {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
469
480
|
if (pkg.main && pkg.main !== "index.js") {
|
|
470
481
|
writeFileSync(indexPath, `module.exports = require("./${pkg.main}");`);
|
|
471
482
|
}
|
|
@@ -521,7 +532,7 @@ function generateEntryPoint(options) {
|
|
|
521
532
|
continue;
|
|
522
533
|
const dest = join(externalDir, mod);
|
|
523
534
|
mkdirSync(join(dest, ".."), { recursive: true });
|
|
524
|
-
|
|
535
|
+
copyFileSync(src, dest);
|
|
525
536
|
runtimeFiles.push({
|
|
526
537
|
absolutePath: dest,
|
|
527
538
|
relativePath: `__external/${mod}`,
|
|
@@ -533,14 +544,20 @@ function generateEntryPoint(options) {
|
|
|
533
544
|
}
|
|
534
545
|
const canonicalResolutions = buildCanonicalResolutions(externalDir, turbopackAliases);
|
|
535
546
|
validateAliasResolutions(turbopackAliases, canonicalResolutions);
|
|
536
|
-
rewriteTurbopackAliases(standaloneNextDir, turbopackAliases, canonicalResolutions);
|
|
537
|
-
const
|
|
538
|
-
const { assetPrefix } = ctx;
|
|
547
|
+
const rewrittenChunks = rewriteTurbopackAliases(standaloneNextDir, turbopackAliases, canonicalResolutions);
|
|
548
|
+
const assetPrefix = readAssetPrefix(distDir);
|
|
539
549
|
const assetsToEmbed = assetPrefix ? [...publicFiles, ...runtimeFiles] : [...staticFiles, ...publicFiles, ...runtimeFiles];
|
|
540
550
|
if (assetPrefix) {
|
|
541
551
|
console.log(`next-bun-compile: assetPrefix detected — skipping ${staticFiles.length} static assets (served from CDN)`);
|
|
542
552
|
}
|
|
543
553
|
console.log(`next-bun-compile: Embedding ${assetsToEmbed.length} assets (${staticFiles.length} static + ${publicFiles.length} public + ${runtimeFiles.length} runtime)`);
|
|
554
|
+
const hasher = createHash("sha256");
|
|
555
|
+
for (const asset of assetsToEmbed) {
|
|
556
|
+
hasher.update(asset.urlPath);
|
|
557
|
+
hasher.update("\x00");
|
|
558
|
+
hasher.update(readFileSync(asset.absolutePath));
|
|
559
|
+
}
|
|
560
|
+
const buildHash = hasher.digest("hex");
|
|
544
561
|
const imports = [];
|
|
545
562
|
const mapEntries = [];
|
|
546
563
|
for (const asset of assetsToEmbed) {
|
|
@@ -747,45 +764,59 @@ process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
|
|
|
747
764
|
const currentPort = parseInt(process.env.PORT, 10) || 3000;
|
|
748
765
|
const hostname = process.env.HOSTNAME || "0.0.0.0";
|
|
749
766
|
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
|
|
750
|
-
if (
|
|
767
|
+
if (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
|
|
751
768
|
keepAliveTimeout = undefined;
|
|
752
769
|
}
|
|
753
770
|
|
|
754
771
|
const extractions = ${JSON.stringify(assetExtractions)};
|
|
772
|
+
// Chunks that got __NBC_BASE__ placeholders injected at build time by
|
|
773
|
+
// rewriteTurbopackAliases — only these need text substitution on extract;
|
|
774
|
+
// everything else streams straight from the binary.
|
|
775
|
+
const rewrittenChunks = new Set(${JSON.stringify(rewrittenChunks)});
|
|
776
|
+
// Written to the manifest after a complete extraction. Includes baseDir:
|
|
777
|
+
// if the deploy directory moves, the substituted absolute paths in the
|
|
778
|
+
// rewritten chunks are wrong and everything must be re-extracted.
|
|
779
|
+
const buildStamp = ${JSON.stringify(buildHash)} + "\\n" + baseDir;
|
|
780
|
+
const manifestPath = path.join(baseDir, ".next", ".nbc-extracted");
|
|
755
781
|
async function extractAssets() {
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
for (const [urlPath, diskPath] of extractions) {
|
|
762
|
-
const fullPath = path.join(baseDir, diskPath);
|
|
763
|
-
if (fs.existsSync(fullPath)) continue;
|
|
764
|
-
const embedded = assetMap.get(urlPath);
|
|
765
|
-
if (!embedded) continue;
|
|
766
|
-
todo.push({ diskPath, fullPath, embedded });
|
|
767
|
-
}
|
|
768
|
-
if (todo.length === 0) return;
|
|
782
|
+
// Fast path: a previous boot of this exact build in this exact directory
|
|
783
|
+
// finished extracting — one file read, no per-asset stats.
|
|
784
|
+
try {
|
|
785
|
+
if (fs.readFileSync(manifestPath, "utf-8") === buildStamp) return;
|
|
786
|
+
} catch {}
|
|
769
787
|
|
|
788
|
+
// Full extraction, overwriting whatever is on disk. Skipping existing
|
|
789
|
+
// files would let stale ones (crashed half-extraction, previous build,
|
|
790
|
+
// pre-placed tampering) shadow the embedded assets forever.
|
|
770
791
|
const dirs = new Set();
|
|
771
|
-
for (const
|
|
792
|
+
for (const [, diskPath] of extractions) {
|
|
793
|
+
dirs.add(path.dirname(path.join(baseDir, diskPath)));
|
|
794
|
+
}
|
|
772
795
|
for (const d of dirs) fs.mkdirSync(d, { recursive: true });
|
|
773
796
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
if (
|
|
797
|
+
// Concurrent writes, bounded so thousands of in-flight fds can't trip
|
|
798
|
+
// EMFILE under conservative ulimits.
|
|
799
|
+
let idx = 0;
|
|
800
|
+
async function worker() {
|
|
801
|
+
while (idx < extractions.length) {
|
|
802
|
+
const [urlPath, diskPath] = extractions[idx++];
|
|
803
|
+
const embedded = assetMap.get(urlPath);
|
|
804
|
+
if (!embedded) continue;
|
|
805
|
+
const fullPath = path.join(baseDir, diskPath);
|
|
806
|
+
if (rewrittenChunks.has(diskPath)) {
|
|
807
|
+
const text = await Bun.file(embedded).text();
|
|
782
808
|
await Bun.write(fullPath, text.split("__NBC_BASE__").join(baseDir));
|
|
783
|
-
|
|
809
|
+
} else {
|
|
810
|
+
await Bun.write(fullPath, Bun.file(embedded));
|
|
784
811
|
}
|
|
785
812
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
813
|
+
}
|
|
814
|
+
await Promise.all(
|
|
815
|
+
Array.from({ length: Math.min(64, extractions.length) }, worker)
|
|
816
|
+
);
|
|
817
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
818
|
+
fs.writeFileSync(manifestPath, buildStamp);
|
|
819
|
+
console.log(\`Extracted \${extractions.length} assets\`);
|
|
789
820
|
}
|
|
790
821
|
|
|
791
822
|
extractAssets().then(() => {
|
|
@@ -826,8 +857,16 @@ function compile(options) {
|
|
|
826
857
|
...extraArgs
|
|
827
858
|
];
|
|
828
859
|
console.log(`next-bun-compile: Compiling to ${outfile}...`);
|
|
829
|
-
|
|
860
|
+
try {
|
|
861
|
+
execFileSync("bun", args, { stdio: "inherit" });
|
|
862
|
+
} catch (err) {
|
|
863
|
+
if (err.code === "ENOENT") {
|
|
864
|
+
console.error("next-bun-compile: `bun` was not found on PATH. Install it from https://bun.sh and re-run.");
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
throw err;
|
|
868
|
+
}
|
|
830
869
|
console.log(`next-bun-compile: Done → ${outfile}`);
|
|
831
870
|
}
|
|
832
871
|
|
|
833
|
-
export {
|
|
872
|
+
export { generateEntryPoint, compile };
|
package/dist/index.js
CHANGED
|
@@ -1,46 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
__require,
|
|
3
2
|
compile,
|
|
4
3
|
generateEntryPoint
|
|
5
|
-
} from "./index-
|
|
6
|
-
|
|
7
|
-
// src/index.ts
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
var knownTranspilePackages = ["pino", "pino-pretty"];
|
|
10
|
-
var adapter = {
|
|
11
|
-
name: "next-bun-compile",
|
|
12
|
-
modifyConfig(config, ctx) {
|
|
13
|
-
if (!ctx) {
|
|
14
|
-
throw new Error("next-bun-compile: Next.js 16+ is required. Please upgrade your Next.js version.");
|
|
15
|
-
}
|
|
16
|
-
if (ctx.phase !== "phase-production-build")
|
|
17
|
-
return config;
|
|
18
|
-
if (process.argv.includes("--webpack")) {
|
|
19
|
-
throw new Error("next-bun-compile: Webpack builds are not supported. Remove --webpack to use Turbopack (default).");
|
|
20
|
-
}
|
|
21
|
-
if (config.output !== "standalone") {
|
|
22
|
-
console.warn('next-bun-compile: Setting output to "standalone" (required for compilation)');
|
|
23
|
-
config.output = "standalone";
|
|
24
|
-
}
|
|
25
|
-
const existing = config.transpilePackages ?? [];
|
|
26
|
-
const toAdd = knownTranspilePackages.filter((p) => !existing.includes(p));
|
|
27
|
-
if (toAdd.length > 0) {
|
|
28
|
-
config.transpilePackages = [...existing, ...toAdd];
|
|
29
|
-
}
|
|
30
|
-
return config;
|
|
31
|
-
},
|
|
32
|
-
async onBuildComplete(ctx) {
|
|
33
|
-
const { writeFileSync } = await import("node:fs");
|
|
34
|
-
writeFileSync(join(ctx.distDir, "bun-compile-ctx.json"), JSON.stringify({
|
|
35
|
-
distDir: ctx.distDir,
|
|
36
|
-
projectDir: ctx.projectDir,
|
|
37
|
-
assetPrefix: ctx.config.assetPrefix || ""
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
var src_default = adapter;
|
|
4
|
+
} from "./index-45tzedws.js";
|
|
42
5
|
export {
|
|
43
6
|
generateEntryPoint,
|
|
44
|
-
src_default as default,
|
|
45
7
|
compile
|
|
46
8
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-bun-compile",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Next.js
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Compile your Next.js app into a Bun single-file executable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"next": "16.1.6",
|
|
29
|
+
"@types/bun": "^1",
|
|
29
30
|
"@types/node": "^20",
|
|
30
31
|
"typescript": "^5"
|
|
31
32
|
},
|
|
@@ -34,8 +35,7 @@
|
|
|
34
35
|
"bun",
|
|
35
36
|
"compile",
|
|
36
37
|
"standalone",
|
|
37
|
-
"executable"
|
|
38
|
-
"adapter"
|
|
38
|
+
"executable"
|
|
39
39
|
],
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"homepage": "https://github.com/ramonmalcolm10/next-bun-compile#readme",
|