Skip to content

Commit

Permalink
refactor(config): refactor the config object. (#1809)
Browse files Browse the repository at this point in the history
This also will load angular-cli.json in the HOME directory as a fallback, supports more stuff from the JSON Schema (like default values) than the old one, and actually verify that what you inputs is the right thing.

This will be its own NPM package at some point, as other people will probably be interested in having a JSON Schema loader that gives type safety and provides fallbacks and metadata.

Closes #1763
  • Loading branch information
hansl authored Aug 24, 2016
1 parent 0d914b2 commit 1ecd72d
Show file tree
Hide file tree
Showing 24 changed files with 1,092 additions and 234 deletions.
4 changes: 3 additions & 1 deletion addon/ng2/commands/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const GetCommand = Command.extend({

run: function (commandOptions, rawArgs): Promise<void> {
return new Promise(resolve => {
const value = new CliConfig().get(rawArgs[0]);
const config = CliConfig.fromProject();
const value = config.get(rawArgs[0]);

if (value === null) {
console.error(chalk.red('Value cannot be found.'));
} else if (typeof value == 'object') {
Expand Down
10 changes: 5 additions & 5 deletions addon/ng2/commands/github-pages-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ module.exports = Command.extend({
type: String,
default: 'new gh-pages version',
description: 'The commit message to include with the build, must be wrapped in quotes.'
}, {
}, {
name: 'target',
type: String,
default: 'production',
default: 'production',
aliases: ['t', { 'dev': 'development' }, { 'prod': 'production' }]
}, {
}, {
name: 'environment',
type: String,
default: '',
Expand Down Expand Up @@ -72,12 +72,12 @@ module.exports = Command.extend({
}
if (options.target === 'production') {
options.environment = 'prod';
}
}
}

var projectName = this.project.pkg.name;

const outDir = CliConfig.fromProject().apps[0].outDir;
const outDir = CliConfig.fromProject().config.apps[0].outDir;

let ghPagesBranch = 'gh-pages';
let destinationBranch = options.userPage ? 'master' : ghPagesBranch;
Expand Down
33 changes: 31 additions & 2 deletions addon/ng2/commands/set.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as SilentError from 'silent-error';
import * as Command from 'ember-cli/lib/models/command';
import {CliConfig} from '../models/config';

Expand All @@ -11,10 +12,38 @@ const SetCommand = Command.extend({
{ name: 'global', type: Boolean, default: false, aliases: ['g'] },
],

asBoolean: function (raw: string): boolean {
if (raw == 'true' || raw == '1') {
return true;
} else if (raw == 'false' || raw == '' || raw == '0') {
return false;
} else {
throw new SilentError(`Invalid boolean value: "${raw}"`);
}
},
asNumber: function (raw: string): number {
if (Number.isNaN(+raw)) {
throw new SilentError(`Invalid number value: "${raw}"`);
}
return +raw;
},

run: function (commandOptions, rawArgs): Promise<void> {
return new Promise(resolve => {
const config = new CliConfig();
config.set(rawArgs[0], rawArgs[1], commandOptions.force);
const [jsonPath, rawValue] = rawArgs;
const config = CliConfig.fromProject();
const type = config.typeOf(jsonPath);
let value: any = rawValue;

switch (type) {
case 'boolean': value = this.asBoolean(rawValue); break;
case 'number': value = this.asNumber(rawValue); break;
case 'string': value = rawValue; break;

default: value = JSON.parse(rawValue);
}

config.set(jsonPath, value);
config.save();
resolve();
});
Expand Down
4 changes: 2 additions & 2 deletions addon/ng2/commands/test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as TestCommand from 'ember-cli/lib/commands/test';
import * as config from '../models/config';
import * as TestTask from '../tasks/test';
import {CliConfig} from '../models/config';

module.exports = TestCommand.extend({
availableOptions: [
Expand All @@ -14,7 +14,7 @@ module.exports = TestCommand.extend({
],

run: function (commandOptions) {
this.project.ngConfig = this.project.ngConfig || config.CliConfig.fromProject();
this.project.ngConfig = this.project.ngConfig || CliConfig.fromProject();

var testTask = new TestTask({
ui: this.ui,
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion addon/ng2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = {
name: 'ng2',

config: function () {
this.project.ngConfig = this.project.ngConfig || config.CliConfig.fromProject();
this.project.ngConfig = this.project.ngConfig || config.CliConfig.fromProject().config;
},

includedCommands: function () {
Expand Down
169 changes: 21 additions & 148 deletions addon/ng2/models/config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {CliConfig as CliConfigBase} from './config/config';
import {CliConfig as ConfigInterface} from '../../../lib/config/schema';
import * as chalk from 'chalk';
import * as fs from 'fs';
import * as path from 'path';
import * as chalk from 'chalk';

const schemaPath = path.resolve(process.env.CLI_ROOT, 'lib/config/schema.json');
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;
Expand All @@ -16,175 +18,46 @@ function _findUp(name: string, from: string) {
return p;
}

currentDir = path.resolve(currentDir, '..');
currentDir = path.dirname(currentDir);
}

return null;
}


export class CliConfig {
private _config: any;

constructor(path?: string) {
if (path) {
try {
fs.accessSync(path);
this._config = require(path);
} catch (e) {
throw new Error(`Config file does not exits.`);
}
} else {
this._config = CliConfig.fromProject();
}
}

save(path: string = CliConfig._configFilePath()) {
if (!path) {
throw new Error('Could not find config path.');
}

fs.writeFileSync(path, JSON.stringify(this._config, null, 2), { encoding: 'utf-8' });
}

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('.');
}

let { parent, name, remaining } = this._findParent(jsonPath);
let properties: any;
let additionalProperties: boolean;

const checkPath = jsonPath.split('.').reduce((o, i) => {
if (!o || !o.properties) {
throw new Error(`Invalid config path.`);
}
properties = o.properties;
additionalProperties = o.additionalProperties;

return o.properties[i];
}, schema);
const configPath = jsonPath.split('.').reduce((o, i) => o[i], this._config);

if (!properties[name] && !additionalProperties) {
throw new Error(`${name} is not a known property.`);
}

if (method) {
if (Array.isArray(configPath) && checkPath.type === 'array') {
[][method].call(configPath, value);
return true;
} else {
throw new Error(`Trying to use array method on non-array property type.`);
}
}

if (typeof checkPath.type === 'string' && isNaN(value)) {
parent[name] = value;
return true;
}

if (typeof checkPath.type === 'number' && !isNaN(value)) {
parent[name] = value;
return true;
}

if (typeof value != checkPath.type) {
throw new Error(`Invalid value type. Trying to set ${typeof value} to ${path.type}`);
}
}

get(jsonPath: string): any {
let { parent, name, remaining } = this._findParent(jsonPath);
if (remaining || !(name in parent)) {
return null;
} else {
return parent[name];
}
}

private _validatePath(jsonPath: string) {
if (!jsonPath.match(/^(?:[-_\w\d]+(?:\[\d+\])*\.)*(?:[-_\w\d]+(?:\[\d+\])*)$/)) {
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('.')
};
}
}
}
function getUserHome() {
return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
}

return { parent, name };
}

export class CliConfig extends CliConfigBase<ConfigInterface> {
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());
|| _findUp(CLI_CONFIG_FILE_NAME, process.cwd())
|| _findUp(CLI_CONFIG_FILE_NAME, __dirname);
}

public static fromProject(): any {
const configPath = CliConfig._configFilePath();

static fromProject(): CliConfig {
const configPath = this._configFilePath();
const globalConfigPath = path.join(getUserHome(), CLI_CONFIG_FILE_NAME);

if (!configPath) {
return {};
}

let config = require(configPath);

if (config.defaults.sourceDir || config.defaults.prefix) {
config.apps[0].root = config.apps[0].root || config.defaults.sourceDir;
config.apps[0].prefix = config.apps[0].prefix || config.defaults.prefix;

const cliConfig = CliConfigBase.fromConfigPath(CliConfig._configFilePath(), [globalConfigPath]);
if (cliConfig.alias('apps.0.root', 'defaults.sourceDir')
+ cliConfig.alias('apps.0.prefix', 'defaults.prefix')) {
console.error(chalk.yellow(
'The "defaults.prefix" and "defaults.sourceDir" properties of angular-cli.json '
'The "defaults.prefix" and "defaults.sourceDir" properties of angular-cli.json\n'
+ 'are deprecated in favor of "apps[0].root" and "apps[0].prefix".\n'
+ 'Please update in order to avoid errors in future versions of angular-cli.'
));
}
return config;

return cliConfig as CliConfig;
}
}
Loading

0 comments on commit 1ecd72d

Please sign in to comment.