Skip to content

Commit 6d51bcb

Browse files
committed
Compile TypeScript incrementally
1 parent 60dfd91 commit 6d51bcb

File tree

5 files changed

+224
-50
lines changed

5 files changed

+224
-50
lines changed

cli/js/compiler.ts

+126-25
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,14 @@ import { assert, log } from "./util.ts";
2323
import * as util from "./util.ts";
2424
import { TextDecoder, TextEncoder } from "./web/text_encoding.ts";
2525
import { core } from "./core.ts";
26+
import Timer from "./timer.ts";
2627

27-
export function resolveModules(
28-
specifiers: string[],
29-
referrer?: string
30-
): string[] {
28+
function resolveModules(specifiers: string[], referrer?: string): string[] {
3129
util.log("compiler::resolveModules", { specifiers, referrer });
3230
return sendSync("op_resolve_modules", { specifiers, referrer });
3331
}
3432

35-
export function fetchSourceFiles(
33+
function fetchSourceFiles(
3634
specifiers: string[],
3735
referrer?: string
3836
): Promise<
@@ -61,11 +59,17 @@ function getAsset(name: string): string {
6159
return decoder.decode(sourceCodeBytes!);
6260
}
6361

62+
function getCache(url: string): string {
63+
const { content } = sendSync("op_get_cache", { url });
64+
return content;
65+
}
66+
6467
// Constants used by `normalizeString` and `resolvePath`
6568
const CHAR_DOT = 46; /* . */
6669
const CHAR_FORWARD_SLASH = 47; /* / */
6770
const ASSETS = "$asset$";
6871
const OUT_DIR = "$deno$";
72+
const TS_BUILD_INFO = `${OUT_DIR}/tsbuildinfo.json`;
6973

7074
// TODO(Bartlomieju): this check should be done in Rust
7175
const IGNORED_COMPILER_OPTIONS: readonly string[] = [
@@ -141,14 +145,17 @@ const DEFAULT_COMPILE_OPTIONS: ts.CompilerOptions = {
141145
allowNonTsExtensions: true,
142146
checkJs: false,
143147
esModuleInterop: true,
148+
incremental: true,
144149
jsx: ts.JsxEmit.React,
145150
module: ts.ModuleKind.ESNext,
151+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
146152
outDir: OUT_DIR,
147153
resolveJsonModule: true,
148154
sourceMap: true,
149155
strict: true,
150156
stripComments: true,
151157
target: ts.ScriptTarget.ESNext,
158+
tsBuildInfoFile: TS_BUILD_INFO,
152159
};
153160

154161
const DEFAULT_RUNTIME_COMPILE_OPTIONS: ts.CompilerOptions = {
@@ -174,6 +181,7 @@ interface CompilerHostOptions {
174181
target: CompilerHostTarget;
175182
unstable?: boolean;
176183
writeFile: WriteFileCallback;
184+
rootNames?: string[];
177185
}
178186

179187
interface ConfigureResponse {
@@ -370,6 +378,10 @@ class SourceFile {
370378
}
371379
return undefined;
372380
}
381+
382+
static urls(): string[] {
383+
return Array.from(SOURCE_FILE_CACHE.keys()).filter(ts.pathIsAbsolute);
384+
}
373385
}
374386

375387
function getAssetInternal(filename: string): SourceFile {
@@ -391,10 +403,12 @@ function getAssetInternal(filename: string): SourceFile {
391403
});
392404
}
393405

394-
class Host implements ts.CompilerHost {
406+
export class Host implements ts.CompilerHost {
395407
readonly #options = DEFAULT_COMPILE_OPTIONS;
396408
#target: CompilerHostTarget;
397409
#writeFile: WriteFileCallback;
410+
#rootPath = ".";
411+
#rootName = "";
398412

399413
/* Deno specific APIs */
400414

@@ -403,6 +417,7 @@ class Host implements ts.CompilerHost {
403417
target,
404418
unstable,
405419
writeFile,
420+
rootNames,
406421
}: CompilerHostOptions) {
407422
this.#target = target;
408423
this.#writeFile = writeFile;
@@ -418,6 +433,10 @@ class Host implements ts.CompilerHost {
418433
"lib.deno.unstable.d.ts",
419434
];
420435
}
436+
if (rootNames) {
437+
this.#rootName = rootNames[0];
438+
this.#rootPath = this.#rootName.split("/").slice(0, -1).join("/");
439+
}
421440
}
422441

423442
configure(
@@ -494,6 +513,24 @@ class Host implements ts.CompilerHost {
494513
return "\n";
495514
}
496515

516+
getAbsolutePath(fileName: string): string {
517+
return ts.resolvePath(this.#rootPath, fileName);
518+
}
519+
520+
getModuleAbsolutePath(containingFile: string, specifier: string): string {
521+
return ts.resolvePath(
522+
ts.getDirectoryPath(this.getAbsolutePath(containingFile)),
523+
specifier
524+
);
525+
}
526+
527+
getRelativePath(fileName: string): string {
528+
if (!ts.pathIsAbsolute(fileName)) {
529+
return fileName;
530+
}
531+
return ts.getRelativePathFromDirectory(this.#rootPath, fileName, false);
532+
}
533+
497534
getSourceFile(
498535
fileName: string,
499536
languageVersion: ts.ScriptTarget,
@@ -505,21 +542,26 @@ class Host implements ts.CompilerHost {
505542
assert(!shouldCreateNewSourceFile);
506543
const sourceFile = fileName.startsWith(ASSETS)
507544
? getAssetInternal(fileName)
508-
: SourceFile.getCached(fileName);
545+
: SourceFile.getCached(this.getAbsolutePath(fileName));
509546
assert(sourceFile != null);
510547
if (!sourceFile.tsSourceFile) {
511548
assert(sourceFile.sourceCode != null);
512549
const tsSourceFileName = fileName.startsWith(ASSETS)
513550
? sourceFile.filename
514-
: fileName;
551+
: this.getRelativePath(fileName);
515552

516553
sourceFile.tsSourceFile = ts.createSourceFile(
517554
tsSourceFileName,
518555
sourceFile.sourceCode,
519556
languageVersion
520557
);
558+
//@ts-ignore
559+
sourceFile.tsSourceFile.version = this.createHash(
560+
sourceFile.tsSourceFile.text
561+
);
521562
delete sourceFile.sourceCode;
522563
}
564+
523565
return sourceFile.tsSourceFile;
524566
} catch (e) {
525567
if (onError) {
@@ -531,8 +573,18 @@ class Host implements ts.CompilerHost {
531573
}
532574
}
533575

534-
readFile(_fileName: string): string | undefined {
535-
return util.notImplemented();
576+
readFile(fileName: string): string | undefined {
577+
util.log("compiler::host.readFile", fileName);
578+
if (fileName === TS_BUILD_INFO) {
579+
return getCache(`${this.#rootName}.json`);
580+
}
581+
return getCache(
582+
this.getAbsolutePath(fileName.replace(OUT_DIR, this.#rootPath))
583+
);
584+
}
585+
586+
createHash(data: string): string {
587+
return ts.generateDjb2Hash(data);
536588
}
537589

538590
resolveModuleNames(
@@ -544,22 +596,25 @@ class Host implements ts.CompilerHost {
544596
containingFile,
545597
});
546598
return moduleNames.map((specifier) => {
547-
const maybeUrl = SourceFile.getResolvedUrl(specifier, containingFile);
599+
const url = this.getModuleAbsolutePath(containingFile, specifier);
548600

549601
let sourceFile: SourceFile | undefined = undefined;
550602

551603
if (specifier.startsWith(ASSETS)) {
552604
sourceFile = getAssetInternal(specifier);
553-
} else if (typeof maybeUrl !== "undefined") {
554-
sourceFile = SourceFile.getCached(maybeUrl);
605+
} else {
606+
sourceFile = SourceFile.getCached(url);
555607
}
556608

557609
if (!sourceFile) {
558610
return undefined;
559611
}
560612

613+
const resolvedFileName = specifier.startsWith(ASSETS)
614+
? sourceFile.url
615+
: this.getRelativePath(sourceFile.url);
561616
return {
562-
resolvedFileName: sourceFile.url,
617+
resolvedFileName,
563618
isExternalLibraryImport: specifier.startsWith(ASSETS),
564619
extension: sourceFile.extension,
565620
};
@@ -578,7 +633,11 @@ class Host implements ts.CompilerHost {
578633
sourceFiles?: readonly ts.SourceFile[]
579634
): void {
580635
util.log("compiler::host.writeFile", fileName);
581-
this.#writeFile(fileName, data, sourceFiles);
636+
this.#writeFile(
637+
this.getAbsolutePath(fileName.replace(OUT_DIR, this.#rootPath)),
638+
data,
639+
sourceFiles
640+
);
582641
}
583642
}
584643

@@ -876,18 +935,26 @@ function createBundleWriteFile(state: WriteFileState): WriteFileCallback {
876935

877936
// TODO(bartlomieju): probably could be defined inline?
878937
function createCompileWriteFile(state: WriteFileState): WriteFileCallback {
938+
const rootPath = state.rootNames[0].split("/").slice(0, -1).join("/");
879939
return function writeFile(
880940
fileName: string,
881941
data: string,
882942
sourceFiles?: readonly ts.SourceFile[]
883943
): void {
884-
assert(sourceFiles != null);
944+
const isBuildInfo = fileName === TS_BUILD_INFO.replace(OUT_DIR, rootPath);
945+
if (sourceFiles != null) {
946+
assert(sourceFiles.length === 1);
947+
} else {
948+
assert(isBuildInfo);
949+
}
885950
assert(state.host);
886951
assert(state.emitMap);
887952
assert(!state.bundle);
888-
assert(sourceFiles.length === 1);
953+
const filename = isBuildInfo
954+
? `${state.rootNames[0]}.json`
955+
: ts.resolvePath(rootPath, sourceFiles![0].fileName);
889956
state.emitMap[fileName] = {
890-
filename: sourceFiles[0].fileName,
957+
filename,
891958
contents: data,
892959
};
893960
};
@@ -1040,6 +1107,30 @@ function processConfigureResponse(
10401107
return diagnostics;
10411108
}
10421109

1110+
const getPreEmitDiagnostics = (
1111+
program: ts.EmitAndSemanticDiagnosticsBuilderProgram
1112+
): readonly ts.Diagnostic[] => {
1113+
const allDiagnostics = program.getConfigFileParsingDiagnostics().slice();
1114+
const configFileParsingDiagnosticsLength = allDiagnostics.length;
1115+
allDiagnostics.push(...program.getSyntacticDiagnostics());
1116+
1117+
// If we didn't have any syntactic errors, then also try getting the global and
1118+
// semantic errors.
1119+
if (allDiagnostics.length === configFileParsingDiagnosticsLength) {
1120+
allDiagnostics.push(
1121+
...program.getOptionsDiagnostics(),
1122+
...program.getGlobalDiagnostics()
1123+
);
1124+
1125+
if (allDiagnostics.length === configFileParsingDiagnosticsLength) {
1126+
allDiagnostics.push(...program.getSemanticDiagnostics());
1127+
}
1128+
}
1129+
return allDiagnostics.filter(
1130+
({ code }) => !ignoredDiagnostics.includes(code)
1131+
);
1132+
};
1133+
10431134
function normalizeString(path: string): string {
10441135
let res = "";
10451136
let lastSegmentLength = 0;
@@ -1246,6 +1337,7 @@ type CompilerRequest =
12461337
interface CompileResult {
12471338
emitMap?: Record<string, EmmitedSource>;
12481339
bundleOutput?: string;
1340+
sources: string[];
12491341
diagnostics: Diagnostic;
12501342
}
12511343

@@ -1276,6 +1368,8 @@ async function compile(
12761368
type: CompilerRequestType[request.type],
12771369
});
12781370

1371+
const compileTimer = new Timer("Compilation");
1372+
12791373
// When a programme is emitted, TypeScript will call `writeFile` with
12801374
// each file that needs to be emitted. The Deno compiler host delegates
12811375
// this, to make it easier to perform the right actions, which vary
@@ -1294,6 +1388,7 @@ async function compile(
12941388
writeFile = createCompileWriteFile(state);
12951389
}
12961390
const host = (state.host = new Host({
1391+
rootNames,
12971392
bundle,
12981393
target,
12991394
writeFile,
@@ -1323,25 +1418,29 @@ async function compile(
13231418
// to generate the program and possibly emit it.
13241419
if (diagnostics.length === 0) {
13251420
const options = host.getCompilationSettings();
1326-
const program = ts.createProgram({
1327-
rootNames,
1421+
const relativeRootNames = rootNames.map(host.getRelativePath.bind(host));
1422+
const creationTimer = new Timer("Create program");
1423+
const program = ts.createIncrementalProgram({
1424+
rootNames: relativeRootNames,
13281425
options,
13291426
host,
1330-
oldProgram: TS_SNAPSHOT_PROGRAM,
13311427
});
1428+
creationTimer.end();
13321429

1333-
diagnostics = ts
1334-
.getPreEmitDiagnostics(program)
1335-
.filter(({ code }) => !ignoredDiagnostics.includes(code));
1430+
const diagnosticsTimer = new Timer("Pre emit diagnostics");
1431+
diagnostics = getPreEmitDiagnostics(program);
1432+
diagnosticsTimer.end();
13361433

13371434
// We will only proceed with the emit if there are no diagnostics.
13381435
if (diagnostics && diagnostics.length === 0) {
13391436
if (bundle) {
13401437
// we only support a single root module when bundling
13411438
assert(resolvedRootModules.length === 1);
1342-
setRootExports(program, resolvedRootModules[0]);
1439+
// setRootExports(program, resolvedRootModules[0]);
13431440
}
1441+
const emitTimer = new Timer("Emit");
13441442
const emitResult = program.emit();
1443+
emitTimer.end();
13451444
assert(emitResult.emitSkipped === false, "Unexpected skip of the emit.");
13461445
// emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned
13471446
// without casting.
@@ -1360,9 +1459,11 @@ async function compile(
13601459
const result: CompileResult = {
13611460
emitMap: state.emitMap,
13621461
bundleOutput,
1462+
sources: SourceFile.urls(),
13631463
diagnostics: fromTypeScriptDiagnostic(diagnostics),
13641464
};
13651465

1466+
compileTimer.end();
13661467
util.log("<<< compile end", {
13671468
rootNames,
13681469
type: CompilerRequestType[request.type],

cli/js/timer.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { log } from "./util.ts";
2+
3+
export default class Timer {
4+
#startTime: number = new Date().getTime();
5+
#name?: string;
6+
constructor(name: string) {
7+
this.#name = name;
8+
}
9+
end(): void {
10+
const endTime = new Date().getTime();
11+
const seconds = (endTime - this.#startTime) / 1000;
12+
log(`${this.#name ?? "Time"}: ${seconds.toFixed(2)}s`);
13+
}
14+
}

0 commit comments

Comments
 (0)