Skip to content

Commit

Permalink
fix: support Node16 and NodeNext module resolution in `experiment…
Browse files Browse the repository at this point in the history
…alDts` (#1225)
  • Loading branch information
aryaemami59 authored Oct 25, 2024
1 parent bf2aaf2 commit 41c98ff
Show file tree
Hide file tree
Showing 8 changed files with 917 additions and 103 deletions.
44 changes: 38 additions & 6 deletions src/api-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,18 @@ async function rollupDtsFiles(
exports: ExportDeclaration[],
format: Format,
) {
if (!options.experimentalDts || !options.experimentalDts?.entry) {
return
}

/**
* `.tsup/declaration` directory
*/
const declarationDir = ensureTempDeclarationDir()
const outDir = options.outDir || 'dist'
const pkg = await loadPkg(process.cwd())
const dtsExtension = defaultOutExtension({ format, pkgType: pkg.type }).dts
const tsconfig = options.tsconfig || 'tsconfig.json'

let dtsInputFilePath = path.join(
declarationDir,
Expand All @@ -113,16 +121,40 @@ async function rollupDtsFiles(
formatAggregationExports(exports, declarationDir),
)

rollupDtsFile(
dtsInputFilePath,
dtsOutputFilePath,
options.tsconfig || 'tsconfig.json',
)
rollupDtsFile(dtsInputFilePath, dtsOutputFilePath, tsconfig)

for (let [out, sourceFileName] of Object.entries(
options.experimentalDts!.entry,
options.experimentalDts.entry,
)) {
/**
* Source file name (`src/index.ts`)
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: { index: 'src/index.ts' },
* // Here `src/index.ts` is our `sourceFileName`.
* })
* ```
*/
sourceFileName = toAbsolutePath(sourceFileName)
/**
* Output file name (`dist/index.d.ts`)
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: { index: 'src/index.ts' },
* // Here `dist/index.d.ts` is our `outFileName`.
* })
* ```
*/
const outFileName = path.join(outDir, out + dtsExtension)

// Find all declarations that are exported from the current source file
Expand Down
8 changes: 4 additions & 4 deletions src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path'
import { slash, trimDtsExtension, truthy } from './utils'
import { replaceDtsWithJsExtensions, slash, truthy } from './utils'

export type ExportDeclaration = ModuleExport | NamedExport

Expand Down Expand Up @@ -41,14 +41,14 @@ function formatAggregationExport(
declaration: ExportDeclaration,
declarationDirPath: string,
): string {
const dest = trimDtsExtension(
const dest = replaceDtsWithJsExtensions(
`./${path.posix.normalize(
slash(path.relative(declarationDirPath, declaration.destFileName)),
)}`,
)

if (declaration.kind === 'module') {
// No implemeted
// Not implemented
return ''
} else if (declaration.kind === 'named') {
return [
Expand All @@ -72,7 +72,7 @@ export function formatDistributionExports(
fromFilePath: string,
toFilePath: string,
) {
let importPath = trimDtsExtension(
let importPath = replaceDtsWithJsExtensions(
path.posix.relative(
path.posix.dirname(path.posix.normalize(slash(fromFilePath))),
path.posix.normalize(slash(toFilePath)),
Expand Down
36 changes: 12 additions & 24 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import {
type MaybePromise,
debouncePromise,
removeFiles,
resolveExperimentalDtsConfig,
resolveInitialExperimentalDtsConfig,
slash,
toObjectEntry,
} from './utils'
import { createLogger, setSilent } from './log'
import { runEsbuild } from './esbuild'
Expand Down Expand Up @@ -92,20 +93,10 @@ const normalizeOptions = async (
: typeof _options.dts === 'string'
? { entry: _options.dts }
: _options.dts,
experimentalDts: _options.experimentalDts
? typeof _options.experimentalDts === 'boolean'
? _options.experimentalDts
? { entry: {} }
: undefined
: typeof _options.experimentalDts === 'string'
? {
entry: toObjectEntry(_options.experimentalDts),
}
: {
..._options.experimentalDts,
entry: toObjectEntry(_options.experimentalDts.entry || {}),
}
: undefined,

experimentalDts: await resolveInitialExperimentalDtsConfig(
_options.experimentalDts,
),
}

setSilent(options.silent)
Expand Down Expand Up @@ -151,17 +142,14 @@ const normalizeOptions = async (
...(options.dts.compilerOptions || {}),
}
}

if (options.experimentalDts) {
options.experimentalDts.compilerOptions = {
...(tsconfig.data.compilerOptions || {}),
...(options.experimentalDts.compilerOptions || {}),
}
options.experimentalDts.entry = toObjectEntry(
Object.keys(options.experimentalDts.entry).length > 0
? options.experimentalDts.entry
: options.entry,
options.experimentalDts = await resolveExperimentalDtsConfig(
options as NormalizedOptions,
tsconfig,
)
}

if (!options.target) {
options.target = tsconfig.data?.compilerOptions?.target?.toLowerCase()
}
Expand Down Expand Up @@ -252,7 +240,7 @@ export async function build(_options: Options) {
worker.on('message', (data) => {
if (data === 'error') {
terminateWorker()
reject(new Error('error occured in dts build'))
reject(new Error('error occurred in dts build'))
} else if (data === 'success') {
terminateWorker()
resolve()
Expand Down
1 change: 1 addition & 0 deletions src/tsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ function emit(compilerOptions?: any, tsconfig?: string) {
...rawTsconfig.data,
compilerOptions: {
...rawTsconfig.data?.compilerOptions,
...compilerOptions,

// Enable declaration emit and disable javascript emit
noEmit: false,
Expand Down
182 changes: 181 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import fs from 'node:fs'
import path from 'node:path'
import resolveFrom from 'resolve-from'
import type { InputOption } from 'rollup'
import strip from 'strip-json-comments'
import { glob } from 'tinyglobby'
import type { Entry, Format } from './options'
import type {
Entry,
Format,
NormalizedExperimentalDtsConfig,
NormalizedOptions,
Options,
} from './options'

export type MaybePromise<T> = T | Promise<T>

Expand Down Expand Up @@ -242,3 +249,176 @@ export function writeFileSync(filePath: string, content: string) {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, content)
}

/**
* Replaces TypeScript declaration file
* extensions (`.d.ts`, `.d.mts`, `.d.cts`)
* with their corresponding JavaScript variants (`.js`, `.mjs`, `.cjs`).
*
* @param dtsFilePath - The file path to be transformed.
* @returns The updated file path with the JavaScript extension.
*
* @internal
*/
export function replaceDtsWithJsExtensions(dtsFilePath: string) {
return dtsFilePath.replace(
/\.d\.(ts|mts|cts)$/,
(_, fileExtension: string) => {
switch (fileExtension) {
case 'ts':
return '.js'
case 'mts':
return '.mjs'
case 'cts':
return '.cjs'
default:
return ''
}
},
)
}

/**
* Converts an array of {@link NormalizedOptions.entry | entry paths}
* into an object where the keys represent the output
* file names (without extensions) and the values
* represent the corresponding input file paths.
*
* @param arrayOfEntries - An array of file path entries as strings.
* @returns An object where the keys are the output file name and the values are the input file name.
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: ['src/index.ts', 'src/types.ts'],
* // Becomes `{ index: 'src/index.ts', types: 'src/types.ts' }`
* })
* ```
*
* @internal
*/
const convertArrayEntriesToObjectEntries = (arrayOfEntries: string[]) => {
const objectEntries = Object.fromEntries(
arrayOfEntries.map(
(entry) =>
[
path.posix.join(
...entry
.split(path.posix.sep)
.slice(1, -1)
.concat(path.parse(entry).name),
),
entry,
] as const,
),
)

return objectEntries
}

/**
* Resolves and standardizes entry paths into an object format. If the provided
* entry is a string or an array of strings, it resolves any potential glob
* patterns amd converts the result into an entry object. If the input is
* already an object, it is returned as-is.
*
* @example
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: { index: 'src/index.ts' },
* format: ['esm', 'cjs'],
* experimentalDts: { entry: 'src/**\/*.ts' },
* // becomes experimentalDts: { entry: { index: 'src/index.ts', types: 'src/types.ts } }
* })
* ```
*
* @internal
*/
const resolveEntryPaths = async (entryPaths: InputOption) => {
const resolvedEntryPaths =
typeof entryPaths === 'string' || Array.isArray(entryPaths)
? convertArrayEntriesToObjectEntries(await glob(entryPaths))
: entryPaths

return resolvedEntryPaths
}

/**
* Resolves the
* {@link NormalizedExperimentalDtsConfig | experimental DTS config} by
* resolving entry paths and merging the provided TypeScript configuration
* options.
*
* @param options - The options containing entry points and experimental DTS
* configuration.
* @param tsconfig - The loaded TypeScript configuration data.
*
* @internal
*/
export const resolveExperimentalDtsConfig = async (
options: NormalizedOptions,
tsconfig: any,
): Promise<NormalizedExperimentalDtsConfig> => {
const resolvedEntryPaths = await resolveEntryPaths(
options.experimentalDts?.entry || options.entry,
)

// Fallback to `options.entry` if we end up with an empty object.
const experimentalDtsObjectEntry =
Object.keys(resolvedEntryPaths).length === 0
? Array.isArray(options.entry)
? convertArrayEntriesToObjectEntries(options.entry)
: options.entry
: resolvedEntryPaths

const normalizedExperimentalDtsConfig: NormalizedExperimentalDtsConfig = {
compilerOptions: {
...(tsconfig.data.compilerOptions || {}),
...(options.experimentalDts?.compilerOptions || {}),
},

entry: experimentalDtsObjectEntry,
}

return normalizedExperimentalDtsConfig
}

/**
* Resolves the initial experimental DTS configuration into a consistent
* {@link NormalizedExperimentalDtsConfig} object.
*
* @internal
*/
export const resolveInitialExperimentalDtsConfig = async (
experimentalDts: Options['experimentalDts'],
): Promise<NormalizedExperimentalDtsConfig | undefined> => {
if (experimentalDts == null) {
return
}

if (typeof experimentalDts === 'boolean')
return experimentalDts ? { entry: {} } : undefined

if (typeof experimentalDts === 'string') {
// Treats the string as a glob pattern, resolving it to entry paths and
// returning an object with the `entry` property.
return {
entry: convertArrayEntriesToObjectEntries(await glob(experimentalDts)),
}
}

return {
...experimentalDts,

entry:
experimentalDts?.entry == null
? {}
: await resolveEntryPaths(experimentalDts.entry),
}
}
Loading

0 comments on commit 41c98ff

Please sign in to comment.