next-bun-compile 0.8.0 → 0.10.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
@@ -89,7 +89,7 @@ You'll need to upload `.next/static/` to your CDN separately.
89
89
 
90
90
  ## Performance
91
91
 
92
- The compiled binary uses Bun's `--bytecode` flag to pre-compile JavaScript to bytecode at build time, skipping the parsing step at startup. Code is also minified and dead code paths (dev-only modules, non-turbo runtimes) are eliminated via `--define` flags.
92
+ The compiled binary is minified, and dead code paths (dev-only modules, non-turbo runtimes) are eliminated via `--define` flags. Startup skips module resolution for the bundled server core entirely — the code is already in the binary.
93
93
 
94
94
  Benchmarks on a real Next.js app (both running on Bun's runtime):
95
95
 
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  compile,
4
4
  generateEntryPoint
5
- } from "./index-34djf48r.js";
5
+ } from "./index-d3wb5t4x.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { existsSync } from "node:fs";
@@ -7,21 +7,17 @@ import {
7
7
  statSync,
8
8
  lstatSync,
9
9
  realpathSync,
10
- mkdirSync
10
+ mkdirSync,
11
+ copyFileSync
11
12
  } from "node:fs";
12
13
  import { join, relative, basename } from "node:path";
13
14
  import { createHash } from "node:crypto";
14
15
  function readAssetPrefix(distDir) {
15
- const ctxPath = join(distDir, "bun-compile-ctx.json");
16
- if (existsSync(ctxPath)) {
17
- return JSON.parse(readFileSync(ctxPath, "utf-8")).assetPrefix ?? "";
18
- }
19
16
  const rsfPath = join(distDir, "required-server-files.json");
20
- if (existsSync(rsfPath)) {
21
- const rsf = JSON.parse(readFileSync(rsfPath, "utf-8"));
22
- return rsf.config?.assetPrefix ?? "";
23
- }
24
- return "";
17
+ if (!existsSync(rsfPath))
18
+ return "";
19
+ const rsf = JSON.parse(readFileSync(rsfPath, "utf-8"));
20
+ return rsf.config?.assetPrefix ?? "";
25
21
  }
