Skip to content

Commit 81e03ff

Browse files
committed
feat: support type-only export
closes #24
1 parent 587c95d commit 81e03ff

File tree

12 files changed

+98
-13
lines changed

12 files changed

+98
-13
lines changed

src/fake-js.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import _generate from '@babel/generator'
22
import { parse } from '@babel/parser'
33
import * as t from '@babel/types'
4-
import { isDeclarationType, isTypeOf } from 'ast-kit'
4+
import { isDeclarationType, isTypeOf, resolveString } from 'ast-kit'
55
import { walk } from 'estree-walker'
66
import {
77
filename_dts_to,
@@ -45,6 +45,7 @@ export function createFakeJsPlugin({
4545
const identifierMap: Record<string, number> = Object.create(null)
4646
const symbolMap = new Map<number /* symbol id */, SymbolInfo>()
4747
const commentsMap = new Map<string /* filename */, t.Comment[]>()
48+
const typeOnlyMap = new Map<string, string[]>()
4849

4950
return {
5051
name: 'rolldown-plugin-dts:fake-js',
@@ -99,6 +100,7 @@ export function createFakeJsPlugin({
99100
sourceType: 'module',
100101
})
101102
const { program, comments } = file
103+
const typeOnlyIds: string[] = []
102104

103105
if (comments) {
104106
const directives = collectReferenceDirectives(comments)
@@ -110,7 +112,7 @@ export function createFakeJsPlugin({
110112

111113
for (const [i, stmt] of program.body.entries()) {
112114
const setStmt = (node: t.Node) => (program.body[i] = node as any)
113-
if (rewriteImportExport(stmt, setStmt)) continue
115+
if (rewriteImportExport(stmt, setStmt, typeOnlyIds)) continue
114116

115117
const sideEffect =
116118
stmt.type === 'TSModuleDeclaration' && stmt.kind !== 'namespace'
@@ -231,6 +233,8 @@ export function createFakeJsPlugin({
231233
...appendStmts,
232234
]
233235

236+
typeOnlyMap.set(id, typeOnlyIds)
237+
234238
const result = generate(file, {
235239
comments: false,
236240
sourceMaps: sourcemap,
@@ -244,6 +248,12 @@ export function createFakeJsPlugin({
244248
return
245249
}
246250

251+
const typeOnlyIds: string[] = []
252+
for (const module of chunk.moduleIds) {
253+
const ids = typeOnlyMap.get(module)
254+
if (ids) typeOnlyIds.push(...ids)
255+
}
256+
247257
const file = parse(code, {
248258
sourceType: 'module',
249259
})
@@ -254,7 +264,7 @@ export function createFakeJsPlugin({
254264
program.body = program.body
255265
.map((node) => {
256266
if (isHelperImport(node)) return null
257-
if (patchImportSource(node)) return node
267+
if (patchImportExport(node, typeOnlyIds)) return node
258268
if (node.type !== 'VariableDeclaration') return node
259269

260270
const [decl] = node.declarations
@@ -513,18 +523,31 @@ function isHelperImport(node: t.Node) {
513523
}
514524

515525
// patch `.d.ts` suffix in import source to `.js`
516-
function patchImportSource(node: t.Node) {
526+
function patchImportExport(node: t.Node, typeOnlyIds: string[]) {
517527
if (
518528
isTypeOf(node, [
519529
'ImportDeclaration',
520530
'ExportAllDeclaration',
521531
'ExportNamedDeclaration',
522-
]) &&
523-
node.source?.value &&
524-
RE_DTS.test(node.source.value)
532+
])
525533
) {
526-
node.source.value = filename_dts_to(node.source.value, 'js')
527-
return true
534+
if (typeOnlyIds.length && node.type === 'ExportNamedDeclaration') {
535+
for (const spec of node.specifiers) {
536+
const name = resolveString(spec.exported)
537+
if (typeOnlyIds.includes(name)) {
538+
if (spec.type === 'ExportSpecifier') {
539+
spec.exportKind = 'type'
540+
} else {
541+
node.exportKind = 'type'
542+
}
543+
}
544+
}
545+
}
546+
547+
if (node.source?.value && RE_DTS.test(node.source.value)) {
548+
node.source.value = filename_dts_to(node.source.value, 'js')
549+
return true
550+
}
528551
}
529552
}
530553

@@ -595,13 +618,14 @@ function patchTsNamespace(nodes: t.Statement[]) {
595618
// - import { type ... } from '...'
596619
// - export type { ... }
597620
// - export { type ... }
598-
// - export type * as '...'
621+
// - export type * as x '...'
599622
// - import Foo = require("./bar")
600623
// - export = Foo
601624
// - export default x
602625
function rewriteImportExport(
603626
node: t.Node,
604627
set: (node: t.Node) => void,
628+
typeOnlyIds: string[],
605629
): node is
606630
| t.ImportDeclaration
607631
| t.ExportAllDeclaration
@@ -611,6 +635,22 @@ function rewriteImportExport(
611635
(node.type === 'ExportNamedDeclaration' && !node.declaration)
612636
) {
613637
for (const specifier of node.specifiers) {
638+
if (
639+
('exportKind' in specifier && specifier.exportKind === 'type') ||
640+
('exportKind' in node && node.exportKind === 'type')
641+
) {
642+
typeOnlyIds.push(
643+
resolveString(
644+
(
645+
specifier as
646+
| t.ExportSpecifier
647+
| t.ExportDefaultSpecifier
648+
| t.ExportNamespaceSpecifier
649+
).exported,
650+
),
651+
)
652+
}
653+
614654
if (specifier.type === 'ImportSpecifier') {
615655
specifier.importKind = 'value'
616656
} else if (specifier.type === 'ExportSpecifier') {

tests/__snapshots__/index.test.ts.snap

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ exports[`tree-shaking 1`] = `
128128
129129
type A = string;
130130
//#endregion
131-
export { A };
131+
export { type A };
132132
// index.js
133133
"
134134
`;
@@ -152,3 +152,20 @@ function createComponent() {
152152
//#endregion
153153
export { createComponent };"
154154
`;
155+
156+
exports[`type-only export 1`] = `
157+
"// index.d.ts
158+
//#endregion
159+
//#region tests/fixtures/type-only-export/mod.d.ts
160+
declare class A {}
161+
declare class B {}
162+
declare namespace namespace_d_exports {
163+
export { ns };
164+
}
165+
declare const ns = 42;
166+
//#endregion
167+
//#region tests/fixtures/type-only-export/all.d.ts
168+
declare const all = 42;
169+
//#endregion
170+
export { A as RuntimeA, type A as TypeA, type B as TypeB, A as TypeC, all, type namespace_d_exports as ns };"
171+
`;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const all = 42
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './mod'
2+
export type * as ns from './namespace'
3+
export type * from './all'
4+
import { type RuntimeA } from './mod'
5+
export { RuntimeA as TypeC }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class A {}
2+
class B {}
3+
4+
export { type A as TypeA }
5+
export type { B as TypeB }
6+
export { A as RuntimeA }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const ns = 42

tests/index.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,11 @@ test('same-name output', async () => {
136136
)
137137
expect(chunks.every((chunk) => chunk.fileName.endsWith('.d.ts'))).toBe(true)
138138
})
139+
140+
test('type-only export', async () => {
141+
const { snapshot } = await rolldownBuild(
142+
[path.resolve(dirname, 'fixtures/type-only-export/index.ts')],
143+
[dts({ emitDtsOnly: true })],
144+
)
145+
expect(snapshot).toMatchSnapshot()
146+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export declare const foo: number
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type * from './all'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// index.d.ts
2+
//#region tests/rollup-plugin-dts/export-all-as-type/all.d.ts
3+
declare const foo: number;
4+
//#endregion
5+
export { foo };

0 commit comments

Comments
 (0)