Skip to content

Commit 9ecafc8

Browse files
committed
fix: only-type imports
1 parent 06880e0 commit 9ecafc8

File tree

11 files changed

+366
-93
lines changed

11 files changed

+366
-93
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
},
9292
"dependencies": {
9393
"@rollup/pluginutils": "^5.1.0",
94+
"oxc-parser": "^0.25.0",
9495
"oxc-transform": "^0.24.3",
9596
"unplugin": "^1.12.2"
9697
},
@@ -104,7 +105,9 @@
104105
"eslint": "^9.9.0",
105106
"fast-glob": "^3.3.2",
106107
"prettier": "^3.3.3",
108+
"rolldown": "nightly",
107109
"rollup": "^4.21.0",
110+
"rollup-plugin-esbuild": "^6.1.1",
108111
"tsdown": "^0.2.6",
109112
"tsx": "^4.17.0",
110113
"typescript": "^5.5.4",

pnpm-lock.yaml

Lines changed: 157 additions & 52 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1-
import { mkdir, writeFile } from 'node:fs/promises'
1+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
22
import path from 'node:path'
33
import { createFilter } from '@rollup/pluginutils'
4-
import { createUnplugin, type UnpluginInstance } from 'unplugin'
4+
import { parseAsync } from 'oxc-parser'
5+
import {
6+
createUnplugin,
7+
type UnpluginBuildContext,
8+
type UnpluginContext,
9+
type UnpluginInstance,
10+
} from 'unplugin'
511
import { resolveOptions, type Options } from './core/options'
612
import {
713
oxcTransform,
814
swcTransform,
915
tsTransform,
1016
type TransformResult,
1117
} from './core/transformer'
12-
import type { Plugin } from 'rollup'
18+
import type { Plugin, PluginContext } from 'rollup'
1319

1420
export type { Options }
1521

1622
export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
17-
createUnplugin((rawOptions = {}) => {
23+
createUnplugin((rawOptions = {}, meta) => {
1824
const options = resolveOptions(rawOptions)
1925
const filter = createFilter(options.include, options.exclude)
2026

@@ -61,32 +67,8 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
6167
return filter(id)
6268
},
6369

64-
async transform(code, id): Promise<undefined> {
65-
let result: TransformResult
66-
switch (options.transformer) {
67-
case 'oxc':
68-
result = oxcTransform(id, code)
69-
break
70-
case 'swc':
71-
result = await swcTransform(id, code)
72-
break
73-
case 'typescript':
74-
result = await tsTransform(
75-
id,
76-
code,
77-
(options as any).transformOptions,
78-
)
79-
}
80-
const { sourceText, errors } = result
81-
if (errors.length) {
82-
if (options.ignoreErrors) {
83-
this.warn(errors[0])
84-
} else {
85-
this.error(errors[0])
86-
return
87-
}
88-
}
89-
addOutput(id, sourceText)
70+
transform(code, id): Promise<undefined> {
71+
return transform.call(this, code, id)
9072
},
9173

9274
esbuild: {
@@ -155,6 +137,80 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
155137
...rollup,
156138
},
157139
}
140+
141+
async function transform(
142+
this: UnpluginBuildContext & UnpluginContext,
143+
code: string,
144+
id: string,
145+
): Promise<undefined> {
146+
let result: TransformResult
147+
switch (options.transformer) {
148+
case 'oxc':
149+
result = oxcTransform(id, code)
150+
break
151+
case 'swc':
152+
result = await swcTransform(id, code)
153+
break
154+
case 'typescript':
155+
result = await tsTransform(
156+
id,
157+
code,
158+
(options as any).transformOptions,
159+
)
160+
}
161+
const { sourceText, errors } = result
162+
if (errors.length) {
163+
if (options.ignoreErrors) {
164+
this.warn(errors[0])
165+
} else {
166+
this.error(errors[0])
167+
return
168+
}
169+
}
170+
addOutput(id, sourceText)
171+
172+
let program: any
173+
try {
174+
program = JSON.parse(
175+
(await parseAsync(code, { sourceFilename: id })).program,
176+
)
177+
} catch {
178+
return
179+
}
180+
const typeImports = program.body.filter((node: any) => {
181+
if (node.type !== 'ImportDeclaration') return false
182+
if (node.importKind === 'type') return true
183+
return (node.specifiers || []).every(
184+
(spec: any) =>
185+
spec.type === 'ImportSpecifier' && spec.importKind === 'type',
186+
)
187+
})
188+
189+
const resolve = async (id: string, importer: string) => {
190+
if (meta.framework === 'esbuild') {
191+
return (
192+
await meta.build!.resolve(id, {
193+
importer,
194+
resolveDir: path.dirname(importer),
195+
kind: 'import-statement',
196+
})
197+
).path
198+
}
199+
return (await (this as any as PluginContext).resolve(id, importer))?.id
200+
}
201+
for (const i of typeImports) {
202+
const resolved = await resolve(i.source.value, id)
203+
if (resolved) {
204+
let source: string
205+
try {
206+
source = await readFile(resolved, 'utf8')
207+
} catch {
208+
continue
209+
}
210+
transform.call(this, source, resolved)
211+
}
212+
}
213+
}
158214
})
159215

160216
function lowestCommonAncestor(...filepaths: string[]) {
Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`esbuild > generate mode 1`] = `
4-
"export type Str = string;
4+
[
5+
"// <stdout>
6+
// tests/fixtures/main.ts
7+
function hello(s) {
8+
return "hello" + s;
9+
}
10+
var num = 1;
11+
export {
12+
hello,
13+
num
14+
};
15+
",
16+
"// main.d.ts
17+
import { type Num } from "./types";
18+
export type Str = string;
519
export declare function hello(s: Str): Str;
6-
"
20+
export declare let num: Num;
21+
",
22+
"// types.d.ts
23+
export type Num = number;
24+
",
25+
]
726
`;
827
928
exports[`esbuild > write mode 1`] = `
10-
"export type Str = string;
29+
[
30+
"import { type Num } from "./types";
31+
export type Str = string;
1132
export declare function hello(s: Str): Str;
12-
"
33+
export declare let num: Num;
34+
",
35+
"// tests/fixtures/main.ts
36+
function hello(s) {
37+
return "hello" + s;
38+
}
39+
var num = 1;
40+
export {
41+
hello,
42+
num
43+
};
44+
",
45+
"export type Num = number;
46+
",
47+
]
1348
`;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`rolldown 1`] = `
4+
[
5+
"// main.d.ts
6+
import { type Num } from "./types";
7+
export type Str = string;
8+
export declare function hello(s: Str): Str;
9+
export declare let num: Num;
10+
",
11+
"// main.js
12+
13+
//#region tests/fixtures/main.ts
14+
function hello(s) {
15+
return "hello" + s;
16+
}
17+
let num = 1;
18+
19+
//#endregion
20+
export { hello, num };",
21+
"// types.d.ts
22+
export type Num = number;
23+
",
24+
]
25+
`;

