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:
parent
80a6179ac4
commit
ab18dac09d
4 changed files with 1576 additions and 1480 deletions
|
@ -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
832
cli/tsc/97_ts_host.js
Normal 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
486
cli/tsc/98_lsp.js
Normal 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
Loading…
Add table
Reference in a new issue