UNPKG

lbundle

Version:

Small zero-configuration bundler build on top of Rollup.js and SWC for NPM libraries

556 lines (529 loc) 16.5 kB
#!/usr/bin/env node 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 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 = [ '.mtsx', '.ctsx', '.tsx', '.mts', '.cts', '.ts', '.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' ]; 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 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(), swc({ ...getSwcConfig(ctx, output), tsconfig: ctx.tsconfigPath ?? false }), commonjs({ extensions: jsExtensions$1 }), nodeResolve({ rootDir: options.cwd, extensions: [ ...jsExtensions$1, ...stylesExtensions ] }), 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(), swc({ ...getSwcConfig(ctx, binOutput), tsconfig: ctx.tsconfigPath ?? false }), commonjs({ extensions: jsExtensions$1 }), nodeResolve({ rootDir: options.cwd, extensions: [ ...jsExtensions$1, ...stylesExtensions ] }) ] }); 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.4"; 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