92 lines
6.5 KiB
TypeScript
92 lines
6.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Builds the npm package's CLI: one Node ESM bundle, dist/storebound.mjs (package.json "bin"), so `npx storebound`
|
|
// runs on Node 20+ and `bunx storebound` keeps working. Zero runtime dependencies: playwright stays an optional peer
|
|
// resolved from the project at run time (playwright-core only in the compiled sidecar), so it is left out.
|
|
// bun tools/build-node.ts write dist/storebound.mjs (npm pack / publish run it: prepack)
|
|
// bun tools/build-node.ts --out <f> write elsewhere (tests)
|
|
// The bundle is checked for this machine's home and checkout paths (like the sidecar): nothing private may ship.
|
|
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
import { privateStrings, ROOT } from './build-sidecars';
|
|
|
|
/** Packages the CLI only imports at run time from the project (or never under Node). */
|
|
export const NODE_EXTERNALS = ['playwright', 'playwright-core', 'electron', 'chromium-bidi'];
|
|
export const NODE_BUNDLE = join(ROOT, 'dist', 'storebound.mjs');
|
|
|
|
export function buildNode(out = NODE_BUNDLE): { path: string; bytes: number } {
|
|
const dir = mkdtempSync(join(tmpdir(), 'sb-node-'));
|
|
try {
|
|
const tmp = join(dir, 'storebound.mjs');
|
|
const p = Bun.spawnSync(['bun', 'build', 'cli/index.ts', '--target=node', '--format=esm', ...NODE_EXTERNALS.flatMap(e => ['--external', e]), '--outfile', tmp],
|
|
{ cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
|
|
if (p.exitCode !== 0) throw new Error(`bun build failed\n${p.stderr.toString()}${p.stdout.toString()}`);
|
|
// the entry's own `#!/usr/bin/env bun` and Bun's `// @bun` marker go; node runs this file
|
|
const body = readFileSync(tmp, 'utf8').replace(/^#!.*\n/, '').replace(/^\/\/ @bun[^\n]*\n/, '');
|
|
const js = `#!/usr/bin/env node\n${body}`;
|
|
const bunGlobal = /\bBun\.[a-zA-Z]/.exec(js);
|
|
if (bunGlobal) throw new Error(`the Node bundle uses the Bun global (${bunGlobal[0]}...): go through cli/lib/rt.ts`);
|
|
const leaked = privateStrings().filter(s => js.includes(s));
|
|
if (leaked.length) throw new Error(`the Node bundle contains private paths (${leaked.length}); refusing to write it`);
|
|
mkdirSync(dirname(out), { recursive: true });
|
|
writeFileSync(out, js);
|
|
chmodSync(out, 0o755);
|
|
return { path: out, bytes: statSync(out).size };
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
}
|
|
|
|
/** The package's import entry ('storebound', the ShipConfig types of storebound.config.ts): declarations in
|
|
* dist/types for tsc (next build, vue-tsc, ts-loader: TypeScript never takes a package's .ts source as its types) and
|
|
* dist/config.js for runtimes other than Bun (Bun keeps importing cli/lib/config.ts). */
|
|
export function buildEntry(dir = join(ROOT, 'dist')): void {
|
|
const types = join(dir, 'types');
|
|
rmSync(types, { recursive: true, force: true });
|
|
if (!statSync(join(ROOT, 'node_modules', '.bin', 'tsc'), { throwIfNoEntry: false })) throw new Error('no TypeScript in node_modules: run `bun install` first (npm pack builds the declarations with it)');
|
|
const tsc = Bun.spawnSync([join(ROOT, 'node_modules', '.bin', 'tsc'), '-p', 'tsconfig.types.json', '--outDir', types], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
|
|
if (tsc.exitCode !== 0) throw new Error(`tsc (declarations) failed\n${tsc.stdout.toString()}${tsc.stderr.toString()}`);
|
|
if (!statSync(join(types, 'lib', 'config.d.ts'), { throwIfNoEntry: false })) throw new Error('no dist/types/lib/config.d.ts');
|
|
pruneDeclarations(types, 'lib/config.d.ts');
|
|
// the same declarations for CommonJS projects (no "type": "module"): node16 / nodenext read a .d.ts as ESM or CJS
|
|
// from the nearest package.json, and a CJS file may not type-import an ESM-only declaration (TS1541)
|
|
const cjs = join(dir, 'types-cjs');
|
|
rmSync(cjs, { recursive: true, force: true });
|
|
cpSync(types, cjs, { recursive: true });
|
|
writeFileSync(join(cjs, 'package.json'), '{ "type": "commonjs" }\n');
|
|
const js = Bun.spawnSync(['bun', 'build', 'cli/lib/config.ts', '--target=node', '--format=esm', ...NODE_EXTERNALS.flatMap(e => ['--external', e]), '--outfile', join(dir, 'config.js')], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
|
|
if (js.exitCode !== 0) throw new Error(`bun build (config entry) failed\n${js.stderr.toString()}`);
|
|
const leaked = privateStrings().filter(s => readFileSync(join(dir, 'config.js'), 'utf8').includes(s));
|
|
if (leaked.length) throw new Error(`dist/config.js contains private paths (${leaked.length})`);
|
|
}
|
|
|
|
/** Keeps the declarations reachable from `entry` (tsc emits one per file of the program) and makes them stand alone
|
|
* in any project: relative imports get `.js` (node16 / nodenext need it), and `Buffer` becomes a local alias of
|
|
* Uint8Array where the project may not have @types/node. Pure on the folder; returns the files kept. */
|
|
export function pruneDeclarations(dir: string, entry: string): string[] {
|
|
const keep = new Set<string>();
|
|
const todo = [entry];
|
|
const spec = /(from\s+|import\()(['"])(\.{1,2}\/[^'"]+?)(?:\.js)?\2/g;
|
|
while (todo.length) {
|
|
const f = todo.pop()!;
|
|
if (keep.has(f)) continue;
|
|
keep.add(f);
|
|
for (const m of readFileSync(join(dir, f), 'utf8').matchAll(spec)) todo.push(join(dirname(f), `${m[3]}.d.ts`));
|
|
}
|
|
const walk = (d: string): string[] => readdirSync(join(dir, d), { withFileTypes: true }).flatMap(e => e.isDirectory() ? walk(join(d, e.name)) : [join(d, e.name)]);
|
|
for (const f of walk('.')) {
|
|
if (!keep.has(f)) { rmSync(join(dir, f)); continue; }
|
|
let t = readFileSync(join(dir, f), 'utf8').replace(spec, (_m, kw: string, q: string, p: string) => `${kw}${q}${p}.js${q}`);
|
|
if (/\bBuffer\b/.test(t)) t = `type Buffer = Uint8Array;\n${t}`;
|
|
if (/['"]node:|\bBun\./.test(t.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ''))) throw new Error(`${f}: the package's declarations need Node or Bun types`);
|
|
writeFileSync(join(dir, f), t);
|
|
}
|
|
for (const d of walk('.').map(f => dirname(f))) if (!readdirSync(join(dir, d)).length) rmSync(join(dir, d), { recursive: true });
|
|
return [...keep].sort();
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
const i = process.argv.indexOf('--out');
|
|
const t = Date.now();
|
|
const r = buildNode(i >= 0 ? resolve(process.argv[i + 1]!) : NODE_BUNDLE);
|
|
console.log(`node bundle -> ${relative(process.cwd(), r.path) || r.path} (${(r.bytes / 1e6).toFixed(2)} MB, ${((Date.now() - t) / 1000).toFixed(1)}s)`);
|
|
if (i < 0) { buildEntry(); console.log('package entry -> dist/types (declarations), dist/config.js'); }
|
|
}
|