From 6546413cdf90d1993290f90328a13f3df3963c24 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 18 Oct 2024 17:05:08 -0500 Subject: [PATCH] Add `experimentalDts.only` --- src/cli-main.ts | 19 +++++++ src/index.ts | 2 +- src/options.ts | 37 +++++++++++- src/utils.ts | 31 +++++++++- test/experimental-dts.test.ts | 103 ++++++++++++++++++++++++++++++++++ 5 files changed, 187 insertions(+), 5 deletions(-) diff --git a/src/cli-main.ts b/src/cli-main.ts index 070236e1e..0022b8599 100644 --- a/src/cli-main.ts +++ b/src/cli-main.ts @@ -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', @@ -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 diff --git a/src/index.ts b/src/index.ts index 85a6ea041..a7c03d25c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 */ diff --git a/src/options.ts b/src/options.ts index 827790d43..cb973818f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -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 + * #### Generate only TypeScript declaration files + * + * ```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` @@ -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 } diff --git a/src/utils.ts b/src/utils.ts index e69cd3ad7..bdb513581 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -325,6 +325,9 @@ const convertArrayEntriesToObjectEntries = (arrayOfEntries: string[]) => { * patterns amd 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 @@ -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 */ @@ -378,6 +382,8 @@ export const resolveExperimentalDtsConfig = async ( : resolvedEntryPaths const normalizedExperimentalDtsConfig: NormalizedExperimentalDtsConfig = { + only: options.experimentalDts?.only || false, + compilerOptions: { ...(tsconfig.data.compilerOptions || {}), ...(options.experimentalDts?.compilerOptions || {}), @@ -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 */ @@ -403,12 +424,14 @@ 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)), } } @@ -416,6 +439,8 @@ export const resolveInitialExperimentalDtsConfig = async ( return { ...experimentalDts, + only: experimentalDts?.only || false, + entry: experimentalDts?.entry == null ? {} diff --git a/test/experimental-dts.test.ts b/test/experimental-dts.test.ts index 825afbbfc..e2d9926a2 100644 --- a/test/experimental-dts.test.ts +++ b/test/experimental-dts.test.ts @@ -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', + ]) +})