Skip to content

Commit fb4c2b6

Browse files
authored
feat: Ensure matching declaration file exists for each output bundle format (#934)
* Ensure dts files match all output formats * Update docs
1 parent 3606e45 commit fb4c2b6

File tree

6 files changed

+85
-43
lines changed

6 files changed

+85
-43
lines changed

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export default defineConfig({
191191
tsup index.ts --dts
192192
```
193193

194-
This will emit `./dist/index.js` and `./dist/index.d.ts`.
194+
This will emit `./dist/index.js` and `./dist/index.d.ts`. When emitting multiple [bundle formats](#bundle-formats), one declaration file per bundle format is generated. This is required for consumers to get accurate type checking with TypeScript. Note that declaration files generated by any tool other than `tsc` are not guaranteed to be error-free, so it's a good idea to test the output with `tsc` or a tool like [@arethetypeswrong/cli](https://www.npmjs.com/package/@arethetypeswrong/cli) before publishing.
195195

196196
If you have multiple entry files, each entry will get a corresponding `.d.ts` file. So when you only want to generate declaration file for a single entry, use `--dts <entry>` format, e.g. `--dts src/index.ts`.
197197

src/esbuild/index.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,12 @@ import { externalPlugin } from './external'
1414
import { postcssPlugin } from './postcss'
1515
import { sveltePlugin } from './svelte'
1616
import consola from 'consola'
17-
import { truthy } from '../utils'
17+
import { defaultOutExtension, truthy } from '../utils'
1818
import { swcPlugin } from './swc'
1919
import { nativeNodeModulesPlugin } from './native-node-module'
2020
import { PluginContainer } from '../plugin'
2121
import { OutExtensionFactory } from '../options'
2222

23-
const defaultOutExtension = ({
24-
format,
25-
pkgType,
26-
}: {
27-
format: Format
28-
pkgType?: string
29-
}): { js: string } => {
30-
let jsExtension = '.js'
31-
const isModule = pkgType === 'module'
32-
if (isModule && format === 'cjs') {
33-
jsExtension = '.cjs'
34-
}
35-
if (!isModule && format === 'esm') {
36-
jsExtension = '.mjs'
37-
}
38-
if (format === 'iife') {
39-
jsExtension = '.global.js'
40-
}
41-
return {
42-
js: jsExtension,
43-
}
44-
}
45-
4623
const getOutputExtensionMap = (
4724
options: NormalizedOptions,
4825
format: Format,

src/options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type ContextForOutPathGeneration = {
1414
pkgType?: string
1515
}
1616

17-
export type OutExtensionObject = { js?: string }
17+
export type OutExtensionObject = { js?: string, dts?: string }
1818

1919
export type OutExtensionFactory = (
2020
ctx: ContextForOutPathGeneration

src/rollup.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import ts from 'typescript'
55
import hashbangPlugin from 'rollup-plugin-hashbang'
66
import jsonPlugin from '@rollup/plugin-json'
77
import { handleError } from './errors'
8-
import { removeFiles } from './utils'
8+
import { defaultOutExtension, removeFiles } from './utils'
99
import { TsResolveOptions, tsResolvePlugin } from './rollup/ts-resolve'
1010
import { createLogger, setSilent } from './log'
11-
import { getProductionDeps } from './load'
11+
import { getProductionDeps, loadPkg } from './load'
1212
import path from 'path'
1313
import { reportSize } from './lib/report-size'
1414
import resolveFrom from 'resolve-from'
@@ -32,7 +32,7 @@ const dtsPlugin: typeof import('rollup-plugin-dts') = require('rollup-plugin-dts
3232

3333
type RollupConfig = {
3434
inputConfig: InputOptions
35-
outputConfig: OutputOptions
35+
outputConfig: OutputOptions[]
3636
}
3737

3838
const findLowestCommonAncestor = (filepaths: string[]) => {
@@ -111,6 +111,7 @@ const getRollupConfig = async (
111111
}
112112
}
113113

114+
const pkg = await loadPkg(process.cwd())
114115
const deps = await getProductionDeps(process.cwd())
115116

116117
const tsupCleanPlugin: Plugin = {
@@ -188,13 +189,19 @@ const getRollupConfig = async (
188189
...(options.external || []),
189190
],
190191
},
191-
outputConfig: {
192-
dir: options.outDir || 'dist',
193-
format: 'esm',
194-
exports: 'named',
195-
banner: dtsOptions.banner,
196-
footer: dtsOptions.footer,
197-
},
192+
outputConfig: options.format.map((format) => {
193+
const outputExtension =
194+
options.outExtension?.({ format, options, pkgType: pkg.type }).dts ||
195+
defaultOutExtension({ format, pkgType: pkg.type }).dts
196+
return {
197+
dir: options.outDir || 'dist',
198+
format: 'esm',
199+
exports: 'named',
200+
banner: dtsOptions.banner,
201+
footer: dtsOptions.footer,
202+
entryFileNames: `[name]${outputExtension}`,
203+
}
204+
}),
198205
}
199206
}
200207

@@ -207,15 +214,16 @@ async function runRollup(options: RollupConfig) {
207214
}
208215
logger.info('dts', 'Build start')
209216
const bundle = await rollup(options.inputConfig)
210-
const result = await bundle.write(options.outputConfig)
217+
const results = await Promise.all(options.outputConfig.map(bundle.write))
218+
const outputs = results.flatMap((result) => result.output);
211219
logger.success('dts', `⚡️ Build success in ${getDuration()}`)
212220
reportSize(
213221
logger,
214222
'dts',
215-
result.output.reduce((res, info) => {
223+
outputs.reduce((res, info) => {
216224
const name = path.relative(
217225
process.cwd(),
218-
path.join(options.outputConfig.dir || '.', info.fileName)
226+
path.join(options.outputConfig[0].dir || '.', info.fileName)
219227
)
220228
return {
221229
...res,
@@ -231,7 +239,7 @@ async function runRollup(options: RollupConfig) {
231239

232240
async function watchRollup(options: {
233241
inputConfig: InputOptions
234-
outputConfig: OutputOptions
242+
outputConfig: OutputOptions[]
235243
}) {
236244
const { watch } = await import('rollup')
237245

src/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'fs'
22
import glob from 'globby'
33
import resolveFrom from 'resolve-from'
44
import strip from 'strip-json-comments'
5+
import { Format } from './options'
56

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

@@ -129,3 +130,30 @@ export function jsoncParse(data: string) {
129130
return {}
130131
}
131132
}
133+
134+
export function defaultOutExtension({
135+
format,
136+
pkgType,
137+
}: {
138+
format: Format
139+
pkgType?: string
140+
}): { js: string, dts: string } {
141+
let jsExtension = '.js'
142+
let dtsExtension = '.d.ts'
143+
const isModule = pkgType === 'module'
144+
if (isModule && format === 'cjs') {
145+
jsExtension = '.cjs'
146+
dtsExtension = '.d.cts'
147+
}
148+
if (!isModule && format === 'esm') {
149+
jsExtension = '.mjs'
150+
dtsExtension = '.d.mts'
151+
}
152+
if (format === 'iife') {
153+
jsExtension = '.global.js'
154+
}
155+
return {
156+
js: jsExtension,
157+
dts: dtsExtension,
158+
}
159+
}

test/index.test.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,7 +1248,7 @@ test(`custom tsconfig should pass to dts plugin`, async () => {
12481248
}
12491249
`,
12501250
})
1251-
expect(outFiles).toEqual(['input.d.ts'])
1251+
expect(outFiles).toEqual(['input.d.mts'])
12521252
})
12531253

