Skip to content

Commit 1f3002e

Browse files
committed
feat: add extraOutdir option
1 parent bc56d70 commit 1f3002e

File tree

9 files changed

+125
-107
lines changed

9 files changed

+125
-107
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ export interface Options {
9090
/** Only for typescript transformer */
9191
transformOptions?: TranspileOptions
9292
ignoreErrors?: boolean
93+
94+
/** An extra directory layer for output files. */
95+
extraOutdir?: string
9396
}
9497
```
9598

src/core/options.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export type Options = {
66
exclude?: FilterPattern
77
enforce?: 'pre' | 'post' | undefined
88
ignoreErrors?: boolean
9+
/** An extra directory layer for output files. */
10+
extraOutdir?: string
911
} & (
1012
| {
1113
/**
@@ -28,7 +30,7 @@ type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U
2830

2931
export type OptionsResolved = Overwrite<
3032
Required<Options>,
31-
Pick<Options, 'enforce'>
33+
Pick<Options, 'enforce' | 'extraOutdir'>
3234
>
3335

3436
export function resolveOptions(options: Options): OptionsResolved {
@@ -38,5 +40,6 @@ export function resolveOptions(options: Options): OptionsResolved {
3840
enforce: 'enforce' in options ? options.enforce : 'pre',
3941
transformer: options.transformer || 'oxc',
4042
ignoreErrors: options.ignoreErrors || false,
43+
extraOutdir: options.extraOutdir,
4144
}
4245
}

src/index.ts

Lines changed: 91 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
tsTransform,
1616
type TransformResult,
1717
} from './core/transformer'
18+
import type { PluginBuild } from 'esbuild'
1819
import type { Plugin, PluginContext } from 'rollup'
1920

2021
export type { Options }
@@ -43,11 +44,15 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
4344
return this.error('entryFileNames must be a string')
4445
}
4546

46-
const entryFileNames = outputOptions.entryFileNames.replace(
47+
let entryFileNames = outputOptions.entryFileNames.replace(
4748
/\.(.)?[jt]s$/,
4849
(_, s) => `.d.${s || ''}ts`,
4950
)
5051

52+
if (options.extraOutdir) {
53+
entryFileNames = path.join(options.extraOutdir, entryFileNames)
54+
}
55+
5156
for (const [filename, source] of Object.entries(outputFiles)) {
5257
this.emitFile({
5358
type: 'asset',
@@ -69,66 +74,11 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
6974
},
7075

7176
transform(code, id): Promise<undefined> {
72-
return transform.call(this, code, id)
77+
return transform(this, code, id)
7378
},
7479

7580
esbuild: {
76-
setup(build) {
77-
build.onEnd(async (result) => {
78-
const esbuildOptions = build.initialOptions
79-
80-
const entries = esbuildOptions.entryPoints
81-
if (
82-
!(
83-
entries &&
84-
Array.isArray(entries) &&
85-
entries.every((entry) => typeof entry === 'string')
86-
)
87-
)
88-
throw new Error('unsupported entryPoints, must be an string[]')
89-
90-
const outBase = lowestCommonAncestor(...entries)
91-
const jsExt = esbuildOptions.outExtension?.['.js']
92-
let outExt: string
93-
switch (jsExt) {
94-
case '.cjs':
95-
outExt = 'cts'
96-
break
97-
case '.mjs':
98-
outExt = 'mts'
99-
break
100-
default:
101-
outExt = 'ts'
102-
break
103-
}
104-
105-
const write = build.initialOptions.write ?? true
106-
if (write) {
107-
if (!build.initialOptions.outdir)
108-
throw new Error('outdir is required when write is true')
109-
} else {
110-
result.outputFiles ||= []
111-
}
112-
113-
const textEncoder = new TextEncoder()
114-
for (const [filename, source] of Object.entries(outputFiles)) {
115-
const outDir = build.initialOptions.outdir
116-
const outFile = `${path.relative(outBase, filename)}.d.${outExt}`
117-
const filePath = outDir ? path.resolve(outDir, outFile) : outFile
118-
if (write) {
119-
await mkdir(path.dirname(filePath), { recursive: true })
120-
await writeFile(filePath, source)
121-
} else {
122-
result.outputFiles!.push({
123-
path: filePath,
124-
contents: textEncoder.encode(source),
125-
hash: '',
126-
text: source,
127-
})
128-
}
129-
}
130-
})
131-
},
81+
setup: esbuildSetup,
13282
},
13383
rollup,
13484
rolldown: rollup as any,
@@ -140,7 +90,7 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
14090
}
14191

14292
async function transform(
143-
this: UnpluginBuildContext & UnpluginContext,
93+
context: UnpluginBuildContext & UnpluginContext,
14494
code: string,
14595
id: string,
14696
): Promise<undefined> {
@@ -162,9 +112,9 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
162112
const { code: sourceText, errors } = result
163113
if (errors.length) {
164114
if (options.ignoreErrors) {
165-
this.warn(errors[0])
115+
context.warn(errors[0])
166116
} else {
167-
this.error(errors[0])
117+
context.error(errors[0])
168118
return
169119
}
170120
}
@@ -187,34 +137,99 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
187137
)
188138
})
189139

190-
const resolve = async (id: string, importer: string) => {
191-
const context = this.getNativeBuildContext?.()
192-
if (context?.framework === 'esbuild') {
193-
return (
194-
await context.build.resolve(id, {
195-
importer,
196-
resolveDir: path.dirname(importer),
197-
kind: 'import-statement',
198-
})
199-
).path
200-
}
201-
return (await (this as any as PluginContext).resolve(id, importer))?.id
202-
}
203140
for (const i of typeImports) {
204-
const resolved = await resolve(i.source.value, id)
141+
const resolved = await resolve(context, i.source.value, id)
205142
if (resolved && filter(resolved) && !outputFiles[stripExt(resolved)]) {
206143
let source: string
207144
try {
208145
source = await readFile(resolved, 'utf8')
209146
} catch {
210147
continue
211148
}
212-
await transform.call(this, source, resolved)
149+
await transform(context, source, resolved)
213150
}
214151
}
215152
}
153+
154+
function esbuildSetup(build: PluginBuild) {
155+
build.onEnd(async (result) => {
156+
const esbuildOptions = build.initialOptions
157+
158+
const entries = esbuildOptions.entryPoints
159+
if (
160+
!(
161+
entries &&
162+
Array.isArray(entries) &&
163+
entries.every((entry) => typeof entry === 'string')
164+
)
165+
)
166+
throw new Error('unsupported entryPoints, must be an string[]')
167+
168+
const outBase = lowestCommonAncestor(...entries)
169+
const jsExt = esbuildOptions.outExtension?.['.js']
170+
let outExt: string
171+
switch (jsExt) {
172+
case '.cjs':
173+
outExt = 'cts'
174+
break
175+
case '.mjs':
176+
outExt = 'mts'
177+
break
178+
default:
179+
outExt = 'ts'
180+
break
181+
}
182+
183+
const write = build.initialOptions.write ?? true
184+
if (write) {
185+
if (!build.initialOptions.outdir)
186+
throw new Error('outdir is required when write is true')
187+
} else {
188+
result.outputFiles ||= []
189+
}
190+
191+
const textEncoder = new TextEncoder()
192+
for (const [filename, source] of Object.entries(outputFiles)) {
193+
const outDir = build.initialOptions.outdir
194+
let outFile = `${path.relative(outBase, filename)}.d.${outExt}`
195+
if (options.extraOutdir) {
196+
outFile = path.join(options.extraOutdir, outFile)
197+
}
198+
const filePath = outDir ? path.resolve(outDir, outFile) : outFile
199+
if (write) {
200+
await mkdir(path.dirname(filePath), { recursive: true })
201+
await writeFile(filePath, source)
202+
} else {
203+
result.outputFiles!.push({
204+
path: filePath,
205+
contents: textEncoder.encode(source),
206+
hash: '',
207+
text: source,
208+
})
209+
}
210+
}
211+
})
212+
}
216213
})
217214

215+
const resolve = async (
216+
context: UnpluginBuildContext,
217+
id: string,
218+
importer: string,
219+
) => {
220+
const nativeContext = context.getNativeBuildContext?.()
221+
if (nativeContext?.framework === 'esbuild') {
222+
return (
223+
await nativeContext.build.resolve(id, {
224+
importer,
225+
resolveDir: path.dirname(importer),
226+
kind: 'import-statement',
227+
})
228+
).path
229+
}
230+
return (await (context as PluginContext).resolve(id, importer))?.id
231+
}
232+
218233
function stripExt(filename: string) {
219234
return filename.replace(/\.(.?)[jt]s$/, '')
220235
}

tests/__snapshots__/esbuild.test.ts.snap

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ export {
1313
num
1414
};
1515
",
16-
"// main.d.ts
16+
"// temp/main.d.ts
1717
import { type Num } from "./types";
1818
export type Str = string;
1919
export declare function hello(s: Str): Str;
2020
export declare let num: Num;
2121
",
22-
"// types.d.ts
22+
"// temp/types.d.ts
2323
export type Num = number;
2424
",
25-
"// types2.d.ts
25+
"// temp/types2.d.ts
2626
export type Num2 = number;
2727
",
2828
]
@@ -34,16 +34,6 @@ exports[`esbuild > write mode 1`] = `
3434
export type Str = string;
3535
export declare function hello(s: Str): Str;
3636
export declare let num: Num;
37-
",
38-
"// tests/fixtures/main.ts
39-
function hello(s) {
40-
return "hello" + s;
41-
}
42-
var num = 1;
43-
export {
44-
hello,
45-
num
46-
};
4737
",
4838
"export type Num = number;
4939
",

