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

Use @definitelytyped/types-registry for ATA #33791

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 66 additions & 6 deletions src/testRunner/unittests/tsserver/typingsInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1724,21 +1724,21 @@ namespace ts.projectSystem {
];
it("works when the command is too long to install all packages at once", () => {
const commands: string[] = [];
const hasError = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => {
const succeeded = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => {
commands.push(command);
return false;
return true;
});
assert.isFalse(hasError);
assert.isTrue(succeeded);
assert.deepEqual(commands, expectedCommands, "commands");
});

it("installs remaining packages when one of the partial command fails", () => {
const commands: string[] = [];
const hasError = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => {
const succeeded = TI.installNpmPackages(npmPath, tsVersion, packageNames, command => {
commands.push(command);
return commands.length === 1;
return commands.length === 0;
});
assert.isTrue(hasError);
assert.isFalse(succeeded);
assert.deepEqual(commands, expectedCommands, "commands");
});
});
Expand Down Expand Up @@ -1955,4 +1955,64 @@ declare module "stream" {
checkProjectActualFiles(project, [file.path]);
});
});

describe("unittests:: tsserver:: nodeTypingsInstaller", () => {
function createHost() {
const log = {
out: "",
isEnabled: () => true,
writeLine(text: string) {
this.out += text + sys.newLine;
},
};
return {
log,
useCaseSensitiveFileNames: true,
execSyncAndLog(command: string, options: string): boolean {
log.writeLine(`Running ${command} ${options}`);
return true;
},
writeFile(): void { },
createDirectory(): void { },

directoryExists(): boolean {
return true;
},
fileExists(): boolean {
return true;
},
readFile(): string | undefined {
return '{ "yep": true }';
},
readDirectory(): string[] {
return [];
},
};
}
it("constructs successfully from @definitelytyped", () => {
const host = createHost();
const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log);
assert.isUndefined((i as any).delayedInitializationError);
assert.include(host.log.out, "Updated @definitelytyped/types-registry");
});
it("constructs successfully from npm (falling back from @definitelytyped)", () => {
const host = createHost();
host.execSyncAndLog = function(command) {
return !command.includes("@definitelytyped");
}
const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log);
assert.isUndefined((i as any).delayedInitializationError);
assert.notInclude(host.log.out, "Updated @definitelytyped/types-registry");
assert.include(host.log.out, "Updated types-registry");
});
it("fails construction", () => {
const host = createHost();
host.execSyncAndLog = function() {
return false;
}
const i = new TI.NodeTypingsInstaller(host, "a", "b", "c", "d", true, 1, host.log);
assert.equal("event::initializationFailed", (i as any).delayedInitializationError.kind);
assert.equal("UPDATE FAILED", (i as any).delayedInitializationError.message);
});
});
}
242 changes: 39 additions & 203 deletions src/typingsInstaller/nodeTypingsInstaller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
/**
* Create a NodeTypingsInstaller for use in ATA.
*
* This file creates an instance of NodeTypingsInstaller *upon evaluation*,
* so is not suitable for inclusion in other compilations.
* See ../typingsInstallerCore/nodeTypingsInstallerCore.ts for the class definition,
* and ../testRunner/unittests/tsserver/typingsInstaller.ts for the tests.
*/
namespace ts.server.typingsInstaller {
const fs: {
appendFileSync(file: string, content: string): void
} = require("fs");

const path: {
join(...parts: string[]): string;
dirname(path: string): string;
basename(path: string, extension?: string): string;
} = require("path");

class FileLog implements Log {
constructor(private logFile: string | undefined) {
}
Expand All @@ -28,202 +30,6 @@ namespace ts.server.typingsInstaller {
};
}

/** Used if `--npmLocation` is not passed. */
function getDefaultNPMLocation(processName: string, validateDefaultNpmLocation: boolean, host: InstallTypingHost): string {
if (path.basename(processName).indexOf("node") === 0) {
const npmPath = path.join(path.dirname(process.argv[0]), "npm");
if (!validateDefaultNpmLocation) {
return npmPath;
}
if (host.fileExists(npmPath)) {
return `"${npmPath}"`;
}
}
return "npm";
}

interface TypesRegistryFile {
entries: MapLike<MapLike<string>>;
}

function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map<MapLike<string>> {
if (!host.fileExists(typesRegistryFilePath)) {
if (log.isEnabled()) {
log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`);
}
return createMap<MapLike<string>>();
}
try {
const content = <TypesRegistryFile>JSON.parse(host.readFile(typesRegistryFilePath)!);
return createMapFromTemplate(content.entries);
}
catch (e) {
if (log.isEnabled()) {
log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(<Error>e).message}, ${(<Error>e).stack}`);
}
return createMap<MapLike<string>>();
}
}

