Skip to content

Commit 9c19f74

Browse files
ocavuesxzz
andauthored
feat: support tsc -b (#42)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
1 parent 0627c80 commit 9c19f74

File tree

18 files changed

+407
-16
lines changed

18 files changed

+407
-16
lines changed

src/generate.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import type { TscFunctions } from './utils/tsc-worker.ts'
1515
import type { TscOptions, TscResult } from './utils/tsc.ts'
1616
import type { OptionsResolved } from './index.ts'
17-
import type { Plugin } from 'rolldown'
17+
import type { Plugin, SourceMapInput } from 'rolldown'
1818

1919
const debug = Debug('rolldown-plugin-dts:generate')
2020

@@ -31,17 +31,21 @@ export interface TsModule {
3131
export type DtsMap = Map<string, TsModule>
3232

3333
export function createGeneratePlugin({
34+
tsconfig,
3435
tsconfigRaw,
35-
tsconfigDir,
36+
incremental,
37+
cwd,
3638
isolatedDeclarations,
3739
emitDtsOnly,
3840
vue,
3941
parallel,
4042
eager,
4143
}: Pick<
4244
OptionsResolved,
45+
| 'cwd'
46+
| 'tsconfig'
4347
| 'tsconfigRaw'
44-
| 'tsconfigDir'
48+
| 'incremental'
4549
| 'isolatedDeclarations'
4650
| 'emitDtsOnly'
4751
| 'vue'
@@ -169,7 +173,7 @@ export function createGeneratePlugin({
169173

170174
const { code, id } = dtsMap.get(dtsId)!
171175
let dtsCode: string | undefined
172-
let map: any
176+
let map: SourceMapInput | undefined
173177
debug('generate dts %s from %s', dtsId, id)
174178

175179
if (isolatedDeclarations && !RE_VUE.test(id)) {
@@ -193,8 +197,10 @@ export function createGeneratePlugin({
193197
.filter((v) => v.isEntry)
194198
.map((v) => v.id)
195199
const options: Omit<TscOptions, 'programs'> = {
200+
tsconfig,
196201
tsconfigRaw,
197-
tsconfigDir,
202+
incremental,
203+
cwd,
198204
entries,
199205
id,
200206
vue,

src/index.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,37 @@ export interface Options {
6363
*/
6464
tsconfigRaw?: Omit<TsConfigJson, 'compilerOptions'>
6565

66+
/**
67+
* If your tsconfig.json has
68+
* [`references`](https://www.typescriptlang.org/tsconfig/#references) option,
69+
* `rolldown-plugin-dts` will use [`tsc
70+
* -b`](https://www.typescriptlang.org/docs/handbook/project-references.html#build-mode-for-typescript)
71+
* to build the project and all referenced projects before emitting `.d.ts`
72+
* files.
73+
*
74+
* In such case, if this option is `true`, `rolldown-plugin-dts` will write
75+
* down all built files into your disk, including
76+
* [`.tsbuildinfo`](https://www.typescriptlang.org/tsconfig/#tsBuildInfoFile)
77+
* and other built files. This is equivalent to running `tsc -b` in your
78+
* project.
79+
*
80+
* Otherwise, if this option is `false`, `rolldown-plugin-dts` will write
81+
* built files only into memory and leave a small footprint in your disk.
82+
*
83+
* Enabling this option will decrease the build time by caching previous build
84+
* results. This is helpful when you have a large project with multiple
85+
* referenced projects.
86+
*
87+
* By default, `incremental` is `true` if your tsconfig has
88+
* [`incremental`](https://www.typescriptlang.org/tsconfig/#incremental) or
89+
* [`tsBuildInfoFile`](https://www.typescriptlang.org/tsconfig/#tsBuildInfoFile)
90+
* enabled.
91+
*
92+
* This option is only used when {@link Options.isolatedDeclarations} is
93+
* `false`.
94+
*/
95+
incremental?: boolean
96+
6697
/**
6798
* Override the `compilerOptions` specified in `tsconfig.json`.
6899
*
@@ -117,7 +148,6 @@ export type OptionsResolved = Overwrite<
117148
tsconfig: string | undefined
118149
isolatedDeclarations: IsolatedDeclarationsOptions | false
119150
tsconfigRaw: TsConfigJson
120-
tsconfigDir: string
121151
}
122152
>
123153

@@ -141,6 +171,7 @@ export { createFakeJsPlugin, createGeneratePlugin }
141171
export function resolveOptions({
142172
cwd = process.cwd(),
143173
tsconfig,
174+
incremental = false,
144175
compilerOptions = {},
145176
tsconfigRaw: overriddenTsconfigRaw = {},
146177
isolatedDeclarations,
@@ -169,6 +200,8 @@ export function resolveOptions({
169200
...compilerOptions,
170201
}
171202

203+
incremental ||=
204+
compilerOptions.incremental || !!compilerOptions.tsBuildInfoFile
172205
sourcemap ??= !!compilerOptions.declarationMap
173206
compilerOptions.declarationMap = sourcemap
174207

@@ -177,7 +210,6 @@ export function resolveOptions({
177210
...overriddenTsconfigRaw,
178211
compilerOptions,
179212
}
180-
const tsconfigDir = tsconfig ? path.dirname(tsconfig) : cwd
181213

182214
if (isolatedDeclarations == null) {
183215
isolatedDeclarations = !!compilerOptions?.isolatedDeclarations
@@ -194,8 +226,8 @@ export function resolveOptions({
194226
return {
195227
cwd,
196228
tsconfig,
197-
tsconfigDir,
198229
tsconfigRaw,
230+
incremental,
199231
isolatedDeclarations,
200232
sourcemap,
201233
dtsInput,

src/utils/tsc-system.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Debug from 'debug'
2+
import ts from 'typescript'
3+
4+
const debug = Debug('rolldown-plugin-dts:tsc-system')
5+
6+
const files: Map<string, string> = new Map()
7+
8+
// A system that writes files to both memory and disk. It will try read files
9+
// from memory firstly and fallback to disk if not found.ƒ
10+
export const fsSystem: ts.System = {
11+
...ts.sys,
12+
13+
// Hide the output of tsc by default
14+
write(message: string): void {
15+
debug(message)
16+
},
17+
18+
// Copied from
19+
// https://github.com/microsoft/TypeScript-Website/blob/b0e9a5c0/packages/typescript-vfs/src/index.ts#L571C5-L574C7
20+
resolvePath(path) {
21+
if (files.has(path)) {
22+
return path
23+
}
24+
return ts.sys.resolvePath(path)
25+
},
26+
27+
// Copied from
28+
// https://github.com/microsoft/TypeScript-Website/blob/b0e9a5c0/packages/typescript-vfs/src/index.ts#L532C1-L534C8
29+
directoryExists(directory) {
30+
if (Array.from(files.keys()).some((path) => path.startsWith(directory))) {
31+
return true
32+
}
33+
return ts.sys.directoryExists(directory)
34+
},
35+
36+
fileExists(fileName) {
37+
if (files.has(fileName)) {
38+
return true
39+
}
40+
return ts.sys.fileExists(fileName)
41+
},
42+
43+
readFile(fileName, ...args) {
44+
if (files.has(fileName)) {
45+
return files.get(fileName)
46+
}
47+
return ts.sys.readFile(fileName, ...args)
48+
},
49+
50+
writeFile(path, data, ...args) {
51+
files.set(path, data)
52+
ts.sys.writeFile(path, data, ...args)
53+
},
54+
55+
deleteFile(fileName, ...args) {
56+
files.delete(fileName)
57+
ts.sys.deleteFile?.(fileName, ...args)
58+
},
59+
}
60+
61+
// A system that only writes files to memory. It will read files from both
62+
// memory and disk.
63+
export const memorySystem: ts.System = {
64+
...fsSystem,
65+
66+
writeFile(path, data) {
67+
files.set(path, data)
68+
},
69+
70+
deleteFile(fileName) {
71+
files.delete(fileName)
72+
},
73+
}

src/utils/tsc.ts

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import path from 'node:path'
12
import Debug from 'debug'
23
import ts from 'typescript'
4+
import { fsSystem, memorySystem } from './tsc-system.ts'
35
import { createVueProgramFactory } from './vue.ts'
46
import type { TsConfigJson } from 'get-tsconfig'
7+
import type { SourceMapInput } from 'rolldown'
58

69
const debug = Debug('rolldown-plugin-dts:tsc')
710
debug(`loaded typescript: ${ts.version}`)
@@ -35,8 +38,10 @@ export interface TscModule {
3538
}
3639

3740
export 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+
69100
function 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

113168
export interface TscResult {
114169
code?: string
115-
map?: any
170+
map?: SourceMapInput
116171
error?: string
117172
}
118173

119174
export 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

Comments
 (0)