diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index de300efd3cf05..a9915759e4c08 100644 --- a/src/compiler/builderState.ts +++ b/src/compiler/builderState.ts @@ -225,7 +225,7 @@ namespace ts { } // From ambient modules - for (const ambientModule of program.getTypeChecker().getAmbientModules()) { + for (const ambientModule of program.getTypeChecker().getAmbientModules(sourceFile)) { if (ambientModule.declarations && ambientModule.declarations.length > 1) { addReferenceFromAmbientModule(ambientModule); } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 194f51256860b..d34325eabd6a9 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -375,6 +375,7 @@ namespace ts { const nodeBuilder = createNodeBuilder(); const globals = createSymbolTable(); + const nodeGlobals = createSymbolTable(); const undefinedSymbol = createSymbol(SymbolFlags.Property, "undefined" as __String); undefinedSymbol.declarations = []; @@ -383,6 +384,18 @@ namespace ts { globalThisSymbol.declarations = []; globals.set(globalThisSymbol.escapedName, globalThisSymbol); + const denoContext = ts.deno.createDenoForkContext({ + globals, + nodeGlobals, + mergeSymbol, + ambientModuleSymbolRegex, + }); + + const nodeGlobalThisSymbol = createSymbol(SymbolFlags.Module, "globalThis" as __String, CheckFlags.Readonly); + nodeGlobalThisSymbol.exports = denoContext.combinedGlobals; + nodeGlobalThisSymbol.declarations = []; + nodeGlobals.set(nodeGlobalThisSymbol.escapedName, nodeGlobalThisSymbol); + const argumentsSymbol = createSymbol(SymbolFlags.Property, "arguments" as __String); const requireSymbol = createSymbol(SymbolFlags.Property, "require" as __String); @@ -941,6 +954,7 @@ namespace ts { const reverseMappedCache = new Map(); let inInferTypeForHomomorphicMappedType = false; let ambientModulesCache: Symbol[] | undefined; + let nodeAmbientModulesCache: Symbol[] | undefined; /** * List of every ambient module with a "*" wildcard. * Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches. @@ -1347,7 +1361,7 @@ namespace ts { // Do not report an error when merging `var globalThis` with the built-in `globalThis`, // as we will already report a "Declaration name conflicts..." error, and this error // won't make much sense. - if (target !== globalThisSymbol) { + if (target !== globalThisSymbol && target !== nodeGlobalThisSymbol) { error( source.declarations && getNameOfDeclaration(source.declarations[0]), Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, @@ -1441,7 +1455,7 @@ namespace ts { } if (isGlobalScopeAugmentation(moduleAugmentation)) { - mergeSymbolTable(globals, moduleAugmentation.symbol.exports!); + denoContext.mergeGlobalSymbolTable(moduleAugmentation, moduleAugmentation.symbol.exports!); } else { // find a module that about to be augmented @@ -2176,7 +2190,12 @@ namespace ts { } if (!excludeGlobals) { - result = lookup(globals, name, meaning); + if (denoContext.hasNodeSourceFile(lastLocation)) { + result = lookup(nodeGlobals, name, meaning); + } + if (!result) { + result = lookup(globals, name, meaning); + } } } if (!result) { @@ -2760,7 +2779,7 @@ namespace ts { const usageMode = file && getUsageModeForExpression(usage); if (file && usageMode !== undefined) { const result = isESMFormatImportImportingCommonjsFormatFile(usageMode, file.impliedNodeFormat); - if (usageMode === ModuleKind.ESNext || result) { + if (result) { return result; } // fallthrough on cjs usages so we imply defaults for interop'd imports, too @@ -3575,6 +3594,25 @@ namespace ts { } function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage | undefined, errorNode: Node, isForAugmentation = false): Symbol | undefined { + const result = resolveExternalModuleInner(location, moduleReference, moduleNotFoundError, errorNode, isForAugmentation); + + // deno: attempt to resolve an npm package reference to its bare specifier w/ path ambient module + // when not found and the symbol has zero exports + if (moduleReference.startsWith("npm:") && (result == null || result?.exports?.size === 0)) { + const npmPackageRef = deno.tryParseNpmPackageReference(moduleReference); + if (npmPackageRef) { + const bareSpecifier = npmPackageRef.name + (npmPackageRef.subPath == null ? "" : "/" + npmPackageRef.subPath); + const ambientModule = tryFindAmbientModule(bareSpecifier, /*withAugmentations*/ true); + if (ambientModule) { + return ambientModule; + } + } + } + + return result; + } + + function resolveExternalModuleInner(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage | undefined, errorNode: Node, isForAugmentation = false): Symbol | undefined { if (startsWith(moduleReference, "@types/")) { const diag = Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1; const withoutAtTypePrefix = removePrefix(moduleReference, "@types/"); @@ -4404,6 +4442,13 @@ namespace ts { } } + if (denoContext.hasNodeSourceFile(enclosingDeclaration)) { + result = callback(nodeGlobals, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true); + if (result) { + return result; + } + } + return callback(globals, /*ignoreQualification*/ undefined, /*isLocalNameLookup*/ true); } @@ -4498,7 +4543,11 @@ namespace ts { }); // If there's no result and we're looking at the global symbol table, treat `globalThis` like an alias and try to lookup thru that - return result || (symbols === globals ? getCandidateListForSymbol(globalThisSymbol, globalThisSymbol, ignoreQualification) : undefined); + if (result) { + return result; + } + const globalSymbol = symbols === nodeGlobals ? nodeGlobalThisSymbol : symbols === globals ? globalThisSymbol : undefined; + return globalSymbol != null ? getCandidateListForSymbol(globalSymbol, globalSymbol, ignoreQualification) : undefined; } function getCandidateListForSymbol(symbolFromSymbolTable: Symbol, resolvedImportedSymbol: Symbol, ignoreQualification: boolean | undefined) { @@ -11634,7 +11683,7 @@ namespace ts { let indexInfos: IndexInfo[] | undefined; if (symbol.exports) { members = getExportsOfSymbol(symbol); - if (symbol === globalThisSymbol) { + if (symbol === globalThisSymbol || symbol === nodeGlobalThisSymbol) { const varsOnly = new Map() as SymbolTable; members.forEach(p => { if (!(p.flags & SymbolFlags.BlockScoped) && !(p.flags & SymbolFlags.ValueModule && p.declarations?.length && every(p.declarations, isAmbientModule))) { @@ -12868,7 +12917,7 @@ namespace ts { if (isExternalModuleNameRelative(moduleName)) { return undefined; } - const symbol = getSymbol(globals, '"' + moduleName + '"' as __String, SymbolFlags.ValueModule); + const symbol = getSymbol(denoContext.combinedGlobals, '"' + moduleName + '"' as __String, SymbolFlags.ValueModule); // merged symbol is module declaration symbol combined with all augmentations return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol; } @@ -15740,6 +15789,10 @@ namespace ts { if (objectType.symbol === globalThisSymbol && propName !== undefined && globalThisSymbol.exports!.has(propName) && (globalThisSymbol.exports!.get(propName)!.flags & SymbolFlags.BlockScoped)) { error(accessExpression, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); } + // deno: ensure condition and body match the above + else if (objectType.symbol === nodeGlobalThisSymbol && propName !== undefined && nodeGlobalThisSymbol.exports!.has(propName) && (nodeGlobalThisSymbol.exports!.get(propName)!.flags & SymbolFlags.BlockScoped)) { + error(accessExpression, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); + } else if (noImplicitAny && !compilerOptions.suppressImplicitAnyIndexErrors && !(accessFlags & AccessFlags.SuppressNoImplicitAnyError)) { if (propName !== undefined && typeHasStaticProperty(propName, objectType)) { const typeName = typeToString(objectType); @@ -25119,7 +25172,7 @@ namespace ts { if (type.flags & TypeFlags.Union || type.flags & TypeFlags.Object && declaredType !== type && !(declaredType === unknownType && isEmptyAnonymousObjectType(type)) || isThisTypeParameter(type) - || type.flags & TypeFlags.Intersection && every((type as IntersectionType).types, t => t.symbol !== globalThisSymbol)) { + || type.flags & TypeFlags.Intersection && every((type as IntersectionType).types, t => t.symbol !== globalThisSymbol && t.symbol !== nodeGlobalThisSymbol)) { return filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); } return type; @@ -26379,6 +26432,9 @@ namespace ts { return undefinedType; } else if (includeGlobalThis) { + if (denoContext.hasNodeSourceFile(container)) { + return getTypeOfSymbol(nodeGlobalThisSymbol); + } return getTypeOfSymbol(globalThisSymbol); } } @@ -29356,6 +29412,11 @@ namespace ts { } return anyType; } + // deno: ensure condition matches above + if (leftType.symbol === nodeGlobalThisSymbol) { + // deno: don't bother with errors like above for simplicity + return anyType; + } if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) { reportNonexistentProperty(right, isThisTypeParameter(leftType) ? apparentType : leftType, isUncheckedJS); } @@ -29682,7 +29743,7 @@ namespace ts { // However, resolveNameHelper will continue and call this callback again, so we'll eventually get a correct suggestion. if (symbol) return symbol; let candidates: Symbol[]; - if (symbols === globals) { + if (symbols === globals || symbols === nodeGlobals) { const primitives = mapDefined( ["string", "number", "boolean", "object", "bigint", "symbol"], s => symbols.has((s.charAt(0).toUpperCase() + s.slice(1)) as __String) @@ -41549,7 +41610,7 @@ namespace ts { // find immediate value referenced by exported name (SymbolFlags.Alias is set so we don't chase down aliases) const symbol = resolveName(exportedName, exportedName.escapedText, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); - if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || symbol.declarations && isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { + if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || symbol === nodeGlobalThisSymbol || symbol.declarations && isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { error(exportedName, Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, idText(exportedName)); } else { @@ -42297,6 +42358,10 @@ namespace ts { location = location.parent; } + if (denoContext.hasNodeSourceFile(location)) { + copySymbols(nodeGlobals, meaning); + } + copySymbols(globals, meaning); } @@ -43815,10 +43880,10 @@ namespace ts { diagnostics.add(createDiagnosticForNode(declaration, Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0, "globalThis")); } } - mergeSymbolTable(globals, file.locals!); + denoContext.mergeGlobalSymbolTable(file, file.locals!); } if (file.jsGlobalAugmentations) { - mergeSymbolTable(globals, file.jsGlobalAugmentations); + denoContext.mergeGlobalSymbolTable(file, file.jsGlobalAugmentations); } if (file.patternAmbientModules && file.patternAmbientModules.length) { patternAmbientModules = concatenate(patternAmbientModules, file.patternAmbientModules); @@ -43829,9 +43894,11 @@ namespace ts { if (file.symbol && file.symbol.globalExports) { // Merge in UMD exports with first-in-wins semantics (see #9771) const source = file.symbol.globalExports; + const isNodeFile = denoContext.hasNodeSourceFile(file); source.forEach((sourceSymbol, id) => { - if (!globals.has(id)) { - globals.set(id, sourceSymbol); + const envGlobals = isNodeFile ? denoContext.getGlobalsForName(id) : globals; + if (!envGlobals.has(id)) { + envGlobals.set(id, sourceSymbol); } }); } @@ -43861,6 +43928,7 @@ namespace ts { getSymbolLinks(argumentsSymbol).type = getGlobalType("IArguments" as __String, /*arity*/ 0, /*reportErrors*/ true); getSymbolLinks(unknownSymbol).type = errorType; getSymbolLinks(globalThisSymbol).type = createObjectType(ObjectFlags.Anonymous, globalThisSymbol); + getSymbolLinks(nodeGlobalThisSymbol).type = createObjectType(ObjectFlags.Anonymous, nodeGlobalThisSymbol); // Initialize special types globalArrayType = getGlobalType("Array" as __String, /*arity*/ 1, /*reportErrors*/ true); @@ -45613,17 +45681,30 @@ namespace ts { return false; } - function getAmbientModules(): Symbol[] { - if (!ambientModulesCache) { - ambientModulesCache = []; - globals.forEach((global, sym) => { + function getAmbientModules(sourceFile?: SourceFile): Symbol[] { + const isNode = denoContext.hasNodeSourceFile(sourceFile); + if (isNode) { + if (!nodeAmbientModulesCache) { + nodeAmbientModulesCache = getAmbientModules(denoContext.combinedGlobals); + } + return nodeAmbientModulesCache; + } else { + if (!ambientModulesCache) { + ambientModulesCache = getAmbientModules(globals); + } + return ambientModulesCache; + } + + function getAmbientModules(envGlobals: SymbolTable) { + const cache: Symbol[] = []; + envGlobals.forEach((global, sym) => { // No need to `unescapeLeadingUnderscores`, an escaped symbol is never an ambient module. if (ambientModuleSymbolRegex.test(sym as string)) { - ambientModulesCache!.push(global); + cache.push(global); } }); + return cache; } - return ambientModulesCache; } function checkGrammarImportClause(node: ImportClause): boolean { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 370461abd8559..7632c3c142d88 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -38,6 +38,8 @@ namespace ts { // Host only ["dom", "lib.dom.d.ts"], ["dom.iterable", "lib.dom.iterable.d.ts"], + ["dom.asynciterable", "lib.dom.asynciterable.d.ts"], + ["dom.extras", "lib.dom.extras.d.ts"], ["webworker", "lib.webworker.d.ts"], ["webworker.importscripts", "lib.webworker.importscripts.d.ts"], ["webworker.iterable", "lib.webworker.iterable.d.ts"], @@ -85,7 +87,7 @@ namespace ts { ["es2022.object", "lib.es2022.object.d.ts"], ["es2022.sharedmemory", "lib.es2022.sharedmemory.d.ts"], ["es2022.string", "lib.es2022.string.d.ts"], - ["esnext.array", "lib.es2022.array.d.ts"], + ["esnext.array", "lib.esnext.array.d.ts"], ["esnext.symbol", "lib.es2019.symbol.d.ts"], ["esnext.asynciterable", "lib.es2018.asynciterable.d.ts"], ["esnext.intl", "lib.esnext.intl.d.ts"], diff --git a/src/compiler/deno.ts b/src/compiler/deno.ts new file mode 100644 index 0000000000000..1554ac299b872 --- /dev/null +++ b/src/compiler/deno.ts @@ -0,0 +1,220 @@ +/* @internal */ +namespace ts.deno { + export type IsNodeSourceFileCallback = (sourceFile: SourceFile) => boolean; + + let isNodeSourceFile: IsNodeSourceFileCallback = () => false; + + export function setIsNodeSourceFileCallback(callback: IsNodeSourceFileCallback) { + isNodeSourceFile = callback; + } + + // When upgrading: + // 1. Inspect all usages of "globals" and "globalThisSymbol" in checker.ts + // - Beware that `globalThisType` might refer to the global `this` type + // and not the global `globalThis` type + // 2. Inspect the types in @types/node for anything that might need to go below + // as well. + + const nodeOnlyGlobalNames = new Set([ + "NodeRequire", + "RequireResolve", + "RequireResolve", + "process", + "console", + "__filename", + "__dirname", + "require", + "module", + "exports", + "gc", + "BufferEncoding", + "BufferConstructor", + "WithImplicitCoercion", + "Buffer", + "Console", + "ImportMeta", + "setTimeout", + "setInterval", + "setImmediate", + "Global", + "AbortController", + "AbortSignal", + "Blob", + "BroadcastChannel", + "MessageChannel", + "MessagePort", + "Event", + "EventTarget", + "performance", + "TextDecoder", + "TextEncoder", + "URL", + "URLSearchParams", + ]) as Set; + + export function createDenoForkContext({ + mergeSymbol, + globals, + nodeGlobals, + ambientModuleSymbolRegex, + }: { + mergeSymbol(target: Symbol, source: Symbol, unidirectional?: boolean): Symbol; + globals: SymbolTable; + nodeGlobals: SymbolTable; + ambientModuleSymbolRegex: RegExp, + }) { + return { + hasNodeSourceFile, + getGlobalsForName, + mergeGlobalSymbolTable, + combinedGlobals: createNodeGlobalsSymbolTable(), + }; + + function hasNodeSourceFile(node: Node | undefined) { + if (!node) return false; + const sourceFile = getSourceFileOfNode(node); + return isNodeSourceFile(sourceFile); + } + + function getGlobalsForName(id: ts.__String) { + // Node ambient modules are only accessible in the node code, + // so put them on the node globals + if (ambientModuleSymbolRegex.test(id as string)) + return nodeGlobals; + return nodeOnlyGlobalNames.has(id) ? nodeGlobals : globals; + } + + function mergeGlobalSymbolTable(node: Node, source: SymbolTable, unidirectional = false) { + const sourceFile = getSourceFileOfNode(node); + const isNodeFile = hasNodeSourceFile(sourceFile); + source.forEach((sourceSymbol, id) => { + const target = isNodeFile ? getGlobalsForName(id) : globals; + const targetSymbol = target.get(id); + target.set(id, targetSymbol ? mergeSymbol(targetSymbol, sourceSymbol, unidirectional) : sourceSymbol); + }); + } + + function createNodeGlobalsSymbolTable() { + return new Proxy(globals, { + get(target, prop: string | symbol, receiver) { + if (prop === "get") { + return (key: ts.__String) => { + return nodeGlobals.get(key) ?? globals.get(key); + }; + } else if (prop === "has") { + return (key: ts.__String) => { + return nodeGlobals.has(key) || globals.has(key); + }; + } else if (prop === "size") { + let i = 0; + forEachEntry(() => { + i++; + }); + return i; + } else if (prop === "forEach") { + return (action: (value: Symbol, key: ts.__String) => void) => { + forEachEntry(([key, value]) => { + action(value, key); + }); + }; + } else if (prop === "entries") { + return () => { + return getEntries(kv => kv); + }; + } else if (prop === "keys") { + return () => { + return getEntries(kv => kv[0]); + }; + } else if (prop === "values") { + return () => { + return getEntries(kv => kv[1]); + }; + } else if (prop === Symbol.iterator) { + return () => { + // Need to convert this to an array since typescript targets ES5 + // and providing back the iterator won't work here. I don't want + // to change the target to ES6 because I'm not sure if that would + // surface any issues. + return arrayFrom(getEntries(kv => kv))[Symbol.iterator](); + }; + } else { + const value = (target as any)[prop]; + if (value instanceof Function) { + return function (this: any, ...args: any[]) { + return value.apply(this === receiver ? target : this, args); + }; + } + return value; + } + }, + }); + + function forEachEntry(action: (value: [__String, Symbol]) => void) { + const iterator = getEntries((entry) => { + action(entry); + }); + // drain the iterator to do the action + while (!iterator.next().done) {} + } + + function* getEntries( + transform: (value: [__String, Symbol]) => R + ) { + const foundKeys = new Set(); + for (const entries of [nodeGlobals.entries(), globals.entries()]) { + let next = entries.next(); + while (!next.done) { + if (!foundKeys.has(next.value[0])) { + yield transform(next.value); + foundKeys.add(next.value[0]); + } + next = entries.next(); + } + } + } + } + } + + export interface NpmPackageReference { + name: string; + versionReq: string; + subPath: string | undefined; + } + + export function tryParseNpmPackageReference(text: string) { + try { + return parseNpmPackageReference(text); + } catch { + return undefined; + } + } + + export function parseNpmPackageReference(text: string) { + if (!text.startsWith("npm:")) { + throw new Error(`Not an npm specifier: ${text}`); + } + text = text.replace(/^npm:\/?/, ""); + const parts = text.split("/"); + const namePartLen = text.startsWith("@") ? 2 : 1; + if (parts.length < namePartLen) { + throw new Error(`Not a valid package: ${text}`); + } + const nameParts = parts.slice(0, namePartLen); + const lastNamePart = nameParts.at(-1)!; + const lastAtIndex = lastNamePart.lastIndexOf("@"); + let versionReq: string | undefined = undefined; + if (lastAtIndex > 0) { + versionReq = lastNamePart.substring(lastAtIndex + 1); + nameParts[nameParts.length - 1] = lastNamePart.substring(0, lastAtIndex); + } + const name = nameParts.join("/"); + if (name.length === 0) { + throw new Error(`Npm specifier did not have a name: ${text}`); + } + return { + name, + versionReq, + subPath: parts.length > nameParts.length ? parts.slice(nameParts.length).join("/") : undefined, + }; + } +} diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index fccbe06558a23..ab8986c9df11e 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -42,6 +42,7 @@ "binder.ts", "symbolWalker.ts", "checker.ts", + "deno.ts", "visitorPublic.ts", "sourcemap.ts", "transformers/utilities.ts", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e7aa743008d69..000851c2b9e03 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4494,7 +4494,7 @@ namespace ts { /* @internal */ forEachExportAndPropertyOfModule(moduleSymbol: Symbol, cb: (symbol: Symbol, key: __String) => void): void; getJsxIntrinsicTagNamesAt(location: Node): Symbol[]; isOptionalParameter(node: ParameterDeclaration): boolean; - getAmbientModules(): Symbol[]; + getAmbientModules(sourceFile?: SourceFile): Symbol[]; tryGetMemberInModuleExports(memberName: string, moduleSymbol: Symbol): Symbol | undefined; /** diff --git a/src/services/completions.ts b/src/services/completions.ts index c24bc8dcaf165..5cdbceb88230b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -4209,7 +4209,8 @@ namespace ts.Completions { if (globalSymbol && checker.getTypeOfSymbolAtLocation(globalSymbol, sourceFile) === type) { return true; } - const globalThisSymbol = checker.resolveName("globalThis", /*location*/ undefined, SymbolFlags.Value, /*excludeGlobals*/ false); + // deno: provide sourceFile so that it can figure out if it's a node or deno globalThis + const globalThisSymbol = checker.resolveName("globalThis", /*location*/ sourceFile, SymbolFlags.Value, /*excludeGlobals*/ false); if (globalThisSymbol && checker.getTypeOfSymbolAtLocation(globalThisSymbol, sourceFile) === type) { return true; }