const typesRegistryPackageName = "types-registry";
function getTypesRegistryFileLocation(globalTypingsCacheLocation: string): string {
return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${typesRegistryPackageName}/index.json`);
}

interface ExecSyncOptions {
cwd: string;
encoding: "utf-8";
}
type ExecSync = (command: string, options: ExecSyncOptions) => string;

export class NodeTypingsInstaller extends TypingsInstaller {
private readonly nodeExecSync: ExecSync;
private readonly npmPath: string;
readonly typesRegistry: Map<MapLike<string>>;

private delayedInitializationError: InitializationFailedResponse | undefined;

constructor(globalTypingsCacheLocation: string, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, validateDefaultNpmLocation: boolean, throttleLimit: number, log: Log) {
super(
sys,
globalTypingsCacheLocation,
typingSafeListLocation ? toPath(typingSafeListLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
typesMapLocation ? toPath(typesMapLocation, "", createGetCanonicalFileName(sys.useCaseSensitiveFileNames)) : toPath("typesMap.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
throttleLimit,
log);
this.npmPath = npmLocation !== undefined ? npmLocation : getDefaultNPMLocation(process.argv[0], validateDefaultNpmLocation, this.installTypingHost);

// If the NPM path contains spaces and isn't wrapped in quotes, do so.
if (stringContains(this.npmPath, " ") && this.npmPath[0] !== `"`) {
this.npmPath = `"${this.npmPath}"`;
}
if (this.log.isEnabled()) {
this.log.writeLine(`Process id: ${process.pid}`);
this.log.writeLine(`NPM location: ${this.npmPath} (explicit '${Arguments.NpmLocation}' ${npmLocation === undefined ? "not " : ""} provided)`);
this.log.writeLine(`validateDefaultNpmLocation: ${validateDefaultNpmLocation}`);
}
({ execSync: this.nodeExecSync } = require("child_process"));

this.ensurePackageDirectoryExists(globalTypingsCacheLocation);

try {
if (this.log.isEnabled()) {
this.log.writeLine(`Updating ${typesRegistryPackageName} npm package...`);
}
this.execSyncAndLog(`${this.npmPath} install --ignore-scripts ${typesRegistryPackageName}@${this.latestDistTag}`, { cwd: globalTypingsCacheLocation });
if (this.log.isEnabled()) {
this.log.writeLine(`Updated ${typesRegistryPackageName} npm package`);
}
}
catch (e) {
if (this.log.isEnabled()) {
this.log.writeLine(`Error updating ${typesRegistryPackageName} package: ${(<Error>e).message}`);
}
// store error info to report it later when it is known that server is already listening to events from typings installer
this.delayedInitializationError = {
kind: "event::initializationFailed",
message: (<Error>e).message
};
}

this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation), this.installTypingHost, this.log);
}

