ts-project-builder
Version:
Rollup-based TypeScript builder with multi-format output and built-in common plugins.
220 lines (217 loc) • 10.3 kB
JavaScript
import { glob, rm } from 'node:fs/promises';
import { resolve, relative, isAbsolute } from 'node:path';
import { pathToFileURL } from 'node:url';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import * as _isGlob from 'is-glob';
import { cloneDeep, merge } from 'lodash-es';
import prettyMilliseconds from 'pretty-ms';
import { rollup } from 'rollup';
import { minify } from 'rollup-plugin-esbuild';
import { nodeExternals } from 'rollup-plugin-node-externals';
import { pathIsFile } from './utils/index.mjs';
import { cyan, bold, green } from './utils/rollup/colors.mjs';
import { stderr } from './utils/rollup/logging.mjs';
const availableOutputFormats = new Set([
'amd',
'cjs',
'commonjs',
'es',
'esm',
'iife',
'module',
'system',
'systemjs',
'umd',
]);
const defaultConfigFilePath = './ts-project-builder.config.mjs';
const defaultOutputDir = './dist';
const defaultOutputPreserveModulesRoot = './src';
const isGlob = 'default' in _isGlob ? _isGlob.default : _isGlob;
const outputFormatToExtMap = {
amd: 'amd.js',
cjs: 'cjs',
commonjs: 'cjs',
es: 'mjs',
esm: 'mjs',
iife: 'iife.js',
module: 'mjs',
system: 'system.js',
systemjs: 'system.js',
umd: 'umd.js',
};
class Builder {
#configFilePath;
#options;
constructor(options) {
options = cloneDeep(options);
if (!options.inputs.length)
throw new Error('No inputs specified');
if (!options.output.formats.size)
throw new Error('No output formats specified');
this.#configFilePath = resolve(options.configFilePath || defaultConfigFilePath);
this.#options = options;
}
async #getConfig() {
if (!this.#configFilePath)
return {};
if (!await pathIsFile(this.#configFilePath)) {
if (relative(this.#configFilePath, resolve(defaultConfigFilePath)) !== '') {
throw new Error(`Config file not found: ${this.#configFilePath}`);
}
return {};
}
const config = await import(pathToFileURL(resolve(this.#configFilePath)).toString());
return (config && typeof config === 'object' && 'default' in config ? config.default : config);
}
#isOutputOptionEnabled(format, optionKey) {
if (!this.#options.output[optionKey])
return;
return this.#options.output[optionKey] === true || this.#options.output[optionKey].has(format);
}
#prepareInputPlugins(config) {
const plugins = config.additionalInputPlugins?.beforeBuiltIns || [];
if (config.enableBuiltInInputPlugins?.nodeExternal !== false) {
plugins.push(nodeExternals(config.builtInInputPluginOptions?.nodeExternal));
}
if (config.enableBuiltInInputPlugins?.nodeResolve !== false) {
plugins.push(nodeResolve(config.builtInInputPluginOptions?.nodeResolve));
}
if (config.enableBuiltInInputPlugins?.commonjs !== false) {
plugins.push(commonjs(config.builtInInputPluginOptions?.commonjs));
}
if (config.enableBuiltInInputPlugins?.json !== false) {
plugins.push(json(config.builtInInputPluginOptions?.json));
}
if (config.enableBuiltInInputPlugins?.typescript !== false) {
plugins.push(typescript(config.builtInInputPluginOptions?.typescript));
}
plugins.push(...config.additionalInputPlugins?.afterBuiltIns || []);
return plugins;
}
async build() {
stderr(cyan('Starting build...'));
const startAt = Date.now();
const config = await this.#getConfig();
const baseOutputOptions = {
dir: this.#options.output.dirs?.default || defaultOutputDir,
ext: this.#options.output.exts?.default,
file: this.#options.output.files?.default,
preserveModulesRoot: this.#options.output.preserveModulesRoots?.default || defaultOutputPreserveModulesRoot,
sourcemap: this.#options.output.sourcemaps?.default,
};
const inputFiles = await Promise.all([...new Set(this.#options.inputs)].map(async (input) => {
if (!isGlob(input, { strict: false }))
return input;
const files = [];
for await (const file of glob(input))
files.push(file);
if (!files.length)
console.warn(`⚠️ No files matched for glob pattern: ${input}`);
return files;
}));
const logOutputTargetsStrings = [];
const rollupInputPlugins = this.#prepareInputPlugins(config);
const rollupOptions = {
...config.rollupOptions,
input: [...new Set(inputFiles.flat())].sort(),
};
const rollupOutputs = [];
const rootPath = resolve();
const toRemovePaths = new Set();
for (const format of this.#options.output.formats) {
if (!availableOutputFormats.has(format))
throw new Error(`Invalid output format: ${format}`);
const configOutputOptions = config.outputOptions?.[format] || config.outputOptions?.default;
let outputOptions;
if (configOutputOptions?.processMethod === 'replace') {
outputOptions = configOutputOptions.options;
}
else {
const entryFileNames = `[name].${this.#options.output.exts?.[format]
|| baseOutputOptions.ext
|| outputFormatToExtMap[format]}`;
outputOptions = {
dir: this.#options.output.dirs?.[format] || baseOutputOptions.dir,
entryFileNames,
exports: 'named',
externalLiveBindings: false,
file: this.#options.output.files?.[format] || baseOutputOptions.file,
generatedCode: {
arrowFunctions: true,
constBindings: true,
objectShorthand: true,
},
interop: 'compat',
plugins: [],
preserveModules: this.#isOutputOptionEnabled(format, 'preserveModules'),
// eslint-disable-next-line style/max-len
preserveModulesRoot: this.#options.output.preserveModulesRoots?.[format] || baseOutputOptions.preserveModulesRoot,
sourcemap: this.#options.output.sourcemaps?.[format] ?? baseOutputOptions.sourcemap,
};
if (this.#isOutputOptionEnabled(format, 'minify')) {
// eslint-disable-next-line style/max-len
const minifyOptions = config.builtInOutputPluginOptions?.minify?.[format] || config.builtInOutputPluginOptions?.minify?.default;
outputOptions.plugins?.push(minify(minifyOptions));
}
outputOptions.plugins?.push(...config.additionalOutputPlugins?.[format]?.afterBuiltIns
|| config.additionalOutputPlugins?.default?.afterBuiltIns
|| []);
outputOptions.plugins?.unshift(...config.additionalOutputPlugins?.[format]?.beforeBuiltIns
|| config.additionalOutputPlugins?.default?.beforeBuiltIns
|| []);
if (configOutputOptions?.processMethod === 'assign') {
Object.assign(outputOptions, configOutputOptions.options);
}
else
merge(outputOptions, configOutputOptions?.options);
}
outputOptions.format = format;
if (outputOptions.file) {
delete outputOptions.dir;
logOutputTargetsStrings.push(`${outputOptions.file} (${format})`);
}
else if (outputOptions.dir) {
delete outputOptions.file;
logOutputTargetsStrings.push(`${outputOptions.dir} (${format})`);
}
if (this.#isOutputOptionEnabled(format, 'clean')) {
const outputPath = outputOptions.dir || outputOptions.file;
if (outputPath) {
const absoluteOutputPath = resolve(outputPath);
const relativePath = relative(rootPath, absoluteOutputPath);
if (relativePath === '') {
throw new Error('The directory to be cleared is the same as the running directory.');
}
if (!(!isAbsolute(relativePath) && !relativePath.startsWith('..'))
&& !this.#isOutputOptionEnabled(format, 'forceClean')) {
// eslint-disable-next-line style/max-len
throw new Error(`The path "${absoluteOutputPath}" to be cleaned is not under the running directory. To force clean, please add the --force-clean parameter.`);
}
toRemovePaths.add(absoluteOutputPath);
}
}
rollupOutputs.push(outputOptions);
}
const logInputFiles = [...rollupOptions.input];
if (logInputFiles.length > 20) {
logInputFiles.splice(20, logInputFiles.length, `... (${logInputFiles.length - 20} more)`);
}
const logOutputTargetsString = bold(logOutputTargetsStrings.join(', ').trim());
stderr(cyan(`${bold(logInputFiles.join(', ').trim())} → ${logOutputTargetsString}...`));
const rollupResult = await rollup({
...rollupOptions,
plugins: rollupInputPlugins,
});
await Promise.all([...toRemovePaths].map((path) => rm(path, {
force: true,
recursive: true,
})));
await Promise.all(rollupOutputs.map((outputOptions) => rollupResult.write(outputOptions)));
stderr(green(`Created ${logOutputTargetsString} in ${bold(prettyMilliseconds(Date.now() - startAt))}`));
}
}
export { Builder, defaultConfigFilePath, defaultOutputDir, defaultOutputPreserveModulesRoot };
//# sourceMappingURL=builder.mjs.map