mirror of
https://github.com/denoland/deno.git
synced 2024-12-27 01:29:14 -05:00
676 lines
20 KiB
Text
676 lines
20 KiB
Text
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
// @ts-check
|
|
/// <reference path="./compiler.d.ts" />
|
|
// deno-lint-ignore-file no-undef
|
|
|
|
// This module is the entry point for "compiler" isolate, ie. the one
|
|
// that is created when Deno needs to type check TypeScript, and in some
|
|
// instances convert TypeScript to JavaScript.
|
|
|
|
// Removes the `__proto__` for security reasons. This intentionally makes
|
|
// Deno non compliant with ECMA-262 Annex B.2.2.1
|
|
delete Object.prototype.__proto__;
|
|
|
|
((window) => {
|
|
/** @type {DenoCore} */
|
|
const core = window.Deno.core;
|
|
|
|
let logDebug = false;
|
|
let logSource = "JS";
|
|
|
|
function setLogDebug(debug, source) {
|
|
logDebug = debug;
|
|
if (source) {
|
|
logSource = source;
|
|
}
|
|
}
|
|
|
|
function debug(...args) {
|
|
if (logDebug) {
|
|
const stringifiedArgs = args.map((arg) =>
|
|
typeof arg === "string" ? arg : JSON.stringify(arg)
|
|
).join(" ");
|
|
// adding a non-zero integer value to the end of the debug string causes
|
|
// the message to be printed to stderr instead of stdout, which is better
|
|
// aligned to the behaviour of debug messages
|
|
core.print(`DEBUG ${logSource} - ${stringifiedArgs}\n`, 1);
|
|
}
|
|
}
|
|
|
|
function error(...args) {
|
|
const stringifiedArgs = args.map((arg) =>
|
|
typeof arg === "string" || arg instanceof Error
|
|
? String(arg)
|
|
: JSON.stringify(arg)
|
|
).join(" ");
|
|
core.print(`ERROR ${logSource} = ${stringifiedArgs}\n`, 1);
|
|
}
|
|
|
|
class AssertionError extends Error {
|
|
constructor(msg) {
|
|
super(msg);
|
|
this.name = "AssertionError";
|
|
}
|
|
}
|
|
|
|
function assert(cond, msg = "Assertion failed.") {
|
|
if (!cond) {
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
|
|
/** @type {Map<string, ts.SourceFile>} */
|
|
const sourceFileCache = new Map();
|
|
|
|
/** @param {ts.DiagnosticRelatedInformation} diagnostic */
|
|
function fromRelatedInformation({
|
|
start,
|
|
length,
|
|
file,
|
|
messageText: msgText,
|
|
...ri
|
|
}) {
|
|
let messageText;
|
|
let messageChain;
|
|
if (typeof msgText === "object") {
|
|
messageChain = msgText;
|
|
} else {
|
|
messageText = msgText;
|
|
}
|
|
if (start !== undefined && length !== undefined && file) {
|
|
const startPos = file.getLineAndCharacterOfPosition(start);
|
|
const sourceLine = file.getFullText().split("\n")[startPos.line];
|
|
const fileName = file.fileName;
|
|
return {
|
|
start: startPos,
|
|
end: file.getLineAndCharacterOfPosition(start + length),
|
|
fileName,
|
|
messageChain,
|
|
messageText,
|
|
sourceLine,
|
|
...ri,
|
|
};
|
|
} else {
|
|
return {
|
|
messageChain,
|
|
messageText,
|
|
...ri,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** @param {ts.Diagnostic[]} diagnostics */
|
|
function fromTypeScriptDiagnostic(diagnostics) {
|
|
return diagnostics.map(({ relatedInformation: ri, source, ...diag }) => {
|
|
/** @type {any} */
|
|
const value = fromRelatedInformation(diag);
|
|
value.relatedInformation = ri
|
|
? ri.map(fromRelatedInformation)
|
|
: undefined;
|
|
value.source = source;
|
|
return value;
|
|
});
|
|
}
|
|
|
|
// Using incremental compile APIs requires that all
|
|
// paths must be either relative or absolute. Since
|
|
// analysis in Rust operates on fully resolved URLs,
|
|
// it makes sense to use the same scheme here.
|
|
const ASSETS = "asset:///";
|
|
const CACHE = "cache:///";
|
|
|
|
/** Diagnostics that are intentionally ignored when compiling TypeScript in
|
|
* Deno, as they provide misleading or incorrect information. */
|
|
const IGNORED_DIAGNOSTICS = [
|
|
// TS1208: All files must be modules when the '--isolatedModules' flag is
|
|
// provided. We can ignore because we guarantee that all files are
|
|
// modules.
|
|
1208,
|
|
// TS1375: 'await' expressions are only allowed at the top level of a file
|
|
// when that file is a module, but this file has no imports or exports.
|
|
// Consider adding an empty 'export {}' to make this file a module.
|
|
1375,
|
|
// TS1103: 'for-await-of' statement is only allowed within an async function
|
|
// or async generator.
|
|
1103,
|
|
// TS2306: File 'file:///Users/rld/src/deno/subdir/amd_like.js' is
|
|
// not a module.
|
|
2306,
|
|
// TS2691: An import path cannot end with a '.ts' extension. Consider
|
|
// importing 'bad-module' instead.
|
|
2691,
|
|
// TS2792: Cannot find module. Did you mean to set the 'moduleResolution'
|
|
// option to 'node', or to add aliases to the 'paths' option?
|
|
2792,
|
|
// TS5009: Cannot find the common subdirectory path for the input files.
|
|
5009,
|
|
// TS5055: Cannot write file
|
|
// 'http://localhost:4545/subdir/mt_application_x_javascript.j4.js'
|
|
// because it would overwrite input file.
|
|
5055,
|
|
// TypeScript is overly opinionated that only CommonJS modules kinds can
|
|
// support JSON imports. Allegedly this was fixed in
|
|
// Microsoft/TypeScript#26825 but that doesn't seem to be working here,
|
|
// so we will ignore complaints about this compiler setting.
|
|
5070,
|
|
// TS7016: Could not find a declaration file for module '...'. '...'
|
|
// implicitly has an 'any' type. This is due to `allowJs` being off by
|
|
// default but importing of a JavaScript module.
|
|
7016,
|
|
];
|
|
|
|
const SNAPSHOT_COMPILE_OPTIONS = {
|
|
esModuleInterop: true,
|
|
jsx: ts.JsxEmit.React,
|
|
module: ts.ModuleKind.ESNext,
|
|
noEmit: true,
|
|
strict: true,
|
|
target: ts.ScriptTarget.ESNext,
|
|
};
|
|
|
|
class ScriptSnapshot {
|
|
/** @type {string} */
|
|
specifier;
|
|
/** @type {string} */
|
|
version;
|
|
/**
|
|
* @param {string} specifier
|
|
* @param {string} version
|
|
*/
|
|
constructor(specifier, version) {
|
|
this.specifier = specifier;
|
|
this.version = version;
|
|
}
|
|
/**
|
|
* @param {number} start
|
|
* @param {number} end
|
|
* @returns {string}
|
|
*/
|
|
getText(start, end) {
|
|
const { specifier, version } = this;
|
|
debug(
|
|
`snapshot.getText(${start}, ${end}) specifier: ${specifier} version: ${version}`,
|
|
);
|
|
return core.jsonOpSync("op_get_text", { specifier, version, start, end });
|
|
}
|
|
/**
|
|
* @returns {number}
|
|
*/
|
|
getLength() {
|
|
const { specifier, version } = this;
|
|
debug(`snapshot.getLength() specifier: ${specifier} version: ${version}`);
|
|
return core.jsonOpSync("op_get_length", { specifier, version });
|
|
}
|
|
/**
|
|
* @param {ScriptSnapshot} oldSnapshot
|
|
* @returns {ts.TextChangeRange | undefined}
|
|
*/
|
|
getChangeRange(oldSnapshot) {
|
|
const { specifier, version } = this;
|
|
const { version: oldVersion } = oldSnapshot;
|
|
const oldLength = oldSnapshot.getLength();
|
|
debug(
|
|
`snapshot.getLength() specifier: ${specifier} oldVersion: ${oldVersion} version: ${version}`,
|
|
);
|
|
return core.jsonOpSync(
|
|
"op_get_change_range",
|
|
{ specifier, oldLength, oldVersion, version },
|
|
);
|
|
}
|
|
dispose() {
|
|
const { specifier, version } = this;
|
|
debug(`snapshot.dispose() specifier: ${specifier} version: ${version}`);
|
|
core.jsonOpSync("op_dispose", { specifier, version });
|
|
}
|
|
}
|
|
|
|
/** @type {ts.CompilerOptions} */
|
|
let compilationSettings = {};
|
|
|
|
/** @type {ts.LanguageService} */
|
|
let languageService;
|
|
|
|
/** An object literal of the incremental compiler host, which provides the
|
|
* specific "bindings" to the Deno environment that tsc needs to work.
|
|
*
|
|
* @type {ts.CompilerHost & ts.LanguageServiceHost} */
|
|
const host = {
|
|
fileExists(fileName) {
|
|
debug(`host.fileExists("${fileName}")`);
|
|
return false;
|
|
},
|
|
readFile(specifier) {
|
|
debug(`host.readFile("${specifier}")`);
|
|
return core.jsonOpSync("op_load", { specifier }).data;
|
|
},
|
|
getSourceFile(
|
|
specifier,
|
|
languageVersion,
|
|
_onError,
|
|
_shouldCreateNewSourceFile,
|
|
) {
|
|
debug(
|
|
`host.getSourceFile("${specifier}", ${
|
|
ts.ScriptTarget[languageVersion]
|
|
})`,
|
|
);
|
|
let sourceFile = sourceFileCache.get(specifier);
|
|
if (sourceFile) {
|
|
return sourceFile;
|
|
}
|
|
|
|
/** @type {{ data: string; hash?: string; scriptKind: ts.ScriptKind }} */
|
|
const { data, hash, scriptKind } = core.jsonOpSync(
|
|
"op_load",
|
|
{ specifier },
|
|
);
|
|
assert(
|
|
data != null,
|
|
`"data" is unexpectedly null for "${specifier}".`,
|
|
);
|
|
sourceFile = ts.createSourceFile(
|
|
specifier,
|
|
data,
|
|
languageVersion,
|
|
false,
|
|
scriptKind,
|
|
);
|
|
sourceFile.moduleName = specifier;
|
|
sourceFile.version = hash;
|
|
sourceFileCache.set(specifier, sourceFile);
|
|
return sourceFile;
|
|
},
|
|
getDefaultLibFileName() {
|
|
return `${ASSETS}/lib.esnext.d.ts`;
|
|
},
|
|
getDefaultLibLocation() {
|
|
return ASSETS;
|
|
},
|
|
writeFile(fileName, data, _writeByteOrderMark, _onError, sourceFiles) {
|
|
debug(`host.writeFile("${fileName}")`);
|
|
let maybeSpecifiers;
|
|
if (sourceFiles) {
|
|
maybeSpecifiers = sourceFiles.map((sf) => sf.moduleName);
|
|
}
|
|
return core.jsonOpSync(
|
|
"op_emit",
|
|
{ maybeSpecifiers, fileName, data },
|
|
);
|
|
},
|
|
getCurrentDirectory() {
|
|
return CACHE;
|
|
},
|
|
getCanonicalFileName(fileName) {
|
|
return fileName;
|
|
},
|
|
useCaseSensitiveFileNames() {
|
|
return true;
|
|
},
|
|
getNewLine() {
|
|
return "\n";
|
|
},
|
|
resolveModuleNames(specifiers, base) {
|
|
debug(`host.resolveModuleNames()`);
|
|
debug(` base: ${base}`);
|
|
debug(` specifiers: ${specifiers.join(", ")}`);
|
|
/** @type {Array<[string, ts.Extension] | undefined>} */
|
|
const resolved = core.jsonOpSync("op_resolve", {
|
|
specifiers,
|
|
base,
|
|
});
|
|
if (resolved) {
|
|
const result = resolved.map((item) => {
|
|
if (item) {
|
|
const [resolvedFileName, extension] = item;
|
|
return {
|
|
resolvedFileName,
|
|
extension,
|
|
isExternalLibraryImport: false,
|
|
};
|
|
}
|
|
return undefined;
|
|
});
|
|
result.length = specifiers.length;
|
|
return result;
|
|
} else {
|
|
return new Array(specifiers.length);
|
|
}
|
|
},
|
|
createHash(data) {
|
|
return core.jsonOpSync("op_create_hash", { data }).hash;
|
|
},
|
|
|
|
// LanguageServiceHost
|
|
getCompilationSettings() {
|
|
debug("host.getCompilationSettings()");
|
|
return compilationSettings;
|
|
},
|
|
getScriptFileNames() {
|
|
debug("host.getScriptFileNames()");
|
|
return core.jsonOpSync("op_script_names", undefined);
|
|
},
|
|
getScriptVersion(specifier) {
|
|
debug(`host.getScriptVersion("${specifier}")`);
|
|
const sourceFile = sourceFileCache.get(specifier);
|
|
if (sourceFile) {
|
|
return sourceFile.version ?? "1";
|
|
}
|
|
return core.jsonOpSync("op_script_version", { specifier });
|
|
},
|
|
getScriptSnapshot(specifier) {
|
|
debug(`host.getScriptSnapshot("${specifier}")`);
|
|
const sourceFile = sourceFileCache.get(specifier);
|
|
if (sourceFile) {
|
|
return {
|
|
getText(start, end) {
|
|
return sourceFile.text.substring(start, end);
|
|
},
|
|
getLength() {
|
|
return sourceFile.text.length;
|
|
},
|
|
getChangeRange() {
|
|
return undefined;
|
|
},
|
|
};
|
|
}
|
|
/** @type {string | undefined} */
|
|
const version = core.jsonOpSync("op_script_version", { specifier });
|
|
if (version != null) {
|
|
return new ScriptSnapshot(specifier, version);
|
|
}
|
|
return undefined;
|
|
},
|
|
};
|
|
|
|
/** @type {Array<[string, number]>} */
|
|
const stats = [];
|
|
let statsStart = 0;
|
|
|
|
function performanceStart() {
|
|
stats.length = 0;
|
|
statsStart = Date.now();
|
|
ts.performance.enable();
|
|
}
|
|
|
|
/**
|
|
* @param {{ program: ts.Program | ts.EmitAndSemanticDiagnosticsBuilderProgram, fileCount?: number }} options
|
|
*/
|
|
function performanceProgram({ program, fileCount }) {
|
|
if (program) {
|
|
if ("getProgram" in program) {
|
|
program = program.getProgram();
|
|
}
|
|
stats.push(["Files", program.getSourceFiles().length]);
|
|
stats.push(["Nodes", program.getNodeCount()]);
|
|
stats.push(["Identifiers", program.getIdentifierCount()]);
|
|
stats.push(["Symbols", program.getSymbolCount()]);
|
|
stats.push(["Types", program.getTypeCount()]);
|
|
stats.push(["Instantiations", program.getInstantiationCount()]);
|
|
} else if (fileCount != null) {
|
|
stats.push(["Files", fileCount]);
|
|
}
|
|
const programTime = ts.performance.getDuration("Program");
|
|
const bindTime = ts.performance.getDuration("Bind");
|
|
const checkTime = ts.performance.getDuration("Check");
|
|
const emitTime = ts.performance.getDuration("Emit");
|
|
stats.push(["Parse time", programTime]);
|
|
stats.push(["Bind time", bindTime]);
|
|
stats.push(["Check time", checkTime]);
|
|
stats.push(["Emit time", emitTime]);
|
|
stats.push(
|
|
["Total TS time", programTime + bindTime + checkTime + emitTime],
|
|
);
|
|
}
|
|
|
|
function performanceEnd() {
|
|
const duration = Date.now() - statsStart;
|
|
stats.push(["Compile time", duration]);
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* @typedef {object} Request
|
|
* @property {Record<string, any>} config
|
|
* @property {boolean} debug
|
|
* @property {string[]} rootNames
|
|
*/
|
|
|
|
/** The API that is called by Rust when executing a request.
|
|
* @param {Request} request
|
|
*/
|
|
function exec({ config, debug: debugFlag, rootNames }) {
|
|
setLogDebug(debugFlag, "TS");
|
|
performanceStart();
|
|
debug(">>> exec start", { rootNames });
|
|
debug(config);
|
|
|
|
const { options, errors: configFileParsingDiagnostics } = ts
|
|
.convertCompilerOptionsFromJson(config, "");
|
|
// The `allowNonTsExtensions` is a "hidden" compiler option used in VSCode
|
|
// which is not allowed to be passed in JSON, we need it to allow special
|
|
// URLs which Deno supports. So we need to either ignore the diagnostic, or
|
|
// inject it ourselves.
|
|
Object.assign(options, { allowNonTsExtensions: true });
|
|
const program = ts.createIncrementalProgram({
|
|
rootNames,
|
|
options,
|
|
host,
|
|
configFileParsingDiagnostics,
|
|
});
|
|
|
|
const { diagnostics: emitDiagnostics } = program.emit();
|
|
|
|
const diagnostics = [
|
|
...program.getConfigFileParsingDiagnostics(),
|
|
...program.getSyntacticDiagnostics(),
|
|
...program.getOptionsDiagnostics(),
|
|
...program.getGlobalDiagnostics(),
|
|
...program.getSemanticDiagnostics(),
|
|
...emitDiagnostics,
|
|
].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code));
|
|
performanceProgram({ program });
|
|
|
|
core.jsonOpSync("op_respond", {
|
|
diagnostics: fromTypeScriptDiagnostic(diagnostics),
|
|
stats: performanceEnd(),
|
|
});
|
|
debug("<<< exec stop");
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
* @param {any} data
|
|
*/
|
|
function respond(id, data = null) {
|
|
core.jsonOpSync("op_respond", { id, data });
|
|
}
|
|
|
|
/**
|
|
* @param {LanguageServerRequest} request
|
|
*/
|
|
function serverRequest({ id, ...request }) {
|
|
debug(`serverRequest()`, { id, ...request });
|
|
switch (request.method) {
|
|
case "configure": {
|
|
const { options, errors } = ts
|
|
.convertCompilerOptionsFromJson(request.compilerOptions, "");
|
|
Object.assign(options, { allowNonTsExtensions: true });
|
|
if (errors.length) {
|
|
debug(ts.formatDiagnostics(errors, host));
|
|
}
|
|
compilationSettings = options;
|
|
return respond(id, true);
|
|
}
|
|
case "getAsset": {
|
|
const sourceFile = host.getSourceFile(
|
|
request.specifier,
|
|
ts.ScriptTarget.ESNext,
|
|
);
|
|
return respond(id, sourceFile && sourceFile.text);
|
|
}
|
|
case "getDiagnostics": {
|
|
try {
|
|
/** @type {Record<string, any[]>} */
|
|
const diagnosticMap = {};
|
|
for (const specifier of request.specifiers) {
|
|
diagnosticMap[specifier] = fromTypeScriptDiagnostic([
|
|
...languageService.getSemanticDiagnostics(specifier),
|
|
...languageService.getSuggestionDiagnostics(specifier),
|
|
...languageService.getSyntacticDiagnostics(specifier),
|
|
].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)));
|
|
}
|
|
return respond(id, diagnosticMap);
|
|
} catch (e) {
|
|
if ("stack" in e) {
|
|
error(e.stack);
|
|
} else {
|
|
error(e);
|
|
}
|
|
return respond(id, {});
|
|
}
|
|
}
|
|
case "getQuickInfo": {
|
|
return respond(
|
|
id,
|
|
languageService.getQuickInfoAtPosition(
|
|
request.specifier,
|
|
request.position,
|
|
),
|
|
);
|
|
}
|
|
case "getCompletions": {
|
|
return respond(
|
|
id,
|
|
languageService.getCompletionsAtPosition(
|
|
request.specifier,
|
|
request.position,
|
|
request.preferences,
|
|
),
|
|
);
|
|
}
|
|
case "getDocumentHighlights": {
|
|
return respond(
|
|
id,
|
|
languageService.getDocumentHighlights(
|
|
request.specifier,
|
|
request.position,
|
|
request.filesToSearch,
|
|
),
|
|
);
|
|
}
|
|
case "getReferences": {
|
|
return respond(
|
|
id,
|
|
languageService.getReferencesAtPosition(
|
|
request.specifier,
|
|
request.position,
|
|
),
|
|
);
|
|
}
|
|
case "getDefinition": {
|
|
return respond(
|
|
id,
|
|
languageService.getDefinitionAndBoundSpan(
|
|
request.specifier,
|
|
request.position,
|
|
),
|
|
);
|
|
}
|
|
case "getImplementation": {
|
|
return respond(
|
|
id,
|
|
languageService.getImplementationAtPosition(
|
|
request.specifier,
|
|
request.position,
|
|
),
|
|
);
|
|
}
|
|
case "findRenameLocations": {
|
|
return respond(
|
|
id,
|
|
languageService.findRenameLocations(
|
|
request.specifier,
|
|
request.position,
|
|
request.findInStrings,
|
|
request.findInComments,
|
|
request.providePrefixAndSuffixTextForRename,
|
|
),
|
|
);
|
|
}
|
|
default:
|
|
throw new TypeError(
|
|
// @ts-ignore exhausted case statement sets type to never
|
|
`Invalid request method for request: "${request.method}" (${id})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** @param {{ debug: boolean; }} init */
|
|
function serverInit({ debug: debugFlag }) {
|
|
if (hasStarted) {
|
|
throw new Error("The language server has already been initialized.");
|
|
}
|
|
hasStarted = true;
|
|
languageService = ts.createLanguageService(host);
|
|
core.ops();
|
|
setLogDebug(debugFlag, "TSLS");
|
|
debug("serverInit()");
|
|
}
|
|
|
|
let hasStarted = false;
|
|
|
|
/** Startup the runtime environment, setting various flags.
|
|
* @param {{ debugFlag?: boolean; legacyFlag?: boolean; }} msg
|
|
*/
|
|
function startup({ debugFlag = false }) {
|
|
if (hasStarted) {
|
|
throw new Error("The compiler runtime already started.");
|
|
}
|
|
hasStarted = true;
|
|
core.ops();
|
|
setLogDebug(!!debugFlag, "TS");
|
|
}
|
|
|
|
// Setup the compiler runtime during the build process.
|
|
core.ops();
|
|
core.registerErrorClass("Error", Error);
|
|
|
|
// A build time only op that provides some setup information that is used to
|
|
// ensure the snapshot is setup properly.
|
|
/** @type {{ buildSpecifier: string; libs: string[] }} */
|
|
const { buildSpecifier, libs } = core.jsonOpSync("op_build_info", {});
|
|
for (const lib of libs) {
|
|
const specifier = `lib.${lib}.d.ts`;
|
|
// we are using internal APIs here to "inject" our custom libraries into
|
|
// tsc, so things like `"lib": [ "deno.ns" ]` are supported.
|
|
if (!ts.libs.includes(lib)) {
|
|
ts.libs.push(lib);
|
|
ts.libMap.set(lib, `lib.${lib}.d.ts`);
|
|
}
|
|
// we are caching in memory common type libraries that will be re-used by
|
|
// tsc on when the snapshot is restored
|
|
assert(
|
|
host.getSourceFile(`${ASSETS}${specifier}`, ts.ScriptTarget.ESNext),
|
|
);
|
|
}
|
|
// this helps ensure as much as possible is in memory that is re-usable
|
|
// before the snapshotting is done, which helps unsure fast "startup" for
|
|
// subsequent uses of tsc in Deno.
|
|
const TS_SNAPSHOT_PROGRAM = ts.createProgram({
|
|
rootNames: [buildSpecifier],
|
|
options: SNAPSHOT_COMPILE_OPTIONS,
|
|
host,
|
|
});
|
|
ts.getPreEmitDiagnostics(TS_SNAPSHOT_PROGRAM);
|
|
|
|
// exposes the two functions that are called by `tsc::exec()` when type
|
|
// checking TypeScript.
|
|
globalThis.startup = startup;
|
|
globalThis.exec = exec;
|
|
|
|
// exposes the functions that are called when the compiler is used as a
|
|
// language service.
|
|
globalThis.serverInit = serverInit;
|
|
globalThis.serverRequest = serverRequest;
|
|
})(this);
|