listen() {
process.on("message", (req: TypingInstallerRequestUnion) => {
if (this.delayedInitializationError) {
// report initializationFailed error
this.sendResponse(this.delayedInitializationError);
this.delayedInitializationError = undefined;
}
switch (req.kind) {
case "discover":
this.install(req);
break;
case "closeProject":
this.closeProject(req);
break;
case "typesRegistry": {
const typesRegistry: { [key: string]: MapLike<string> } = {};
this.typesRegistry.forEach((value, key) => {
typesRegistry[key] = value;
});
const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry };
this.sendResponse(response);
break;
}
case "installPackage": {
const { fileName, packageName, projectName, projectRootPath } = req;
const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath;
if (cwd) {
this.installWorker(-1, [packageName], cwd, success => {
const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`;
const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success, message };
this.sendResponse(response);
});
}
else {
const response: PackageInstalledResponse = { kind: ActionPackageInstalled, projectName, success: false, message: "Could not determine a project root path." };
this.sendResponse(response);
}
break;
}
default:
Debug.assertNever(req);
}
});
}

protected sendResponse(response: TypingInstallerResponseUnion) {
if (this.log.isEnabled()) {
this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`);
}
process.send!(response); // TODO: GH#18217
if (this.log.isEnabled()) {
this.log.writeLine(`Response has been sent.`);
}
}

protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
if (this.log.isEnabled()) {
this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`);
}
const start = Date.now();
const hasError = installNpmPackages(this.npmPath, version, packageNames, command => this.execSyncAndLog(command, { cwd }));
if (this.log.isEnabled()) {
this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms`);
}
onRequestCompleted(!hasError);
}

/** Returns 'true' in case of error. */
private execSyncAndLog(command: string, options: Pick<ExecSyncOptions, "cwd">): boolean {
if (this.log.isEnabled()) {
this.log.writeLine(`Exec: ${command}`);
}
try {
const stdout = this.nodeExecSync(command, { ...options, encoding: "utf-8" });
if (this.log.isEnabled()) {
this.log.writeLine(` Succeeded. stdout:${indent(sys.newLine, stdout)}`);
}
return false;
}
catch (error) {
const { stdout, stderr } = error;
this.log.writeLine(` Failed. stdout:${indent(sys.newLine, stdout)}${sys.newLine} stderr:${indent(sys.newLine, stderr)}`);
return true;
}
}
}

function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined {
return forEachAncestorDirectory(getDirectoryPath(fileName), directory => {
if (host.fileExists(combinePaths(directory, "package.json"))) {
return directory;
}
});
}

const logFilePath = findArgument(Arguments.LogFile);
const globalTypingsCacheLocation = findArgument(Arguments.GlobalCacheLocation);
const typingSafeListLocation = findArgument(Arguments.TypingSafeListLocation);
Expand All @@ -243,7 +49,37 @@ namespace ts.server.typingsInstaller {
}
process.exit(0);
});
const installer = new NodeTypingsInstaller(globalTypingsCacheLocation!, typingSafeListLocation!, typesMapLocation!, npmLocation, validateDefaultNpmLocation, /*throttleLimit*/5, log); // TODO: GH#18217

const nodeHost: NodeInstallTypingHost = {
useCaseSensitiveFileNames: sys.useCaseSensitiveFileNames,
writeFile: sys.writeFile,
createDirectory: sys.createDirectory,
directoryExists: sys.directoryExists,
fileExists: sys.fileExists,
readFile: sys.readFile,
readDirectory: sys.readDirectory,
watchFile: sys.watchFile,

/** Returns false on error */
execSyncAndLog(command: string, options: string, log: Log): boolean {
if (log.isEnabled()) {
log.writeLine(`Exec: ${command}`);
}
try {
const stdout = require("child_process").execSync(command, { cwd: options, encoding: "utf-8" });
if (log.isEnabled()) {
log.writeLine(` Succeeded. stdout:${indent(sys.newLine, stdout)}`);
}
return true;
}
catch (error) {
const { stdout, stderr } = error;
log.writeLine(` Failed. stdout:${indent(sys.newLine, stdout)}${sys.newLine} stderr:${indent(sys.newLine, stderr)}`);
return false;
}
}
}
const installer = new NodeTypingsInstaller(nodeHost, globalTypingsCacheLocation, typingSafeListLocation!, typesMapLocation!, npmLocation, validateDefaultNpmLocation, /*throttleLimit*/5, log); // TODO: GH#18217
installer.listen();

function indent(newline: string, str: string): string {
Expand Down