tests/__snapshots__/rollup.test.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33
exports[`rollup 1`] = `
44
[
55
"// main.js
6+
function hello(s) {
7+
return "hello" + s;
8+
}
9+
let num = 1;
610
11+
export { hello, num };
712
",
813
"// main.d.ts
14+
import { type Num } from "./types";
915
export type Str = string;
1016
export declare function hello(s: Str): Str;
17+
export declare let num: Num;
18+
",
19+
"// types.d.ts
20+
export type Num = number;
1121
",
1222
]
1323
`;

tests/esbuild.test.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFile } from 'node:fs/promises'
1+
import { readdir, readFile } from 'node:fs/promises'
22
import path from 'node:path'
33
import { build } from 'esbuild'
44
import { describe, expect, test } from 'vitest'
@@ -18,11 +18,15 @@ describe('esbuild', () => {
1818
external: Object.keys(dependencies),
1919
platform: 'node',
2020
outdir: dist,
21+
format: 'esm',
2122
})
22-
23-
expect(
24-
await readFile(path.resolve(dist, 'main.d.ts'), 'utf8'),
25-
).toMatchSnapshot()
23+
await expect(
24+
Promise.all(
25+
(await readdir(dist))
26+
.sort()
27+
.map((file) => readFile(path.resolve(dist, file), 'utf8')),
28+
),
29+
).resolves.toMatchSnapshot()
2630
})
2731

2832
test('generate mode', async () => {
@@ -34,8 +38,11 @@ describe('esbuild', () => {
3438
external: Object.keys(dependencies),
3539
platform: 'node',
3640
write: false,
41+
format: 'esm',
3742
})
3843

39-
expect(outputFiles[1].text).toMatchSnapshot()
44+
expect(
45+
outputFiles.map((file) => `// ${file.path}\n${file.text}`),
46+
).toMatchSnapshot()
4047
})
4148
})

tests/fixtures/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { type Num } from './types'
12
export type Str = string
23

34
export function hello(s: Str): Str {
45
return 'hello' + s
56
}
7+
8+
export let num: Num = 1

tests/fixtures/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Num = number

tests/rolldown.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import path from 'node:path'
2+
import { rolldown } from 'rolldown'
3+
import { expect, test } from 'vitest'
4+
import UnpluginIsolatedDecl from '../src/rolldown'
5+
6+
test('rolldown', async () => {
7+
const input = path.resolve(__dirname, 'fixtures/main.ts')
8+
const dist = path.resolve(__dirname, 'temp')
9+
10+
const bundle = await rolldown({
11+
input,
12+
plugins: [UnpluginIsolatedDecl()],
13+
logLevel: 'silent',
14+
})
15+
16+
const result = await bundle.generate({
17+
dir: dist,
18+
})
19+
expect(
20+
result.output
21+
.sort((a, b) => a.fileName.localeCompare(b.fileName))
22+
.map(
23+
(asset) =>
24+
`// ${asset.fileName}\n${asset.type === 'chunk' ? asset.code : asset.source}`,
25+
),
26+
).toMatchSnapshot()
27+
})

0 commit comments

Comments
 (0)