1+ import path from 'node:path'
12import Debug from 'debug'
23import ts from 'typescript'
4+ import { fsSystem , memorySystem } from './tsc-system.ts'
35import { createVueProgramFactory } from './vue.ts'
46import type { TsConfigJson } from 'get-tsconfig'
7+ import type { SourceMapInput } from 'rolldown'
58
69const debug = Debug ( 'rolldown-plugin-dts:tsc' )
710debug ( `loaded typescript: ${ ts . version } ` )
@@ -35,8 +38,10 @@ export interface TscModule {
3538}
3639
3740export interface TscOptions {
41+ tsconfig ?: string
3842 tsconfigRaw : TsConfigJson
39- tsconfigDir : string
43+ cwd : string
44+ incremental : boolean
4045 entries ?: string [ ]
4146 id : string
4247 vue ?: boolean
@@ -66,31 +71,71 @@ function createOrGetTsModule(options: TscOptions): TscModule {
6671 return module
6772}
6873
74+ /**
75+ * Build the root project and all its dependencies projects.
76+ * This is designed for a project (e.g. tsconfig.json) that has "references" to
77+ * other composite projects (e.g., tsconfig.node.json and tsconfig.app.json).
78+ * If `incremental` is `true`, the build result will be cached in the
79+ * `.tsbuildinfo` file so that the next time the project is built (without
80+ * changes) the build will be super fast. If `incremental` is `false`, the
81+ * `.tsbuildinfo` file will only be written to the memory.
82+ */
83+ function buildSolution ( tsconfig : string , incremental : boolean ) {
84+ debug ( `building projects for ${ tsconfig } with incremental: ${ incremental } ` )
85+ const system = incremental ? fsSystem : memorySystem
86+
87+ const host = ts . createSolutionBuilderHost ( system )
88+ const builder = ts . createSolutionBuilder ( host , [ tsconfig ] , {
89+ // If `incremental` is `false`, we want to force the builder to rebuild the
90+ // project even if the project is already built (i.e., `.tsbuildinfo` exists
91+ // on the disk).
92+ force : ! incremental ,
93+ verbose : true ,
94+ } )
95+
96+ const exitStatus = builder . build ( )
97+ debug ( `built solution for ${ tsconfig } with exit status ${ exitStatus } ` )
98+ }
99+
69100function createTsProgram ( {
70101 entries,
71102 id,
103+ tsconfig,
72104 tsconfigRaw,
73- tsconfigDir ,
105+ incremental ,
74106 vue,
107+ cwd,
75108} : TscOptions ) : TscModule {
76109 const parsedCmd = ts . parseJsonConfigFileContent (
77110 tsconfigRaw ,
78- ts . sys ,
79- tsconfigDir ,
111+ fsSystem ,
112+ tsconfig ? path . dirname ( tsconfig ) : cwd ,
80113 )
114+
115+ // If the tsconfig has project references, build the project tree.
116+ if ( tsconfig && parsedCmd . projectReferences ?. length ) {
117+ buildSolution ( tsconfig , incremental )
118+ }
119+
81120 const compilerOptions : ts . CompilerOptions = {
82121 ...defaultCompilerOptions ,
83122 ...parsedCmd . options ,
84123 }
85124 const rootNames = [
86125 ...new Set (
87126 [ id , ...( entries || parsedCmd . fileNames ) ] . map ( ( f ) =>
88- ts . sys . resolvePath ( f ) ,
127+ fsSystem . resolvePath ( f ) ,
89128 ) ,
90129 ) ,
91130 ]
92131
93132 const host = ts . createCompilerHost ( compilerOptions , true )
133+
134+ // Try to read files from memory first, which was added by `buildSolution`
135+ host . readFile = fsSystem . readFile
136+ host . fileExists = fsSystem . fileExists
137+ host . directoryExists = fsSystem . directoryExists
138+
94139 const createProgram = vue ? createVueProgramFactory ( ts ) : ts . createProgram
95140 const program = createProgram ( {
96141 rootNames,
@@ -100,8 +145,18 @@ function createTsProgram({
100145 } )
101146
102147 const sourceFile = program . getSourceFile ( id )
148+
103149 if ( ! sourceFile ) {
104- throw new Error ( `Source file not found: ${ id } ` )
150+ debug ( `source file not found in program: ${ id } ` )
151+ if ( ! fsSystem . fileExists ( id ) ) {
152+ debug ( `File ${ id } does not exist on disk.` )
153+ throw new Error ( `Source file not found: ${ id } ` )
154+ } else {
155+ debug ( `File ${ id } exists on disk.` )
156+ throw new Error (
157+ `Unable to load file ${ id } from the program. This seems like a bug of rolldown-plugin-dts. Please report this issue to https://github.com/sxzz/rolldown-plugin-dts/issues` ,
158+ )
159+ }
105160 }
106161
107162 return {
@@ -112,15 +167,17 @@ function createTsProgram({
112167
113168export interface TscResult {
114169 code ?: string
115- map ?: any
170+ map ?: SourceMapInput
116171 error ?: string
117172}
118173
119174export function tscEmit ( tscOptions : TscOptions ) : TscResult {
175+ debug ( `running tscEmit ${ tscOptions . id } ` )
120176 const module = createOrGetTsModule ( tscOptions )
121177 const { program, file } = module
178+ debug ( `got source file: ${ file . fileName } ` )
122179 let dtsCode : string | undefined
123- let map : any
180+ let map : SourceMapInput | undefined
124181 const { emitSkipped, diagnostics } = program . emit (
125182 file ,
126183 ( fileName , code ) => {
@@ -141,5 +198,15 @@ export function tscEmit(tscOptions: TscOptions): TscResult {
141198 if ( emitSkipped && diagnostics . length ) {
142199 return { error : ts . formatDiagnostics ( diagnostics , formatHost ) }
143200 }
201+
202+ // If TypeScript skipped emitting because the file is already a .d.ts (e.g. a
203+ // redirected output from a composite project build), the emit callback above
204+ // will never be invoked. In that case, fall back to the text of the source
205+ // file itself so that callers still receive a declaration string.
206+ if ( ! dtsCode && file . isDeclarationFile ) {
207+ debug ( 'nothing was emitted. fallback to sourceFile text.' )
208+ dtsCode = file . getFullText ( )
209+ }
210+
144211 return { code : dtsCode , map }
145212}
0 commit comments