Skip to content

Commit a6c3d50

Browse files
committed
feat: 🎸 dtsOnly mode
1 parent 8b9c6ba commit a6c3d50

File tree

4 files changed

+135
-4
lines changed

4 files changed

+135
-4
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"Bash(git restore:*)",
4343
"Bash(pnpm test:coverage:*)",
4444
"Bash(pnpm docs:build:*)",
45-
"Bash(git checkout:*)"
45+
"Bash(git checkout:*)",
46+
"Bash(pnpm test:types:*)"
4647
]
4748
}
4849
}

src/builders/bundle.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
} from 'rolldown'
99
import type { Options as DtsOptions } from 'rolldown-plugin-dts'
1010
import type { BuildConfig, BuildContext, BuildHooks, BundleEntry } from '../types'
11-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
11+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
1212
import { builtinModules } from 'node:module'
1313
import { basename, dirname, extname, join, relative, resolve } from 'node:path'
1414
import { colors as c } from 'consola/utils'
@@ -77,8 +77,9 @@ export async function rolldownBuild(
7777
logger.info('Running in dtsOnly mode - only generating declaration files')
7878
// Force dts to be enabled
7979
entry.dts = entry.dts === false ? true : (entry.dts || true)
80-
// We'll skip the normal build and only run DTS generation
81-
// This will be handled in the format loop below
80+
// Force ESM format since DTS plugin only works with ESM
81+
formats.length = 0
82+
formats.push('esm')
8283
}
8384

8485
if (entry.stub) {
@@ -432,6 +433,40 @@ export async function rolldownBuild(
432433
}
433434
}
434435

436+
// Handle dtsOnly mode: delete JS files, keep only .d.ts files
437+
if (entry.dtsOnly) {
438+
const jsFilesToDelete: string[] = []
439+
for (const outputEntry of allOutputEntries) {
440+
const filePath = filePathMap.get(outputEntry.name)
441+
if (filePath && !filePath.endsWith('.d.ts') && !filePath.endsWith('.d.mts') && !filePath.endsWith('.d.cts')) {
442+
jsFilesToDelete.push(filePath)
443+
}
444+
}
445+
// Delete JS files
446+
for (const filePath of jsFilesToDelete) {
447+
try {
448+
await unlink(filePath)
449+
// Also try to delete sourcemap if exists
450+
try {
451+
await unlink(`${filePath}.map`)
452+
}
453+
catch {
454+
// ignore if no map file
455+
}
456+
}
457+
catch {
458+
// ignore if file doesn't exist
459+
}
460+
}
461+
// Filter out JS entries from output display
462+
const dtsEntries = allOutputEntries.filter(o =>
463+
o.name.endsWith('.d.ts') || o.name.endsWith('.d.mts') || o.name.endsWith('.d.cts'),
464+
)
465+
// Clear and re-add only DTS entries
466+
allOutputEntries.length = 0
467+
allOutputEntries.push(...dtsEntries)
468+
}
469+
435470
// Copy files if specified
436471
if (entry.copy) {
437472
await copyFiles(ctx.pkgDir, fullOutDir, entry.copy)

test/__snapshots__/bundle.test.ts.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,34 @@ return exports;
124124
"
125125
`;
126126
127+
exports[`bundle mode > dtsOnly mode > should force ESM format in dtsOnly mode even if cjs is specified 1`] = `
128+
"## index.d.mts
129+
130+
\`\`\`ts
131+
//#region index.d.ts
132+
declare const value = 42;
133+
//#endregion
134+
export { value };
135+
\`\`\`
136+
"
137+
`;
138+
139+
exports[`bundle mode > dtsOnly mode > should only generate .d.ts files when dtsOnly is true 1`] = `
140+
"## index.d.mts
141+
142+
\`\`\`ts
143+
//#region index.d.ts
144+
interface User {
145+
name: string;
146+
age: number;
147+
}
148+
declare function greet(user: User): string;
149+
//#endregion
150+
export { User, greet };
151+
\`\`\`
152+
"
153+
`;
154+
127155
exports[`bundle mode > external dependencies > should handle external dependencies 1`] = `
128156
"## index.mjs
129157

test/bundle.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,4 +365,71 @@ describe('bundle mode', () => {
365365
})
366366
})
367367
})
368+
369+
describe('dtsOnly mode', () => {
370+
it('should only generate .d.ts files when dtsOnly is true', async (context) => {
371+
await testBuild({
372+
context,
373+
files: {
374+
'index.ts': `
375+
export interface User {
376+
name: string
377+
age: number
378+
}
379+
export function greet(user: User): string {
380+
return \`Hello, \${user.name}!\`
381+
}
382+
`,
383+
},
384+
config: {
385+
entries: [
386+
{
387+
type: 'bundle',
388+
input: 'index.ts',
389+
dtsOnly: true,
390+
},
391+
],
392+
},
393+
afterBuild: async (outputDir) => {
394+
const { readdirSync } = await import('node:fs')
395+
const files = readdirSync(outputDir)
396+
// Should only have .d.ts files, no .mjs files
397+
const jsFiles = files.filter(f => f.endsWith('.mjs') || f.endsWith('.js') || f.endsWith('.cjs'))
398+
const dtsFiles = files.filter(f => f.endsWith('.d.ts') || f.endsWith('.d.mts'))
399+
expect(jsFiles.length).toBe(0)
400+
expect(dtsFiles.length).toBeGreaterThan(0)
401+
},
402+
})
403+
})
404+
405+
it('should force ESM format in dtsOnly mode even if cjs is specified', async (context) => {
406+
await testBuild({
407+
context,
408+
files: {
409+
'index.ts': `
410+
export const value = 42
411+
`,
412+
},
413+
config: {
414+
entries: [
415+
{
416+
type: 'bundle',
417+
input: 'index.ts',
418+
format: 'cjs',
419+
dtsOnly: true,
420+
},
421+
],
422+
},
423+
afterBuild: async (outputDir) => {
424+
const { readdirSync } = await import('node:fs')
425+
const files = readdirSync(outputDir)
426+
// Should only have .d.ts files
427+
const jsFiles = files.filter(f => f.endsWith('.mjs') || f.endsWith('.js') || f.endsWith('.cjs'))
428+
const dtsFiles = files.filter(f => f.endsWith('.d.ts') || f.endsWith('.d.mts'))
429+
expect(jsFiles.length).toBe(0)
430+
expect(dtsFiles.length).toBeGreaterThan(0)
431+
},
432+
})
433+
})
434+
})
368435
})

0 commit comments

Comments
 (0)