Skip to content

Commit

Permalink
Conventional entry for special platform exports (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi authored Jun 18, 2023
1 parent 64bf415 commit dab2999
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 66 deletions.
93 changes: 65 additions & 28 deletions src/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
BundleConfig,
ParsedExportCondition,
ExportPaths,
FullExportCondition,
} from './types'
import type { JsMinifyOptions } from '@swc/core'
import type { InputOptions, OutputOptions, Plugin } from 'rollup'
Expand All @@ -30,6 +31,8 @@ import {
getSourcePathFromExportPath,
resolveSourceFile,
filenameWithoutExtension,
nonNullable,
availableExportConventions,
} from './utils'

const minifyOptions: JsMinifyOptions = {
Expand Down Expand Up @@ -196,8 +199,9 @@ function hasEsmExport(
let hasEsm = false
for (const key in exportPaths) {
const exportInfo = exportPaths[key]
const exportNames = Object.keys(exportInfo)
if (exportNames.some((name) => isEsmExportName(name))) {
const exportInfoEntries = Object.entries(exportInfo)

if (exportInfoEntries.some(([exportType, file]) => isEsmExportName(exportType, file))) {
hasEsm = true
break
}
Expand Down Expand Up @@ -263,40 +267,73 @@ export async function buildEntryConfig(
tsOptions: TypescriptOptions,
dts: boolean
): Promise<BuncheeRollupConfig[]> {
const configs = Object.keys(exportPaths).map(async (entryExport) => {
const configs: Promise<BuncheeRollupConfig | undefined>[] = []
Object.keys(exportPaths).forEach(async (entryExport) => {
// TODO: improve the source detection
const source = entryPath ||
await getSourcePathFromExportPath(
cwd,
entryExport
)
const exportCond = exportPaths[entryExport]
const hasEdgeLight = !!exportCond['edge-light']
const hasReactServer = !!exportCond['react-server']

const buildConfigs = [
createBuildConfig('', exportCond) // default config
]
if (hasEdgeLight) {
buildConfigs.push(createBuildConfig('edge-light', exportCond))
}
if (hasReactServer) {
buildConfigs.push(createBuildConfig('react-server', exportCond))
}

async function createBuildConfig(exportType: string, exportCondRef: FullExportCondition) {
let exportCondForType: FullExportCondition = { ...exportCondRef }
// Special cases of export type, only pass down the exportPaths for the type
if (availableExportConventions.includes(exportType)) {
exportCondForType = {
[entryExport]: exportCondRef[exportType]
}
// Basic export type, pass down the exportPaths with erasing the special ones
} else {
for (const exportType of availableExportConventions) {
delete exportCondForType[exportType]
}

}
const source = entryPath ||
await getSourcePathFromExportPath(
cwd,
entryExport,
exportType,
)

if (!source) return undefined

if (!source) return undefined
// For dts, only build types filed
if (dts && !exportCondRef['types']) return undefined

// For dts, only build types filed
if (dts && !exportPaths[entryExport]['types']) return undefined
const exportCondition: ParsedExportCondition = {
source,
name: entryExport,
export: exportCondForType,
}

const exportCondition: ParsedExportCondition = {
source,
name: entryExport,
export: exportPaths[entryExport],
const entry = resolveSourceFile(cwd!, source)
const rollupConfig = buildConfig(
entry,
pkg,
exportPaths,
bundleConfig,
exportCondition,
cwd,
tsOptions,
dts
)
return rollupConfig
}

const entry = resolveSourceFile(cwd!, source)
const rollupConfig = buildConfig(
entry,
pkg,
exportPaths,
bundleConfig,
exportCondition,
cwd,
tsOptions,
dts
)
return rollupConfig
configs.push(...buildConfigs)
})

return (await Promise.all(configs)).filter(<T>(n?: T): n is T => Boolean(n))
return (await Promise.all(configs)).filter(nonNullable)
}

function buildConfig(
Expand Down
2 changes: 1 addition & 1 deletion src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async function bundle(
// e.g. "exports": "./dist/index.js" -> use "./index.<ext>" as entry
entryPath =
entryPath ||
(await getSourcePathFromExportPath(cwd, '.')) ||
(await getSourcePathFromExportPath(cwd, '.', 'default')) ||
''
}

Expand Down
16 changes: 9 additions & 7 deletions src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PackageMetadata, ExportCondition, FullExportCondition, PackageType, ParsedExportCondition } from './types'
import { join, resolve, dirname } from 'path'
import { join, resolve, dirname, extname } from 'path'
import { filenameWithoutExtension } from './utils'

export function getTypings(pkg: PackageMetadata) {
Expand Down Expand Up @@ -210,12 +210,12 @@ export function constructDefaultExportCondition(
)
}

export function isEsmExportName(name: string) {
return ['import', 'module', 'react-native', 'react-server', 'edge-light'].includes(name)
export function isEsmExportName(name: string, ext: string) {
return ['import', 'module'].includes(name) || ext === 'mjs'
}

export function isCjsExportName(name: string) {
return ['require', 'main', 'node', 'default'].includes(name)
export function isCjsExportName(name: string, ext: string) {
return ['require', 'main', 'node', 'default'].includes(name) || ext === 'cjs'
}

export function getExportConditionDist(
Expand All @@ -231,13 +231,15 @@ export function getExportConditionDist(
if (key === 'types') {
continue
}
const filePath = parsedExportCondition.export[key]
const ext = extname(filePath).slice(1)
const relativePath = parsedExportCondition.export[key]
const distFile = getDistPath(relativePath, cwd)

let format: 'cjs' | 'esm' = 'esm'
if (isEsmExportName(key)) {
if (isEsmExportName(key, ext)) {
format = 'esm'
} else if (isCjsExportName(key)) {
} else if (isCjsExportName(key, ext)) {
format = 'cjs'
}

Expand Down
55 changes: 47 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const logger = {
console.log('\x1b[33m' + arg + '\x1b[0m')
},
error(arg: any) {
console.error('\x1b[31m' + arg + '\x1b[0m')
console.error('\x1b[31m' + (arg instanceof Error ? arg.stack : arg) + '\x1b[0m')
},
}

Expand Down Expand Up @@ -67,20 +67,57 @@ const SRC = 'src' // resolve from src/ directory
export function resolveSourceFile(cwd: string, filename: string) {
return path.resolve(cwd, SRC, filename)
}

async function findSourceEntryFile(
cwd: string,
exportPath: string,
exportTypeSuffix: string | null,
ext: string
): Promise<string | undefined> {
const filename = resolveSourceFile(
cwd,
`${exportPath}${exportTypeSuffix ? `.${exportTypeSuffix}` : ''}.${ext}`
)

if (await fileExists(filename)) {
return filename
}

const subFolderIndexFilename = resolveSourceFile(
cwd,
`${exportPath}/index${exportTypeSuffix ? `.${exportTypeSuffix}` : ''}.${ext}`
)

if (await fileExists(subFolderIndexFilename)) {
return subFolderIndexFilename
}
return undefined
}


// Map '.' -> './index.[ext]'
// Map './lite' -> './lite.[ext]'
// Return undefined if no match or if it's package.json exports
export async function getSourcePathFromExportPath(cwd: string, exportPath: string): Promise<string | undefined> {
const exts = ['js', 'cjs', 'mjs', 'jsx', 'ts', 'tsx']
for (const ext of exts) {
export const availableExtensions = ['js', 'cjs', 'mjs', 'jsx', 'ts', 'tsx']
export const availableExportConventions = ['react-server', 'react-native', 'edge-light']
export async function getSourcePathFromExportPath(
cwd: string,
exportPath: string,
exportType: string
): Promise<string | undefined> {
for (const ext of availableExtensions) {
// ignore package.json
if (exportPath.endsWith('package.json')) return
if (exportPath === '.') exportPath = './index'
const filename = resolveSourceFile(cwd, `${exportPath}.${ext}`)

if (await fileExists(filename)) {
return filename
// Find convention-based source file for specific export types
if (availableExportConventions.includes(exportType)) {
const filename = await findSourceEntryFile(cwd, exportPath, exportType, ext)
if (filename) return filename
}

const filename = await findSourceEntryFile(cwd, exportPath, null, ext)
if (filename) return filename
}
return
}
Expand All @@ -90,4 +127,6 @@ export function filenameWithoutExtension(file: string | undefined) {
return file
? file.replace(new RegExp(`${path.extname(file)}$`), '')
: undefined
}
}

export const nonNullable = <T>(n?: T): n is T => Boolean(n)
58 changes: 40 additions & 18 deletions test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,31 +87,53 @@ const testCases: {
args: [],
async expected(dir, { stdout }) {
const distFiles = [
join(dir, './dist/index.js'),
join(dir, './dist/lite.js'),
join(dir, './dist/client/index.cjs'),
join(dir, './dist/client/index.mjs'),
join(dir, './dist/edge.server/index.mjs'),
join(dir, './dist/edge.server/react-server.mjs'),
'./dist/index.js',
'./dist/lite.js',
'./dist/client/index.cjs',
'./dist/client/index.mjs',
'./dist/shared/index.mjs',
'./dist/shared/edge-light.mjs',
'./dist/server/edge.mjs',
'./dist/server/react-server.mjs',

// types
join(dir, './dist/client/index.d.ts'),
join(dir, './dist/index.d.ts'),
'./dist/client/index.d.ts',
'./dist/index.d.ts',
]

for (const f of distFiles) {
expect({ [f]: await existsFile(f) ? 'existed' : 'missing' }).toMatchObject({ [f]: 'existed' })
const contentsRegex = {
'./dist/index.js': /'index'/,
'./dist/shared/index.mjs': /'shared'/,
'./dist/shared/edge-light.mjs': /'shared.edge-light'/,
'./dist/server/edge.mjs': /'server.edge-light'/,
'./dist/server/react-server.mjs': /'server.react-server'/,
}

for (const relativeFile of distFiles) {
const file = join(dir, relativeFile)
expect({
[file]: await existsFile(file) ? 'existed' : 'missing'
}).toMatchObject({ [file]: 'existed' })
}

for (const [file, regex] of Object.entries(contentsRegex)) {
const content = await fs.readFile(join(dir, file), {
encoding: 'utf-8',
})
expect(content).toMatch(regex)
}

const log = `\
✓ Typed dist/index.d.ts - 65 B
✓ Typed dist/client/index.d.ts - 74 B
✓ Built dist/index.js - 110 B
✓ Built dist/client/index.cjs - 138 B
✓ Built dist/client/index.mjs - 78 B
✓ Built dist/edge.server/index.mjs - 45 B
✓ Built dist/edge.server/react-server.mjs - 45 B
✓ Built dist/lite.js - 132 B
✓ Typed dist/client/index.d.ts - 74 B
✓ Typed dist/index.d.ts - 65 B
✓ Built dist/client/index.cjs - 138 B
✓ Built dist/client/index.mjs - 78 B
✓ Built dist/index.js - 110 B
✓ Built dist/shared/index.mjs - 53 B
✓ Built dist/lite.js - 132 B
✓ Built dist/shared/edge-light.mjs - 84 B
✓ Built dist/server/react-server.mjs - 53 B
✓ Built dist/server/edge.mjs - 51 B
`

const rawStdout = stripANSIColor(stdout)
Expand Down
10 changes: 7 additions & 3 deletions test/integration/multi-entries/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
"import": "./dist/client/index.mjs",
"types": "./dist/client/index.d.ts"
},
"./edge.server": {
"edge-light": "./dist/edge.server/index.mjs",
"react-server": "./dist/edge.server/react-server.mjs"
"./shared": {
"import": "./dist/shared/index.mjs",
"edge-light": "./dist/shared/edge-light.mjs"
},
"./server": {
"edge-light": "./dist/server/edge.mjs",
"react-server": "./dist/server/react-server.mjs"
}
}
}
1 change: 0 additions & 1 deletion test/integration/multi-entries/src/edge.server.ts

This file was deleted.

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'
1 change: 1 addition & 0 deletions test/integration/multi-entries/src/shared.edge-light.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'shared.edge-light'
1 change: 1 addition & 0 deletions test/integration/multi-entries/src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'shared'

0 comments on commit dab2999

Please sign in to comment.