prebundle
Version:
375 lines (374 loc) • 12.7 kB
JavaScript
import { dirname, extname, join, resolve } from "node:path";
import fs_extra from "../compiled/fs-extra/index.js";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import ncc from "@vercel/ncc";
import fast_glob from "../compiled/fast-glob/index.js";
import rslog from "../compiled/rslog/index.js";
import { dts } from "rollup-plugin-dts";
import { rollup } from "rollup";
import { minify } from "terser";
import { format } from "prettier";
import { existsSync } from "node:fs";
const DIST_DIR = 'compiled';
const DEFAULT_EXTERNALS = {
'./package.json': './package.json',
'../package.json': './package.json',
'../../package.json': './package.json'
};
const NODE_BUILTINS = [
'_stream_duplex',
'_stream_passthrough',
'_stream_readable',
'_stream_transform',
'_stream_writable',
'assert',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'dns',
'domain',
'events',
'fs',
'http',
'https',
'module',
'net',
'os',
'path',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'sys',
'timers',
'tls',
'tty',
'url',
'util',
'vm',
'zlib'
];
const cwd = process.cwd();
const helper_require = createRequire(import.meta.url);
function findDepPath(name) {
try {
let entry = dirname(helper_require.resolve(name, {
paths: [
cwd
]
}));
while(!dirname(entry).endsWith('node_modules'))entry = dirname(entry);
if (name.includes('/')) return join(dirname(entry), name);
return entry;
} catch {
return null;
}
}
const resolveConfig = async (configFile)=>{
const configFiles = configFile ? [
resolve(cwd, configFile)
] : [
'prebundle.config.ts',
'prebundle.config.mts',
'prebundle.config.mjs',
'prebundle.config.js'
].map((filename)=>join(cwd, filename));
for (const filename of configFiles)if (fs_extra.existsSync(filename)) {
const config = await import(pathToFileURL(filename).href);
return config.default ?? config;
}
if (configFile) throw new Error(`Unable to locate prebundle config file at: ${configFile}`);
throw new Error('Unable to locate prebundle config file.');
};
function parseTasks(dependencies, globalPrettier) {
const result = [];
for (const dep of dependencies){
const depName = 'string' == typeof dep ? dep : dep.name;
const importPath = join(cwd, DIST_DIR, depName);
const distPath = join(cwd, DIST_DIR, depName);
const depPath = findDepPath(depName);
if (!depPath) throw new Error(`Failed to resolve dependency: ${depName}`);
const depEntry = helper_require.resolve(depName, {
paths: [
cwd
]
});
const info = {
depName,
depPath,
depEntry,
distPath,
importPath
};
if ('string' == typeof dep) result.push({
minify: false,
target: 'es2019',
externals: {},
dtsExternals: [],
emitFiles: [],
packageJsonField: [],
prettier: globalPrettier,
...info
});
else result.push({
minify: dep.minify ?? false,
target: dep.target ?? 'es2019',
ignoreDts: dep.ignoreDts,
copyDts: dep.copyDts,
externals: dep.externals ?? {},
dtsOnly: dep.dtsOnly ?? false,
dtsExternals: dep.dtsExternals ?? [],
emitFiles: dep.emitFiles ?? [],
prettier: dep.prettier ?? globalPrettier,
afterBundle: dep.afterBundle,
beforeBundle: dep.beforeBundle,
packageJsonField: dep.packageJsonField ?? [],
...info
});
}
return result;
}
function pick(obj, keys) {
return keys.reduce((ret, key)=>{
if (void 0 !== obj[key]) ret[key] = obj[key];
return ret;
}, {});
}
function pkgNameToAtTypes(name) {
const mangled = name.replace(/^@([^\/]+)\/([^\/]+)/, '$1__$2');
return `@types/${mangled}`;
}
function findDirectTypeFile(filepath) {
if (/\.d\.[cm]?ts/.test(filepath)) return filepath;
const ext = extname(filepath);
const base = filepath.slice(0, -ext.length);
const _find = (list)=>{
for (const f of list)try {
return helper_require.resolve(f, {
paths: [
cwd
]
});
} catch {
continue;
}
};
switch(ext){
case '.js':
case '.ts':
return _find([
base + '.d.ts'
]);
case '.mjs':
case '.mts':
return _find([
base + '.d.mts',
base + '.d.ts'
]);
case '.cjs':
case '.cts':
return _find([
base + '.d.cts',
base + '.d.ts'
]);
default:
}
}
const { logger: logger } = rslog;
function emitAssets(assets, distPath) {
for (const key of Object.keys(assets)){
const asset = assets[key];
fs_extra.outputFileSync(join(distPath, key), asset.source);
}
}
async function emitIndex(code, distPath, prettier) {
const distIndex = join(distPath, 'index.js');
if (prettier) {
const minimized = await minify(code, {
compress: false,
mangle: false,
ecma: 2019
});
if (!minimized.code) throw new Error('terser minify failed');
const formatted = await format(minimized.code, {
filepath: distIndex
});
await fs_extra.outputFile(distIndex, formatted);
} else await fs_extra.outputFile(distIndex, code);
}
const getPackageJsonField = (json, key)=>{
const value = json[key];
return 'string' == typeof value ? value : null;
};
const getTypes = (json)=>json && (getPackageJsonField(json, 'types') || getPackageJsonField(json, 'typing') || getPackageJsonField(json, 'typings') || getTypes(json.exports?.['.'])) || null;
async function emitDts(task, externals) {
const outputDefaultDts = ()=>{
fs_extra.outputFileSync(join(task.distPath, 'index.d.ts'), 'export = any;\n');
};
if (task.ignoreDts) return void outputDefaultDts();
if (task.copyDts) {
const dtsFiles = fast_glob.sync('**/*.d.{ts,cts}', {
cwd: task.depPath,
absolute: false
});
for (const dtsFile of dtsFiles)fs_extra.copySync(join(task.depPath, dtsFile), join(task.distPath, dtsFile));
return;
}
const getInput = ()=>{
const pkgPath = join(task.depPath, 'package.json');
const pkgJson = fs_extra.readJsonSync(pkgPath, 'utf-8');
const types = getTypes(pkgJson);
if (types) return join(task.depPath, types);
const directTypeFile = findDirectTypeFile(task.depEntry);
if (directTypeFile) return directTypeFile;
const depTypesPath = findDepPath(`${pkgNameToAtTypes(task.depName)}/package.json`);
if (!depTypesPath) return null;
const depTypesPkg = fs_extra.readJsonSync(depTypesPath, 'utf-8');
const depTypes = getTypes(depTypesPkg);
return depTypes ? join(dirname(depTypesPath), depTypes) : null;
};
const input = getInput();
if (!input) return void outputDefaultDts();
try {
const inputConfig = {
input,
external: [
...Object.keys(externals),
...task.dtsExternals,
...NODE_BUILTINS
],
plugins: [
dts({
respectExternal: true,
compilerOptions: {
skipLibCheck: true,
preserveSymlinks: false,
composite: false,
declarationMap: false,
declaration: true,
noEmit: false,
emitDeclarationOnly: true,
noEmitOnError: true,
checkJs: false,
target: task.target
}
})
]
};
const outputConfig = {
dir: task.distPath,
format: 'esm',
exports: 'named',
entryFileNames: 'index.d.ts'
};
const bundle = await rollup(inputConfig);
await bundle.write(outputConfig);
} catch (error) {
logger.error(`rollup-plugin-dts failed: ${task.depName}`);
logger.error(error);
}
}
function emitPackageJson(task, assets) {
const packageJsonPath = join(task.depPath, 'package.json');
const packageJson = fs_extra.readJsonSync(packageJsonPath, 'utf-8');
const outputPath = join(task.distPath, 'package.json');
const pickedPackageJson = pick(packageJson, [
'name',
'author',
'version',
'funding',
'license',
...task.packageJsonField
]);
if (task.depName !== pickedPackageJson.name) pickedPackageJson.name = task.depName;
if (task.copyDts) {
const types = getTypes(packageJson);
if (types) pickedPackageJson.types = types;
} else pickedPackageJson.types = 'index.d.ts';
if (task.dtsOnly) pickedPackageJson.type = packageJson.type || 'commonjs';
else pickedPackageJson.type = 'commonjs';
if (assets['package.json']) try {
Object.assign(pickedPackageJson, pick(JSON.parse(assets['package.json'].source), [
'type'
]));
} catch {}
fs_extra.outputFileSync(outputPath, JSON.stringify(pickedPackageJson, null, 2));
}
function emitLicense(task) {
const licensePath = join(task.depPath, 'LICENSE');
if (fs_extra.existsSync(licensePath)) fs_extra.copySync(licensePath, join(task.distPath, 'license'));
}
function emitExtraFiles(task) {
const { emitFiles } = task;
for (const item of emitFiles){
const path = join(task.distPath, item.path);
fs_extra.outputFileSync(path, item.content);
}
}
function removeSourceMap(task) {
const maps = fast_glob.sync(join(task.distPath, '**/*.map'));
for (const mapPath of maps)fs_extra.removeSync(mapPath);
}
function renameDistFolder(task) {
const pkgPath = join(task.distPath, 'package.json');
const pkgJson = fs_extra.readJsonSync(pkgPath, 'utf-8');
for (const key of [
'types',
'typing',
'typings'
])if (pkgJson[key]?.startsWith('dist/')) {
pkgJson[key] = pkgJson[key].replace('dist/', 'types/');
const distFolder = join(task.distPath, 'dist');
const typesFolder = join(task.distPath, 'types');
if (fs_extra.existsSync(distFolder)) fs_extra.renameSync(distFolder, typesFolder);
}
fs_extra.writeJSONSync(pkgPath, pkgJson);
}
async function prebundle(task, commonExternals = {}) {
logger.start(`prebundle: ${task.depName}`);
fs_extra.removeSync(task.distPath);
if (task.beforeBundle) await task.beforeBundle(task);
const mergedExternals = {
...DEFAULT_EXTERNALS,
...commonExternals,
...task.externals
};
const nodeModulesPath = join(process.cwd(), 'node_modules');
const hasNodeModules = existsSync(nodeModulesPath);
const enableCache = !process.env.CI && hasNodeModules;
if (task.dtsOnly) emitPackageJson(task, {});
else {
const { code, assets } = await ncc(task.depEntry, {
minify: task.minify,
target: task.target,
externals: mergedExternals,
assetBuilds: false,
cache: enableCache ? join(nodeModulesPath, '.cache', 'ncc-cache') : false
});
await emitIndex(code, task.distPath, task.prettier && !task.minify);
emitAssets(assets, task.distPath);
emitPackageJson(task, assets);
}
await emitDts(task, mergedExternals);
emitLicense(task);
removeSourceMap(task);
renameDistFolder(task);
emitExtraFiles(task);
if (task.afterBundle) await task.afterBundle(task);
logger.success(`prebundle: ${task.depName}\n\n`);
}
async function run(options = {}) {
const config = await resolveConfig(options.config);
const parsedTasks = parseTasks(config.dependencies, config.prettier);
const filters = options.packages?.length ? new Set(options.packages) : null;
for (const task of parsedTasks)if (!filters || filters.has(task.depName)) await prebundle(task, config.externals);
}
export { run };