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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # next-bun-compile
2
2
 
3
- A Next.js Build Adapter that compiles your app into a single-file [Bun](https://bun.sh) executable.
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
- Add the adapter to your `next.config.ts`:
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
- adapterPath: import.meta.resolve("next-bun-compile"),
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. The adapter detects this automatically and skips embedding static assets in the binary — only public files are embedded. This results in a smaller binary.
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. **Adapter hook** — `modifyConfig()` sets `output: "standalone"` automatically so you don't need to configure it
119
- 2. **Asset discovery** — Scans `.next/static/` and `public/` for all static files
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
- 4. **Compilation** — Runs `bun build --compile` with `--define` flags to eliminate dead code branches (dev-only modules, non-turbo runtimes)
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). The adapter 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.
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-jevfyh6f.js";
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. Run "next build" first with output: "standalone" in next.config.ts.');
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("md5").update(filePath).digest("hex").slice(0, 6);
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
- rewritten++;
331
+ rewrittenPaths.push(`.next/server/${f.relativePath.replace(/\\/g, "/")}`);
327
332
  }
328
333
  }
329
- if (rewritten > 0) {
330
- console.log(`next-bun-compile: Rewrote turbopack-mangled aliases in ${rewritten} server chunks`);
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
- const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
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
- writeFileSync(dest, readFileSync(src));
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 ctx = JSON.parse(readFileSync(join(distDir, "bun-compile-ctx.json"), "utf-8"));
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 (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
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
- // Collect everything that isn't already on disk, dedupe parent dirs
757
- // for a single mkdir pass, then issue all writes concurrently.
758
- // ~45% faster than serial extraction on a typical Next.js app with
759
- // sharp; trivially correct because each write is to a distinct path.
760
- const todo = [];
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 t of todo) dirs.add(path.dirname(t.fullPath));
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
- await Promise.all(todo.map(async ({ diskPath, fullPath, embedded }) => {
775
- // Server chunks may contain __NBC_BASE__ placeholders injected at
776
- // build time by rewriteTurbopackAliases. Substitute the real baseDir
777
- // before writing so bun resolves the absolute paths at chunk load
778
- // (works for both CJS require and ESM import).
779
- if (diskPath.startsWith(".next/server/")) {
780
- const text = await Bun.file(embedded).text();
781
- if (text.indexOf("__NBC_BASE__") !== -1) {
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
- return;
809
+ } else {
810
+ await Bun.write(fullPath, Bun.file(embedded));
784
811
  }
785
812
  }
786
- await Bun.write(fullPath, Bun.file(embedded));
787
- }));
788
- console.log(\`Extracted \${todo.length} assets\`);
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
- execFileSync("bun", args, { stdio: "inherit" });
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 { __require, generateEntryPoint, compile };
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-jevfyh6f.js";
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.7.1",
4
- "description": "Next.js Build Adapter that compiles your app into a Bun single-file executable",
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",