Skip to content

Commit 41c98ff

Browse files
authored
fix: support Node16 and NodeNext module resolution in experimentalDts (#1225)
1 parent bf2aaf2 commit 41c98ff

File tree

8 files changed

+917
-103
lines changed

8 files changed

+917
-103
lines changed

src/api-extractor.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,18 @@ async function rollupDtsFiles(
8888
exports: ExportDeclaration[],
8989
format: Format,
9090
) {
91+
if (!options.experimentalDts || !options.experimentalDts?.entry) {
92+
return
93+
}
94+
95+
/**
96+
* `.tsup/declaration` directory
97+
*/
9198
const declarationDir = ensureTempDeclarationDir()
9299
const outDir = options.outDir || 'dist'
93100
const pkg = await loadPkg(process.cwd())
94101
const dtsExtension = defaultOutExtension({ format, pkgType: pkg.type }).dts
102+
const tsconfig = options.tsconfig || 'tsconfig.json'
95103

96104
let dtsInputFilePath = path.join(
97105
declarationDir,
@@ -113,16 +121,40 @@ async function rollupDtsFiles(
113121
formatAggregationExports(exports, declarationDir),
114122
)
115123

116-
rollupDtsFile(
117-
dtsInputFilePath,
118-
dtsOutputFilePath,
119-
options.tsconfig || 'tsconfig.json',
120-
)
124+
rollupDtsFile(dtsInputFilePath, dtsOutputFilePath, tsconfig)
121125

122126
for (let [out, sourceFileName] of Object.entries(
123-
options.experimentalDts!.entry,
127+
options.experimentalDts.entry,
124128
)) {
129+
/**
130+
* Source file name (`src/index.ts`)
131+
*
132+
* @example
133+
*
134+
* ```ts
135+
* import { defineConfig } from 'tsup'
136+
*
137+
* export default defineConfig({
138+
* entry: { index: 'src/index.ts' },
139+
* // Here `src/index.ts` is our `sourceFileName`.
140+
* })
141+
* ```
142+
*/
125143
sourceFileName = toAbsolutePath(sourceFileName)
144+
/**
145+
* Output file name (`dist/index.d.ts`)
146+
*
147+
* @example
148+
*
149+
* ```ts
150+
* import { defineConfig } from 'tsup'
151+
*
152+
* export default defineConfig({
153+
* entry: { index: 'src/index.ts' },
154+
* // Here `dist/index.d.ts` is our `outFileName`.
155+
* })
156+
* ```
157+
*/
126158
const outFileName = path.join(outDir, out + dtsExtension)
127159

128160
// Find all declarations that are exported from the current source file

src/exports.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'node:path'
2-
import { slash, trimDtsExtension, truthy } from './utils'
2+
import { replaceDtsWithJsExtensions, slash, truthy } from './utils'
33

44
export type ExportDeclaration = ModuleExport | NamedExport
55

@@ -41,14 +41,14 @@ function formatAggregationExport(
4141
declaration: ExportDeclaration,
4242
declarationDirPath: string,
4343
): string {
44-
const dest = trimDtsExtension(
44+
const dest = replaceDtsWithJsExtensions(
4545
`./${path.posix.normalize(
4646
slash(path.relative(declarationDirPath, declaration.destFileName)),
4747
)}`,
4848
)
4949

5050
if (declaration.kind === 'module') {
51-
// No implemeted
51+
// Not implemented
5252
return ''
5353
} else if (declaration.kind === 'named') {
5454
return [
@@ -72,7 +72,7 @@ export function formatDistributionExports(
7272
fromFilePath: string,
7373
toFilePath: string,
7474
) {
75-
let importPath = trimDtsExtension(
75+
let importPath = replaceDtsWithJsExtensions(
7676
path.posix.relative(
7777
path.posix.dirname(path.posix.normalize(slash(fromFilePath))),
7878
path.posix.normalize(slash(toFilePath)),

src/index.ts

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
type MaybePromise,
1313
debouncePromise,
1414
removeFiles,
15+
resolveExperimentalDtsConfig,
16+
resolveInitialExperimentalDtsConfig,
1517
slash,
16-
toObjectEntry,
1718
} from './utils'
1819
import { createLogger, setSilent } from './log'
1920
import { runEsbuild } from './esbuild'
@@ -92,20 +93,10 @@ const normalizeOptions = async (
9293
: typeof _options.dts === 'string'
9394
? { entry: _options.dts }
9495
: _options.dts,
95-
experimentalDts: _options.experimentalDts
96-
? typeof _options.experimentalDts === 'boolean'
97-
? _options.experimentalDts
98-
? { entry: {} }
99-
: undefined
100-
: typeof _options.experimentalDts === 'string'
101-
? {
102-
entry: toObjectEntry(_options.experimentalDts),
103-
}
104-
: {
105-
..._options.experimentalDts,
106-
entry: toObjectEntry(_options.experimentalDts.entry || {}),
107-
}
108-
: undefined,
96+
97+
experimentalDts: await resolveInitialExperimentalDtsConfig(
98+
_options.experimentalDts,
99+
),
109100
}
110101

111102
setSilent(options.silent)
@@ -151,17 +142,14 @@ const normalizeOptions = async (
151142
...(options.dts.compilerOptions || {}),
152143
}
153144
}
145+
154146
if (options.experimentalDts) {
155-
options.experimentalDts.compilerOptions = {
156-
...(tsconfig.data.compilerOptions || {}),
157-
...(options.experimentalDts.compilerOptions || {}),
158-
}
159-
options.experimentalDts.entry = toObjectEntry(
160-
Object.keys(options.experimentalDts.entry).length > 0
161-
? options.experimentalDts.entry
162-
: options.entry,
147+
options.experimentalDts = await resolveExperimentalDtsConfig(
148+
options as NormalizedOptions,
149+
tsconfig,
163150
)
164151
}
152+
165153
if (!options.target) {
166154
options.target = tsconfig.data?.compilerOptions?.target?.toLowerCase()
167155
}
@@ -252,7 +240,7 @@ export async function build(_options: Options) {
252240
worker.on('message', (data) => {
253241
if (data === 'error') {
254242
terminateWorker()
255-
reject(new Error('error occured in dts build'))
243+
reject(new Error('error occurred in dts build'))
256244
} else if (data === 'success') {
257245
terminateWorker()
258246
resolve()

src/tsc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ function emit(compilerOptions?: any, tsconfig?: string) {
174174
...rawTsconfig.data,
175175
compilerOptions: {
176176
...rawTsconfig.data?.compilerOptions,
177+
...compilerOptions,
177178

178179
// Enable declaration emit and disable javascript emit
179180
noEmit: false,

src/utils.ts

Lines changed: 181 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
33
import resolveFrom from 'resolve-from'
4+
import type { InputOption } from 'rollup'
45
import strip from 'strip-json-comments'
56
import { glob } from 'tinyglobby'
6-
import type { Entry, Format } from './options'
7+
import type {
8+
Entry,
9+
Format,
10+
NormalizedExperimentalDtsConfig,
11+
NormalizedOptions,
12+
Options,
13+
} from './options'
714

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

@@ -242,3 +249,176 @@ export function writeFileSync(filePath: string, content: string) {
242249
fs.mkdirSync(path.dirname(filePath), { recursive: true })
243250
fs.writeFileSync(filePath, content)
244251
}
252+
253+
/**
254+
* Replaces TypeScript declaration file
255+
* extensions (`.d.ts`, `.d.mts`, `.d.cts`)
256+
* with their corresponding JavaScript variants (`.js`, `.mjs`, `.cjs`).
257+
*
258+
* @param dtsFilePath - The file path to be transformed.
259+
* @returns The updated file path with the JavaScript extension.
260+
*
261+
* @internal
262+
*/
263+
export function replaceDtsWithJsExtensions(dtsFilePath: string) {
264+
return dtsFilePath.replace(
265+
/\.d\.(ts|mts|cts)$/,
266+
(_, fileExtension: string) => {
267+
switch (fileExtension) {
268+
case 'ts':
269+
return '.js'
270+
case 'mts':
271+
return '.mjs'
272+
case 'cts':
273+
return '.cjs'
274+
default:
275+
return ''
276+
}
277+
},
278+
)
279+
}
280+
281+
/**
282+
* Converts an array of {@link NormalizedOptions.entry | entry paths}
283+
* into an object where the keys represent the output
284+
* file names (without extensions) and the values
285+
* represent the corresponding input file paths.
286+
*
287+
* @param arrayOfEntries - An array of file path entries as strings.
288+
* @returns An object where the keys are the output file name and the values are the input file name.
289+
*
290+
* @example
291+
*
292+
* ```ts
293+
* import { defineConfig } from 'tsup'
294+
*
295+
* export default defineConfig({
296+
* entry: ['src/index.ts', 'src/types.ts'],
297+
* // Becomes `{ index: 'src/index.ts', types: 'src/types.ts' }`
298+
* })
299+
* ```
300+
*
301+
* @internal
302+
*/
303+
const convertArrayEntriesToObjectEntries = (arrayOfEntries: string[]) => {
304+
const objectEntries = Object.fromEntries(
305+
arrayOfEntries.map(
306+
(entry) =>
307+
[
308+
path.posix.join(
309+
...entry
310+
.split(path.posix.sep)
311+
.slice(1, -1)
312+
.concat(path.parse(entry).name),
313+
),
314+
entry,
315+
] as const,
316+
),
317+
)
318+
319+
return objectEntries
320+
}
321+
322+
/**
323+
* Resolves and standardizes entry paths into an object format. If the provided
324+
* entry is a string or an array of strings, it resolves any potential glob
325+
* patterns amd converts the result into an entry object. If the input is
326+
* already an object, it is returned as-is.
327+
*
328+
* @example
329+
*
330+
* ```ts
331+
* import { defineConfig } from 'tsup'
332+
*
333+
* export default defineConfig({
334+
* entry: { index: 'src/index.ts' },
335+
* format: ['esm', 'cjs'],
336+
* experimentalDts: { entry: 'src/**\/*.ts' },
337+
* // becomes experimentalDts: { entry: { index: 'src/index.ts', types: 'src/types.ts } }
338+
* })
339+
* ```
340+
*
341+
* @internal
342+
*/
343+
const resolveEntryPaths = async (entryPaths: InputOption) => {
344+
const resolvedEntryPaths =
345+
typeof entryPaths === 'string' || Array.isArray(entryPaths)
346+
? convertArrayEntriesToObjectEntries(await glob(entryPaths))
347+
: entryPaths
348+
349+
return resolvedEntryPaths
350+
}
351+
352+
/**
353+
* Resolves the
354+
* {@link NormalizedExperimentalDtsConfig | experimental DTS config} by
355+
* resolving entry paths and merging the provided TypeScript configuration
356+
* options.
357+
*
358+
* @param options - The options containing entry points and experimental DTS
359+
* configuration.
360+
* @param tsconfig - The loaded TypeScript configuration data.
361+
*
362+
* @internal
363+
*/
364+
export const resolveExperimentalDtsConfig = async (
365+
options: NormalizedOptions,
366+
tsconfig: any,
367+
): Promise<NormalizedExperimentalDtsConfig> => {
368+
const resolvedEntryPaths = await resolveEntryPaths(
369+
options.experimentalDts?.entry || options.entry,
370+
)
371+
372+
// Fallback to `options.entry` if we end up with an empty object.
373+
const experimentalDtsObjectEntry =
374+
Object.keys(resolvedEntryPaths).length === 0
375+
? Array.isArray(options.entry)
376+
? convertArrayEntriesToObjectEntries(options.entry)
377+
: options.entry
378+
: resolvedEntryPaths
379+
380+
const normalizedExperimentalDtsConfig: NormalizedExperimentalDtsConfig = {
381+
compilerOptions: {
382+
...(tsconfig.data.compilerOptions || {}),
383+
...(options.experimentalDts?.compilerOptions || {}),
384+
},
385+
386+
entry: experimentalDtsObjectEntry,
387+
}
388+
389+
return normalizedExperimentalDtsConfig
390+
}
391+
392+
/**
393+
* Resolves the initial experimental DTS configuration into a consistent
394+
* {@link NormalizedExperimentalDtsConfig} object.
395+
*
396+
* @internal
397+
*/
398+
export const resolveInitialExperimentalDtsConfig = async (
399+
experimentalDts: Options['experimentalDts'],
400+
): Promise<NormalizedExperimentalDtsConfig | undefined> => {
401+
if (experimentalDts == null) {
402+
return
403+
}
404+
405+
if (typeof experimentalDts === 'boolean')
406+
return experimentalDts ? { entry: {} } : undefined
407+
408+
if (typeof experimentalDts === 'string') {
409+
// Treats the string as a glob pattern, resolving it to entry paths and
410+
// returning an object with the `entry` property.
411+
return {
412+
entry: convertArrayEntriesToObjectEntries(await glob(experimentalDts)),
413+
}
414+
}
415+
416+
return {
417+
...experimentalDts,
418+
419+
entry:
420+
experimentalDts?.entry == null
421+
? {}
422+
: await resolveEntryPaths(experimentalDts.entry),
423+
}
424+
}

0 commit comments

Comments
 (0)