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

refactor: remove export handling from getExportPaths for wildcard matching #244

Merged
merged 20 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
4 changes: 3 additions & 1 deletion src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import type { BuildMetadata } from './types'
import { TypescriptOptions, resolveTsConfig } from './typescript'
import { logSizeStats } from './logging'
import { resolveWildcardExports } from './experimental/wildcard'

function assignDefault(
options: BundleConfig,
Expand Down Expand Up @@ -58,9 +59,10 @@ async function bundle(
assignDefault(options, 'target', 'es2015')

const pkg = await getPackageMeta(cwd)
const resolvedWildcardExports = await resolveWildcardExports(pkg.exports, cwd)
const packageType = getPackageType(pkg)
const exportPaths = getExportPaths(pkg, cwd)

const exportPaths = getExportPaths(pkg, packageType, resolvedWildcardExports)
const exportKeys = Object.keys(exportPaths).filter(
(key) => key !== './package.json',
)
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export const availableExportConventions = [
]
export const availableESExtensionsRegex = /\.(m|c)?[jt]sx?$/
export const dtsExtensionRegex = /\.d\.(m|c)?ts$/

export const SRC = 'src'
114 changes: 114 additions & 0 deletions src/experimental/wildcard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import fs from 'fs/promises'
import type { Dirent } from 'fs'
import path from 'path'
import { SRC } from '../constants'
import { ExportCondition } from '../types'
import {
filenameWithoutExtension,
hasAvailableExtension,
nonNullable,
} from '../utils'

// TODO: support nested wildcard exportsCondition (e.g. './foo/*')
const getWildcardExports = (
exportsCondition: Record<string, ExportCondition>,
): Record<string, ExportCondition> => {
return { './*': exportsCondition['./*'] }
}

const isExportable = async (
dirent: Dirent,
pathname: string,
): Promise<boolean> => {
if (dirent.isDirectory()) {
const innerDirents = await fs.readdir(path.join(pathname, dirent.name), {
withFileTypes: true,
})
return innerDirents.some(
({ name }) => name.startsWith('index') && hasAvailableExtension(name),
)
}
return (
dirent.isFile() &&
!dirent.name.startsWith('index') &&
hasAvailableExtension(dirent.name)
)
}

async function getExportables(
cwd: string,
excludeKeys: string[],
): Promise<string[]> {
const pathname = path.resolve(cwd, SRC)
const dirents = await fs.readdir(pathname, { withFileTypes: true })
const exportables: (string | undefined)[] = await Promise.all(
dirents.map(async (dirent) =>
(await isExportable(dirent, pathname)) &&
!excludeKeys.includes(dirent.name)
? dirent.name
: undefined,
),
)
return exportables.filter(nonNullable)
}

function mapWildcard(
wildcardExports: Record<string, ExportCondition>,
exportables: string[],
): ExportCondition[] {
return exportables.map((exportable) => {
const filename = exportable.includes('.')
? filenameWithoutExtension(exportable)
: undefined

if (!filename) {
return {
[`./${exportable}`]: JSON.parse(
JSON.stringify(wildcardExports['./*']).replace(
/\*/g,
`${exportable}/index`,
),
),
}
}
return JSON.parse(JSON.stringify(wildcardExports).replace(/\*/g, filename))
})
}

export async function resolveWildcardExports(
exportsCondition: string | Record<string, ExportCondition> | undefined,
cwd: string,
): Promise<ExportCondition | undefined> {
if (!exportsCondition || typeof exportsCondition === 'string')
return undefined

const hasWildcard = !!exportsCondition['./*']

if (hasWildcard) {
console.warn(
`The wildcard export "./*" is experimental and may change or be removed at any time.\n` +
'To open an issue, please visit https://github.com/huozhi/bunchee/issues' +
'.\n',
)

// './foo' -> ['foo']; './foo/bar' -> ['bar']
const excludeKeys = Object.keys(exportsCondition)
.filter((key) => !['.', './*'].includes(key))
.flatMap((key) => key.split('/').pop() ?? '')
const exportables = await getExportables(cwd, excludeKeys)

if (exportables.length > 0) {
const wildcardExports = getWildcardExports(exportsCondition)
const resolvedWildcardExports = mapWildcard(wildcardExports, exportables)
const resolvedExports = Object.assign(
exportsCondition,
huozhi marked this conversation as resolved.
Show resolved Hide resolved
...resolvedWildcardExports,
)

delete resolvedExports['./*']
return resolvedExports
}
}

return undefined
}
124 changes: 8 additions & 116 deletions src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import fs from 'fs'
import { join, resolve, dirname, extname } from 'path'
import type {
PackageMetadata,
Expand All @@ -8,7 +7,6 @@ import type {
ParsedExportCondition,
} from './types'
import { filenameWithoutExtension } from './utils'
import { availableExtensions, availableExportConventions } from './constants'

export function getTypings(pkg: PackageMetadata) {
return pkg.types || pkg.typings
Expand Down Expand Up @@ -82,107 +80,6 @@ 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 @@ -271,24 +168,19 @@ function parseExport(
* pkg.main and pkg.module will be added to ['.'] if exists
*/

export function getExportPaths(pkg: PackageMetadata, cwd: string) {
export function getExportPaths(
pkg: PackageMetadata,
pkgType?: PackageType,
resolvedWildcardExports?: ExportCondition,
) {
const pathsMap: Record<string, FullExportCondition> = {}
const packageType = getPackageType(pkg)
const packageType = pkgType ?? getPackageType(pkg)
const isCjsPackage = packageType === 'commonjs'

const { exports: exportsConditions } = pkg
const exportsConditions = resolvedWildcardExports ?? pkg.exports

if (exportsConditions) {
let resolvedExportsConditions = exportsConditions

if (
Object.keys(exportsConditions).some((key) => key === './*') &&
typeof exportsConditions !== 'string'
) {
resolvedExportsConditions = resolveWildcardExports(exportsConditions, cwd)
}

const paths = parseExport(resolvedExportsConditions, packageType)
const paths = parseExport(exportsConditions, packageType)
Object.assign(pathsMap, paths)
}

Expand Down
15 changes: 12 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import fs from 'fs/promises'
import path from 'path'
import type { PackageMetadata } from './types'
import { availableExportConventions, availableExtensions } from './constants'
import { PackageMetadata } from './types'
import {
availableExportConventions,
availableExtensions,
SRC,
} from './constants'

export function exit(err: string | Error) {
logger.error(err)
Expand Down Expand Up @@ -72,7 +76,6 @@ export function getExportPath(

export const isNotNull = <T>(n: T | false): n is T => Boolean(n)

const SRC = 'src' // resolve from src/ directory
export function resolveSourceFile(cwd: string, filename: string) {
return path.resolve(cwd, SRC, filename)
}
Expand Down Expand Up @@ -143,3 +146,9 @@ export function filenameWithoutExtension(file: string | undefined) {
}

export const nonNullable = <T>(n?: T): n is T => Boolean(n)

export const fileExtension = (file: string | undefined) =>
file ? path.extname(file).slice(1) : undefined

export const hasAvailableExtension = (filename: string): boolean =>
availableExtensions.includes(path.extname(filename).slice(1))
3 changes: 2 additions & 1 deletion test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ const testCases: {
{
name: 'wildcard-exports',
args: [],
async expected(dir, { stdout }) {
async expected(dir, { stdout, stderr }) {
const contentsRegex = {
'./dist/index.js': /'index'/,
'./dist/layout/index.js': /'layout'/,
Expand Down Expand Up @@ -270,6 +270,7 @@ const testCases: {
log.split('\n').forEach((line: string) => {
expect(rawStdout).toContain(line.trim())
})
expect(stderr).toContain('is experimental')
},
},
]
Expand Down
Loading