bob-the-bundler
Version:
Bob The Bundler!
225 lines (224 loc) • 8.95 kB
JavaScript
import { spawn } from 'child_process';
import { dirname, join, resolve } from 'path';
import { DepGraph } from 'dependency-graph';
import fse from 'fs-extra';
import fs from 'fs-extra';
import { globby } from 'globby';
import pLimit from 'p-limit';
import { build as tsup } from 'tsup';
import ncc from '@vercel/ncc';
import { createCommand } from '../command.js';
export const distDir = 'dist';
export const runifyCommand = createCommand(api => {
const { reporter } = api;
return {
command: 'runify',
describe: 'Runify',
builder(yargs) {
return yargs.options({
tag: {
describe: 'Run only for following tags',
array: true,
type: 'string',
},
});
},
async handler({ tag }) {
const [rootPackageJSONPath] = await globby('package.json', {
cwd: process.cwd(),
absolute: true,
});
const rootPackageJSON = await fse.readJSON(rootPackageJSONPath);
const isSinglePackage = Array.isArray(rootPackageJSON.workspaces) === false;
if (isSinglePackage) {
return runify(join(process.cwd(), 'package.json'), reporter);
}
const limit = pLimit(1);
const packageJsonFiles = await globby('packages/**/package.json', {
cwd: process.cwd(),
absolute: true,
ignore: ['**/node_modules/**', `**/${distDir}/**`],
});
const packageJsons = await Promise.all(packageJsonFiles.map(packagePath => fs.readJSON(packagePath)));
const depGraph = new DepGraph();
packageJsons.forEach((pkg, i) => {
var _a, _b;
depGraph.addNode(pkg.name, {
path: packageJsonFiles[i],
tags: (_b = (_a = pkg.buildOptions) === null || _a === void 0 ? void 0 : _a.tags) !== null && _b !== void 0 ? _b : [],
});
});
packageJsons.forEach(pkg => {
var _a, _b, _c;
[
...Object.keys((_a = pkg.dependencies) !== null && _a !== void 0 ? _a : {}),
...Object.keys((_b = pkg.peerDependencies) !== null && _b !== void 0 ? _b : {}),
...Object.keys((_c = pkg.devDependencies) !== null && _c !== void 0 ? _c : {}),
]
.filter(dep => depGraph.hasNode(dep))
.forEach(dep => {
depGraph.addDependency(pkg.name, dep);
});
});
const ordered = depGraph.overallOrder();
await Promise.all(ordered.map(name => {
const data = depGraph.getNodeData(name);
if (tag) {
if (!data.tags.some(t => tag.includes(t))) {
return;
}
}
return limit(() => runify(depGraph.getNodeData(name).path, reporter));
}));
},
};
});
async function runify(packagePath, reporter) {
var _a, _b, _c, _d, _e, _f;
const cwd = packagePath.replace('/package.json', '');
const pkg = await readPackageJson(cwd);
const buildOptions = pkg.buildOptions || {};
if (!buildOptions.runify) {
return;
}
if (isNext(pkg)) {
const additionalRequire = (_d = (_c = (_b = (_a = pkg === null || pkg === void 0 ? void 0 : pkg.buildOptions) === null || _a === void 0 ? void 0 : _a.runify) === null || _b === void 0 ? void 0 : _b.next) === null || _c === void 0 ? void 0 : _c.header) !== null && _d !== void 0 ? _d : null;
await buildNext(cwd, additionalRequire);
await rewritePackageJson(pkg, cwd, newPkg => ({
...newPkg,
dependencies: pkg.dependencies,
type: 'commonjs',
}));
}
else {
await compile(cwd, (_e = buildOptions.bin) !== null && _e !== void 0 ? _e : 'src/index.ts', buildOptions, Object.keys((_f = pkg.dependencies) !== null && _f !== void 0 ? _f : {}), pkg.type === 'module');
await rewritePackageJson(pkg, cwd);
}
reporter.success(`Built ${pkg.name}`);
}
export async function readPackageJson(baseDir) {
return JSON.parse(await fs.readFile(resolve(baseDir, 'package.json'), {
encoding: 'utf-8',
}));
}
async function rewritePackageJson(pkg, cwd, modify) {
let filename = 'index.js';
// only tsup keep the file name
if (pkg.buildOptions.bin && pkg.buildOptions.tsup) {
const bits = pkg.buildOptions.bin.split('/');
const lastBit = bits[bits.length - 1];
filename = lastBit.replace('.ts', '.js');
}
let newPkg = {
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) {
var _a, _b;
return ((_a = pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) === null || _a === void 0 ? void 0 : _a.next) || ((_b = pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies) === null || _b === void 0 ? void 0 : _b.next);
}
async function buildNext(cwd, additionalRequire) {
await new Promise((resolve, reject) => {
const child = spawn('next', ['build'], {
stdio: 'inherit',
cwd,
});
child.on('exit', code => (code ? reject(code) : resolve(code)));
child.on('error', reject);
});
await fs.mkdirp(join(cwd, 'dist'));
if (additionalRequire) {
await tsup({
entryPoints: [join(cwd, additionalRequire)],
outDir: join(cwd, 'dist'),
target: 'node18',
format: ['cjs'],
splitting: false,
skipNodeModulesBundle: true,
});
}
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'), [
`#!/usr/bin/env node`,
`process.on('SIGTERM', () => process.exit(0))`,
`process.on('SIGINT', () => process.exit(0))`,
additionalRequire ? `require('${additionalRequire.replace('.ts', '')}')` : ``,
`
require('next/dist/server/lib/start-server').startServer({
dir: __dirname,
hostname: '0.0.0.0',
port: parseInt(process.env.PORT),
conf: {},
}).then(async (app)=>{
const appUrl = 'http://' + app.hostname + ':' + app.port;
console.log('started server on '+ app.hostname + ':' + app.port + ', url: ' + appUrl);
await app.prepare();
}).catch((err)=>{
console.error(err);
process.exit(1);
})
`,
].join('\n')),
]);
}
async function compile(cwd, entryPoint, buildOptions, dependencies, useEsm = false) {
if (buildOptions.tsup) {
const out = join(cwd, 'dist');
await tsup({
entryPoints: [join(cwd, entryPoint)],
outDir: out,
target: 'node18',
format: [useEsm ? 'esm' : 'cjs'],
splitting: false,
sourcemap: true,
clean: true,
shims: true,
skipNodeModulesBundle: false,
noExternal: dependencies,
external: buildOptions.external,
banner: buildOptions.banner
? {
js: buildOptions.banner.includes('.js') || buildOptions.banner.includes('.mjs')
? await fs.readFile(join(cwd, buildOptions.banner), 'utf-8')
: buildOptions.banner,
}
: {},
});
return;
}
const { code, map, assets } = await ncc(join(cwd, entryPoint), {
cache: false,
sourceMap: true,
});
await fs.mkdirp(join(cwd, 'dist'));
await Promise.all([
fs.writeFile(join(cwd, 'dist/index.js'), code, 'utf8'),
fs.writeFile(join(cwd, 'dist/index.js.map'), map, 'utf8'),
...Object.keys(assets).map(async (filepath) => {
if (filepath.endsWith('package.json')) {
return;
}
await fs.ensureDir(dirname(join(cwd, 'dist', filepath)), {});
await fs.writeFile(join(cwd, 'dist', filepath), assets[filepath].source);
}),
]);
}