diff --git a/cli/build.rs b/cli/build.rs index 62f7101942..742f227ec9 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -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>, diff --git a/cli/tsc/97_ts_host.js b/cli/tsc/97_ts_host.js new file mode 100644 index 0000000000..ba82a12b7c --- /dev/null +++ b/cli/tsc/97_ts_host.js @@ -0,0 +1,832 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +// @ts-check +/// +// 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} */ +const normalizedToOriginalMap = new Map(); + +/** @type {ReadonlySet} */ +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): " + + '/// '; + +/** + * @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} */ +export const SOURCE_FILE_CACHE = new Map(); + +/** @type {Map} */ +export const SCRIPT_SNAPSHOT_CACHE = new Map(); + +/** @type {Map} */ +export const SOURCE_REF_COUNTS = new Map(); + +/** @type {Map} */ +export const SCRIPT_VERSION_CACHE = new Map(); + +/** @type {Map} */ +export const IS_NODE_SOURCE_FILE_CACHE = new Map(); + +// Maps asset specifiers to the first scope that the asset was loaded into. +/** @type {Map} */ +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): /// '; + } + 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 }} */ +export const LANGUAGE_SERVICE_ENTRIES = { + // @ts-ignore Will be set later. + unscoped: null, + byScope: new Map(), +}; + +/** @type {{ unscoped: string[], byScope: Map } | 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} */ + 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} */ + 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; +} diff --git a/cli/tsc/98_lsp.js b/cli/tsc/98_lsp.js new file mode 100644 index 0000000000..e9be7cc123 --- /dev/null +++ b/cli/tsc/98_lsp.js @@ -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} */ +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} 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} 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 */ + +/** @returns {Promise<[number, string, any[], string | null, Option] | 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} */ + 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})`, + ); + } +} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 65319211fb..c99cfce9a2 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -12,1491 +12,263 @@ // 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; +import { + assert, + AssertionError, + ASSETS_URL_PREFIX, + debug, + filterMapDiagnostic, + fromTypeScriptDiagnostics, + getAssets, + host, + setLogDebug, + SOURCE_FILE_CACHE, +} from "./97_ts_host.js"; +import { serverMainLoop } from "./98_lsp.js"; - let logDebug = false; - let logSource = "JS"; +/** @type {DenoCore} */ +const core = globalThis.Deno.core; +const ops = core.ops; - // 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} */ - const normalizedToOriginalMap = new Map(); +// 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} */ +const normalizedToOriginalMap = new Map(); - /** @type {ReadonlySet} */ - 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): " + - '/// '; +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"], +}; - /** - * @param {unknown} value - * @returns {value is ts.CreateSourceFileOptions} - */ - function isCreateSourceFileOptions(value) { - return value != null && typeof value === "object" && - "languageVersion" in value; +/** @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]); } - - /** - * @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); - } - } - - // In the case of the LSP, this will only ever contain the assets. - /** @type {Map} */ - const sourceFileCache = new Map(); - - /** @type {Map} */ - const scriptSnapshotCache = new Map(); - - /** @type {Map} */ - const sourceRefCounts = new Map(); - - /** @type {Map} */ - const scriptVersionCache = new Map(); - - /** @type {Map} */ - const isNodeSourceFileCache = new Map(); - - // Maps asset specifiers to the first scope that the asset was loaded into. - /** @type {Map} */ - const assetScopes = new Map(); - - /** @type {number | null} */ - let projectVersionCache = null; - - /** @type {string | null} */ - let lastRequestMethod = null; - - /** @type {string | null} */ - let lastRequestScope = 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} */ - 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 = sourceRefCounts.get(fileName) ?? 0; - sourceRefCounts.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 = sourceRefCounts.get(path) ?? 1; - if (sourceRefCount <= 1) { - sourceRefCounts.delete(path); - // We call `cleanupSemanticCache` for other purposes, don't bust the - // source cache in this case. - if (lastRequestMethod != "cleanupSemanticCache") { - const mapKey = path + key; - documentRegistrySourceFileCache.delete(mapKey); - scriptSnapshotCache.delete(path); - ops.op_release(path); - } - } else { - sourceRefCounts.set(path, sourceRefCount - 1); - } - }, - - 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): /// '; - } - 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 */ - 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, - // 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, - ]; - - 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(); - } - } - } - - /** @typedef {{ - * ls: ts.LanguageService & { [k:string]: any }, - * compilerOptions: ts.CompilerOptions, - * }} LanguageServiceEntry */ - /** @type {{ unscoped: LanguageServiceEntry, byScope: Map }} */ - const languageServiceEntries = { - // @ts-ignore Will be set later. - unscoped: null, - byScope: new Map(), - }; - - /** @type {{ unscoped: string[], byScope: Map } | null} */ - let scriptNamesCache = 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} */ - 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, - // 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 = sourceFileCache.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"; - } - 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( - 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} */ - 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} */ - 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()"); - } - return (lastRequestScope - ? languageServiceEntries.byScope.get(lastRequestScope)?.compilerOptions - : null) ?? languageServiceEntries.unscoped.compilerOptions; - }, - getScriptFileNames() { - if (logDebug) { - debug("host.getScriptFileNames()"); - } - if (!scriptNamesCache) { - const { unscoped, byScope } = ops.op_script_names(); - scriptNamesCache = { - unscoped, - byScope: new Map(Object.entries(byScope)), - }; - } - return (lastRequestScope - ? scriptNamesCache.byScope.get(lastRequestScope) - : null) ?? scriptNamesCache.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 (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}")`); - } - if (specifier.startsWith(ASSETS_URL_PREFIX)) { - const sourceFile = this.getSourceFile( - specifier, - ts.ScriptTarget.ESNext, - ); - if (sourceFile) { - if (!assetScopes.has(specifier)) { - assetScopes.set(specifier, lastRequestScope); - } - // This case only occurs for assets. - return ts.ScriptSnapshot.fromString(sourceFile.text); - } - } - let scriptSnapshot = scriptSnapshotCache.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; - scriptSnapshotCache.set(specifier, scriptSnapshot); - scriptVersionCache.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.", - }; - })()); - - /** @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} 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} 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} 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 && logDebug) { - debug(ts.formatDiagnostics(errors, host)); - } - return options; - } - - /** 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, - }); - - let checkFiles = undefined; - - if (localOnly) { - const checkFileNames = new Set(); - checkFiles = []; - - for (const checkName of rootNames) { - if (checkName.startsWith("http")) { - continue; - } - const sourceFile = program.getSourceFile(checkName); - if (sourceFile != null) { - checkFiles.push(sourceFile); - } - checkFileNames.add(checkName); - } - - // 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. - 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(filterMapDiagnostic); - - // 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"); - } - - /** @param {ts.Diagnostic} diagnostic */ - 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; - } - - /** - * @param {any} e - * @returns {e is (OperationCanceledError | ts.OperationCanceledException)} - */ - function isCancellationError(e) { - return e instanceof OperationCanceledError || - e instanceof ts.OperationCanceledException; - } - - 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 - * @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 */ - - /** @returns {Promise<[number, string, any[], string | null, Option] | null>} */ - async function pollRequests() { - return await ops.op_poll_requests(); - } - - let hasStarted = false; - - /** @param {boolean} enableDebugLogging */ - async function serverMainLoop(enableDebugLogging) { - if (hasStarted) { - throw new Error("The language server has already been initialized."); - } - hasStarted = true; - languageServiceEntries.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) { - if (logDebug) { - debug(`serverRequest()`, id, method, args, scope, maybeChange); - } - if (maybeChange !== null) { - const changedScripts = maybeChange[0]; - const newProjectVersion = maybeChange[1]; - const newConfigsByScope = maybeChange[2]; - if (newConfigsByScope) { - isNodeSourceFileCache.clear(); - assetScopes.clear(); - /** @type { typeof languageServiceEntries.byScope } */ - const newByScope = new Map(); - for (const [scope, config] of newConfigsByScope) { - lastRequestScope = scope; - const oldEntry = languageServiceEntries.byScope.get(scope); - const ls = oldEntry - ? oldEntry.ls - : ts.createLanguageService(host, documentRegistry); - const compilerOptions = lspTsConfigToCompilerOptions(config); - newByScope.set(scope, { ls, compilerOptions }); - languageServiceEntries.byScope.delete(scope); - } - for (const oldEntry of languageServiceEntries.byScope.values()) { - oldEntry.ls.dispose(); - } - languageServiceEntries.byScope = newByScope; - } - - projectVersionCache = 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; - } - scriptVersionCache.delete(script); - scriptSnapshotCache.delete(script); - } - - if (newConfigsByScope || opened || closed) { - scriptNamesCache = null; - } - } - - // 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 = assetScopes.get(scope) ?? null; - } - lastRequestMethod = method; - lastRequestScope = scope; - const ls = (scope ? languageServiceEntries.byScope.get(scope)?.ls : null) ?? - languageServiceEntries.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). - if (projectVersionCache && projectVersion !== projectVersionCache) { - return respond(id, {}); - } - try { - /** @type {Record} */ - 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})`, - ); - } - } - - // 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 } = ops.op_build_info(); - - // 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", - ]); - - 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", + 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], ); +} - // remove this now that we don't need it anymore for warming up tsc - sourceFileCache.delete(buildSpecifier); +function performanceEnd() { + const duration = Date.now() - statsStart; + stats.push(["Compile time", duration]); + return stats; +} - // exposes the functions that are called by `tsc::exec()` when type - // checking TypeScript. - /** @type {any} */ - const global = globalThis; - global.exec = exec; - global.getAssets = getAssets; +/** + * @typedef {object} Request + * @property {Record} config + * @property {boolean} debug + * @property {string[]} rootNames + * @property {boolean} localOnly + */ - // exposes the functions that are called when the compiler is used as a - // language service. - global.serverMainLoop = serverMainLoop; -})(this); +/** + * 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} 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; +} + +/** 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); + + 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, + }); + + let checkFiles = undefined; + + if (localOnly) { + const checkFileNames = new Set(); + checkFiles = []; + + for (const checkName of rootNames) { + if (checkName.startsWith("http")) { + continue; + } + const sourceFile = program.getSourceFile(checkName); + if (sourceFile != null) { + checkFiles.push(sourceFile); + } + checkFileNames.add(checkName); + } + + // 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. + 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(filterMapDiagnostic); + + // 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"); +} + +// 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 } = ops.op_build_info(); + +for (const lib of libs) { + const specifier = `lib.${lib}.d.ts`; + // we are using internal APIs here to "inject" our custom libraries into + // tsc, so things like `"lib": [ "deno.ns" ]` are supported. + if (!ts.libs.includes(lib)) { + ts.libs.push(lib); + ts.libMap.set(lib, `lib.${lib}.d.ts`); + } + // we are caching in memory common type libraries that will be re-used by + // tsc on when the snapshot is restored + assert( + !!host.getSourceFile( + `${ASSETS_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 +SOURCE_FILE_CACHE.delete(buildSpecifier); + +// exposes the functions that are called by `tsc::exec()` when type +// checking TypeScript. +globalThis.exec = exec; +globalThis.getAssets = getAssets; + +// exposes the functions that are called when the compiler is used as a +// language service. +globalThis.serverMainLoop = serverMainLoop;