From 95f55b6867e5e5a0783ba9b58797265e983db6cd Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Thu, 1 Feb 2018 10:09:43 +0200 Subject: [PATCH 1/8] feat(global-config): intitial config methods and defaults --- lib/ProjectConfig.ts | 44 ++++++++++++++++++++++++++++++++-------- lib/config/defaults.json | 3 +++ lib/types/Config.d.ts | 9 ++++++-- 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 lib/config/defaults.json diff --git a/lib/ProjectConfig.ts b/lib/ProjectConfig.ts index 62962cbd9..41b607d43 100644 --- a/lib/ProjectConfig.ts +++ b/lib/ProjectConfig.ts @@ -1,26 +1,54 @@ import * as fs from "fs-extra"; +import * as os from "os"; import * as path from "path"; export class ProjectConfig { public static configFile: string = "ignite-ui-cli.json"; - public static getConfig(): Config { + /** TODO: Use this to check for existing local project instead of `getConfig` */ + public static localConfigFile(): boolean { const filePath = path.join(process.cwd(), this.configFile); - if (fs.existsSync(filePath)) { + return fs.existsSync(filePath); + } + + public static getConfig(global: boolean = false): Config { + const filePath = path.join(process.cwd(), this.configFile); + let config = this.globalDefaults(); + + if (!global && fs.existsSync(filePath)) { try { - return JSON.parse(fs.readFileSync(filePath, "utf8")) as Config; + const localConfig = JSON.parse(fs.readFileSync(filePath, "utf8")) as Config; + config = Object.assign(config, localConfig); } catch (error) { throw new Error(`The ${this.configFile} file is not parsed correctly. ` + `The following error has occurred: ${error.message}`); } } - return null; + return config; } - public static setConfig(config: Config) { - const filePath = path.join(process.cwd(), this.configFile); - if (fs.existsSync(filePath)) { - fs.writeJsonSync(filePath, config, { spaces: 4 }); + public static setConfig(config: Config, global: boolean = false) { + const basePath = global ? os.homedir() : process.cwd(); + const filePath = path.join(basePath, this.configFile); + fs.writeJsonSync(filePath, config, { spaces: 4 }); + } + + public static globalConfig(): Config { + const globalConfigPath = path.join(os.homedir(), this.configFile); + let globalConfig = {}; + + if (fs.existsSync(globalConfigPath)) { + globalConfig = require(globalConfigPath); } + return globalConfig as Config; + } + + private static globalDefaults(): Config { + let defaults: Config = require("./config/defaults.json"); + const globalConfig = this.globalConfig(); + + // TODO: `Object.assign` doesn't do deep extend, nested object properties override! + defaults = Object.assign(defaults, globalConfig); + return defaults; } } diff --git a/lib/config/defaults.json b/lib/config/defaults.json new file mode 100644 index 000000000..a0dd94d02 --- /dev/null +++ b/lib/config/defaults.json @@ -0,0 +1,3 @@ +{ + "igPackageRegistry": "https://packages.infragistics.com/npm/js-licensed/" +} \ No newline at end of file diff --git a/lib/types/Config.d.ts b/lib/types/Config.d.ts index 1314c44f2..5960d6dc0 100644 --- a/lib/types/Config.d.ts +++ b/lib/types/Config.d.ts @@ -27,11 +27,16 @@ declare interface Config { igniteuiSource: string; [key: string]: any; - }; + } build: { /** This object contains information related to the build configuration * and server configuration of the project */ //"projectBuild": "tsc", //"serverType": "webpack" - }; + } + + /** An array of paths to red custom templates from */ + customTemplates: string[]; + /** Infragistics package registry Url */ + igPackageRegistry: string; } From c724bc458c1b5974264390041c7bec294723a6cf Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Thu, 1 Feb 2018 17:55:44 +0200 Subject: [PATCH 2/8] refactor: use hasLocalConfig check for existing project --- lib/ProjectConfig.ts | 4 ++-- lib/PromptSession.ts | 2 +- lib/commands/add.ts | 5 ++--- lib/commands/new.ts | 2 +- spec/unit/add-spec.ts | 1 + spec/unit/new-spec.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/ProjectConfig.ts b/lib/ProjectConfig.ts index 41b607d43..cfc4c8ed4 100644 --- a/lib/ProjectConfig.ts +++ b/lib/ProjectConfig.ts @@ -6,8 +6,8 @@ export class ProjectConfig { public static configFile: string = "ignite-ui-cli.json"; - /** TODO: Use this to check for existing local project instead of `getConfig` */ - public static localConfigFile(): boolean { + /** Returns true if there's a CLI config file in the current working directory */ + public static hasLocalConfig(): boolean { const filePath = path.join(process.cwd(), this.configFile); return fs.existsSync(filePath); } diff --git a/lib/PromptSession.ts b/lib/PromptSession.ts index a7d3435c0..0b01787e1 100644 --- a/lib/PromptSession.ts +++ b/lib/PromptSession.ts @@ -22,7 +22,7 @@ export class PromptSession { add.templateManager = this.templateManager; // tslint:disable:object-literal-sort-keys - if (config != null && !config.project.isShowcase) { + if (ProjectConfig.hasLocalConfig() && !config.project.isShowcase) { projLibrary = this.templateManager.getProjectLibrary(config.project.framework, config.project.projectType); await this.chooseActionLoop(projLibrary, config.project.theme); } else { diff --git a/lib/commands/add.ts b/lib/commands/add.ts index d308826a5..500d44847 100644 --- a/lib/commands/add.ts +++ b/lib/commands/add.ts @@ -36,12 +36,11 @@ command = { return true; }, async execute(argv) { - //command.template; - const config = ProjectConfig.getConfig(); - if (config == null) { + if (!ProjectConfig.hasLocalConfig()) { Util.error("Add command is supported only on existing project created with igniteui-cli", "red"); return; } + const config = ProjectConfig.getConfig(); if (config.project.isShowcase) { Util.error("Showcases and quickstart projects don't support the add command", "red"); return; diff --git a/lib/commands/new.ts b/lib/commands/new.ts index 6300a94bc..f73243933 100644 --- a/lib/commands/new.ts +++ b/lib/commands/new.ts @@ -40,7 +40,7 @@ command = { }, template: null, async execute(argv) { - if (ProjectConfig.getConfig() !== null) { + if (ProjectConfig.hasLocalConfig()) { return Util.error("There is already an existing project.", "red"); } if (!argv.name && !argv.type && !argv.theme) { diff --git a/spec/unit/add-spec.ts b/spec/unit/add-spec.ts index 83072d13f..a87881534 100644 --- a/spec/unit/add-spec.ts +++ b/spec/unit/add-spec.ts @@ -9,6 +9,7 @@ import { resetSpy } from "../helpers/utils"; describe("Unit - Add command", () => { it("Should start prompt session with missing arg", async done => { + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); spyOn(ProjectConfig, "getConfig").and.returnValue({ project: { framework: "angular", theme: "infragistics"}}); diff --git a/spec/unit/new-spec.ts b/spec/unit/new-spec.ts index aeee20d35..95a003f30 100644 --- a/spec/unit/new-spec.ts +++ b/spec/unit/new-spec.ts @@ -12,7 +12,7 @@ describe("Unit - New command", () => { it("New command in existing project", async done => { spyOn(Util, "error"); - spyOn(ProjectConfig, "getConfig").and.returnValue({}); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); await newCmd.execute({}); expect(Util.error).toHaveBeenCalledWith("There is already an existing project.", "red"); From da56ec10cdf4473f13dbba02bef89bb660642ee1 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Thu, 1 Feb 2018 19:46:26 +0200 Subject: [PATCH 3/8] feat: add config get/set commands --- lib/ProjectConfig.ts | 30 +++++++++---- lib/cli.ts | 4 ++ lib/commands/config.ts | 76 ++++++++++++++++++++++++++++++++ spec/unit/config-spec.ts | 93 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 lib/commands/config.ts create mode 100644 spec/unit/config-spec.ts diff --git a/lib/ProjectConfig.ts b/lib/ProjectConfig.ts index cfc4c8ed4..e8f49352c 100644 --- a/lib/ProjectConfig.ts +++ b/lib/ProjectConfig.ts @@ -12,18 +12,17 @@ export class ProjectConfig { return fs.existsSync(filePath); } + /** + * Get effective CLI configuration (merged defaults, global and local) + * @param global return only global values + */ public static getConfig(global: boolean = false): Config { const filePath = path.join(process.cwd(), this.configFile); let config = this.globalDefaults(); - if (!global && fs.existsSync(filePath)) { - try { - const localConfig = JSON.parse(fs.readFileSync(filePath, "utf8")) as Config; - config = Object.assign(config, localConfig); - } catch (error) { - throw new Error(`The ${this.configFile} file is not parsed correctly. ` + - `The following error has occurred: ${error.message}`); - } + if (!global) { + const localConfig = this.localConfig(); + config = Object.assign(config, localConfig); } return config; } @@ -33,6 +32,21 @@ export class ProjectConfig { fs.writeJsonSync(filePath, config, { spaces: 4 }); } + public static localConfig(): Config { + const filePath = path.join(process.cwd(), this.configFile); + let localConfig = {}; + + if (fs.existsSync(filePath)) { + try { + localConfig = JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (error) { + throw new Error(`The ${this.configFile} file is not parsed correctly. ` + + `The following error has occurred: ${error.message}`); + } + } + return localConfig as Config; + } + public static globalConfig(): Config { const globalConfigPath = path.join(os.homedir(), this.configFile); let globalConfig = {}; diff --git a/lib/cli.ts b/lib/cli.ts index 2f0ff33ef..5cd108c99 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -3,6 +3,7 @@ import * as inquirer from "inquirer"; import * as yargs from "yargs"; import { default as add } from "./commands/add"; import { default as build } from "./commands/build"; +import { default as config } from "./commands/config"; import { default as newCommand } from "./commands/new"; import { default as quickstart } from "./commands/quickstart"; import { default as start } from "./commands/start"; @@ -28,6 +29,7 @@ export async function run(args = null) { .command(start) .command(newCommand) .command(build) + .command(config) .command(test) .command(add) .options({ @@ -66,6 +68,8 @@ export async function run(args = null) { case "build": await build.execute(argv); break; + case "config": + break; case "test": await test.execute(argv); break; diff --git a/lib/commands/config.ts b/lib/commands/config.ts new file mode 100644 index 000000000..7e59c8c6f --- /dev/null +++ b/lib/commands/config.ts @@ -0,0 +1,76 @@ +import { Util } from "../Util"; +import { ProjectConfig } from "./../ProjectConfig"; + +const command = { + command: "config", + desc: "Get or set configuration properties", + builder: yargs => { + yargs.command({ + command: "get ", + desc: "Get configuration properties", + builder: { + property: { + describe: "Config property to get", + type: "string" + } + }, + handler: command.getHandler + }).command({ + command: "set ", + desc: "Set configuration properties", + builder: { + property: { + describe: "Config property to set", + type: "string" + }, + value: { + describe: "New value for the property", + type: "string" + } + }, + handler: command.setHandler + }).option("global", { + alias: "g", + type: "boolean", + global: true, + describe: "Specify if the global configuration should be used" + }) + // at least one command is required + .demand(1, "Please use either get or set command"); + }, + getHandler(argv) { + if (!argv.global && !ProjectConfig.hasLocalConfig()) { + Util.error("No configuration file found in this folder!", "red"); + return; + } + const config = ProjectConfig.getConfig(argv.global); + if (config[argv.property] !== undefined) { + Util.log(config[argv.property]); + } else { + Util.error(`No value found for "${argv.property}" property`, "red"); + } + }, + setHandler(argv) { + let config; + + if (argv.global) { + config = ProjectConfig.globalConfig(); + } else { + if (!ProjectConfig.hasLocalConfig()) { + Util.error("No configuration file found in this folder!", "red"); + return; + } + config = ProjectConfig.localConfig(); + } + + if (config[argv.property]) { + // TODO: Schema/property validation? + } + + config[argv.property] = argv.value; + ProjectConfig.setConfig(config, argv.global); + Util.log(`Property "${argv.property}" set`); + } +}; + +export default command; diff --git a/spec/unit/config-spec.ts b/spec/unit/config-spec.ts new file mode 100644 index 000000000..42f1d2a80 --- /dev/null +++ b/spec/unit/config-spec.ts @@ -0,0 +1,93 @@ +import { default as configCmd } from "../../lib/commands/config"; +import { ProjectConfig } from "../../lib/ProjectConfig"; +import { Util } from "../../lib/Util"; + +describe("Unit - Config command", () => { + + describe("Get", () => { + it("Should show error w/o existing project and global flag", async done => { + spyOn(Util, "error"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(false); + + await configCmd.getHandler({ property: "test" }); + expect(Util.error).toHaveBeenCalledWith("No configuration file found in this folder!", "red"); + done(); + }); + + it("Should show error for missing prop", async done => { + spyOn(Util, "error"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); + spyOn(ProjectConfig, "getConfig").and.returnValue({ notTest: "ig" }); + + await configCmd.getHandler({ property: "test" }); + expect(ProjectConfig.getConfig).toHaveBeenCalledWith(undefined); + expect(Util.error).toHaveBeenCalledWith(`No value found for "test" property`, "red"); + done(); + }); + + it("Should show error for missing prop and global flag", async done => { + spyOn(Util, "error"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); + spyOn(ProjectConfig, "getConfig").and.returnValue({ notTest: "ig" }); + + await configCmd.getHandler({ property: "test", global: true }); + expect(ProjectConfig.getConfig).toHaveBeenCalledWith(true); + expect(Util.error).toHaveBeenCalledWith(`No value found for "test" property`, "red"); + done(); + }); + + it("Should return value for property", async done => { + spyOn(Util, "log"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); + spyOn(ProjectConfig, "getConfig").and.returnValue({ test: "igValue" }); + + await configCmd.getHandler({ property: "test" }); + expect(ProjectConfig.getConfig).toHaveBeenCalledWith(undefined); + expect(Util.log).toHaveBeenCalledWith("igValue"); + done(); + }); + }); + + describe("Set", () => { + it("Should show error w/o existing project and global flag", async done => { + spyOn(Util, "error"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(false); + + await configCmd.setHandler({ property: "test", value: true }); + expect(Util.error).toHaveBeenCalledWith("No configuration file found in this folder!", "red"); + done(); + }); + + it("Should set global prop", async done => { + spyOn(Util, "log"); + spyOn(ProjectConfig, "hasLocalConfig"); + spyOn(ProjectConfig, "globalConfig").and.returnValue({ test: "ig" }); + spyOn(ProjectConfig, "localConfig"); + spyOn(ProjectConfig, "setConfig"); + + await configCmd.setHandler({ property: "test", value: true, global: true }); + + expect(ProjectConfig.hasLocalConfig).toHaveBeenCalledTimes(0); + expect(ProjectConfig.localConfig).toHaveBeenCalledTimes(0); + expect(ProjectConfig.globalConfig).toHaveBeenCalled(); + expect(ProjectConfig.setConfig).toHaveBeenCalledWith({ test: true }, true /*global*/); + expect(Util.log).toHaveBeenCalledWith(`Property "test" set`); + done(); + }); + + it("Should set local prop", async done => { + spyOn(Util, "log"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); + spyOn(ProjectConfig, "localConfig").and.returnValue({ notTest: "ig" }); + spyOn(ProjectConfig, "globalConfig"); + spyOn(ProjectConfig, "setConfig"); + + await configCmd.setHandler({ property: "test", value: true }); + expect(ProjectConfig.globalConfig).toHaveBeenCalledTimes(0); + expect(ProjectConfig.localConfig).toHaveBeenCalled(); + expect(ProjectConfig.setConfig).toHaveBeenCalledWith({ notTest: "ig", test: true }, undefined); + expect(Util.log).toHaveBeenCalledWith(`Property "test" set`); + done(); + }); + }); +}); From d16f557d0596a31037dcebe2b7e29134ec2bc95f Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Fri, 2 Feb 2018 19:29:29 +0200 Subject: [PATCH 4/8] refactor(global-config): allow for deep merge on config objects --- lib/ProjectConfig.ts | 20 +++++++++++++++----- lib/Util.ts | 26 ++++++++++++++++++++++++++ lib/config/defaults.json | 3 ++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/ProjectConfig.ts b/lib/ProjectConfig.ts index e8f49352c..111cd1d3e 100644 --- a/lib/ProjectConfig.ts +++ b/lib/ProjectConfig.ts @@ -1,6 +1,7 @@ import * as fs from "fs-extra"; import * as os from "os"; import * as path from "path"; +import { Util } from "./Util"; export class ProjectConfig { @@ -18,20 +19,28 @@ export class ProjectConfig { */ public static getConfig(global: boolean = false): Config { const filePath = path.join(process.cwd(), this.configFile); - let config = this.globalDefaults(); + const config = this.globalDefaults(); if (!global) { const localConfig = this.localConfig(); - config = Object.assign(config, localConfig); + Util.merge(config, localConfig); } return config; } + + /** + * Write a configuration file (either local or global) with given `Config` object. + * Will create or overwrite. + * @param config Config object to set + * @param global Set global values instead + */ public static setConfig(config: Config, global: boolean = false) { const basePath = global ? os.homedir() : process.cwd(); const filePath = path.join(basePath, this.configFile); fs.writeJsonSync(filePath, config, { spaces: 4 }); } + /*** Get local configuration only */ public static localConfig(): Config { const filePath = path.join(process.cwd(), this.configFile); let localConfig = {}; @@ -47,6 +56,7 @@ export class ProjectConfig { return localConfig as Config; } + /*** Get global configuration only */ public static globalConfig(): Config { const globalConfigPath = path.join(os.homedir(), this.configFile); let globalConfig = {}; @@ -57,12 +67,12 @@ export class ProjectConfig { return globalConfig as Config; } + /** Get effective global configuration defaults */ private static globalDefaults(): Config { - let defaults: Config = require("./config/defaults.json"); + const defaults: Config = require("./config/defaults.json"); const globalConfig = this.globalConfig(); - // TODO: `Object.assign` doesn't do deep extend, nested object properties override! - defaults = Object.assign(defaults, globalConfig); + Util.merge(defaults, globalConfig); return defaults; } } diff --git a/lib/Util.ts b/lib/Util.ts index 0a4d248c3..ed46de7a3 100644 --- a/lib/Util.ts +++ b/lib/Util.ts @@ -237,6 +237,32 @@ class Util { public static isAlphanumericExt(name: string) { return /^[\sa-zA-Z][\w\s\-]+$/.test(name); } + + /** + * Simple object merge - deep nested objects and arrays (of primitive values only) + * @param target Object to merge values into + * @param source Object to merge values from + */ + public static merge(target: any, source: any) { + for (const key of Object.keys(source)) { + if (!target.hasOwnProperty(key) || typeof source[key] !== "object") { + // primitive/new value: + target[key] = source[key]; + } else if (Array.isArray(source[key])) { + // skip array merge on target type mismatch: + if (!Array.isArray(target[key])) { + continue; + } + for (const item of source[key]) { + if (target[key].indexOf(item) === -1) { + target[key].push(item); + } + } + } else { + this.merge(target[key], source[key]); + } + } + } } export { Util }; diff --git a/lib/config/defaults.json b/lib/config/defaults.json index a0dd94d02..5135441d8 100644 --- a/lib/config/defaults.json +++ b/lib/config/defaults.json @@ -1,3 +1,4 @@ { - "igPackageRegistry": "https://packages.infragistics.com/npm/js-licensed/" + "igPackageRegistry": "https://packages.infragistics.com/npm/js-licensed/", + "customTemplates": [] } \ No newline at end of file From 50c0a98406cad49973913e232ea0178fa210dbe3 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Mon, 5 Feb 2018 15:34:36 +0200 Subject: [PATCH 5/8] feat: `config add` sub command for array values --- lib/Util.ts | 11 +++++++ lib/commands/config.ts | 44 +++++++++++++++++++++++++++ spec/unit/config-spec.ts | 64 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/lib/Util.ts b/lib/Util.ts index ed46de7a3..36f8e2b5f 100644 --- a/lib/Util.ts +++ b/lib/Util.ts @@ -263,6 +263,17 @@ class Util { } } } + + private static propertyByPath(object: any, propPath: string) { + if (!propPath) { + return object; + } + const pathParts = propPath.split("."); + const currentProp = pathParts.shift(); + if (currentProp in object) { + return this.propertyByPath(object[currentProp], pathParts.join(".")); + } + } } export { Util }; diff --git a/lib/commands/config.ts b/lib/commands/config.ts index 7e59c8c6f..61f4c6582 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -29,6 +29,20 @@ const command = { } }, handler: command.setHandler + }).command({ + command: "add ", + desc: "Add a value to an existing configuration array", + builder: { + property: { + describe: "Config property to add to", + type: "string" + }, + value: { + describe: "New value to add", + type: "string" + } + }, + handler: command.addHandler }).option("global", { alias: "g", type: "boolean", @@ -70,6 +84,36 @@ const command = { config[argv.property] = argv.value; ProjectConfig.setConfig(config, argv.global); Util.log(`Property "${argv.property}" set`); + }, + addHandler(argv) { + let config; + + if (argv.global) { + config = ProjectConfig.globalConfig(); + } else { + if (!ProjectConfig.hasLocalConfig()) { + Util.error("No configuration file found in this folder!", "red"); + return; + } + config = ProjectConfig.localConfig(); + } + + // TODO: Schema/property validation? + if (!config[argv.property]) { + config[argv.property] = []; + } else if (!Array.isArray(config[argv.property])) { + Util.error(`Configuration property "${argv.property}" is not an array, use config set instead.`, "red"); + return; + } + + if (config[argv.property].indexOf(argv.value) !== -1) { + Util.log(`Value already exists in "${argv.property}".`); + return; + } + + config[argv.property].push(argv.value); + ProjectConfig.setConfig(config, argv.global); + Util.log(`Property "${argv.property}" updated.`); } }; diff --git a/spec/unit/config-spec.ts b/spec/unit/config-spec.ts index 42f1d2a80..34c283393 100644 --- a/spec/unit/config-spec.ts +++ b/spec/unit/config-spec.ts @@ -58,6 +58,9 @@ describe("Unit - Config command", () => { done(); }); + xit("Should show error for array property", async done => { + }); + it("Should set global prop", async done => { spyOn(Util, "log"); spyOn(ProjectConfig, "hasLocalConfig"); @@ -90,4 +93,65 @@ describe("Unit - Config command", () => { done(); }); }); + + describe("Add", () => { + it("Should show error w/o existing project and global flag", async done => { + spyOn(Util, "error"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(false); + + await configCmd.addHandler({ property: "test", value: true }); + expect(Util.error).toHaveBeenCalledWith("No configuration file found in this folder!", "red"); + done(); + }); + + it("Should show error for non-array property", async done => { + spyOn(Util, "error"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); + spyOn(ProjectConfig, "localConfig").and.returnValue({ test: "notArray" }); + + await configCmd.addHandler({ property: "test", value: "" }); + expect(Util.error).toHaveBeenCalledWith( + `Configuration property "test" is not an array, use config set instead.`, + "red"); + done(); + }); + + it("Should skip existing", async done => { + spyOn(Util, "log"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); + spyOn(ProjectConfig, "localConfig").and.returnValue({ test: ["existing"] }); + + await configCmd.addHandler({ property: "test", value: "existing" }); + expect(Util.log).toHaveBeenCalledWith(`Value already exists in "test".`); + done(); + }); + + it("Should create/add to global prop", async done => { + spyOn(Util, "log"); + spyOn(ProjectConfig, "globalConfig").and.returnValue({ }); + spyOn(ProjectConfig, "setConfig"); + + await configCmd.addHandler({ property: "test", value: "one", global: true }); + expect(ProjectConfig.setConfig).toHaveBeenCalledWith({ test: ["one"] }, true); + expect(Util.log).toHaveBeenCalledWith(`Property "test" updated.`); + + await configCmd.addHandler({ property: "test", value: "two", global: true }); + expect(ProjectConfig.setConfig).toHaveBeenCalledWith({ test: ["one", "two"] }, true); + expect(Util.log).toHaveBeenCalledWith(`Property "test" updated.`); + + done(); + }); + + it("Should add to local prop", async done => { + spyOn(Util, "log"); + spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); + spyOn(ProjectConfig, "localConfig").and.returnValue({ test: [] }); + spyOn(ProjectConfig, "setConfig"); + + await configCmd.addHandler({ property: "test", value: "first" }); + expect(ProjectConfig.setConfig).toHaveBeenCalledWith({ test: ["first"] }, undefined); + expect(Util.log).toHaveBeenCalledWith(`Property "test" updated.`); + done(); + }); + }); }); From bbd76f73c6a0eea5c2ba54f3b061f2d6ffa7c686 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Mon, 5 Feb 2018 18:46:21 +0200 Subject: [PATCH 6/8] chore: config tslint, description typo --- lib/commands/config.ts | 2 ++ lib/types/Config.d.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/commands/config.ts b/lib/commands/config.ts index 61f4c6582..69a1e07de 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -2,6 +2,7 @@ import { Util } from "../Util"; import { ProjectConfig } from "./../ProjectConfig"; const command = { + // tslint:disable:object-literal-sort-keys command: "config", desc: "Get or set configuration properties", builder: yargs => { @@ -52,6 +53,7 @@ const command = { // at least one command is required .demand(1, "Please use either get or set command"); }, + // tslint:enable:object-literal-sort-keys getHandler(argv) { if (!argv.global && !ProjectConfig.hasLocalConfig()) { Util.error("No configuration file found in this folder!", "red"); diff --git a/lib/types/Config.d.ts b/lib/types/Config.d.ts index 5960d6dc0..e680c9eb1 100644 --- a/lib/types/Config.d.ts +++ b/lib/types/Config.d.ts @@ -35,7 +35,7 @@ declare interface Config { //"serverType": "webpack" } - /** An array of paths to red custom templates from */ + /** An array of paths to read custom templates from */ customTemplates: string[]; /** Infragistics package registry Url */ igPackageRegistry: string; From e96076bdc93528af1702dd0eb9fb245b195b2a19 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Mon, 5 Feb 2018 18:51:23 +0200 Subject: [PATCH 7/8] refactor: don't merge into defaults due to require module caching --- lib/ProjectConfig.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/ProjectConfig.ts b/lib/ProjectConfig.ts index 111cd1d3e..aa56eead2 100644 --- a/lib/ProjectConfig.ts +++ b/lib/ProjectConfig.ts @@ -6,6 +6,7 @@ import { Util } from "./Util"; export class ProjectConfig { public static configFile: string = "ignite-ui-cli.json"; + public static readonly defaults: Config = require("./config/defaults.json"); /** Returns true if there's a CLI config file in the current working directory */ public static hasLocalConfig(): boolean { @@ -19,13 +20,15 @@ export class ProjectConfig { */ public static getConfig(global: boolean = false): Config { const filePath = path.join(process.cwd(), this.configFile); - const config = this.globalDefaults(); + const config = {}; + + Util.merge(config, this.defaults); + Util.merge(config, this.globalConfig()); if (!global) { - const localConfig = this.localConfig(); - Util.merge(config, localConfig); + Util.merge(config, this.localConfig()); } - return config; + return config as Config; } /** @@ -66,13 +69,4 @@ export class ProjectConfig { } return globalConfig as Config; } - - /** Get effective global configuration defaults */ - private static globalDefaults(): Config { - const defaults: Config = require("./config/defaults.json"); - const globalConfig = this.globalConfig(); - - Util.merge(defaults, globalConfig); - return defaults; - } } From f1dcf122713101e3103e926153581f0d40d45be3 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Tue, 6 Feb 2018 09:08:38 +0200 Subject: [PATCH 8/8] test: add config acceptance, update help command --- lib/commands/config.ts | 2 +- spec/acceptance/config-spec.ts | 91 ++++++++++++++++++++++++++++++++++ spec/acceptance/help-spec.ts | 22 ++++++++ spec/helpers/utils.ts | 2 +- spec/unit/config-spec.ts | 4 +- 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 spec/acceptance/config-spec.ts diff --git a/lib/commands/config.ts b/lib/commands/config.ts index 69a1e07de..2aa63e785 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -85,7 +85,7 @@ const command = { config[argv.property] = argv.value; ProjectConfig.setConfig(config, argv.global); - Util.log(`Property "${argv.property}" set`); + Util.log(`Property "${argv.property}" set.`); }, addHandler(argv) { let config; diff --git a/spec/acceptance/config-spec.ts b/spec/acceptance/config-spec.ts new file mode 100644 index 000000000..047c66bf3 --- /dev/null +++ b/spec/acceptance/config-spec.ts @@ -0,0 +1,91 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import cli = require("../../lib/cli"); +import { deleteAll, resetSpy } from "../helpers/utils"; + +describe("Config command", () => { + let testFolder = path.parse(__filename).name; + + beforeEach(() => { + spyOn(console, "log"); + spyOn(console, "error"); + + // test folder, w/ existing check: + while (fs.existsSync(`./output/${testFolder}`)) { + testFolder += 1; + } + fs.mkdirSync(`./output/${testFolder}`); + process.chdir(`./output/${testFolder}`); + + // ~/.global/ homedir for global files: + fs.mkdirSync(`.global`); + spyOn(os, "homedir").and.returnValue(path.join(process.cwd(), ".global")); + }); + + afterEach(() => { + // clean test folder: + process.chdir("../../"); + deleteAll(`./output/${testFolder}`); + fs.rmdirSync(`./output/${testFolder}`); + }); + + it("Should not work without a project & global flag", async done => { + await cli.run(["config", "get", "igPackageRegistry"]); + expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(/No configuration file found in this folder!\s*/)); + + resetSpy(console.error); + await cli.run(["config", "set", "igPackageRegistry", "maybe"]); + expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(/No configuration file found in this folder!\s*/)); + + resetSpy(console.error); + await cli.run(["config", "add", "igPackageRegistry", "maybe"]); + expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(/No configuration file found in this folder!\s*/)); + expect(console.log).toHaveBeenCalledTimes(0); + done(); + }); + + it("Should correctly read and update global values", async done => { + await cli.run(["config", "get", "igPackageRegistry", "--global"]); + expect(console.log).toHaveBeenCalledWith( + jasmine.stringMatching("https://packages.infragistics.com/npm/js-licensed/") + ); + + resetSpy(console.log); + await cli.run(["config", "set", "igPackageRegistry", "https://example.com", "--global"]); + expect(console.log).toHaveBeenCalledWith(`Property "igPackageRegistry" set.`); + expect(fs.existsSync("./.global/ignite-ui-cli.json")).toBeTruthy("Global config file not created"); + const test: any = { igPackageRegistry: "https://example.com" }; + expect(JSON.parse(fs.readFileSync("./.global/ignite-ui-cli.json", "utf-8"))).toEqual(test); + await cli.run(["config", "get", "igPackageRegistry", "--global"]); + expect(console.log).toHaveBeenCalledWith(jasmine.stringMatching("https://example.com")); + + expect(console.error).toHaveBeenCalledTimes(0); + done(); + }); + + it("Should correctly read and update local values", async done => { + fs.writeFileSync("ignite-ui-cli.json", JSON.stringify({ igPackageRegistry: "https://example.com" })); + await cli.run(["config", "get", "igPackageRegistry", "--global"]); + expect(console.log).toHaveBeenCalledWith( + jasmine.stringMatching("https://packages.infragistics.com/npm/js-licensed/") + ); + await cli.run(["config", "get", "igPackageRegistry"]); + expect(console.log).toHaveBeenCalledWith(jasmine.stringMatching("https://example.com")); + await cli.run(["config", "get", "customTemplates"]); + expect(console.log).toHaveBeenCalledWith([]); + resetSpy(console.log); + + resetSpy(console.log); + await cli.run(["config", "add", "customTemplates", "path:C:\\Test"]); + expect(console.log).toHaveBeenCalledWith(`Property "customTemplates" updated.`); + expect(fs.existsSync("./.global/ignite-ui-cli.json")).toBeFalsy(); + const test: any = { igPackageRegistry: "https://example.com", customTemplates: ["path:C:\\Test"] }; + expect(JSON.parse(fs.readFileSync("ignite-ui-cli.json", "utf-8"))).toEqual(test); + await cli.run(["config", "get", "customTemplates"]); + expect(console.log).toHaveBeenCalledWith(["path:C:\\Test"]); + + expect(console.error).toHaveBeenCalledTimes(0); + done(); + }); +}); diff --git a/spec/acceptance/help-spec.ts b/spec/acceptance/help-spec.ts index 05ae0ad0d..cbde1a5b5 100644 --- a/spec/acceptance/help-spec.ts +++ b/spec/acceptance/help-spec.ts @@ -10,6 +10,7 @@ describe("Help command", () => { start start the project new [name] Creating a new project build build the project + config Get or set configuration properties test test the project add [template] [name] Add component by it ID and providing a name. Options: @@ -42,4 +43,25 @@ describe("Help command", () => { expect(actualNewText).toContain(replacedNewHelpText); done(); }); + + it("should show help config sub-commands", async done => { + const child = spawnSync("node", ["bin/execute.js", "config", "--help" ], { + encoding: "utf-8" + }); + const originalNewHelpText: string = `Commands: + get Get configuration properties + set Set configuration properties + add Add a value to an existing configuration array + + Options: + --version, -v Show current Ignite UI CLI version [boolean] + --help, -h Show help [boolean] + --global, -g Specify if the global configuration should be used [boolean]`; + + const replacedNewHelpText: string = originalNewHelpText.replace(/\s/g, ""); + const actualNewText: string = (child.stdout.toString("utf-8")).replace(/\s/g, ""); + + expect(actualNewText).toContain(replacedNewHelpText); + done(); + }); }); diff --git a/spec/helpers/utils.ts b/spec/helpers/utils.ts index 41e0c4338..9f28c71da 100644 --- a/spec/helpers/utils.ts +++ b/spec/helpers/utils.ts @@ -8,7 +8,7 @@ import * as glob from "glob"; export function deleteAll(folderPath: string) { const files: string[] = glob.sync(folderPath + "/**/*", { nodir: true, dot: true }); files.forEach(x => fs.unlinkSync(x)); - const folders: string[] = glob.sync(folderPath + "/**/*"); + const folders: string[] = glob.sync(folderPath + "/**/*", { dot: true }); folders.reverse().forEach(x => fs.rmdirSync(x)); } diff --git a/spec/unit/config-spec.ts b/spec/unit/config-spec.ts index 34c283393..1bd5b20bb 100644 --- a/spec/unit/config-spec.ts +++ b/spec/unit/config-spec.ts @@ -74,7 +74,7 @@ describe("Unit - Config command", () => { expect(ProjectConfig.localConfig).toHaveBeenCalledTimes(0); expect(ProjectConfig.globalConfig).toHaveBeenCalled(); expect(ProjectConfig.setConfig).toHaveBeenCalledWith({ test: true }, true /*global*/); - expect(Util.log).toHaveBeenCalledWith(`Property "test" set`); + expect(Util.log).toHaveBeenCalledWith(`Property "test" set.`); done(); }); @@ -89,7 +89,7 @@ describe("Unit - Config command", () => { expect(ProjectConfig.globalConfig).toHaveBeenCalledTimes(0); expect(ProjectConfig.localConfig).toHaveBeenCalled(); expect(ProjectConfig.setConfig).toHaveBeenCalledWith({ notTest: "ig", test: true }, undefined); - expect(Util.log).toHaveBeenCalledWith(`Property "test" set`); + expect(Util.log).toHaveBeenCalledWith(`Property "test" set.`); done(); }); });