UNPKG

bunchee

Version:

zero config bundler for js/ts/jsx libraries

1,258 lines (1,232 loc) 53.7 kB
#!/usr/bin/env node var path = require('path'); var yargs = require('yargs'); var helpers = require('yargs/helpers'); var perf_hooks = require('perf_hooks'); var fs = require('fs'); var fsp = require('fs/promises'); var require$$0 = require('tty'); var picomatch = require('picomatch'); var index_js = require('../index.js'); require('module'); var fastGlob = require('fast-glob'); var prettyBytes = require('pretty-bytes'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var path__default = /*#__PURE__*/_interopDefault(path); var yargs__default = /*#__PURE__*/_interopDefault(yargs); var fs__default = /*#__PURE__*/_interopDefault(fs); var fsp__default = /*#__PURE__*/_interopDefault(fsp); var require$$0__default = /*#__PURE__*/_interopDefault(require$$0); var picomatch__default = /*#__PURE__*/_interopDefault(picomatch); var prettyBytes__default = /*#__PURE__*/_interopDefault(prettyBytes); const availableExtensions = new Set([ 'js', 'cjs', 'mjs', 'jsx', 'ts', 'tsx', 'cts', 'mts' ]); // You can find the list of runtime keys here: // https://runtime-keys.proposal.wintercg.org/ const runtimeExportConventions = new Set([ 'electron', 'react-server', 'react-native', 'edge-light', 'node', 'deno', 'bun', 'workerd', // Browser only 'browser' ]); const optimizeConventions = new Set([ 'development', 'production' ]); const specialExportConventions = new Set([ ...runtimeExportConventions, ...optimizeConventions ]); const SRC = 'src'; const DIST = 'dist'; const dtsExtensionsMap = { js: 'd.ts', cjs: 'd.cts', mjs: 'd.mts' }; const tsExtensions = new Set([ 'ts', 'tsx', 'cts', 'mts' ]); const DEFAULT_TS_CONFIG = { compilerOptions: { target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler' }, include: [ 'src' ] }; const BINARY_TAG = '$binary'; const PRIVATE_GLOB_PATTERN = '**/_*/**'; const TESTS_GLOB_PATTERN = '**/{__tests__/**,__mocks__/**,*.{test,spec}.*}'; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var picocolors = {exports: {}}; var hasRequiredPicocolors; function requirePicocolors () { if (hasRequiredPicocolors) return picocolors.exports; hasRequiredPicocolors = 1; let tty = require$$0__default.default; let isColorSupported = !("NO_COLOR" in process.env || process.argv.includes("--no-color")) && ("FORCE_COLOR" in process.env || process.argv.includes("--color") || process.platform === "win32" || tty.isatty(1) && process.env.TERM !== "dumb" || "CI" in process.env); let formatter = (open, close, replace = open)=>(input)=>{ let string = "" + input; let index = string.indexOf(close, open.length); return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close; }; let replaceClose = (string, close, replace, index)=>{ let start = string.substring(0, index) + replace; let end = string.substring(index + close.length); let nextIndex = end.indexOf(close); return ~nextIndex ? start + replaceClose(end, close, replace, nextIndex) : start + end; }; let createColors = (enabled = isColorSupported)=>({ isColorSupported: enabled, reset: enabled ? (s)=>`\x1b[0m${s}\x1b[0m` : String, bold: enabled ? formatter("\x1b[1m", "\x1b[22m", "\x1b[22m\x1b[1m") : String, dim: enabled ? formatter("\x1b[2m", "\x1b[22m", "\x1b[22m\x1b[2m") : String, italic: enabled ? formatter("\x1b[3m", "\x1b[23m") : String, underline: enabled ? formatter("\x1b[4m", "\x1b[24m") : String, inverse: enabled ? formatter("\x1b[7m", "\x1b[27m") : String, hidden: enabled ? formatter("\x1b[8m", "\x1b[28m") : String, strikethrough: enabled ? formatter("\x1b[9m", "\x1b[29m") : String, black: enabled ? formatter("\x1b[30m", "\x1b[39m") : String, red: enabled ? formatter("\x1b[31m", "\x1b[39m") : String, green: enabled ? formatter("\x1b[32m", "\x1b[39m") : String, yellow: enabled ? formatter("\x1b[33m", "\x1b[39m") : String, blue: enabled ? formatter("\x1b[34m", "\x1b[39m") : String, magenta: enabled ? formatter("\x1b[35m", "\x1b[39m") : String, cyan: enabled ? formatter("\x1b[36m", "\x1b[39m") : String, white: enabled ? formatter("\x1b[37m", "\x1b[39m") : String, gray: enabled ? formatter("\x1b[90m", "\x1b[39m") : String, bgBlack: enabled ? formatter("\x1b[40m", "\x1b[49m") : String, bgRed: enabled ? formatter("\x1b[41m", "\x1b[49m") : String, bgGreen: enabled ? formatter("\x1b[42m", "\x1b[49m") : String, bgYellow: enabled ? formatter("\x1b[43m", "\x1b[49m") : String, bgBlue: enabled ? formatter("\x1b[44m", "\x1b[49m") : String, bgMagenta: enabled ? formatter("\x1b[45m", "\x1b[49m") : String, bgCyan: enabled ? formatter("\x1b[46m", "\x1b[49m") : String, bgWhite: enabled ? formatter("\x1b[47m", "\x1b[49m") : String }); picocolors.exports = createColors(); picocolors.exports.createColors = createColors; return picocolors.exports; } var picocolorsExports = /*@__PURE__*/ requirePicocolors(); var pc = /*@__PURE__*/getDefaultExportFromCjs(picocolorsExports); const defaultColorFn = (text)=>text; function color(prefixColor) { return pc.isColorSupported ? pc[prefixColor] : defaultColorFn; } const logger = { log (...arg) { console.log(...arg); }, warn (...arg) { console.warn(color('yellow')('!'), ...arg); }, error (...arg) { console.error(color('red')('⨯'), ...arg); }, info (...arg) { console.log(color('green')('✓'), ...arg); } }; function posixRelativify(path) { return path.startsWith('.') ? path : `./${path}`; } // Example: ./src/util/foo.development.ts -> foo.development // Example: ./src/util/foo.react-server.ts -> foo.react-server const baseNameWithoutExtension = (filePath)=>{ return path__default.default.basename(filePath, path__default.default.extname(filePath)); }; function validateEntryFiles(entryFiles) { const fileBasePaths = new Set(); const duplicatePaths = new Set(); for (const filePath of entryFiles){ // Check if there are multiple files with the same base name const filePathWithoutExt = filePath.slice(0, -path__default.default.extname(filePath).length).replace(/\\/g, '/'); const segments = filePathWithoutExt.split('/'); let lastSegment = segments[segments.length - 1]; while(lastSegment && (lastSegment === 'index' || lastSegment === '')){ segments.pop(); lastSegment = segments[segments.length - 1]; } const fileBasePath = segments.join('/'); if (fileBasePaths.has(fileBasePath)) { duplicatePaths.add(// Add a dot if the base name is empty, 'foo' -> './foo', '' -> '.' './' + filePath.replace(/\\/g, '/')); } fileBasePaths.add(fileBasePath); } if (duplicatePaths.size > 0) { throw new Error(`Conflicted entry files found for entries: ${[ ...duplicatePaths ].join(', ')}`); } } function exit(err) { logger.error(err); process.exit(1); } function hasPackageJson(cwd) { return fileExists(path__default.default.resolve(cwd, 'package.json')); } async function getPackageMeta(cwd) { const pkgFilePath = path__default.default.resolve(cwd, 'package.json'); let targetPackageJson = {}; try { targetPackageJson = JSON.parse(await fsp__default.default.readFile(pkgFilePath, { encoding: 'utf-8' })); } catch (_) {} return targetPackageJson; } function isTypescriptFile(filename) { const ext = path__default.default.extname(filename).slice(1); return tsExtensions.has(ext); } function fileExists(filePath) { return fs__default.default.existsSync(filePath); } const hasCjsExtension = (filename)=>path__default.default.extname(filename) === '.cjs'; const getMainFieldExportType = (pkg)=>{ const isEsmPkg = isESModulePackage(pkg.type); const mainExportType = isEsmPkg && pkg.main ? hasCjsExtension(pkg.main) ? 'require' : 'import' : 'require'; return mainExportType; }; const isTestFile = (filename)=>/\.(test|spec)$/.test(baseNameWithoutExtension(filename)); function joinRelativePath(...segments) { let result = path__default.default.join(...segments); // If the first segment starts with '.', ensure the result does too. if (segments[0] === '.' && !result.startsWith('.')) { result = './' + result; } return result; } function isESModulePackage(packageType) { return packageType === 'module'; } function isBinExportPath(exportPath) { return exportPath === BINARY_TAG || exportPath.startsWith(BINARY_TAG + '/'); } function isTypeFile(filename) { return filename.endsWith('.d.ts') || filename.endsWith('.d.mts') || filename.endsWith('.d.cts'); } // shared.ts -> ./shared // shared.<export condition>.ts -> ./shared.<export condition> // index.ts -> ./index // index.development.ts -> ./index.development // foo/index.ts -> ./foo function sourceFilenameToExportFullPath(filename) { const ext = path__default.default.extname(filename); const exportPath = filename.slice(0, -ext.length); return posixRelativify(exportPath); } // If the file is matching the private module convention file export path. // './lib/_foo' -> true // './_util/index' -> true // './lib/_foo/bar' -> true // './foo' -> false function isPrivateExportPath(exportPath) { return /\/_/.test(exportPath); } function normalizePath(filePath) { return filePath.replace(/\\/g, '/'); } function collectExportPath(exportValue, exportKey, currentPath, exportTypes, exportToDist) { // End of searching, export value is file path. // <export key>: <export value> (string) if (typeof exportValue === 'string') { const composedTypes = new Set(exportTypes); const exportType = exportKey.startsWith('.') ? 'default' : exportKey; composedTypes.add(exportType); const exportInfo = exportToDist.get(mapExportFullPath(currentPath)); const exportCondition = Array.from(composedTypes).join('.'); if (!exportInfo) { const outputConditionPair = [ exportValue, exportCondition ]; addToExportDistMap(exportToDist, currentPath, [ outputConditionPair ]); } else { exportInfo.push([ exportValue, exportCondition ]); } return; } const exportKeys = Object.keys(exportValue); for (const exportKey of exportKeys){ // Clone the set to avoid modifying the parent set const childExports = new Set(exportTypes); // Normalize child export value to a map const childExportValue = exportValue[exportKey]; // Visit export path: ./subpath, ./subpath2, ... if (exportKey.startsWith('.')) { const childPath = joinRelativePath(currentPath, exportKey); collectExportPath(childExportValue, exportKey, childPath, childExports, exportToDist); } else { // Visit export type: import, require, ... childExports.add(exportKey); collectExportPath(childExportValue, exportKey, currentPath, childExports, exportToDist); } } } const mapExportFullPath = (exportPath)=>exportPath === '.' ? './index' : exportPath; function addToExportDistMap(exportToDist, exportPath, outputConditionPairs) { const fullPath = mapExportFullPath(exportPath); const existingExportInfo = exportToDist.get(fullPath); if (!existingExportInfo) { exportToDist.set(fullPath, outputConditionPairs); } else { existingExportInfo.push(...outputConditionPairs); } } /** * parseExports - parse package.exports field and other fields like main,module to a map * * map from export path to output path and export conditions * * exportToDist: { * './index': { development: ..., default: ... } * './index.react-server': { development: ..., default: ... } * } */ function parseExports(pkg) { var _pkg_exports; const exportsField = (_pkg_exports = pkg.exports) != null ? _pkg_exports : {}; var _pkg_bin; const bins = (_pkg_bin = pkg.bin) != null ? _pkg_bin : {}; const exportToDist = new Map(); const isEsmPkg = isESModulePackage(pkg.type); const defaultCondition = isEsmPkg ? 'import' : 'require'; let currentPath = '.'; if (typeof exportsField === 'string') { const outputConditionPair = [ exportsField, defaultCondition ]; addToExportDistMap(exportToDist, currentPath, [ outputConditionPair ]); } else { // keys means unknown if they're relative path or export type const exportConditionKeys = Object.keys(exportsField); for (const exportKey of exportConditionKeys){ const exportValue = exportsField[exportKey]; const exportTypes = new Set(); const isExportPath = exportKey.startsWith('.'); const childPath = isExportPath ? joinRelativePath(currentPath, exportKey) : currentPath; if (!isExportPath) { exportTypes.add(exportKey); } collectExportPath(exportValue, exportKey, childPath, exportTypes, exportToDist); } } if (typeof bins === 'string') { const outputConditionPair = [ bins, defaultCondition ]; addToExportDistMap(exportToDist, BINARY_TAG, [ outputConditionPair ]); } else { for (const binName of Object.keys(bins)){ const binDistPath = bins[binName]; const exportType = getExportTypeFromFile(binDistPath, pkg.type); const exportPath = path.posix.join(BINARY_TAG, binName); const outputConditionPair = [ binDistPath, exportType ]; addToExportDistMap(exportToDist, exportPath, [ outputConditionPair ]); } } // Handle package.json global exports fields if (pkg.main || pkg.module || pkg.types) { const mainExportPath = pkg.main; const moduleExportPath = pkg.module; const typesEntryPath = pkg.types; addToExportDistMap(exportToDist, './index', [ Boolean(mainExportPath) && [ mainExportPath, getMainFieldExportType(pkg) ], Boolean(moduleExportPath) && [ moduleExportPath, 'module' ], Boolean(typesEntryPath) && [ typesEntryPath, 'types' ] ].filter(Boolean)); } return exportToDist; } function getExportTypeFromFile(filename, pkgType) { const isESModule = isESModulePackage(pkgType); const isCjsExt = filename.endsWith('.cjs'); const isEsmExt = filename.endsWith('.mjs'); const exportType = isEsmExt ? 'import' : isCjsExt ? 'require' : isESModule ? 'import' : 'require'; return exportType; } const matchFile = (matchingPattern, filePath)=>{ return matchingPattern.some((pattern)=>{ // pattern is always posix const normalizedPattern = path.posix.normalize(pattern); const expandedPattern = normalizedPattern.endsWith('/') ? `${normalizedPattern}**` : `${normalizedPattern}/**`; const matcher = picomatch__default.default(expandedPattern); const normalizedFilePath = path.posix.normalize(filePath); return matcher(normalizedFilePath); }); }; function validateTypesFieldCondition(pair) { const [outputPath, composedExportType] = pair; const exportTypes = new Set(composedExportType.split('.')); if (!exportTypes.has('types') && isTypeFile(outputPath)) { return true; } return false; } function validateFilesField(packageJson) { const state = { missingFiles: [] }; const filesField = packageJson.files || [ '*' ]; const exportsField = packageJson.exports || {}; const resolveExportsPaths = (exports)=>{ const paths = []; if (typeof exports === 'string') { paths.push(exports); } else if (typeof exports === 'object') { for(const key in exports){ paths.push(...resolveExportsPaths(exports[key])); } } return paths; }; const exportedPaths = resolveExportsPaths(exportsField).map((p)=>normalizePath(path__default.default.normalize(p))); const commonFields = [ 'main', 'module', 'types', 'module-sync' ]; for (const field of commonFields){ if (field in packageJson) { exportedPaths.push(packageJson[field]); } } state.missingFiles = exportedPaths.filter((exportPath)=>{ // Special case for package.json if (exportPath === 'package.json') { return false; } return !matchFile(filesField, exportPath); }); return state; } function lint$1(pkg) { const { name, main, exports } = pkg; const isESM = isESModulePackage(pkg.type); const parsedExports = parseExports(pkg); if (!name) { logger.warn('Missing package name'); } const exportsState = { badMainExtension: false, badMainExport: false, invalidExportsFieldType: false, badCjsRequireExport: { value: false, paths: [] }, badCjsImportExport: { value: false, paths: [] }, badEsmRequireExport: { value: false, paths: [] }, badEsmImportExport: { value: false, paths: [] }, badTypesExport: [] }; // Validate ESM package if (isESM) { if (exports) { if (typeof exports === 'string') { if (hasCjsExtension(exports)) { exportsState.badMainExport = true; } } else if (typeof exports !== 'object') { exportsState.invalidExportsFieldType = true; } else { parsedExports.forEach((outputPairs)=>{ for (const outputPair of outputPairs){ const [outputPath, composedExportType] = outputPair; if (validateTypesFieldCondition([ outputPath, composedExportType ])) { exportsState.badTypesExport.push([ outputPath, composedExportType ]); } const exportTypes = new Set(composedExportType.split('.')); let requirePath = ''; let importPath = ''; if (exportTypes.has('require')) { requirePath = outputPath; } if (exportTypes.has('import')) { importPath = outputPath; } const requireExt = requirePath && path__default.default.extname(requirePath); const importExt = importPath && path__default.default.extname(importPath); if (requireExt === '.mjs' || requireExt === '.js') { exportsState.badEsmRequireExport.value = true; exportsState.badEsmRequireExport.paths.push(requirePath); } if (importExt === '.cjs') { exportsState.badEsmImportExport.value = true; exportsState.badEsmImportExport.paths.push(importPath); } } }); } } } else { // Validate CJS package if (main && path__default.default.extname(main) === '.mjs') { exportsState.badMainExtension = true; } if (exports) { if (typeof exports === 'string') { if (path__default.default.extname(exports) === '.mjs') { exportsState.badMainExport = true; } } else if (typeof exports !== 'object') { exportsState.invalidExportsFieldType = true; } else { parsedExports.forEach((outputPairs)=>{ for (const outputPair of outputPairs){ const [outputPath, composedExportType] = outputPair; if (validateTypesFieldCondition([ outputPath, composedExportType ])) { exportsState.badTypesExport.push([ outputPath, composedExportType ]); } const exportTypes = new Set(composedExportType.split('.')); let requirePath = ''; let importPath = ''; if (exportTypes.has('require')) { requirePath = outputPath; } if (exportTypes.has('import')) { importPath = outputPath; } const requireExt = requirePath && path__default.default.extname(requirePath); const importExt = importPath && path__default.default.extname(importPath); if (requireExt === '.mjs') { exportsState.badCjsRequireExport.value = true; exportsState.badCjsRequireExport.paths.push(requirePath); } if (importExt === '.js' || importExt === '.cjs') { exportsState.badCjsImportExport.value = true; exportsState.badCjsImportExport.paths.push(importPath); } } }); } } } const fieldState = validateFilesField(pkg); const warningsCount = exportsState.badTypesExport.length + fieldState.missingFiles.length; if (warningsCount) { logger.warn(`Lint: ${warningsCount} issues found.`); } if (fieldState.missingFiles.length) { logger.warn('Missing files in package.json'); fieldState.missingFiles.forEach((p)=>{ logger.warn(` ${p}`); }); } if (exportsState.badMainExtension) { logger.warn('Cannot export `main` field with .mjs extension in CJS package, only .js extension is allowed'); } if (exportsState.badMainExport) { if (isESM) { logger.warn('Cannot export `exports` field with .cjs extension in ESM package, only .mjs and .js extensions are allowed'); } else { logger.warn('Cannot export `exports` field with .mjs extension in CJS package, only .js and .cjs extensions are allowed'); } } if (exportsState.invalidExportsFieldType) { logger.warn('Invalid exports field type, only object or string is allowed'); } if (exportsState.badCjsRequireExport.value) { logger.warn('Cannot export `require` field with .mjs extension in CJS package, only .cjs and .js extensions are allowed'); exportsState.badCjsRequireExport.paths.forEach((p)=>{ logger.warn(` ${p}`); }); } if (exportsState.badCjsImportExport.value) { logger.warn('Cannot export `import` field with .js or .cjs extension in CJS package, only .mjs extensions are allowed'); exportsState.badCjsImportExport.paths.forEach((p)=>{ logger.warn(` ${p}`); }); } if (exportsState.badEsmRequireExport.value) { logger.warn('Cannot export `require` field with .js or .mjs extension in ESM package, only .cjs extensions are allowed'); exportsState.badEsmRequireExport.paths.forEach((p)=>{ logger.warn(` ${p}`); }); } if (exportsState.badEsmImportExport.value) { logger.warn('Cannot export `import` field with .cjs extension in ESM package, only .js and .mjs extensions are allowed'); exportsState.badEsmImportExport.paths.forEach((p)=>{ logger.warn(` ${p}`); }); } if (exportsState.badTypesExport.length) { exportsState.badTypesExport.forEach(([outputPath, composedExportType])=>{ logger.error(`Bad export types field with ${composedExportType} in ${outputPath}, use "types" export condition for it`); }); } } var version = "6.6.0"; async function writeDefaultTsconfig(tsConfigPath) { await fs.promises.writeFile(tsConfigPath, JSON.stringify(DEFAULT_TS_CONFIG, null, 2), 'utf-8'); logger.log(`Detected using TypeScript but tsconfig.json is missing, created a ${pc.blue('tsconfig.json')} for you.`); } // ./index -> default // ./index.development -> development // ./index.react-server -> react-server function getExportTypeFromExportPath(exportPath) { // Skip the first two segments: `.` and `index` const exportTypes = exportPath.split('.').slice(2); return getExportTypeFromExportTypesArray(exportTypes); } function getSpecialExportTypeFromComposedExportPath(composedExportType) { const exportTypes = composedExportType.split('.'); for (const exportType of exportTypes){ if (specialExportConventions.has(exportType)) { return exportType; } } return 'default'; } function getExportTypeFromExportTypesArray(types) { let exportType = 'default'; new Set(types).forEach((value)=>{ if (specialExportConventions.has(value)) { exportType = value; } else if (value === 'import' || value === 'require' || value === 'types') { exportType = value; } }); return exportType; } // ./index -> . // ./index.development -> . // ./index.react-server -> . // ./shared -> ./shared // ./shared.development -> ./shared // $binary -> $binary // $binary/index -> $binary // $binary/foo -> $binary/foo function normalizeExportPath(exportPath) { if (exportPath.startsWith(BINARY_TAG)) { if (exportPath === `${BINARY_TAG}/index`) { exportPath = BINARY_TAG; } return exportPath; } const baseName = exportPath.split('.').slice(0, 2).join('.'); if (baseName === './index') { return '.'; } return baseName; } async function collectSourceEntriesByExportPath(sourceFolderPath, originalSubpath, bins, exportsEntries) { const isBinaryPath = isBinExportPath(originalSubpath); const subpath = originalSubpath.replace(BINARY_TAG, 'bin'); const absoluteDirPath = path__default.default.join(sourceFolderPath, subpath); const dirName = path__default.default.dirname(subpath) // Get directory name regardless of file/directory ; const baseName = path__default.default.basename(subpath) // Get base name regardless of file/directory ; const dirPath = path__default.default.join(sourceFolderPath, dirName); // Match <name>{,/index}.{<ext>,<runtime>.<ext>} const entryFilesPatterns = [ `${baseName}.{${[ ...availableExtensions ].join(',')}}`, `${baseName}.{${[ ...runtimeExportConventions ].join(',')}}.{${[ ...availableExtensions ].join(',')}}`, `${baseName}/index.{${[ ...availableExtensions ].join(',')}}`, `${baseName}/index.{${[ ...runtimeExportConventions ].join(',')}}.{${[ ...availableExtensions ].join(',')}}` ]; const entryFiles = await fastGlob.glob(entryFilesPatterns, { cwd: dirPath, ignore: [ PRIVATE_GLOB_PATTERN ] }); validateEntryFiles(entryFiles); for (const file of entryFiles){ const ext = path__default.default.extname(file).slice(1); if (!availableExtensions.has(ext) || isTestFile(file)) continue; const sourceFileAbsolutePath = path__default.default.join(dirPath, file); const exportPath = posixRelativify(fs.existsSync(absoluteDirPath) && (await fsp__default.default.stat(absoluteDirPath)).isDirectory() ? subpath : originalSubpath); if (isBinaryPath) { bins.set(normalizeExportPath(originalSubpath), sourceFileAbsolutePath); } else { const parts = path__default.default.basename(file).split('.'); const exportType = parts.length > 2 ? parts[1] : getExportTypeFromExportPath(exportPath); const specialExportPath = exportType !== 'index' && parts.length > 2 ? exportPath + '.' + exportType : exportPath // Adjust for direct file matches ; const sourceFilesMap = exportsEntries.get(specialExportPath) || {}; sourceFilesMap[exportType] = sourceFileAbsolutePath; if (specialExportConventions.has(exportType)) { const fallbackExportPath = sourceFilenameToExportFullPath(originalSubpath); const fallbackSourceFilesMap = exportsEntries.get(fallbackExportPath) || {}; Object.assign(sourceFilesMap, fallbackSourceFilesMap); } exportsEntries.set(specialExportPath, sourceFilesMap); } } } // For `prepare` command async function collectSourceEntries(sourceFolderPath) { const bins = new Map(); const exportsEntries = new Map(); if (!fs.existsSync(sourceFolderPath)) { return { bins, exportsEntries }; } // Match with global patterns // bin/**/*.<ext>, bin/**/index.<ext> const binPattern = `bin/**/*.{${[ ...availableExtensions ].join(',')}}`; const srcPattern = `**/*.{${[ ...availableExtensions ].join(',')}}`; const binMatches = await fastGlob.glob(binPattern, { cwd: sourceFolderPath, ignore: [ PRIVATE_GLOB_PATTERN, TESTS_GLOB_PATTERN ] }); const srcMatches = await fastGlob.glob(srcPattern, { cwd: sourceFolderPath, ignore: [ PRIVATE_GLOB_PATTERN, TESTS_GLOB_PATTERN ] }); for (const file of binMatches){ // convert relative path to export path const exportPath = sourceFilenameToExportFullPath(normalizePath(file)); const binExportPath = exportPath.replace(/^\.[\//]bin/, BINARY_TAG); await collectSourceEntriesByExportPath(sourceFolderPath, binExportPath, bins, exportsEntries); } for (const file of srcMatches){ const binExportPath = normalizePath(file).replace(/^bin/, BINARY_TAG)// Remove index.<ext> to [^index].<ext> to build the export path .replace(/(\/index)?\.[^/]+$/, ''); await collectSourceEntriesByExportPath(sourceFolderPath, binExportPath, bins, exportsEntries); } return { bins, exportsEntries }; } // Output with posix style in package.json function getDistPath(...subPaths) { return posixRelativify(path.posix.join(DIST, ...subPaths)); } function stripeBinaryTag(exportName) { // Add \ to decode leading $ return exportName.replace(/\$binary\//, ''); } const normalizeBaseNameToExportName = (name)=>{ const baseName = stripeBinaryTag(name); return /^\.\/index(\.|$)/.test(baseName) ? '.' : posixRelativify(baseName); }; function createExportCondition(exportName, sourceFile, moduleType) { const isTsSourceFile = isTypescriptFile(sourceFile); let cjsExtension = 'js'; let esmExtension = 'mjs'; if (moduleType === 'module') { cjsExtension = 'cjs'; esmExtension = 'js'; } if (exportName === '.') { exportName = 'index'; } if (isTsSourceFile) { return { import: { types: getDistPath('es', `${exportName}.${dtsExtensionsMap[esmExtension]}`), default: getDistPath('es', `${exportName}.${esmExtension}`) }, require: { types: getDistPath('cjs', `${exportName}.${dtsExtensionsMap[cjsExtension]}`), default: getDistPath('cjs', `${exportName}.${cjsExtension}`) } }; } return { import: getDistPath(`${exportName}.mjs`), require: getDistPath(`${exportName}.${cjsExtension}`) }; } function createExportConditionPair(exportName, sourceFile, moduleType) { // <exportName>.<specialCondition> let specialCondition; const specialConditionName = getSpecialExportTypeFromComposedExportPath(exportName); const normalizedExportPath = normalizeExportPath(exportName); if (specialConditionName !== 'default') { // e.g. // ./index.develop -> index // ./foo.react-server -> foo const fileBaseName = exportName.split('.').slice(0, 2).join('.').replace('./', ''); specialCondition = { [specialConditionName]: getDistPath('es', `${fileBaseName}-${specialConditionName}.mjs`) }; return [ normalizedExportPath, specialCondition ]; } const exportCond = createExportCondition(exportName, sourceFile, moduleType); return [ normalizedExportPath, exportCond ]; } async function prepare(cwd) { const sourceFolder = path__default.default.resolve(cwd, SRC); if (!fs__default.default.existsSync(sourceFolder)) { logger.error(`Source folder ${sourceFolder} does not exist. Cannot proceed to configure \`exports\` field.`); process.exit(1); } let hasPackageJson = false; const pkgJsonPath = path__default.default.join(cwd, 'package.json'); let pkgJson = {}; if (fs__default.default.existsSync(pkgJsonPath)) { hasPackageJson = true; const pkgJsonString = await fsp__default.default.readFile(pkgJsonPath, 'utf-8'); pkgJson = JSON.parse(pkgJsonString); } // configure `files` field with `dist` const files = pkgJson.files || []; if (!files.includes(DIST)) { files.push(DIST); } pkgJson.files = files; let isUsingTs = false; // Collect bins and exports entries const { bins, exportsEntries } = await collectSourceEntries(sourceFolder); const tsConfigPath = path__default.default.join(cwd, 'tsconfig.json'); const exportsSourceFiles = [ ...exportsEntries.values() ].reduce((acc, sourceFiles)=>{ Object.values(sourceFiles).forEach((sourceFile)=>acc.add(sourceFile)); return acc; }, new Set()); const allSourceFiles = [ ...exportsSourceFiles, ...bins.values() ].map((absoluteFilePath)=>absoluteFilePath); const hasTypeScriptFiles = allSourceFiles.some((filename)=>isTypescriptFile(filename)); if (hasTypeScriptFiles) { isUsingTs = true; if (!fs__default.default.existsSync(tsConfigPath)) { await writeDefaultTsconfig(tsConfigPath); } } // Configure as ESM package by default if there's no package.json if (!hasPackageJson) { pkgJson.type = 'module'; } if (bins.size > 0) { logger.log('Discovered binaries entries:'); const maxLengthOfBinName = Math.max(...Array.from(bins.keys()).map((binName)=>normalizeBaseNameToExportName(binName).length)); for (const [binName, binFile] of bins.entries()){ const spaces = ' '.repeat(Math.max(maxLengthOfBinName - normalizeBaseNameToExportName(binName).length, 0)); logger.log(` ${normalizeBaseNameToExportName(binName)}${spaces}: ${path__default.default.basename(binFile)}`); } if (bins.size === 1 && bins.has(BINARY_TAG)) { pkgJson.bin = getDistPath('bin', 'index.js'); } else { pkgJson.bin = {}; for (const [binOriginName] of bins.entries()){ const binName = stripeBinaryTag(binOriginName); pkgJson.bin[binName === '.' ? pkgJson.name : binName] = getDistPath('bin', binName + '.js'); } } } if (exportsEntries.size > 0) { logger.log('Discovered exports entries:'); const maxLengthOfExportName = Math.max(...Array.from(exportsEntries.keys()).map((exportName)=>normalizeBaseNameToExportName(exportName).length)); for (const [exportName, sourceFilesMap] of exportsEntries.entries()){ const spaces = ' '.repeat(Math.max(maxLengthOfExportName - normalizeBaseNameToExportName(exportName).length, 0)); for (const exportFile of Object.values(sourceFilesMap)){ logger.log(` ${normalizeBaseNameToExportName(exportName)}${spaces}: ${path__default.default.basename(exportFile)}`); } } const pkgExports = {}; for (const [exportName, sourceFilesMap] of exportsEntries.entries()){ for (const sourceFile of Object.values(sourceFilesMap)){ const [normalizedExportPath, conditions] = createExportConditionPair(exportName, sourceFile, pkgJson.type); pkgExports[normalizedExportPath] = { ...conditions, ...pkgExports[normalizedExportPath] }; } } // Configure node10 module resolution if (exportsEntries.has('./index')) { const isESM = pkgJson.type === 'module'; const mainExport = pkgExports['.']; const mainCondition = isESM ? 'import' : 'require'; pkgJson.main = isUsingTs ? mainExport[mainCondition].default : mainExport[mainCondition]; pkgJson.module = isUsingTs ? mainExport.import.default : mainExport.import; if (isUsingTs) { pkgJson.types = mainExport[mainCondition].types; } } // Assign the properties by order: files, main, module, types, exports if (Object.keys(pkgExports).length > 0) { if (!pkgJson.exports) { pkgJson.exports = pkgExports; } else { // Update existing exports Object.keys(pkgExports).forEach((exportName)=>{ pkgJson.exports[exportName] = { // Apply the new export conditions ...pkgJson.exports[exportName], // Keep the existing export conditions ...pkgJson.exports[exportName] }; }); } } } await fsp__default.default.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n'); logger.info('Configured `exports` in package.json'); } function normalizeExportName(exportName) { const isBinary = isBinExportPath(exportName); let result = exportName; if (isBinary) { result = (exportName.replace(new RegExp(`^\\${BINARY_TAG}\\/?`), '') || '.') + ' (bin)'; } else { const normalizedExportPath = normalizeExportPath(exportName); const specialConditionName = getSpecialExportTypeFromComposedExportPath(exportName); result = normalizedExportPath + (specialConditionName !== 'default' ? ` (${specialConditionName})` : ''); } return result; } function logOutputState(stats) { if (stats.size === 0) { logger.warn('No build info can be logged'); return; } const allFileNameLengths = Array.from(stats.values()).flat(1).map(([filename])=>filename.length); const maxFilenameLength = Math.max(...allFileNameLengths); const statsArray = [ ...stats.entries() ]// filter out empty file states. // e.g. if you're building a file that does not listed in the exports, or there's no exports condition. .filter(([, fileStates])=>fileStates.length > 0).sort(([a], [b])=>{ const comp = normalizeExportPath(a).length - normalizeExportPath(b).length; return comp === 0 ? a.localeCompare(b) : comp; }); const maxLengthOfExportName = Math.max(...statsArray.map(([exportName])=>normalizeExportName(exportName).length)); console.log(pc.underline('Exports'), ' '.repeat(Math.max(maxLengthOfExportName - 'Exports'.length, 0)), pc.underline('File'), ' '.repeat(Math.max(maxFilenameLength - 'File'.length, 0)), pc.underline('Size')); statsArray.forEach(([exportName, filesList])=>{ // sort by file type, first js files then types, js/mjs/cjs are prioritized than .d.ts/.d.mts/.d.cts filesList.sort(([a], [b])=>{ const aIsType = isTypeFile(a); const bIsType = isTypeFile(b); if (aIsType && bIsType) { return 0; } if (aIsType) { return 1; } if (bIsType) { return -1; } return 0; }).forEach((item, index)=>{ const [filename, , size] = item; const normalizedExportName = normalizeExportName(exportName); const exportNameWithPadding = index === 0 ? // For other formats, just show the padding spaces. normalizedExportName : ' '.repeat(normalizedExportName.length); const filenamePadding = ' '.repeat(Math.max(maxLengthOfExportName, 'Exports'.length) - normalizedExportName.length); const isType = isTypeFile(filename); const prettiedSize = prettyBytes__default.default(size); const sizePadding = ' '.repeat(Math.max(maxFilenameLength, 'File'.length) - filename.length); // Logging shared in debug mode if (isPrivateExportPath(exportName)) { if (index === 0 && process.env.DEBUG) { const label = '(chunk)'; const sizePadding = ' '.repeat(Math.max(maxFilenameLength, 'File'.length) - label.length); console.log(pc.dim(normalizeExportName(exportName)), filenamePadding, pc.dim(label), sizePadding, pc.dim(prettiedSize)); } return; } console.log(exportNameWithPadding, filenamePadding, `${pc[isType ? 'dim' : 'bold'](filename)}`, sizePadding, prettiedSize); }); }); } function normalizeError(error) { // Remove the noise from rollup plugin error if (error.code === 'PLUGIN_ERROR') { const normalizedError = new Error(error.message); normalizedError.stack = error.stack; error = normalizedError; } return error; } const helpMessage = ` Usage: bunchee [options] Commands: prepare auto setup package.json for building lint lint configuration in package.json Options: -v, --version output the version number -w, --watch watch src files changes -m, --minify compress output. default: false -o, --output <file> specify output filename -f, --format <format> type of output (esm, amd, cjs, iife, umd, system), default: esm -h, --help output usage information --external <mod> specify an external dependency, separate by comma --no-external do not bundle external dependencies --no-clean do not clean dist folder before building, default: false --target <target> js features target: swc target es versions. default: es2015 --runtime <runtime> build runtime (nodejs, browser). default: browser --env <env> inlined process env variables, separate by comma. default: NODE_ENV --cwd <cwd> specify current working directory --sourcemap enable sourcemap generation --no-dts do not generate types, default: undefined --tsconfig path to tsconfig file, default: tsconfig.json --dts-bundle bundle type declaration files, default: false --success <cmd> run command after build success `; function help() { logger.log(helpMessage); } async function lint(cwd) { // Not package.json detected, skip package linting if (!await hasPackageJson(cwd)) { return; } await lint$1(await getPackageMeta(cwd)); } async function parseCliArgs(argv) { const args = await yargs__default.default(helpers.hideBin(argv)).option('watch', { type: 'boolean', alias: 'w', description: 'watch src files changes' }).option('cwd', { type: 'string', description: 'specify current working directory' }).option('dts', { coerce (arg) { return arg === false ? false : undefined; }, description: 'do not generate types' }).option('clean', { coerce (arg) { return arg === false ? false : undefined; }, description: 'do not clean dist folder before building' }).option('output', { type: 'string', alias: 'o', description: 'specify output filename' }).option('format', { type: 'string', alias: 'f', default: 'esm', description: 'type of output (esm, amd, cjs, iife, umd, system)' }).option('minify', { type: 'boolean', alias: 'm', description: 'compress output' }).option('help', { type: 'boolean', alias: 'h', description: 'output usage information' }).option('runtime', { type: 'string', default: 'browser', description: 'build runtime (nodejs, browser)' }).option('target', { type: 'string', description: 'js features target: swc target es versions' }).option('sourcemap', { type: 'boolean', default: undefined, description: 'enable sourcemap generation' }).option('env', { type: 'string', description: 'inlined process env variables, separate by comma' }).option('external', { coerce (arg) { return typeof arg === 'string' || typeof arg === 'boolean' ? arg : undefined; }, description: 'specify an external dependency, separate by comma' }).option('tsconfig', { type: 'string', description: 'path to tsconfig file' }).option('dts-bundle', { type: 'boolean', description: 'bundle type declaration files' }).option('prepare', { type: 'boolean', description: 'auto setup package.json for building' }).option('success', { type: 'string', description: 'run command after build success' }).command('prepare', 'auto configure package.json exports for building', ()=>{}, (argv)=>{ return prepare(argv.cwd || process.cwd()); }).command('lint', 'lint package.json', ()=>{}, (argv)=>{ return lint(argv.cwd || process.cwd()); }).version(version).help('help', 'output usage information').showHelpOnFail(true).parse(); const cmd = args._[0]; if (cmd === 'prepare' || cmd === 'lint') { return { cmd }; } // Warn about this command being deprecated if (args['prepare']) { logger.warn('The "--prepare" option is deprecated. Please use `bunchee prepare` instead.'); return; } const source = args._[0]; const parsedArgs = { source, format: args['format'], file: args['output'], watch: !!args['watch'], minify: args['minify'], sourcemap: !!args['sourcemap'], cwd: args['cwd'], dts: args['dts'] === false ? false : undefined, dtsBundle: args['dts-bundle'], help: args['help'], runtime: args['runtime'], target: args['target'], // no-external is a boolean flag, turning external to `false` external: args['external'] === false ? null : args['external'], clean: args['clean'] !== false, env: args['env'], tsconfig: args['tsconfig'], onSuccess: args['success'] }; // When minify is enabled, sourcemap should be enabled by default, unless explicitly opted out if (parsedArgs.minify && typeof args['sourcemap'] === 'undefined') { parsedArgs.sourcemap = true; } return parsedArgs; } async function run(args) { var _args_external; const { source, format, watch, minify, sourcemap, target, runtime, dts, dtsBundle, env, clean, tsconfig, onSuccess } = args; const cwd = args.cwd || process.cwd(); const file = args.file ? path__default.default.resolve(cwd, args.file) : undefined; const bundleConfig = { dts: dts !== false && { respectExternal: dtsBundle ? true : undefined }, file, format, cwd, target, runtime, external: args.external === null ? null : ((_args_external = args.external) == null ? void 0 : _args_external.split(',')) || [], watch: !!watch, minify: !!minify, sourcemap: sourcemap === false ? false : true, env: (env == null ? void 0 : env.split(',')) || [], clean, tsconfig, onSuccess }; const cliEntry = source ? path__default.default.resolve(cwd, source) : ''; // lint package by default await lint(cwd); const { default: ora } = await import('ora'); const oraInstance = process.stdout.isTTY ? ora({ text: 'Building...\n\n', color: 'green' }) : { start: ()=>{}, stop: ()=>{}, clear: ()=>{}, stopAndPersist: ()=>{}, isSpinning: false }; const spinner = { start: startSpinner, stop: stopSpinner }; function startSpinner() { oraInstance.start(); } funct