Skip to content

Commit

Permalink
feat: Initial attempt at support for project references
Browse files Browse the repository at this point in the history
Thanks to @berickson1 for the example project!

This probably still has work to be done - see #1414.
  • Loading branch information
Gerrit0 committed Dec 7, 2020
1 parent ae8512a commit e1106dd
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 65 deletions.
33 changes: 27 additions & 6 deletions src/lib/application.ts
Expand Up @@ -23,6 +23,7 @@ import {
} from "./utils/component";
import { Options, BindOption } from "./utils";
import { TypeDocOptions } from "./utils/options/declaration";
import { flatMap } from "./utils/array";

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

const program = ts.createProgram(
this.application.options.getFileNames(),
this.application.options.getCompilerOptions()
);
const programs = [
ts.createProgram({
rootNames: this.application.options.getFileNames(),
options: this.application.options.getCompilerOptions(),
projectReferences: this.application.options.getProjectReferences(),
}),
];

// This might be a solution style tsconfig, in which case we need to add a program for each
// reference so that the converter can look through each of these.
const resolvedReferences = programs[0].getResolvedProjectReferences();
for (const ref of resolvedReferences ?? []) {
if (!ref) continue; // This indicates bad configuration... will be reported later.

programs.push(
ts.createProgram({
options: ref.commandLine.options,
rootNames: ref.commandLine.fileNames,
projectReferences: ref.commandLine.projectReferences,
})
);
}

this.logger.verbose(`Converting with ${programs.length} programs`);

const errors = ts.getPreEmitDiagnostics(program);
const errors = flatMap(programs, ts.getPreEmitDiagnostics);
if (errors.length) {
this.logger.diagnostics(errors);
return;
}

return this.converter.convert(
this.expandInputFiles(this.entryPoints),
program
programs
);
}

Expand Down
39 changes: 29 additions & 10 deletions src/lib/converter/context.ts
@@ -1,3 +1,4 @@
import { ok as assert } from "assert";
import * as ts from "typescript";

