diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2022-10-21 11:20:18 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-21 15:20:18 +0000 |
commit | bcfe279fba865763c87f9cd8d5a2d0b2cbf451be (patch) | |
tree | 68e4d1bc52e261df50279f9ecea14795d1c46f6c /cli/tsc/99_main_compiler.js | |
parent | 0e1a71fec6fff5fe62d7e6b2bfffb7ab877d7b71 (diff) |
feat(unstable/npm): initial type checking of npm specifiers (#16332)
Diffstat (limited to 'cli/tsc/99_main_compiler.js')
-rw-r--r-- | cli/tsc/99_main_compiler.js | 316 |
1 files changed, 305 insertions, 11 deletions
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index b39f56cf6..7929d3b44 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -29,6 +29,7 @@ delete Object.prototype.__proto__; // 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(); /** @@ -40,6 +41,16 @@ delete Object.prototype.__proto__; "languageVersion" in value; } + /** + * @param {ts.ScriptTarget | ts.CreateSourceFileOptions | undefined} versionOrOptions + * @returns {ts.CreateSourceFileOptions} + */ + function getCreateSourceFileOptions(versionOrOptions) { + return isCreateSourceFileOptions(versionOrOptions) + ? versionOrOptions + : { languageVersion: versionOrOptions ?? ts.ScriptTarget.ESNext }; + } + function setLogDebug(debug, source) { logDebug = debug; if (source) { @@ -119,8 +130,23 @@ delete Object.prototype.__proto__; return result; } - // In the case of the LSP, this is initialized with the assets - // when snapshotting and never added to or removed after that. + class SpecifierIsCjsCache { + /** @type {Set<string>} */ + #cache = new Set(); + + /** @param {[string, ts.Extension]} param */ + add([specifier, ext]) { + if (ext === ".cjs" || ext === ".d.cts" || ext === ".cts") { + this.#cache.add(specifier); + } + } + + has(specifier) { + return this.#cache.has(specifier); + } + } + + // In the case of the LSP, this will only ever contain the assets. /** @type {Map<string, ts.SourceFile>} */ const sourceFileCache = new Map(); @@ -130,6 +156,181 @@ delete Object.prototype.__proto__; /** @type {Map<string, string>} */ const scriptVersionCache = new Map(); + /** @type {Map<string, boolean>} */ + const isNodeSourceFileCache = new Map(); + + const isCjsCache = new SpecifierIsCjsCache(); + + /** + * @param {ts.CompilerOptions | ts.MinimalResolutionCacheHost} settingsOrHost + * @returns {ts.CompilerOptions} + */ + function getCompilationSettings(settingsOrHost) { + if (typeof settingsOrHost.getCompilationSettings === "function") { + return settingsOrHost.getCompilationSettings(); + } + return /** @type {ts.CompilerOptions} */ (settingsOrHost); + } + + // We need to use a custom document registry in order to provide source files + // with an impliedNodeFormat to the ts language service + + /** @type {Map<string, ts.SourceFile} */ + const documentRegistrySourceFileCache = new Map(); + const { getKeyForCompilationSettings } = ts.createDocumentRegistry(); // reuse this code + /** @type {ts.DocumentRegistry} */ + const documentRegistry = { + acquireDocument( + fileName, + compilationSettingsOrHost, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const key = getKeyForCompilationSettings( + getCompilationSettings(compilationSettingsOrHost), + ); + return this.acquireDocumentWithKey( + fileName, + /** @type {ts.Path} */ (fileName), + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ); + }, + + acquireDocumentWithKey( + fileName, + path, + _compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const mapKey = path + key; + let sourceFile = documentRegistrySourceFileCache.get(mapKey); + if (!sourceFile || sourceFile.version !== version) { + sourceFile = ts.createLanguageServiceSourceFile( + fileName, + scriptSnapshot, + { + ...getCreateSourceFileOptions(sourceFileOptions), + impliedNodeFormat: isCjsCache.has(fileName) + ? ts.ModuleKind.CommonJS + : ts.ModuleKind.ESNext, + }, + version, + true, + scriptKind, + ); + documentRegistrySourceFileCache.set(mapKey, sourceFile); + } + return sourceFile; + }, + + updateDocument( + fileName, + compilationSettingsOrHost, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const key = getKeyForCompilationSettings( + getCompilationSettings(compilationSettingsOrHost), + ); + return this.updateDocumentWithKey( + fileName, + /** @type {ts.Path} */ (fileName), + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ); + }, + + updateDocumentWithKey( + fileName, + path, + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ) { + const mapKey = path + key; + let sourceFile = documentRegistrySourceFileCache.get(mapKey) ?? + this.acquireDocumentWithKey( + fileName, + path, + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions, + ); + + if (sourceFile.version !== version) { + sourceFile = ts.updateLanguageServiceSourceFile( + sourceFile, + scriptSnapshot, + version, + scriptSnapshot.getChangeRange(sourceFile.scriptSnapShot), + ); + } + return sourceFile; + }, + + getKeyForCompilationSettings(settings) { + return getKeyForCompilationSettings(settings); + }, + + releaseDocument( + fileName, + compilationSettings, + scriptKind, + impliedNodeFormat, + ) { + const key = getKeyForCompilationSettings(compilationSettings); + return this.releaseDocumentWithKey( + /** @type {ts.Path} */ (fileName), + key, + scriptKind, + impliedNodeFormat, + ); + }, + + releaseDocumentWithKey(path, key, _scriptKind, _impliedNodeFormat) { + const mapKey = path + key; + documentRegistrySourceFileCache.remove(mapKey); + }, + + reportStats() { + return "[]"; + }, + }; + + ts.deno.setIsNodeSourceFileCallback((sourceFile) => { + const fileName = sourceFile.fileName; + let isNodeSourceFile = isNodeSourceFileCache.get(fileName); + if (isNodeSourceFile == null) { + const result = ops.op_is_node_file(fileName); + isNodeSourceFile = /** @type {boolean} */ (result); + isNodeSourceFileCache.set(fileName, isNodeSourceFile); + } + return isNodeSourceFile; + }); + /** @param {ts.DiagnosticRelatedInformation} diagnostic */ function fromRelatedInformation({ start, @@ -189,6 +390,10 @@ delete Object.prototype.__proto__; /** Diagnostics that are intentionally ignored when compiling TypeScript in * Deno, as they provide misleading or incorrect information. */ const IGNORED_DIAGNOSTICS = [ + // TS1452: 'resolution-mode' assertions are only supported when `moduleResolution` is `node16` or `nodenext`. + // We specify the resolution mode to be CommonJS for some npm files and this + // diagnostic gets generated even though we're using custom module resolution. + 1452, // TS2306: File '.../index.d.ts' is not a module. // We get this for `x-typescript-types` declaration files which don't export // anything. We prefer to treat these as modules with no exports. @@ -228,10 +433,12 @@ delete Object.prototype.__proto__; target: ts.ScriptTarget.ESNext, }; + // todo(dsherret): can we remove this and just use ts.OperationCanceledException? /** Error thrown on cancellation. */ class OperationCanceledError extends Error { } + // todo(dsherret): we should investigate if throttling is really necessary /** * Inspired by ThrottledCancellationToken in ts server. * @@ -291,13 +498,10 @@ delete Object.prototype.__proto__; _onError, _shouldCreateNewSourceFile, ) { + const createOptions = getCreateSourceFileOptions(languageVersion); debug( `host.getSourceFile("${specifier}", ${ - ts.ScriptTarget[ - isCreateSourceFileOptions(languageVersion) - ? languageVersion.languageVersion - : languageVersion - ] + ts.ScriptTarget[createOptions.languageVersion] })`, ); @@ -320,7 +524,12 @@ delete Object.prototype.__proto__; sourceFile = ts.createSourceFile( specifier, data, - languageVersion, + { + ...createOptions, + impliedNodeFormat: isCjsCache.has(specifier) + ? ts.ModuleKind.CommonJS + : ts.ModuleKind.ESNext, + }, false, scriptKind, ); @@ -355,6 +564,50 @@ delete Object.prototype.__proto__; getNewLine() { return "\n"; }, + resolveTypeReferenceDirectives( + typeDirectiveNames, + containingFilePath, + redirectedReference, + options, + containingFileMode, + ) { + return typeDirectiveNames.map((arg) => { + /** @type {ts.FileReference} */ + const fileReference = typeof arg === "string" + ? { + pos: -1, + end: -1, + fileName: arg, + } + : arg; + if (fileReference.fileName.startsWith("npm:")) { + /** @type {[string, ts.Extension] | undefined} */ + const resolved = ops.op_resolve({ + specifiers: [fileReference.fileName], + base: containingFilePath, + })?.[0]; + if (resolved) { + isCjsCache.add(resolved); + return { + primary: true, + resolvedFileName: resolved[0], + }; + } else { + return undefined; + } + } else { + return ts.resolveTypeReferenceDirective( + fileReference.fileName, + containingFilePath, + options, + host, + redirectedReference, + undefined, + containingFileMode ?? fileReference.resolutionMode, + ).resolvedTypeReferenceDirective; + } + }); + }, resolveModuleNames(specifiers, base) { debug(`host.resolveModuleNames()`); debug(` base: ${base}`); @@ -367,7 +620,12 @@ delete Object.prototype.__proto__; if (resolved) { const result = resolved.map((item) => { if (item) { + isCjsCache.add(item); const [resolvedFileName, extension] = item; + if (resolvedFileName.startsWith("node:")) { + // probably means the user doesn't have @types/node, so resolve to undefined + return undefined; + } return { resolvedFileName, extension, @@ -444,6 +702,23 @@ delete Object.prototype.__proto__; }, }; + // override the npm install @types package diagnostics to be deno specific + ts.setLocalizedDiagnosticMessages((() => { + const nodeMessage = "Cannot find name '{0}'."; // don't offer any suggestions + const jqueryMessage = + "Cannot find name '{0}'. Did you mean to import jQuery? Try adding `import $ from \"npm:jquery\";`."; + return { + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashno_2580": + nodeMessage, + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashno_2591": + nodeMessage, + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slash_2581": + jqueryMessage, + "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slash_2592": + jqueryMessage, + }; + })()); + /** @type {Array<[string, number]>} */ const stats = []; let statsStart = 0; @@ -557,7 +832,25 @@ delete Object.prototype.__proto__; ...program.getOptionsDiagnostics(), ...program.getGlobalDiagnostics(), ...program.getSemanticDiagnostics(), - ].filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)); + ].filter((diagnostic) => { + if (IGNORED_DIAGNOSTICS.includes(diagnostic.code)) { + return false; + } else if ( + diagnostic.code === 1259 && + typeof diagnostic.messageText === "string" && + diagnostic.messageText.startsWith( + "Module '\"deno:///missing_dependency.d.ts\"' can only be default-imported using the 'allowSyntheticDefaultImports' flag", + ) + ) { + // For now, ignore diagnostics like: + // > TS1259 [ERROR]: Module '"deno:///missing_dependency.d.ts"' can only be default-imported using the 'allowSyntheticDefaultImports' flag + // This diagnostic has surfaced due to supporting node cjs imports because this module does `export =`. + // See discussion in https://github.com/microsoft/TypeScript/pull/51136 + return false; + } else { + return true; + } + }); // emit the tsbuildinfo file // @ts-ignore: emitBuildInfo is not exposed (https://github.com/microsoft/TypeScript/issues/49871) @@ -922,13 +1215,14 @@ delete Object.prototype.__proto__; } hasStarted = true; cwd = rootUri; - languageService = ts.createLanguageService(host); + languageService = ts.createLanguageService(host, documentRegistry); setLogDebug(debugFlag, "TSLS"); debug("serverInit()"); } function serverRestart() { - languageService = ts.createLanguageService(host); + languageService = ts.createLanguageService(host, documentRegistry); + isNodeSourceFileCache.clear(); debug("serverRestart()"); } |