diff --git a/README.md b/README.md index a471e33..60697a9 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ export interface Options { /** Only for typescript transformer */ transformOptions?: TranspileOptions ignoreErrors?: boolean + + /** An extra directory layer for output files. */ + extraOutdir?: string } ``` diff --git a/src/core/options.ts b/src/core/options.ts index a1e6df8..f9950e1 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -6,6 +6,8 @@ export type Options = { exclude?: FilterPattern enforce?: 'pre' | 'post' | undefined ignoreErrors?: boolean + /** An extra directory layer for output files. */ + extraOutdir?: string } & ( | { /** @@ -28,7 +30,7 @@ type Overwrite = Pick> & U export type OptionsResolved = Overwrite< Required, - Pick + Pick > export function resolveOptions(options: Options): OptionsResolved { @@ -38,5 +40,6 @@ export function resolveOptions(options: Options): OptionsResolved { enforce: 'enforce' in options ? options.enforce : 'pre', transformer: options.transformer || 'oxc', ignoreErrors: options.ignoreErrors || false, + extraOutdir: options.extraOutdir, } } diff --git a/src/index.ts b/src/index.ts index 90a766f..f32b973 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { tsTransform, type TransformResult, } from './core/transformer' +import type { PluginBuild } from 'esbuild' import type { Plugin, PluginContext } from 'rollup' export type { Options } @@ -43,11 +44,15 @@ export const IsolatedDecl: UnpluginInstance = return this.error('entryFileNames must be a string') } - const entryFileNames = outputOptions.entryFileNames.replace( + let entryFileNames = outputOptions.entryFileNames.replace( /\.(.)?[jt]s$/, (_, s) => `.d.${s || ''}ts`, ) + if (options.extraOutdir) { + entryFileNames = path.join(options.extraOutdir, entryFileNames) + } + for (const [filename, source] of Object.entries(outputFiles)) { this.emitFile({ type: 'asset', @@ -69,66 +74,11 @@ export const IsolatedDecl: UnpluginInstance = }, transform(code, id): Promise { - return transform.call(this, code, id) + return transform(this, code, id) }, esbuild: { - setup(build) { - build.onEnd(async (result) => { - const esbuildOptions = build.initialOptions - - const entries = esbuildOptions.entryPoints - if ( - !( - entries && - Array.isArray(entries) && - entries.every((entry) => typeof entry === 'string') - ) - ) - throw new Error('unsupported entryPoints, must be an string[]') - - const outBase = lowestCommonAncestor(...entries) - const jsExt = esbuildOptions.outExtension?.['.js'] - let outExt: string - switch (jsExt) { - case '.cjs': - outExt = 'cts' - break - case '.mjs': - outExt = 'mts' - break - default: - outExt = 'ts' - break - } - - const write = build.initialOptions.write ?? true - if (write) { - if (!build.initialOptions.outdir) - throw new Error('outdir is required when write is true') - } else { - result.outputFiles ||= [] - } - - const textEncoder = new TextEncoder() - for (const [filename, source] of Object.entries(outputFiles)) { - const outDir = build.initialOptions.outdir - const outFile = `${path.relative(outBase, filename)}.d.${outExt}` - const filePath = outDir ? path.resolve(outDir, outFile) : outFile - if (write) { - await mkdir(path.dirname(filePath), { recursive: true }) - await writeFile(filePath, source) - } else { - result.outputFiles!.push({ - path: filePath, - contents: textEncoder.encode(source), - hash: '', - text: source, - }) - } - } - }) - }, + setup: esbuildSetup, }, rollup, rolldown: rollup as any, @@ -140,7 +90,7 @@ export const IsolatedDecl: UnpluginInstance = } async function transform( - this: UnpluginBuildContext & UnpluginContext, + context: UnpluginBuildContext & UnpluginContext, code: string, id: string, ): Promise { @@ -162,9 +112,9 @@ export const IsolatedDecl: UnpluginInstance = const { code: sourceText, errors } = result if (errors.length) { if (options.ignoreErrors) { - this.warn(errors[0]) + context.warn(errors[0]) } else { - this.error(errors[0]) + context.error(errors[0]) return } } @@ -187,21 +137,8 @@ export const IsolatedDecl: UnpluginInstance = ) }) - const resolve = async (id: string, importer: string) => { - const context = this.getNativeBuildContext?.() - if (context?.framework === 'esbuild') { - return ( - await context.build.resolve(id, { - importer, - resolveDir: path.dirname(importer), - kind: 'import-statement', - }) - ).path - } - return (await (this as any as PluginContext).resolve(id, importer))?.id - } for (const i of typeImports) { - const resolved = await resolve(i.source.value, id) + const resolved = await resolve(context, i.source.value, id) if (resolved && filter(resolved) && !outputFiles[stripExt(resolved)]) { let source: string try { @@ -209,12 +146,90 @@ export const IsolatedDecl: UnpluginInstance = } catch { continue } - await transform.call(this, source, resolved) + await transform(context, source, resolved) } } } + + function esbuildSetup(build: PluginBuild) { + build.onEnd(async (result) => { + const esbuildOptions = build.initialOptions + + const entries = esbuildOptions.entryPoints + if ( + !( + entries && + Array.isArray(entries) && + entries.every((entry) => typeof entry === 'string') + ) + ) + throw new Error('unsupported entryPoints, must be an string[]') + + const outBase = lowestCommonAncestor(...entries) + const jsExt = esbuildOptions.outExtension?.['.js'] + let outExt: string + switch (jsExt) { + case '.cjs': + outExt = 'cts' + break + case '.mjs': + outExt = 'mts' + break + default: + outExt = 'ts' + break + } + + const write = build.initialOptions.write ?? true + if (write) { + if (!build.initialOptions.outdir) + throw new Error('outdir is required when write is true') + } else { + result.outputFiles ||= [] + } + + const textEncoder = new TextEncoder() + for (const [filename, source] of Object.entries(outputFiles)) { + const outDir = build.initialOptions.outdir + let outFile = `${path.relative(outBase, filename)}.d.${outExt}` + if (options.extraOutdir) { + outFile = path.join(options.extraOutdir, outFile) + } + const filePath = outDir ? path.resolve(outDir, outFile) : outFile + if (write) { + await mkdir(path.dirname(filePath), { recursive: true }) + await writeFile(filePath, source) + } else { + result.outputFiles!.push({ + path: filePath, + contents: textEncoder.encode(source), + hash: '', + text: source, + }) + } + } + }) + } }) +const resolve = async ( + context: UnpluginBuildContext, + id: string, + importer: string, +) => { + const nativeContext = context.getNativeBuildContext?.() + if (nativeContext?.framework === 'esbuild') { + return ( + await nativeContext.build.resolve(id, { + importer, + resolveDir: path.dirname(importer), + kind: 'import-statement', + }) + ).path + } + return (await (context as PluginContext).resolve(id, importer))?.id +} + function stripExt(filename: string) { return filename.replace(/\.(.?)[jt]s$/, '') } diff --git a/tests/__snapshots__/esbuild.test.ts.snap b/tests/__snapshots__/esbuild.test.ts.snap index d1058a7..704f611 100644 --- a/tests/__snapshots__/esbuild.test.ts.snap +++ b/tests/__snapshots__/esbuild.test.ts.snap @@ -13,16 +13,16 @@ export { num }; ", - "// main.d.ts + "// temp/main.d.ts import { type Num } from "./types"; export type Str = string; export declare function hello(s: Str): Str; export declare let num: Num; ", - "// types.d.ts + "// temp/types.d.ts export type Num = number; ", - "// types2.d.ts + "// temp/types2.d.ts export type Num2 = number; ", ] @@ -34,16 +34,6 @@ exports[`esbuild > write mode 1`] = ` export type Str = string; export declare function hello(s: Str): Str; export declare let num: Num; -", - "// tests/fixtures/main.ts -function hello(s) { - return "hello" + s; -} -var num = 1; -export { - hello, - num -}; ", "export type Num = number; ", diff --git a/tests/__snapshots__/rolldown.test.ts.snap b/tests/__snapshots__/rolldown.test.ts.snap index 1831b23..581a999 100644 --- a/tests/__snapshots__/rolldown.test.ts.snap +++ b/tests/__snapshots__/rolldown.test.ts.snap @@ -2,12 +2,6 @@ exports[`rolldown 1`] = ` [ - "// main.d.ts -import { type Num } from "./types"; -export type Str = string; -export declare function hello(s: Str): Str; -export declare let num: Num; -", "// main.js //#region tests/fixtures/main.ts @@ -18,10 +12,16 @@ let num = 1; //#endregion export { hello, num };", - "// types.d.ts + "// temp/main.d.ts +import { type Num } from "./types"; +export type Str = string; +export declare function hello(s: Str): Str; +export declare let num: Num; +", + "// temp/types.d.ts export type Num = number; ", - "// types2.d.ts + "// temp/types2.d.ts export type Num2 = number; ", ] diff --git a/tests/__snapshots__/rollup.test.ts.snap b/tests/__snapshots__/rollup.test.ts.snap index deaab19..bf254dc 100644 --- a/tests/__snapshots__/rollup.test.ts.snap +++ b/tests/__snapshots__/rollup.test.ts.snap @@ -10,16 +10,16 @@ let num = 1; export { hello, num }; ", - "// main.d.ts + "// temp/main.d.ts import { type Num } from "./types"; export type Str = string; export declare function hello(s: Str): Str; export declare let num: Num; ", - "// types.d.ts + "// temp/types.d.ts export type Num = number; ", - "// types2.d.ts + "// temp/types2.d.ts export type Num2 = number; ", ] diff --git a/tests/esbuild.test.ts b/tests/esbuild.test.ts index fa20f9a..18f4068 100644 --- a/tests/esbuild.test.ts +++ b/tests/esbuild.test.ts @@ -12,7 +12,7 @@ describe('esbuild', () => { const dist = path.resolve(__dirname, 'temp') await build({ entryPoints: [input], - plugins: [UnpluginIsolatedDecl()], + plugins: [UnpluginIsolatedDecl({ extraOutdir: 'temp' })], logLevel: 'silent', bundle: true, external: Object.keys(dependencies), @@ -20,11 +20,13 @@ describe('esbuild', () => { outdir: dist, format: 'esm', }) + + const outDir = path.resolve(dist, 'temp') await expect( Promise.all( - (await readdir(dist)) + (await readdir(outDir)) .sort() - .map((file) => readFile(path.resolve(dist, file), 'utf8')), + .map((file) => readFile(path.resolve(outDir, file), 'utf8')), ), ).resolves.toMatchSnapshot() }) @@ -32,7 +34,7 @@ describe('esbuild', () => { test('generate mode', async () => { const { outputFiles } = await build({ entryPoints: [input], - plugins: [UnpluginIsolatedDecl()], + plugins: [UnpluginIsolatedDecl({ extraOutdir: 'temp' })], logLevel: 'silent', bundle: true, external: Object.keys(dependencies), diff --git a/tests/rolldown.test.ts b/tests/rolldown.test.ts index c374333..96be20c 100644 --- a/tests/rolldown.test.ts +++ b/tests/rolldown.test.ts @@ -9,7 +9,7 @@ test('rolldown', async () => { const bundle = await rolldown({ input, - plugins: [UnpluginIsolatedDecl()], + plugins: [UnpluginIsolatedDecl({ extraOutdir: 'temp' })], logLevel: 'silent', }) diff --git a/tests/rollup.test.ts b/tests/rollup.test.ts index 6918594..d240509 100644 --- a/tests/rollup.test.ts +++ b/tests/rollup.test.ts @@ -10,7 +10,12 @@ test('rollup', async () => { const bundle = await rollup({ input, - plugins: [UnpluginIsolatedDecl(), esbuild()], + plugins: [ + UnpluginIsolatedDecl({ + extraOutdir: 'temp', + }), + esbuild(), + ], logLevel: 'silent', })