import {
Expand Down Expand Up @@ -26,12 +27,27 @@ export class Context {
/**
* The TypeChecker instance returned by the TypeScript compiler.
*/
readonly checker: ts.TypeChecker;
get checker(): ts.TypeChecker {
return this.program.getTypeChecker();
}

/**
* The program being converted.
* The program currently being converted.
* Accessing this property will throw if a source file is not currently being converted.
*/
readonly program: ts.Program;
get program(): ts.Program {
assert(
this._program,
"Tried to access Context.program when not converting a source file"
);
return this._program;
}
private _program?: ts.Program;

/**
* All programs being converted.
*/
readonly programs: readonly ts.Program[];

/**
* The project that is currently processed.
Expand All @@ -53,14 +69,12 @@ export class Context {
*/
constructor(
converter: Converter,
checker: ts.TypeChecker,
program: ts.Program,
project = new ProjectReflection(converter.name),
programs: readonly ts.Program[],
project: ProjectReflection,
scope: Context["scope"] = project
) {
this.converter = converter;
this.checker = checker;
this.program = program;
this.programs = programs;

this.project = project;
this.scope = scope;
Expand Down Expand Up @@ -202,17 +216,22 @@ export class Context {
this.converter.trigger(name, this, reflection, node);
}

/** @internal */
setActiveProgram(program: ts.Program | undefined) {
this._program = program;
}

/**
* @param callback The callback function that should be executed with the changed context.
*/
public withScope(scope: Reflection): Context {
const context = new Context(
this.converter,
this.checker,
this.program,
this.programs,
this.project,
scope
);
context.setActiveProgram(this._program);
return context;
}
}
Expand Down
64 changes: 37 additions & 27 deletions src/lib/converter/converter.ts
Expand Up @@ -2,6 +2,7 @@ import * as ts from "typescript";
import * as _ts from "../ts-internal";
import * as _ from "lodash";
import * as assert from "assert";
import { resolve } from "path";

import { Application } from "../application";
import {
Expand Down Expand Up @@ -143,17 +144,18 @@ export class Converter extends ChildableComponent<
*/
convert(
entryPoints: readonly string[],
program: ts.Program
programs: ts.Program | readonly ts.Program[]
): ProjectReflection | undefined {
programs = Array.isArray(programs) ? programs : [programs];
this.externalPatternCache = void 0;

const checker = program.getTypeChecker();
const context = new Context(this, checker, program);
const project = new ProjectReflection(this.name);
const context = new Context(this, programs, project);

this.trigger(Converter.EVENT_BEGIN, context);

this.compile(program, entryPoints, context);
const project = this.resolve(context);
this.compile(entryPoints, context);
this.resolve(context);
// This should only do anything if a plugin does something bad.
project.removeDanglingReferences();

Expand Down Expand Up @@ -246,37 +248,46 @@ export class Converter extends ChildableComponent<
* @param context The context object describing the current state the converter is in.
* @returns An array containing all errors generated by the TypeScript compiler.
*/
private compile(
program: ts.Program,
entryPoints: readonly string[],
context: Context
) {
private compile(entryPoints: readonly string[], context: Context) {
const baseDir = getCommonDirectory(entryPoints);
const needsSecondPass: ts.SourceFile[] = [];

for (const entry of entryPoints) {
const sourceFile = program.getSourceFile(normalizePath(entry));
if (!sourceFile) {
this.application.logger.warn(
`Unable to locate entry point: ${entry}`
);
continue;
const entries: [string, ts.SourceFile, ts.Program][] = [];

entryLoop: for (const entry of entryPoints.map(normalizePath)) {
for (const program of context.programs) {
const sourceFile = program.getSourceFile(entry);
if (sourceFile) {
entries.push([entry, sourceFile, program]);
continue entryLoop;
}
}
this.application.logger.warn(
`Unable to locate entry point: ${entry}`
);
}

needsSecondPass.push(sourceFile);
this.convertExports(context, sourceFile, entryPoints, baseDir);
for (const [entry, file, program] of entries) {
context.setActiveProgram(program);
this.convertExports(
context,
file,
entryPoints,
getModuleName(resolve(entry), baseDir)
);
}

for (const file of needsSecondPass) {
for (const [, file, program] of entries) {
context.setActiveProgram(program);
this.convertReExports(context, file);
}

context.setActiveProgram(undefined);
}

private convertExports(
context: Context,
node: ts.SourceFile,
entryPoints: readonly string[],
baseDir: string
entryName: string
) {
const symbol = context.checker.getSymbolAtLocation(node) ?? node.symbol;
let moduleContext: Context;
Expand All @@ -290,7 +301,7 @@ export class Converter extends ChildableComponent<
const reflection = context.createDeclarationReflection(
ReflectionKind.Module,
symbol,
getModuleName(node.fileName, baseDir)
entryName
);
moduleContext = context.withScope(reflection);
} else {
Expand Down Expand Up @@ -340,7 +351,7 @@ export class Converter extends ChildableComponent<
* @param context The context object describing the current state the converter is in.
* @returns The final project reflection.
*/
private resolve(context: Context): ProjectReflection {
private resolve(context: Context): void {
this.trigger(Converter.EVENT_RESOLVE_BEGIN, context);
const project = context.project;

Expand All @@ -349,7 +360,6 @@ export class Converter extends ChildableComponent<
}

this.trigger(Converter.EVENT_RESOLVE_END, context);
return project;
}

/** @internal */
Expand Down Expand Up @@ -389,7 +399,7 @@ export class Converter extends ChildableComponent<

function getModuleName(fileName: string, baseDir: string) {
return normalizePath(relative(baseDir, fileName)).replace(
/(\.d)?\.[tj]sx?$/,
/(\/index)?(\.d)?\.[tj]sx?$/,
""
);
}
Expand Down
7 changes: 6 additions & 1 deletion src/lib/converter/plugins/PackagePlugin.ts
Expand Up @@ -6,6 +6,7 @@ import { Converter } from "../converter";
import { Context } from "../context";
import { BindOption, readFile } from "../../utils";
import { getCommonDirectory } from "../../utils/fs";
import { flatMap } from "../../utils/array";

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

let dirName = Path.resolve(
getCommonDirectory(context.program.getRootFileNames())
getCommonDirectory(
flatMap(context.programs, (program) =>
program.getRootFileNames()
)
)
);
while (!packageAndReadmeFound() && !reachedTopDirectory(dirName)) {
FS.readdirSync(dirName).forEach((file) => {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/utils/array.ts
Expand Up @@ -128,18 +128,18 @@ export function filterMap<T, U>(

export function flatMap<T, U>(
arr: readonly T[],
fn: (item: T, index: number) => U | U[]
fn: (item: T) => U | readonly U[]
): U[] {
const result: U[] = [];

arr.forEach((item, index) => {
const newItem = fn(item, index);
for (const item of arr) {
const newItem = fn(item);
if (Array.isArray(newItem)) {
result.push(...newItem);
} else {
result.push(newItem);
}
});
}

return result;
}
16 changes: 13 additions & 3 deletions src/lib/utils/options/options.ts
Expand Up @@ -79,7 +79,8 @@ export class Options {
private _declarations = new Map<string, Readonly<DeclarationOption>>();
private _values: Record<string, unknown> = {};
private _compilerOptions: ts.CompilerOptions = {};
private _fileNames: string[] = [];
private _fileNames: readonly string[] = [];
private _projectReferences: readonly ts.ProjectReference[] = [];
private _logger: Logger;

constructor(logger: Logger) {
Expand Down Expand Up @@ -283,15 +284,24 @@ export class Options {
return this._fileNames;
}

/**
* Gets the project references - used in solution style tsconfig setups.
*/
getProjectReferences(): readonly ts.ProjectReference[] {
return this._projectReferences;
}

/**
* Sets the compiler options that will be used to get a TS program.
*/
setCompilerOptions(
fileNames: readonly string[],
options: ts.CompilerOptions
options: ts.CompilerOptions,
projectReferences: readonly ts.ProjectReference[] | undefined
) {
this._fileNames = fileNames.slice();
this._fileNames = fileNames;
this._compilerOptions = _.cloneDeep(options);
this._projectReferences = projectReferences ?? [];
}

/**
Expand Down

0 comments on commit e1106dd

Please sign in to comment.