mirror of
https://github.com/denoland/deno.git
synced 2024-12-26 17:19:06 -05:00
c803c0eaab
This commit removes "ThrottledCancellationToken" in favor of "CancellationToken". Since calling into Rust to check if Tokio's cancellation token has already been canceled is really cheap, there's no need for us to throttle this check and let TSC burn up CPU with heavy computation.
1173 lines
35 KiB
JavaScript
1173 lines
35 KiB
JavaScript
// 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.
|
|
// https://tc39.es/ecma262/#sec-get-object.prototype.__proto__
|
|
delete Object.prototype.__proto__;
|
|
|
|
((/** @type {any} */ window) => {
|
|
/** @type {DenoCore} */
|
|
const core = window.Deno.core;
|
|
const ops = core.ops;
|
|
|
|
let logDebug = false;
|
|
let logSource = "JS";
|
|
|
|
// The map from the normalized specifier to the original.
|
|
// TypeScript normalizes the specifier in its internal processing,
|
|
// but the original specifier is needed when looking up the source from the runtime.
|
|
// This map stores that relationship, and the original can be restored by the
|
|
// normalized specifier.
|
|
// See: https://github.com/denoland/deno/issues/9277#issuecomment-769653834
|
|
/** @type {Map<string, string>} */
|
|
const normalizedToOriginalMap = new Map();
|
|
|
|
/** @type {ReadonlySet<string>} */
|
|
const unstableDenoProps = new Set([
|
|
"AtomicOperation",
|
|
"CreateHttpClientOptions",
|
|
"DatagramConn",
|
|
"HttpClient",
|
|
"Kv",
|
|
"KvListIterator",
|
|
"KvU64",
|
|
"UnsafeCallback",
|
|
"UnsafePointer",
|
|
"UnsafePointerView",
|
|
"UnsafeFnPointer",
|
|
"UnixConnectOptions",
|
|
"UnixListenOptions",
|
|
"createHttpClient",
|
|
"dlopen",
|
|
"flock",
|
|
"flockSync",
|
|
"funlock",
|
|
"funlockSync",
|
|
"listen",
|
|
"listenDatagram",
|
|
"openKv",
|
|
"upgradeHttp",
|
|
"umask",
|
|
]);
|
|
const unstableMsgSuggestion =
|
|
"If not, try changing the 'lib' compiler option to include 'deno.unstable' " +
|
|
"or add a triple-slash directive to the top of your entrypoint (main file): " +
|
|
'/// <reference lib="deno.unstable" />';
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
* @returns {value is ts.CreateSourceFileOptions}
|
|
*/
|
|
function isCreateSourceFileOptions(value) {
|
|
return value != null && typeof value === "object" &&
|
|
"languageVersion" in value;
|
|
}
|
|
|
|
/**
|
|
* @param {ts.ScriptTarget | ts.CreateSourceFileOptions | undefined} versionOrOptions
|
|
* @returns {ts.CreateSourceFileOptions}
|
|
*/
|
|
function getCreateSourceFileOptions(versionOrOptions) {
|
|
return isCreateSourceFileOptions(versionOrOptions)
|
|
? versionOrOptions
|
|
: { languageVersion: versionOrOptions ?? ts.ScriptTarget.ESNext };
|
|
}
|
|
|
|
/**
|
|
* @param debug {boolean}
|
|
* @param source {string}
|
|
*/
|
|
function setLogDebug(debug, source) {
|
|
logDebug = debug;
|
|
if (source) {
|
|
logSource = source;
|
|
}
|
|
}
|
|
|
|
/** @param msg {string} */
|
|
function printStderr(msg) {
|
|
core.print(msg, true);
|
|
}
|
|
|
|
/** @param args {any[]} */
|
|
function debug(...args) {
|
|
if (logDebug) {
|
|
const stringifiedArgs = args.map((arg) =>
|
|
typeof arg === "string" ? arg : JSON.stringify(arg)
|
|
).join(" ");
|
|
printStderr(`DEBUG ${logSource} - ${stringifiedArgs}\n`);
|
|
}
|
|
}
|
|
|
|
/** @param args {any[]} */
|
|
function error(...args) {
|
|
const stringifiedArgs = args.map((arg) =>
|
|
typeof arg === "string" || arg instanceof Error
|
|
? String(arg)
|
|
: JSON.stringify(arg)
|
|
).join(" ");
|
|
printStderr(`ERROR ${logSource} = ${stringifiedArgs}\n`);
|
|
}
|
|
|
|
class AssertionError extends Error {
|
|
/** @param msg {string} */
|
|
constructor(msg) {
|
|
super(msg);
|
|
this.name = "AssertionError";
|
|
}
|
|
}
|
|
|
|
/** @param cond {boolean} */
|
|
function assert(cond, msg = "Assertion failed.") {
|
|
if (!cond) {
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
|
|
class SpecifierIsCjsCache {
|
|
/** @type {Set<string>} */
|
|
#cache = new Set();
|
|
|
|
/** @param {[string, ts.Extension]} param */
|
|
add([specifier, ext]) {
|
|
if (ext === ".cjs" || ext === ".d.cts" || ext === ".cts") {
|
|
this.#cache.add(specifier);
|
|
}
|
|
}
|
|
|
|
/** @param specifier {string} */
|
|
has(specifier) {
|
|
return this.#cache.has(specifier);
|
|
}
|
|
}
|
|
|
|
// In the case of the LSP, this will only ever contain the assets.
|
|
/** @type {Map<string, ts.SourceFile>} */
|
|
const sourceFileCache = new Map();
|
|
|
|
/** @type {string[]=} */
|
|
let scriptFileNamesCache;
|
|
|
|
/** @type {Map<string, string>} */
|
|
const scriptVersionCache = new Map();
|
|
|
|
/** @type {Map<string, boolean>} */
|
|
const isNodeSourceFileCache = new Map();
|
|
|
|
const isCjsCache = new SpecifierIsCjsCache();
|
|
|
|
/**
|
|
* @param {ts.CompilerOptions | ts.MinimalResolutionCacheHost} settingsOrHost
|
|
* @returns {ts.CompilerOptions}
|
|
*/
|
|
function getCompilationSettings(settingsOrHost) {
|
|
if (typeof settingsOrHost.getCompilationSettings === "function") {
|
|
return settingsOrHost.getCompilationSettings();
|
|
}
|
|
return /** @type {ts.CompilerOptions} */ (settingsOrHost);
|
|
}
|
|
|
|
// We need to use a custom document registry in order to provide source files
|
|
// with an impliedNodeFormat to the ts language service
|
|
|
|
/** @type {Map<string, ts.SourceFile>} */
|
|
const documentRegistrySourceFileCache = new Map();
|
|
const { getKeyForCompilationSettings } = ts.createDocumentRegistry(); // reuse this code
|
|
/** @type {ts.DocumentRegistry} */
|
|
const documentRegistry = {
|
|
acquireDocument(
|
|
fileName,
|
|
compilationSettingsOrHost,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptKind,
|
|
sourceFileOptions,
|
|
) {
|
|
const key = getKeyForCompilationSettings(
|
|
getCompilationSettings(compilationSettingsOrHost),
|
|
);
|
|
return this.acquireDocumentWithKey(
|
|
fileName,
|
|
/** @type {ts.Path} */ (fileName),
|
|
compilationSettingsOrHost,
|
|
key,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptKind,
|
|
sourceFileOptions,
|
|
);
|
|
},
|
|
|
|
acquireDocumentWithKey(
|
|
fileName,
|
|
path,
|
|
_compilationSettingsOrHost,
|
|
key,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptKind,
|
|
sourceFileOptions,
|
|
) {
|
|
const mapKey = path + key;
|
|
let sourceFile = documentRegistrySourceFileCache.get(mapKey);
|
|
if (!sourceFile || sourceFile.version !== version) {
|
|
sourceFile = ts.createLanguageServiceSourceFile(
|
|
fileName,
|
|
scriptSnapshot,
|
|
{
|
|
...getCreateSourceFileOptions(sourceFileOptions),
|
|
impliedNodeFormat: isCjsCache.has(fileName)
|
|
? ts.ModuleKind.CommonJS
|
|
: ts.ModuleKind.ESNext,
|
|
},
|
|
version,
|
|
true,
|
|
scriptKind,
|
|
);
|
|
documentRegistrySourceFileCache.set(mapKey, sourceFile);
|
|
}
|
|
return sourceFile;
|
|
},
|
|
|
|
updateDocument(
|
|
fileName,
|
|
compilationSettingsOrHost,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptKind,
|
|
sourceFileOptions,
|
|
) {
|
|
const key = getKeyForCompilationSettings(
|
|
getCompilationSettings(compilationSettingsOrHost),
|
|
);
|
|
return this.updateDocumentWithKey(
|
|
fileName,
|
|
/** @type {ts.Path} */ (fileName),
|
|
compilationSettingsOrHost,
|
|
key,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptKind,
|
|
sourceFileOptions,
|
|
);
|
|
},
|
|
|
|
updateDocumentWithKey(
|
|
fileName,
|
|
path,
|
|
compilationSettingsOrHost,
|
|
key,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptKind,
|
|
sourceFileOptions,
|
|
) {
|
|
const mapKey = path + key;
|
|
let sourceFile = documentRegistrySourceFileCache.get(mapKey) ??
|
|
this.acquireDocumentWithKey(
|
|
fileName,
|
|
path,
|
|
compilationSettingsOrHost,
|
|
key,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptKind,
|
|
sourceFileOptions,
|
|
);
|
|
|
|
if (sourceFile.version !== version) {
|
|
sourceFile = ts.updateLanguageServiceSourceFile(
|
|
sourceFile,
|
|
scriptSnapshot,
|
|
version,
|
|
scriptSnapshot.getChangeRange(
|
|
/** @type {ts.IScriptSnapshot} */ (sourceFile.scriptSnapShot),
|
|
),
|
|
);
|
|
}
|
|
return sourceFile;
|
|
},
|
|
|
|
getKeyForCompilationSettings(settings) {
|
|
return getKeyForCompilationSettings(settings);
|
|
},
|
|
|
|
releaseDocument(
|
|
fileName,
|
|
compilationSettings,
|
|
scriptKind,
|
|
impliedNodeFormat,
|
|
) {
|
|
const key = getKeyForCompilationSettings(compilationSettings);
|
|
return this.releaseDocumentWithKey(
|
|
/** @type {ts.Path} */ (fileName),
|
|
key,
|
|
scriptKind,
|
|
impliedNodeFormat,
|
|
);
|
|
},
|
|
|
|
releaseDocumentWithKey(path, key, _scriptKind, _impliedNodeFormat) {
|
|
const mapKey = path + key;
|
|
documentRegistrySourceFileCache.delete(mapKey);
|
|
},
|
|
|
|
reportStats() {
|
|
return "[]";
|
|
},
|
|
};
|
|
|
|
ts.deno.setIsNodeSourceFileCallback((sourceFile) => {
|
|
const fileName = sourceFile.fileName;
|
|
let isNodeSourceFile = isNodeSourceFileCache.get(fileName);
|
|
if (isNodeSourceFile == null) {
|
|
const result = ops.op_is_node_file(fileName);
|
|
isNodeSourceFile = /** @type {boolean} */ (result);
|
|
isNodeSourceFileCache.set(fileName, isNodeSourceFile);
|
|
}
|
|
return isNodeSourceFile;
|
|
});
|
|
|
|
/**
|
|
* @param msg {string}
|
|
* @param code {number}
|
|
*/
|
|
function formatMessage(msg, code) {
|
|
switch (code) {
|
|
case 2304: {
|
|
if (msg === "Cannot find name 'Deno'.") {
|
|
msg += " Do you need to change your target library? " +
|
|
"Try changing the 'lib' compiler option to include 'deno.ns' " +
|
|
"or add a triple-slash directive to the top of your entrypoint " +
|
|
'(main file): /// <reference lib="deno.ns" />';
|
|
}
|
|
return msg;
|
|
}
|
|
case 2339: {
|
|
const property = getProperty();
|
|
if (property && unstableDenoProps.has(property)) {
|
|
return `${msg} 'Deno.${property}' is an unstable API. Did you forget to run with the '--unstable' flag? ${unstableMsgSuggestion}`;
|
|
}
|
|
return msg;
|
|
}
|
|
default: {
|
|
const property = getProperty();
|
|
if (property && unstableDenoProps.has(property)) {
|
|
const suggestion = getMsgSuggestion();
|
|
if (suggestion) {
|
|
return `${msg} 'Deno.${property}' is an unstable API. Did you forget to run with the '--unstable' flag, or did you mean '${suggestion}'? ${unstableMsgSuggestion}`;
|
|
}
|
|
}
|
|
return msg;
|
|
}
|
|
}
|
|
|
|
function getProperty() {
|
|
return /Property '([^']+)' does not exist on type 'typeof Deno'/
|
|
.exec(msg)?.[1];
|
|
}
|
|
|
|
function getMsgSuggestion() {
|
|
return / Did you mean '([^']+)'\?/.exec(msg)?.[1];
|
|
}
|
|
}
|
|
|
|
/** @param {ts.DiagnosticRelatedInformation} diagnostic */
|
|
function fromRelatedInformation({
|
|
start,
|
|
length,
|
|
file,
|
|
messageText: msgText,
|
|
...ri
|
|
}) {
|
|
let messageText;
|
|
let messageChain;
|
|
if (typeof msgText === "object") {
|
|
messageChain = msgText;
|
|
} else {
|
|
messageText = formatMessage(msgText, ri.code);
|
|
}
|
|
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 {readonly ts.Diagnostic[]} diagnostics */
|
|
function fromTypeScriptDiagnostics(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_URL_PREFIX = "asset:///";
|
|
const CACHE_URL_PREFIX = "cache:///";
|
|
|
|
/** Diagnostics that are intentionally ignored when compiling TypeScript in
|
|
* Deno, as they provide misleading or incorrect information. */
|
|
const IGNORED_DIAGNOSTICS = [
|
|
// TS1452: 'resolution-mode' assertions are only supported when `moduleResolution` is `node16` or `nodenext`.
|
|
// We specify the resolution mode to be CommonJS for some npm files and this
|
|
// diagnostic gets generated even though we're using custom module resolution.
|
|
1452,
|
|
// TS2306: File '.../index.d.ts' is not a module.
|
|
// We get this for `x-typescript-types` declaration files which don't export
|
|
// anything. We prefer to treat these as modules with no exports.
|
|
2306,
|
|
// TS2688: Cannot find type definition file for '...'.
|
|
// We ignore because type defintion files can end with '.ts'.
|
|
2688,
|
|
// 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,
|
|
lib: ["lib.deno.window.d.ts"],
|
|
};
|
|
|
|
// todo(dsherret): can we remove this and just use ts.OperationCanceledException?
|
|
/** Error thrown on cancellation. */
|
|
class OperationCanceledError extends Error {
|
|
}
|
|
|
|
/**
|
|
* This implementation calls into Rust to check if Tokio's cancellation token
|
|
* has already been canceled.
|
|
* @implements {ts.CancellationToken}
|
|
*/
|
|
class CancellationToken {
|
|
isCancellationRequested() {
|
|
return ops.op_is_cancelled();
|
|
}
|
|
|
|
throwIfCancellationRequested() {
|
|
if (this.isCancellationRequested()) {
|
|
throw new OperationCanceledError();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @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(specifier) {
|
|
if (logDebug) {
|
|
debug(`host.fileExists("${specifier}")`);
|
|
}
|
|
// this is used by typescript to find the libs path
|
|
// so we can completely ignore it
|
|
return false;
|
|
},
|
|
readFile(specifier) {
|
|
if (logDebug) {
|
|
debug(`host.readFile("${specifier}")`);
|
|
}
|
|
return ops.op_load({ specifier }).data;
|
|
},
|
|
getCancellationToken() {
|
|
// createLanguageService will call this immediately and cache it
|
|
return new CancellationToken();
|
|
},
|
|
getSourceFile(
|
|
specifier,
|
|
languageVersion,
|
|
_onError,
|
|
_shouldCreateNewSourceFile,
|
|
) {
|
|
const createOptions = getCreateSourceFileOptions(languageVersion);
|
|
if (logDebug) {
|
|
debug(
|
|
`host.getSourceFile("${specifier}", ${
|
|
ts.ScriptTarget[createOptions.languageVersion]
|
|
})`,
|
|
);
|
|
}
|
|
|
|
// Needs the original specifier
|
|
specifier = normalizedToOriginalMap.get(specifier) ?? specifier;
|
|
|
|
let sourceFile = sourceFileCache.get(specifier);
|
|
if (sourceFile) {
|
|
return sourceFile;
|
|
}
|
|
|
|
/** @type {{ data: string; scriptKind: ts.ScriptKind; version: string; }} */
|
|
const { data, scriptKind, version } = ops.op_load(
|
|
{ specifier },
|
|
);
|
|
assert(
|
|
data != null,
|
|
`"data" is unexpectedly null for "${specifier}".`,
|
|
);
|
|
sourceFile = ts.createSourceFile(
|
|
specifier,
|
|
data,
|
|
{
|
|
...createOptions,
|
|
impliedNodeFormat: isCjsCache.has(specifier)
|
|
? ts.ModuleKind.CommonJS
|
|
: ts.ModuleKind.ESNext,
|
|
},
|
|
false,
|
|
scriptKind,
|
|
);
|
|
sourceFile.moduleName = specifier;
|
|
sourceFile.version = version;
|
|
sourceFileCache.set(specifier, sourceFile);
|
|
scriptVersionCache.set(specifier, version);
|
|
return sourceFile;
|
|
},
|
|
getDefaultLibFileName() {
|
|
return `${ASSETS_URL_PREFIX}lib.esnext.d.ts`;
|
|
},
|
|
getDefaultLibLocation() {
|
|
return ASSETS_URL_PREFIX;
|
|
},
|
|
writeFile(fileName, data, _writeByteOrderMark, _onError, _sourceFiles) {
|
|
if (logDebug) {
|
|
debug(`host.writeFile("${fileName}")`);
|
|
}
|
|
return ops.op_emit(
|
|
{ fileName, data },
|
|
);
|
|
},
|
|
getCurrentDirectory() {
|
|
if (logDebug) {
|
|
debug(`host.getCurrentDirectory()`);
|
|
}
|
|
return CACHE_URL_PREFIX;
|
|
},
|
|
getCanonicalFileName(fileName) {
|
|
return fileName;
|
|
},
|
|
useCaseSensitiveFileNames() {
|
|
return true;
|
|
},
|
|
getNewLine() {
|
|
return "\n";
|
|
},
|
|
resolveTypeReferenceDirectives(
|
|
typeDirectiveNames,
|
|
containingFilePath,
|
|
redirectedReference,
|
|
options,
|
|
containingFileMode,
|
|
) {
|
|
return typeDirectiveNames.map((arg) => {
|
|
/** @type {ts.FileReference} */
|
|
const fileReference = typeof arg === "string"
|
|
? {
|
|
pos: -1,
|
|
end: -1,
|
|
fileName: arg,
|
|
}
|
|
: arg;
|
|
if (fileReference.fileName.startsWith("npm:")) {
|
|
/** @type {[string, ts.Extension] | undefined} */
|
|
const resolved = ops.op_resolve({
|
|
specifiers: [fileReference.fileName],
|
|
base: containingFilePath,
|
|
})?.[0];
|
|
if (resolved) {
|
|
isCjsCache.add(resolved);
|
|
return {
|
|
primary: true,
|
|
resolvedFileName: resolved[0],
|
|
};
|
|
} else {
|
|
return undefined;
|
|
}
|
|
} else {
|
|
return ts.resolveTypeReferenceDirective(
|
|
fileReference.fileName,
|
|
containingFilePath,
|
|
options,
|
|
host,
|
|
redirectedReference,
|
|
undefined,
|
|
containingFileMode ?? fileReference.resolutionMode,
|
|
).resolvedTypeReferenceDirective;
|
|
}
|
|
});
|
|
},
|
|
resolveModuleNames(specifiers, base) {
|
|
if (logDebug) {
|
|
debug(`host.resolveModuleNames()`);
|
|
debug(` base: ${base}`);
|
|
debug(` specifiers: ${specifiers.join(", ")}`);
|
|
}
|
|
/** @type {Array<[string, ts.Extension] | undefined>} */
|
|
const resolved = ops.op_resolve({
|
|
specifiers,
|
|
base,
|
|
});
|
|
if (resolved) {
|
|
const result = resolved.map((item) => {
|
|
if (item) {
|
|
isCjsCache.add(item);
|
|
const [resolvedFileName, extension] = item;
|
|
if (resolvedFileName.startsWith("node:")) {
|
|
// probably means the user doesn't have @types/node, so resolve to undefined
|
|
return undefined;
|
|
}
|
|
return {
|
|
resolvedFileName,
|
|
extension,
|
|
isExternalLibraryImport: false,
|
|
};
|
|
}
|
|
return undefined;
|
|
});
|
|
result.length = specifiers.length;
|
|
return result;
|
|
} else {
|
|
return new Array(specifiers.length);
|
|
}
|
|
},
|
|
createHash(data) {
|
|
return ops.op_create_hash(data);
|
|
},
|
|
|
|
// LanguageServiceHost
|
|
getCompilationSettings() {
|
|
if (logDebug) {
|
|
debug("host.getCompilationSettings()");
|
|
}
|
|
return compilationSettings;
|
|
},
|
|
getScriptFileNames() {
|
|
if (logDebug) {
|
|
debug("host.getScriptFileNames()");
|
|
}
|
|
// tsc requests the script file names multiple times even though it can't
|
|
// possibly have changed, so we will memoize it on a per request basis.
|
|
if (scriptFileNamesCache) {
|
|
return scriptFileNamesCache;
|
|
}
|
|
return scriptFileNamesCache = ops.op_script_names();
|
|
},
|
|
getScriptVersion(specifier) {
|
|
if (logDebug) {
|
|
debug(`host.getScriptVersion("${specifier}")`);
|
|
}
|
|
const sourceFile = sourceFileCache.get(specifier);
|
|
if (sourceFile) {
|
|
return sourceFile.version ?? "1";
|
|
}
|
|
// tsc requests the script version multiple times even though it can't
|
|
// possibly have changed, so we will memoize it on a per request basis.
|
|
if (scriptVersionCache.has(specifier)) {
|
|
return scriptVersionCache.get(specifier);
|
|
}
|
|
const scriptVersion = ops.op_script_version({ specifier });
|
|
scriptVersionCache.set(specifier, scriptVersion);
|
|
return scriptVersion;
|
|
},
|
|
getScriptSnapshot(specifier) {
|
|
if (logDebug) {
|
|
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;
|
|
},
|
|
};
|
|
}
|
|
|
|
const fileInfo = ops.op_load(
|
|
{ specifier },
|
|
);
|
|
if (fileInfo) {
|
|
scriptVersionCache.set(specifier, fileInfo.version);
|
|
return ts.ScriptSnapshot.fromString(fileInfo.data);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
},
|
|
};
|
|
|
|
// override the npm install @types package diagnostics to be deno specific
|
|
ts.setLocalizedDiagnosticMessages((() => {
|
|
const nodeMessage = "Cannot find name '{0}'."; // don't offer any suggestions
|
|
const jqueryMessage =
|
|
"Cannot find name '{0}'. Did you mean to import jQuery? Try adding `import $ from \"npm:jquery\";`.";
|
|
return {
|
|
"Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashno_2580":
|
|
nodeMessage,
|
|
"Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashno_2591":
|
|
nodeMessage,
|
|
"Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slash_2581":
|
|
jqueryMessage,
|
|
"Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slash_2592":
|
|
jqueryMessage,
|
|
};
|
|
})());
|
|
|
|
/** @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
|
|
* @property {boolean} localOnly
|
|
*/
|
|
|
|
/**
|
|
* Checks the normalized version of the root name and stores it in
|
|
* `normalizedToOriginalMap`. If the normalized specifier is already
|
|
* registered for the different root name, it throws an AssertionError.
|
|
*
|
|
* @param {string} rootName
|
|
*/
|
|
function checkNormalizedPath(rootName) {
|
|
const normalized = ts.normalizePath(rootName);
|
|
const originalRootName = normalizedToOriginalMap.get(normalized);
|
|
if (typeof originalRootName === "undefined") {
|
|
normalizedToOriginalMap.set(normalized, rootName);
|
|
} else if (originalRootName !== rootName) {
|
|
// The different root names are normalizd to the same path.
|
|
// This will cause problem when looking up the source for each.
|
|
throw new AssertionError(
|
|
`The different names for the same normalized specifier are specified: normalized=${normalized}, rootNames=${originalRootName},${rootName}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** @param {Record<string, string>} config */
|
|
function normalizeConfig(config) {
|
|
// the typescript compiler doesn't know about the precompile
|
|
// transform at the moment, so just tell it we're using react-jsx
|
|
if (config.jsx === "precompile") {
|
|
config.jsx = "react-jsx";
|
|
}
|
|
return config;
|
|
}
|
|
|
|
/** The API that is called by Rust when executing a request.
|
|
* @param {Request} request
|
|
*/
|
|
function exec({ config, debug: debugFlag, rootNames, localOnly }) {
|
|
setLogDebug(debugFlag, "TS");
|
|
performanceStart();
|
|
|
|
config = normalizeConfig(config);
|
|
|
|
if (logDebug) {
|
|
debug(">>> exec start", { rootNames });
|
|
debug(config);
|
|
}
|
|
|
|
rootNames.forEach(checkNormalizedPath);
|
|
|
|
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 checkFiles = localOnly
|
|
? rootNames
|
|
.filter((n) => !n.startsWith("http"))
|
|
.map((checkName) => {
|
|
const sourceFile = program.getSourceFile(checkName);
|
|
if (sourceFile == null) {
|
|
throw new Error("Could not find source file for: " + checkName);
|
|
}
|
|
return sourceFile;
|
|
})
|
|
: undefined;
|
|
|
|
if (checkFiles != null) {
|
|
// When calling program.getSemanticDiagnostics(...) with a source file, we
|
|
// need to call this code first in order to get it to invalidate cached
|
|
// diagnostics correctly. This is what program.getSemanticDiagnostics()
|
|
// does internally when calling without any arguments.
|
|
const checkFileNames = new Set(checkFiles.map((f) => f.fileName));
|
|
while (
|
|
program.getSemanticDiagnosticsOfNextAffectedFile(
|
|
undefined,
|
|
/* ignoreSourceFile */ (s) => !checkFileNames.has(s.fileName),
|
|
)
|
|
) {
|
|
// keep going until there are no more affected files
|
|
}
|
|
}
|
|
|
|
const diagnostics = [
|
|
...program.getConfigFileParsingDiagnostics(),
|
|
...(checkFiles == null
|
|
? program.getSyntacticDiagnostics()
|
|
: ts.sortAndDeduplicateDiagnostics(
|
|
checkFiles.map((s) => program.getSyntacticDiagnostics(s)).flat(),
|
|
)),
|
|
...program.getOptionsDiagnostics(),
|
|
...program.getGlobalDiagnostics(),
|
|
...(checkFiles == null
|
|
? program.getSemanticDiagnostics()
|
|
: ts.sortAndDeduplicateDiagnostics(
|
|
checkFiles.map((s) => program.getSemanticDiagnostics(s)).flat(),
|
|
)),
|
|
].filter((diagnostic) => !IGNORED_DIAGNOSTICS.includes(diagnostic.code));
|
|
|
|
// emit the tsbuildinfo file
|
|
// @ts-ignore: emitBuildInfo is not exposed (https://github.com/microsoft/TypeScript/issues/49871)
|
|
program.emitBuildInfo(host.writeFile);
|
|
|
|
performanceProgram({ program });
|
|
|
|
ops.op_respond({
|
|
diagnostics: fromTypeScriptDiagnostics(diagnostics),
|
|
stats: performanceEnd(),
|
|
});
|
|
debug("<<< exec stop");
|
|
}
|
|
|
|
function getAssets() {
|
|
/** @type {{ specifier: string; text: string; }[]} */
|
|
const assets = [];
|
|
for (const sourceFile of sourceFileCache.values()) {
|
|
if (sourceFile.fileName.startsWith(ASSETS_URL_PREFIX)) {
|
|
assets.push({
|
|
specifier: sourceFile.fileName,
|
|
text: sourceFile.text,
|
|
});
|
|
}
|
|
}
|
|
return assets;
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
* @param {any} data
|
|
*/
|
|
function respond(id, data = null) {
|
|
ops.op_respond({ id, data });
|
|
}
|
|
|
|
function serverRequest(id, method, args) {
|
|
if (logDebug) {
|
|
debug(`serverRequest()`, id, method, args);
|
|
}
|
|
|
|
// reset all memoized source files names
|
|
scriptFileNamesCache = undefined;
|
|
// evict all memoized source file versions
|
|
scriptVersionCache.clear();
|
|
switch (method) {
|
|
case "$restart": {
|
|
serverRestart();
|
|
return respond(id, true);
|
|
}
|
|
case "$configure": {
|
|
const config = normalizeConfig(args[0]);
|
|
const { options, errors } = ts
|
|
.convertCompilerOptionsFromJson(config, "");
|
|
Object.assign(options, {
|
|
allowNonTsExtensions: true,
|
|
allowImportingTsExtensions: true,
|
|
});
|
|
if (errors.length > 0 && logDebug) {
|
|
debug(ts.formatDiagnostics(errors, host));
|
|
}
|
|
compilationSettings = options;
|
|
return respond(id, true);
|
|
}
|
|
case "$getSupportedCodeFixes": {
|
|
return respond(
|
|
id,
|
|
ts.getSupportedCodeFixes(),
|
|
);
|
|
}
|
|
case "$getAssets": {
|
|
return respond(id, getAssets());
|
|
}
|
|
case "$getDiagnostics": {
|
|
try {
|
|
/** @type {Record<string, any[]>} */
|
|
const diagnosticMap = {};
|
|
for (const specifier of args[0]) {
|
|
diagnosticMap[specifier] = fromTypeScriptDiagnostics([
|
|
...languageService.getSemanticDiagnostics(specifier),
|
|
...languageService.getSuggestionDiagnostics(specifier),
|
|
...languageService.getSyntacticDiagnostics(specifier),
|
|
].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)));
|
|
}
|
|
return respond(id, diagnosticMap);
|
|
} catch (e) {
|
|
if (
|
|
!(e instanceof OperationCanceledError ||
|
|
e instanceof ts.OperationCanceledException)
|
|
) {
|
|
if ("stack" in e) {
|
|
error(e.stack);
|
|
} else {
|
|
error(e);
|
|
}
|
|
}
|
|
return respond(id, {});
|
|
}
|
|
}
|
|
default:
|
|
if (typeof languageService[method] === "function") {
|
|
// The `getCompletionEntryDetails()` method returns null if the
|
|
// `source` is `null` for whatever reason. It must be `undefined`.
|
|
if (method == "getCompletionEntryDetails") {
|
|
args[4] ??= undefined;
|
|
}
|
|
return respond(id, languageService[method](...args));
|
|
}
|
|
throw new TypeError(
|
|
// @ts-ignore exhausted case statement sets type to never
|
|
`Invalid request method for 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, documentRegistry);
|
|
setLogDebug(debugFlag, "TSLS");
|
|
debug("serverInit()");
|
|
}
|
|
|
|
function serverRestart() {
|
|
languageService = ts.createLanguageService(host, documentRegistry);
|
|
isNodeSourceFileCache.clear();
|
|
debug("serverRestart()");
|
|
}
|
|
|
|
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;
|
|
setLogDebug(!!debugFlag, "TS");
|
|
}
|
|
|
|
// 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[]; nodeBuiltInModuleNames: string[] }} */
|
|
const { buildSpecifier, libs, nodeBuiltInModuleNames } = ops.op_build_info();
|
|
|
|
ts.deno.setNodeBuiltInModuleNames(nodeBuiltInModuleNames);
|
|
|
|
// list of globals that should be kept in Node's globalThis
|
|
ts.deno.setNodeOnlyGlobalNames([
|
|
// when bumping the @types/node version we should check if
|
|
// anything needs to be updated here
|
|
"NodeRequire",
|
|
"RequireResolve",
|
|
"RequireResolve",
|
|
"process",
|
|
"console",
|
|
"__filename",
|
|
"__dirname",
|
|
"require",
|
|
"module",
|
|
"exports",
|
|
"gc",
|
|
"BufferEncoding",
|
|
"BufferConstructor",
|
|
"WithImplicitCoercion",
|
|
"Buffer",
|
|
"Console",
|
|
"ImportMeta",
|
|
"setTimeout",
|
|
"setInterval",
|
|
"setImmediate",
|
|
"Global",
|
|
"AbortController",
|
|
"AbortSignal",
|
|
"Blob",
|
|
"BroadcastChannel",
|
|
"MessageChannel",
|
|
"MessagePort",
|
|
"Event",
|
|
"EventTarget",
|
|
"performance",
|
|
"TextDecoder",
|
|
"TextEncoder",
|
|
"URL",
|
|
"URLSearchParams",
|
|
]);
|
|
|
|
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_URL_PREFIX}${specifier}`,
|
|
ts.ScriptTarget.ESNext,
|
|
),
|
|
`failed to load '${ASSETS_URL_PREFIX}${specifier}'`,
|
|
);
|
|
}
|
|
// 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,
|
|
});
|
|
assert(
|
|
ts.getPreEmitDiagnostics(TS_SNAPSHOT_PROGRAM).length === 0,
|
|
"lib.d.ts files have errors",
|
|
);
|
|
|
|
// remove this now that we don't need it anymore for warming up tsc
|
|
sourceFileCache.delete(buildSpecifier);
|
|
|
|
// exposes the functions that are called by `tsc::exec()` when type
|
|
// checking TypeScript.
|
|
/** @type {any} */
|
|
const global = globalThis;
|
|
global.startup = startup;
|
|
global.exec = exec;
|
|
global.getAssets = getAssets;
|
|
|
|
// exposes the functions that are called when the compiler is used as a
|
|
// language service.
|
|
global.serverInit = serverInit;
|
|
global.serverRequest = serverRequest;
|
|
})(this);
|