Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: additive global context for npm packages (first pass) #1

Open
wants to merge 15 commits into
base: branch_v4.8.3
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion src/compiler/builderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
121 changes: 101 additions & 20 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
220 changes: 220 additions & 0 deletions src/compiler/deno.ts
Original file line number Diff line number Diff line change
@@ -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<ts.__String>;

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<R>(
transform: (value: [__String, Symbol]) => R
) {
const foundKeys = new Set<ts.__String>();
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,
};
}
}
1 change: 1 addition & 0 deletions src/compiler/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"binder.ts",
"symbolWalker.ts",
"checker.ts",
"deno.ts",
"visitorPublic.ts",
"sourcemap.ts",
"transformers/utilities.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
3 changes: 2 additions & 1 deletion src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down