From 149af90e0329c606cfbf3ff31e571b368f946c61 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 10 Jul 2023 06:49:53 +0900 Subject: [PATCH 01/16] feat: add wildcard exports --- .gitignore | 3 +- src/bundle.ts | 2 +- src/exports.ts | 113 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 6 deletions(-) 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/src/bundle.ts b/src/bundle.ts index 90c6659a..e6382829 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -58,7 +58,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) diff --git a/src/exports.ts b/src/exports.ts index def8c4c8..bf729c51 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,6 +1,17 @@ -import type { PackageMetadata, ExportCondition, FullExportCondition, PackageType, ParsedExportCondition } from './types' +import fs from 'fs' import { join, resolve, dirname, extname } from 'path' -import { filenameWithoutExtension } from './utils' +import type { + PackageMetadata, + ExportCondition, + FullExportCondition, + PackageType, + ParsedExportCondition, +} from './types' +import { + availableExtensions, + availableExportConventions, + filenameWithoutExtension, +} from './utils' export function getTypings(pkg: PackageMetadata) { return pkg.types || pkg.typings @@ -74,6 +85,87 @@ function findExport( }) } +function getEntries(path: string, hasSrc: boolean): string[] { + const entryPath = hasSrc ? join(path, 'src') : path + + const entries = fs.readdirSync(entryPath, { withFileTypes: true }) + + const availableExt = availableExtensions + .concat(availableExportConventions) + .map((ext) => `.${ext}`) + + return entries.flatMap((entry) => { + if (entry.isDirectory()) { + if (entry.name === 'src') { + return getEntries(entryPath, (hasSrc = true)) + } + return entry.name + '/index' + } + + if ( + entry.isFile() && + availableExt.includes(extname(entry.name)) && + !entry.name.includes('index') + ) { + return filenameWithoutExtension(entry.name)! + } + return [] + }) +} + +function replaceWildcardsWithEntryName( + obj: ExportCondition, + dirName: string +): ExportCondition { + if (typeof obj === 'string') { + return obj.replace('*', dirName) + } else if (typeof obj === 'object' && obj !== null) { + const newObj: { [key: string]: ExportCondition | string } = {} + + for (let key in obj) { + newObj[key] = replaceWildcardsWithEntryName(obj[key], dirName) + } + + return newObj + } else { + return obj + } +} + +function resolveWildcardExports( + exportsConditions: ExportCondition, + cwd: string +): { [key: string]: ExportCondition | string } { + const entryNames = getEntries(cwd, /*hasSrc=*/ false) + + const exportPaths = Object.keys(exportsConditions) + + const newExportsConditions: { [key: string]: ExportCondition | string } = {} + + for (let [exportPath, exportValue] of Object.entries(exportsConditions)) { + if (exportPath.includes('*')) { + entryNames.forEach((entryName) => { + const newExportPath = exportPath + .replace('*', entryName) + .replace('/index', '') + + if (exportPaths.some((path) => path.startsWith(newExportPath))) { + return + } + const newExportValue = replaceWildcardsWithEntryName( + exportValue, + entryName + ) + newExportsConditions[newExportPath] = newExportValue + }) + } else { + newExportsConditions[exportPath] = exportValue + } + } + + return newExportsConditions +} + /** * * Convert package.exports field to paths mapping @@ -160,13 +252,26 @@ function parseExport(exportsCondition: ExportCondition, packageType: PackageType * 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 { exports: exportsConditions } = pkg if (exportsConditions) { - const paths = parseExport(exportsConditions, packageType) + let validatedExportsConditions = exportsConditions + + const hasWildCardExport = Object.keys(exportsConditions).some((key) => + key.includes('*') + ) + + if (hasWildCardExport) { + validatedExportsConditions = resolveWildcardExports( + exportsConditions, + cwd + ) + console.log(validatedExportsConditions, 'validatedExportsConditions') + } + const paths = parseExport(validatedExportsConditions, packageType) Object.assign(pathsMap, paths) } From a984749d9f7d939cad6849710b8d6768ae26ba22 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 10 Jul 2023 06:50:31 +0900 Subject: [PATCH 02/16] test: add exports test for wildcards export --- test/lib-unit/exports.test.ts | 172 ++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 58 deletions(-) diff --git a/test/lib-unit/exports.test.ts b/test/lib-unit/exports.test.ts index bdd0c0be..cbcb04f3 100644 --- a/test/lib-unit/exports.test.ts +++ b/test/lib-unit/exports.test.ts @@ -2,6 +2,8 @@ import type { PackageMetadata } from 'src/types' import path from 'path' import { getExportPaths, getExportConditionDist } from '../../src/exports' +const cwd = path.resolve(__dirname) + describe('lib exports', () => { describe('getExportPaths', () => { it('should handle the basic main fields paths (cjs)', () => { @@ -9,7 +11,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', @@ -20,34 +22,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', }, }) }) @@ -58,7 +100,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', @@ -69,15 +111,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', @@ -87,14 +132,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', @@ -105,16 +153,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', @@ -125,17 +176,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', @@ -152,7 +206,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, @@ -167,21 +221,23 @@ describe('lib exports', () => { it('should dedupe the same path import and module if they are the same path', () => { // dedupe import and module are they have the same path - expect(getExportConditionDistHelper({ - type: 'module', - main: './dist/index.mjs', - module: './dist/index.mjs', - })).toEqual([ - { file: 'index.mjs', format: 'esm' }, - ]) + expect( + getExportConditionDistHelper({ + type: 'module', + main: './dist/index.mjs', + module: './dist/index.mjs', + }) + ).toEqual([{ file: 'index.mjs', format: 'esm' }]) // Do not dedupe import and module are they're different paths - expect(getExportConditionDistHelper({ - module: './dist/index.esm.js', - exports: { - import: './dist/index.mjs', - } - })).toEqual([ + expect( + getExportConditionDistHelper({ + module: './dist/index.esm.js', + exports: { + import: './dist/index.mjs', + }, + }) + ).toEqual([ { file: 'index.mjs', format: 'esm' }, { file: 'index.esm.js', format: 'esm' }, ]) From 5e3a13fe3d1a432b3616654332bd951026d3e513 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 10 Jul 2023 06:54:33 +0900 Subject: [PATCH 03/16] test: add integration test --- test/integration.test.ts | 56 +++++++++++++++++++ .../integration/wildcard-exports/package.json | 20 +++++++ .../wildcard-exports/src/button.ts | 1 + .../integration/wildcard-exports/src/index.ts | 1 + .../integration/wildcard-exports/src/input.ts | 1 + .../wildcard-exports/src/layout/index.ts | 1 + test/integration/wildcard-exports/src/lite.ts | 3 + .../src/server/index.edge-light.ts | 1 + .../src/server/index.react-server.ts | 1 + .../wildcard-exports/src/server/index.ts | 2 + .../wildcard-exports/tsconfig.json | 5 ++ 11 files changed, 92 insertions(+) create mode 100644 test/integration/wildcard-exports/package.json create mode 100644 test/integration/wildcard-exports/src/button.ts create mode 100644 test/integration/wildcard-exports/src/index.ts create mode 100644 test/integration/wildcard-exports/src/input.ts create mode 100644 test/integration/wildcard-exports/src/layout/index.ts create mode 100644 test/integration/wildcard-exports/src/lite.ts create mode 100644 test/integration/wildcard-exports/src/server/index.edge-light.ts create mode 100644 test/integration/wildcard-exports/src/server/index.react-server.ts create mode 100644 test/integration/wildcard-exports/src/server/index.ts create mode 100644 test/integration/wildcard-exports/tsconfig.json diff --git a/test/integration.test.ts b/test/integration.test.ts index 212869ba..f14f31e2 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -198,6 +198,62 @@ const testCases: { ) }, }, + { + name: 'wildcard-exports', + args: [], + async expected(dir, { stdout }) { + const distFiles = [ + './dist/index.js', + './dist/lite.js', + './dist/input.js', + './dist/index.js', + './dist/button.js', + './dist/lite.js', + './dist/layout/index.js', + './dist/server/react-server.mjs', + './dist/server/edge.mjs', + './dist/server/index.mjs', + + // types + './dist/index.d.ts', + './dist/button.d.ts', + './dist/layout/index.d.ts', + './dist/server/index.d.ts', + ] + + 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 + } +} From cafbbcc5095807bb9b2ed395d047b5d694e15fbb Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 10 Jul 2023 12:16:00 +0900 Subject: [PATCH 04/16] fix: add outdirs to not include them in entry --- src/exports.ts | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index bf729c51..3144ff9d 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -85,19 +85,24 @@ function findExport( }) } -function getEntries(path: string, hasSrc: boolean): string[] { +function getEntries( + path: string, + hasSrc: boolean, + outDirs: string[] +): string[] { const entryPath = hasSrc ? join(path, 'src') : path const entries = fs.readdirSync(entryPath, { withFileTypes: true }) - const availableExt = availableExtensions - .concat(availableExportConventions) - .map((ext) => `.${ext}`) + const availableExt = [ + ...availableExtensions, + ...availableExportConventions, + ].map((ext) => `.${ext}`) return entries.flatMap((entry) => { if (entry.isDirectory()) { - if (entry.name === 'src') { - return getEntries(entryPath, (hasSrc = true)) + if (entry.name === 'src' || outDirs.includes(entry.name)) { + return getEntries(entryPath, true, outDirs) } return entry.name + '/index' } @@ -136,7 +141,19 @@ function resolveWildcardExports( exportsConditions: ExportCondition, cwd: string ): { [key: string]: ExportCondition | string } { - const entryNames = getEntries(cwd, /*hasSrc=*/ false) + const outDirs = [ + ...new Set( + Object.values(exportsConditions).flatMap((value) => + (typeof value === 'string' ? [value] : Object.values(value)).flatMap( + (innerValue) => (typeof innerValue === 'string' ? [innerValue] : []) + ) + ) + ), + ] + .map((value) => value.split('/')[1]) + .filter(Boolean) + + const entryNames = getEntries(cwd, false, outDirs) const exportPaths = Object.keys(exportsConditions) @@ -269,7 +286,6 @@ export function getExportPaths(pkg: PackageMetadata, cwd: string) { exportsConditions, cwd ) - console.log(validatedExportsConditions, 'validatedExportsConditions') } const paths = parseExport(validatedExportsConditions, packageType) Object.assign(pathsMap, paths) From c36788d86115abb80eb3d9bc5e1fab4864db8be8 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 10 Jul 2023 12:48:43 +0900 Subject: [PATCH 05/16] chore: add jsdocs for explanation --- src/exports.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index 3144ff9d..983d5d71 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -85,6 +85,14 @@ function findExport( }) } +/** + * Get the entries (files and directories) within a given path. + * + * @param {string} path - The base path to search for entries. + * @param {boolean} hasSrc - Indicates whether the path has a 'src' directory. + * @param {string[]} outDirs - The list of output directories to consider. + * @returns {string[]} The array of entry names. + */ function getEntries( path: string, hasSrc: boolean, @@ -118,6 +126,13 @@ function getEntries( }) } +/** + * Replace wildcards in an ExportCondition object or string with the provided directory name. + * + * @param {ExportCondition | string} obj - The ExportCondition object or string to process. + * @param {string} dirName - The directory name to replace wildcards with. + * @returns {ExportCondition} The modified ExportCondition object or string. + */ function replaceWildcardsWithEntryName( obj: ExportCondition, dirName: string @@ -130,18 +145,20 @@ function replaceWildcardsWithEntryName( for (let key in obj) { newObj[key] = replaceWildcardsWithEntryName(obj[key], dirName) } - return newObj } else { return obj } } -function resolveWildcardExports( - exportsConditions: ExportCondition, - cwd: string -): { [key: string]: ExportCondition | string } { - const outDirs = [ +/** + * Get the output directories from the given ExportCondition object. + * + * @param {ExportCondition} exportsConditions - The ExportCondition object to process. + * @returns {string[]} The array of output directories. + */ +function getOutDirs(exportsConditions: ExportCondition) { + return [ ...new Set( Object.values(exportsConditions).flatMap((value) => (typeof value === 'string' ? [value] : Object.values(value)).flatMap( @@ -152,6 +169,49 @@ function resolveWildcardExports( ] .map((value) => value.split('/')[1]) .filter(Boolean) +} + +/** + * Resolve wildcard exports in the given ExportCondition object. + * + * @param {ExportCondition} exportsConditions - The ExportCondition object to process. + * @param {string} cwd - The current working directory. + * @returns {Object.} The resolved ExportCondition object with wildcard exports replaced to correct path. + * @example + * Input: + * { + * ".": { + * "types": "./dist/index.d.ts", + * "import": "./dist/index.js" + * }, + * "./*": { + * "types": "./dist/*.d.ts", + * "import": "./dist/*.js" + * } + * } + * + * Output: + * { + * ".": { + * "types": "./dist/index.d.ts", + * "import": "./dist/index.js" + * }, + * "./button": { + * "types": "./dist/button.d.ts", + * "import": "./dist/button.js" + * }, + * "./input": { + * "types": "./dist/input.d.ts", + * "import": "./dist/input.js" + * }, + * // ... + * } + */ +function resolveWildcardExports( + exportsConditions: ExportCondition, + cwd: string +): { [key: string]: ExportCondition | string } { + const outDirs = getOutDirs(exportsConditions) const entryNames = getEntries(cwd, false, outDirs) From e11a1ad048d25ec1fe02c33c148a0498721ce045 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 11 Jul 2023 10:22:57 +0900 Subject: [PATCH 06/16] chore: modify imports based on moved paths --- src/exports.ts | 7 ++----- test/lib-unit/exports.test.ts | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index 31afb2af..80bcc6ff 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -7,11 +7,8 @@ import type { PackageType, ParsedExportCondition, } from './types' -import { - availableExtensions, - availableExportConventions, - filenameWithoutExtension, -} from './utils' +import { filenameWithoutExtension } from './utils' +import { availableExtensions, availableExportConventions } from './constants' export function getTypings(pkg: PackageMetadata) { return pkg.types || pkg.typings diff --git a/test/lib-unit/exports.test.ts b/test/lib-unit/exports.test.ts index a2d5774f..0260f7d4 100644 --- a/test/lib-unit/exports.test.ts +++ b/test/lib-unit/exports.test.ts @@ -35,8 +35,8 @@ describe('lib exports', () => { }, }, }, - cwd - ) + cwd, + ), ).toEqual({ '.': { import: './dist/index.mjs', @@ -66,8 +66,8 @@ describe('lib exports', () => { }, }, }, - path.join(__dirname, '../integration/wildcard-exports') - ) + path.join(__dirname, '../integration/wildcard-exports'), + ), ).toEqual({ '.': { types: './dist/index.d.ts', @@ -105,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', @@ -127,8 +127,8 @@ describe('lib exports', () => { }, }, }, - cwd - ) + cwd, + ), ).toEqual({ '.': { require: './dist/index.cjs', @@ -147,8 +147,8 @@ describe('lib exports', () => { }, }, }, - cwd - ) + cwd, + ), ).toEqual({ '.': { import: './dist/index.mjs', @@ -170,8 +170,8 @@ describe('lib exports', () => { }, }, }, - cwd - ) + cwd, + ), ).toEqual({ '.': { require: './dist/index.cjs', @@ -194,8 +194,8 @@ describe('lib exports', () => { require: './dist/index.js', }, }, - cwd - ) + cwd, + ), ).toEqual({ '.': { types: './dist/index.d.ts', @@ -272,7 +272,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, From 70570312ed2bb2bdf64efedfc8f57cb059889f1f Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 11 Jul 2023 11:49:15 +0900 Subject: [PATCH 07/16] fix if case for outDirs --- src/exports.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index 80bcc6ff..6c38553d 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -105,8 +105,8 @@ function getEntries( ].map((ext) => `.${ext}`) return entries.flatMap((entry) => { - if (entry.isDirectory()) { - if (entry.name === 'src' || outDirs.includes(entry.name)) { + if (entry.isDirectory() && !outDirs.includes(entry.name)) { + if (entry.name === 'src') { return getEntries(entryPath, true, outDirs) } return entry.name + '/index' From c853b9bff1fe954dd28846b29aafc6033661ee70 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 17 Jul 2023 02:44:12 +0900 Subject: [PATCH 08/16] refac: remove unnecessary codes - added objra, custom module by @devjiwonchoi --- package.json | 1 + pnpm-lock.yaml | 15 ++-- src/exports.ts | 189 +++++++++++++------------------------------------ 3 files changed, 63 insertions(+), 142 deletions(-) diff --git a/package.json b/package.json index 3eaa3213..e9ab441b 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@swc/core": "^1.3.68", "@swc/helpers": "^0.5.0", "arg": "~5.0.2", + "objra": "^0.0.0", "pretty-bytes": "~5.6.0", "publint": "~0.1.11", "rollup": "~3.20.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 427c45a9..022df3e1 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 @@ -29,6 +25,9 @@ dependencies: arg: specifier: ~5.0.2 version: 5.0.2 + objra: + specifier: ^0.0.0 + version: 0.0.0 pretty-bytes: specifier: ~5.6.0 version: 5.6.0 @@ -2819,6 +2818,10 @@ packages: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true + /objra@0.0.0: + resolution: {integrity: sha512-IzXbSJTkdzhGZPpQj3gfG2KXUkJyTsF9dViDi5dJOwU24MnJHksjeEx6ax9RPQ6jbIHDvaj451EIulqDr4dgLw==} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -3440,3 +3443,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false diff --git a/src/exports.ts b/src/exports.ts index 6c38553d..ca37ad0c 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,5 +1,6 @@ import fs from 'fs' import { join, resolve, dirname, extname } from 'path' +import objectReplaceAll from 'objra' import type { PackageMetadata, ExportCondition, @@ -82,162 +83,79 @@ function findExport( }) } -/** - * Get the entries (files and directories) within a given path. - * - * @param {string} path - The base path to search for entries. - * @param {boolean} hasSrc - Indicates whether the path has a 'src' directory. - * @param {string[]} outDirs - The list of output directories to consider. - * @returns {string[]} The array of entry names. - */ -function getEntries( - path: string, - hasSrc: boolean, - outDirs: string[] -): string[] { - const entryPath = hasSrc ? join(path, 'src') : path - - const entries = fs.readdirSync(entryPath, { withFileTypes: true }) - - const availableExt = [ +function getEntries(entryPath: string, excludes: string[]) { + const allowedExtensions = [ ...availableExtensions, ...availableExportConventions, ].map((ext) => `.${ext}`) - return entries.flatMap((entry) => { - if (entry.isDirectory() && !outDirs.includes(entry.name)) { - if (entry.name === 'src') { - return getEntries(entryPath, true, outDirs) - } - return entry.name + '/index' + const excludesArray = excludes.map((exclude) => exclude.replace('./', '')) + + const dirents = fs.readdirSync(entryPath, { withFileTypes: true }) + + const entries: string[] = dirents.flatMap((dirent) => { + if (excludesArray.includes(dirent.name)) { + return '' } - if ( - entry.isFile() && - availableExt.includes(extname(entry.name)) && - !entry.name.includes('index') - ) { - return filenameWithoutExtension(entry.name)! + if (dirent.isDirectory() && dirent.name === 'src') { + return getEntries(`${entryPath}/src`, excludesArray) } - return [] - }) -} -/** - * Replace wildcards in an ExportCondition object or string with the provided directory name. - * - * @param {ExportCondition | string} obj - The ExportCondition object or string to process. - * @param {string} dirName - The directory name to replace wildcards with. - * @returns {ExportCondition} The modified ExportCondition object or string. - */ -function replaceWildcardsWithEntryName( - obj: ExportCondition, - dirName: string -): ExportCondition { - if (typeof obj === 'string') { - return obj.replace('*', dirName) - } else if (typeof obj === 'object' && obj !== null) { - const newObj: { [key: string]: ExportCondition | string } = {} - - for (let key in obj) { - newObj[key] = replaceWildcardsWithEntryName(obj[key], dirName) + if (allowedExtensions.includes(extname(dirent.name))) { + return filenameWithoutExtension(dirent.name)! } - return newObj - } else { - return obj - } + + if (dirent.isDirectory()) { + return `${dirent.name}/index` + } + + return '' + }) + + return entries.filter(Boolean) } -/** - * Get the output directories from the given ExportCondition object. - * - * @param {ExportCondition} exportsConditions - The ExportCondition object to process. - * @returns {string[]} The array of output directories. - */ function getOutDirs(exportsConditions: ExportCondition) { return [ ...new Set( Object.values(exportsConditions).flatMap((value) => (typeof value === 'string' ? [value] : Object.values(value)).flatMap( - (innerValue) => (typeof innerValue === 'string' ? [innerValue] : []) - ) - ) + (innerValue) => (typeof innerValue === 'string' ? [innerValue] : []), + ), + ), ), ] .map((value) => value.split('/')[1]) .filter(Boolean) } -/** - * Resolve wildcard exports in the given ExportCondition object. - * - * @param {ExportCondition} exportsConditions - The ExportCondition object to process. - * @param {string} cwd - The current working directory. - * @returns {Object.} The resolved ExportCondition object with wildcard exports replaced to correct path. - * @example - * Input: - * { - * ".": { - * "types": "./dist/index.d.ts", - * "import": "./dist/index.js" - * }, - * "./*": { - * "types": "./dist/*.d.ts", - * "import": "./dist/*.js" - * } - * } - * - * Output: - * { - * ".": { - * "types": "./dist/index.d.ts", - * "import": "./dist/index.js" - * }, - * "./button": { - * "types": "./dist/button.d.ts", - * "import": "./dist/button.js" - * }, - * "./input": { - * "types": "./dist/input.d.ts", - * "import": "./dist/input.js" - * }, - * // ... - * } - */ -function resolveWildcardExports( - exportsConditions: ExportCondition, - cwd: string -): { [key: string]: ExportCondition | string } { +function resolveWildcardExports(exportsConditions: any, cwd: string) { + const exportConditionsKeys = Object.keys(exportsConditions) + const wildcardEntry = Object.entries(exportsConditions).filter(([key]) => + key.includes('*'), + ) + const wildcardEntryKey = wildcardEntry.map(([key]) => key)[0] const outDirs = getOutDirs(exportsConditions) + const excludes = [...outDirs, ...exportConditionsKeys] + const entries = getEntries(cwd, excludes) - const entryNames = getEntries(cwd, false, outDirs) - - const exportPaths = Object.keys(exportsConditions) + const replaced = entries.map((entry) => { + return objectReplaceAll('*', entry!, Object.fromEntries(wildcardEntry)) + }) - const newExportsConditions: { [key: string]: ExportCondition | string } = {} + delete exportsConditions[wildcardEntryKey] - for (let [exportPath, exportValue] of Object.entries(exportsConditions)) { - if (exportPath.includes('*')) { - entryNames.forEach((entryName) => { - const newExportPath = exportPath - .replace('*', entryName) - .replace('/index', '') + const updatedExports = Object.assign({}, exportsConditions, ...replaced) - if (exportPaths.some((path) => path.startsWith(newExportPath))) { - return - } - const newExportValue = replaceWildcardsWithEntryName( - exportValue, - entryName - ) - newExportsConditions[newExportPath] = newExportValue - }) - } else { - newExportsConditions[exportPath] = exportValue + const result = Object.entries(updatedExports).map(([key, value]) => { + if (key.includes('/index')) { + return [key.replace('/index', ''), value] } - } + return [key, value] + }) - return newExportsConditions + return Object.fromEntries(result) } /** @@ -333,20 +251,15 @@ export function getExportPaths(pkg: PackageMetadata, cwd: string) { const packageType = getPackageType(pkg) const { exports: exportsConditions } = pkg - if (exportsConditions) { - let validatedExportsConditions = exportsConditions - const hasWildCardExport = Object.keys(exportsConditions).some((key) => - key.includes('*') - ) + if (exportsConditions) { + let resolvedExportsConditions = exportsConditions - if (hasWildCardExport) { - validatedExportsConditions = resolveWildcardExports( - exportsConditions, - cwd - ) + if (Object.keys(exportsConditions).some((key) => key.includes('*'))) { + resolvedExportsConditions = resolveWildcardExports(exportsConditions, cwd) } - const paths = parseExport(validatedExportsConditions, packageType) + + const paths = parseExport(resolvedExportsConditions, packageType) Object.assign(pathsMap, paths) } From 532c14fa7933993081d86bc3d986f023c5e825e8 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 17 Jul 2023 03:57:43 +0900 Subject: [PATCH 09/16] reface: clearer codes & add comments --- src/exports.ts | 56 +++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index 24587a67..ee5df9f8 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -89,33 +89,37 @@ function getEntries(entryPath: string, excludes: string[]) { ...availableExportConventions, ].map((ext) => `.${ext}`) - const excludesArray = excludes.map((exclude) => exclude.replace('./', '')) - const dirents = fs.readdirSync(entryPath, { withFileTypes: true }) const entries: string[] = dirents.flatMap((dirent) => { - if (excludesArray.includes(dirent.name)) { - return '' - } + // Skip all excludes + if (excludes.includes(dirent.name)) return [] - if (dirent.isDirectory() && dirent.name === 'src') { - return getEntries(`${entryPath}/src`, excludesArray) + // Should include dirs as entires also + if (dirent.isDirectory()) { + // If src dir exists, read inside src + if (dirent.name === 'src') { + return getEntries(`${entryPath}/src`, excludes) + } + // else add it as dir/index + return `${dirent.name}/index` } - if (allowedExtensions.includes(extname(dirent.name))) { + // Only include files with allowed extensions + if (dirent.isFile() && allowedExtensions.includes(extname(dirent.name))) { + // added ! at the end since it will never be undefined return filenameWithoutExtension(dirent.name)! } - if (dirent.isDirectory()) { - return `${dirent.name}/index` - } - - return '' + return [] }) + // Remove all [] return entries.filter(Boolean) } +// Should exclude all outDirs since they are readable dirs on `getEntries` +// Example: { 'import': './someOutDir/index.js' } => 'someOutDir' function getOutDirs(exportsConditions: ExportCondition) { return [ ...new Set( @@ -131,24 +135,30 @@ function getOutDirs(exportsConditions: ExportCondition) { } function resolveWildcardExports(exportsConditions: any, cwd: string) { - const exportConditionsKeys = Object.keys(exportsConditions) - const wildcardEntry = Object.entries(exportsConditions).filter(([key]) => - key.includes('*'), - ) - const wildcardEntryKey = wildcardEntry.map(([key]) => key)[0] const outDirs = getOutDirs(exportsConditions) - const excludes = [...outDirs, ...exportConditionsKeys] + // './dir1/dir2' => ['dir1', 'dir2'] + const exportConditionsKeyFilenames = [ + ...new Set(Object.keys(exportsConditions).flatMap((key) => key.split('/'))), + ] + + const excludes = [...outDirs, ...exportConditionsKeyFilenames] const entries = getEntries(cwd, excludes) - const replaced = entries.map((entry) => { - return objectReplaceAll('*', entry!, Object.fromEntries(wildcardEntry)) + const wildcardEntry = Object.entries(exportsConditions).filter(([key]) => + key.includes('*'), + ) + const resolvedEntry = entries.map((entry) => { + return objectReplaceAll('*', entry, Object.fromEntries(wildcardEntry)) }) + // Remove './*' from exports + const wildcardEntryKey = wildcardEntry.map(([key]) => key)[0] delete exportsConditions[wildcardEntryKey] - const updatedExports = Object.assign({}, exportsConditions, ...replaced) + const resolvedExports = Object.assign({}, exportsConditions, ...resolvedEntry) - const result = Object.entries(updatedExports).map(([key, value]) => { + const result = Object.entries(resolvedExports).map(([key, value]) => { + // Remove '/index' on keys which got from `getEntries` if (key.includes('/index')) { return [key.replace('/index', ''), value] } From e0dbd0927f82210177aa4eb83eddacbca0bc9be8 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 17 Jul 2023 03:59:57 +0900 Subject: [PATCH 10/16] chore: add cwd to added test --- test/lib-unit/exports.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/lib-unit/exports.test.ts b/test/lib-unit/exports.test.ts index cbdbf2ac..ea748234 100644 --- a/test/lib-unit/exports.test.ts +++ b/test/lib-unit/exports.test.ts @@ -210,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', From 8e5d811355ffe82f057297c2bf44d7ec775e4695 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Fri, 28 Jul 2023 02:02:01 +0900 Subject: [PATCH 11/16] chore: wildcard should be './*' - removed 'objra' dependency --- package.json | 1 - pnpm-lock.yaml | 7 ------- src/exports.ts | 22 ++++++++++++---------- src/utils.ts | 16 ++++++++++++++++ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index e9ab441b..3eaa3213 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@swc/core": "^1.3.68", "@swc/helpers": "^0.5.0", "arg": "~5.0.2", - "objra": "^0.0.0", "pretty-bytes": "~5.6.0", "publint": "~0.1.11", "rollup": "~3.20.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 022df3e1..f8a32cbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,9 +25,6 @@ dependencies: arg: specifier: ~5.0.2 version: 5.0.2 - objra: - specifier: ^0.0.0 - version: 0.0.0 pretty-bytes: specifier: ~5.6.0 version: 5.6.0 @@ -2818,10 +2815,6 @@ packages: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true - /objra@0.0.0: - resolution: {integrity: sha512-IzXbSJTkdzhGZPpQj3gfG2KXUkJyTsF9dViDi5dJOwU24MnJHksjeEx6ax9RPQ6jbIHDvaj451EIulqDr4dgLw==} - dev: false - /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: diff --git a/src/exports.ts b/src/exports.ts index ee5df9f8..51f2102e 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,6 +1,5 @@ import fs from 'fs' import { join, resolve, dirname, extname } from 'path' -import objectReplaceAll from 'objra' import type { PackageMetadata, ExportCondition, @@ -8,7 +7,7 @@ import type { PackageType, ParsedExportCondition, } from './types' -import { filenameWithoutExtension } from './utils' +import { filenameWithoutExtension, objectReplaceAll } from './utils' import { availableExtensions, availableExportConventions } from './constants' export function getTypings(pkg: PackageMetadata) { @@ -95,6 +94,9 @@ function getEntries(entryPath: string, excludes: string[]) { // Skip all excludes if (excludes.includes(dirent.name)) return [] + // Skip all index files + if (dirent.name.includes('index')) return [] + // Should include dirs as entires also if (dirent.isDirectory()) { // If src dir exists, read inside src @@ -107,8 +109,7 @@ function getEntries(entryPath: string, excludes: string[]) { // Only include files with allowed extensions if (dirent.isFile() && allowedExtensions.includes(extname(dirent.name))) { - // added ! at the end since it will never be undefined - return filenameWithoutExtension(dirent.name)! + return filenameWithoutExtension(dirent.name) ?? [] } return [] @@ -134,7 +135,10 @@ function getOutDirs(exportsConditions: ExportCondition) { .filter(Boolean) } -function resolveWildcardExports(exportsConditions: any, cwd: string) { +function resolveWildcardExports( + exportsConditions: ExportCondition, + cwd: string, +) { const outDirs = getOutDirs(exportsConditions) // './dir1/dir2' => ['dir1', 'dir2'] const exportConditionsKeyFilenames = [ @@ -145,17 +149,15 @@ function resolveWildcardExports(exportsConditions: any, cwd: string) { const entries = getEntries(cwd, excludes) const wildcardEntry = Object.entries(exportsConditions).filter(([key]) => - key.includes('*'), + key.includes('./*'), ) + const resolvedEntry = entries.map((entry) => { return objectReplaceAll('*', entry, Object.fromEntries(wildcardEntry)) }) - // Remove './*' from exports - const wildcardEntryKey = wildcardEntry.map(([key]) => key)[0] - delete exportsConditions[wildcardEntryKey] - const resolvedExports = Object.assign({}, exportsConditions, ...resolvedEntry) + delete resolvedExports['./*'] const result = Object.entries(resolvedExports).map(([key, value]) => { // Remove '/index' on keys which got from `getEntries` diff --git a/src/utils.ts b/src/utils.ts index 2a152e8b..566c8cd5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -143,3 +143,19 @@ export function filenameWithoutExtension(file: string | undefined) { } export const nonNullable = (n?: T): n is T => Boolean(n) + +export function objectReplaceAll( + regex: string | RegExp, + str: string, + obj: object, +): object { + const regexp = typeof regex === 'string' ? regex : new RegExp(regex, 'g') + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => { + if (typeof key === 'string') key = key.replace(regexp, str) + if (value === null || value === undefined) value = str + if (typeof value === 'string') return [key, value.replace(regexp, str)] + return [key, objectReplaceAll(regexp, str, value)] + }), + ) +} From 8f5e15a35fa2dbfe2d85e2e6eaf4b594622b3f82 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Fri, 28 Jul 2023 02:04:55 +0900 Subject: [PATCH 12/16] chore: check for wildcard './*' --- src/exports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exports.ts b/src/exports.ts index 51f2102e..5131324f 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -268,7 +268,7 @@ export function getExportPaths(pkg: PackageMetadata, cwd: string) { if (exportsConditions) { let resolvedExportsConditions = exportsConditions - if (Object.keys(exportsConditions).some((key) => key.includes('*'))) { + if (Object.keys(exportsConditions).some((key) => key.includes('./*'))) { resolvedExportsConditions = resolveWildcardExports(exportsConditions, cwd) } From b2874818f2e037cc70302688d43e9b78504b4a95 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Sun, 30 Jul 2023 23:35:01 +0900 Subject: [PATCH 13/16] chore: replace objra --- src/exports.ts | 17 +++++++++++++---- src/utils.ts | 16 ---------------- test/integration.test.ts | 20 -------------------- 3 files changed, 13 insertions(+), 40 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index 5131324f..98b0b923 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -7,7 +7,7 @@ import type { PackageType, ParsedExportCondition, } from './types' -import { filenameWithoutExtension, objectReplaceAll } from './utils' +import { filenameWithoutExtension } from './utils' import { availableExtensions, availableExportConventions } from './constants' export function getTypings(pkg: PackageMetadata) { @@ -135,6 +135,17 @@ function getOutDirs(exportsConditions: ExportCondition) { .filter(Boolean) } +function replaceWildcardToEntries( + wildcardEntry: [string, ExportCondition][], + entries: string[], +) { + return entries.map((entry) => + Object.fromEntries( + JSON.parse(JSON.stringify(wildcardEntry).replace(/\*/g, entry)), + ), + ) +} + function resolveWildcardExports( exportsConditions: ExportCondition, cwd: string, @@ -152,9 +163,7 @@ function resolveWildcardExports( key.includes('./*'), ) - const resolvedEntry = entries.map((entry) => { - return objectReplaceAll('*', entry, Object.fromEntries(wildcardEntry)) - }) + const resolvedEntry = replaceWildcardToEntries(wildcardEntry, entries) const resolvedExports = Object.assign({}, exportsConditions, ...resolvedEntry) delete resolvedExports['./*'] diff --git a/src/utils.ts b/src/utils.ts index 566c8cd5..2a152e8b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -143,19 +143,3 @@ export function filenameWithoutExtension(file: string | undefined) { } export const nonNullable = (n?: T): n is T => Boolean(n) - -export function objectReplaceAll( - regex: string | RegExp, - str: string, - obj: object, -): object { - const regexp = typeof regex === 'string' ? regex : new RegExp(regex, 'g') - return Object.fromEntries( - Object.entries(obj).map(([key, value]) => { - if (typeof key === 'string') key = key.replace(regexp, str) - if (value === null || value === undefined) value = str - if (typeof value === 'string') return [key, value.replace(regexp, str)] - return [key, objectReplaceAll(regexp, str, value)] - }), - ) -} diff --git a/test/integration.test.ts b/test/integration.test.ts index c563a9af..53f77c46 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -240,31 +240,11 @@ const testCases: { name: 'wildcard-exports', args: [], async expected(dir, { stdout }) { - const distFiles = [ - './dist/index.js', - './dist/lite.js', - './dist/input.js', - './dist/index.js', - './dist/button.js', - './dist/lite.js', - './dist/layout/index.js', - './dist/server/react-server.mjs', - './dist/server/edge.mjs', - './dist/server/index.mjs', - - // types - './dist/index.d.ts', - './dist/button.d.ts', - './dist/layout/index.d.ts', - './dist/server/index.d.ts', - ] - 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) From 0ba7e7218d1fdae518556033ba4c278326324260 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 31 Jul 2023 15:18:59 +0900 Subject: [PATCH 14/16] chore: simplify iteration --- src/exports.ts | 132 ++++++++++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 56 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index 98b0b923..cee8643a 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -82,43 +82,6 @@ function findExport( }) } -function getEntries(entryPath: string, excludes: string[]) { - const allowedExtensions = [ - ...availableExtensions, - ...availableExportConventions, - ].map((ext) => `.${ext}`) - - const dirents = fs.readdirSync(entryPath, { withFileTypes: true }) - - const entries: string[] = dirents.flatMap((dirent) => { - // Skip all excludes - if (excludes.includes(dirent.name)) return [] - - // Skip all index files - if (dirent.name.includes('index')) return [] - - // Should include dirs as entires also - if (dirent.isDirectory()) { - // If src dir exists, read inside src - if (dirent.name === 'src') { - return getEntries(`${entryPath}/src`, excludes) - } - // else add it as dir/index - return `${dirent.name}/index` - } - - // Only include files with allowed extensions - if (dirent.isFile() && allowedExtensions.includes(extname(dirent.name))) { - return filenameWithoutExtension(dirent.name) ?? [] - } - - return [] - }) - - // Remove all [] - return entries.filter(Boolean) -} - // Should exclude all outDirs since they are readable dirs on `getEntries` // Example: { 'import': './someOutDir/index.js' } => 'someOutDir' function getOutDirs(exportsConditions: ExportCondition) { @@ -135,19 +98,66 @@ function getOutDirs(exportsConditions: ExportCondition) { .filter(Boolean) } -function replaceWildcardToEntries( - wildcardEntry: [string, ExportCondition][], - entries: string[], -) { - return entries.map((entry) => - Object.fromEntries( - JSON.parse(JSON.stringify(wildcardEntry).replace(/\*/g, entry)), - ), - ) +function resolveWildcardEntry( + wildcardEntry: { + [key: string]: ExportCondition + }, + cwd: string, + excludes: string[], +): { + [key: string]: ExportCondition +}[] { + const allowedExtensions = [ + ...availableExtensions, + ...availableExportConventions, + ].map((ext) => `.${ext}`) + + const dirents = fs.readdirSync(cwd, { withFileTypes: true }) + + const resolvedExports = dirents.flatMap((dirent) => { + if (excludes.includes(dirent.name)) return + + if (dirent.isDirectory()) { + if (dirent.name === 'src') { + return resolveWildcardEntry(wildcardEntry, `${cwd}/src`, excludes) + } + + const dirName = dirent.name + const possibleIndexFile = fs.readdirSync(`${cwd}/${dirName}`) + + if (possibleIndexFile[0].startsWith('index')) { + return { + [`./${dirName}`]: JSON.parse( + JSON.stringify(wildcardEntry).replace(/\*/g, `${dirName}/index`), + ), + } + } + } + + if (dirent.isFile()) { + const fileName = filenameWithoutExtension(dirent.name)! + 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) as { + [key: string]: ExportCondition + }[] } function resolveWildcardExports( - exportsConditions: ExportCondition, + exportsConditions: { + [key: string]: ExportCondition + }, cwd: string, ) { const outDirs = getOutDirs(exportsConditions) @@ -157,15 +167,22 @@ function resolveWildcardExports( ] const excludes = [...outDirs, ...exportConditionsKeyFilenames] - const entries = getEntries(cwd, excludes) - const wildcardEntry = Object.entries(exportsConditions).filter(([key]) => - key.includes('./*'), - ) + const wildcardEntry = exportsConditions['./*'] - const resolvedEntry = replaceWildcardToEntries(wildcardEntry, entries) + const resolvedWildcardEntry = resolveWildcardEntry( + wildcardEntry as { + [key: string]: ExportCondition + }, + cwd, + excludes, + ) - const resolvedExports = Object.assign({}, exportsConditions, ...resolvedEntry) + const resolvedExports = Object.assign( + {}, + exportsConditions, + ...resolvedWildcardEntry, + ) delete resolvedExports['./*'] const result = Object.entries(resolvedExports).map(([key, value]) => { @@ -271,13 +288,16 @@ export function getExportPaths(pkg: PackageMetadata, cwd: string) { const pathsMap: Record = {} const packageType = getPackageType(pkg) const isCjsPackage = packageType === 'commonjs' - + const { exports: exportsConditions } = pkg if (exportsConditions) { let resolvedExportsConditions = exportsConditions - if (Object.keys(exportsConditions).some((key) => key.includes('./*'))) { + if ( + Object.keys(exportsConditions).some((key) => key === './*') && + typeof exportsConditions !== 'string' + ) { resolvedExportsConditions = resolveWildcardExports(exportsConditions, cwd) } @@ -294,7 +314,7 @@ export function getExportPaths(pkg: PackageMetadata, cwd: string) { }, packageType, ) - + if (isCjsPackage && pathsMap['.']?.['require']) { // pathsMap's exports.require are prioritized. defaultMainExport['require'] = pathsMap['.']['require'] From 0f290c2ed7137c509f3ec4f5b7442597290abc3c Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 31 Jul 2023 15:38:19 +0900 Subject: [PATCH 15/16] refac: reduce unnecessary assignments - added comments --- src/exports.ts | 51 ++++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index cee8643a..1bd2d567 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -107,25 +107,29 @@ function resolveWildcardEntry( ): { [key: string]: ExportCondition }[] { + const dirents = fs.readdirSync(cwd, { withFileTypes: true }) + const allowedExtensions = [ ...availableExtensions, ...availableExportConventions, ].map((ext) => `.${ext}`) - const dirents = fs.readdirSync(cwd, { withFileTypes: true }) - 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 possibleIndexFile = fs.readdirSync(`${cwd}/${dirName}`) + const hasIndexFile = fs + .readdirSync(`${cwd}/${dirName}`) + .some((file) => file.startsWith('index')) - if (possibleIndexFile[0].startsWith('index')) { + if (hasIndexFile) { return { [`./${dirName}`]: JSON.parse( JSON.stringify(wildcardEntry).replace(/\*/g, `${dirName}/index`), @@ -136,6 +140,7 @@ function resolveWildcardEntry( 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 { @@ -149,9 +154,7 @@ function resolveWildcardEntry( return }) - return resolvedExports.filter(Boolean) as { - [key: string]: ExportCondition - }[] + return resolvedExports.filter(Boolean) } function resolveWildcardExports( @@ -161,39 +164,21 @@ function resolveWildcardExports( cwd: string, ) { const outDirs = getOutDirs(exportsConditions) - // './dir1/dir2' => ['dir1', 'dir2'] - const exportConditionsKeyFilenames = [ + const existingKeys = [ ...new Set(Object.keys(exportsConditions).flatMap((key) => key.split('/'))), ] + const excludes = [...outDirs, ...existingKeys] - const excludes = [...outDirs, ...exportConditionsKeyFilenames] - - const wildcardEntry = exportsConditions['./*'] + const wildcardEntry = exportsConditions['./*'] as { + [key: string]: ExportCondition + } - const resolvedWildcardEntry = resolveWildcardEntry( - wildcardEntry as { - [key: string]: ExportCondition - }, - cwd, - excludes, - ) + const resolvedEntry = resolveWildcardEntry(wildcardEntry, cwd, excludes) - const resolvedExports = Object.assign( - {}, - exportsConditions, - ...resolvedWildcardEntry, - ) + const resolvedExports = Object.assign({}, exportsConditions, ...resolvedEntry) delete resolvedExports['./*'] - const result = Object.entries(resolvedExports).map(([key, value]) => { - // Remove '/index' on keys which got from `getEntries` - if (key.includes('/index')) { - return [key.replace('/index', ''), value] - } - return [key, value] - }) - - return Object.fromEntries(result) + return resolvedExports } /** From ae2e8e016c7ab3c7ed6fcffb2046dc05d96d81e4 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 31 Jul 2023 16:27:36 +0900 Subject: [PATCH 16/16] chore: add description to getOutDirs --- src/exports.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index 1bd2d567..5c928cd5 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -82,16 +82,18 @@ function findExport( }) } -// Should exclude all outDirs since they are readable dirs on `getEntries` -// Example: { 'import': './someOutDir/index.js' } => 'someOutDir' +// 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) => - (typeof value === 'string' ? [value] : Object.values(value)).flatMap( - (innerValue) => (typeof innerValue === 'string' ? [innerValue] : []), + Object.values(exportsConditions) + .flatMap((value) => Object.values(value)) + .flatMap((innerValue) => + typeof innerValue === 'string' ? [innerValue] : [], ), - ), ), ] .map((value) => value.split('/')[1])