techor
Version:
Author technology like a top leader
385 lines (382 loc) • 18.7 kB
JavaScript
import { readJSONFileSync } from '@techor/fs';
import log from '@techor/log';
import { existsSync, rmSync } from 'fs';
import { watch, rollup } from 'rollup';
import extend from '@techor/extend';
import { explorePathsSync } from '@techor/glob';
import { normalize, extname, basename, relative } from 'path';
import upath from 'upath';
import prettyBytes from 'pretty-bytes';
import align from 'wide-align';
import prettyHartime from 'pretty-hrtime';
import { logRollupWarning, logRollupError } from '../utils/log-rollup.mjs';
import { execaCommand } from 'execa';
import clsx from 'clsx';
import getWideExternal from '../utils/get-wide-external.mjs';
import config from '../config.mjs';
import yargsParser from 'yargs-parser';
import loadConfig from '../load-config.mjs';
import replace from '@rollup/plugin-replace';
import esmShim from '../plugins/esm-shim.mjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import swc from '../plugins/swc.mjs';
import preserveDirectives from 'rollup-plugin-preserve-directives';
const yargsParserOptions = {
alias: {
formats: 'f',
watch: 'w',
clean: 'c',
'output.file': 'o'
},
configuration: {
'strip-aliased': true,
'strip-dashed': true
},
array: [
'formats'
]
};
async function build() {
const { _, ...cmdConfig } = yargsParser(process.argv.slice(2), yargsParserOptions);
const [commandName, ...commandInputs] = _;
try {
const useConfig = loadConfig();
const config$1 = extend(config, useConfig, {
build: cmdConfig
});
if (commandName === 'dev') {
process.env.NODE_ENV = 'development';
config$1.build.watch = true;
} else {
process.env.NODE_ENV = 'production';
}
if (process.env.DEBUG) console.log('[techor] cmdConfig', cmdConfig);
const pkg = readJSONFileSync('package.json') || {};
const { dependencies, peerDependencies, optionalDependencies, types } = pkg;
const buildMap = new Map();
if (config$1.build.declare === undefined && types) config$1.build.declare = true;
if (Array.isArray(config$1.build.input.external)) {
if (dependencies) config$1.build.input.external.push(...Object.keys(dependencies));
if (peerDependencies) config$1.build.input.external.push(...Object.keys(peerDependencies));
if (optionalDependencies) config$1.build.input.external.push(...Object.keys(optionalDependencies));
}
// remove output dir first
if (config$1.build.clean && existsSync(config$1.build.output.dir)) {
rmSync(config$1.build.output.dir, {
force: true,
recursive: true
});
log.i`Cleaned up **${config$1.build.output.dir}**`;
}
function addBuild(entries, rollupOutputOptions) {
if (Array.isArray(entries)) {
entries = entries.map((eachEntry)=>normalize(eachEntry));
} else {
entries = normalize(entries);
}
let forceBundle;
const extendedBuild = extend(config$1.build, {
output: rollupOutputOptions
});
// single entry
if (extendedBuild.output.file) {
if (!extendedBuild.output.format) {
extendedBuild.output.format = config$1.build.formatOfExt[extname(extendedBuild.output.file)];
}
const fileBasenameSplits = basename(extendedBuild.output.file).split('.');
if (fileBasenameSplits.includes('min')) {
forceBundle = true;
extendedBuild.minify = true;
}
if (fileBasenameSplits.includes('global') || fileBasenameSplits.includes('iife')) extendedBuild.output.format = 'iife';
extendedBuild.env = fileBasenameSplits.includes('development') ? 'development' : 'production';
for (const [eachInput, eachBuildOptions] of buildMap){
for (const eachOutputOptions of eachBuildOptions.outputOptionsList){
if (normalize(eachOutputOptions.output.file) === normalize(extendedBuild.output.file)) {
if (process.env.DEBUG) console.log('[techor] extendedBuild', extendedBuild);
return;
}
}
}
} else {
extendedBuild.output.entryFileNames = (chunkInfo)=>{
return `${chunkInfo.name}${config$1.build.extOfFormat[extendedBuild.output.format]}`;
};
}
if (extendedBuild.output.preserveModules && !extendedBuild.output.preserveModulesRoot) {
extendedBuild.output.preserveModulesRoot = extendedBuild.srcDir;
}
let buildOptions = buildMap.get(entries);
if (buildOptions) {
// 合併同一個 input 來源並對應多個 RollupOutputOptions 避免重新 parse 相同的 input
buildOptions.outputOptionsList.push(extendedBuild);
} else {
buildOptions = {
outputOptionsList: [
extendedBuild
]
};
buildOptions.input = extend({
onwarn: (warning, warn)=>{
switch(warning.code){
case 'MODULE_LEVEL_DIRECTIVE':
// https://github.com/Ephem/rollup-plugin-preserve-directives?tab=readme-ov-file#rollup-warning
if (config$1.build.preserveDirectives && extendedBuild.output.preserveModules) {
return;
}
break;
}
logRollupWarning(warning);
}
}, config$1.build.input);
buildOptions.input.input = entries;
buildOptions.input.external = config$1.build.input.external && !forceBundle && getWideExternal(config$1.build.input.external);
const extendedSWCOptions = extend(config$1.build.swc, {
tsconfigFile: config$1.build.tsconfig
});
if (extendedBuild.minify) {
extendedSWCOptions.minify = true;
} else {
delete extendedSWCOptions.minify;
delete extendedSWCOptions.jsc.minify;
}
buildOptions.input.plugins.unshift(...[
swc(extendedSWCOptions),
config$1.build.commonjs && commonjs(config$1.build.commonjs),
config$1.build.nodeResolve && nodeResolve(config$1.build.nodeResolve),
config$1.build.esmShim && esmShim(),
config$1.build.preserveDirectives && !extendedBuild.output.file && preserveDirectives(config$1.build.preserveDirectives),
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify(extendedBuild.env)
})
].filter((existence)=>existence));
if (process.env.DEBUG) console.log('[techor] buildOptions', buildOptions);
buildMap.set(entries, buildOptions);
}
}
if (commandInputs.length) {
const foundEntries = explorePathsSync(commandInputs);
config$1.build.formats.map((eachFormat)=>{
addBuild(foundEntries, {
format: eachFormat
});
});
} else {
log.i`Detected entries (package.json)`;
const exploreMapptedEntry = (filePath)=>{
const subFilePath = upath.relative(config$1.build.output.dir, filePath);
const srcFilePath = upath.join(config$1.build.srcDir, subFilePath);
const pattern = upath.changeExt(srcFilePath, `.{${config$1.build.sourceExtensions.join(',')}}`);
const foundEntries = explorePathsSync(pattern);
if (!foundEntries.length) {
throw new Error(`Cannot find the entry file **${pattern}**`);
}
return foundEntries[0];
};
if (pkg.exports) {
(function handleExports(eachExports, eachFormat) {
if (typeof eachExports === 'string') {
addBuild(exploreMapptedEntry(eachExports), {
format: eachFormat,
file: eachExports
});
} else {
for(const eachExportKey in eachExports){
const eachUnknowExports = eachExports[eachExportKey];
if (eachExportKey.startsWith('.')) {
handleExports(eachUnknowExports, eachFormat);
} else {
switch(eachExportKey){
case 'node':
handleExports(eachUnknowExports, eachFormat);
break;
case 'browser':
handleExports(eachUnknowExports, eachFormat);
break;
case 'default':
handleExports(eachUnknowExports, eachFormat);
break;
case 'require':
handleExports(eachUnknowExports, 'cjs');
break;
case 'import':
handleExports(eachUnknowExports, 'esm');
break;
case 'development':
handleExports(eachUnknowExports, eachFormat);
break;
case 'production':
handleExports(eachUnknowExports, eachFormat);
break;
}
}
}
}
})(pkg.exports);
}
if (pkg.main) {
addBuild(exploreMapptedEntry(pkg.main), {
file: pkg.main
});
}
if (pkg.module) {
addBuild(exploreMapptedEntry(pkg.module), {
format: 'esm',
file: pkg.module
});
}
if (pkg.browser) {
if (typeof pkg.browser !== 'string') {
throw new Error('Not implemented for browser field object.');
}
addBuild(exploreMapptedEntry(pkg.browser), {
format: 'iife',
file: pkg.browser
});
}
if (pkg.bin) {
if (typeof pkg.bin === 'string') {
addBuild(exploreMapptedEntry(pkg.bin), {
file: pkg.bin
});
} else {
for(const eachCommandName in pkg.bin){
const eachCommandFile = pkg.bin[eachCommandName];
addBuild(exploreMapptedEntry(eachCommandFile), {
file: eachCommandFile
});
}
}
}
}
if (buildMap.size === 0) {
log.x`No build tasks created`;
log.i`Please check your **package.json** or specify **entries**`;
return;
}
if (config$1.build.watch) log.i`Start watching for file changes...`;
const outputResults = [];
const printOutputResults = (eachOutputResults, eachBuildStartTime)=>{
const buildTime = process.hrtime(eachBuildStartTime);
const colSizes = {};
const chunks = [];
const resolveFilename = (eachOutputResult)=>{
if (eachOutputResult.output.preserveModules) {
return eachOutputResult.artifact.fileName;
} else {
return relative(eachOutputResult.output.dir, eachOutputResult.output.file);
}
};
for (const eachOutputResult of eachOutputResults){
switch(eachOutputResult.artifact.type){
case 'asset':
throw new Error('Not implemented for assets.');
case 'chunk':
chunks.push(eachOutputResult.artifact);
colSizes[1] = Math.max(colSizes[1] || 0, resolveFilename(eachOutputResult).length);
colSizes[2] = Math.max(colSizes[2] || 0, eachOutputResult.output.format.length);
colSizes[3] = Math.max(colSizes[3] || 0, prettyBytes(eachOutputResult.artifact.code.length, {
space: false
}).length);
break;
}
}
console.log('');
eachOutputResults.sort((a, b)=>resolveFilename(a).length - resolveFilename(b).length || resolveFilename(a).localeCompare(resolveFilename(b))).forEach((eachOutputResult)=>{
let fileName, format, codeLength;
switch(eachOutputResult.artifact.type){
case 'asset':
throw new Error('Not implemented for assets.');
case 'chunk':
fileName = align.left('**' + resolveFilename(eachOutputResult) + '**', colSizes[1] + 6);
format = align.right(eachOutputResult.output.format, colSizes[2] + 2);
codeLength = align.right(prettyBytes(eachOutputResult.artifact.code.length, {
space: false
}), colSizes[3] + 2);
break;
}
log.o(`${fileName} ${format} ${codeLength} ${eachOutputResult.minify && '(minified)' || ''}`);
});
console.log('');
log.ok(clsx(`Built **${chunks.length}** chunks`, config$1.build.declare && `and types`, `in ${prettyHartime(buildTime).replace(' ', '')}`));
};
const buildStartTime = process.hrtime();
const output = async (rollupBuild, eachOutputOptionsList, outputResults)=>{
await Promise.all(eachOutputOptionsList.map(async (eachOutputOptions)=>{
const eachRollupOutputOptions = extend({}, eachOutputOptions.output);
if (eachRollupOutputOptions.file) {
// fix: Invalid value for option "output.dir" - you must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.
delete eachRollupOutputOptions.dir;
// fix: Invalid value for option "output.file" - you must set "output.dir" instead of "output.file" when using the "output.preserveModules" option.
delete eachRollupOutputOptions.preserveModules;
}
const result = await rollupBuild.write(eachRollupOutputOptions);
result.output.forEach((chunkOrAsset)=>{
outputResults.push({
...eachOutputOptions,
artifact: chunkOrAsset
});
});
}));
};
await Promise.all([
...Array.from(buildMap.entries()).map(async ([input, eachBuildOptions])=>{
if (config$1.build.watch) {
const watcher = watch({
...eachBuildOptions.input,
watch: {
skipWrite: true
}
});
let buildStartTime;
watcher.on('event', async (event)=>{
if (event.code === 'BUNDLE_START') {
buildStartTime = process.hrtime();
}
if (event.code === 'BUNDLE_END') {
const eachOutputResults = [];
await output(event.result, eachBuildOptions.outputOptionsList, eachOutputResults);
printOutputResults(eachOutputResults, buildStartTime);
}
if (event.code === 'ERROR') {
console.error(event.error);
}
});
watcher.on('change', (id, { event })=>{
console.log('');
log`[${event.toUpperCase()}] ${relative(process.cwd(), id)}`;
});
} else {
const rollupBuild = await rollup(eachBuildOptions.input);
await output(rollupBuild, eachBuildOptions.outputOptionsList, outputResults);
if (rollupBuild) {
// closes the rollupBuild
await rollupBuild.close();
}
}
}),
config$1.build.declare && new Promise((resolve)=>{
execaCommand(clsx('npx tsc --emitDeclarationOnly --preserveWatchOutput --declaration', config$1.build.output.dir && ' --project ' + config$1.build.tsconfig, config$1.build.watch && '--watch'), {
stdio: 'inherit',
stripFinalNewline: false,
cwd: process.cwd()
}).catch(()=>{
process.exit();
}).finally(resolve);
})
]);
if (!config$1.build.watch) {
printOutputResults(outputResults, buildStartTime);
}
} catch (error) {
if (error.name === 'RollupError') {
logRollupError(error);
} else {
log(error);
}
process.exit(1);
}
}
export { build as default, yargsParserOptions };