UNPKG

bunchee

Version:

zero config bundler for js/ts/jsx libraries

1,329 lines (1,305 loc) 84.4 kB
Object.defineProperty(exports, '__esModule', { value: true }); var fsp = require('fs/promises'); var fs = require('fs'); var path = require('path'); require('pretty-bytes'); var fastGlob = require('fast-glob'); var require$$0 = require('tty'); var module$1 = require('module'); var rollup = require('rollup'); var pluginWasm = require('@rollup/plugin-wasm'); var rollupPluginSwc3 = require('rollup-plugin-swc3'); var commonjs = require('@rollup/plugin-commonjs'); var json = require('@rollup/plugin-json'); var pluginNodeResolve = require('@rollup/plugin-node-resolve'); var replace = require('@rollup/plugin-replace'); var preserveDirectives = require('rollup-preserve-directives'); var MagicString = require('magic-string'); var CleanCSS = require('clean-css'); var pluginutils = require('@rollup/pluginutils'); var child_process = require('child_process'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var fsp__default = /*#__PURE__*/_interopDefault(fsp); var fs__default = /*#__PURE__*/_interopDefault(fs); var path__default = /*#__PURE__*/_interopDefault(path); var require$$0__default = /*#__PURE__*/_interopDefault(require$$0); var commonjs__default = /*#__PURE__*/_interopDefault(commonjs); var json__default = /*#__PURE__*/_interopDefault(json); var replace__default = /*#__PURE__*/_interopDefault(replace); var preserveDirectives__default = /*#__PURE__*/_interopDefault(preserveDirectives); var MagicString__default = /*#__PURE__*/_interopDefault(MagicString); var CleanCSS__default = /*#__PURE__*/_interopDefault(CleanCSS); 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); } }; const availableExtensions = new Set([ 'js', 'cjs', 'mjs', 'jsx', 'ts', 'tsx', 'cts', 'mts' ]); const nodeResolveExtensions = [ '.mjs', '.cjs', '.js', '.json', '.node', '.jsx' ]; // 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 runtimeExportConventionsFallback = new Map([ // ESM only runtime [ 'workerd', [ 'import', 'default' ] ], [ 'edge-light', [ 'import', 'default' ] ], // Fallback to default when unsure [ 'electron', [ 'default' ] ], [ 'react-server', [ 'default' ] ], [ 'react-native', [ 'default' ] ], [ 'node', [ 'default' ] ], [ 'deno', [ 'default' ] ], [ 'bun', [ 'default' ] ], [ 'development', [ 'default' ] ], [ 'production', [ 'default' ] ], [ 'browser', [ 'import', 'require', 'default' ] ] ]); const optimizeConventions = new Set([ 'development', 'production' ]); const specialExportConventions = new Set([ ...runtimeExportConventions, ...optimizeConventions ]); const availableESExtensionsRegex = /\.(m|c)?[jt]sx?$/; const SRC = 'src'; const dtsExtensionsMap = { js: 'd.ts', cjs: 'd.cts', mjs: 'd.mts' }; const disabledWarnings = new Set([ 'EMPTY_BUNDLE', 'MIXED_EXPORTS', 'PREFER_NAMED_EXPORTS', 'UNRESOLVED_IMPORT', 'THIS_IS_UNDEFINED', 'INVALID_ANNOTATION', 'UNUSED_EXTERNAL_IMPORT' ]); 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 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); } 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 isNotNull = (n)=>Boolean(n); function resolveSourceFile(cwd, filename) { return path__default.default.resolve(cwd, SRC, filename); } function findSourceEntryFile(cwd, exportPath, exportTypeSuffix, ext) { const filename = resolveSourceFile(cwd, `${exportPath}${exportTypeSuffix ? `.${exportTypeSuffix}` : ''}.${ext}`); if (fileExists(filename)) { return filename; } const subFolderIndexFilename = resolveSourceFile(cwd, `${exportPath}/index${exportTypeSuffix ? `.${exportTypeSuffix}` : ''}.${ext}`); try { if (fileExists(subFolderIndexFilename)) { return subFolderIndexFilename; } } catch {} return undefined; } // Map '.' -> './index.[ext]' // Map './lite' -> './lite.[ext]' // Return undefined if no match or if it's package.json exports async function getSourcePathFromExportPath(cwd, exportPath, exportType) { for (const ext of availableExtensions){ // ignore package.json if (exportPath === '/package.json') return; if (exportPath === '.') exportPath = './index'; // Find convention-based source file for specific export types // $binary represents `pkg.bin` if (runtimeExportConventions.has(exportType) && exportType !== BINARY_TAG) { const filename = await findSourceEntryFile(cwd, exportPath, exportType, ext); if (filename) return filename; } const [, optimizeType] = exportType.split('.'); if (optimizeConventions.has(optimizeType)) { const filename = await findSourceEntryFile(cwd, exportPath, optimizeType, ext); if (filename) return filename; } const filename = await findSourceEntryFile(cwd, exportPath, null, ext); if (filename) return filename; } return; } // TODO: add unit test // Unlike path.basename, forcedly removing extension function filePathWithoutExtension(filePath) { if (!filePath) return ''; const lastDotIndex = filePath.lastIndexOf('.'); const lastSlashIndex = filePath.lastIndexOf('/'); if (lastDotIndex !== -1 && lastDotIndex > lastSlashIndex) { return filePath.slice(0, filePath.indexOf('.', lastSlashIndex + 1)); } return 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'; } async function removeDir(dirPath) { try { const dirStat = await fsp__default.default.stat(dirPath); if (dirStat.isDirectory()) { await fsp__default.default.rm(dirPath, { recursive: true, force: true }); } } catch (err) { if (err.code !== 'ENOENT') { throw err; } } } const removedDirs = new Set(); async function removeOutputDir(output, cwd) { const dir = output.dir; if (dir && // not equal to cwd dir !== cwd && // not equal to src/ dir dir !== path__default.default.resolve(cwd, SRC) && !removedDirs.has(dir)) { await removeDir(dir); removedDirs.add(dir); } } function isBinExportPath(exportPath) { return exportPath === BINARY_TAG || exportPath.startsWith(BINARY_TAG + '/'); } // 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); } 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 isEsmExportName(name, ext) { return [ 'import', 'module', 'module-sync' ].includes(name) || ext === 'mjs'; } function isCjsExportName(pkg, exportCondition, ext) { const isESModule = isESModulePackage(pkg.type); const isCjsCondition = [ 'require', 'main' ].includes(exportCondition); const isNotEsmExportName = !isEsmExportName(exportCondition, ext); return !isESModule && isNotEsmExportName && (ext !== 'mjs' || isCjsCondition) || ext === 'cjs'; } function getFileExportType(composedTypes) { return composedTypes.split('.').pop(); } function getExportsDistFilesOfCondition(pkg, parsedExportCondition, cwd, dts) { const dist = []; const exportConditionNames = Object.keys(parsedExportCondition.export); const uniqueFiles = new Set(); for (const exportCondition of exportConditionNames){ const exportType = getFileExportType(exportCondition); // Filter out non-types field when generating types jobs if (dts && exportType !== 'types') { continue; } // Filter out types field when generating asset jobs if (!dts && exportType === 'types') { continue; } const filePath = parsedExportCondition.export[exportCondition]; const ext = path.extname(filePath).slice(1); const relativePath = parsedExportCondition.export[exportCondition]; const distFile = path.resolve(cwd, relativePath); const format = isCjsExportName(pkg, exportCondition, ext) ? 'cjs' : 'esm'; if (uniqueFiles.has(distFile)) { continue; } uniqueFiles.add(distFile); dist.push({ format, file: distFile, exportCondition }); } return dist; } function getExportFileTypePath(absoluteJsBundlePath) { const dirName = path.dirname(absoluteJsBundlePath); const baseName = baseNameWithoutExtension(absoluteJsBundlePath); const ext = path.extname(absoluteJsBundlePath).slice(1); const typeExtension = dtsExtensionsMap[ext]; return normalizePath(path.join(dirName, baseName + '.' + typeExtension)); } 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; } async function collectEntriesFromParsedExports(cwd, parsedExportsInfo, pkg, sourceFile) { const entries = {}; if (sourceFile) { const defaultExport = parsedExportsInfo.get('./index')[0]; entries['./index'] = { source: sourceFile, name: '.', export: { default: defaultExport[0] } }; } // Find source files const { bins, exportsEntries } = await collectSourceEntriesFromExportPaths(path__default.default.join(cwd, SRC), parsedExportsInfo, pkg); // A mapping between each export path and its related special export conditions, // excluding the 'default' export condition. // { './index' => Set('development', 'edge-light') } const pathSpecialConditionsMap = {}; for (const [exportPath] of exportsEntries){ const normalizedExportPath = stripSpecialCondition(exportPath); if (!pathSpecialConditionsMap[normalizedExportPath]) { pathSpecialConditionsMap[normalizedExportPath] = new Set(); } const exportType = getExportTypeFromExportPath(exportPath); if (exportType !== 'default') { pathSpecialConditionsMap[normalizedExportPath].add(exportType); } } // Traverse source files and try to match the entries // Find exports from parsed exports info // entryExportPath can be: './index', './index.development', './shared.edge-light', etc. for (const [entryExportPath, sourceFilesMap] of exportsEntries){ const normalizedExportPath = stripSpecialCondition(entryExportPath); const entryExportPathType = getExportTypeFromExportPath(entryExportPath); const outputExports = parsedExportsInfo.get(normalizedExportPath); if (!outputExports) { continue; } for (const [outputPath, outputComposedExportType] of outputExports){ // export type can be: default, development, react-server, etc. const matchedExportType = getSpecialExportTypeFromComposedExportPath(outputComposedExportType); const specialSet = pathSpecialConditionsMap[normalizedExportPath]; const hasSpecialEntry = specialSet.has(matchedExportType); const sourceFile = sourceFilesMap[matchedExportType] || sourceFilesMap.default; if (!sourceFile) { continue; } if (!entries[entryExportPath]) { // Create a new entry entries[entryExportPath] = { source: sourceFile, name: normalizedExportPath, export: {} }; } else if (matchedExportType === entryExportPathType) { entries[entryExportPath].source = sourceFile; } // output exports match if (matchedExportType === entryExportPathType || !hasSpecialEntry && matchedExportType !== 'default') { // When we dealing with special export conditions, we need to make sure // the outputs won't override the default export output paths. // e.g. We have './index' -> { default: 'index.js', development: 'index.development.js' }; // When we generate './index.react-server' -> { 'react-server': 'index.react-server.js' }, // Normalize the entryExportPath to './index' first and check if it already exists with output paths. const normalizedEntryExportPath = stripSpecialCondition(entryExportPath); if (// The entry already exists, e.g. normalize './index.react-server' to './index' entries[normalizedEntryExportPath] && // Is special export condition entryExportPathType !== 'default' && // The extracted special condition is not the current loop one. entryExportPathType !== matchedExportType) { continue; } const exportMap = entries[entryExportPath].export; exportMap[outputComposedExportType] = outputPath; } } } // Handling binaries for (const [exportPath, sourceFile] of bins){ const outputExports = parsedExportsInfo.get(exportPath); if (!outputExports) { continue; } for (const [outputPath, exportType] of outputExports){ entries[exportPath] = { source: sourceFile, name: exportPath, export: { [exportType]: outputPath } }; } } return entries; } // ./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 getSpecialExportTypeFromSourcePath(sourcePath) { const fileBaseName = baseNameWithoutExtension(sourcePath); return getSpecialExportTypeFromComposedExportPath(fileBaseName); } 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; } function getSpecialExportTypeFromConditionNames(conditionNames) { let exportType = 'default'; conditionNames.forEach((value)=>{ if (specialExportConventions.has(value)) { 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; } // ./index.react-server -> ./index function stripSpecialCondition(exportPath) { return exportPath.split('.').slice(0, 2).join('.'); } 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); } } } /** * exportsEntries { * "./index" => { * "development" => source" * "react-server" => "source" * }, * "./index.react-server" => { * "development" => source" * "react-server" => "source" * } * } */ async function collectSourceEntriesFromExportPaths(sourceFolderPath, parsedExportsInfo, pkg) { const bins = new Map(); const exportsEntries = new Map(); for (const [exportPath, exportInfo] of parsedExportsInfo.entries()){ const specialConditions = new Set(); for (const [_, composedExportType] of exportInfo){ const specialExportType = getSpecialExportTypeFromComposedExportPath(composedExportType); if (specialExportType !== 'default') { specialConditions.add(specialExportType); } } await collectSourceEntriesByExportPath(sourceFolderPath, exportPath, bins, exportsEntries); for (const specialCondition of specialConditions){ await collectSourceEntriesByExportPath(sourceFolderPath, exportPath + '.' + specialCondition, bins, exportsEntries); } } // Search private shared module files which are not in the parsedExportsInfo, but start with _. // Leading underscore: e.g. _utils.ts, _utils/index.ts // Segment contains leading underscore: e.g. a/_b/_c.ts, a/b/_c/index.ts // Contains special suffix: e.g. _utils.development.ts, _utils/index.development.js const suffixPattern = [ ...runtimeExportConventions ].join(','); const extPattern = [ ...availableExtensions ].join(','); const privatePattern = `**/_*{,/*}{,{.${suffixPattern}}}.{${extPattern}}`; const privateFiles = await fastGlob.glob(privatePattern, { cwd: sourceFolderPath, ignore: [ TESTS_GLOB_PATTERN ] }); for (const file of privateFiles){ const sourceFileAbsolutePath = path__default.default.join(sourceFolderPath, file); const exportPath = sourceFilenameToExportFullPath(file); const isEsmPkg = isESModulePackage(pkg.type); const specialExportType = getSpecialExportTypeFromSourcePath(file); const normalizedExportPath = stripSpecialCondition(exportPath); const isSpecialExport = specialExportType !== 'default'; // export type: default => '' // export type: development => '.development' const condPart = isSpecialExport ? specialExportType + '.' : ''; // Map private shared files to the dist directory // e.g. ./_utils.ts -> ./dist/_utils.js // TODO: improve the logic to only generate the required files, not all possible files const isTs = isTypescriptFile(file); const typesInfos = [ [ posixRelativify(path.posix.join('./dist', exportPath + (isEsmPkg ? '.d.ts' : '.d.mts'))), condPart + 'import.types' ], [ posixRelativify(path.posix.join('./dist', exportPath + (isEsmPkg ? '.d.cts' : '.d.ts'))), condPart + 'require.types' ] ]; const privateExportInfo = [ ...isTs ? typesInfos : [], [ posixRelativify(path.posix.join('./dist', exportPath + (isEsmPkg ? '.js' : '.mjs'))), condPart + 'import.default' ], [ posixRelativify(path.posix.join('./dist', exportPath + (isEsmPkg ? '.cjs' : '.js'))), condPart + 'require.default' ] ]; const exportsInfo = parsedExportsInfo.get(normalizedExportPath); if (!exportsInfo) { // Add private shared files to parsedExportsInfo parsedExportsInfo.set(normalizedExportPath, privateExportInfo); } else { // Merge private shared files to the existing exportsInfo exportsInfo.push(...privateExportInfo); } // Insert private shared modules into the entries const entry = exportsEntries.get(exportPath); if (!entry) { exportsEntries.set(exportPath, { [specialExportType]: sourceFileAbsolutePath }); } else { entry[specialExportType] = sourceFileAbsolutePath; } } return { bins, exportsEntries }; } // Example: @foo/bar -> bar const removeScope = (exportPath)=>exportPath.replace(/^@[^/]+\//, ''); function createOutputState({ entries }) { const sizeStats = new Map(); const uniqFiles = new Set(); function addSize({ fileName, size, sourceFileName, exportPath }) { if (!sizeStats.has(exportPath)) { sizeStats.set(exportPath, []); } const distFilesStats = sizeStats.get(exportPath); if (!uniqFiles.has(fileName)) { uniqFiles.add(fileName); if (distFilesStats) { distFilesStats.push([ fileName, sourceFileName, size ]); } } } const reversedMapping = new Map(); Object.entries(entries).forEach(([resolvedExportName, entry])=>{ reversedMapping.set(entry.source, resolvedExportName); }); return { plugin: (cwd)=>{ return { name: 'collect-sizes', writeBundle (options, bundle) { const dir = options.dir || path__default.default.dirname(options.file); Object.entries(bundle).forEach(([fileName, chunk])=>{ const filePath = path__default.default.join(dir, fileName); if (chunk.type !== 'chunk') { return; } if (!chunk.isEntry) { return; } const size = chunk.code.length; const sourceFileName = chunk.facadeModuleId || ''; const exportPath = removeScope(reversedMapping.get(sourceFileName) || '.'); addSize({ fileName: normalizePath(path__default.default.relative(cwd, filePath)), size, sourceFileName: normalizePath(sourceFileName), exportPath }); }); } }; }, getSizeStats () { return sizeStats; } }; } const createMemoize = (fn, cacheKey, cacheArg)=>{ const cache = cacheArg || new Map(); return (...args)=>{ const key = cacheKey ? typeof cacheKey === 'function' ? cacheKey(...args) : cacheKey : JSON.stringify({ args }); const existing = cache.get(key); if (existing !== undefined) { return existing; } const result = fn(...args); cache.set(key, result); return result; }; }; const memoizeByKey = (fn)=>{ const cache = new Map(); return (cacheKey)=>createMemoize(fn, cacheKey, cache); }; const memoize = (fn)=>createMemoize(fn); let hasLoggedTsWarning = false; function resolveTypescript(cwd) { let ts; const m = new module$1.Module('', undefined); m.paths = module$1.Module._nodeModulePaths(cwd); try { // Bun does not yet support the `Module` class properly. if (typeof (m == null ? void 0 : m.require) === 'undefined') { const tsPath = require.resolve('typescript', { paths: [ cwd ] }); ts = require(tsPath); } else { ts = m.require('typescript'); } } catch (e) { console.error(e); if (!hasLoggedTsWarning) { hasLoggedTsWarning = true; exit('Could not load TypeScript compiler. Try to install `typescript` as dev dependency'); } } return ts; } const resolveTsConfigPath = memoize((cwd, tsconfigFileName = 'tsconfig.json')=>{ let tsConfigPath; tsConfigPath = path.resolve(cwd, tsconfigFileName); return fileExists(tsConfigPath) ? tsConfigPath : undefined; }); function resolveTsConfigHandler(cwd, tsConfigPath) { let tsCompilerOptions = {}; if (tsConfigPath) { // Use the original ts handler to avoid memory leak const ts = resolveTypescript(cwd); const basePath = tsConfigPath ? path.dirname(tsConfigPath) : cwd; const tsconfigJSON = ts.readConfigFile(tsConfigPath, ts.sys.readFile).config; tsCompilerOptions = ts.parseJsonConfigFileContent(tsconfigJSON, ts.sys, basePath).options; } else { return null; } return { tsCompilerOptions, tsConfigPath }; } const resolveTsConfig = memoize(resolveTsConfigHandler); async function convertCompilerOptions(cwd, json) { // Use the original ts handler to avoid memory leak const ts = resolveTypescript(cwd); return ts.convertCompilerOptionsFromJson(json, './'); } 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.`); } /** * @return {Record<string, string>} env { 'process.env.<key>': '<value>' } */ function getDefinedInlineVariables(envs, parsedExportCondition) { const envVars = envs.reduce((acc, key)=>{ const value = process.env[key]; if (typeof value !== 'undefined') { acc['process.env.' + key] = JSON.stringify(value); } return acc; }, {}); const exportConditionNames = Object.keys(parsedExportCondition.export).reduce((acc, key)=>{ // key could be 'require' or 'import.development' etc. const exportTypes = key.split('.'); for (const exportType of exportTypes){ acc.add(exportType); } return acc; }, new Set()); // For development and production convention, we override the NODE_ENV value if (exportConditionNames.has('development')) { envVars['process.env.NODE_ENV'] = JSON.stringify('development'); } else if (exportConditionNames.has('production')) { envVars['process.env.NODE_ENV'] = JSON.stringify('production'); } if (exportConditionNames.has('edge-light')) { envVars['EdgeRuntime'] = JSON.stringify('edge-runtime'); } return envVars; } const FILENAME_REGEX = /__filename/; const DIRNAME_REGEX = /__dirname/; // not char, or space before require(.resolve)?( const GLOBAL_REQUIRE_REGEX = /(?:^|[^.\w'"`])\brequire(\.resolve)?\(\s*[\r\n]*(\w|['"`])/; const PolyfillComment = '/** rollup-private-do-not-use-esm-shim-polyfill */'; const createESMShim = ({ filename, dirname, globalRequire })=>{ const useNodeUrl = filename || dirname; const useNodePath = dirname; const useNodeModule = globalRequire; return `\ ${PolyfillComment} ${useNodeUrl ? `import __node_cjsUrl from 'node:url'` : ''}; ${useNodePath ? `import __node_cjsPath from 'node:path';` : ''} ${useNodeModule ? `import __node_cjsModule from 'node:module';` : ''} ${useNodeUrl ? 'const __filename = __node_cjsUrl.fileURLToPath(import.meta.url);' : ''} ${useNodePath ? 'const __dirname = __node_cjsPath.dirname(__filename);' : ''} ${useNodeModule ? 'const require = __node_cjsModule.createRequire(import.meta.url);' : ''} `.trim() + '\n'; }; function esmShim() { return { name: 'esm-shim', transform: { order: 'post', handler (code, id) { const ext = path.extname(id); if (!availableESExtensionsRegex.test(ext) || code.includes(PolyfillComment)) { return null; } let hasFilename = false; let hasDirname = false; let hasGlobalRequire = false; if (FILENAME_REGEX.test(code)) { hasFilename = true; } if (DIRNAME_REGEX.test(code)) { hasDirname = true; } if (GLOBAL_REQUIRE_REGEX.test(code)) { hasGlobalRequire = true; } if (!hasFilename && !hasDirname && !hasGlobalRequire) { return null; } const magicString = new MagicString__default.default(code); let ast = null; try { // rollup 2 built-in parser doesn't have `allowShebang`, we need to use the sliced code here. Hence the `magicString.toString()` ast = this.parse(magicString.toString(), { allowReturnOutsideFunction: true }); } catch (e) { console.warn(e); return null; } if (ast.type !== 'Program') { return null; } let lastImportNode = null; for (const node of ast.body){ if (node.type === 'ImportDeclaration') { lastImportNode = node; continue; } } let end = 0; if (lastImportNode) { end = lastImportNode.end; } else { end = ast.body.length > 0 ? ast.body[0].end : 0; } magicString.appendRight(end, '\n' + createESMShim({ filename: hasFilename, dirname: hasDirname, globalRequire: hasGlobalRequire })); return { code: magicString.toString(), map: magicString.generateMap({ hires: true }) }; } } }; } const helpers = { cssImport: { // have to assign r.type = 'text/css' to make it work in Safari global: `\ function __insertCSS(code) { if (!code || typeof document == 'undefined') return let head = document.head || document.getElementsByTagName('head')[0] let style = document.createElement('style') style.type = 'text/css' head.appendChild(style) ;style.styleSheet ? (style.styleSheet.cssText = code) : style.appendChild(document.createTextNode(code)) } `, create (code) { return `__insertCSS(${JSON.stringify(code)});`; } }}; const cleanCssInstance = new CleanCSS__default.default({}); function minify(code) { return cleanCssInstance.minify(code).styles; } function inlineCss(options) { const cssIds = new Set(); var _options_exclude; const filter = pluginutils.createFilter([ '**/*.css' ], (_options_exclude = options.exclude) != null ? _options_exclude : []); // Follow up for rollup 4 for better support of assertion support https://github.com/rollup/rollup/issues/4818 return { name: 'inline-css', transform: { order: 'post', handler (code, id) { if (!filter(id)) return; if (options.skip) return ''; const cssCode = minify(code); cssIds.add(id); return { code: helpers.cssImport.create(cssCode), map: { mappings: '' } }; } }, renderChunk: { order: 'pre', handler (code) { const dependenciesIds = this.getModuleIds(); let foundCss = false; for (const depId of dependenciesIds){ if (depId && cssIds.has(depId)) { foundCss = true; break; } } if (!foundCss) return; return { code: `${helpers.cssImport.global}\n${code}`, map: { mappings: '' } }; } } }; } function rawContent({ exclude }) { const filter = pluginutils.createFilter([ '**/*.data', '**/*.txt' ], exclude); return { name: 'string', resolveId (id, importer) { // Handle ?raw query parameter if (id.includes('?raw')) { const cleanId = id.split('?')[0]; // Resolve the actual file path if (importer) { const path = require('path'); return path.resolve(path.dirname(importer), cleanId) + '?raw'; } return cleanId + '?raw'; } return null; }, async load (id) { // Handle ?raw query parameter - read the actual file without the query if (id.includes('?raw')) { const cleanId = id.split('?')[0]; try { const content = await fsp.readFile(cleanId, 'utf-8'); // Normalize line endings only on Windows: convert \r\n to \n return process.platform === 'win32' ? content.replace(/\r\n/g, '\n') : content; } catch (error) { this.error(`[bunchee] failed to read file: ${cleanId}`); } } return null; }, transform (code, id) { // Check if the file has ?raw query parameter const isRawQuery = id.includes('?raw'); const cleanId = isRawQuery ? id.split('?')[0] : id; if (filter(cleanId) || isRawQuery) { // Normalize line endings only on Windows for .txt and .data files const normalizedCode = process.platform === 'win32' ? code.replace(/\r\n/g, '\n') : code; return { code: `const data = ${JSON.stringify(normalizedCode)};\nexport default data;`, map: null }; } return null; } }; } function hasNoSpecialCondition(conditionNames) {