summaryrefslogtreecommitdiff
path: root/js/compiler.ts
diff options
context:
space:
mode:
Diffstat (limited to 'js/compiler.ts')
-rw-r--r--js/compiler.ts371
1 files changed, 222 insertions, 149 deletions
diff --git a/js/compiler.ts b/js/compiler.ts
index 77d88dd8b..57e5e3a47 100644
--- a/js/compiler.ts
+++ b/js/compiler.ts
@@ -10,7 +10,7 @@ import { core } from "./core.ts";
import { Diagnostic, fromTypeScriptDiagnostic } from "./diagnostics.ts";
import { cwd } from "./dir.ts";
import * as dispatch from "./dispatch.ts";
-import { sendSync } from "./dispatch_json.ts";
+import { sendAsync, sendSync } from "./dispatch_json.ts";
import * as os from "./os.ts";
import { TextEncoder } from "./text_encoding.ts";
import { getMappedModuleName, parseTypeDirectives } from "./type_directives.ts";
@@ -121,12 +121,123 @@ const ignoredCompilerOptions: readonly string[] = [
"watch"
];
-interface SourceFile {
- moduleName: string | undefined;
- filename: string | undefined;
+/** The shape of the SourceFile that comes from the privileged side */
+interface SourceFileJson {
+ url: string;
+ filename: string;
mediaType: MediaType;
- sourceCode: string | undefined;
- typeDirectives?: Record<string, string>;
+ sourceCode: string;
+}
+
+/** A self registering abstraction of source files. */
+class SourceFile {
+ extension!: ts.Extension;
+ filename!: string;
+
+ /** An array of tuples which represent the imports for the source file. The
+ * first element is the one that will be requested at compile time, the
+ * second is the one that should be actually resolved. This provides the
+ * feature of type directives for Deno. */
+ importedFiles?: Array<[string, string]>;
+
+ mediaType!: MediaType;
+ processed = false;
+ sourceCode!: string;
+ tsSourceFile?: ts.SourceFile;
+ url!: string;
+
+ constructor(json: SourceFileJson) {
+ if (SourceFile._moduleCache.has(json.url)) {
+ throw new TypeError("SourceFile already exists");
+ }
+ Object.assign(this, json);
+ this.extension = getExtension(this.url, this.mediaType);
+ SourceFile._moduleCache.set(this.url, this);
+ }
+
+ /** Cache the source file to be able to be retrieved by `moduleSpecifier` and
+ * `containingFile`. */
+ cache(moduleSpecifier: string, containingFile: string): void {
+ let innerCache = SourceFile._specifierCache.get(containingFile);
+ if (!innerCache) {
+ innerCache = new Map();
+ SourceFile._specifierCache.set(containingFile, innerCache);
+ }
+ innerCache.set(moduleSpecifier, this);
+ }
+
+ /** Process the imports for the file and return them. */
+ imports(): Array<[string, string]> {
+ if (this.processed) {
+ throw new Error("SourceFile has already been processed.");
+ }
+ assert(this.sourceCode != null);
+ const preProcessedFileInfo = ts.preProcessFile(
+ this.sourceCode!,
+ true,
+ true
+ );
+ this.processed = true;
+ const files = (this.importedFiles = [] as Array<[string, string]>);
+
+ function process(references: ts.FileReference[]): void {
+ for (const { fileName } of references) {
+ files.push([fileName, fileName]);
+ }
+ }
+
+ const {
+ importedFiles,
+ referencedFiles,
+ libReferenceDirectives,
+ typeReferenceDirectives
+ } = preProcessedFileInfo;
+ const typeDirectives = parseTypeDirectives(this.sourceCode);
+ if (typeDirectives) {
+ for (const importedFile of importedFiles) {
+ files.push([
+ importedFile.fileName,
+ getMappedModuleName(importedFile, typeDirectives)
+ ]);
+ }
+ } else {
+ process(importedFiles);
+ }
+ process(referencedFiles);
+ process(libReferenceDirectives);
+ process(typeReferenceDirectives);
+ return files;
+ }
+
+ /** A cache of all the source files which have been loaded indexed by the
+ * url. */
+ private static _moduleCache: Map<string, SourceFile> = new Map();
+
+ /** A cache of source files based on module specifiers and containing files
+ * which is used by the TypeScript compiler to resolve the url */
+ private static _specifierCache: Map<
+ string,
+ Map<string, SourceFile>
+ > = new Map();
+
+ /** Retrieve a `SourceFile` based on a `moduleSpecifier` and `containingFile`
+ * or return `undefined` if not preset. */
+ static getUrl(
+ moduleSpecifier: string,
+ containingFile: string
+ ): string | undefined {
+ const containingCache = this._specifierCache.get(containingFile);
+ if (containingCache) {
+ const sourceFile = containingCache.get(moduleSpecifier);
+ return sourceFile && sourceFile.url;
+ }
+ return undefined;
+ }
+
+ /** Retrieve a `SourceFile` based on a `url` */
+ static get(url: string): SourceFile | undefined {
+ return this._moduleCache.get(url);
+ }
}
interface EmitResult {
@@ -134,6 +245,7 @@ interface EmitResult {
diagnostics?: Diagnostic;
}
+/** Ops to Rust to resolve special static assets. */
function fetchAsset(name: string): string {
return sendSync(dispatch.OP_FETCH_ASSET, { name });
}
@@ -142,21 +254,39 @@ function fetchAsset(name: string): string {
function fetchSourceFiles(
specifiers: string[],
referrer: string
-): SourceFile[] {
- util.log("compiler.fetchSourceFiles", { specifiers, referrer });
- const res = sendSync(dispatch.OP_FETCH_SOURCE_FILES, {
+): Promise<SourceFileJson[]> {
+ util.log("compiler::fetchSourceFiles", { specifiers, referrer });
+ return sendAsync(dispatch.OP_FETCH_SOURCE_FILES, {
specifiers,
referrer
});
+}
- return res.map(
- (sourceFile: SourceFile): SourceFile => {
- return {
- ...sourceFile,
- typeDirectives: parseTypeDirectives(sourceFile.sourceCode)
- };
+/** Recursively process the imports of modules, generating `SourceFile`s of any
+ * imported files.
+ *
+ * Specifiers are supplied in an array of tupples where the first is the
+ * specifier that will be requested in the code and the second is the specifier
+ * that should be actually resolved. */
+async function processImports(
+ specifiers: Array<[string, string]>,
+ referrer = ""
+): Promise<void> {
+ if (!specifiers.length) {
+ return;
+ }
+ const sources = specifiers.map(([, moduleSpecifier]) => moduleSpecifier);
+ const sourceFiles = await fetchSourceFiles(sources, referrer);
+ assert(sourceFiles.length === specifiers.length);
+ for (let i = 0; i < sourceFiles.length; i++) {
+ const sourceFileJson = sourceFiles[i];
+ const sourceFile =
+ SourceFile.get(sourceFileJson.url) || new SourceFile(sourceFileJson);
+ sourceFile.cache(specifiers[i][0], referrer);
+ if (!sourceFile.processed) {
+ await processImports(sourceFile.imports(), sourceFile.url);
}
- );
+ }
}
/** Utility function to turn the number of bytes into a human readable
@@ -177,6 +307,7 @@ function humanFileSize(bytes: number): string {
/** Ops to rest for caching source map and compiled js */
function cache(extension: string, moduleId: string, contents: string): void {
+ util.log("compiler::cache", { extension, moduleId });
sendSync(dispatch.OP_CACHE, { extension, moduleId, contents });
}
@@ -215,8 +346,6 @@ function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
}
class Host implements ts.CompilerHost {
- private _extensionCache: Record<string, ts.Extension> = {};
-
private readonly _options: ts.CompilerOptions = {
allowJs: true,
allowNonTsExtensions: true,
@@ -231,71 +360,20 @@ class Host implements ts.CompilerHost {
jsx: ts.JsxEmit.React
};
- private _sourceFileCache: Record<string, SourceFile> = {};
-
- private _getAsset(specifier: string): SourceFile {
- const moduleName = specifier.split("/").pop()!;
- if (moduleName in this._sourceFileCache) {
- return this._sourceFileCache[moduleName];
+ private _getAsset(filename: string): SourceFile {
+ const sourceFile = SourceFile.get(filename);
+ if (sourceFile) {
+ return sourceFile;
}
- const assetName = moduleName.includes(".")
- ? moduleName
- : `${moduleName}.d.ts`;
+ const url = filename.split("/").pop()!;
+ const assetName = url.includes(".") ? url : `${url}.d.ts`;
const sourceCode = fetchAsset(assetName);
- const sourceFile = {
- moduleName,
- filename: specifier,
+ return new SourceFile({
+ url,
+ filename,
mediaType: MediaType.TypeScript,
sourceCode
- };
- this._sourceFileCache[moduleName] = sourceFile;
- return sourceFile;
- }
-
- private _resolveModule(specifier: string, referrer: string): SourceFile {
- return this._resolveModules([specifier], referrer)[0];
- }
-
- private _resolveModules(
- specifiers: string[],
- referrer: string
- ): SourceFile[] {
- util.log("host._resolveModules", { specifiers, referrer });
- const resolvedModules: Array<SourceFile | undefined> = [];
- const modulesToRequest = [];
-
- for (const specifier of specifiers) {
- // Firstly built-in assets are handled specially, so they should
- // be removed from array of files that we'll be requesting from Rust.
- if (specifier.startsWith(ASSETS)) {
- const assetFile = this._getAsset(specifier);
- resolvedModules.push(assetFile);
- } else if (specifier in this._sourceFileCache) {
- const module = this._sourceFileCache[specifier];
- resolvedModules.push(module);
- } else {
- // Temporarily fill with undefined, after fetching file from
- // Rust it will be filled with proper value.
- resolvedModules.push(undefined);
- modulesToRequest.push(specifier);
- }
- }
-
- // Now get files from Rust.
- const sourceFiles = fetchSourceFiles(modulesToRequest, referrer);
-
- for (const sourceFile of sourceFiles) {
- assert(sourceFile.moduleName != null);
- const { moduleName } = sourceFile;
- if (!(moduleName! in this._sourceFileCache)) {
- this._sourceFileCache[moduleName!] = sourceFile;
- }
- // And fill temporary `undefined`s with actual files.
- const index = resolvedModules.indexOf(undefined);
- resolvedModules[index] = sourceFile;
- }
-
- return resolvedModules as SourceFile[];
+ });
}
/* Deno specific APIs */
@@ -321,10 +399,9 @@ class Host implements ts.CompilerHost {
/** Take a configuration string, parse it, and use it to merge with the
* compiler's configuration options. The method returns an array of compiler
- * options which were ignored, or `undefined`.
- */
+ * options which were ignored, or `undefined`. */
configure(path: string, configurationText: string): ConfigureResponse {
- util.log("host.configure", path);
+ util.log("compiler::host.configure", path);
const { config, error } = ts.parseConfigFileTextToJson(
path,
configurationText
@@ -355,20 +432,16 @@ class Host implements ts.CompilerHost {
/* TypeScript CompilerHost APIs */
- fileExists(fileName: string): boolean {
- if (fileName.endsWith("package.json")) {
- throw new TypeError("Automatic type resolution not supported");
- }
+ fileExists(_fileName: string): boolean {
return notImplemented();
}
getCanonicalFileName(fileName: string): string {
- // console.log("getCanonicalFileName", fileName);
return fileName;
}
getCompilationSettings(): ts.CompilerOptions {
- util.log("getCompilationSettings()");
+ util.log("compiler::host.getCompilationSettings()");
return this._options;
}
@@ -390,21 +463,29 @@ class Host implements ts.CompilerHost {
onError?: (message: string) => void,
shouldCreateNewSourceFile?: boolean
): ts.SourceFile | undefined {
- assert(!shouldCreateNewSourceFile);
- util.log("getSourceFile", fileName);
- const sourceFile =
- fileName in this._sourceFileCache
- ? this._sourceFileCache[fileName]
- : this._resolveModule(fileName, ".");
- assert(sourceFile != null);
- if (!sourceFile.sourceCode) {
+ util.log("compiler::host.getSourceFile", fileName);
+ try {
+ assert(!shouldCreateNewSourceFile);
+ const sourceFile = fileName.startsWith(ASSETS)
+ ? this._getAsset(fileName)
+ : SourceFile.get(fileName);
+ assert(sourceFile != null);
+ if (!sourceFile!.tsSourceFile) {
+ sourceFile!.tsSourceFile = ts.createSourceFile(
+ fileName,
+ sourceFile!.sourceCode,
+ languageVersion
+ );
+ }
+ return sourceFile!.tsSourceFile;
+ } catch (e) {
+ if (onError) {
+ onError(String(e));
+ } else {
+ throw e;
+ }
return undefined;
}
- return ts.createSourceFile(
- fileName,
- sourceFile.sourceCode,
- languageVersion
- );
}
readFile(_fileName: string): string | undefined {
@@ -415,46 +496,26 @@ class Host implements ts.CompilerHost {
moduleNames: string[],
containingFile: string
): Array<ts.ResolvedModuleFull | undefined> {
- util.log("resolveModuleNames()", { moduleNames, containingFile });
- const typeDirectives: Record<string, string> | undefined =
- containingFile in this._sourceFileCache
- ? this._sourceFileCache[containingFile].typeDirectives
+ util.log("compiler::host.resolveModuleNames", {
+ moduleNames,
+ containingFile
+ });
+ return moduleNames.map(specifier => {
+ const url = SourceFile.getUrl(specifier, containingFile);
+ const sourceFile = specifier.startsWith(ASSETS)
+ ? this._getAsset(specifier)
+ : url
+ ? SourceFile.get(url)
: undefined;
-
- const mappedModuleNames = moduleNames.map(
- (moduleName: string): string => {
- return getMappedModuleName(moduleName, containingFile, typeDirectives);
+ if (!sourceFile) {
+ return undefined;
}
- );
-
- return this._resolveModules(mappedModuleNames, containingFile).map(
- (
- sourceFile: SourceFile,
- index: number
- ): ts.ResolvedModuleFull | undefined => {
- if (sourceFile.moduleName) {
- const resolvedFileName = sourceFile.moduleName;
- // This flags to the compiler to not go looking to transpile functional
- // code, anything that is in `/$asset$/` is just library code
- const isExternalLibraryImport = mappedModuleNames[index].startsWith(
- ASSETS
- );
- const extension = getExtension(
- resolvedFileName,
- sourceFile.mediaType
- );
- this._extensionCache[resolvedFileName] = extension;
-
- return {
- resolvedFileName,
- isExternalLibraryImport,
- extension
- };
- } else {
- return undefined;
- }
- }
- );
+ return {
+ resolvedFileName: sourceFile.url,
+ isExternalLibraryImport: specifier.startsWith(ASSETS),
+ extension: sourceFile.extension
+ };
+ });
}
useCaseSensitiveFileNames(): boolean {
@@ -464,39 +525,42 @@ class Host implements ts.CompilerHost {
writeFile(
fileName: string,
data: string,
- writeByteOrderMark: boolean,
+ _writeByteOrderMark: boolean,
onError?: (message: string) => void,
sourceFiles?: readonly ts.SourceFile[]
): void {
- util.log("writeFile", fileName);
+ util.log("compiler::host.writeFile", fileName);
try {
if (this._bundle) {
emitBundle(this._bundle, data);
} else {
assert(sourceFiles != null && sourceFiles.length == 1);
- const sourceFileName = sourceFiles![0].fileName;
- const maybeExtension = this._extensionCache[sourceFileName];
+ const url = sourceFiles![0].fileName;
+ const sourceFile = SourceFile.get(url);
- if (maybeExtension) {
+ if (sourceFile) {
// NOTE: If it's a `.json` file we don't want to write it to disk.
// JSON files are loaded and used by TS compiler to check types, but we don't want
// to emit them to disk because output file is the same as input file.
- if (maybeExtension === ts.Extension.Json) {
+ if (sourceFile.extension === ts.Extension.Json) {
return;
}
// NOTE: JavaScript files are only emitted to disk if `checkJs` option in on
- if (maybeExtension === ts.Extension.Js && !this._options.checkJs) {
+ if (
+ sourceFile.extension === ts.Extension.Js &&
+ !this._options.checkJs
+ ) {
return;
}
}
if (fileName.endsWith(".map")) {
// Source Map
- cache(".map", sourceFileName, data);
+ cache(".map", url, data);
} else if (fileName.endsWith(".js") || fileName.endsWith(".json")) {
// Compiled JavaScript
- cache(".js", sourceFileName, data);
+ cache(".js", url, data);
} else {
assert(false, "Trying to cache unhandled file type " + fileName);
}
@@ -515,8 +579,15 @@ class Host implements ts.CompilerHost {
// lazy instantiating the compiler web worker
window.compilerMain = function compilerMain(): void {
// workerMain should have already been called since a compiler is a worker.
- window.onmessage = ({ data }: { data: CompilerReq }): void => {
+ window.onmessage = async ({ data }: { data: CompilerReq }): Promise<void> => {
const { rootNames, configPath, config, bundle } = data;
+ util.log(">>> compile start", { rootNames, bundle });
+
+ // This will recursively analyse all the code for other imports, requesting
+ // those from the privileged side, populating the in memory cache which
+ // will be used by the host, before resolving.
+ await processImports(rootNames.map(rootName => [rootName, rootName]));
+
const host = new Host(bundle);
let emitSkipped = true;
let diagnostics: ts.Diagnostic[] | undefined;
@@ -588,6 +659,8 @@ window.compilerMain = function compilerMain(): void {
postMessage(result);
+ util.log("<<< compile end", { rootNames, bundle });
+
// The compiler isolate exits after a single message.
workerClose();
};