26
22
  function tryStat(p) {
27
23
  try {
@@ -51,7 +47,7 @@ function walkDir(dir, base = dir) {
51
47
  return results;
52
48
  }
53
49
  function toVarName(filePath) {
54
- const hash = createHash("md5").update(filePath).digest("hex").slice(0, 6);
50
+ const hash = createHash("sha256").update(filePath).digest("hex").slice(0, 6);
55
51
  const safe = filePath.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
56
52
  return `asset_${safe}_${hash}`;
57
53
  }
@@ -307,14 +303,14 @@ function validateAliasResolutions(aliases, resolutions) {
307
303
  return unresolved;
308
304
  }
309
305
  function rewriteTurbopackAliases(standaloneNextDir, aliases, resolutions) {
306
+ const rewrittenPaths = [];
310
307
  if (aliases.length === 0 || resolutions.size === 0)
311
- return;
308
+ return rewrittenPaths;
312
309
  const serverDir = join(standaloneNextDir, "server");
313
310
  if (!existsSync(serverDir))
314
- return;
311
+ return rewrittenPaths;
315
312
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
316
313
  const pattern = new RegExp(`(["'])((?:` + aliases.map((a) => escape(a.alias)).join("|") + `)(?:/[^"']+)?)\\1`, "g");
317
- let rewritten = 0;
318
314
  for (const f of walkDir(serverDir)) {
319
315
  if (!f.absolutePath.endsWith(".js"))
320
316
  continue;
@@ -332,12 +328,13 @@ function rewriteTurbopackAliases(standaloneNextDir, aliases, resolutions) {
332
328
  });
333
329
  if (next !== content) {
334
330
  writeFileSync(f.absolutePath, next);
335
- rewritten++;
331
+ rewrittenPaths.push(`.next/server/${f.relativePath.replace(/\\/g, "/")}`);
336
332
  }
337
333
  }
338
- if (rewritten > 0) {
339
- 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`);
340
336
  }
337
+ return rewrittenPaths;
341
338
  }
342
339
  function findServerDir(standaloneDir) {
343
340
  if (existsSync(join(standaloneDir, "server.js"))) {
@@ -474,7 +471,12 @@ function fixModuleResolution(standaloneDir) {
474
471
  const indexPath = join(dir, "index.js");
475
472
  if (!existsSync(pkgJsonPath) || existsSync(indexPath))
476
473
  continue;
477
- 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
+ }
478
480
  if (pkg.main && pkg.main !== "index.js") {
479
481
  writeFileSync(indexPath, `module.exports = require("./${pkg.main}");`);
480
482
  }
@@ -530,7 +532,7 @@ function generateEntryPoint(options) {
530
532
  continue;
531
533
  const dest = join(externalDir, mod);
532
534
  mkdirSync(join(dest, ".."), { recursive: true });
533
- writeFileSync(dest, readFileSync(src));
535
+ copyFileSync(src, dest);
534
536
  runtimeFiles.push({
535
537
  absolutePath: dest,
536
538
  relativePath: `__external/${mod}`,
@@ -542,13 +544,20 @@ function generateEntryPoint(options) {
542
544
  }
543
545
  const canonicalResolutions = buildCanonicalResolutions(externalDir, turbopackAliases);
544
546
  validateAliasResolutions(turbopackAliases, canonicalResolutions);
545
- rewriteTurbopackAliases(standaloneNextDir, turbopackAliases, canonicalResolutions);
547
+ const rewrittenChunks = rewriteTurbopackAliases(standaloneNextDir, turbopackAliases, canonicalResolutions);
546
548
  const assetPrefix = readAssetPrefix(distDir);
547
549
  const assetsToEmbed = assetPrefix ? [...publicFiles, ...runtimeFiles] : [...staticFiles, ...publicFiles, ...runtimeFiles];
548
550
  if (assetPrefix) {
549
551
  console.log(`next-bun-compile: assetPrefix detected — skipping ${staticFiles.length} static assets (served from CDN)`);
550
552
  }
551
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");
552
561
  const imports = [];
553
562
  const mapEntries = [];
554
563
  for (const asset of assetsToEmbed) {
@@ -755,45 +764,59 @@ process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
755
764
  const currentPort = parseInt(process.env.PORT, 10) || 3000;
756
765
  const hostname = process.env.HOSTNAME || "0.0.0.0";
757
766
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
758
- if (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
767
+ if (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
759
768
  keepAliveTimeout = undefined;
760
769
  }
761
770
 
762
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");
763
781
  async function extractAssets() {
764
- // Collect everything that isn't already on disk, dedupe parent dirs
765
- // for a single mkdir pass, then issue all writes concurrently.
766
- // ~45% faster than serial extraction on a typical Next.js app with
767
- // sharp; trivially correct because each write is to a distinct path.
768
- const todo = [];
769
- for (const [urlPath, diskPath] of extractions) {
770
- const fullPath = path.join(baseDir, diskPath);
771
- if (fs.existsSync(fullPath)) continue;
772
- const embedded = assetMap.get(urlPath);
773
- if (!embedded) continue;
774
- todo.push({ diskPath, fullPath, embedded });
775
- }
776
- 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 {}
777
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.
778
791
  const dirs = new Set();
779
- 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
+ }
780
795
  for (const d of dirs) fs.mkdirSync(d, { recursive: true });
781
796
 
782
- await Promise.all(todo.map(async ({ diskPath, fullPath, embedded }) => {
783
- // Server chunks may contain __NBC_BASE__ placeholders injected at
784
- // build time by rewriteTurbopackAliases. Substitute the real baseDir
785
- // before writing so bun resolves the absolute paths at chunk load
786
- // (works for both CJS require and ESM import).
787
- if (diskPath.startsWith(".next/server/")) {
788
- const text = await Bun.file(embedded).text();
789
- 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();
790
808
  await Bun.write(fullPath, text.split("__NBC_BASE__").join(baseDir));
791
- return;
809
+ } else {
810
+ await Bun.write(fullPath, Bun.file(embedded));
792
811
  }
793
812
  }
794
- await Bun.write(fullPath, Bun.file(embedded));
795
- }));
796
- 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\`);
797
820
  }
798
821
 
799
822
  extractAssets().then(() => {
@@ -821,7 +844,6 @@ function compile(options) {
821
844
  "--production",
822
845
  "--compile",
823
846
  "--minify",
824
- "--bytecode",
825
847
  "--sourcemap",
826
848
  "--define",
827
849
  "process.env.TURBOPACK=1",
@@ -834,7 +856,15 @@ function compile(options) {
834
856
  ...extraArgs
835
857
  ];
836
858
  console.log(`next-bun-compile: Compiling to ${outfile}...`);
837
- execFileSync("bun", args, { stdio: "inherit" });
859
+ try {
860
+ execFileSync("bun", args, { stdio: "inherit" });
861
+ } catch (err) {
862
+ if (err.code === "ENOENT") {
863
+ console.error("next-bun-compile: `bun` was not found on PATH. Install it from https://bun.sh and re-run.");
864
+ process.exit(1);
865
+ }
866
+ throw err;
867
+ }
838
868
  console.log(`next-bun-compile: Done → ${outfile}`);
839
869
  }
840
870
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  compile,
3
3
  generateEntryPoint
4
- } from "./index-34djf48r.js";
4
+ } from "./index-d3wb5t4x.js";
5
5
  export {
6
6
  generateEntryPoint,
7
7
  compile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-bun-compile",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Compile your Next.js app into a Bun single-file executable",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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",