0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-02-07 23:06:50 -05:00

refactor(tsc): split TS compiler into multiple files, use ESM (#27784)

This is a pure refactor, the `99_main_compiler.js` file
was getting out of hand, being over 1500 lines and serving
3 distinct purposes:
- snapshotting
- type-checking
- running LSP

The file was split into:
- 97_ts_host.js
- 98_lsp.js
- 99_main_compiler.js
This commit is contained in:
Bartek Iwańczuk 2025-01-23 00:37:50 +00:00 committed by GitHub
parent 80a6179ac4
commit ab18dac09d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1576 additions and 1480 deletions

View file

@ -122,10 +122,16 @@ mod ts {
deno_core::extension!(deno_tsc,
ops = [op_build_info, op_is_node_file, op_load, op_script_version],
esm_entry_point = "ext:deno_tsc/99_main_compiler.js",
esm = [
dir "tsc",
"97_ts_host.js",
"98_lsp.js",
"99_main_compiler.js",
],
js = [
dir "tsc",
"00_typescript.js",
"99_main_compiler.js",
],
options = {
op_crate_libs: HashMap<&'static str, PathBuf>,

832
cli/tsc/97_ts_host.js Normal file
View file

@ -0,0 +1,832 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// @ts-check
/// <reference path="./compiler.d.ts" />
// deno-lint-ignore-file no-undef
/** @type {DenoCore} */
const core = globalThis.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",
"DatagramConn",
"Kv",
"KvListIterator",
"KvU64",
"UnixConnectOptions",
"UnixListenOptions",
"listen",
"listenDatagram",
"openKv",
"connectQuic",
"listenQuic",
"QuicBidirectionalStream",
"QuicConn",
"QuicListener",
"QuicReceiveStream",
"QuicSendStream",
]);
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}
*/
export function getCreateSourceFileOptions(versionOrOptions) {
return isCreateSourceFileOptions(versionOrOptions)
? versionOrOptions
: { languageVersion: versionOrOptions ?? ts.ScriptTarget.ESNext };
}
/**
* @param debug {boolean}
* @param source {string}
*/
export function setLogDebug(debug, source) {
logDebug = debug;
if (source) {
logSource = source;
}
}
/** @param msg {string} */
function printStderr(msg) {
core.print(msg, true);
}
/** @param args {any[]} */
export 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[]} */
export 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`);
}
export class AssertionError extends Error {
/** @param msg {string} */
constructor(msg) {
super(msg);
this.name = "AssertionError";
}
}
/** @param cond {boolean} */
export function assert(cond, msg = "Assertion failed.") {
if (!cond) {
throw new AssertionError(msg);
}
}
// In the case of the LSP, this will only ever contain the assets.
/** @type {Map<string, ts.SourceFile>} */
export const SOURCE_FILE_CACHE = new Map();
/** @type {Map<string, ts.IScriptSnapshot & { isCjs?: boolean; }>} */
export const SCRIPT_SNAPSHOT_CACHE = new Map();
/** @type {Map<string, number>} */
export const SOURCE_REF_COUNTS = new Map();
/** @type {Map<string, string>} */
export const SCRIPT_VERSION_CACHE = new Map();
/** @type {Map<string, boolean>} */
export const IS_NODE_SOURCE_FILE_CACHE = new Map();
// Maps asset specifiers to the first scope that the asset was loaded into.
/** @type {Map<string, string | null>} */
export const ASSET_SCOPES = new Map();
/** @type {number | null} */
let projectVersionCache = null;
export const PROJECT_VERSION_CACHE = {
get: () => projectVersionCache,
set: (version) => {
projectVersionCache = version;
},
};
/** @type {string | null} */
let lastRequestMethod = null;
export const LAST_REQUEST_METHOD = {
get: () => lastRequestMethod,
set: (method) => {
lastRequestMethod = method;
},
};
/** @type {string | null} */
let lastRequestScope = null;
export const LAST_REQUEST_SCOPE = {
get: () => lastRequestScope,
set: (scope) => {
lastRequestScope = scope;
},
};
ts.deno.setIsNodeSourceFileCallback((sourceFile) => {
const fileName = sourceFile.fileName;
let isNodeSourceFile = IS_NODE_SOURCE_FILE_CACHE.get(fileName);
if (isNodeSourceFile == null) {
const result = ops.op_is_node_file(fileName);
isNodeSourceFile = /** @type {boolean} */ (result);
IS_NODE_SOURCE_FILE_CACHE.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) {
let startPos = file.getLineAndCharacterOfPosition(start);
let sourceLine = file.getFullText().split("\n")[startPos.line];
const originalFileName = file.fileName;
const fileName = ops.op_remap_specifier
? (ops.op_remap_specifier(file.fileName) ?? file.fileName)
: file.fileName;
// Bit of a hack to detect when we have a .wasm file and want to hide
// the .d.ts text. This is not perfect, but will work in most scenarios
if (
fileName.endsWith(".wasm") && originalFileName.endsWith(".wasm.d.mts")
) {
startPos = { line: 0, character: 0 };
sourceLine = undefined;
}
return {
start: startPos,
end: file.getLineAndCharacterOfPosition(start + length),
fileName,
messageChain,
messageText,
sourceLine,
...ri,
};
} else {
return {
messageChain,
messageText,
...ri,
};
}
}
/** @param {readonly ts.Diagnostic[]} diagnostics */
export 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.
export 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,
// Module '...' cannot be imported using this construct. The specifier only resolves to an
// ES module, which cannot be imported with 'require'.
1471,
// TS1479: The current file is a CommonJS module whose imports will produce 'require' calls;
// however, the referenced file is an ECMAScript module and cannot be imported with 'require'.
1479,
// 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 definition 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,
// TS2307: Cannot find module '{0}' or its corresponding type declarations.
2307,
// Relative import errors to add an extension
2834,
2835,
// 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,
// TS6053: File '{0}' not found.
6053,
// 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,
];
// todo(dsherret): can we remove this and just use ts.OperationCanceledException?
/** Error thrown on cancellation. */
export 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();
}
}
}
/** @typedef {{
* ls: ts.LanguageService & { [k:string]: any },
* compilerOptions: ts.CompilerOptions,
* }} LanguageServiceEntry */
/** @type {{ unscoped: LanguageServiceEntry, byScope: Map<string, LanguageServiceEntry> }} */
export const LANGUAGE_SERVICE_ENTRIES = {
// @ts-ignore Will be set later.
unscoped: null,
byScope: new Map(),
};
/** @type {{ unscoped: string[], byScope: Map<string, string[]> } | null} */
let SCRIPT_NAMES_CACHE = null;
export function clearScriptNamesCache() {
SCRIPT_NAMES_CACHE = null;
}
/** 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} */
export 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() {
const cachedProjectVersion = PROJECT_VERSION_CACHE.get();
if (
cachedProjectVersion
) {
debug(`getProjectVersion cache hit : ${cachedProjectVersion}`);
return cachedProjectVersion;
}
const projectVersion = ops.op_project_version();
PROJECT_VERSION_CACHE.set(projectVersion);
debug(`getProjectVersion cache miss : ${projectVersion}`);
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,
// this is not used by the lsp because source
// files are created in the document registry
_shouldCreateNewSourceFile,
) {
if (logDebug) {
debug(
`host.getSourceFile("${specifier}", ${
ts.ScriptTarget[
getCreateSourceFileOptions(languageVersion).languageVersion
]
})`,
);
}
// Needs the original specifier
specifier = normalizedToOriginalMap.get(specifier) ?? specifier;
let sourceFile = SOURCE_FILE_CACHE.get(specifier);
if (sourceFile) {
return sourceFile;
}
/** @type {{ data: string; scriptKind: ts.ScriptKind; version: string; isCjs: boolean }} */
const fileInfo = ops.op_load(specifier);
if (!fileInfo) {
return undefined;
}
const { data, scriptKind, version, isCjs } = fileInfo;
assert(
data != null,
`"data" is unexpectedly null for "${specifier}".`,
);
sourceFile = ts.createSourceFile(
specifier,
data,
{
...getCreateSourceFileOptions(languageVersion),
impliedNodeFormat: isCjs
? 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";
}
SOURCE_FILE_CACHE.set(specifier, sourceFile);
SCRIPT_VERSION_CACHE.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(
data,
fileName,
);
},
getCurrentDirectory() {
if (logDebug) {
debug(`host.getCurrentDirectory()`);
}
return CACHE_URL_PREFIX;
},
getCanonicalFileName(fileName) {
return fileName;
},
useCaseSensitiveFileNames() {
return true;
},
getNewLine() {
return "\n";
},
resolveTypeReferenceDirectiveReferences(
typeDirectiveReferences,
containingFilePath,
_redirectedReference,
options,
containingSourceFile,
_reusedNames,
) {
const isCjs =
containingSourceFile?.impliedNodeFormat === ts.ModuleKind.CommonJS;
const toResolve = typeDirectiveReferences.map((arg) => {
/** @type {ts.FileReference} */
const fileReference = typeof arg === "string"
? {
pos: -1,
end: -1,
fileName: arg,
}
: arg;
return [
fileReference.resolutionMode == null
? isCjs
: fileReference.resolutionMode === ts.ModuleKind.CommonJS,
fileReference.fileName,
];
});
/** @type {Array<[string, ts.Extension | null] | undefined>} */
const resolved = ops.op_resolve(
containingFilePath,
toResolve,
);
/** @type {Array<ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations>} */
const result = resolved.map((item) => {
if (item && item[1]) {
const [resolvedFileName, extension] = item;
return {
resolvedTypeReferenceDirective: {
primary: true,
resolvedFileName,
extension,
isExternalLibraryImport: false,
},
};
} else {
return {
resolvedTypeReferenceDirective: undefined,
};
}
});
if (logDebug) {
debug(
"resolveTypeReferenceDirectiveReferences ",
typeDirectiveReferences,
containingFilePath,
options,
containingSourceFile?.fileName,
" => ",
result,
);
}
return result;
},
resolveModuleNameLiterals(
moduleLiterals,
base,
_redirectedReference,
compilerOptions,
containingSourceFile,
_reusedNames,
) {
const specifiers = moduleLiterals.map((literal) => [
ts.getModeForUsageLocation(
containingSourceFile,
literal,
compilerOptions,
) === ts.ModuleKind.CommonJS,
literal.text,
]);
if (logDebug) {
debug(`host.resolveModuleNames()`);
debug(` base: ${base}`);
debug(` specifiers: ${specifiers.map((s) => s[1]).join(", ")}`);
}
/** @type {Array<[string, ts.Extension | null] | undefined>} */
const resolved = ops.op_resolve(
base,
specifiers,
);
if (resolved) {
/** @type {Array<ts.ResolvedModuleWithFailedLookupLocations>} */
const result = resolved.map((item) => {
if (item && item[1]) {
const [resolvedFileName, extension] = item;
return {
resolvedModule: {
resolvedFileName,
extension,
// todo(dsherret): we should probably be setting this
isExternalLibraryImport: false,
},
};
}
return {
resolvedModule: 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()");
}
const lastRequestScope = LAST_REQUEST_SCOPE.get();
return (lastRequestScope
? LANGUAGE_SERVICE_ENTRIES.byScope.get(lastRequestScope)
?.compilerOptions
: null) ?? LANGUAGE_SERVICE_ENTRIES.unscoped.compilerOptions;
},
getScriptFileNames() {
if (logDebug) {
debug("host.getScriptFileNames()");
}
if (!SCRIPT_NAMES_CACHE) {
const { unscoped, byScope } = ops.op_script_names();
SCRIPT_NAMES_CACHE = {
unscoped,
byScope: new Map(Object.entries(byScope)),
};
}
const lastRequestScope = LAST_REQUEST_SCOPE.get();
return (lastRequestScope
? SCRIPT_NAMES_CACHE.byScope.get(lastRequestScope)
: null) ?? SCRIPT_NAMES_CACHE.unscoped;
},
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 (SCRIPT_VERSION_CACHE.has(specifier)) {
return SCRIPT_VERSION_CACHE.get(specifier);
}
const scriptVersion = ops.op_script_version(specifier);
SCRIPT_VERSION_CACHE.set(specifier, scriptVersion);
return scriptVersion;
},
getScriptSnapshot(specifier) {
if (logDebug) {
debug(`host.getScriptSnapshot("${specifier}")`);
}
if (specifier.startsWith(ASSETS_URL_PREFIX)) {
const sourceFile = this.getSourceFile(
specifier,
ts.ScriptTarget.ESNext,
);
if (sourceFile) {
if (!ASSET_SCOPES.has(specifier)) {
ASSET_SCOPES.set(specifier, LAST_REQUEST_SCOPE.get());
}
// This case only occurs for assets.
return ts.ScriptSnapshot.fromString(sourceFile.text);
}
}
let scriptSnapshot = SCRIPT_SNAPSHOT_CACHE.get(specifier);
if (scriptSnapshot == undefined) {
/** @type {{ data: string, version: string, isCjs: boolean }} */
const fileInfo = ops.op_load(specifier);
if (!fileInfo) {
return undefined;
}
scriptSnapshot = ts.ScriptSnapshot.fromString(fileInfo.data);
scriptSnapshot.isCjs = fileInfo.isCjs;
SCRIPT_SNAPSHOT_CACHE.set(specifier, scriptSnapshot);
SCRIPT_VERSION_CACHE.set(specifier, fileInfo.version);
}
return scriptSnapshot;
},
};
// @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,
"Module_0_was_resolved_to_1_but_allowArbitraryExtensions_is_not_set_6263":
"Module '{0}' was resolved to '{1}', but importing these modules is not supported.",
};
})());
/** @param {ts.Diagnostic} diagnostic */
export function filterMapDiagnostic(diagnostic) {
if (IGNORED_DIAGNOSTICS.includes(diagnostic.code)) {
return false;
}
// ignore diagnostics resulting from the `ImportMeta` declaration in deno merging with
// the one in @types/node. the types of the filename and dirname properties are different,
// which causes tsc to error.
const importMetaFilenameDirnameModifiersRe =
/^All declarations of '(filename|dirname)'/;
const importMetaFilenameDirnameTypesRe =
/^Subsequent property declarations must have the same type.\s+Property '(filename|dirname)'/;
// Declarations of X must have identical modifiers.
if (diagnostic.code === 2687) {
if (
typeof diagnostic.messageText === "string" &&
(importMetaFilenameDirnameModifiersRe.test(diagnostic.messageText)) &&
(diagnostic.file?.fileName.startsWith("asset:///") ||
diagnostic.file?.fileName?.includes("@types/node"))
) {
return false;
}
}
// Subsequent property declarations must have the same type.
if (diagnostic.code === 2717) {
if (
typeof diagnostic.messageText === "string" &&
(importMetaFilenameDirnameTypesRe.test(diagnostic.messageText)) &&
(diagnostic.file?.fileName.startsWith("asset:///") ||
diagnostic.file?.fileName?.includes("@types/node"))
) {
return false;
}
}
// make the diagnostic for using an `export =` in an es module a warning
if (diagnostic.code === 1203) {
diagnostic.category = ts.DiagnosticCategory.Warning;
if (typeof diagnostic.messageText === "string") {
const message =
" This will start erroring in a future version of Deno 2 " +
"in order to align with TypeScript.";
// seems typescript shares objects, so check if it's already been set
if (!diagnostic.messageText.endsWith(message)) {
diagnostic.messageText += message;
}
}
}
return true;
}
// list of globals that should be kept in Node's globalThis
ts.deno.setNodeOnlyGlobalNames([
"__dirname",
"__filename",
"Buffer",
"BufferConstructor",
"BufferEncoding",
"clearImmediate",
"clearInterval",
"clearTimeout",
"console",
"Console",
"ErrorConstructor",
"gc",
"Global",
"localStorage",
"queueMicrotask",
"RequestInit",
"ResponseInit",
"sessionStorage",
"setImmediate",
"setInterval",
"setTimeout",
]);
export function getAssets() {
/** @type {{ specifier: string; text: string; }[]} */
const assets = [];
for (const sourceFile of SOURCE_FILE_CACHE.values()) {
if (sourceFile.fileName.startsWith(ASSETS_URL_PREFIX)) {
assets.push({
specifier: sourceFile.fileName,
text: sourceFile.text,
});
}
}
return assets;
}

486
cli/tsc/98_lsp.js Normal file
View file

@ -0,0 +1,486 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import {
ASSET_SCOPES,
ASSETS_URL_PREFIX,
clearScriptNamesCache,
debug,
error,
filterMapDiagnostic,
fromTypeScriptDiagnostics,
getAssets,
getCreateSourceFileOptions,
host,
IS_NODE_SOURCE_FILE_CACHE,
LANGUAGE_SERVICE_ENTRIES,
LAST_REQUEST_METHOD,
LAST_REQUEST_SCOPE,
OperationCanceledError,
PROJECT_VERSION_CACHE,
SCRIPT_SNAPSHOT_CACHE,
SCRIPT_VERSION_CACHE,
setLogDebug,
SOURCE_REF_COUNTS,
} from "./97_ts_host.js";
/** @type {DenoCore} */
const core = globalThis.Deno.core;
const ops = core.ops;
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) {
const isCjs = /** @type {any} */ (scriptSnapshot).isCjs;
sourceFile = ts.createLanguageServiceSourceFile(
fileName,
scriptSnapshot,
{
...getCreateSourceFileOptions(sourceFileOptions),
impliedNodeFormat: isCjs
? 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);
}
const sourceRefCount = SOURCE_REF_COUNTS.get(fileName) ?? 0;
SOURCE_REF_COUNTS.set(fileName, sourceRefCount + 1);
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),
),
);
documentRegistrySourceFileCache.set(mapKey, sourceFile);
}
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 sourceRefCount = SOURCE_REF_COUNTS.get(path) ?? 1;
if (sourceRefCount <= 1) {
SOURCE_REF_COUNTS.delete(path);
// We call `cleanupSemanticCache` for other purposes, don't bust the
// source cache in this case.
if (LAST_REQUEST_METHOD.get() != "cleanupSemanticCache") {
const mapKey = path + key;
documentRegistrySourceFileCache.delete(mapKey);
SCRIPT_SNAPSHOT_CACHE.delete(path);
ops.op_release(path);
}
} else {
SOURCE_REF_COUNTS.set(path, sourceRefCount - 1);
}
},
reportStats() {
return "[]";
},
};
/** @param {Record<string, unknown>} 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";
}
if (config.jsxPrecompileSkipElements) {
delete config.jsxPrecompileSkipElements;
}
return config;
}
/** @param {Record<string, unknown>} config */
function lspTsConfigToCompilerOptions(config) {
const normalizedConfig = normalizeConfig(config);
const { options, errors } = ts
.convertCompilerOptionsFromJson(normalizedConfig, "");
Object.assign(options, {
allowNonTsExtensions: true,
allowImportingTsExtensions: true,
module: ts.ModuleKind.NodeNext,
moduleResolution: ts.ModuleResolutionKind.NodeNext,
});
if (errors.length > 0) {
debug(ts.formatDiagnostics(errors, host));
}
return options;
}
/**
* @param {any} e
* @returns {e is (OperationCanceledError | ts.OperationCanceledException)}
*/
function isCancellationError(e) {
return e instanceof OperationCanceledError ||
e instanceof ts.OperationCanceledException;
}
/**
* @param {number} _id
* @param {any} data
* @param {string | null} error
*/
// TODO(bartlomieju): this feels needlessly generic, both type checking
// and language server use it with inefficient serialization. Id is not used
// anyway...
function respond(_id, data = null, error = null) {
if (error) {
ops.op_respond(
"error",
error,
);
} else {
ops.op_respond(JSON.stringify(data), "");
}
}
/** @typedef {[[string, number][], number, [string, any][]] } PendingChange */
/**
* @template T
* @typedef {T | null} Option<T> */
/** @returns {Promise<[number, string, any[], string | null, Option<PendingChange>] | null>} */
async function pollRequests() {
return await ops.op_poll_requests();
}
let hasStarted = false;
/** @param {boolean} enableDebugLogging */
export async function serverMainLoop(enableDebugLogging) {
if (hasStarted) {
throw new Error("The language server has already been initialized.");
}
hasStarted = true;
LANGUAGE_SERVICE_ENTRIES.unscoped = {
ls: ts.createLanguageService(
host,
documentRegistry,
),
compilerOptions: lspTsConfigToCompilerOptions({
"allowJs": true,
"esModuleInterop": true,
"experimentalDecorators": false,
"isolatedModules": true,
"lib": ["deno.ns", "deno.window", "deno.unstable"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"moduleDetection": "force",
"noEmit": true,
"noImplicitOverride": true,
"resolveJsonModule": true,
"strict": true,
"target": "esnext",
"useDefineForClassFields": true,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
}),
};
setLogDebug(enableDebugLogging, "TSLS");
debug("serverInit()");
while (true) {
const request = await pollRequests();
if (request === null) {
break;
}
try {
serverRequest(
request[0],
request[1],
request[2],
request[3],
request[4],
);
} catch (err) {
error(`Internal error occurred processing request: ${err}`);
}
}
}
/**
* @param {any} error
* @param {any[] | null} args
*/
function formatErrorWithArgs(error, args) {
let errorString = "stack" in error
? error.stack.toString()
: error.toString();
if (args) {
errorString += `\nFor request: [${
args.map((v) => JSON.stringify(v)).join(", ")
}]`;
}
return errorString;
}
/**
* @param {number} id
* @param {string} method
* @param {any[]} args
* @param {string | null} scope
* @param {PendingChange | null} maybeChange
*/
function serverRequest(id, method, args, scope, maybeChange) {
debug(`serverRequest()`, id, method, args, scope, maybeChange);
if (maybeChange !== null) {
const changedScripts = maybeChange[0];
const newProjectVersion = maybeChange[1];
const newConfigsByScope = maybeChange[2];
if (newConfigsByScope) {
IS_NODE_SOURCE_FILE_CACHE.clear();
ASSET_SCOPES.clear();
/** @type { typeof LANGUAGE_SERVICE_ENTRIES.byScope } */
const newByScope = new Map();
for (const [scope, config] of newConfigsByScope) {
LAST_REQUEST_SCOPE.set(scope);
const oldEntry = LANGUAGE_SERVICE_ENTRIES.byScope.get(scope);
const ls = oldEntry
? oldEntry.ls
: ts.createLanguageService(host, documentRegistry);
const compilerOptions = lspTsConfigToCompilerOptions(config);
newByScope.set(scope, { ls, compilerOptions });
LANGUAGE_SERVICE_ENTRIES.byScope.delete(scope);
}
for (const oldEntry of LANGUAGE_SERVICE_ENTRIES.byScope.values()) {
oldEntry.ls.dispose();
}
LANGUAGE_SERVICE_ENTRIES.byScope = newByScope;
}
PROJECT_VERSION_CACHE.set(newProjectVersion);
let opened = false;
let closed = false;
for (const { 0: script, 1: changeKind } of changedScripts) {
if (changeKind === ChangeKind.Opened) {
opened = true;
} else if (changeKind === ChangeKind.Closed) {
closed = true;
}
SCRIPT_VERSION_CACHE.delete(script);
SCRIPT_SNAPSHOT_CACHE.delete(script);
}
if (newConfigsByScope || opened || closed) {
clearScriptNamesCache();
}
}
// For requests pertaining to an asset document, we make it so that the
// passed scope is just its own specifier. We map it to an actual scope here
// based on the first scope that the asset was loaded into.
if (scope?.startsWith(ASSETS_URL_PREFIX)) {
scope = ASSET_SCOPES.get(scope) ?? null;
}
LAST_REQUEST_METHOD.set(method);
LAST_REQUEST_SCOPE.set(scope);
const ls = (scope ? LANGUAGE_SERVICE_ENTRIES.byScope.get(scope)?.ls : null) ??
LANGUAGE_SERVICE_ENTRIES.unscoped.ls;
switch (method) {
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).
const cachedProjectVersion = PROJECT_VERSION_CACHE.get();
if (cachedProjectVersion && projectVersion !== cachedProjectVersion) {
return respond(id, {});
}
try {
/** @type {Record<string, any[]>} */
const diagnosticMap = {};
for (const specifier of args[0]) {
diagnosticMap[specifier] = fromTypeScriptDiagnostics([
...ls.getSemanticDiagnostics(specifier),
...ls.getSuggestionDiagnostics(specifier),
...ls.getSyntacticDiagnostics(specifier),
].filter(filterMapDiagnostic));
}
return respond(id, diagnosticMap);
} catch (e) {
if (
!isCancellationError(e)
) {
return respond(
id,
{},
formatErrorWithArgs(e, [id, method, args, scope, maybeChange]),
);
}
return respond(id, {});
}
}
default:
if (typeof ls[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;
}
try {
return respond(id, ls[method](...args));
} catch (e) {
if (!isCancellationError(e)) {
return respond(
id,
null,
formatErrorWithArgs(e, [id, method, args, scope, maybeChange]),
);
}
return respond(id);
}
}
throw new TypeError(
// @ts-ignore exhausted case statement sets type to never
`Invalid request method for request: "${method}" (${id})`,
);
}
}

File diff suppressed because it is too large Load diff