diff --git a/src/loader.ts b/src/loader.ts index 29b0545c..5d6e4535 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -24,6 +24,7 @@ export interface OutputFile { extension?: string; contents?: string; declaration?: boolean; + errors?: Error[]; raw?: boolean; skip?: boolean; } diff --git a/src/make.ts b/src/make.ts index 0af298f4..6c4da9f2 100644 --- a/src/make.ts +++ b/src/make.ts @@ -10,7 +10,11 @@ import { OutputFile, Loader, } from "./loader"; -import { getDeclarations, normalizeCompilerOptions } from "./utils/dts"; +import { + DeclarationOutput, + getDeclarations, + normalizeCompilerOptions, +} from "./utils/dts"; import { getVueDeclarations } from "./utils/vue-dts"; import { LoaderName } from "./loaders"; import { glob, type GlobOptions } from "tinyglobby"; @@ -83,17 +87,17 @@ export async function mkdist( ); } options.typescript.compilerOptions = defu( - { noEmit: false }, + { noEmit: false } satisfies TSConfig["compilerOptions"], options.typescript.compilerOptions, { allowJs: true, declaration: true, - incremental: true, skipLibCheck: true, strictNullChecks: true, emitDeclarationOnly: true, + allowImportingTsExtensions: true, allowNonTsExtensions: true, - }, + } satisfies TSConfig["compilerOptions"], ); // Create loader @@ -120,12 +124,16 @@ export async function mkdist( const dtsOutputs = outputs.filter((o) => o.declaration && !o.skip); if (dtsOutputs.length > 0) { const vfs = new Map(dtsOutputs.map((o) => [o.srcPath, o.contents || ""])); - const declarations = Object.create(null); + const declarations: DeclarationOutput = Object.create(null); for (const loader of [getVueDeclarations, getDeclarations]) { Object.assign(declarations, await loader(vfs, options)); } for (const output of dtsOutputs) { - output.contents = declarations[output.srcPath] || ""; + const result = declarations[output.srcPath]; + output.contents = result?.contents || ""; + if (result.errors) { + output.errors = result.errors; + } } } @@ -187,6 +195,7 @@ export async function mkdist( // Write outputs const writtenFiles: string[] = []; + const errors: Array<{ filename: string; errors: TypeError[] }> = []; await Promise.all( outputs .filter((o) => !o.skip) @@ -197,10 +206,15 @@ export async function mkdist( ? copyFileWithStream(output.srcPath, outFile) : fsp.writeFile(outFile, output.contents, "utf8")); writtenFiles.push(outFile); + + if (output.errors) { + errors.push({ filename: outFile, errors: output.errors }); + } }), ); return { + errors, writtenFiles, }; } diff --git a/src/utils/dts.ts b/src/utils/dts.ts index 279cac14..7aeb57a6 100644 --- a/src/utils/dts.ts +++ b/src/utils/dts.ts @@ -3,6 +3,7 @@ import { findStaticImports, findExports, findTypeExports } from "mlly"; import { resolve } from "pathe"; import type { TSConfig } from "pkg-types"; import type { MkdistOptions } from "../make"; +import type { CompilerHost, EmitResult } from "typescript"; export async function normalizeCompilerOptions( _options: TSConfig["compilerOptions"], @@ -11,10 +12,15 @@ export async function normalizeCompilerOptions( return ts.convertCompilerOptionsFromJson(_options, process.cwd()).options; } +export type DeclarationOutput = Record< + string, + { contents: string; errors?: Error[] } +>; + export async function getDeclarations( vfs: Map, opts?: MkdistOptions, -) { +): Promise { const ts = await import("typescript").then((r) => r.default || r); const inputFiles = [...vfs.keys()]; @@ -38,11 +44,10 @@ export async function getDeclarations( tsHost, ); const result = program.emit(); - if (result.diagnostics?.length) { - console.error(ts.formatDiagnostics(result.diagnostics, tsHost)); - } + const output = extractDeclarations(vfs, inputFiles, opts); + augmentWithDiagnostics(result, output, tsHost, ts); - return extractDeclarations(vfs, inputFiles, opts); + return output; } const JS_EXT_RE = /\.(m|c)?(ts|js)$/; @@ -53,8 +58,8 @@ export function extractDeclarations( vfs: Map, inputFiles: string[], opts?: MkdistOptions, -) { - const output: Record = {}; +): DeclarationOutput { + const output: DeclarationOutput = {}; for (const filename of inputFiles) { const dtsFilename = filename.replace(JSX_EXT_RE, ".d.$1ts"); @@ -88,10 +93,32 @@ export function extractDeclarations( ); } } - output[filename] = contents; + output[filename] = { contents }; vfs.delete(filename); } return output; } + +export function augmentWithDiagnostics( + result: EmitResult, + output: DeclarationOutput, + tsHost: CompilerHost, + ts: typeof import("typescript"), +) { + if (result.diagnostics?.length) { + for (const diagnostic of result.diagnostics) { + const filename = diagnostic.file?.fileName; + if (filename in output) { + output[filename].errors = output[filename].errors || []; + output[filename].errors.push( + new TypeError(ts.formatDiagnostics([diagnostic], tsHost), { + cause: diagnostic, + }), + ); + } + } + console.error(ts.formatDiagnostics(result.diagnostics, tsHost)); + } +} diff --git a/src/utils/vue-dts.ts b/src/utils/vue-dts.ts index ae2f987e..3c43db1c 100644 --- a/src/utils/vue-dts.ts +++ b/src/utils/vue-dts.ts @@ -1,17 +1,21 @@ import { createRequire } from "node:module"; -import { CompilerOptions, CreateProgramOptions } from "typescript"; +import { CreateProgramOptions } from "typescript"; import { readPackageJSON } from "pkg-types"; import { satisfies } from "semver"; import { normalize } from "pathe"; import { MkdistOptions } from "../make"; -import { extractDeclarations } from "./dts"; +import { + augmentWithDiagnostics, + DeclarationOutput, + extractDeclarations, +} from "./dts"; const require = createRequire(import.meta.url); export async function getVueDeclarations( vfs: Map, opts?: MkdistOptions, -) { +): Promise { const fileMapping = getFileMapping(vfs); const srcFiles = Object.keys(fileMapping); const originFiles = Object.values(fileMapping); @@ -28,26 +32,22 @@ export async function getVueDeclarations( } const { version } = pkgInfo; + let output: DeclarationOutput; switch (true) { case satisfies(version, "^1.8.27"): { - await emitVueTscV1(vfs, opts.typescript.compilerOptions, srcFiles); + output = await emitVueTscV1(vfs, srcFiles, originFiles, opts); break; } case satisfies(version, "~v2.0.0"): { - await emitVueTscV2(vfs, opts.typescript.compilerOptions, srcFiles); + output = await emitVueTscV2(vfs, srcFiles, originFiles, opts); break; } default: { - await emitVueTscLatest( - vfs, - opts.typescript.compilerOptions, - srcFiles, - opts.rootDir!, - ); + output = await emitVueTscLatest(vfs, srcFiles, originFiles, opts); } } - return extractDeclarations(vfs, originFiles, opts); + return output; } const SFC_EXT_RE = /\.vue\.[cm]?[jt]s$/; @@ -64,8 +64,9 @@ function getFileMapping(vfs: Map): Record { async function emitVueTscV1( vfs: Map, - compilerOptions: CompilerOptions, - srcFiles: string[], + inputFiles: string[], + originFiles: string[], + opts?: MkdistOptions, ) { const vueTsc: typeof import("vue-tsc1") = await import("vue-tsc") .then((r) => r.default || r) @@ -75,7 +76,7 @@ async function emitVueTscV1( const ts = require("typescript") as typeof import("typescript/lib/tsserverlibrary"); - const tsHost = ts.createCompilerHost(compilerOptions); + const tsHost = ts.createCompilerHost(opts.typescript.compilerOptions); const _tsSysWriteFile = ts.sys.writeFile; ts.sys.writeFile = (filename, content) => { @@ -91,15 +92,17 @@ async function emitVueTscV1( try { const program = vueTsc.createProgram({ - rootNames: srcFiles, - options: compilerOptions, + rootNames: inputFiles, + options: opts.typescript.compilerOptions, host: tsHost, }); const result = program.emit(); - if (result.diagnostics?.length) { - console.error(ts.formatDiagnostics(result.diagnostics, tsHost)); - } + const output = extractDeclarations(vfs, originFiles, opts); + + augmentWithDiagnostics(result, output, tsHost, ts); + + return output; } finally { ts.sys.writeFile = _tsSysWriteFile; ts.sys.readFile = _tsSysReadFile; @@ -108,8 +111,9 @@ async function emitVueTscV1( async function emitVueTscV2( vfs: Map, - compilerOptions: CompilerOptions, - srcFiles: string[], + inputFiles: string[], + originFiles: string[], + opts?: MkdistOptions, ) { const { resolve: resolveModule } = await import("mlly"); const ts: typeof import("typescript") = await import("typescript").then( @@ -124,7 +128,7 @@ async function emitVueTscV2( const volarTs: typeof import("@volar/typescript") = requireFromVueTsc("@volar/typescript"); - const tsHost = ts.createCompilerHost(compilerOptions); + const tsHost = ts.createCompilerHost(opts.typescript.compilerOptions); tsHost.writeFile = (filename, content) => { vfs.set(filename, vueTsc.removeEmitGlobalTypes(content)); }; @@ -140,8 +144,8 @@ async function emitVueTscV2( return vfs.has(filename) || _tsFileExist(filename); }; const programOptions: CreateProgramOptions = { - rootNames: srcFiles, - options: compilerOptions, + rootNames: inputFiles, + options: opts.typescript.compilerOptions, host: tsHost, }; const createProgram = volarTs.proxyCreateProgram( @@ -167,18 +171,22 @@ async function emitVueTscV2( return [vueLanguagePlugin]; }, ); + const program = createProgram(programOptions); + const result = program.emit(); - if (result.diagnostics?.length) { - console.error(ts.formatDiagnostics(result.diagnostics, tsHost)); - } + const output = extractDeclarations(vfs, originFiles, opts); + + augmentWithDiagnostics(result, output, tsHost, ts); + + return output; } async function emitVueTscLatest( vfs: Map, - compilerOptions: CompilerOptions, - srcFiles: string[], - rootDir: string, + inputFiles: string[], + originFiles: string[], + opts?: MkdistOptions, ) { const { resolve: resolveModule } = await import("mlly"); const ts: typeof import("typescript") = await import("typescript").then( @@ -190,7 +198,7 @@ async function emitVueTscLatest( const volarTs: typeof import("@volar/typescript") = requireFromVueTsc("@volar/typescript"); - const tsHost = ts.createCompilerHost(compilerOptions); + const tsHost = ts.createCompilerHost(opts.typescript.compilerOptions); tsHost.writeFile = (filename, content) => { vfs.set(filename, content); }; @@ -207,8 +215,8 @@ async function emitVueTscLatest( }; const programOptions: CreateProgramOptions = { - rootNames: srcFiles, - options: compilerOptions, + rootNames: inputFiles, + options: opts.typescript.compilerOptions, host: tsHost, }; @@ -222,7 +230,7 @@ async function emitVueTscLatest( vueLanguageCore.createParsedCommandLineByJson( ts, ts.sys, - rootDir, + opts.rootDir, {}, undefined, true, @@ -234,8 +242,11 @@ async function emitVueTscLatest( ); const program = createProgram(programOptions); + const result = program.emit(); - if (result.diagnostics?.length) { - console.error(ts.formatDiagnostics(result.diagnostics, tsHost)); - } + const output = extractDeclarations(vfs, originFiles, opts); + + augmentWithDiagnostics(result, output, tsHost, ts); + + return output; } diff --git a/test/index.test.ts b/test/index.test.ts index 854fc2e9..f2dc0ddc 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,5 +1,5 @@ import { readFile } from "node:fs/promises"; -import { resolve } from "pathe"; +import { relative, resolve } from "pathe"; import { describe, it, @@ -497,6 +497,28 @@ describe("mkdist", () => { " `); }); + + it("emits DTS errors", async () => { + const rootDir = resolve(__dirname, "fixture"); + const { mkdist } = await import("../src/make"); + const { errors } = await mkdist({ + rootDir, + declaration: true, + typescript: { + compilerOptions: { + // force compiler errors to be emitted + noEmitOnError: true, + }, + }, + }); + const files = errors.map((e) => relative(rootDir, e.filename)); + expect(files).toMatchInlineSnapshot(` + [ + "dist/components/index.d.ts", + "dist/components/tsx.d.ts", + ] + `); + }, 50_000); }); describe("mkdist with vue-tsc v1", () => {