From 373f8f418471e83200e48c00932fb8e6d8138b18 Mon Sep 17 00:00:00 2001 From: Jan Kuri Date: Wed, 3 Aug 2016 07:00:56 +0200 Subject: [PATCH] refactor(config): NgConfig --- addon/ng2/commands/get.ts | 46 +++++- addon/ng2/commands/set.ts | 23 ++- addon/ng2/index.js | 6 +- addon/ng2/models/config.ts | 219 +++++++++++++---------------- addon/ng2/models/webpack-config.ts | 9 +- angular-cli.json | 32 +++++ lib/config/schema.json | 14 +- package.json | 1 + tests/models/config.spec.ts | 16 +-- 9 files changed, 217 insertions(+), 149 deletions(-) create mode 100644 angular-cli.json diff --git a/addon/ng2/commands/get.ts b/addon/ng2/commands/get.ts index 50974e154a3d..654073c74ae4 100644 --- a/addon/ng2/commands/get.ts +++ b/addon/ng2/commands/get.ts @@ -1,5 +1,8 @@ import * as chalk from 'chalk'; import * as Command from 'ember-cli/lib/models/command'; +import * as jp from 'jsonpath'; +import * as chalk from 'chalk'; +import * as path from 'path'; import {CliConfig} from '../models/config'; @@ -8,21 +11,50 @@ const GetCommand = Command.extend({ description: 'Get a value from the configuration.', works: 'everywhere', - availableOptions: [], + availableOptions: [ + { name: 'global', type: Boolean, default: false, aliases: ['g'] } + ], run: function (commandOptions, rawArgs): Promise { return new Promise(resolve => { - const value = new CliConfig().get(rawArgs[0]); - if (value === null) { - console.error(chalk.red('Value cannot be found.')); - } else if (typeof value == 'object') { - console.log(JSON.stringify(value)); + const cliConfig = new CliConfig(); + const config = commandOptions.global ? cliConfig.global : cliConfig.project; + const value = cliConfig.get(rawArgs[0]); + + if (!value) { + const lastProp = _lastProp(rawArgs[0]); + let results = jp.query(config, `$..${lastProp}`); + let paths = jp.paths(config, `$..${lastProp}`); + if (results.length) { + let result; + let foundPath; + this.ui.writeLine('We could not find value on the path you were requested.'); + if (results.length > 1) { + this.ui.writeLine('But, we found values on other paths:'); + results.forEach((r, i) => { + result = chalk.green(JSON.stringify(results[i])); + foundPath = chalk.green(paths[i].filter((p, idx) => idx !== 0).join('.')); + this.ui.writeLine(`${result} on path ${foundPath}?`); + }); + } else { + result = chalk.green(JSON.stringify(results[0])); + foundPath = paths[0].filter((p, i) => i !== 0).join('.'); + this.ui.writeLine(`Looking for ${result} on path ${chalk.green(foundPath)}?`); + } + } else { + this.ui.writeLine(chalk.red('Value not found.')); + } } else { - console.log(value); + this.ui.writeLine(JSON.stringify(value, null, ' ')); } + resolve(); }); } }); +private static function _lastProp (jsonPath: string): string { + return (jsonPath.match(/\./)) ? path.extname(jsonPath) : jsonPath; +} + module.exports = GetCommand; diff --git a/addon/ng2/commands/set.ts b/addon/ng2/commands/set.ts index ae7cd54a9e75..cf14deaef249 100644 --- a/addon/ng2/commands/set.ts +++ b/addon/ng2/commands/set.ts @@ -1,6 +1,6 @@ import * as Command from 'ember-cli/lib/models/command'; import {CliConfig} from '../models/config'; - +import * as chalk from 'chalk'; const SetCommand = Command.extend({ name: 'set', @@ -8,13 +8,28 @@ const SetCommand = Command.extend({ works: 'everywhere', availableOptions: [ - { name: 'global', type: Boolean, default: false, aliases: ['g'] }, + { name: 'global', type: Boolean, default: false, aliases: ['g'] } ], run: function (commandOptions, rawArgs): Promise { - return new Promise(resolve => { + return new Promise((resolve, reject) => { + if (rawArgs.length < 2) { + this.ui.writeLine(` + ${chalk.red.bold('Error: not enough parameters provided.')} + 'Examples:' + ${chalk.yellow('ng set project.name "My awesome project"')} + ${chalk.yellow('ng set defaults.styleExt sass')} + ${chalk.yellow('ng set apps[0].mobile true')} + ${chalk.yellow('ng set "apps[0].styles[\'src/styles.css\'].autoImported" = false')} + ${chalk.yellow('ng set "apps[0].styles[\'src/app.sass\']" = "{ output: \'app.css\', autoImported: true }"')} + `); + reject(); + } + const config = new CliConfig(); - config.set(rawArgs[0], rawArgs[1], commandOptions.force); + const value = rawArgs[1] === '=' ? rawArgs[2] : rawArgs[1]; + + config.set(rawArgs[0], value, commandOptions.force); config.save(); resolve(); }); diff --git a/addon/ng2/index.js b/addon/ng2/index.js index 1d6248f0c179..e31e83d3312c 100644 --- a/addon/ng2/index.js +++ b/addon/ng2/index.js @@ -1,15 +1,11 @@ /* jshint node: true */ 'use strict'; -const config = require('./models/config'); +const NgConfig = require('./models/config'); module.exports = { name: 'ng2', - config: function () { - this.project.ngConfig = this.project.ngConfig || config.CliConfig.fromProject(); - }, - includedCommands: function () { return { 'build': require('./commands/build'), diff --git a/addon/ng2/models/config.ts b/addon/ng2/models/config.ts index 8b41a2e2dc49..2e862fa8a8f5 100644 --- a/addon/ng2/models/config.ts +++ b/addon/ng2/models/config.ts @@ -1,5 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as jp from 'jsonpath'; +import * as chalk from 'chalk'; const schemaPath = path.resolve(process.env.CLI_ROOT, 'lib/config/schema.json'); const schema = require(schemaPath); @@ -7,165 +9,138 @@ const schema = require(schemaPath); export const CLI_CONFIG_FILE_NAME = 'angular-cli.json'; export const ARRAY_METHODS = ['push', 'splice', 'sort', 'reverse', 'pop', 'shift']; -function _findUp(name: string, from: string) { - let currentDir = from; - while (currentDir && currentDir !== path.parse(currentDir).root) { - const p = path.join(currentDir, name); - if (fs.existsSync(p)) { - return p; - } +export class CliConfig { + public project: any; + public global: any; - currentDir = path.resolve(currentDir, '..'); + constructor() { + this.global = this._fromCli(); + this.project = this._fromProject(); } - return null; -} + save(global = false) { + let config = (global || !this.project) ? this.global : this.project; + fs.writeFileSync(path, JSON.stringify(config, null, 2), { encoding: 'utf-8' }); + } + checkValidSchemaPath(jsonPath: Object): boolean { + const parsed = jp.parse(jsonPath); + const invalidMsg = `${jsonPath} does not match schema.`; + let propertiesPath; -export class CliConfig { - private _config: any; + parsed.forEach((p, i) => { + let type = p.expression.type; + let value = p.expression.value; - constructor(path?: string) { - if (path) { - try { - fs.accessSync(path); - this._config = require(path); - } catch (e) { - throw new Error(`Config file does not exits.`); + if (i === parsed.length - 1) { + return; } - } else { - this._config = CliConfig.fromProject(); - } - } - save(path: string = CliConfig._configFilePath()) { - if (!path) { - throw new Error('Could not find config path.'); - } + if (!i) { + propertiesPath = `properties.${value}`; + } else { + if (type === 'numeric_literal') { + let prop = propertiesPath.split('.').reduce((prev, curr) => prev[curr], schema); + if (prop.type !== 'array') { + throw new Error(invalidMsg); + } else { + propertiesPath += `.items`; + } + } else { + propertiesPath += `.properties.${value}`; + } + } + }); - fs.writeFileSync(path, JSON.stringify(this._config, null, 2), { encoding: 'utf-8' }); + if (!propertiesPath.split('.').reduce((prev, curr) => prev[curr], schema)) { + throw new Error(invalidMsg); + } else { + return true; + } } - set(jsonPath: string, value: any, force: boolean = false): boolean { - let method: any = null; - let splittedPath = jsonPath.split('.'); - if (ARRAY_METHODS.indexOf(splittedPath[splittedPath.length - 1]) != -1) { - method = splittedPath[splittedPath.length - 1]; - splittedPath.splice(splittedPath.length - 1, 1); - jsonPath = splittedPath.join('.'); - } + set(jsonPath: string, value: any, global = false): boolean { + let config = (global || !this.project) ? this.global : this.project; - let { parent, name, remaining } = this._findParent(jsonPath); - let properties: any; - let additionalProperties: boolean; + this._validatePath(jsonPath); + this.checkValidSchemaPath(jsonPath); - const checkPath = jsonPath.split('.').reduce((o, i) => { - if (!o || !o.properties) { - throw new Error(`Invalid config path.`); + if (value.slice(0, 1) === '{' && value.slice(-1) === '}') { + try { + value = JSON.parse(value.replace(/\'/g, '\"')); + } catch (e) { + throw new Error(`Invalid JSON value ${value}`); } - properties = o.properties; - additionalProperties = o.additionalProperties; + } - return o.properties[i]; - }, schema); - const configPath = jsonPath.split('.').reduce((o, i) => o[i], this._config); + let prop = jsonPath.split('.').reduce((prev, curr) => prev[curr], config); - if (!properties[name] && !additionalProperties) { - throw new Error(`${name} is not a known property.`); - } + if (ARRAY_METHODS.indexOf(path.extname(jsonPath).replace('.', '')) !== -1) { + let method = path.extname(jsonPath); + let parentPath = jsonPath.replace(path.extname(jsonPath), ''); - if (method) { - if (Array.isArray(configPath) && checkPath.type === 'array') { - [][method].call(configPath, value); - return true; + if (typeof jp.query(config, `$.${parentPath}`)[0] === 'string') { + throw new Error(`Cannot use array method on non-array type.`); } else { - throw new Error(`Trying to use array method on non-array property type.`); + [][method].call(parent, value); } } - if (typeof checkPath.type === 'string' && isNaN(value)) { - parent[name] = value; - return true; + if (!prop) { + throw new Error(`Property does not exists.`); } - if (typeof checkPath.type === 'number' && !isNaN(value)) { - parent[name] = value; - return true; - } + jp.value(config, `$.${jsonPath}`, value); - if (typeof value != checkPath.type) { - throw new Error(`Invalid value type. Trying to set ${typeof value} to ${path.type}`); - } + return true; } - get(jsonPath: string): any { - let { parent, name, remaining } = this._findParent(jsonPath); - if (remaining || !(name in parent)) { - return null; - } else { - return parent[name]; - } + get(jsonPath: string, global = false): any { + let config = (global || !this.project) ? this.global : this.project; + let results = jp.query(config, `$.${jsonPath}`); + + return (results.length) ? results[0] : null; } private _validatePath(jsonPath: string) { - if (!jsonPath.match(/^(?:[-_\w\d]+(?:\[\d+\])*\.)*(?:[-_\w\d]+(?:\[\d+\])*)$/)) { + try { + jp.parse(jsonPath); + } catch (e) { throw `Invalid JSON path: "${jsonPath}"`; } } - private _findParent(jsonPath: string): { parent: any, name: string | number, remaining?: string } { - this._validatePath(jsonPath); - - let parent: any = null; - let current: any = this._config; - - const splitPath = jsonPath.split('.'); - let name: string | number = ''; - - while (splitPath.length > 0) { - const m = splitPath.shift().match(/^(.*?)(?:\[(\d+)\])*$/); - - name = m[1]; - const index: string = m[2]; - parent = current; - current = current[name]; - - if (current === null || current === undefined) { - return { - parent, - name, - remaining: (!isNaN(index) ? `[${index}]` : '') + splitPath.join('.') - }; - } - - if (!isNaN(index)) { - name = index; - parent = current; - current = current[index]; - - if (current === null || current === undefined) { - return { - parent, - name, - remaining: splitPath.join('.') - }; - } - } + private _fromProject(): any { + const configPath = this._findUp(CLI_CONFIG_FILE_NAME, process.cwd()); + try { + fs.accessSync(configPath); + return JSON.parse(fs.readFileSync(configPath, { encoding: 'utf8' })); + } catch (e) { + return {}; } - - return { parent, name }; } - private static _configFilePath(projectPath?: string): string { - // Find the configuration, either where specified, in the angular-cli project - // (if it's in node_modules) or from the current process. - return (projectPath && _findUp(CLI_CONFIG_FILE_NAME, projectPath)) - || _findUp(CLI_CONFIG_FILE_NAME, __dirname) - || _findUp(CLI_CONFIG_FILE_NAME, process.cwd()); + private _fromCli(): any { + const configPath = this._findUp(CLI_CONFIG_FILE_NAME, process.env.CLI_ROOT); + try { + fs.accessSync(configPath); + return JSON.parse(fs.readFileSync(configPath, { encoding: 'utf8' })); + } catch (e) { + return {}; + } } - public static fromProject(): any { - const configPath = CliConfig._configFilePath(); - return configPath ? require(configPath) : {}; + private _findUp(name: string, from: string): any { + let currentDir = from; + while (currentDir && currentDir !== path.parse(currentDir).root) { + let p = path.join(currentDir, name); + try { + fs.accessSync(p); + return p; + } catch (e) { + currentDir = path.resolve(currentDir, '..'); + } + } + return null; } } diff --git a/addon/ng2/models/webpack-config.ts b/addon/ng2/models/webpack-config.ts index a215217c0afa..287de7ea5ca4 100644 --- a/addon/ng2/models/webpack-config.ts +++ b/addon/ng2/models/webpack-config.ts @@ -24,7 +24,12 @@ export class NgCliWebpackConfig { private webpackMobileProdConfigPartial: any; constructor(public ngCliProject: any, public target: string, public environment: string) { - const sourceDir = CliConfig.fromProject().defaults.sourceDir; + const cliConfig = new CliConfig(); + + console.log(cliConfig); + + const sourceDir = cliConfig.project.defaults.sourceDir; + const isMobile = cliConfig.project.apps[0].mobile; const environmentPath = `./${sourceDir}/app/environments/environment.${environment}.ts`; @@ -32,7 +37,7 @@ export class NgCliWebpackConfig { this.webpackDevConfigPartial = getWebpackDevConfigPartial(this.ngCliProject.root, sourceDir); this.webpackProdConfigPartial = getWebpackProdConfigPartial(this.ngCliProject.root, sourceDir); - if (CliConfig.fromProject().apps[0].mobile){ + if (isMobile){ this.webpackMobileConfigPartial = getWebpackMobileConfigPartial(this.ngCliProject.root, sourceDir); this.webpackMobileProdConfigPartial = getWebpackMobileProdConfigPartial(this.ngCliProject.root, sourceDir); this.webpackBaseConfig = webpackMerge(this.webpackBaseConfig, this.webpackMobileConfigPartial); diff --git a/angular-cli.json b/angular-cli.json new file mode 100644 index 000000000000..be1175f35d36 --- /dev/null +++ b/angular-cli.json @@ -0,0 +1,32 @@ +{ + "project": { + "name": "Angular CLI global configuration" + }, + "apps": [ + { + "main": "src/main.ts", + "tsconfig": "src/tsconfig.json", + "mobile": false, + "styles": {} + } + ], + "addons": [], + "packages": [], + "e2e": { + "protractor": { + "config": "config/protractor.conf.js" + } + }, + "test": { + "karma": { + "config": "config/karma.conf.js" + } + }, + "defaults": { + "prefix": "app", + "sourceDir": "src", + "styleExt": "css", + "prefixInterfaces": false, + "lazyRoutePrefix": "+" + } +} \ No newline at end of file diff --git a/lib/config/schema.json b/lib/config/schema.json index d8598052dd14..a3b11f8cd614 100644 --- a/lib/config/schema.json +++ b/lib/config/schema.json @@ -24,7 +24,19 @@ "properties": { "main": "string", "tsconfig": "string", - "mobile": "boolean" + "mobile": "boolean", + "styles": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "autoImported": { + "type": "boolean" + } + }, + "additionalProperties": false + } }, "additionalProperties": false }, diff --git a/package.json b/package.json index d6c4d54b3990..a4495a293975 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "html-webpack-plugin": "^2.19.0", "istanbul-instrumenter-loader": "^0.2.0", "json-loader": "^0.5.4", + "jsonpath": "^0.2.6", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.0", "leek": "0.0.21", diff --git a/tests/models/config.spec.ts b/tests/models/config.spec.ts index d9a78da3c921..a45ca3c30765 100644 --- a/tests/models/config.spec.ts +++ b/tests/models/config.spec.ts @@ -1,17 +1,17 @@ import {CliConfig} from '../../addon/ng2/models/config'; import * as fs from 'fs'; import * as path from 'path'; +import { expect } from 'chai'; -const expect = require('chai').expect; -const config = path.resolve(process.cwd(), 'addon/ng2/blueprints/ng2/files/angular-cli.json'); -const configCopy = path.resolve(process.cwd(), 'angular-cli.json'); +const config = path.resolve(process.cwd(), 'angular-cli.json'); +const configCopy = path.resolve(process.cwd(), 'angular-cli-tmp.json'); function getContents() { - return require(configCopy); + return JSON.parse(fs.readFileSync(configCopy, 'utf8')); } // TODO: revisit this test to make non-valid-JSON-friendly. -describe.skip('Config Tests', () => { +describe('Config Tests', () => { before(() => { process.chdir(process.cwd()); }); @@ -41,7 +41,7 @@ describe.skip('Config Tests', () => { it('Updates property of type `string` successfully', () => { let c = new CliConfig(configCopy); c.set('project.name', 'new-project-name'); - c.save(); + c.save(configCopy); let contents = getContents(); @@ -55,7 +55,7 @@ describe.skip('Config Tests', () => { let fn = () => { c.set('project.foo', 'bar'); - c.save(); + c.save(configCopy); } expect(fn).to.throw(Error); @@ -66,7 +66,7 @@ describe.skip('Config Tests', () => { let fn = () => { c.set('project.name.push', 'new-project-name'); - c.save(); + c.save(configCopy); } expect(fn).to.throw(Error);