|
1 | 1 | import * as fs from 'fs';
|
2 | 2 | import * as path from 'path';
|
3 |
| - |
| 3 | +import * as Proxy from 'harmony-proxy'; |
| 4 | +import * as Reflect from 'harmony-reflect'; |
| 5 | +import * as ObjectAssign from '../utilities/object-assign'; |
4 | 6 |
|
5 | 7 | export const CLI_CONFIG_FILE_NAME = 'angular-cli.json';
|
6 | 8 |
|
| 9 | +export const ArrayMethods: Array<string> = ['pop', 'push', 'reverse', 'shift', 'sort', 'unshift']; |
7 | 10 |
|
8 |
| -export interface CliConfigJson { |
9 |
| - routes?: { [name: string]: any }, |
10 |
| - packages?: { [name: string]: any } |
11 |
| -} |
| 11 | +export const handler = { |
| 12 | + get: function(target: any, key: any, receiver: any) { |
| 13 | + if (key === 'toJSON') { |
| 14 | + return () => target; |
| 15 | + } |
12 | 16 |
|
| 17 | + if (key === 'length') { |
| 18 | + return; |
| 19 | + } |
13 | 20 |
|
14 |
| -function _findUp(name: string, from: string) { |
15 |
| - let currentDir = from; |
16 |
| - while (currentDir && currentDir != '/') { |
17 |
| - const p = path.join(currentDir, name); |
18 |
| - if (fs.existsSync(p)) { |
19 |
| - return p; |
| 21 | + if (key === 'inspect') { |
| 22 | + return target; |
20 | 23 | }
|
21 | 24 |
|
22 |
| - currentDir = path.resolve(currentDir, '..'); |
23 |
| - } |
| 25 | + if (target.type === 'array') { |
| 26 | + const arr: Array<any> = []; |
| 27 | + arr[key].call(target.enum); |
| 28 | + } |
24 | 29 |
|
25 |
| - return null; |
26 |
| -} |
| 30 | + if (ArrayMethods.indexOf(key) === -1) { |
| 31 | + if (!(key in target)) { |
| 32 | + target[key] = new Proxy({ type: 'object' }, handler); |
| 33 | + } |
| 34 | + return Reflect.get(target, key, receiver); |
| 35 | + } else { |
| 36 | + return Reflect.get([], key, receiver); |
| 37 | + } |
| 38 | + }, |
| 39 | + set: function(target: any, key: any, value: any) { |
| 40 | + if (key === 'length') { |
| 41 | + return; |
| 42 | + } |
27 | 43 |
|
| 44 | + if (!isNaN(key) && parseInt(key, 10) === 0) { |
| 45 | + if (!target.enum) { |
| 46 | + target.type = 'array'; |
| 47 | + target.enum = []; |
| 48 | + } |
28 | 49 |
|
29 |
| -export class CliConfig { |
30 |
| - constructor(private _config: CliConfigJson = CliConfig.fromProject()) {} |
| 50 | + if (value) { |
| 51 | + target.enum.push(value); |
| 52 | + } |
| 53 | + } else { |
| 54 | + if (target[key] && target[key].type) { |
| 55 | + let type: string = target[key].type; |
| 56 | + let assigningType: string; |
| 57 | + |
| 58 | + if (!value && value === null) { |
| 59 | + assigningType = 'null'; |
| 60 | + } else if (value && Array.isArray(value) && typeof value !== 'string') { |
| 61 | + assigningType = 'array'; |
| 62 | + } else { |
| 63 | + assigningType = typeof value; |
| 64 | + } |
31 | 65 |
|
32 |
| - save(path: string = CliConfig.configFilePath()) { |
33 |
| - if (!path) { |
34 |
| - throw new Error('Could not find config path.'); |
35 |
| - } |
| 66 | + if (type !== assigningType) { |
| 67 | + throw new Error(`Cannot assign value of type '${assigningType}' to an property with type '${type}'.`); |
| 68 | + } |
| 69 | + } |
36 | 70 |
|
37 |
| - fs.writeFileSync(path, JSON.stringify(this._config, null, 2), { encoding: 'utf-8' }); |
| 71 | + if (!value && value === null) { |
| 72 | + target[key] = { type: 'null', value: value }; |
| 73 | + } else if (value && Array.isArray(value) && typeof value !== 'string') { |
| 74 | + target[key] = { type: 'array', enum: value }; |
| 75 | + } else { |
| 76 | + if (typeof value === 'object' && Object.getOwnPropertyNames(value).length === 0) { |
| 77 | + target[key] = { type: typeof value }; |
| 78 | + } else { |
| 79 | + target[key] = { type: typeof value, value: value }; |
| 80 | + } |
| 81 | + } |
| 82 | + } |
38 | 83 | }
|
| 84 | +}; |
| 85 | + |
| 86 | +export interface ConfigJson { |
| 87 | + routes?: { [name: string]: any }, |
| 88 | + packages?: { [name: string]: any } |
| 89 | +} |
39 | 90 |
|
40 |
| - set(jsonPath: string, value: any, force: boolean = false): boolean { |
41 |
| - let { parent, name, remaining } = this._findParent(jsonPath); |
42 |
| - while (force && remaining) { |
43 |
| - if (remaining.indexOf('.') != -1) { |
44 |
| - // Create an empty map. |
45 |
| - // TODO: create the object / array based on the Schema of the configuration. |
46 |
| - parent[name] = {}; |
| 91 | +export class Config { |
| 92 | + path: ConfigJson; |
| 93 | + config: Proxy; |
| 94 | + |
| 95 | + constructor(path?: string) { |
| 96 | + if (path) { |
| 97 | + try { |
| 98 | + fs.accessSync(path); |
| 99 | + this.path = path; |
| 100 | + this.config = new Proxy({}, handler); |
| 101 | + } catch (e) { |
| 102 | + throw new Error(`${path} not found.`); |
47 | 103 | }
|
| 104 | + } else { |
| 105 | + this.path = this._configFilePath(); |
| 106 | + this.config = new Proxy(this._fromProject(), handler); |
| 107 | + } |
| 108 | + } |
48 | 109 |
|
| 110 | + public save(): void { |
| 111 | + try { |
| 112 | + let config = ObjectAssign({}, JSON.parse(fs.readFileSync(this.path, 'utf8')), this.config.toJSON()); |
| 113 | + fs.writeFileSync(this.path, JSON.stringify(config, null, 2), 'utf8'); |
| 114 | + } catch (e) { |
| 115 | + throw new Error(`Error while saving config.`); |
49 | 116 | }
|
| 117 | + } |
50 | 118 |
|
51 |
| - parent[name] = value; |
52 |
| - return true; |
| 119 | + public set(path: string, value: any): void { |
| 120 | + const levels = path.split('.'); |
| 121 | + let current = this.config; |
| 122 | + let i = 0; |
| 123 | + while (i < levels.length - 1) { |
| 124 | + delete current[levels[i]]; |
| 125 | + current = current[levels[i]]; |
| 126 | + i += 1; |
| 127 | + } |
| 128 | + |
| 129 | + current[levels[levels.length - 1]] = value; |
53 | 130 | }
|
54 | 131 |
|
55 |
| - get(jsonPath: string): any { |
56 |
| - let { parent, name, remaining } = this._findParent(jsonPath); |
57 |
| - if (remaining || !(name in parent)) { |
58 |
| - return null; |
| 132 | + public get(obj: any, path: string): any { |
| 133 | + const levels = path.split('.'); |
| 134 | + let current = obj; |
| 135 | + let i = 0; |
| 136 | + while (i < levels.length) { |
| 137 | + if (current[levels[i]]) { |
| 138 | + current = current[levels[i]]; |
| 139 | + i += 1; |
| 140 | + } else { |
| 141 | + return null; |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + if (current.type === 'array') { |
| 146 | + return current.enum; |
| 147 | + } else if (current.type === 'object') { |
| 148 | + return current; |
59 | 149 | } else {
|
60 |
| - return parent[name]; |
| 150 | + return current.value; |
61 | 151 | }
|
62 | 152 | }
|
63 | 153 |
|
64 |
| - private _validatePath(jsonPath: string) { |
| 154 | + public validatePath(jsonPath: string) { |
65 | 155 | if (!jsonPath.match(/^(?:[-_\w\d]+(?:\[\d+\])*\.)*(?:[-_\w\d]+(?:\[\d+\])*)$/)) {
|
66 | 156 | throw `Invalid JSON path: "${jsonPath}"`;
|
67 | 157 | }
|
68 | 158 | }
|
69 | 159 |
|
70 |
| - private _findParent(jsonPath: string): { parent: any, name: string | number, remaining?: string } { |
71 |
| - this._validatePath(jsonPath); |
72 |
| - |
73 |
| - let parent: any = null; |
74 |
| - let current: any = this._config; |
75 |
| - |
76 |
| - const splitPath = jsonPath.split('.'); |
77 |
| - let name: string | number = ''; |
78 |
| - |
79 |
| - while (splitPath.length > 0) { |
80 |
| - const m = splitPath.shift().match(/^(.*?)(?:\[(\d+)\])*$/); |
81 |
| - |
82 |
| - name = m[1]; |
83 |
| - const index: string = m[2]; |
84 |
| - parent = current; |
85 |
| - current = current[name]; |
86 |
| - |
87 |
| - if (current === null || current === undefined) { |
88 |
| - return { |
89 |
| - parent, |
90 |
| - name, |
91 |
| - remaining: (!isNaN(index) ? `[${index}]` : '') + splitPath.join('.') |
92 |
| - }; |
| 160 | + private _findUp(name: string, from: string): string { |
| 161 | + let currentDir = from; |
| 162 | + while (currentDir && currentDir != '/') { |
| 163 | + const p = path.join(currentDir, name); |
| 164 | + if (fs.existsSync(p)) { |
| 165 | + return p; |
93 | 166 | }
|
94 | 167 |
|
95 |
| - if (!isNaN(index)) { |
96 |
| - name = index; |
97 |
| - parent = current; |
98 |
| - current = current[index]; |
99 |
| - |
100 |
| - if (current === null || current === undefined) { |
101 |
| - return { |
102 |
| - parent, |
103 |
| - name, |
104 |
| - remaining: splitPath.join('.') |
105 |
| - }; |
106 |
| - } |
107 |
| - } |
| 168 | + currentDir = path.resolve(currentDir, '..'); |
108 | 169 | }
|
109 | 170 |
|
110 |
| - return { parent, name }; |
| 171 | + return null; |
111 | 172 | }
|
112 | 173 |
|
113 |
| - static configFilePath(projectPath?: string): string { |
| 174 | + private _configFilePath(projectPath?: string): string { |
114 | 175 | // Find the configuration, either where specified, in the angular-cli project
|
115 | 176 | // (if it's in node_modules) or from the current process.
|
116 |
| - return (projectPath && _findUp(CLI_CONFIG_FILE_NAME, projectPath)) |
117 |
| - || _findUp(CLI_CONFIG_FILE_NAME, __dirname) |
118 |
| - || _findUp(CLI_CONFIG_FILE_NAME, process.cwd()); |
| 177 | + return (projectPath && this._findUp(CLI_CONFIG_FILE_NAME, projectPath)) |
| 178 | + || this._findUp(CLI_CONFIG_FILE_NAME, __dirname) |
| 179 | + || this._findUp(CLI_CONFIG_FILE_NAME, process.cwd()); |
119 | 180 | }
|
120 | 181 |
|
121 |
| - static fromProject(): CliConfigJson { |
122 |
| - const configPath = this.configFilePath(); |
| 182 | + private _fromProject(): ConfigJson { |
| 183 | + const configPath = this._configFilePath(); |
123 | 184 | return configPath ? require(configPath) : {};
|
124 | 185 | }
|
125 | 186 | }
|
0 commit comments