diff --git a/package.json b/package.json index 40c2cbf..1a6c6ae 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@rollup/plugin-replace": "^5.0.7", "@rollup/pluginutils": "^5.1.0", "esbuild": "^0.23.0", + "glob": "^11.0.0", "magic-string": "^0.30.10", "rollup": "^4.18.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc26f97..25913f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: esbuild: specifier: ^0.23.0 version: 0.23.0 + glob: + specifier: ^11.0.0 + version: 11.0.0 magic-string: specifier: ^0.30.10 version: 0.30.10 @@ -1480,8 +1483,14 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported global-cache-dir@6.0.0: resolution: {integrity: sha512-UOwXU6ulg3VQsSyKf0QAVcW4EFq3hFehFHV/ne76iQ9FAw4ZpXHXsmw8AwUueGI13y4apVML/Pb+njilLn/RCw==} @@ -1781,6 +1790,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.0.1: + resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==} + engines: {node: 20 || >=22} + jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1901,6 +1914,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.0.0: + resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==} + engines: {node: 20 || >=22} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1959,6 +1976,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2148,6 +2169,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4228,6 +4253,15 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 1.11.1 + glob@11.0.0: + dependencies: + foreground-child: 3.2.1 + jackspeak: 4.0.1 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 2.0.0 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -4521,6 +4555,12 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.0.1: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -4682,6 +4722,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.0.0: {} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -4744,6 +4786,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4934,6 +4980,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.0 + minipass: 7.1.2 + path-type@4.0.0: {} path-type@5.0.0: {} diff --git a/src/cli.ts b/src/cli.ts index bd8407e..f8e7cad 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,7 +6,7 @@ import { readPackageJson } from './utils/read-package-json.js'; import { getExportEntries } from './utils/parse-package-json/get-export-entries.js'; import { getAliases } from './utils/parse-package-json/get-aliases.js'; import { normalizePath } from './utils/normalize-path.js'; -import { getSourcePath } from './utils/get-source-path.js'; +import { getSourcePaths } from './utils/get-source-path.js'; import { getRollupConfigs } from './utils/get-rollup-configs.js'; import { getTsconfig } from './utils/get-tsconfig'; import { log } from './utils/log.js'; @@ -140,10 +140,8 @@ if (tsconfigTarget) { throw new Error('No export entries found in package.json'); } - const sourcePaths = await Promise.all(exportEntries.map(async exportEntry => ({ - ...(await getSourcePath(exportEntry, sourcePath, distPath)), - exportEntry, - }))); + const sourcePaths = (await Promise.all(exportEntries.map(exportEntry => getSourcePaths(exportEntry, sourcePath, distPath, cwd)))); + const flatSourcePaths = sourcePaths.flat(); const rollupConfigs = await getRollupConfigs( @@ -155,7 +153,7 @@ if (tsconfigTarget) { */ normalizePath(fs.realpathSync.native(sourcePath), true), distPath, - sourcePaths, + flatSourcePaths, argv.flags, getAliases(packageJson, cwd), packageJson, diff --git a/src/local-typescript-loader.ts b/src/local-typescript-loader.ts index 8d1f455..781bd64 100644 --- a/src/local-typescript-loader.ts +++ b/src/local-typescript-loader.ts @@ -9,5 +9,5 @@ const getLocalTypescriptPath = () => { } }; -// eslint-disable-next-line n/global-require, import-x/no-dynamic-require +// eslint-disable-next-line import-x/no-dynamic-require export default require(getLocalTypescriptPath()); diff --git a/src/utils/get-source-path.ts b/src/utils/get-source-path.ts index 2b50c53..bccb203 100644 --- a/src/utils/get-source-path.ts +++ b/src/utils/get-source-path.ts @@ -1,3 +1,4 @@ +import { globSync } from 'glob'; import type { ExportEntry } from '../types.js'; import { fsExists } from './fs-exists.js'; @@ -6,10 +7,11 @@ const { stringify } = JSON; const tryExtensions = async ( pathWithoutExtension: string, extensions: readonly string[], + checker: (sourcePath: string)=>Promise, ) => { for (const extension of extensions) { const pathWithExtension = pathWithoutExtension + extension; - if (await fsExists(pathWithExtension)) { + if (await checker(pathWithExtension)) { return { extension, path: pathWithExtension, @@ -33,7 +35,8 @@ export const getSourcePath = async ( exportEntry: ExportEntry, source: string, dist: string, -) => { + checker: (sourcePath: string)=>Promise = fsExists, +): Promise> => { const sourcePathUnresolved = source + exportEntry.outputPath.slice(dist.length); for (const distExtension of distExtensions) { @@ -41,6 +44,7 @@ export const getSourcePath = async ( const sourcePath = await tryExtensions( sourcePathUnresolved.slice(0, -distExtension.length), extensionMap[distExtension], + checker, ); if (sourcePath) { @@ -55,3 +59,43 @@ export const getSourcePath = async ( throw new Error(`Could not find matching source file for export path ${stringify(exportEntry.outputPath)}`); }; + +interface SourcePath { + exportEntry: ExportEntry; + input: string; + srcExtension: string; + distExtension: string; +} + +export const getSourcePaths = async ( + exportEntry: ExportEntry, + sourcePath: string, + distPath: string, + cwd: string, +): Promise => { + if (exportEntry.outputPath.includes('*')) { + // use glob to resolve matches from the packageJsonRoot directory + const matchSet = new Set(); + const sourceMatch = await getSourcePath(exportEntry, sourcePath, distPath, async (path) => { + const matches = globSync(path, { cwd }); + for (const match of matches) { matchSet.add(match); } + return matches.length > 0; // always return false to prevent early exit + }); + + const matchedPaths = Array.from(matchSet); + + const allMatches = matchedPaths.map(match => ({ + exportEntry, + input: match, + srcExtension: sourceMatch.srcExtension, + distExtension: sourceMatch.distExtension, + })); + + return allMatches; + } + + return [{ + exportEntry, + ...await getSourcePath(exportEntry, sourcePath, distPath, fsExists), + }]; +}; diff --git a/src/utils/parse-package-json/get-export-entries.ts b/src/utils/parse-package-json/get-export-entries.ts index 231fd96..c024e36 100644 --- a/src/utils/parse-package-json/get-export-entries.ts +++ b/src/utils/parse-package-json/get-export-entries.ts @@ -15,18 +15,24 @@ const getFileType = ( const isPath = (filePath: string) => filePath.startsWith('.'); +interface ParseExportsContext { + type: PackageType | 'types'; + platform?: 'node'; + path: string; +} + const parseExportsMap = ( exportMap: PackageJson['exports'], - packageType: PackageType, - packagePath = 'exports', + parameters: ParseExportsContext, ): ExportEntry[] => { + const { type, path } = parameters; if (exportMap) { if (typeof exportMap === 'string') { if (isPath(exportMap)) { return [{ outputPath: exportMap, - type: getFileType(exportMap) || packageType, - from: packagePath, + type: getFileType(exportMap) || type, + from: path, }]; } @@ -35,72 +41,69 @@ const parseExportsMap = ( if (Array.isArray(exportMap)) { return exportMap.flatMap( - (exportPath, index) => { - const from = `${packagePath}[${index}]`; - - return ( - typeof exportPath === 'string' - ? ( - isPath(exportPath) - ? { - outputPath: exportPath, - type: getFileType(exportPath) || packageType, - from, - } - : [] - ) - : parseExportsMap(exportPath, packageType, from) - ); - }, + (exportPath, index) => parseExportsMap(exportPath, { + ...parameters, + path: `${path}[${index}]`, + }), ); } if (typeof exportMap === 'object') { return Object.entries(exportMap).flatMap(([key, value]) => { - if (typeof value === 'string') { - const baseEntry = { - outputPath: value, - from: `${packagePath}.${key}`, - }; - - if (key === 'require') { - return { - ...baseEntry, - type: 'commonjs', - }; - } - - if (key === 'import') { - return { - ...baseEntry, - type: getFileType(value) || packageType, - }; - } - - if (key === 'types') { - return { - ...baseEntry, - type: 'types', - }; - } - - if (key === 'node') { - return { - ...baseEntry, - type: getFileType(value) || packageType, - platform: 'node', - }; - } - - if (key === 'default') { - return { - ...baseEntry, - type: getFileType(value) || packageType, - }; - } + const baseParameters = { + ...parameters, + path: `${path}.${key}`, + }; + + // otherwise, key is an export condition + if (key === 'require') { + return parseExportsMap(value, { + ...baseParameters, + type: 'commonjs', + }); + } + + if (key === 'import') { + return parseExportsMap(value, { + ...baseParameters, + type: 'module', + }); + } + + if (key === 'types') { + return parseExportsMap(value, { + ...baseParameters, + type: 'types' as PackageType, + }); + } + + if (key === 'node') { + return parseExportsMap(value, { + ...baseParameters, + platform: 'node', + }); } - return parseExportsMap(value, packageType, `${packagePath}.${key}`); + if (key === 'default') { + return parseExportsMap(value, { + ...baseParameters, + }); + } + + if (isPath(key)) { + // key is a relative path + // format the path a little more nicely + return parseExportsMap(value, { + ...parameters, + path: `${path}["${key}"]`, + }); + } + + // non-standard export condition, probably + return parseExportsMap(value, { + ...parameters, + path: `${path}.${key}`, + }); }); } } @@ -134,13 +137,13 @@ const addExportPath = ( export const getExportEntries = (packageJson: PackageJson) => { const exportEntriesMap: Record = {}; - const packageType = packageJson.type ?? 'commonjs'; + const type = packageJson.type ?? 'commonjs'; if (packageJson.main) { const mainPath = packageJson.main; addExportPath(exportEntriesMap, { outputPath: mainPath, - type: getFileType(mainPath) ?? packageType, + type: getFileType(mainPath) ?? type, from: 'main', }); } @@ -170,7 +173,7 @@ export const getExportEntries = (packageJson: PackageJson) => { if (typeof bin === 'string') { addExportPath(exportEntriesMap, { outputPath: bin, - type: getFileType(bin) ?? packageType, + type: getFileType(bin) ?? type, isExecutable: true, from: 'bin', }); @@ -178,7 +181,7 @@ export const getExportEntries = (packageJson: PackageJson) => { for (const [binName, binPath] of Object.entries(bin)) { addExportPath(exportEntriesMap, { outputPath: binPath!, - type: getFileType(binPath!) ?? packageType, + type: getFileType(binPath!) ?? type, isExecutable: true, from: `bin.${binName}`, }); @@ -187,7 +190,10 @@ export const getExportEntries = (packageJson: PackageJson) => { } if (packageJson.exports) { - const exportMap = parseExportsMap(packageJson.exports, packageType); + const exportMap = parseExportsMap(packageJson.exports, { + type, + path: 'exports', + }); for (const exportEntry of exportMap) { addExportPath(exportEntriesMap, exportEntry); } diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 8b38387..d3c20a5 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -35,6 +35,19 @@ export const fixtureFiles = { `, }, + pages: { + 'a.ts': outdent` + export function render() { + console.log('Page A'); + } + `, + 'b.ts': outdent` + export function render() { + console.log('Page B'); + } + `, + }, + 'cjs.cjs': outdent` #! /usr/bin/env node @@ -184,7 +197,7 @@ export const fixtureDependencyExportsMap = ( 'dependency-exports-import.js': outdent` import esm from 'dependency-exports-dual'; - + console.log(esm); `, }, diff --git a/tests/specs/builds/minification.ts b/tests/specs/builds/minification.ts index cb506fe..2303b36 100644 --- a/tests/specs/builds/minification.ts +++ b/tests/specs/builds/minification.ts @@ -31,7 +31,7 @@ export default testSuite(({ describe }, nodePath: string) => { // Minification should preserve name expect( - // eslint-disable-next-line n/global-require, @typescript-eslint/no-var-requires + // eslint-disable-next-line @typescript-eslint/no-var-requires require(fixture.getPath('dist/target.js')).functionName, ).toBe('preservesName'); }); diff --git a/tests/specs/builds/package-exports.ts b/tests/specs/builds/package-exports.ts index da5ead5..72e4a19 100644 --- a/tests/specs/builds/package-exports.ts +++ b/tests/specs/builds/package-exports.ts @@ -136,5 +136,44 @@ export default testSuite(({ describe }, nodePath: string) => { const utilsMjs = await fixture.readFile('dist/utils.js', 'utf8'); expect(utilsMjs).toMatch('exports.sayHello ='); }); + + test('conditions - wildcard subpath exports', async () => { + await using fixture = await createFixture({ + ...packageFixture({ installTypeScript: true }), + 'package.json': createPackageJson({ + exports: { + './pages/*': { + import: './dist/pages/*.mjs', + require: './dist/pages/*.cjs', + types: './dist/pages/*.d.ts', + }, + '.': './dist/index.js', + }, + }), + }); + + const pkgrollProcess = await pkgroll([], { + cwd: fixture.path, + nodePath, + }); + + expect(pkgrollProcess.exitCode).toBe(0); + expect(pkgrollProcess.stderr).toBe(''); + + const indexCjs = await fixture.readFile('dist/pages/a.cjs', 'utf8'); + expect(indexCjs).toMatch('exports.render'); + const indexMjs = await fixture.readFile('dist/pages/a.mjs', 'utf8'); + expect(indexMjs).toMatch('export { render }'); + const utilsCjs = await fixture.readFile('dist/pages/b.cjs', 'utf8'); + expect(utilsCjs).toMatch('exports.render'); + const utilsMjs = await fixture.readFile('dist/pages/b.mjs', 'utf8'); + expect(utilsMjs).toMatch('export { render }'); + + expect(await fixture.exists('dist/index.js')).toEqual(true); + expect(await fixture.exists('dist/pages/a.js')).toEqual(true); + expect(await fixture.exists('dist/pages/b.js')).toEqual(true); + expect(await fixture.exists('dist/pages/a.d.ts')).toEqual(true); + expect(await fixture.exists('dist/pages/b.d.ts')).toEqual(true); + }); }); });