Skip to content

Commit e1106dd

Browse files
committed
feat: Initial attempt at support for project references
Thanks to @berickson1 for the example project! This probably still has work to be done - see #1414.
1 parent ae8512a commit e1106dd

File tree

7 files changed

+138
-65
lines changed

7 files changed

+138
-65
lines changed

src/lib/application.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from "./utils/component";
2424
import { Options, BindOption } from "./utils";
2525
import { TypeDocOptions } from "./utils/options/declaration";
26+
import { flatMap } from "./utils/array";
2627

2728
// eslint-disable-next-line @typescript-eslint/no-var-requires
2829
const packageInfo = require("../../package.json") as {
@@ -201,20 +202,40 @@ export class Application extends ChildableComponent<
201202
);
202203
}
203204

204-
const program = ts.createProgram(
205-
this.application.options.getFileNames(),
206-
this.application.options.getCompilerOptions()
207-
);
205+
const programs = [
206+
ts.createProgram({
207+
rootNames: this.application.options.getFileNames(),
208+
options: this.application.options.getCompilerOptions(),
209+
projectReferences: this.application.options.getProjectReferences(),
210+
}),
211+
];
212+
213+
// This might be a solution style tsconfig, in which case we need to add a program for each
214+
// reference so that the converter can look through each of these.
215+
const resolvedReferences = programs[0].getResolvedProjectReferences();
216+
for (const ref of resolvedReferences ?? []) {
217+
if (!ref) continue; // This indicates bad configuration... will be reported later.
218+
219+
programs.push(
220+
ts.createProgram({
221+
options: ref.commandLine.options,
222+
rootNames: ref.commandLine.fileNames,
223+
projectReferences: ref.commandLine.projectReferences,
224+
})
225+
);
226+
}
227+
228+
this.logger.verbose(`Converting with ${programs.length} programs`);
208229

209-
const errors = ts.getPreEmitDiagnostics(program);
230+
const errors = flatMap(programs, ts.getPreEmitDiagnostics);
210231
if (errors.length) {
211232
this.logger.diagnostics(errors);
212233
return;
213234
}
214235

215236
return this.converter.convert(
216237
this.expandInputFiles(this.entryPoints),
217-
program
238+
programs
218239
);
219240
}
220241

src/lib/converter/context.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ok as assert } from "assert";
12
import * as ts from "typescript";
23

