mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00

Currently we evict a lot of the caches on the JS side of things on every request, namely script versions, script file names, and compiler settings (as of #23283, it's not quite every request but it's still unnecessarily often). This PR reports changes to the JS side, so that it can evict exactly the caches that it needs too. We might want to do some batching in the future so as not to do 1 request per change.
1244 lines
37 KiB
JavaScript
1244 lines
37 KiB
JavaScript
// Copyright 2018-2024 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",
|
|
"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();
|
|
|
|
/** @type {ts.CompilerOptions | null} */
|
|
let tsConfigCache = null;
|
|
|
|
/** @type {string | null} */
|
|
let projectVersionCache = null;
|
|
|
|
const ChangeKind = {
|
|
Opened: 0,
|
|
Modified: 1,
|
|
Closed: 2,
|
|
};
|
|
|
|
/**
|
|
* @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,
|
|
// in the lsp we want to be able to show documentation
|
|
jsDocParsingMode: ts.JSDocParsingMode.ParseAll,
|
|
},
|
|
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. ${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 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.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}")`);
|
|
}
|
|
// TODO(bartlomieju): is this assumption still valid?
|
|
// 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();
|
|
},
|
|
getProjectVersion() {
|
|
if (
|
|
projectVersionCache
|
|
) {
|
|
debug(`getProjectVersion cache hit : ${projectVersionCache}`);
|
|
return projectVersionCache;
|
|
}
|
|
const projectVersion = ops.op_project_version();
|
|
projectVersionCache = projectVersion;
|
|
debug(`getProjectVersion cache miss : ${projectVersionCache}`);
|
|
return projectVersion;
|
|
},
|
|
// @ts-ignore Undocumented method.
|
|
getModuleSpecifierCache() {
|
|
return moduleSpecifierCache;
|
|
},
|
|
// @ts-ignore Undocumented method.
|
|
getCachedExportInfoMap() {
|
|
return exportMapCache;
|
|
},
|
|
getGlobalTypingsCacheLocation() {
|
|
return undefined;
|
|
},
|
|
// @ts-ignore Undocumented method.
|
|
toPath(fileName) {
|
|
// @ts-ignore Undocumented function.
|
|
ts.toPath(
|
|
fileName,
|
|
this.getCurrentDirectory(),
|
|
this.getCanonicalFileName.bind(this),
|
|
);
|
|
},
|
|
// @ts-ignore Undocumented method.
|
|
watchNodeModulesForPackageJsonChanges() {
|
|
return { close() {} };
|
|
},
|
|
getSourceFile(
|
|
specifier,
|
|
languageVersion,
|
|
_onError,
|
|
_shouldCreateNewSourceFile,
|
|
) {
|
|
if (logDebug) {
|
|
debug(
|
|
`host.getSourceFile("${specifier}", ${
|
|
ts.ScriptTarget[
|
|
getCreateSourceFileOptions(languageVersion).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 fileInfo = ops.op_load(specifier);
|
|
if (!fileInfo) {
|
|
return undefined;
|
|
}
|
|
const { data, scriptKind, version } = fileInfo;
|
|
assert(
|
|
data != null,
|
|
`"data" is unexpectedly null for "${specifier}".`,
|
|
);
|
|
sourceFile = ts.createSourceFile(
|
|
specifier,
|
|
data,
|
|
{
|
|
...getCreateSourceFileOptions(languageVersion),
|
|
impliedNodeFormat: isCjsCache.has(specifier)
|
|
? ts.ModuleKind.CommonJS
|
|
: ts.ModuleKind.ESNext,
|
|
// no need to parse docs for `deno check`
|
|
jsDocParsingMode: ts.JSDocParsingMode.ParseForTypeErrors,
|
|
},
|
|
false,
|
|
scriptKind,
|
|
);
|
|
sourceFile.moduleName = specifier;
|
|
sourceFile.version = version;
|
|
if (specifier.startsWith(ASSETS_URL_PREFIX)) {
|
|
sourceFile.version = "1";
|
|
}
|
|
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()");
|
|
}
|
|
if (tsConfigCache) {
|
|
return tsConfigCache;
|
|
}
|
|
const tsConfig = normalizeConfig(ops.op_ts_config());
|
|
const { options, errors } = ts
|
|
.convertCompilerOptionsFromJson(tsConfig, "");
|
|
Object.assign(options, {
|
|
allowNonTsExtensions: true,
|
|
allowImportingTsExtensions: true,
|
|
});
|
|
if (errors.length > 0 && logDebug) {
|
|
debug(ts.formatDiagnostics(errors, host));
|
|
}
|
|
tsConfigCache = options;
|
|
return options;
|
|
},
|
|
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}")`);
|
|
}
|
|
if (specifier.startsWith(ASSETS_URL_PREFIX)) {
|
|
return "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}")`);
|
|
}
|
|
let sourceFile = sourceFileCache.get(specifier);
|
|
if (!sourceFile) {
|
|
sourceFile = this.getSourceFile(
|
|
specifier,
|
|
specifier.endsWith(".json")
|
|
? ts.ScriptTarget.JSON
|
|
: ts.ScriptTarget.ESNext,
|
|
);
|
|
}
|
|
if (sourceFile) {
|
|
return ts.ScriptSnapshot.fromString(sourceFile.text);
|
|
}
|
|
return undefined;
|
|
},
|
|
};
|
|
|
|
// @ts-ignore Undocumented function.
|
|
const moduleSpecifierCache = ts.server.createModuleSpecifierCache(host);
|
|
|
|
// @ts-ignore Undocumented function.
|
|
const exportMapCache = ts.createCacheableExportInfoMap(host);
|
|
|
|
// 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
|
|
*/
|
|
// TODO(bartlomieju): this feels needlessly generic, both type chcking
|
|
// and language server use it with inefficient serialization. Id is not used
|
|
// anyway...
|
|
function respond(id, data = null) {
|
|
ops.op_respond({ id, data });
|
|
}
|
|
|
|
function serverRequest(id, method, args) {
|
|
if (logDebug) {
|
|
debug(`serverRequest()`, id, method, args);
|
|
}
|
|
switch (method) {
|
|
case "$projectChanged": {
|
|
/** @type {[string, number][]} */
|
|
const changedScripts = args[0];
|
|
/** @type {string} */
|
|
const newProjectVersion = args[1];
|
|
/** @type {boolean} */
|
|
const configChanged = args[2];
|
|
|
|
if (configChanged) {
|
|
tsConfigCache = null;
|
|
}
|
|
|
|
projectVersionCache = newProjectVersion;
|
|
|
|
let opened = false;
|
|
for (const { 0: script, 1: changeKind } of changedScripts) {
|
|
if (changeKind == ChangeKind.Opened) {
|
|
opened = true;
|
|
}
|
|
scriptVersionCache.delete(script);
|
|
sourceFileCache.delete(script);
|
|
}
|
|
|
|
if (configChanged || opened) {
|
|
scriptFileNamesCache = undefined;
|
|
}
|
|
|
|
return respond(id);
|
|
}
|
|
case "$restart": {
|
|
serverRestart();
|
|
return respond(id, true);
|
|
}
|
|
case "$getSupportedCodeFixes": {
|
|
return respond(
|
|
id,
|
|
ts.getSupportedCodeFixes(),
|
|
);
|
|
}
|
|
case "$getAssets": {
|
|
return respond(id, getAssets());
|
|
}
|
|
case "$getDiagnostics": {
|
|
const projectVersion = args[1];
|
|
// there's a possibility that we receive a change notification
|
|
// but the diagnostic server queues a `$getDiagnostics` request
|
|
// with a stale project version. in that case, treat it as cancelled
|
|
// (it's about to be invalidated anyway).
|
|
if (projectVersionCache && projectVersion !== projectVersionCache) {
|
|
return respond(id, {});
|
|
}
|
|
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})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
let hasStarted = false;
|
|
/** @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()");
|
|
}
|
|
|
|
// 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.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);
|