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 6 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 { validateExports } 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)
pkg.exports &&= await validateExports(pkg.exports, cwd)
devjiwonchoi marked this conversation as resolved.
Show resolved Hide resolved
const packageType = getPackageType(pkg)
const exportPaths = getExportPaths(pkg, cwd)

const exportPaths = getExportPaths(pkg)
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'
devjiwonchoi marked this conversation as resolved.
Show resolved Hide resolved
103 changes: 103 additions & 0 deletions src/experimental/wildcard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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'

const getWildcardEntry = (
key: string | undefined,
exports: Record<string, ExportCondition>,
): Record<string, ExportCondition> | undefined => {
if (!key || !key.includes('./*') || !exports[key]) return undefined
return { [key]: exports[key] }
}

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): 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)) ? dirent.name : undefined,
),
)
return exportables.filter(nonNullable)
}

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

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

export async function validateExports(
devjiwonchoi marked this conversation as resolved.
Show resolved Hide resolved
exports: ExportCondition | Record<string, ExportCondition>,
cwd: string,
): Promise<ExportCondition> {
const wildcardKey = Object.keys(exports).find((key) => key.includes('./*'))
if (typeof exports === 'string' || !wildcardKey) return exports

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',
)

const wildcardEntry = getWildcardEntry(wildcardKey, exports)
if (!wildcardEntry) return exports

const exportables = await getExportables(cwd)
const resolvedWildcardExports = mapWildcard(wildcardEntry, exportables)

const resolvedExports = Object.assign(
{},
exports,
...resolvedWildcardExports,
exports,
)
delete resolvedExports['./*']
return resolvedExports
}
116 changes: 2 additions & 114 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,15 @@ 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) {
const pathsMap: Record<string, FullExportCondition> = {}
const packageType = getPackageType(pkg)
const isCjsPackage = packageType === 'commonjs'

const { exports: exportsConditions } = pkg

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))
Loading