next-bun-compile 0.1.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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/cli.js +196 -0
- package/dist/compile.js +33 -0
- package/dist/generate.js +153 -0
- package/dist/index.js +204 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ramon Malcolm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# next-bun-compile
|
|
2
|
+
|
|
3
|
+
A Next.js Build Adapter that compiles your app into a single-file [Bun](https://bun.sh) executable.
|
|
4
|
+
|
|
5
|
+
One command. One binary. No runtime dependencies.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
next build # → ./server (single executable with embedded assets)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- [Bun](https://bun.sh) >= 1.3
|
|
14
|
+
- [Next.js](https://nextjs.org) >= 16.0.0
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add -D next-bun-compile
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
Add the adapter to your `next.config.ts`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import type { NextConfig } from "next";
|
|
28
|
+
|
|
29
|
+
const nextConfig: NextConfig = {
|
|
30
|
+
experimental: {
|
|
31
|
+
adapterPath: require.resolve("next-bun-compile"),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default nextConfig;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Update your build script in `package.json`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "next build && next-bun-compile"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun run build # Builds Next.js + compiles to ./server
|
|
52
|
+
./server # Starts on port 3000
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The binary is fully self-contained — static assets, public files, and the Next.js server are all embedded. Just copy it anywhere and run.
|
|
56
|
+
|
|
57
|
+
### Environment Variables
|
|
58
|
+
|
|
59
|
+
| Variable | Default | Description |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `PORT` | `3000` | Server port |
|
|
62
|
+
| `HOSTNAME` | `0.0.0.0` | Server hostname |
|
|
63
|
+
| `KEEP_ALIVE_TIMEOUT` | — | HTTP keep-alive timeout (ms) |
|
|
64
|
+
|
|
65
|
+
## How It Works
|
|
66
|
+
|
|
67
|
+
1. **Adapter hook** — `modifyConfig()` sets `output: "standalone"` automatically so you don't need to configure it
|
|
68
|
+
2. **Asset discovery** — Scans `.next/static/` and `public/` for all static files
|
|
69
|
+
3. **Code generation** — Creates a `server-entry.js` that:
|
|
70
|
+
- Embeds all assets into the binary via Bun's `import ... with { type: "file" }`
|
|
71
|
+
- Extracts them to disk on first run
|
|
72
|
+
- Fixes `__dirname` for compiled binary context
|
|
73
|
+
- Starts the Next.js server
|
|
74
|
+
4. **Compilation** — Runs `bun build --compile` with `--define` flags to eliminate dead code branches (dev-only modules, non-turbo runtimes)
|
|
75
|
+
|
|
76
|
+
### Module Stubs
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
## Support
|
|
81
|
+
|
|
82
|
+
If this saved you time, consider supporting the project:
|
|
83
|
+
|
|
84
|
+
[](https://buymeacoffee.com/ramonmalcolm)
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/generate.ts
|
|
6
|
+
import {
|
|
7
|
+
writeFileSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
statSync,
|
|
12
|
+
mkdirSync
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { join, relative } from "node:path";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
function walkDir(dir, base = dir) {
|
|
17
|
+
const results = [];
|
|
18
|
+
if (!existsSync(dir))
|
|
19
|
+
return results;
|
|
20
|
+
for (const entry of readdirSync(dir)) {
|
|
21
|
+
const full = join(dir, entry);
|
|
22
|
+
if (statSync(full).isDirectory()) {
|
|
23
|
+
results.push(...walkDir(full, base));
|
|
24
|
+
} else {
|
|
25
|
+
results.push({ absolutePath: full, relativePath: relative(base, full) });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
function toVarName(filePath) {
|
|
31
|
+
const hash = createHash("md5").update(filePath).digest("hex").slice(0, 6);
|
|
32
|
+
const safe = filePath.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
|
|
33
|
+
return `asset_${safe}_${hash}`;
|
|
34
|
+
}
|
|
35
|
+
function generateStubs(standaloneDir) {
|
|
36
|
+
const stubs = [
|
|
37
|
+
{
|
|
38
|
+
path: "node_modules/next/dist/server/dev/next-dev-server.js",
|
|
39
|
+
content: "module.exports = { default: null };"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
path: "node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.js",
|
|
43
|
+
content: "module.exports = {};"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: "node_modules/@opentelemetry/api/index.js",
|
|
47
|
+
content: "throw new Error('not installed');"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: "node_modules/critters/index.js",
|
|
51
|
+
content: "module.exports = {};"
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
let count = 0;
|
|
55
|
+
for (const stub of stubs) {
|
|
56
|
+
const fullPath = join(standaloneDir, stub.path);
|
|
57
|
+
if (!existsSync(fullPath)) {
|
|
58
|
+
const dir = join(fullPath, "..");
|
|
59
|
+
if (!existsSync(dir)) {
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(fullPath, stub.content);
|
|
63
|
+
count++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (count > 0) {
|
|
67
|
+
console.log(`next-bun-compile: Created ${count} module stubs`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function generateEntryPoint(options) {
|
|
71
|
+
const { standaloneDir, distDir, projectDir } = options;
|
|
72
|
+
generateStubs(standaloneDir);
|
|
73
|
+
const staticDir = join(distDir, "static");
|
|
74
|
+
const staticFiles = walkDir(staticDir).map((f) => ({
|
|
75
|
+
...f,
|
|
76
|
+
urlPath: `/_next/static/${f.relativePath}`
|
|
77
|
+
}));
|
|
78
|
+
const publicDir = join(projectDir, "public");
|
|
79
|
+
const publicFiles = walkDir(publicDir).map((f) => ({
|
|
80
|
+
...f,
|
|
81
|
+
urlPath: `/${f.relativePath}`
|
|
82
|
+
}));
|
|
83
|
+
const allAssets = [...staticFiles, ...publicFiles];
|
|
84
|
+
console.log(`next-bun-compile: Found ${staticFiles.length} static + ${publicFiles.length} public = ${allAssets.length} assets`);
|
|
85
|
+
const imports = [];
|
|
86
|
+
const mapEntries = [];
|
|
87
|
+
for (const asset of allAssets) {
|
|
88
|
+
const varName = toVarName(asset.urlPath);
|
|
89
|
+
const importPath = relative(standaloneDir, asset.absolutePath).replace(/\\/g, "/");
|
|
90
|
+
imports.push(`import ${varName} from "./${importPath}" with { type: "file" };`);
|
|
91
|
+
mapEntries.push(` ["${asset.urlPath}", ${varName}],`);
|
|
92
|
+
}
|
|
93
|
+
writeFileSync(join(standaloneDir, "assets.generated.js"), `${imports.join(`
|
|
94
|
+
`)}
|
|
95
|
+
export const assetMap = new Map([
|
|
96
|
+
${mapEntries.join(`
|
|
97
|
+
`)}
|
|
98
|
+
]);
|
|
99
|
+
`);
|
|
100
|
+
const standaloneServerSrc = readFileSync(join(standaloneDir, "server.js"), "utf-8");
|
|
101
|
+
const configMatch = standaloneServerSrc.match(/const nextConfig = ({[\s\S]*?})\n/);
|
|
102
|
+
if (!configMatch) {
|
|
103
|
+
throw new Error("next-bun-compile: Could not extract nextConfig from standalone server.js");
|
|
104
|
+
}
|
|
105
|
+
const assetExtractions = allAssets.map((a) => {
|
|
106
|
+
const diskPath = a.urlPath.startsWith("/_next/static/") ? ".next/static/" + a.relativePath : "public/" + a.relativePath;
|
|
107
|
+
return [a.urlPath, diskPath];
|
|
108
|
+
});
|
|
109
|
+
const serverEntry = `import { assetMap } from "./assets.generated.js";
|
|
110
|
+
const path = require("path");
|
|
111
|
+
const fs = require("fs");
|
|
112
|
+
|
|
113
|
+
const baseDir = path.dirname(process.execPath);
|
|
114
|
+
process.chdir(baseDir);
|
|
115
|
+
process.env.NODE_ENV = "production";
|
|
116
|
+
|
|
117
|
+
const nextConfig = ${configMatch[1]};
|
|
118
|
+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
|
|
119
|
+
|
|
120
|
+
const currentPort = parseInt(process.env.PORT, 10) || 3000;
|
|
121
|
+
const hostname = process.env.HOSTNAME || "0.0.0.0";
|
|
122
|
+
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
|
|
123
|
+
if (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
|
|
124
|
+
keepAliveTimeout = undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const extractions = ${JSON.stringify(assetExtractions)};
|
|
128
|
+
async function extractAssets() {
|
|
129
|
+
let n = 0;
|
|
130
|
+
for (const [urlPath, diskPath] of extractions) {
|
|
131
|
+
const fullPath = path.join(baseDir, diskPath);
|
|
132
|
+
if (fs.existsSync(fullPath)) continue;
|
|
133
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
134
|
+
const embedded = assetMap.get(urlPath);
|
|
135
|
+
if (embedded) { await Bun.write(fullPath, Bun.file(embedded)); n++; }
|
|
136
|
+
}
|
|
137
|
+
if (n > 0) console.log(\`Extracted \${n} assets\`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
extractAssets().then(() => {
|
|
141
|
+
require("next");
|
|
142
|
+
const { startServer } = require("next/dist/server/lib/start-server");
|
|
143
|
+
return startServer({
|
|
144
|
+
dir: baseDir, isDev: false, config: nextConfig,
|
|
145
|
+
hostname, port: currentPort, allowRetry: false, keepAliveTimeout,
|
|
146
|
+
});
|
|
147
|
+
}).catch((err) => { console.error(err); process.exit(1); });
|
|
148
|
+
`;
|
|
149
|
+
writeFileSync(join(standaloneDir, "server-entry.js"), serverEntry);
|
|
150
|
+
console.log("next-bun-compile: Generated server-entry.js + assets.generated.js");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/compile.ts
|
|
154
|
+
import { execFileSync } from "node:child_process";
|
|
155
|
+
import { join as join2 } from "node:path";
|
|
156
|
+
function compile(options) {
|
|
157
|
+
const { standaloneDir, outfile } = options;
|
|
158
|
+
const entryPoint = join2(standaloneDir, "server-entry.js");
|
|
159
|
+
const args = [
|
|
160
|
+
"build",
|
|
161
|
+
entryPoint,
|
|
162
|
+
"--production",
|
|
163
|
+
"--compile",
|
|
164
|
+
"--minify",
|
|
165
|
+
"--bytecode",
|
|
166
|
+
"--sourcemap",
|
|
167
|
+
"--define",
|
|
168
|
+
"process.env.TURBOPACK=1",
|
|
169
|
+
"--define",
|
|
170
|
+
"process.env.__NEXT_EXPERIMENTAL_REACT=",
|
|
171
|
+
"--define",
|
|
172
|
+
'process.env.NEXT_RUNTIME="nodejs"',
|
|
173
|
+
"--outfile",
|
|
174
|
+
outfile
|
|
175
|
+
];
|
|
176
|
+
console.log(`next-bun-compile: Compiling to ${outfile}...`);
|
|
177
|
+
execFileSync("bun", args, { stdio: "inherit" });
|
|
178
|
+
console.log(`next-bun-compile: Done → ${outfile}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/cli.ts
|
|
182
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
183
|
+
import { join as join3, resolve } from "node:path";
|
|
184
|
+
var projectDir = resolve(".");
|
|
185
|
+
var distDir = join3(projectDir, ".next");
|
|
186
|
+
var standaloneDir = join3(distDir, "standalone");
|
|
187
|
+
if (!existsSync2(standaloneDir)) {
|
|
188
|
+
console.error('next-bun-compile: No standalone output found. Run "next build" first with output: "standalone" in next.config.ts.');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
var ctxPath = join3(distDir, "bun-compile-ctx.json");
|
|
192
|
+
if (existsSync2(ctxPath)) {
|
|
193
|
+
console.log("next-bun-compile: Using build context from adapter");
|
|
194
|
+
}
|
|
195
|
+
generateEntryPoint({ standaloneDir, distDir, projectDir });
|
|
196
|
+
compile({ standaloneDir, outfile: join3(projectDir, "server") });
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/compile.ts
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
function compile(options) {
|
|
8
|
+
const { standaloneDir, outfile } = options;
|
|
9
|
+
const entryPoint = join(standaloneDir, "server-entry.js");
|
|
10
|
+
const args = [
|
|
11
|
+
"build",
|
|
12
|
+
entryPoint,
|
|
13
|
+
"--production",
|
|
14
|
+
"--compile",
|
|
15
|
+
"--minify",
|
|
16
|
+
"--bytecode",
|
|
17
|
+
"--sourcemap",
|
|
18
|
+
"--define",
|
|
19
|
+
"process.env.TURBOPACK=1",
|
|
20
|
+
"--define",
|
|
21
|
+
"process.env.__NEXT_EXPERIMENTAL_REACT=",
|
|
22
|
+
"--define",
|
|
23
|
+
'process.env.NEXT_RUNTIME="nodejs"',
|
|
24
|
+
"--outfile",
|
|
25
|
+
outfile
|
|
26
|
+
];
|
|
27
|
+
console.log(`next-bun-compile: Compiling to ${outfile}...`);
|
|
28
|
+
execFileSync("bun", args, { stdio: "inherit" });
|
|
29
|
+
console.log(`next-bun-compile: Done → ${outfile}`);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
compile
|
|
33
|
+
};
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/generate.ts
|
|
5
|
+
import {
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
statSync,
|
|
11
|
+
mkdirSync
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join, relative } from "node:path";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
function walkDir(dir, base = dir) {
|
|
16
|
+
const results = [];
|
|
17
|
+
if (!existsSync(dir))
|
|
18
|
+
return results;
|
|
19
|
+
for (const entry of readdirSync(dir)) {
|
|
20
|
+
const full = join(dir, entry);
|
|
21
|
+
if (statSync(full).isDirectory()) {
|
|
22
|
+
results.push(...walkDir(full, base));
|
|
23
|
+
} else {
|
|
24
|
+
results.push({ absolutePath: full, relativePath: relative(base, full) });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
function toVarName(filePath) {
|
|
30
|
+
const hash = createHash("md5").update(filePath).digest("hex").slice(0, 6);
|
|
31
|
+
const safe = filePath.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
|
|
32
|
+
return `asset_${safe}_${hash}`;
|
|
33
|
+
}
|
|
34
|
+
function generateStubs(standaloneDir) {
|
|
35
|
+
const stubs = [
|
|
36
|
+
{
|
|
37
|
+
path: "node_modules/next/dist/server/dev/next-dev-server.js",
|
|
38
|
+
content: "module.exports = { default: null };"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: "node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.js",
|
|
42
|
+
content: "module.exports = {};"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
path: "node_modules/@opentelemetry/api/index.js",
|
|
46
|
+
content: "throw new Error('not installed');"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: "node_modules/critters/index.js",
|
|
50
|
+
content: "module.exports = {};"
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const stub of stubs) {
|
|
55
|
+
const fullPath = join(standaloneDir, stub.path);
|
|
56
|
+
if (!existsSync(fullPath)) {
|
|
57
|
+
const dir = join(fullPath, "..");
|
|
58
|
+
if (!existsSync(dir)) {
|
|
59
|
+
mkdirSync(dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
writeFileSync(fullPath, stub.content);
|
|
62
|
+
count++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (count > 0) {
|
|
66
|
+
console.log(`next-bun-compile: Created ${count} module stubs`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function generateEntryPoint(options) {
|
|
70
|
+
const { standaloneDir, distDir, projectDir } = options;
|
|
71
|
+
generateStubs(standaloneDir);
|
|
72
|
+
const staticDir = join(distDir, "static");
|
|
73
|
+
const staticFiles = walkDir(staticDir).map((f) => ({
|
|
74
|
+
...f,
|
|
75
|
+
urlPath: `/_next/static/${f.relativePath}`
|
|
76
|
+
}));
|
|
77
|
+
const publicDir = join(projectDir, "public");
|
|
78
|
+
const publicFiles = walkDir(publicDir).map((f) => ({
|
|
79
|
+
...f,
|
|
80
|
+
urlPath: `/${f.relativePath}`
|
|
81
|
+
}));
|
|
82
|
+
const allAssets = [...staticFiles, ...publicFiles];
|
|
83
|
+
console.log(`next-bun-compile: Found ${staticFiles.length} static + ${publicFiles.length} public = ${allAssets.length} assets`);
|
|
84
|
+
const imports = [];
|
|
85
|
+
const mapEntries = [];
|
|
86
|
+
for (const asset of allAssets) {
|
|
87
|
+
const varName = toVarName(asset.urlPath);
|
|
88
|
+
const importPath = relative(standaloneDir, asset.absolutePath).replace(/\\/g, "/");
|
|
89
|
+
imports.push(`import ${varName} from "./${importPath}" with { type: "file" };`);
|
|
90
|
+
mapEntries.push(` ["${asset.urlPath}", ${varName}],`);
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(join(standaloneDir, "assets.generated.js"), `${imports.join(`
|
|
93
|
+
`)}
|
|
94
|
+
export const assetMap = new Map([
|
|
95
|
+
${mapEntries.join(`
|
|
96
|
+
`)}
|
|
97
|
+
]);
|
|
98
|
+
`);
|
|
99
|
+
const standaloneServerSrc = readFileSync(join(standaloneDir, "server.js"), "utf-8");
|
|
100
|
+
const configMatch = standaloneServerSrc.match(/const nextConfig = ({[\s\S]*?})\n/);
|
|
101
|
+
if (!configMatch) {
|
|
102
|
+
throw new Error("next-bun-compile: Could not extract nextConfig from standalone server.js");
|
|
103
|
+
}
|
|
104
|
+
const assetExtractions = allAssets.map((a) => {
|
|
105
|
+
const diskPath = a.urlPath.startsWith("/_next/static/") ? ".next/static/" + a.relativePath : "public/" + a.relativePath;
|
|
106
|
+
return [a.urlPath, diskPath];
|
|
107
|
+
});
|
|
108
|
+
const serverEntry = `import { assetMap } from "./assets.generated.js";
|
|
109
|
+
const path = require("path");
|
|
110
|
+
const fs = require("fs");
|
|
111
|
+
|
|
112
|
+
const baseDir = path.dirname(process.execPath);
|
|
113
|
+
process.chdir(baseDir);
|
|
114
|
+
process.env.NODE_ENV = "production";
|
|
115
|
+
|
|
116
|
+
const nextConfig = ${configMatch[1]};
|
|
117
|
+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
|
|
118
|
+
|
|
119
|
+
const currentPort = parseInt(process.env.PORT, 10) || 3000;
|
|
120
|
+
const hostname = process.env.HOSTNAME || "0.0.0.0";
|
|
121
|
+
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
|
|
122
|
+
if (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
|
|
123
|
+
keepAliveTimeout = undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const extractions = ${JSON.stringify(assetExtractions)};
|
|
127
|
+
async function extractAssets() {
|
|
128
|
+
let n = 0;
|
|
129
|
+
for (const [urlPath, diskPath] of extractions) {
|
|
130
|
+
const fullPath = path.join(baseDir, diskPath);
|
|
131
|
+
if (fs.existsSync(fullPath)) continue;
|
|
132
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
133
|
+
const embedded = assetMap.get(urlPath);
|
|
134
|
+
if (embedded) { await Bun.write(fullPath, Bun.file(embedded)); n++; }
|
|
135
|
+
}
|
|
136
|
+
if (n > 0) console.log(\`Extracted \${n} assets\`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
extractAssets().then(() => {
|
|
140
|
+
require("next");
|
|
141
|
+
const { startServer } = require("next/dist/server/lib/start-server");
|
|
142
|
+
return startServer({
|
|
143
|
+
dir: baseDir, isDev: false, config: nextConfig,
|
|
144
|
+
hostname, port: currentPort, allowRetry: false, keepAliveTimeout,
|
|
145
|
+
});
|
|
146
|
+
}).catch((err) => { console.error(err); process.exit(1); });
|
|
147
|
+
`;
|
|
148
|
+
writeFileSync(join(standaloneDir, "server-entry.js"), serverEntry);
|
|
149
|
+
console.log("next-bun-compile: Generated server-entry.js + assets.generated.js");
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
generateEntryPoint
|
|
153
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/generate.ts
|
|
5
|
+
import {
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
statSync,
|
|
11
|
+
mkdirSync
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join, relative } from "node:path";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
function walkDir(dir, base = dir) {
|
|
16
|
+
const results = [];
|
|
17
|
+
if (!existsSync(dir))
|
|
18
|
+
return results;
|
|
19
|
+
for (const entry of readdirSync(dir)) {
|
|
20
|
+
const full = join(dir, entry);
|
|
21
|
+
if (statSync(full).isDirectory()) {
|
|
22
|
+
results.push(...walkDir(full, base));
|
|
23
|
+
} else {
|
|
24
|
+
results.push({ absolutePath: full, relativePath: relative(base, full) });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
function toVarName(filePath) {
|
|
30
|
+
const hash = createHash("md5").update(filePath).digest("hex").slice(0, 6);
|
|
31
|
+
const safe = filePath.replace(/[^a-zA-Z0-9]/g, "_").slice(0, 40);
|
|
32
|
+
return `asset_${safe}_${hash}`;
|
|
33
|
+
}
|
|
34
|
+
function generateStubs(standaloneDir) {
|
|
35
|
+
const stubs = [
|
|
36
|
+
{
|
|
37
|
+
path: "node_modules/next/dist/server/dev/next-dev-server.js",
|
|
38
|
+
content: "module.exports = { default: null };"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: "node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.js",
|
|
42
|
+
content: "module.exports = {};"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
path: "node_modules/@opentelemetry/api/index.js",
|
|
46
|
+
content: "throw new Error('not installed');"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: "node_modules/critters/index.js",
|
|
50
|
+
content: "module.exports = {};"
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const stub of stubs) {
|
|
55
|
+
const fullPath = join(standaloneDir, stub.path);
|
|
56
|
+
if (!existsSync(fullPath)) {
|
|
57
|
+
const dir = join(fullPath, "..");
|
|
58
|
+
if (!existsSync(dir)) {
|
|
59
|
+
mkdirSync(dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
writeFileSync(fullPath, stub.content);
|
|
62
|
+
count++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (count > 0) {
|
|
66
|
+
console.log(`next-bun-compile: Created ${count} module stubs`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function generateEntryPoint(options) {
|
|
70
|
+
const { standaloneDir, distDir, projectDir } = options;
|
|
71
|
+
generateStubs(standaloneDir);
|
|
72
|
+
const staticDir = join(distDir, "static");
|
|
73
|
+
const staticFiles = walkDir(staticDir).map((f) => ({
|
|
74
|
+
...f,
|
|
75
|
+
urlPath: `/_next/static/${f.relativePath}`
|
|
76
|
+
}));
|
|
77
|
+
const publicDir = join(projectDir, "public");
|
|
78
|
+
const publicFiles = walkDir(publicDir).map((f) => ({
|
|
79
|
+
...f,
|
|
80
|
+
urlPath: `/${f.relativePath}`
|
|
81
|
+
}));
|
|
82
|
+
const allAssets = [...staticFiles, ...publicFiles];
|
|
83
|
+
console.log(`next-bun-compile: Found ${staticFiles.length} static + ${publicFiles.length} public = ${allAssets.length} assets`);
|
|
84
|
+
const imports = [];
|
|
85
|
+
const mapEntries = [];
|
|
86
|
+
for (const asset of allAssets) {
|
|
87
|
+
const varName = toVarName(asset.urlPath);
|
|
88
|
+
const importPath = relative(standaloneDir, asset.absolutePath).replace(/\\/g, "/");
|
|
89
|
+
imports.push(`import ${varName} from "./${importPath}" with { type: "file" };`);
|
|
90
|
+
mapEntries.push(` ["${asset.urlPath}", ${varName}],`);
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(join(standaloneDir, "assets.generated.js"), `${imports.join(`
|
|
93
|
+
`)}
|
|
94
|
+
export const assetMap = new Map([
|
|
95
|
+
${mapEntries.join(`
|
|
96
|
+
`)}
|
|
97
|
+
]);
|
|
98
|
+
`);
|
|
99
|
+
const standaloneServerSrc = readFileSync(join(standaloneDir, "server.js"), "utf-8");
|
|
100
|
+
const configMatch = standaloneServerSrc.match(/const nextConfig = ({[\s\S]*?})\n/);
|
|
101
|
+
if (!configMatch) {
|
|
102
|
+
throw new Error("next-bun-compile: Could not extract nextConfig from standalone server.js");
|
|
103
|
+
}
|
|
104
|
+
const assetExtractions = allAssets.map((a) => {
|
|
105
|
+
const diskPath = a.urlPath.startsWith("/_next/static/") ? ".next/static/" + a.relativePath : "public/" + a.relativePath;
|
|
106
|
+
return [a.urlPath, diskPath];
|
|
107
|
+
});
|
|
108
|
+
const serverEntry = `import { assetMap } from "./assets.generated.js";
|
|
109
|
+
const path = require("path");
|
|
110
|
+
const fs = require("fs");
|
|
111
|
+
|
|
112
|
+
const baseDir = path.dirname(process.execPath);
|
|
113
|
+
process.chdir(baseDir);
|
|
114
|
+
process.env.NODE_ENV = "production";
|
|
115
|
+
|
|
116
|
+
const nextConfig = ${configMatch[1]};
|
|
117
|
+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
|
|
118
|
+
|
|
119
|
+
const currentPort = parseInt(process.env.PORT, 10) || 3000;
|
|
120
|
+
const hostname = process.env.HOSTNAME || "0.0.0.0";
|
|
121
|
+
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
|
|
122
|
+
if (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
|
|
123
|
+
keepAliveTimeout = undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const extractions = ${JSON.stringify(assetExtractions)};
|
|
127
|
+
async function extractAssets() {
|
|
128
|
+
let n = 0;
|
|
129
|
+
for (const [urlPath, diskPath] of extractions) {
|
|
130
|
+
const fullPath = path.join(baseDir, diskPath);
|
|
131
|
+
if (fs.existsSync(fullPath)) continue;
|
|
132
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
133
|
+
const embedded = assetMap.get(urlPath);
|
|
134
|
+
if (embedded) { await Bun.write(fullPath, Bun.file(embedded)); n++; }
|
|
135
|
+
}
|
|
136
|
+
if (n > 0) console.log(\`Extracted \${n} assets\`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
extractAssets().then(() => {
|
|
140
|
+
require("next");
|
|
141
|
+
const { startServer } = require("next/dist/server/lib/start-server");
|
|
142
|
+
return startServer({
|
|
143
|
+
dir: baseDir, isDev: false, config: nextConfig,
|
|
144
|
+
hostname, port: currentPort, allowRetry: false, keepAliveTimeout,
|
|
145
|
+
});
|
|
146
|
+
}).catch((err) => { console.error(err); process.exit(1); });
|
|
147
|
+
`;
|
|
148
|
+
writeFileSync(join(standaloneDir, "server-entry.js"), serverEntry);
|
|
149
|
+
console.log("next-bun-compile: Generated server-entry.js + assets.generated.js");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/compile.ts
|
|
153
|
+
import { execFileSync } from "node:child_process";
|
|
154
|
+
import { join as join2 } from "node:path";
|
|
155
|
+
function compile(options) {
|
|
156
|
+
const { standaloneDir, outfile } = options;
|
|
157
|
+
const entryPoint = join2(standaloneDir, "server-entry.js");
|
|
158
|
+
const args = [
|
|
159
|
+
"build",
|
|
160
|
+
entryPoint,
|
|
161
|
+
"--production",
|
|
162
|
+
"--compile",
|
|
163
|
+
"--minify",
|
|
164
|
+
"--bytecode",
|
|
165
|
+
"--sourcemap",
|
|
166
|
+
"--define",
|
|
167
|
+
"process.env.TURBOPACK=1",
|
|
168
|
+
"--define",
|
|
169
|
+
"process.env.__NEXT_EXPERIMENTAL_REACT=",
|
|
170
|
+
"--define",
|
|
171
|
+
'process.env.NEXT_RUNTIME="nodejs"',
|
|
172
|
+
"--outfile",
|
|
173
|
+
outfile
|
|
174
|
+
];
|
|
175
|
+
console.log(`next-bun-compile: Compiling to ${outfile}...`);
|
|
176
|
+
execFileSync("bun", args, { stdio: "inherit" });
|
|
177
|
+
console.log(`next-bun-compile: Done → ${outfile}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/index.ts
|
|
181
|
+
import { join as join3 } from "node:path";
|
|
182
|
+
var adapter = {
|
|
183
|
+
name: "next-bun-compile",
|
|
184
|
+
modifyConfig(config) {
|
|
185
|
+
if (config.output !== "standalone") {
|
|
186
|
+
console.warn('next-bun-compile: Setting output to "standalone" (required for compilation)');
|
|
187
|
+
config.output = "standalone";
|
|
188
|
+
}
|
|
189
|
+
return config;
|
|
190
|
+
},
|
|
191
|
+
async onBuildComplete(ctx) {
|
|
192
|
+
const { writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
193
|
+
writeFileSync2(join3(ctx.distDir, "bun-compile-ctx.json"), JSON.stringify({
|
|
194
|
+
distDir: ctx.distDir,
|
|
195
|
+
projectDir: ctx.projectDir
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var src_default = adapter;
|
|
200
|
+
export {
|
|
201
|
+
generateEntryPoint,
|
|
202
|
+
src_default as default,
|
|
203
|
+
compile
|
|
204
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "next-bun-compile",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Next.js Build Adapter that compiles your app into a Bun single-file executable",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"next-bun-compile": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build src/index.ts src/generate.ts src/compile.ts src/cli.ts --outdir dist --target node",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"next": ">=16.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"next": "16.1.6",
|
|
29
|
+
"@types/node": "^20",
|
|
30
|
+
"typescript": "^5"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"nextjs",
|
|
34
|
+
"bun",
|
|
35
|
+
"compile",
|
|
36
|
+
"standalone",
|
|
37
|
+
"executable",
|
|
38
|
+
"adapter"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"homepage": "https://github.com/ramonmalcolm10/next-bun-compile#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/ramonmalcolm10/next-bun-compile/issues"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/ramonmalcolm10/next-bun-compile.git"
|
|
48
|
+
}
|
|
49
|
+
}
|