diff --git a/.gitignore b/.gitignore index 0d70df8d..31732da9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ dist *.log .yalc yalc.lock -test/**/*.js.map \ No newline at end of file +test/**/*.js.map +.DS_Store \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 427c45a9..f8a32cbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: false - excludeLinksFromLockfile: false - dependencies: '@rollup/plugin-commonjs': specifier: ~24.0.1 @@ -3440,3 +3436,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false diff --git a/src/bundle.ts b/src/bundle.ts index c93aa504..67e1b98c 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -59,7 +59,7 @@ async function bundle( const pkg = await getPackageMeta(cwd) const packageType = getPackageType(pkg) - const exportPaths = getExportPaths(pkg) + const exportPaths = getExportPaths(pkg, cwd) const exportKeys = Object.keys(exportPaths).filter( (key) => key !== './package.json', diff --git a/src/exports.ts b/src/exports.ts index c4239c1d..5c928cd5 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,3 +1,5 @@ +import fs from 'fs' +import { join, resolve, dirname, extname } from 'path' import type { PackageMetadata, ExportCondition, @@ -5,8 +7,8 @@ import type { PackageType, ParsedExportCondition, } from './types' -import { join, resolve, dirname, extname } from 'path' import { filenameWithoutExtension } from './utils' +import { availableExtensions, availableExportConventions } from './constants' export function getTypings(pkg: PackageMetadata) { return pkg.types || pkg.typings @@ -80,6 +82,107 @@ function findExport( }) } +// Should exclude all outDirs since they are readable on `fs.readdirSync` +// Example: +// { 'import': './someOutDir/index.js' } => ['someOutDir'] +// { 'import': './importDir/index.js', 'require': './requireDir/index.js' } => ['importDir', 'requireDir'] +function getOutDirs(exportsConditions: ExportCondition) { + return [ + ...new Set( + Object.values(exportsConditions) + .flatMap((value) => Object.values(value)) + .flatMap((innerValue) => + typeof innerValue === 'string' ? [innerValue] : [], + ), + ), + ] + .map((value) => value.split('/')[1]) + .filter(Boolean) +} + +function resolveWildcardEntry( + wildcardEntry: { + [key: string]: ExportCondition + }, + cwd: string, + excludes: string[], +): { + [key: string]: ExportCondition +}[] { + const dirents = fs.readdirSync(cwd, { withFileTypes: true }) + + const allowedExtensions = [ + ...availableExtensions, + ...availableExportConventions, + ].map((ext) => `.${ext}`) + + const resolvedExports = dirents.flatMap((dirent) => { + // Skip outDirs and existing ExportConditions keys + if (excludes.includes(dirent.name)) return + + if (dirent.isDirectory()) { + // Read inside src directory + if (dirent.name === 'src') { + return resolveWildcardEntry(wildcardEntry, `${cwd}/src`, excludes) + } + + const dirName = dirent.name + const hasIndexFile = fs + .readdirSync(`${cwd}/${dirName}`) + .some((file) => file.startsWith('index')) + + if (hasIndexFile) { + return { + [`./${dirName}`]: JSON.parse( + JSON.stringify(wildcardEntry).replace(/\*/g, `${dirName}/index`), + ), + } + } + } + + if (dirent.isFile()) { + const fileName = filenameWithoutExtension(dirent.name)! + // ['.'] is for index file, so skip index + if (fileName === 'index') return + if (allowedExtensions.includes(extname(dirent.name))) { + return { + [`./${fileName}`]: JSON.parse( + JSON.stringify(wildcardEntry).replace(/\*/g, fileName), + ), + } + } + } + + return + }) + + return resolvedExports.filter(Boolean) +} + +function resolveWildcardExports( + exportsConditions: { + [key: string]: ExportCondition + }, + cwd: string, +) { + const outDirs = getOutDirs(exportsConditions) + const existingKeys = [ + ...new Set(Object.keys(exportsConditions).flatMap((key) => key.split('/'))), + ] + const excludes = [...outDirs, ...existingKeys] + + const wildcardEntry = exportsConditions['./*'] as { + [key: string]: ExportCondition + } + + const resolvedEntry = resolveWildcardEntry(wildcardEntry, cwd, excludes) + + const resolvedExports = Object.assign({}, exportsConditions, ...resolvedEntry) + delete resolvedExports['./*'] + + return resolvedExports +} + /** * * Convert package.exports field to paths mapping @@ -168,15 +271,24 @@ function parseExport( * pkg.main and pkg.module will be added to ['.'] if exists */ -export function getExportPaths(pkg: PackageMetadata) { +export function getExportPaths(pkg: PackageMetadata, cwd: string) { const pathsMap: Record = {} const packageType = getPackageType(pkg) const isCjsPackage = packageType === 'commonjs' - + const { exports: exportsConditions } = pkg - + if (exportsConditions) { - const paths = parseExport(exportsConditions, packageType) + let resolvedExportsConditions = exportsConditions + + if ( + Object.keys(exportsConditions).some((key) => key === './*') && + typeof exportsConditions !== 'string' + ) { + resolvedExportsConditions = resolveWildcardExports(exportsConditions, cwd) + } + + const paths = parseExport(resolvedExportsConditions, packageType) Object.assign(pathsMap, paths) } @@ -189,7 +301,7 @@ export function getExportPaths(pkg: PackageMetadata) { }, packageType, ) - + if (isCjsPackage && pathsMap['.']?.['require']) { // pathsMap's exports.require are prioritized. defaultMainExport['require'] = pathsMap['.']['require'] diff --git a/test/integration.test.ts b/test/integration.test.ts index ecd63a1b..53f77c46 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -236,6 +236,42 @@ const testCases: { ) }, }, + { + name: 'wildcard-exports', + args: [], + async expected(dir, { stdout }) { + const contentsRegex = { + './dist/index.js': /'index'/, + './dist/layout/index.js': /'layout'/, + './dist/server/edge.mjs': /'server.edge-light'/, + './dist/server/react-server.mjs': /'server.react-server'/, + } + + assertFilesContent(dir, contentsRegex) + + const log = `\ + ✓ Typed dist/lite.d.ts - 70 B + ✓ Typed dist/input.d.ts - 65 B + ✓ Typed dist/index.d.ts - 65 B + ✓ Typed dist/server/index.d.ts - 87 B + ✓ Typed dist/layout/index.d.ts - 66 B + ✓ Typed dist/button.d.ts - 66 B + ✓ Built dist/input.js - 50 B + ✓ Built dist/index.js - 50 B + ✓ Built dist/button.js - 53 B + ✓ Built dist/lite.js - 72 B + ✓ Built dist/layout/index.js - 51 B + ✓ Built dist/server/react-server.mjs - 53 B + ✓ Built dist/server/edge.mjs - 51 B + ✓ Built dist/server/index.mjs - 71 B + ` + + const rawStdout = stripANSIColor(stdout) + log.split('\n').forEach((line: string) => { + expect(rawStdout).toContain(line.trim()) + }) + }, + }, ] async function runBundle( diff --git a/test/integration/wildcard-exports/package.json b/test/integration/wildcard-exports/package.json new file mode 100644 index 00000000..bb19d910 --- /dev/null +++ b/test/integration/wildcard-exports/package.json @@ -0,0 +1,20 @@ +{ + "name": "wildcard-exports", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "react-server": "./dist/server/react-server.mjs", + "edge-light": "./dist/server/edge.mjs", + "import": "./dist/server/index.mjs" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js" + } + } +} \ No newline at end of file diff --git a/test/integration/wildcard-exports/src/button.ts b/test/integration/wildcard-exports/src/button.ts new file mode 100644 index 00000000..612b01ed --- /dev/null +++ b/test/integration/wildcard-exports/src/button.ts @@ -0,0 +1 @@ +export default 'button' diff --git a/test/integration/wildcard-exports/src/index.ts b/test/integration/wildcard-exports/src/index.ts new file mode 100644 index 00000000..8d97dd4b --- /dev/null +++ b/test/integration/wildcard-exports/src/index.ts @@ -0,0 +1 @@ +export default 'index' diff --git a/test/integration/wildcard-exports/src/input.ts b/test/integration/wildcard-exports/src/input.ts new file mode 100644 index 00000000..59ebf1ee --- /dev/null +++ b/test/integration/wildcard-exports/src/input.ts @@ -0,0 +1 @@ +export default 'input' diff --git a/test/integration/wildcard-exports/src/layout/index.ts b/test/integration/wildcard-exports/src/layout/index.ts new file mode 100644 index 00000000..bbe93e22 --- /dev/null +++ b/test/integration/wildcard-exports/src/layout/index.ts @@ -0,0 +1 @@ +export default 'layout' diff --git a/test/integration/wildcard-exports/src/lite.ts b/test/integration/wildcard-exports/src/lite.ts new file mode 100644 index 00000000..4f6492c6 --- /dev/null +++ b/test/integration/wildcard-exports/src/lite.ts @@ -0,0 +1,3 @@ +export default function lite(c: string) { + return 'lite' + c +} diff --git a/test/integration/wildcard-exports/src/server/index.edge-light.ts b/test/integration/wildcard-exports/src/server/index.edge-light.ts new file mode 100644 index 00000000..7ce8f818 --- /dev/null +++ b/test/integration/wildcard-exports/src/server/index.edge-light.ts @@ -0,0 +1 @@ +export const name = 'server.edge-light' diff --git a/test/integration/wildcard-exports/src/server/index.react-server.ts b/test/integration/wildcard-exports/src/server/index.react-server.ts new file mode 100644 index 00000000..1f9b61e1 --- /dev/null +++ b/test/integration/wildcard-exports/src/server/index.react-server.ts @@ -0,0 +1 @@ +export const name = 'server.react-server' diff --git a/test/integration/wildcard-exports/src/server/index.ts b/test/integration/wildcard-exports/src/server/index.ts new file mode 100644 index 00000000..993389d3 --- /dev/null +++ b/test/integration/wildcard-exports/src/server/index.ts @@ -0,0 +1,2 @@ +export const name = 'server.index' +export const main = true diff --git a/test/integration/wildcard-exports/tsconfig.json b/test/integration/wildcard-exports/tsconfig.json new file mode 100644 index 00000000..2f980427 --- /dev/null +++ b/test/integration/wildcard-exports/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/test/lib-unit/exports.test.ts b/test/lib-unit/exports.test.ts index 55759278..ea748234 100644 --- a/test/lib-unit/exports.test.ts +++ b/test/lib-unit/exports.test.ts @@ -6,6 +6,8 @@ import { getExportTypeDist, } from '../../src/exports' +const cwd = path.resolve(__dirname) + describe('lib exports', () => { describe('getExportPaths', () => { it('should handle the basic main fields paths (cjs)', () => { @@ -13,7 +15,7 @@ describe('lib exports', () => { main: './dist/index.cjs', module: './dist/index.esm.js', } - const result = getExportPaths(pkg) + const result = getExportPaths(pkg, cwd) expect(result).toEqual({ '.': { require: './dist/index.cjs', @@ -24,34 +26,74 @@ describe('lib exports', () => { it('should handle types field', () => { expect( - getExportPaths({ - exports: { - '.': { - import: './dist/index.mjs', - types: './dist/index.d.ts', + getExportPaths( + { + exports: { + '.': { + import: './dist/index.mjs', + types: './dist/index.d.ts', + }, }, }, - }), + cwd, + ), ).toEqual({ '.': { import: './dist/index.mjs', types: './dist/index.d.ts', }, }) + }) + it('should handle wildcard exports', () => { expect( - getExportPaths({ - typings: './dist/index.d.ts', - exports: { - '.': { - import: './dist/index.mjs', + getExportPaths( + { + exports: { + '.': { + types: './dist/index.d.ts', + import: './dist/index.js', + }, + './server': { + types: './dist/server/index.d.ts', + 'react-server': './dist/server/react-server.mjs', + 'edge-light': './dist/server/edge.mjs', + import: './dist/server/index.mjs', + }, + './*': { + types: './dist/*.d.ts', + import: './dist/*.js', + }, }, }, - }), + path.join(__dirname, '../integration/wildcard-exports'), + ), ).toEqual({ '.': { - import: './dist/index.mjs', types: './dist/index.d.ts', + import: './dist/index.js', + }, + './server': { + types: './dist/server/index.d.ts', + 'react-server': './dist/server/react-server.mjs', + 'edge-light': './dist/server/edge.mjs', + import: './dist/server/index.mjs', + }, + './button': { + types: './dist/button.d.ts', + import: './dist/button.js', + }, + './input': { + types: './dist/input.d.ts', + import: './dist/input.js', + }, + './layout': { + types: './dist/layout/index.d.ts', + import: './dist/layout/index.js', + }, + './lite': { + types: './dist/lite.d.ts', + import: './dist/lite.js', }, }) }) @@ -63,7 +105,7 @@ describe('lib exports', () => { main: './dist/index.mjs', module: './dist/index.esm.js', } - const result = getExportPaths(pkg) + const result = getExportPaths(pkg, cwd) expect(result).toEqual({ '.': { import: './dist/index.mjs', @@ -75,15 +117,18 @@ describe('lib exports', () => { it('should handle the exports conditions', () => { expect( - getExportPaths({ - exports: { - '.': { - require: './dist/index.cjs', - module: './dist/index.esm.js', - default: './dist/index.esm.js', + getExportPaths( + { + exports: { + '.': { + require: './dist/index.cjs', + module: './dist/index.esm.js', + default: './dist/index.esm.js', + }, }, }, - }), + cwd, + ), ).toEqual({ '.': { require: './dist/index.cjs', @@ -93,14 +138,17 @@ describe('lib exports', () => { }) expect( - getExportPaths({ - exports: { - '.': { - import: './dist/index.mjs', - require: './dist/index.cjs', + getExportPaths( + { + exports: { + '.': { + import: './dist/index.mjs', + require: './dist/index.cjs', + }, }, }, - }), + cwd, + ), ).toEqual({ '.': { import: './dist/index.mjs', @@ -111,16 +159,19 @@ describe('lib exports', () => { it('should handle the mixed exports conditions', () => { expect( - getExportPaths({ - main: './dist/index.cjs', - exports: { - '.': { - sub: { - require: './dist/index.cjs', + getExportPaths( + { + main: './dist/index.cjs', + exports: { + '.': { + sub: { + require: './dist/index.cjs', + }, }, }, }, - }), + cwd, + ), ).toEqual({ '.': { require: './dist/index.cjs', @@ -131,17 +182,20 @@ describe('lib exports', () => { }) expect( - getExportPaths({ - main: './dist/index.js', - module: './dist/index.esm.js', - types: './dist/index.d.ts', - exports: { - types: './dist/index.d.ts', - import: './dist/index.mjs', + getExportPaths( + { + main: './dist/index.js', module: './dist/index.esm.js', - require: './dist/index.js', + types: './dist/index.d.ts', + exports: { + types: './dist/index.d.ts', + import: './dist/index.mjs', + module: './dist/index.esm.js', + require: './dist/index.js', + }, }, - }), + cwd, + ), ).toEqual({ '.': { types: './dist/index.d.ts', @@ -156,13 +210,16 @@ describe('lib exports', () => { const logSpy = jest.spyOn(console, 'warn') expect( - getExportPaths({ - main: './dist/index.js', - exports: { - import: './dist/index.mjs', - require: './dist/index.cjs', + getExportPaths( + { + main: './dist/index.js', + exports: { + import: './dist/index.mjs', + require: './dist/index.cjs', + }, }, - }), + cwd, + ), ).toEqual({ '.': { import: './dist/index.mjs', @@ -181,7 +238,7 @@ describe('lib exports', () => { pkg: PackageMetadata, exportName: string = '.', ) { - const parsedExportCondition = getExportPaths(pkg) + const parsedExportCondition = getExportPaths(pkg, cwd) const parsedExport = { source: `./src/${exportName === '.' ? 'index' : exportName}.ts`, name: exportName, @@ -241,7 +298,7 @@ describe('lib exports', () => { pkg: PackageMetadata, exportName: string = '.', ) { - const parsedExportCondition = getExportPaths(pkg) + const parsedExportCondition = getExportPaths(pkg, cwd) const parsedExport = { source: `./src/${exportName === '.' ? 'index' : exportName}.ts`, name: exportName,