12541254
test(`should generate export {} when there are no exports in source file`, async () => {
@@ -1268,8 +1268,8 @@ test(`should generate export {} when there are no exports in source file`, async
12681268
}
12691269
`,
12701270
})
1271-
expect(outFiles).toEqual(['input.d.ts', 'input.mjs'])
1272-
expect(await getFileContent('dist/input.d.ts')).toContain('export { }')
1271+
expect(outFiles).toEqual(['input.d.mts', 'input.mjs'])
1272+
expect(await getFileContent('dist/input.d.mts')).toContain('export { }')
12731273
})
12741274

12751275
test('custom inject style function', async () => {
@@ -1335,3 +1335,32 @@ test('should load postcss esm config', async () => {
13351335
expect(outFiles).toEqual(['input.cjs', 'input.css'])
13361336
expect(await getFileContent('dist/input.css')).toContain('color: blue;')
13371337
})
1338+
1339+
test('should emit a declaration file per format', async () => {
1340+
const { outFiles } = await run(getTestName(), {
1341+
'input.ts': `export default 'foo'`,
1342+
'tsup.config.ts': `
1343+
export default {
1344+
entry: ['src/input.ts'],
1345+
format: ['esm', 'cjs'],
1346+
dts: true
1347+
}`,
1348+
});
1349+
expect(outFiles).toEqual(['input.d.mts', 'input.d.ts', 'input.js', 'input.mjs'])
1350+
});
1351+
1352+
test('should emit a declaration file per format (type: module)', async () => {
1353+
const { outFiles } = await run(getTestName(), {
1354+
'input.ts': `export default 'foo'`,
1355+
'package.json': `{
1356+
"type": "module"
1357+
}`,
1358+
'tsup.config.ts': `
1359+
export default {
1360+
entry: ['src/input.ts'],
1361+
format: ['esm', 'cjs'],
1362+
dts: true
1363+
}`,
1364+
});
1365+
expect(outFiles).toEqual(['input.cjs', 'input.d.cts', 'input.d.ts', 'input.js'])
1366+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy