Skip to content

Commit

Permalink
feat(*): add npm package dependencies support for templates
Browse files Browse the repository at this point in the history
  • Loading branch information
damyanpetev committed Apr 19, 2018
1 parent c21d67a commit 0b65ed9
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 73 deletions.
1 change: 1 addition & 0 deletions lib/Util.ts
Expand Up @@ -345,6 +345,7 @@ class Util {
* Execute synchronous command with options
* @param command Command to be executed
* @param options Command options
* @throws {Error} On timeout or non-zero exit code. Error has 'status', 'signal', 'output', 'stdout', 'stderr'
*/
public static exec(command: string, options?: any) {
return execSync(command, options);
Expand Down
2 changes: 2 additions & 0 deletions lib/commands/add.ts
Expand Up @@ -77,6 +77,7 @@ command = {
const selectedTemplate = frameworkLibrary.getTemplateById(argv.template);
if (selectedTemplate) {
await command.addTemplate(argv.name, selectedTemplate);
await PackageManager.flushQueue(true);
PackageManager.ensureIgniteUISource(config.packagesInstalled, command.templateManager);
}
},
Expand All @@ -96,6 +97,7 @@ command = {
//successful
template.registerInProject(process.cwd(), name);
command.templateManager.updateProjectConfiguration(template);
template.packages.forEach(x => PackageManager.queuePackage(x));
Util.log(`${Util.greenCheck()} View '${name}' added.`);
return true;
} else {
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/build.ts
Expand Up @@ -34,7 +34,7 @@ command = {

const config = ProjectConfig.getConfig();

PackageManager.installPackages();
await PackageManager.installPackages();

if (config.project.theme.includes(".less") || config.project.theme.includes(".sass")) {
fs.mkdirSync("./themes");
Expand Down
122 changes: 88 additions & 34 deletions lib/packages/PackageManager.ts
@@ -1,16 +1,16 @@
import { spawnSync } from "child_process";
import { exec, execSync, spawnSync } from "child_process";
import * as path from "path";
import { exec, ExecOutputReturnValue } from "shelljs";
import { ProjectConfig } from "../ProjectConfig";
import { Util } from "../Util";
import { TemplateManager } from "./../TemplateManager";

import componentsConfig = require("./components");

export class PackageManager {
private static ossPackage: string = "ignite-ui";
private static fullPackage: string = "@infragistics/ignite-ui-full";

private static installQueue: Array<Promise<{ packageName, error, stdout, stderr }>> = [];

/**
* Specific for Ignite UI packages handling:
*
Expand Down Expand Up @@ -60,7 +60,7 @@ export class PackageManager {
}
}

public static installPackages(verbose: boolean = false) {
public static async installPackages(verbose: boolean = false) {
const config = ProjectConfig.getConfig();
if (!config.packagesInstalled) {
let command: string;
Expand All @@ -77,15 +77,22 @@ export class PackageManager {
command = `${managerCommand} install --quiet`;
break;
}
await this.flushQueue(false);
Util.log(`Installing ${managerCommand} packages`);
const result = exec(command, { silent: true }) as ExecOutputReturnValue;
if (result.code !== 0) {
try {
const result = execSync(command, { stdio: "pipe", killSignal: "SIGINT" });
Util.log(`Packages installed successfully`);
} catch (error) {
// ^C (SIGINT) produces status:3221225786 https://github.com/sass/node-sass/issues/1283#issuecomment-169450661
if (error.status === 3221225786 || error.status > 128) {
// drop process on user interrupt
process.exit();
return; // just for tests
}
Util.log(`Error installing ${managerCommand} packages.`);
if (verbose) {
Util.log(result.stderr);
Util.log(error.message);
}
} else {
Util.log(`Packages installed successfully`);
}
config.packagesInstalled = true;
config.skipAnalytic = oldSkipAnalytic;
Expand All @@ -103,41 +110,82 @@ export class PackageManager {
command = `${managerCommand} uninstall ${packageName} --quiet --save`;
break;
}
const result = exec(command, { silent: true }) as ExecOutputReturnValue;

if (result.code !== 0) {
try {
const result = execSync(command, { stdio: "pipe", encoding: "utf8" });
} catch (error) {
Util.log(`Error uninstalling package ${packageName} with ${managerCommand}`);
if (verbose) {
Util.log(result.stderr);
Util.log(error.message);
}
return false;
} else {
Util.log(`Package ${packageName} uninstalled successfully`);
return true;
}

Util.log(`Package ${packageName} uninstalled successfully`);
return true;
}

public static addPackage(packageName: string, verbose: boolean = false): boolean {
let command: string;
const managerCommand = this.getManager();
switch (managerCommand) {
case "npm":
/* passes through */
default:
command = `${managerCommand} install ${packageName} --quiet --save`;
break;
}
const result = exec(command, { silent: true }) as ExecOutputReturnValue;

if (result.code !== 0) {
const command = this.getInstallCommand(managerCommand, packageName);
try {
const result = execSync(command, { stdio: "pipe", encoding: "utf8" });
} catch (error) {
Util.log(`Error installing package ${packageName} with ${managerCommand}`);
if (verbose) {
Util.log(result.stderr);
Util.log(error.message);
}
return false;
} else {
Util.log(`Package ${packageName} installed successfully`);
return true;
}
Util.log(`Package ${packageName} installed successfully`);
return true;
}

public static async queuePackage(packageName: string, verbose = false) {
const command = this.getInstallCommand(this.getManager(), packageName);
const packName = packageName.split(/@[^\/]+$/)[0];
if (this.getPackageJSON().dependencies[packName] || this.installQueue.find(x => x["packageName"] === packName)) {
return;
}
// D.P. Concurrent install runs should be supported
// https://github.com/npm/npm/issues/5948
// https://github.com/npm/npm/issues/2500
const task = new Promise<{ packageName, error, stdout, stderr }>((resolve, reject) => {
const child = exec(
command, { },
(error, stdout, stderr) => {
resolve({ packageName, error, stdout, stderr });
}
);
});
task["packageName"] = packName;
this.installQueue.push(task);
}

/** Waits for queued installs to finish, optionally log results and clear queue */
public static async flushQueue(logSuccess: boolean, verbose = false) {
if (this.installQueue.length) {
Util.log(`Waiting for additional packages to install`);
const results = await Promise.all(this.installQueue);
for (const res of results) {
if (res.error) {
Util.log(`Error installing package ${res.packageName}`);
if (verbose) {
Util.log(res.stderr.toString());
}
} else if (logSuccess) {
Util.log(`Package ${res.packageName} installed successfully`);
}
}
this.installQueue = [];
}
}

private static getInstallCommand(managerCommand: string, packageName: string): string {
switch (managerCommand) {
case "npm":
/* passes through */
default:
return `${managerCommand} install ${packageName} --quiet --save`;
}
}

Expand All @@ -152,8 +200,9 @@ export class PackageManager {

private static ensureRegistryUser(config: Config): boolean {
const fullPackageRegistry = config.igPackageRegistry;
const user = exec(`npm whoami --registry=${fullPackageRegistry}`, { silent: true }) as ExecOutputReturnValue;
if (user.code !== 0) {
try {
const user = execSync(`npm whoami --registry=${fullPackageRegistry}`, { stdio: "pipe", encoding: "utf8" });
} catch (error) {
// try registering the user:
Util.log(
"The project you've created requires the full version of Ignite UI from Infragistics private feed.",
Expand All @@ -174,7 +223,12 @@ export class PackageManager {
);
if (login.status === 0) {
//make sure scope is configured:
return exec(`npm config set @infragistics:registry ${fullPackageRegistry}`).code === 0;
try {
execSync(`npm config set @infragistics:registry ${fullPackageRegistry}`);
return true;
} catch (error) {
return false;
}
} else {
Util.log("Something went wrong, " +
"please follow the steps in this guide: https://www.igniteui.com/help/using-ignite-ui-npm-packages", "red");
Expand Down
1 change: 1 addition & 0 deletions lib/templates/AngularTemplate.ts
Expand Up @@ -16,6 +16,7 @@ export class AngularTemplate implements Template {
public framework: string = "angular";
public projectType: string;
public hasExtraConfiguration: boolean = false;
public packages = [];
protected widget: string;

/**
Expand Down
1 change: 1 addition & 0 deletions lib/templates/ReactTemplate.ts
Expand Up @@ -14,6 +14,7 @@ export class ReactTemplate implements Template {
public framework: string = "react";
public projectType: string;
public hasExtraConfiguration: boolean = false;
public packages = [];

// non-standard template prop
protected widget: string;
Expand Down
1 change: 1 addition & 0 deletions lib/templates/jQueryTemplate.ts
Expand Up @@ -19,6 +19,7 @@ export class jQueryTemplate implements Template {
public framework: string = "jquery";
public projectType: string;
public hasExtraConfiguration: boolean;
public packages = [];

private configFile: string = "ignite-cli-views.js";
private replacePattern: RegExp = /\[[\s\S]*\](?=;)/;
Expand Down
2 changes: 2 additions & 0 deletions lib/types/Template.d.ts
Expand Up @@ -8,6 +8,8 @@ declare interface Template extends BaseTemplate {
/** Step by step */
listInComponentTemplates: boolean;
listInCustomTemplates: boolean;
/** Extra packages to install when adding to project */
packages: string[];
/** Generates template files. */
generateFiles(projectPath: string, name: string, ...options: any[]): Promise<boolean>;
/** Called when the template is added to a project */
Expand Down
42 changes: 42 additions & 0 deletions spec/unit/add-spec.ts
Expand Up @@ -2,6 +2,7 @@
import * as fs from "fs-extra";
import { default as addCmd } from "../../lib/commands/add";
import { GoogleAnalytic } from "../../lib/GoogleAnalytic";
import { PackageManager } from "../../lib/packages/PackageManager";
import { ProjectConfig } from "../../lib/ProjectConfig";
import { PromptSession } from "../../lib/PromptSession";
import { Util } from "../../lib/Util";
Expand Down Expand Up @@ -70,4 +71,45 @@ describe("Unit - Add command", () => {
done();
});

it("Should queue package dependencies and wait for install", async done => {
spyOn(ProjectConfig, "getConfig").and.returnValue({ project: {
framework: "angular",
theme: "infragistics"}});
spyOn(Util, "log");
spyOn(PackageManager, "queuePackage");

const mockTemplate = jasmine.createSpyObj("Template", {
generateFiles: Promise.resolve(true),
registerInProject: null
});
mockTemplate.packages = ["tslib" , "test-pack"];
addCmd.templateManager = jasmine.createSpyObj("TemplateManager", ["updateProjectConfiguration"]);

await addCmd.addTemplate("template with packages", mockTemplate);
expect(mockTemplate.generateFiles).toHaveBeenCalled();
expect(mockTemplate.registerInProject).toHaveBeenCalled();
expect(addCmd.templateManager.updateProjectConfiguration).toHaveBeenCalled();
expect(PackageManager.queuePackage).toHaveBeenCalledTimes(2);
expect(PackageManager.queuePackage).toHaveBeenCalledWith("tslib");
expect(PackageManager.queuePackage).toHaveBeenCalledWith("test-pack");

spyOn(GoogleAnalytic, "post");
spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true);
addCmd.templateManager = jasmine.createSpyObj("TemplateManager", {
getFrameworkById: {},
getProjectLibrary: jasmine.createSpyObj("ProjectLibrary", {
getTemplateById: {},
hasTemplate: true
})
});

spyOn(addCmd, "addTemplate");
spyOn(PackageManager, "flushQueue").and.returnValue(Promise.resolve());
spyOn(PackageManager, "ensureIgniteUISource");
await addCmd.execute({name: "template with packages", template: "test-id"});
expect(addCmd.addTemplate).toHaveBeenCalledWith("template with packages", {});
expect(PackageManager.flushQueue).toHaveBeenCalled();

done();
});
});

0 comments on commit 0b65ed9

Please sign in to comment.