// It was ported from `bob runify --single` command. // The idea here is to compile a node service to a single file (not in case of next) and make it executable. import { join, normalize, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; import fs from 'fs-extra'; import { build as tsup } from 'tsup'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const requireShim = fs.readFileSync(normalize(join(__dirname, './banner.js')), 'utf-8'); const entryPoints = parseArgs({ allowPositionals: true, strict: false, }).positionals; interface BuildOptions { external?: string[]; next?: { header?: string; }; } runify(normalize(join(process.cwd(), 'package.json'))); async function runify(packagePath: string) { const cwd = packagePath.replace(`${sep}package.json`, ''); const pkg = await readPackageJson(cwd); const buildOptions: BuildOptions = pkg.buildOptions || {}; console.log(`Building...`); if (isNext(pkg)) { const additionalRequire = pkg?.buildOptions?.next?.header ?? null; await buildWithNext(cwd, additionalRequire); await rewritePackageJson(pkg, cwd, newPkg => ({ ...newPkg, dependencies: pkg.dependencies, type: 'commonjs', })); } else { await compile( cwd, entryPoints?.length ? entryPoints : 'src/index.ts', buildOptions, Object.keys(pkg.dependencies ?? {}).concat(Object.keys(pkg.devDependencies ?? {})), pkg.type === 'module', ); await rewritePackageJson(pkg, cwd); } console.log(`Built!`); } export async function readPackageJson(baseDir: string) { return JSON.parse( await fs.readFile(resolve(baseDir, 'package.json'), { encoding: 'utf-8', }), ); } async function rewritePackageJson( pkg: Record, cwd: string, modify?: (pkg: any) => any, ) { let filename = 'index.js'; let newPkg: Record = { bin: filename, }; const fields = ['name', 'version', 'description', 'registry', 'repository', 'type']; fields.forEach(field => { if (typeof pkg[field] !== 'undefined') { newPkg[field] = pkg[field]; } }); if (modify) { newPkg = modify(newPkg); } await fs.writeFile(join(cwd, 'dist', 'package.json'), JSON.stringify(newPkg, null, 2), { encoding: 'utf-8', }); } function isNext(pkg: any): boolean { return pkg?.dependencies?.next || pkg?.devDependencies?.next; } async function buildWithNext(cwd: string, additionalRequire: string | null) { await fs.mkdirp(normalize(join(cwd, 'dist'))); if (additionalRequire) { await tsup({ entryPoints: [normalize(join(cwd, additionalRequire))], outDir: normalize(join(cwd, 'dist')), target: 'node22', format: ['cjs'], splitting: false, skipNodeModulesBundle: true, }); } const template = await fs.readFile(join(__dirname, 'templates/runify-next.ts'), 'utf-8'); await Promise.all([ fs.copy(join(cwd, '.next'), join(cwd, 'dist/.next'), { filter(src) { // copy without webpack cache (it's 900mb...) return src.includes('cache/webpack') === false; }, }), fs.copy(join(cwd, 'public'), join(cwd, 'dist/public')), fs.writeFile( join(cwd, 'dist/index.js'), template.replace( '// <-- additionalRequire -->', additionalRequire ? `require('${additionalRequire.replace('.ts', '')}')` : ``, ), ), ]); } async function compile( cwd: string, entryPoint: string | string[], buildOptions: BuildOptions, dependencies: string[], useEsm = false, ) { const out = normalize(join(cwd, 'dist')); await tsup({ entryPoints: (Array.isArray(entryPoint) ? entryPoint : [entryPoint]).map(entryPoint => normalize(join(cwd, entryPoint)), ), outDir: out, target: 'node22', format: [useEsm ? 'esm' : 'cjs'], splitting: false, sourcemap: true, clean: true, shims: true, skipNodeModulesBundle: false, // noExternal: dependencies, external: buildOptions.external, banner: { js: requireShim, }, }); }