diff --git a/_old_getMain.js.bak b/_old_getMain.js.bak new file mode 100644 index 00000000..64464147 --- /dev/null +++ b/_old_getMain.js.bak @@ -0,0 +1,152 @@ +function replaceName(filename, name) { + return resolve( + dirname(filename), + name + basename(filename).replace(/^[^.]+/, ''), + ); +} + +/** + * @param {any} exports - package.json "exports" field value + * @param {string} exportPath - the export to look up (eg: '.', './a', './b/c') + * @param {string[]} conditions - conditional export keys to use (note: unlike in resolution, order here *does* define precedence!) + * @param {RegExp|string} [defaultPattern] - only use (resolved) default export filenames that match this pattern + * @param {string} [condition] - (internal) the nearest condition key on the stack + */ +function walk(exports, exportPath, conditions, defaultPattern, condition) { + if (!exports) return; + if (typeof exports === 'string') { + if ( + condition === 'default' && + defaultPattern && + !exports.match(defaultPattern) + ) { + return; + } + return exports; + } + if (Array.isArray(exports)) { + for (const map of exports) { + const r = walk(map, exportPath, conditions, defaultPattern, condition); + if (r) return r; + } + return; + } + const map = exports[exportPath]; + if (map) { + const r = walk(map, exportPath, conditions, defaultPattern, condition); + if (r) return r; + } + for (const condition of conditions) { + const map = exports[condition]; + if (!map) continue; + const r = walk(map, exportPath, conditions, defaultPattern, condition); + if (r) return r; + } + // return walk(exports['.'] || exports.import || exports.module); +} + +function getMain({ options, entry, format }) { + const { pkg } = options; + const pkgMain = options['pkg-main']; + + if (!pkgMain) { + return options.output; + } + + // package.json export name (see https://nodejs.org/api/packages.html#packages_subpath_exports) + let exportPath = '.'; + let mainNoExtension = options.output; + if (options.multipleEntries) { + const commonDir = options.entries + .reduce((acc, entry) => { + entry = dirname(entry).split(sep); + if (!acc) return entry; + if (entry.length < acc.length) acc.length = entry.length; + let last = entry.length - 1; + while (entry[last] !== acc[last]) { + last--; + acc.pop(); + } + return acc; + }, undefined) + .join(sep); + console.log('>>>>>', { first: options.entries[0], commonDir }); + + const isMainEntry = entry.match( + /([\\/])index(\.(umd|cjs|es|m))?\.(mjs|cjs|[tj]sx?)$/, + ); + let name = isMainEntry ? mainNoExtension : entry; + mainNoExtension = resolve(dirname(mainNoExtension), basename(name)); + if (!isMainEntry) { + exportPath = + './' + + posix.relative(commonDir, entry.replace(/\.([mc]js|[tj]sx?)$/g, '')); + } + } + mainNoExtension = mainNoExtension.replace( + /(\.(umd|cjs|es|m))?\.(mjs|cjs|[tj]sx?)$/, + '', + ); + + const mainsByFormat = {}; + + const MJS = pkg.type === 'module' ? /\.m?js$/i : /\.mjs$/i; + const CJS = pkg.type === 'module' ? /\.cjs$/i : /\.js$/i; + const UMD = /[.-]umd\.c?js$/i; + const CONDITIONS_MJS = ['import', 'module', 'default']; + const CONDITIONS_MODERN = ['modern', 'esmodules', ...CONDITIONS_MJS]; + const CONDITIONS_CJS = ['require', 'default']; + const CONDITIONS_UMD = ['umd', 'default']; + + mainsByFormat.modern = + walk(pkg.exports, exportPath, CONDITIONS_MODERN, MJS) || + (pkg.syntax && pkg.syntax.esmodules) || + pkg.esmodule || + replaceName('x.modern.js', mainNoExtension); + + mainsByFormat.es = walk(pkg.exports, exportPath, CONDITIONS_MJS, MJS); + if (!mainsByFormat.es || mainsByFormat.es === mainsByFormat.modern) { + mainsByFormat.es = + pkg.module && !pkg.module.match(/src\//) + ? pkg.module + : pkg['jsnext:main'] || replaceName('x.esm.js', mainNoExtension); + } + + mainsByFormat.umd = + walk(pkg.exports, exportPath, CONDITIONS_UMD, UMD) || + pkg['umd:main'] || + pkg.unpkg || + replaceName('x.umd.js', mainNoExtension); + + mainsByFormat.cjs = walk(pkg.exports, exportPath, CONDITIONS_CJS, CJS); + if (!mainsByFormat.cjs || mainsByFormat.cjs === mainsByFormat.umd) { + mainsByFormat.cjs = + pkg['cjs:main'] || + replaceName(pkg.type === 'module' ? 'x.cjs' : 'x.js', mainNoExtension); + } + + if (pkg.type === 'module') { + let errors = []; + let filenames = []; + if (mainsByFormat.cjs.endsWith('.js')) { + errors.push('CommonJS'); + filenames.push(` "cjs:main": "${mainsByFormat.cjs}",`); + } + if (mainsByFormat.umd.endsWith('.js')) { + errors.push('CommonJS'); + const field = pkg['umd:main'] ? 'umd:main' : 'unpkg'; + filenames.push(` "${field}": "${mainsByFormat.umd}",`); + } + if (errors.length) { + const warning = + `Warning: A package.json with {"type":"module"} should use .cjs file extensions for` + + ` ${errors.join(' and ')} filename${errors.length == 1 ? '' : 's'}:` + + `\n${filenames.join('\n')}`; + stderr(yellow(warning)); + } + } + + console.log('>> MAIN: ', format, entry, exportPath, mainsByFormat[format]); + + return mainsByFormat[format] || mainsByFormat.cjs; +} diff --git a/package-lock.json b/package-lock.json index 5cd9089f..fd847026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "microbundle", - "version": "0.12.3", + "version": "0.13.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2988,6 +2988,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "26.0.23", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz", + "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, "@types/json-schema": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", diff --git a/package.json b/package.json index 279422b7..008b2c5b 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "@babel/plugin-proposal-throw-expressions": "^7.12.1", "@changesets/changelog-github": "^0.2.7", "@changesets/cli": "^2.12.0", + "@types/jest": "^26.0.23", "babel-jest": "^26.6.3", "cross-env": "^7.0.3", "directory-tree": "^2.2.5", diff --git a/src/index.js b/src/index.js index e96352b0..0e6ef36b 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import fs from 'fs'; import { resolve, relative, dirname, basename, extname } from 'path'; import camelCase from 'camelcase'; import escapeStringRegexp from 'escape-string-regexp'; -import { blue } from 'kleur'; +import { blue, yellow } from 'kleur'; import { map, series } from 'asyncro'; import glob from 'tiny-glob/sync'; import autoprefixer from 'autoprefixer'; @@ -19,7 +19,7 @@ import postcss from 'rollup-plugin-postcss'; import typescript from 'rollup-plugin-typescript2'; import json from '@rollup/plugin-json'; import logError from './log-error'; -import { isDir, isFile, stdout, isTruthy, removeScope } from './utils'; +import { isDir, isFile, stdout, isTruthy, removeScope, stderr } from './utils'; import { getSizeInfo } from './lib/compressed-size'; import { normalizeMinifyOptions } from './lib/terser'; import { @@ -29,6 +29,7 @@ import { } from './lib/option-normalization'; import { getConfigFromPkgJson, getName } from './lib/package-info'; import { shouldCssModules, cssModulesConfig } from './lib/css-modules'; +import { computeEntries } from './lib/compute-entries'; // Extensions to use when resolving modules const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.es6', '.es', '.mjs']; @@ -61,8 +62,10 @@ export default async function microbundle(inputOptions) { options.pkg.name = pkgName; if (options.sourcemap === 'inline') { - console.log( - 'Warning: inline sourcemaps should only be used for debugging purposes.', + stderr( + yellow( + 'Warning: inline sourcemaps should only be used for debugging purposes.', + ), ); } else if (options.sourcemap === 'false') { options.sourcemap = false; @@ -249,67 +252,6 @@ async function getEntries({ input, cwd }) { return entries; } -function replaceName(filename, name) { - return resolve( - dirname(filename), - name + basename(filename).replace(/^[^.]+/, ''), - ); -} - -function walk(exports) { - if (typeof exports === 'string') return exports; - return walk(exports['.'] || exports.import || exports.module); -} - -function getMain({ options, entry, format }) { - const { pkg } = options; - const pkgMain = options['pkg-main']; - - if (!pkgMain) { - return options.output; - } - - let mainNoExtension = options.output; - if (options.multipleEntries) { - let name = entry.match( - /([\\/])index(\.(umd|cjs|es|m))?\.(mjs|cjs|[tj]sx?)$/, - ) - ? mainNoExtension - : entry; - mainNoExtension = resolve(dirname(mainNoExtension), basename(name)); - } - mainNoExtension = mainNoExtension.replace( - /(\.(umd|cjs|es|m))?\.(mjs|cjs|[tj]sx?)$/, - '', - ); - - const mainsByFormat = {}; - - mainsByFormat.es = replaceName( - pkg.module && !pkg.module.match(/src\//) - ? pkg.module - : pkg['jsnext:main'] || 'x.esm.js', - mainNoExtension, - ); - mainsByFormat.modern = replaceName( - (pkg.exports && walk(pkg.exports)) || - (pkg.syntax && pkg.syntax.esmodules) || - pkg.esmodule || - 'x.modern.js', - mainNoExtension, - ); - mainsByFormat.cjs = replaceName( - pkg['cjs:main'] || (pkg.type && pkg.type === 'module' ? 'x.cjs' : 'x.js'), - mainNoExtension, - ); - mainsByFormat.umd = replaceName( - pkg['umd:main'] || pkg.unpkg || 'x.umd.js', - mainNoExtension, - ); - - return mainsByFormat[format] || mainsByFormat.cjs; -} - // shebang cache map because the transform only gets run once const shebang = {}; @@ -417,7 +359,13 @@ function createConfig(options, entry, format, writeMeta) { let cache; if (modern) cache = false; - const absMain = resolve(options.cwd, getMain({ options, entry, format })); + const entries = computeEntries({ + ...options, + entry, + format, + }); + const absMain = resolve('.', options.cwd, entries[format] || entries.cjs); + console.log(entries[format], absMain); const outputDir = dirname(absMain); const outputEntryFileName = basename(absMain); diff --git a/src/lib/compute-entries.js b/src/lib/compute-entries.js new file mode 100644 index 00000000..f1b86776 --- /dev/null +++ b/src/lib/compute-entries.js @@ -0,0 +1,228 @@ +import { resolve, relative, dirname, join, basename, sep, posix } from 'path'; +import { yellow } from 'kleur'; +import { stderr } from '../utils'; + +function replaceName(filename, name) { + if (!filename) return filename; + const ext = (basename(filename).match( + /(?:\.(?:umd|cjs|esm|es|m|mjs|module|modern))?\.(?:[mc]js|[tj]sx?)$/i, + ) || [])[0]; + const dir = dirname(filename); + const rel = relative(dir, name); + return ensureRelative( + // join(dirname(filename), name + basename(filename).replace(/^[^.]+/, '')), + join(dir, rel + ext), + ); +} + +function ensureRelative(path) { + if (path[0] !== '.') return './' + path; + return path; +} + +/** + * @param {object} options + * @param {object} options.pkg - parsed package.json object + * @param {string} options.entry - the entry filename to produce a mapping for + * @param {string[]} options.entries - all entries to be compiled + * @param {boolean} [options.multipleEntries=false] - `true` if multiple entries are to be compiled + * @param {string} [options.output=''] - output filename (?) + * @param {string} [options.cwd='.'] + */ +export function computeEntries(options) { + const { pkg } = options; + const cwd = options.cwd || '.'; + const entry = ensureRelative(relative(cwd, resolve(cwd, options.entry))); + + // package.json export name (see https://nodejs.org/api/packages.html#packages_subpath_exports) + let exportPath = '.'; + let mainNoExtension = ensureRelative( + options.output || pkg.main || 'dist/index.js', + ); + const entries = options.entries.map(p => relative(cwd, resolve(cwd, p))); + let isDefaultEntry = true; + if (options.multipleEntries) { + const defaultEntry = ensureRelative( + entries.find(p => basename(p).match(/^index\.([mc]js|[tj]sx?)$/i)) || + entries[0], + ); + isDefaultEntry = defaultEntry === entry; + // isDefaultEntry = entry.match( + // /([\\/])index(\.(umd|cjs|es|m))?\.(mjs|cjs|[tj]sx?)$/, + // ); + const commonDir = entries + .reduce((acc, entry) => { + let parts = dirname(entry).split(sep); + if (parts.length < acc.length) acc.length = parts.length; + let last = parts.length - 1; + while (parts[last] !== acc[last]) { + last--; + acc.pop(); + } + return acc; + }, dirname(entries[0]).split(sep)) + .join(sep); + + let name = isDefaultEntry ? mainNoExtension : entry; + mainNoExtension = ensureRelative( + posix.relative( + cwd, + resolve(cwd, dirname(mainNoExtension), basename(name)), + ), + ); + if (!isDefaultEntry) { + exportPath = ensureRelative( + posix.relative(commonDir, entry.replace(/\.([mc]js|[tj]sx?)$/g, '')), + ); + } + // console.log('>>>>>', entry, { name, commonDir, defaultEntry }); + } + mainNoExtension = mainNoExtension.replace( + /(\.(umd|cjs|es|m|module|modern))?\.([mc]js|[tj]sx?)$/i, + '', + ); + + const mainsByFormat = {}; + + let cjsExt = pkg.type === 'module' ? '.cjs' : '.js'; + let esmExt = pkg.type === 'module' ? '.esm.js' : '.mjs'; + + const MJS = pkg.type === 'module' ? /\.m?js$/i : /\.mjs$/i; + const CJS = pkg.type === 'module' ? /\.cjs$/i : /\.js$/i; + const UMD = /[.-]umd\.c?js$/i; + + const CONDITIONS_MJS = ['import', 'module', 'default']; + const CONDITIONS_MODERN = ['modern', 'esmodules', ...CONDITIONS_MJS]; + const CONDITIONS_CJS = ['require', 'default']; + const CONDITIONS_UMD = ['umd', 'default']; + + const anyMain = walk(pkg.exports, exportPath, [ + ...CONDITIONS_MODERN, + 'require', + 'umd', + ]); + if (anyMain) { + mainNoExtension = anyMain.replace( + /(\.(umd|cjs|esm?|m|module|modern|20\d\d))?\.([mc]js|[tj]sx?)$/i, + '', + ); + } + + mainsByFormat.modern = + walk(pkg.exports, exportPath, CONDITIONS_MODERN, MJS) || + (pkg.syntax && pkg.syntax.esmodules) || + pkg.esmodule || + replaceName('x.modern.js', mainNoExtension); + if (mainsByFormat.modern) { + const m = mainsByFormat.modern.match(/(\.esm?|\.module|\.m)?\.m?js$/); + if (m) esmExt = m[0]; + } + + mainsByFormat.es = walk(pkg.exports, exportPath, CONDITIONS_MJS, MJS); + // if (mainsByFormat.es === mainsByFormat.modern) { + // mainsByFormat.es = walk(pkg.exports, exportPath, ['module'], MJS); + // } + if (!mainsByFormat.es || mainsByFormat.es === mainsByFormat.modern) { + mainsByFormat.es = + pkg.module && !pkg.module.match(/src\//) + ? replaceName(pkg.module, mainNoExtension) + : replaceName(pkg['jsnext:main'], mainNoExtension) || + replaceName('x.esm' + esmExt, mainNoExtension); + } + + mainsByFormat.umd = + walk(pkg.exports, exportPath, CONDITIONS_UMD, UMD) || + pkg['umd:main'] || + pkg.unpkg || + replaceName('x.umd' + cjsExt, mainNoExtension); + + mainsByFormat.cjs = walk(pkg.exports, exportPath, CONDITIONS_CJS, CJS); + if (!mainsByFormat.cjs || mainsByFormat.cjs === mainsByFormat.umd) { + mainsByFormat.cjs = + pkg['cjs:main'] || + (isDefaultEntry && pkg.main) || + replaceName('x' + cjsExt, mainNoExtension); + } + + // const anyMain = mainsByFormat.es || mainsByFormat.cjs || mainsByFormat.umd; + // if (anyMain) { + // mainNoExtension = anyMain.replace( + // /(\.(umd|cjs|esm?|m|module|modern|20\d\d))?\.([mc]js|[tj]sx?)$/i, + // '', + // ); + // } + // if (!mainsByFormat.es || mainsByFormat.es === mainsByFormat.modern) { + // mainsByFormat.es = + // pkg.module && !pkg.module.match(/src\//) + // ? pkg.module + // : pkg['jsnext:main'] || replaceName('x.esm.js', mainNoExtension); + // } + + if (pkg.type === 'module') { + let errors = []; + let filenames = []; + if (mainsByFormat.cjs.endsWith('.js')) { + errors.push('CommonJS'); + filenames.push(` "cjs:main": "${mainsByFormat.cjs}",`); + } + if (mainsByFormat.umd.endsWith('.js')) { + errors.push('CommonJS'); + const field = pkg['umd:main'] ? 'umd:main' : 'unpkg'; + filenames.push(` "${field}": "${mainsByFormat.umd}",`); + } + if (errors.length) { + const warning = + `Warning: A package.json with {"type":"module"} should use .cjs file extensions for` + + ` ${errors.join(' and ')} filename${errors.length == 1 ? '' : 's'}:` + + `\n${filenames.join('\n')}`; + stderr(yellow(warning)); + } + } + + return mainsByFormat; +} + +/** + * @param {any} exports - package.json "exports" field value + * @param {string} exportPath - the export to look up (eg: '.', './a', './b/c') + * @param {string[]} conditions - conditional export keys to use (note: unlike in resolution, order here *does* define precedence!) + * @param {RegExp|string} [defaultPattern] - only use (resolved) default export filenames that match this pattern + * @param {string} [condition] - (internal) the nearest condition key on the stack + */ +function walk( + exports, + exportPath, + conditions, + defaultPattern, + condition = 'default', +) { + if (!exports) return; + if (typeof exports === 'string') { + if ( + condition === 'default' && + defaultPattern && + !exports.match(defaultPattern) + ) { + return; + } + return exports; + } + if (Array.isArray(exports)) { + for (const map of exports) { + const r = walk(map, exportPath, conditions, defaultPattern, condition); + if (r) return r; + } + return; + } + const map = exports[exportPath]; + if (map) { + const r = walk(map, exportPath, conditions, defaultPattern, condition); + if (r) return r; + } + for (const condition of conditions) { + const map = exports[condition]; + if (!map) continue; + const r = walk(map, exportPath, conditions, defaultPattern, condition); + if (r) return r; + } +} diff --git a/test/__snapshots__/index.test.js.snap b/test/__snapshots__/index.test.js.snap index fe22cebe..dd91f6d9 100644 --- a/test/__snapshots__/index.test.js.snap +++ b/test/__snapshots__/index.test.js.snap @@ -2193,6 +2193,77 @@ exports[`fixtures build name-custom-cli with microbundle 5`] = ` " `; +exports[`fixtures build name-multi-source with microbundle 1`] = ` +"Used script: microbundle + +Directory tree: + +name-multi-source + a.js + b.js + dist + a.js + a.js.map + a.mjs + a.mjs.map + a.umd.js + a.umd.js.map + b.js + b.js.map + b.mjs + b.mjs.map + b.umd.js + b.umd.js.map + package.json + + +Build \\"nameMultiSource\\" to dist: +43 B: a.js.gz +27 B: a.js.br +43 B: a.mjs.gz +27 B: a.mjs.br +99 B: a.umd.js.gz +84 B: a.umd.js.br +43 B: b.js.gz +27 B: b.js.br +43 B: b.mjs.gz +27 B: b.mjs.br +99 B: b.umd.js.gz +79 B: b.umd.js.br" +`; + +exports[`fixtures build name-multi-source with microbundle 2`] = `12`; + +exports[`fixtures build name-multi-source with microbundle 3`] = ` +"console.log(\\"i am a\\"); +//# sourceMappingURL=a.js.map +" +`; + +exports[`fixtures build name-multi-source with microbundle 4`] = ` +"console.log(\\"i am b\\"); +//# sourceMappingURL=a.mjs.map +" +`; + +exports[`fixtures build name-multi-source with microbundle 5`] = ` +"!function(n){\\"function\\"==typeof define&&define.amd?define(n):n()}(function(){console.log(\\"i am b\\")}); +//# sourceMappingURL=a.umd.js.map +" +`; + +exports[`fixtures build name-multi-source with microbundle 6`] = ` +"console.log(\\"i am b\\"); +//# sourceMappingURL=b.js.map +" +`; + +exports[`fixtures build name-multi-source with microbundle 7`] = ` +"console.log(\\"i am b\\"); +//# sourceMappingURL=b.mjs.map +" +`; + exports[`fixtures build no-pkg with microbundle 1`] = ` "Used script: microbundle diff --git a/test/compute-entries.test.js b/test/compute-entries.test.js new file mode 100644 index 00000000..60538eb8 --- /dev/null +++ b/test/compute-entries.test.js @@ -0,0 +1,442 @@ +import { computeEntries } from '../src/lib/compute-entries'; + +function mk(pkg, entries) { + entries = entries || pkg.source; + if (!Array.isArray(entries)) entries = [entries]; + return { + pkg: { + name: 'my-mod', + ...pkg, + }, + cwd: '/example/repo', + entry: entries[0], + entries, + multipleEntries: entries.length > 1, + }; +} + +describe('computeEntries()', () => { + describe('single entry', () => { + describe('only legacy entries', () => { + it('"main"', () => { + const entries = computeEntries( + mk({ + main: './dist/mod.js', + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.esm.js", + "modern": "./dist/mod.modern.js", + "umd": "./dist/mod.umd.js", + } + `); + }); + + it('"main" and "module"', () => { + const entries = computeEntries( + mk({ + main: './dist/mod.js', + module: './dist/mod.mjs', + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.mjs", + "modern": "./dist/mod.modern.js", + "umd": "./dist/mod.umd.js", + } + `); + }); + + it('"main", "module", "unpkg"', () => { + const entries = computeEntries( + mk({ + main: './dist/mod.js', + module: './dist/mod.mjs', + unpkg: './dist/mod-umd.js', + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.mjs", + "modern": "./dist/mod.modern.js", + "umd": "./dist/mod-umd.js", + } + `); + }); + + it('"main", "module", "esmodule"', () => { + const entries = computeEntries( + mk({ + main: './dist/mod.js', + module: './dist/mod.mjs', + esmodule: './dist/mod.modern.mjs', + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.mjs", + "modern": "./dist/mod.modern.mjs", + "umd": "./dist/mod.umd.js", + } + `); + }); + + it('"esmodule"', () => { + const entries = computeEntries( + mk({ + esmodule: './dist/mod.modern.mjs', + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/index.js", + "es": "./dist/index.esm.mjs", + "modern": "./dist/mod.modern.mjs", + "umd": "./dist/index.umd.js", + } + `); + }); + }); + + describe('Package Exports', () => { + it('should generate entries using only an export map', () => { + const entries = computeEntries( + mk({ + exports: './dist/mod.mjs', + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.esm.mjs", + "modern": "./dist/mod.mjs", + "umd": "./dist/mod.umd.js", + } + `); + }); + + it('should support array fallbacks for default exports (mjs first)', () => { + const entries = computeEntries( + mk({ + exports: ['./dist/mod.mjs', './dist/mod-cjs.js'], + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod-cjs.js", + "es": "./dist/mod.esm.mjs", + "modern": "./dist/mod.mjs", + "umd": "./dist/mod.umd.js", + } + `); + }); + + it('should support array fallbacks for default exports (cjs first)', () => { + const entries = computeEntries( + mk({ + type: 'module', + exports: ['./dist/mod.cjs', './dist/mod.js'], + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.cjs", + "es": "./dist/mod.esm.js", + "modern": "./dist/mod.js", + "umd": "./dist/mod.umd.cjs", + } + `); + }); + + it('should default to generating modern output for "import"', () => { + const entries = computeEntries( + mk({ + exports: { + import: './dist/mod.modern.mjs', + require: './dist/mod.js', + default: './dist/mod.umd.js', + }, + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.esm.mjs", + "modern": "./dist/mod.modern.mjs", + "umd": "./dist/mod.umd.js", + } + `); + }); + + it('should generate entries when all exports are explicitly defined', () => { + const entries = computeEntries( + mk({ + exports: { + modern: './dist/mod.modern.mjs', + import: './dist/mod.mjs', + require: './dist/mod.js', + default: './dist/mod.umd.js', + }, + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.mjs", + "modern": "./dist/mod.modern.mjs", + "umd": "./dist/mod.umd.js", + } + `); + }); + }); + + describe('both legacy and package exports', () => { + it('should use exports as modern', () => { + const entries = computeEntries( + mk({ + main: './dist/mod.js', + module: './dist/mod.mjs', + exports: './dist/mod.modern.mjs', + unpkg: './dist/mod-umd.js', + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.mjs", + "modern": "./dist/mod.modern.mjs", + "umd": "./dist/mod-umd.js", + } + `); + }); + + it('should generate entries when defined in both maps', () => { + const entries = computeEntries( + mk({ + main: './dist/mod.js', + module: './dist/mod.mjs', + unpkg: './dist/mod-umd.js', + exports: { + import: './dist/mod.modern.mjs', + default: './dist/mod.js', + }, + source: ['src/a.js'], + }), + ); + + expect(entries).toMatchInlineSnapshot(` + Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.mjs", + "modern": "./dist/mod.modern.mjs", + "umd": "./dist/mod-umd.js", + } + `); + }); + }); + }); + + describe('multi entry', () => { + function ensureEntriesHaveFilename(entries, name) { + const reg = new RegExp(`\\/${name}(\\.\\w+)*\\.[mc]?js$`); + expect(entries).toEqual({ + cjs: expect.stringMatching(reg), + es: expect.stringMatching(reg), + modern: expect.stringMatching(reg), + umd: expect.stringMatching(reg), + }); + } + + describe('only legacy entries', () => { + it('"main"', () => { + const opts = mk({ + main: './dist/mod.js', + source: ['src/a.js', 'src/b.js'], + }); + const entries = { + './a': computeEntries({ ...opts, entry: 'src/a.js' }), + './b': computeEntries({ ...opts, entry: 'src/b.js' }), + }; + + // default entry gets its name from `main`: + ensureEntriesHaveFilename(entries['./a'], 'mod'); + // all additional entries compute their own names: + ensureEntriesHaveFilename(entries['./b'], 'b'); + + expect(entries).toMatchInlineSnapshot(` + Object { + "./a": Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.esm.js", + "modern": "./dist/mod.modern.js", + "umd": "./dist/mod.umd.js", + }, + "./b": Object { + "cjs": "./dist/b.js", + "es": "./dist/b.esm.js", + "modern": "./dist/b.modern.js", + "umd": "./dist/b.umd.js", + }, + } + `); + }); + + it('"module", "main"', () => { + const opts = mk({ + module: './dist/mod.esm.js', + main: './dist/mod.js', + source: ['src/a.js', 'src/b.js'], + }); + const entries = { + './a': computeEntries({ ...opts, entry: 'src/a.js' }), + './b': computeEntries({ ...opts, entry: 'src/b.js' }), + }; + + // default entry gets its name from `main`: + ensureEntriesHaveFilename(entries['./a'], 'mod'); + // all additional entries compute their own names: + ensureEntriesHaveFilename(entries['./b'], 'b'); + + expect(entries).toMatchInlineSnapshot(` + Object { + "./a": Object { + "cjs": "./dist/mod.js", + "es": "./dist/mod.esm.js", + "modern": "./dist/mod.modern.js", + "umd": "./dist/mod.umd.js", + }, + "./b": Object { + "cjs": "./dist/b.js", + "es": "./dist/b.esm.js", + "modern": "./dist/b.modern.js", + "umd": "./dist/b.umd.js", + }, + } + `); + }); + }); + + describe('only package exports', () => { + it('should generate multiple entries when all exports are defined, in a CJS/legacy package', () => { + const opts = mk({ + exports: { + '.': { + modern: './dist/main.modern.mjs', + import: './dist/main.mjs', + require: './dist/main.js', + default: './dist/main.umd.js', + }, + './b': { + modern: './dist/b.modern.mjs', + import: './dist/b.mjs', + require: './dist/b.js', + default: './dist/b.umd.js', + }, + }, + source: ['src/a.js', 'src/b.js'], + }); + const entries = { + './a': computeEntries({ ...opts, entry: 'src/a.js' }), + './b': computeEntries({ ...opts, entry: 'src/b.js' }), + }; + + // default entry gets its name from `main`: + ensureEntriesHaveFilename(entries['./a'], 'main'); + // all additional entries compute their own names: + ensureEntriesHaveFilename(entries['./b'], 'b'); + + expect(entries).toMatchInlineSnapshot(` + Object { + "./a": Object { + "cjs": "./dist/main.js", + "es": "./dist/main.mjs", + "modern": "./dist/main.modern.mjs", + "umd": "./dist/main.umd.js", + }, + "./b": Object { + "cjs": "./dist/b.js", + "es": "./dist/b.mjs", + "modern": "./dist/b.modern.mjs", + "umd": "./dist/b.umd.js", + }, + } + `); + }); + + it('should generate multiple entries when all exports are defined, in a {type:module} package', () => { + const opts = mk({ + type: 'module', + exports: { + '.': { + modern: './dist/main.modern.js', + import: './dist/main.js', + require: './dist/main.cjs', + default: './dist/main.umd.cjs', + }, + './b': { + modern: './dist/b.modern.js', + import: './dist/b.js', + require: './dist/b.cjs', + default: './dist/b.umd.cjs', + }, + }, + source: ['src/a.js', 'src/b.js'], + }); + const entries = { + './a': computeEntries({ ...opts, entry: 'src/a.js' }), + './b': computeEntries({ ...opts, entry: 'src/b.js' }), + }; + + // default entry gets its name from `main`: + ensureEntriesHaveFilename(entries['./a'], 'main'); + // all additional entries compute their own names: + ensureEntriesHaveFilename(entries['./b'], 'b'); + + expect(entries).toMatchInlineSnapshot(` + Object { + "./a": Object { + "cjs": "./dist/main.cjs", + "es": "./dist/main.js", + "modern": "./dist/main.modern.js", + "umd": "./dist/main.umd.cjs", + }, + "./b": Object { + "cjs": "./dist/b.cjs", + "es": "./dist/b.js", + "modern": "./dist/b.modern.js", + "umd": "./dist/b.umd.cjs", + }, + } + `); + }); + }); + }); +}); diff --git a/test/fixtures/name-multi-source/a.js b/test/fixtures/name-multi-source/a.js new file mode 100644 index 00000000..cbceb747 --- /dev/null +++ b/test/fixtures/name-multi-source/a.js @@ -0,0 +1 @@ +console.log('i am a'); diff --git a/test/fixtures/name-multi-source/b.js b/test/fixtures/name-multi-source/b.js new file mode 100644 index 00000000..ad58616c --- /dev/null +++ b/test/fixtures/name-multi-source/b.js @@ -0,0 +1 @@ +console.log('i am b'); diff --git a/test/fixtures/name-multi-source/package.json b/test/fixtures/name-multi-source/package.json new file mode 100644 index 00000000..19d005fb --- /dev/null +++ b/test/fixtures/name-multi-source/package.json @@ -0,0 +1,20 @@ +{ + "name": "name-multi-source", + "main": "./dist/a.js", + "module": "./dist/a.mjs", + "umd:main": "./dist/a.umd.js", + "exports": { + ".": { + "import": "./dist/a.mjs", + "default": "./dist/a.js" + }, + "./b": { + "import": "./dist/b.mjs", + "default": "./dist/b.js" + } + }, + "source": [ + "./a.js", + "./b.js" + ] +}