34
import {
@@ -26,12 +27,27 @@ export class Context {
2627
/**
2728
* The TypeChecker instance returned by the TypeScript compiler.
2829
*/
29-
readonly checker: ts.TypeChecker;
30+
get checker(): ts.TypeChecker {
31+
return this.program.getTypeChecker();
32+
}
3033

3134
/**
32-
* The program being converted.
35+
* The program currently being converted.
36+
* Accessing this property will throw if a source file is not currently being converted.
3337
*/
34-
readonly program: ts.Program;
38+
get program(): ts.Program {
39+
assert(
40+
this._program,
41+
"Tried to access Context.program when not converting a source file"
42+
);
43+
return this._program;
44+
}
45+
private _program?: ts.Program;
46+
47+
/**
48+
* All programs being converted.
49+
*/
50+
readonly programs: readonly ts.Program[];
3551

3652
/**
3753
* The project that is currently processed.
@@ -53,14 +69,12 @@ export class Context {
5369
*/
5470
constructor(
5571
converter: Converter,
56-
checker: ts.TypeChecker,
57-
program: ts.Program,
58-
project = new ProjectReflection(converter.name),
72+
programs: readonly ts.Program[],
73+
project: ProjectReflection,
5974
scope: Context["scope"] = project
6075
) {
6176
this.converter = converter;
62-
this.checker = checker;
63-
this.program = program;
77+
this.programs = programs;
6478

6579
this.project = project;
6680
this.scope = scope;
@@ -202,17 +216,22 @@ export class Context {
202216
this.converter.trigger(name, this, reflection, node);
203217
}
204218

219+
/** @internal */
220+
setActiveProgram(program: ts.Program | undefined) {
221+
this._program = program;
222+
}
223+
205224
/**
206225
* @param callback The callback function that should be executed with the changed context.
207226
*/
208227
public withScope(scope: Reflection): Context {
209228
const context = new Context(
210229
this.converter,
211-
this.checker,
212-
this.program,
230+
this.programs,
213231
this.project,
214232
scope
215233
);
234+
context.setActiveProgram(this._program);
216235
return context;
217236
}
218237
}

src/lib/converter/converter.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as ts from "typescript";
22
import * as _ts from "../ts-internal";
33
import * as _ from "lodash";
44
import * as assert from "assert";
5+
import { resolve } from "path";
56

67
import { Application } from "../application";
78
import {
@@ -143,17 +144,18 @@ export class Converter extends ChildableComponent<
143144
*/
144145
convert(
145146
entryPoints: readonly string[],
146-
program: ts.Program
147+
programs: ts.Program | readonly ts.Program[]
147148
): ProjectReflection | undefined {
149+
programs = Array.isArray(programs) ? programs : [programs];
148150
this.externalPatternCache = void 0;
149151

150-
const checker = program.getTypeChecker();
151-
const context = new Context(this, checker, program);
152+
const project = new ProjectReflection(this.name);
153+
const context = new Context(this, programs, project);
152154

153155
this.trigger(Converter.EVENT_BEGIN, context);
154156

155-
this.compile(program, entryPoints, context);
156-
const project = this.resolve(context);
157+
this.compile(entryPoints, context);
158+
this.resolve(context);
157159
// This should only do anything if a plugin does something bad.
158160
project.removeDanglingReferences();
159161

@@ -246,37 +248,46 @@ export class Converter extends ChildableComponent<
246248
* @param context The context object describing the current state the converter is in.
247249
* @returns An array containing all errors generated by the TypeScript compiler.
248250
*/
249-
private compile(
250-
program: ts.Program,
251-
entryPoints: readonly string[],
252-
context: Context
253-
) {
251+
private compile(entryPoints: readonly string[], context: Context) {
254252
const baseDir = getCommonDirectory(entryPoints);
255-
const needsSecondPass: ts.SourceFile[] = [];
256-
257-
for (const entry of entryPoints) {
258-
const sourceFile = program.getSourceFile(normalizePath(entry));
259-
if (!sourceFile) {
260-
this.application.logger.warn(
261-
`Unable to locate entry point: ${entry}`
262-
);
263-
continue;
253+
const entries: [string, ts.SourceFile, ts.Program][] = [];
254+
255+
entryLoop: for (const entry of entryPoints.map(normalizePath)) {
256+
for (const program of context.programs) {
257+
const sourceFile = program.getSourceFile(entry);
258+
if (sourceFile) {
259+
entries.push([entry, sourceFile, program]);
260+
continue entryLoop;
261+
}
264262
}
263+
this.application.logger.warn(
264+
`Unable to locate entry point: ${entry}`
265+
);
266+
}
265267

266-
needsSecondPass.push(sourceFile);
267-
this.convertExports(context, sourceFile, entryPoints, baseDir);
268+
for (const [entry, file, program] of entries) {
269+
context.setActiveProgram(program);
270+
this.convertExports(
271+
context,
272+
file,
273+
entryPoints,
274+
getModuleName(resolve(entry), baseDir)
275+
);
268276
}
269277

270-
for (const file of needsSecondPass) {
278+
for (const [, file, program] of entries) {
279+
context.setActiveProgram(program);
271280
this.convertReExports(context, file);
272281
}
282+
283+
context.setActiveProgram(undefined);
273284
}
274285

275286
private convertExports(
276287
context: Context,
277288
node: ts.SourceFile,
278289
entryPoints: readonly string[],
279-
baseDir: string
290+
entryName: string
280291
) {
281292
const symbol = context.checker.getSymbolAtLocation(node) ?? node.symbol;
282293
let moduleContext: Context;
@@ -290,7 +301,7 @@ export class Converter extends ChildableComponent<
290301
const reflection = context.createDeclarationReflection(
291302
ReflectionKind.Module,
292303
symbol,
293-
getModuleName(node.fileName, baseDir)
304+
entryName
294305
);
295306
moduleContext = context.withScope(reflection);
296307
} else {
@@ -340,7 +351,7 @@ export class Converter extends ChildableComponent<
340351
* @param context The context object describing the current state the converter is in.
341352
* @returns The final project reflection.
342353
*/
343-
private resolve(context: Context): ProjectReflection {
354+
private resolve(context: Context): void {
344355
this.trigger(Converter.EVENT_RESOLVE_BEGIN, context);
345356
const project = context.project;
346357

@@ -349,7 +360,6 @@ export class Converter extends ChildableComponent<
349360
}
350361

351362
this.trigger(Converter.EVENT_RESOLVE_END, context);
352-
return project;
353363
}
354364

355365
/** @internal */
@@ -389,7 +399,7 @@ export class Converter extends ChildableComponent<
389399

390400
function getModuleName(fileName: string, baseDir: string) {
391401
return normalizePath(relative(baseDir, fileName)).replace(
392-
/(\.d)?\.[tj]sx?$/,
402+
/(\/index)?(\.d)?\.[tj]sx?$/,
393403
""
394404
);
395405
}

src/lib/converter/plugins/PackagePlugin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Converter } from "../converter";
66
import { Context } from "../context";
77
import { BindOption, readFile } from "../../utils";
88
import { getCommonDirectory } from "../../utils/fs";
9+
import { flatMap } from "../../utils/array";
910

1011
/**
1112
* A handler that tries to find the package.json and readme.md files of the
@@ -63,7 +64,11 @@ export class PackagePlugin extends ConverterComponent {
6364
dirName === Path.resolve(Path.join(dirName, ".."));
6465

6566
let dirName = Path.resolve(
66-
getCommonDirectory(context.program.getRootFileNames())
67+
getCommonDirectory(
68+
flatMap(context.programs, (program) =>
69+
program.getRootFileNames()
70+
)
71+
)
6772
);
6873
while (!packageAndReadmeFound() && !reachedTopDirectory(dirName)) {
6974
FS.readdirSync(dirName).forEach((file) => {

src/lib/utils/array.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,18 +128,18 @@ export function filterMap<T, U>(
128128

129129
export function flatMap<T, U>(
130130
arr: readonly T[],
131-
fn: (item: T, index: number) => U | U[]
131+
fn: (item: T) => U | readonly U[]
132132
): U[] {
133133
const result: U[] = [];
134134

135-
arr.forEach((item, index) => {
136-
const newItem = fn(item, index);
135+
for (const item of arr) {
136+
const newItem = fn(item);
137137
if (Array.isArray(newItem)) {
138138
result.push(...newItem);
139139
} else {
140140
result.push(newItem);
141141
}
142-
});
142+
}
143143

144144
return result;
145145
}

src/lib/utils/options/options.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export class Options {
7979
private _declarations = new Map<string, Readonly<DeclarationOption>>();
8080
private _values: Record<string, unknown> = {};
8181
private _compilerOptions: ts.CompilerOptions = {};
82-
private _fileNames: string[] = [];
82+
private _fileNames: readonly string[] = [];
83+
private _projectReferences: readonly ts.ProjectReference[] = [];
8384
private _logger: Logger;
8485

8586
constructor(logger: Logger) {
@@ -283,15 +284,24 @@ export class Options {
283284
return this._fileNames;
284285
}
285286

287+
/**
288+
* Gets the project references - used in solution style tsconfig setups.
289+
*/
290+
getProjectReferences(): readonly ts.ProjectReference[] {
291+
return this._projectReferences;
292+
}
293+
286294
/**
287295
* Sets the compiler options that will be used to get a TS program.
288296
*/
289297
setCompilerOptions(
290298
fileNames: readonly string[],
291-
options: ts.CompilerOptions
299+
options: ts.CompilerOptions,
300+
projectReferences: readonly ts.ProjectReference[] | undefined
292301
) {
293-
this._fileNames = fileNames.slice();
302+
this._fileNames = fileNames;
294303
this._compilerOptions = _.cloneDeep(options);
304+
this._projectReferences = projectReferences ?? [];
295305
}
296306

297307
/**

0 commit comments

Comments
 (0)