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

feat: add experimentalDts.only #1237

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions src/cli-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export async function main(options: Options = {}) {
'--experimental-dts [entry]',
'Generate declaration file (experimental)',
)
.option(
'--experimental-dts-only',
'Emit declaration files only (experimental)',
)
.alias('--experimentalDts-only')
.alias('--experimentalDtsOnly')
.option(
'--sourcemap [inline]',
'Generate external sourcemap, or inline source: --sourcemap inline',
Expand Down Expand Up @@ -133,6 +139,19 @@ export async function main(options: Options = {}) {
options.dts.only = true
}
}

if (flags.experimentalDts || flags.experimentalDtsOnly) {
options.experimentalDts = {}

if (typeof flags.experimentalDts === 'string') {
options.experimentalDts.entry = flags.experimentalDts
}

if (flags.experimentalDtsOnly) {
options.experimentalDts.only = true
}
}

if (flags.inject) {
const inject = ensureArray(flags.inject)
options.inject = inject
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export async function build(_options: Options) {
}

const mainTasks = async () => {
if (!options.dts?.only) {
if (!options.experimentalDts?.only && !options.dts?.only) {
let onSuccessProcess: ExecChild | undefined
let onSuccessCleanup: (() => any) | undefined | void
/** Files imported by the entry */
Expand Down
37 changes: 36 additions & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,40 @@ export type DtsConfig = {
}

export type ExperimentalDtsConfig = {
/**
* When set to `true`, this option will emit only
* TypeScript declaration (`.d.ts`) files and skip generating the
* corresponding JavaScript files during the build process.
*
* @example
* <caption>#### Generate only TypeScript declaration files</caption>
*
* ```ts
* import { defineConfig } from 'tsup'
*
* export default defineConfig({
* entry: { index: 'src/index.ts' },
* format: ['esm', 'cjs'],
* experimentalDts: { only: true },
* })
* ```
*
* @default false
*
* @since 8.4.0
*
* #### CLI Equivalent:
* You can use the following CLI commands to achieve the same result:
*
* ```bash
* tsup src/index.ts --experimental-dts-only
* # or
* tsup src/index.ts --experimentalDts-only
* # or
* tsup src/index.ts --experimentalDtsOnly
* ```
*/
only?: boolean
entry?: InputOption
/**
* Overrides `compilerOptions`
Expand Down Expand Up @@ -258,7 +292,8 @@ export type Options = {
removeNodeProtocol?: boolean
}

export interface NormalizedExperimentalDtsConfig {
export interface NormalizedExperimentalDtsConfig extends ExperimentalDtsConfig {
only: boolean
entry: { [entryAlias: string]: string }
compilerOptions?: any
}
Expand Down
31 changes: 28 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ const convertArrayEntriesToObjectEntries = (arrayOfEntries: string[]) => {
* patterns and converts the result into an entry object. If the input is
* already an object, it is returned as-is.
*
* @param entryPaths - The entry paths to resolve. Can be a string, an array of strings, or an object.
* @returns A {@linkcode Promise | promise} that resolves to the standardized entry paths in object format.
*
* @example
*
* ```ts
Expand Down Expand Up @@ -358,6 +361,7 @@ const resolveEntryPaths = async (entryPaths: InputOption) => {
* @param options - The options containing entry points and experimental DTS
* configuration.
* @param tsconfig - The loaded TypeScript configuration data.
* @returns A {@linkcode Promise | promise} that resolves to the normalized experimental DTS configuration, or `undefined` if no entry or experimental DTS option is provided.
*
* @internal
*/
Expand All @@ -378,6 +382,8 @@ export const resolveExperimentalDtsConfig = async (
: resolvedEntryPaths

const normalizedExperimentalDtsConfig: NormalizedExperimentalDtsConfig = {
only: options.experimentalDts?.only || false,

compilerOptions: {
...(tsconfig.data.compilerOptions || {}),
...(options.experimentalDts?.compilerOptions || {}),
Expand All @@ -390,8 +396,23 @@ export const resolveExperimentalDtsConfig = async (
}

/**
* Resolves the initial experimental DTS configuration into a consistent
* {@link NormalizedExperimentalDtsConfig} object.
* Resolves the initial experimental DTS configuration
* into a consistent
* {@linkcode NormalizedExperimentalDtsConfig | experimentalDts config object}.
*
* This function handles different types of
* {@linkcode NormalizedExperimentalDtsConfig | experimentalDts} inputs:
* - If {@linkcode experimentalDts} is a `boolean`:
* - if `true`, it returns a default object with an empty entry (`{ entry: {} }`).
* - if `false`, it returns `undefined`.
* - If {@linkcode experimentalDts} is a `string`, it treats the string as a glob pattern, resolving it to entry paths and returning an object with the `entry` property.
* - If {@linkcode experimentalDts} is already an object ({@linkcode NormalizedExperimentalDtsConfig}), it resolves {@linkcode NormalizedExperimentalDtsConfig.entry | the entry paths} (if necessary) and returns the object with the updated entries.
*
* The function focuses specifically on normalizing the **initial**
* {@linkcode NormalizedExperimentalDtsConfig | experimentalDts configuration}.
*
* @param experimentalDts - The {@linkcode NormalizedExperimentalDtsConfig | experimentalDts} value, which can be a `boolean`, `string`, `object`, or `undefined`.
* @returns A {@linkcode Promise | promise} that resolves to a normalized {@linkcode NormalizedExperimentalDtsConfig | experimentalDts config object}, or `undefined` if the input was `false` or `undefined`.
*
* @internal
*/
Expand All @@ -403,19 +424,23 @@ export const resolveInitialExperimentalDtsConfig = async (
}

if (typeof experimentalDts === 'boolean')
return experimentalDts ? { entry: {} } : undefined
return experimentalDts ? { only: false, 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 {
only: false,

entry: convertArrayEntriesToObjectEntries(await glob(experimentalDts)),
}
}

return {
...experimentalDts,

only: experimentalDts?.only || false,

entry:
experimentalDts?.entry == null
? {}
Expand Down
103 changes: 103 additions & 0 deletions test/experimental-dts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,3 +604,106 @@ test('experimentalDts.entry can be a string of glob pattern', async ({
),
)
})

test('experimentalDts.only works', async ({ expect, task }) => {
const { outFiles } = await run(
getTestName(),
{
'src/types.ts': `export type Person = { name: string }`,
'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`,
'tsup.config.ts': `export default ${JSON.stringify(
{
name: task.name,
entry: { index: 'src/index.ts' },
format: ['esm', 'cjs'],
experimentalDts: { only: true },
} satisfies Options,
null,
2,
)}`,
'package.json': JSON.stringify(
{
name: 'experimental-dts-only-works',
description: task.name,
type: 'module',
},
null,
2,
),
'tsconfig.json': JSON.stringify(
{
compilerOptions: {
outDir: './dist',
rootDir: './src',
skipLibCheck: true,
strict: true,
},
include: ['src'],
},
null,
2,
),
},
{
entry: [],
},
)

expect(outFiles).toStrictEqual([
'_tsup-dts-rollup.d.cts',
'_tsup-dts-rollup.d.ts',
'index.d.cts',
'index.d.ts',
])
})

test('experimental-dts-only cli option works', async ({ expect, task }) => {
const { outFiles } = await run(
getTestName(),
{
'src/types.ts': `export type Person = { name: string }`,
'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types.js'`,
'tsup.config.ts': `export default ${JSON.stringify(
{
name: task.name,
format: ['esm', 'cjs'],
} satisfies Options,
null,
2,
)}`,
'package.json': JSON.stringify(
{
name: 'experimental-dts-only-cli-works',
description: task.name,
type: 'module',
},
null,
2,
),
'tsconfig.json': JSON.stringify(
{
compilerOptions: {
outDir: './dist',
rootDir: './src',
skipLibCheck: true,
strict: true,
},
include: ['src'],
},
null,
2,
),
},
{
entry: ['./src/index.ts'],
flags: ['--experimental-dts-only'],
},
)

expect(outFiles).toStrictEqual([
'_tsup-dts-rollup.d.cts',
'_tsup-dts-rollup.d.ts',
'index.d.cts',
'index.d.ts',
])
})