lbundle
Version:
Small zero-configuration bundler build on top of Rollup.js and SWC for NPM libraries
670 lines (641 loc) • 19.1 kB
JavaScript
import { rollup } from 'rollup';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { swc } from 'rollup-plugin-swc3';
import { typescriptPaths } from 'rollup-plugin-typescript-paths';
import PeerDepsExternalPlugin from 'rollup-plugin-peer-deps-external';
import json from '@rollup/plugin-json';
import typescript from '@rollup/plugin-typescript';
import path from 'path';
import styles from 'rollup-plugin-styler';
import postcssImport from 'postcss-import';
import resolver from 'resolve';
import { resolve, legacy } from 'resolve.exports';
import { fileURLToPath } from 'url';
import url from '@rollup/plugin-url';
import svgr from '@svgr/rollup';
import fs from 'fs';
import merge from 'lodash.merge';
import { pascalCase } from 'change-case';
import { Command } from 'commander';
const getSwcEnv = (pkg)=>{
const coreJsVersion = pkg.dependencies?.['core-js'] ?? pkg.devDependencies?.['core-js'] ?? pkg.peerDependencies?.['core-js'];
if (!coreJsVersion) return {
targets: 'defaults'
};
return {
targets: 'defaults',
coreJs: coreJsVersion,
mode: 'usage',
bugfixes: true
};
};
const getSwcConfig = (param, param1)=>{
let { isTs, isJsx, pkg } = param, { format } = param1;
return {
env: getSwcEnv(pkg),
jsc: {
target: undefined,
parser: isTs ? {
syntax: 'typescript',
tsx: isJsx
} : {
syntax: 'ecmascript',
jsx: isJsx
}
},
module: {
type: formatToTypeMap[format ?? 'es']
},
sourceMaps: true,
swcrc: false
};
};
const formatToTypeMap = {
amd: 'amd',
umd: 'umd',
iife: 'umd',
system: 'systemjs',
systemjs: 'systemjs',
cjs: 'commonjs',
commonjs: 'commonjs',
es: 'es6',
esm: 'es6',
module: 'es6'
};
const jsExtensions$1 = [
'.mjs',
'.cjs',
'.js',
'.mtsx',
'.ctsx',
'.tsx',
'.mts',
'.cts',
'.ts'
];
// string
const isString = (value)=>typeof value === 'string';
const isStringFull = (value)=>value.length > 0;
const isEmptyString = (value)=>value.length === 0;
const isNil = (value)=>value == null;
const isDefined = (value)=>value != null;
// object
const isObject = (value)=>typeof value === 'object' && value != null;
const isEmptyObject = (value)=>Object.keys(value).length === 0;
const isEmptyArray = (value)=>value.length === 0;
const getRollupTypescriptConfig = (param)=>{
let { options, pkg, resolvedSource, tsconfigPath } = param;
const declarationOptions = pkg.types ? {
emitDeclarationOnly: true,
declaration: true,
declarationDir: path.resolve(options.cwd, path.dirname(pkg.types))
} : {};
return {
tsconfig: tsconfigPath ?? false,
rootDir: resolvedSource ? path.dirname(resolvedSource) : undefined,
sourceMap: false,
exclude: [
'**/__tests__/**',
'**/*.test.*',
'**/*.spec.*',
'**/*.stories.*',
'**/*.mock.*'
],
...declarationOptions
};
};
const stylesExtensions = [
'.css',
'.scss',
'.sass',
'.less',
'.pcss',
'.sss',
'.styl',
'.stylus'
];
const imageExtensions = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
'.avif',
'.ico',
'.bmp',
'.tiff',
'.tif'
];
const videoExtensions = [
'.mp4',
'.webm',
'.ogg',
'.avi',
'.mov',
'.wmv',
'.flv',
'.mkv'
];
const audioExtensions = [
'.mp3',
'.wav',
'.ogg',
'.aac',
'.flac',
'.m4a'
];
const fontExtensions = [
'.woff',
'.woff2',
'.eot',
'.ttf',
'.otf'
];
const documentExtensions = [
'.pdf',
'.doc',
'.docx',
'.txt',
'.md'
];
const svgExtensions = [
'.svg'
];
// All asset extensions combined (excluding SVG as it needs special handling)
const assetExtensions = [
...imageExtensions,
...videoExtensions,
...audioExtensions,
...fontExtensions,
...documentExtensions
];
// All extensions including SVG
const allAssetExtensions = [
...assetExtensions,
...svgExtensions
];
function normalizePath() {
for(var _len = arguments.length, paths = new Array(_len), _key = 0; _key < _len; _key++){
paths[_key] = arguments[_key];
}
const f = path.join(...paths).replaceAll('\\', '/');
if (/^\.[/\\]/.test(paths[0])) return `./${f}`;
return f;
}
function getUrlOfPartial(url) {
const { dir, base } = path.parse(url);
return dir ? `${normalizePath(dir)}/_${base}` : `_${base}`;
}
const arrayFmt = (arr)=>arr.map((id, i, arr)=>{
const fmt = `\`${id}\``;
switch(i){
case arr.length - 1:
{
return `or ${fmt}`;
}
case arr.length - 2:
{
return fmt;
}
default:
{
return `${fmt},`;
}
}
}).join(' ');
const baseDir = path.dirname(fileURLToPath(import.meta.url));
const packageFilterBuilder = function() {
let path = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : '.', opts = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
const conditions = opts.conditions ?? [
'style',
'import',
'require'
];
const fields = opts.fields ?? [
'style',
'module',
'main'
];
return (pkg)=>{
// Check `exports` fields
try {
const resolvedExport = resolve(pkg, path, {
conditions,
unsafe: true
});
if (typeof resolvedExport === 'string') {
pkg.main = resolvedExport;
return pkg;
}
} catch {
/* noop */ }
// Check independent fields
try {
const resolvedField = legacy(pkg, {
fields,
browser: false
});
if (typeof resolvedField === 'string') {
pkg.main = resolvedField;
return pkg;
}
} catch {
/* noop */ }
return pkg;
};
};
const defaultOpts = {
caller: 'Resolver',
basedirs: [
baseDir
],
extensions: [
'.mjs',
'.js',
'.cjs',
'.json'
],
preserveSymlinks: true,
packageFilter: packageFilterBuilder()
};
const resolverSync = function(id) {
let options = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
try {
return resolver.sync(id, {
...options,
pathFilter: (pkg, _path, relativePath)=>{
const result = resolve(pkg, relativePath);
if (result) return result[0];
return '';
}
});
} catch {
return;
}
};
function resolveSync(ids, userOpts) {
const options = {
...defaultOpts,
...userOpts
};
for (const basedir of options.basedirs){
const opts = {
...options,
basedir,
basedirs: undefined,
caller: undefined
};
for (const id of ids){
const resolved = resolverSync(id, opts);
if (resolved) return resolved;
}
}
throw new Error(`${options.caller} could not resolve ${arrayFmt(ids)}`);
}
const finalize = (id)=>({
file: id.replace(/\.css$/i, '')
});
const conditions = [
'sass',
'style'
];
const getRollupStylerPlugin = (param)=>{
let { options, cssFilename } = param;
return styles({
autoModules: true,
extensions: stylesExtensions,
mode: [
'extract',
cssFilename
],
sourceMap: true,
to: cssFilename,
sass: {
importer: (url, importer)=>{
if (path.isAbsolute(url) || url.startsWith('.')) return null;
const partialUrl = getUrlOfPartial(url);
const resolverOptions = {
caller: 'Sass importer',
basedirs: [
path.dirname(importer)
],
extensions: stylesExtensions,
packageFilter: packageFilterBuilder(url, {
conditions
})
};
try {
return finalize(resolveSync([
partialUrl,
url
], resolverOptions));
} catch {
return null;
}
}
},
plugins: [
postcssImport({
root: options.cwd
})
]
});
};
const getRollupAssetPlugins = ()=>{
return [
// Custom plugin to handle ?url suffix
{
name: 'url-suffix-handler',
resolveId (id) {
if (id.endsWith('?url')) {
// Remove the ?url suffix and resolve the actual file
const actualId = id.slice(0, -4);
return actualId;
}
return null;
}
},
// Handle SVG files with dual support: React components and URL imports
svgr({
exportType: 'named',
ref: true,
svgo: true,
titleProp: true,
include: '**/*.svg'
}),
// Handle all asset files including SVGs
url({
include: [
...assetExtensions,
...svgExtensions
].map((ext)=>`**/*${ext}`),
limit: 8192,
fileName: 'public/[name]-[hash][extname]',
publicPath: './'
})
];
};
const getRollupAssetPluginsForBin = ()=>{
// For binary builds, we typically don't want to inline assets
// and we don't need React component support for SVGs
return [
url({
include: [
...assetExtensions,
...svgExtensions
].map((ext)=>`**/*${ext}`),
limit: 0,
fileName: 'assets/[name]-[hash][extname]',
publicPath: './'
})
];
};
const bundleLibIfNeeded = async (ctx)=>{
const { pkg, options, libOutputs, pkgPath, resolvedSource } = ctx;
if (isNil(resolvedSource) || isEmptyArray(libOutputs)) return;
await Promise.all(libOutputs.map(async (output)=>{
const bundle = await rollup({
input: resolvedSource,
plugins: [
PeerDepsExternalPlugin({
includeDependencies: true,
packageJsonPath: pkgPath
}),
typescriptPaths({
preserveExtensions: true
}),
json(),
...getRollupAssetPlugins(),
swc({
...getSwcConfig(ctx, output),
tsconfig: ctx.tsconfigPath ?? false
}),
commonjs({
extensions: jsExtensions$1
}),
nodeResolve({
rootDir: options.cwd,
extensions: [
...jsExtensions$1,
...stylesExtensions,
...allAssetExtensions
]
}),
getRollupStylerPlugin(ctx),
isString(pkg.types) && isStringFull(pkg.types) && typescript(getRollupTypescriptConfig(ctx))
]
});
await bundle.write(output);
}));
};
const bundleBinIfNeeded = async (ctx)=>{
const { pkg, pkgPath, options, binOutput } = ctx;
if (isNil(pkg['bin:source']) || isNil(binOutput)) return;
const bundle = await rollup({
input: path.resolve(options.cwd, pkg['bin:source']),
plugins: [
PeerDepsExternalPlugin({
includeDependencies: true,
packageJsonPath: pkgPath
}),
typescriptPaths({
preserveExtensions: true
}),
json(),
...getRollupAssetPluginsForBin(),
swc({
...getSwcConfig(ctx, binOutput),
tsconfig: ctx.tsconfigPath ?? false
}),
commonjs({
extensions: jsExtensions$1
}),
nodeResolve({
rootDir: options.cwd,
extensions: [
...jsExtensions$1,
...stylesExtensions,
...allAssetExtensions
]
})
]
});
await bundle.write(binOutput);
};
const getDefaultOptions = ()=>({
cwd: '.'
});
const getExportsFilenames = (exs)=>{
if (isNil(exs) || isObject(exs) && isEmptyObject(exs) || isString(exs) && isEmptyString(exs)) return [];
if (isString(exs)) return [
exs
];
return Object.values(exs).flatMap((ex)=>getExportsFilenames(ex));
};
const getFilenameOutputFormat = function(filename) {
let isModule = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false;
const ext = path.extname(filename);
filename.substring;
switch(ext){
case '.mjs':
return 'esm';
case '.cjs':
return 'cjs';
}
const filenameWithoutExt = filename.substring(0, filename.length - ext.length);
const luxuryExt = path.extname(filenameWithoutExt);
if (!luxuryExt) {
return isModule ? 'esm' : 'cjs';
}
switch(luxuryExt){
case '.umd':
return 'umd';
case '.amd':
return 'amd';
case '.iife':
return 'iife';
}
throw new Error('[lbundle] unknown output format for filename: ' + filename);
};
const getLibOutputs = (param)=>{
let { options, globalName, pkg, isModule, cssFilename } = param;
const map = new Map();
[
pkg.main,
pkg.module,
pkg.unpkg,
...getExportsFilenames(pkg.exports)
].filter((filename)=>isString(filename) && jsExtensions[path.extname(filename)]).map((filename)=>{
const format = getFilenameOutputFormat(filename, isModule);
const ext = path.extname(filename);
const preserveModules = preserveModulesFormats[format];
const entryFileNames = preserveModules ? `[name]${ext}` : undefined;
const dir = preserveModules ? path.resolve(options.cwd, path.dirname(filename)) : undefined;
const file = preserveModules ? undefined : path.resolve(options.cwd, filename);
return {
name: globalName,
format,
file,
dir,
entryFileNames,
preserveModules,
assetFileNames: (param)=>{
let { names } = param;
const normalizedCssFilename = path.posix.normalize(cssFilename);
if (names.includes(normalizedCssFilename)) {
if (isDefined(dir)) return path.relative(dir, path.resolve(options.cwd, normalizedCssFilename));
return normalizedCssFilename;
}
return 'assets/[name]-[hash][extname]';
},
esModule: format === 'cjs',
strict: true,
sourcemap: true
};
}).forEach((output)=>map.set(output.format, output));
return [
...map.values()
];
};
const preserveModulesFormats = {
'esm': true,
'cjs': true
};
const jsExtensions = {
'.js': true,
'.cjs': true,
'.mjs': true
};
const getBinOutput = (param)=>{
let { options, pkg, isModule } = param;
if (isNil(pkg.bin) || isObject(pkg.bin) && isEmptyObject(pkg.bin)) return;
if (isObject(pkg.bin) && Object.keys(pkg.bin).length > 1) {
console.warn("[lbundle] multiple bin isn't supported yet");
}
const binSource = isString(pkg.bin) ? pkg.bin : Object.values(pkg.bin)[0];
return {
format: getFilenameOutputFormat(binSource, isModule),
file: path.resolve(options.cwd, binSource),
strict: true,
sourcemap: true
};
};
const getCSSFilename = (param)=>{
let { pkg } = param;
const filenames = getExportsFilenames(pkg.exports);
return filenames.find((filename)=>path.extname(filename) === '.css');
};
const isTs = (pkg)=>{
if (pkg.source && path.extname(pkg.source).includes('ts')) return true;
return !!pkg.dependencies?.typescript || !!pkg.devDependencies?.typescript || !!pkg.peerDependencies?.typescript;
};
const isJsx = (pkg)=>{
if (pkg.source && path.extname(pkg.source).includes('sx')) return true;
return !!pkg.dependencies?.react || !!pkg.devDependencies?.react || !!pkg.peerDependencies?.react;
};
const getCtx = async (baseOptions)=>{
const options = merge(getDefaultOptions(), baseOptions);
const pkgPath = path.resolve(options.cwd, 'package.json');
const pkg = JSON.parse(await fs.promises.readFile(pkgPath, 'utf-8'));
const tsconfigPath = path.resolve(options.cwd, 'tsconfig.json');
const swcPath = path.resolve(options.cwd, '.swc');
if (isNil(pkg.source) && isNil(pkg['bin:source'])) {
throw new Error('[bundle] provide source entry for you library, set `package.json` `source` or `bin:source` field');
}
const isModule = pkg.type === 'module';
const globalName = pascalCase(pkg.name);
const cssFilename = getCSSFilename({
pkg
}) ?? 'index.css';
const libOutputs = getLibOutputs({
pkg,
globalName,
options,
isModule,
cssFilename
});
const binOutput = getBinOutput({
pkg,
options,
isModule
});
const resolvedSource = pkg.source ? path.resolve(options.cwd, pkg.source) : undefined;
const resolvedBinSource = pkg['bin:source'] ? path.resolve(options.cwd, pkg['bin:source']) : undefined;
const ts = isTs(pkg);
const jsx = isJsx(pkg);
return {
baseOptions: baseOptions,
options: options,
globalName,
pkgPath,
pkg,
swcPath: fs.existsSync(swcPath) ? swcPath : undefined,
tsconfigPath: fs.existsSync(tsconfigPath) ? tsconfigPath : undefined,
isModule,
resolvedSource,
resolvedBinSource,
isTs: ts,
isJsx: jsx,
libOutputs,
binOutput,
cssFilename
};
};
const lbundle = async (baseOptions)=>{
const ctx = await getCtx(baseOptions);
await Promise.all([
bundleLibIfNeeded(ctx),
bundleBinIfNeeded(ctx)
]);
};
var name = "lbundle";
var version = "1.5.5";
var pkg = {
name: name,
version: version};
const prog = new Command(pkg.name);
prog.version(pkg.version).option('-c, --cwd <cwd>', 'root dir path of your lib', '.').parse();
lbundle(prog.opts());
//# sourceMappingURL=cli.mjs.map