Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add wildcard exports #228

Merged
merged 19 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ dist
*.log
.yalc
yalc.lock
test/**/*.js.map
test/**/*.js.map
.DS_Store
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
124 changes: 118 additions & 6 deletions src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import fs from 'fs'
import { join, resolve, dirname, extname } from 'path'
import type {
PackageMetadata,
ExportCondition,
FullExportCondition,
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string, FullExportCondition> = {}
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)
}

Expand All @@ -189,7 +301,7 @@ export function getExportPaths(pkg: PackageMetadata) {
},
packageType,
)

if (isCjsPackage && pathsMap['.']?.['require']) {
// pathsMap's exports.require are prioritized.
defaultMainExport['require'] = pathsMap['.']['require']
Expand Down
36 changes: 36 additions & 0 deletions test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions test/integration/wildcard-exports/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
1 change: 1 addition & 0 deletions test/integration/wildcard-exports/src/button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'button'
1 change: 1 addition & 0 deletions test/integration/wildcard-exports/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'index'
1 change: 1 addition & 0 deletions test/integration/wildcard-exports/src/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'input'
1 change: 1 addition & 0 deletions test/integration/wildcard-exports/src/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'layout'
3 changes: 3 additions & 0 deletions test/integration/wildcard-exports/src/lite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function lite(c: string) {
return 'lite' + c
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const name = 'server.edge-light'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const name = 'server.react-server'
2 changes: 2 additions & 0 deletions test/integration/wildcard-exports/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const name = 'server.index'
export const main = true
5 changes: 5 additions & 0 deletions test/integration/wildcard-exports/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"esModuleInterop": true
}
}
Loading