Skip to content

Commit

Permalink
feat: Ensure matching declaration file exists for each output bundle …
Browse files Browse the repository at this point in the history
…format (#934)

* Ensure dts files match all output formats

* Update docs
  • Loading branch information
andrewbranch authored Jun 25, 2023
1 parent 3606e45 commit fb4c2b6
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 43 deletions.
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default defineConfig({
tsup index.ts --dts
```

This will emit `./dist/index.js` and `./dist/index.d.ts`.
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.

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`.

Expand Down
25 changes: 1 addition & 24 deletions src/esbuild/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,12 @@ import { externalPlugin } from './external'
import { postcssPlugin } from './postcss'
import { sveltePlugin } from './svelte'
import consola from 'consola'
import { truthy } from '../utils'
import { defaultOutExtension, truthy } from '../utils'
import { swcPlugin } from './swc'
import { nativeNodeModulesPlugin } from './native-node-module'
import { PluginContainer } from '../plugin'
import { OutExtensionFactory } from '../options'

const defaultOutExtension = ({
format,
pkgType,
}: {
format: Format
pkgType?: string
}): { js: string } => {
let jsExtension = '.js'
const isModule = pkgType === 'module'
if (isModule && format === 'cjs') {
jsExtension = '.cjs'
}
if (!isModule && format === 'esm') {
jsExtension = '.mjs'
}
if (format === 'iife') {
jsExtension = '.global.js'
}
return {
js: jsExtension,
}
}

const getOutputExtensionMap = (
options: NormalizedOptions,
format: Format,
Expand Down
2 changes: 1 addition & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type ContextForOutPathGeneration = {
pkgType?: string
}

export type OutExtensionObject = { js?: string }
export type OutExtensionObject = { js?: string, dts?: string }

export type OutExtensionFactory = (
ctx: ContextForOutPathGeneration
Expand Down
36 changes: 22 additions & 14 deletions src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import ts from 'typescript'
import hashbangPlugin from 'rollup-plugin-hashbang'
import jsonPlugin from '@rollup/plugin-json'
import { handleError } from './errors'
import { removeFiles } from './utils'
import { defaultOutExtension, removeFiles } from './utils'
import { TsResolveOptions, tsResolvePlugin } from './rollup/ts-resolve'
import { createLogger, setSilent } from './log'
import { getProductionDeps } from './load'
import { getProductionDeps, loadPkg } from './load'
import path from 'path'
import { reportSize } from './lib/report-size'
import resolveFrom from 'resolve-from'
Expand All @@ -32,7 +32,7 @@ const dtsPlugin: typeof import('rollup-plugin-dts') = require('rollup-plugin-dts

type RollupConfig = {
inputConfig: InputOptions
outputConfig: OutputOptions
outputConfig: OutputOptions[]
}

const findLowestCommonAncestor = (filepaths: string[]) => {
Expand Down Expand Up @@ -111,6 +111,7 @@ const getRollupConfig = async (
}
}

const pkg = await loadPkg(process.cwd())
const deps = await getProductionDeps(process.cwd())

const tsupCleanPlugin: Plugin = {
Expand Down Expand Up @@ -188,13 +189,19 @@ const getRollupConfig = async (
...(options.external || []),
],
},
outputConfig: {
dir: options.outDir || 'dist',
format: 'esm',
exports: 'named',
banner: dtsOptions.banner,
footer: dtsOptions.footer,
},
outputConfig: options.format.map((format) => {
const outputExtension =
options.outExtension?.({ format, options, pkgType: pkg.type }).dts ||
defaultOutExtension({ format, pkgType: pkg.type }).dts
return {
dir: options.outDir || 'dist',
format: 'esm',
exports: 'named',
banner: dtsOptions.banner,
footer: dtsOptions.footer,
entryFileNames: `[name]${outputExtension}`,
}
}),
}
}

Expand All @@ -207,15 +214,16 @@ async function runRollup(options: RollupConfig) {
}
logger.info('dts', 'Build start')
const bundle = await rollup(options.inputConfig)
const result = await bundle.write(options.outputConfig)
const results = await Promise.all(options.outputConfig.map(bundle.write))
const outputs = results.flatMap((result) => result.output);
logger.success('dts', `⚡️ Build success in ${getDuration()}`)
reportSize(
logger,
'dts',
result.output.reduce((res, info) => {
outputs.reduce((res, info) => {
const name = path.relative(
process.cwd(),
path.join(options.outputConfig.dir || '.', info.fileName)
path.join(options.outputConfig[0].dir || '.', info.fileName)
)
return {
...res,
Expand All @@ -231,7 +239,7 @@ async function runRollup(options: RollupConfig) {

async function watchRollup(options: {
inputConfig: InputOptions
outputConfig: OutputOptions
outputConfig: OutputOptions[]
}) {
const { watch } = await import('rollup')

Expand Down
28 changes: 28 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs'
import glob from 'globby'
import resolveFrom from 'resolve-from'
import strip from 'strip-json-comments'
import { Format } from './options'

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

Expand Down Expand Up @@ -129,3 +130,30 @@ export function jsoncParse(data: string) {
return {}
}
}

export function defaultOutExtension({
format,
pkgType,
}: {
format: Format
pkgType?: string
}): { js: string, dts: string } {
let jsExtension = '.js'
let dtsExtension = '.d.ts'
const isModule = pkgType === 'module'
if (isModule && format === 'cjs') {
jsExtension = '.cjs'
dtsExtension = '.d.cts'
}
if (!isModule && format === 'esm') {
jsExtension = '.mjs'
dtsExtension = '.d.mts'
}
if (format === 'iife') {
jsExtension = '.global.js'
}
return {
js: jsExtension,
dts: dtsExtension,
}
}
35 changes: 32 additions & 3 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ test(`custom tsconfig should pass to dts plugin`, async () => {
}
`,
})
expect(outFiles).toEqual(['input.d.ts'])
expect(outFiles).toEqual(['input.d.mts'])
})

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

test('custom inject style function', async () => {
Expand Down Expand Up @@ -1335,3 +1335,32 @@ test('should load postcss esm config', async () => {
expect(outFiles).toEqual(['input.cjs', 'input.css'])
expect(await getFileContent('dist/input.css')).toContain('color: blue;')
})

test('should emit a declaration file per format', async () => {
const { outFiles } = await run(getTestName(), {
'input.ts': `export default 'foo'`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: ['esm', 'cjs'],
dts: true
}`,
});
expect(outFiles).toEqual(['input.d.mts', 'input.d.ts', 'input.js', 'input.mjs'])
});

test('should emit a declaration file per format (type: module)', async () => {
const { outFiles } = await run(getTestName(), {
'input.ts': `export default 'foo'`,
'package.json': `{
"type": "module"
}`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: ['esm', 'cjs'],
dts: true
}`,
});
expect(outFiles).toEqual(['input.cjs', 'input.d.cts', 'input.d.ts', 'input.js'])
});

0 comments on commit fb4c2b6

Please sign in to comment.