mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
E4: Windows node-server bundle keeps sharp and socks external; CLI resolves its server script lazily (#2260, #2439)
build-node-server.sh passes --external sharp and --external socks (Bun on Windows refuses to inline them; on Linux they were silently inlined). The CLI no longer resolves server.ts or server-node.mjs at module load: resolveServerLaunch() runs when a server starts, so a Windows minimal runtime with only server-node.mjs works and a missing script is reported only when one is needed. Co-authored-by: Spooks444 <193448029+Spooks444@users.noreply.github.com> Co-authored-by: punksterlabs <77217235+punksterlabs@users.noreply.github.com>
This commit is contained in:
1 parent
46f4f52f44
commit
5f38b93fdf
4 files changed
+97
-15
No files matched your search
@@ -21,7 +21,9 @@ echo "Building Node-compatible server bundle..."
|
||||
|
||||
# Step 1: Transpile server.ts to a single .mjs bundle (externalize runtime deps)
|
||||
#
|
||||
# Externalize packages with native addons, dynamic imports, or runtime resolution.
|
||||
# Externalize packages with native addons, dynamic imports, or runtime resolution
|
||||
# (sharp loads a platform .node addon; socks is resolved at runtime, and Bun on
|
||||
# Windows refuses to inline either, #2260).
|
||||
# If you add a new dependency that uses `await import()` or has a .node addon,
|
||||
# add it here. Otherwise `bun build --outfile` will fail with
|
||||
# "cannot write multiple output files without an output directory".
|
||||
@@ -32,7 +34,9 @@ bun build "$SRC_DIR/server.ts" \
|
||||
--external playwright-core \
|
||||
--external diff \
|
||||
--external "bun:sqlite" \
|
||||
--external "@ngrok/ngrok"
|
||||
--external "@ngrok/ngrok" \
|
||||
--external socks \
|
||||
--external sharp
|
||||
|
||||
# Step 2: Post-process
|
||||
# Replace import.meta.dir with a resolvable reference
|
||||
|
||||
+22
-13
@@ -84,8 +84,6 @@ export function resolveServerScript(
|
||||
);
|
||||
}
|
||||
|
||||
const SERVER_SCRIPT = resolveServerScript();
|
||||
|
||||
/**
|
||||
* On Windows, resolve the Node.js-compatible server bundle.
|
||||
* Falls back to null if not found (server will use Bun instead).
|
||||
@@ -109,13 +107,23 @@ export function resolveNodeServerScript(
|
||||
return null;
|
||||
}
|
||||
|
||||
const NODE_SERVER_SCRIPT = IS_WINDOWS ? resolveNodeServerScript() : null;
|
||||
|
||||
// On Windows, hard-fail if server-node.mjs is missing — the Bun path is known broken.
|
||||
if (IS_WINDOWS && !NODE_SERVER_SCRIPT) {
|
||||
throw new Error(
|
||||
'server-node.mjs not found. Run `bun run build` to generate the Windows server bundle.'
|
||||
);
|
||||
/**
|
||||
* Which server to start, resolved only when a server is actually started
|
||||
* (#2439). Windows runs the Node bundle and never needs server.ts, which a
|
||||
* minimal runtime root does not ship; resolving server.ts at module load made
|
||||
* every command, even --help, fail there.
|
||||
*/
|
||||
export function resolveServerLaunch(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
metaDir: string = import.meta.dir,
|
||||
execPath: string = process.execPath,
|
||||
): { runtime: 'node' | 'bun'; script: string } {
|
||||
if (platform !== 'win32') return { runtime: 'bun', script: resolveServerScript(env, metaDir, execPath) };
|
||||
// On Windows, hard-fail if server-node.mjs is missing — the Bun path is known broken.
|
||||
const script = resolveNodeServerScript(metaDir, execPath);
|
||||
if (!script) throw new Error('server-node.mjs not found. Run `bun run build` to generate the Windows server bundle.');
|
||||
return { runtime: 'node', script };
|
||||
}
|
||||
|
||||
interface ServerState {
|
||||
@@ -591,6 +599,7 @@ function openDaemonLogSink(): number | 'ignore' {
|
||||
}
|
||||
|
||||
async function startServer(extraEnv?: Record<string, string>): Promise<ServerState> {
|
||||
const server = resolveServerLaunch();
|
||||
ensureStateDir(config);
|
||||
|
||||
// Bound the append-mode daemon log before the new daemon starts writing.
|
||||
@@ -625,7 +634,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
const parentPid = parseInt(process.env.BROWSE_PARENT_PID || '', 10) === 0 ? '0' : String(process.pid);
|
||||
let spawnedServer: { pid: number; startTime: string } | null = null;
|
||||
|
||||
if (IS_WINDOWS && NODE_SERVER_SCRIPT) {
|
||||
if (server.runtime === 'node') {
|
||||
// Windows: Bun.spawn() + proc.unref() doesn't truly detach on Windows —
|
||||
// when the CLI exits, the server dies with it. Use Node's child_process.spawn
|
||||
// with { detached: true } instead, which is the gold standard for Windows
|
||||
@@ -641,7 +650,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
`const{spawn}=require('child_process');` +
|
||||
`const fs=require('fs');` +
|
||||
`let logFd;try{logFd=fs.openSync(${daemonLogPathStr},'a');}catch(e){logFd='ignore';}` +
|
||||
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
|
||||
`spawn(process.execPath,[${JSON.stringify(server.script)}],` +
|
||||
`{detached:true,windowsHide:true,stdio:['ignore',logFd,logFd],env:Object.assign({},process.env,` +
|
||||
`${extraEnvStr})}).unref()`;
|
||||
Bun.spawnSync(['node', '-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true });
|
||||
@@ -658,7 +667,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
// (PPID=1, STAT=Ss) and survives the spawning shell's exit. Mirrors
|
||||
// the Windows path's rationale — same root cause, different OS API.
|
||||
const daemonLogFd = openDaemonLogSink();
|
||||
const child = nodeSpawn('bun', [...BUN_CHILD_FLAGS, 'run', SERVER_SCRIPT], {
|
||||
const child = nodeSpawn('bun', [...BUN_CHILD_FLAGS, 'run', server.script], {
|
||||
detached: true,
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', daemonLogFd, daemonLogFd],
|
||||
@@ -693,7 +702,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
|
||||
if (spawnedServer?.startTime) {
|
||||
const { pid, startTime } = spawnedServer;
|
||||
const stillOurs = () => readPidStartTime(pid) === startTime && readPidCmdline(pid).split(/\s+/).includes(SERVER_SCRIPT);
|
||||
const stillOurs = () => readPidStartTime(pid) === startTime && readPidCmdline(pid).split(/\s+/).includes(server.script);
|
||||
if (stillOurs()) {
|
||||
safeKill(pid, 'SIGTERM');
|
||||
const deadline = Date.now() + 500;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, test, expect } from 'bun:test';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
const DIST_DIR = path.resolve(__dirname, '..', 'dist');
|
||||
const SERVER_NODE = path.join(DIST_DIR, 'server-node.mjs');
|
||||
@@ -26,3 +27,22 @@ describe('build: server-node.mjs', () => {
|
||||
expect(bundle).not.toMatch(/ngrok_napi|ngrokNapi|@ngrok\/ngrok-darwin|@ngrok\/ngrok-linux|@ngrok\/ngrok-win32/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('build: node server bundle externals (#2260)', () => {
|
||||
test('sharp and socks stay external, so their native and runtime-resolved code is loaded from node_modules', () => {
|
||||
const script = fs.readFileSync(path.resolve(__dirname, '..', 'scripts', 'build-node-server.sh'), 'utf-8');
|
||||
const externals = [...script.matchAll(/--external\s+"?([^"\s\\]+)"?/g)].map((m) => m[1]);
|
||||
expect(externals).toEqual(expect.arrayContaining(['sharp', 'socks']));
|
||||
const out = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-node-bundle-'));
|
||||
try {
|
||||
const outfile = path.join(out, 'server-node.mjs');
|
||||
const r = Bun.spawnSync([process.execPath, 'build', path.resolve(__dirname, '..', 'src', 'server.ts'), '--target=node', '--outfile', outfile, ...externals.flatMap((e) => ['--external', e])], { stdout: 'pipe', stderr: 'pipe', timeout: 120_000 });
|
||||
expect(r.exitCode, r.stderr.toString()).toBe(0);
|
||||
const bundle = fs.readFileSync(outfile, 'utf-8');
|
||||
expect(bundle).not.toContain('node_modules/sharp/');
|
||||
expect(bundle).not.toContain('node_modules/socks/');
|
||||
} finally {
|
||||
fs.rmSync(out, { recursive: true, force: true });
|
||||
}
|
||||
}, 150_000);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* E4 (#2439): the CLI resolves its server script only when it starts a
|
||||
* server. A Windows minimal runtime ships browse.exe + server-node.mjs and no
|
||||
* browse/src/server.ts, which used to make every command fail at import.
|
||||
*/
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { resolveServerLaunch } from '../src/cli';
|
||||
|
||||
function withRuntime<T>(files: string[], fn: (dir: string) => T): T {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-runtime-'));
|
||||
try {
|
||||
for (const f of files) fs.writeFileSync(path.join(dir, f), '// stub\n');
|
||||
return fn(dir);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const COMPILED_META = path.join(os.tmpdir(), '$bunfs', 'root');
|
||||
|
||||
describe('resolveServerLaunch', () => {
|
||||
test('Windows minimal runtime starts the adjacent Node bundle without server.ts', () => {
|
||||
withRuntime(['server-node.mjs'], (dir) => {
|
||||
expect(resolveServerLaunch('win32', {}, COMPILED_META, path.join(dir, 'browse.exe')))
|
||||
.toEqual({ runtime: 'node', script: path.join(dir, 'server-node.mjs') });
|
||||
});
|
||||
});
|
||||
|
||||
test('Windows without the Node bundle names the build fix', () => {
|
||||
withRuntime([], (dir) => {
|
||||
expect(() => resolveServerLaunch('win32', {}, COMPILED_META, path.join(dir, 'browse.exe')))
|
||||
.toThrow('server-node.mjs not found. Run `bun run build`');
|
||||
});
|
||||
});
|
||||
|
||||
test('macOS/Linux start server.ts with Bun, honoring BROWSE_SERVER_SCRIPT', () => {
|
||||
expect(resolveServerLaunch('linux', { BROWSE_SERVER_SCRIPT: 'custom-server.ts' }, COMPILED_META, ''))
|
||||
.toEqual({ runtime: 'bun', script: 'custom-server.ts' });
|
||||
});
|
||||
|
||||
test('importing the CLI resolves nothing: a missing server is reported only when one is started', () => {
|
||||
const src = fs.readFileSync(path.join(import.meta.dir, '..', 'src', 'cli.ts'), 'utf-8');
|
||||
expect(src).not.toMatch(/^const \w+ = resolve(Server|NodeServer)\w*\(/m);
|
||||
expect(src).toMatch(/async function startServer\([^)]*\)[^{]*\{\n\s*const server = resolveServerLaunch\(\);/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user