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

fix: support Node16 and NodeNext module resolution in experimentalDts #1225

Merged
merged 5 commits into from
Oct 25, 2024
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
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