tests/__snapshots__/rolldown.test.ts.snap

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22

33
exports[`rolldown 1`] = `
44
[
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-
",
115
"// main.js
126
137
//#region tests/fixtures/main.ts
@@ -18,10 +12,16 @@ let num = 1;
1812
1913
//#endregion
2014
export { hello, num };",
21-
"// types.d.ts
15+
"// temp/main.d.ts
16+
import { type Num } from "./types";
17+
export type Str = string;
18+
export declare function hello(s: Str): Str;
19+
export declare let num: Num;
20+
",
21+
"// temp/types.d.ts
2222
export type Num = number;
2323
",
24-
"// types2.d.ts
24+
"// temp/types2.d.ts
2525
export type Num2 = number;
2626
",
2727
]

tests/__snapshots__/rollup.test.ts.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ let num = 1;
1010
1111
export { hello, num };
1212
",
13-
"// main.d.ts
13+
"// temp/main.d.ts
1414
import { type Num } from "./types";
1515
export type Str = string;
1616
export declare function hello(s: Str): Str;
1717
export declare let num: Num;
1818
",
19-
"// types.d.ts
19+
"// temp/types.d.ts
2020
export type Num = number;
2121
",
22-
"// types2.d.ts
22+
"// temp/types2.d.ts
2323
export type Num2 = number;
2424
",
2525
]

tests/esbuild.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,29 @@ describe('esbuild', () => {
1212
const dist = path.resolve(__dirname, 'temp')
1313
await build({
1414
entryPoints: [input],
15-
plugins: [UnpluginIsolatedDecl()],
15+
plugins: [UnpluginIsolatedDecl({ extraOutdir: 'temp' })],
1616
logLevel: 'silent',
1717
bundle: true,
1818
external: Object.keys(dependencies),
1919
platform: 'node',
2020
outdir: dist,
2121
format: 'esm',
2222
})
23+
24+
const outDir = path.resolve(dist, 'temp')
2325
await expect(
2426
Promise.all(
25-
(await readdir(dist))
27+
(await readdir(outDir))
2628
.sort()
27-
.map((file) => readFile(path.resolve(dist, file), 'utf8')),
29+
.map((file) => readFile(path.resolve(outDir, file), 'utf8')),
2830
),
2931
).resolves.toMatchSnapshot()
3032
})
3133

3234
test('generate mode', async () => {
3335
const { outputFiles } = await build({
3436
entryPoints: [input],
35-
plugins: [UnpluginIsolatedDecl()],
37+
plugins: [UnpluginIsolatedDecl({ extraOutdir: 'temp' })],
3638
logLevel: 'silent',
3739
bundle: true,
3840
external: Object.keys(dependencies),

tests/rolldown.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ test('rolldown', async () => {
99

1010
const bundle = await rolldown({
1111
input,
12-
plugins: [UnpluginIsolatedDecl()],
12+
plugins: [UnpluginIsolatedDecl({ extraOutdir: 'temp' })],
1313
logLevel: 'silent',
1414
})
1515

0 commit comments

Comments
 (0)