diff --git a/src/configurationHelper.ts b/src/configurationHelper.ts index bef16225..c4546866 100644 --- a/src/configurationHelper.ts +++ b/src/configurationHelper.ts @@ -81,15 +81,60 @@ function writeDojoRcConfig(config: Config, indent: string | number) { writeFileSync(dojoRcPath, json); } +function mergeConfigs(config: Config) { + const configs = getExtendingConfigs(config); + let mergedConfig: Config = config; + if (configs.length) { + let baseConfig: Config; + while ((baseConfig = configs.shift())) { + mergedConfig = merge(mergedConfig, baseConfig); + } + } + return mergedConfig; +} + +function getExtendingConfigs(config: Config) { + const configs = []; + let extendingConfig = config; + while (typeof extendingConfig.extends === 'string') { + const extendedConfig = JSON.parse(readFileSync(extendingConfig.extends, 'utf8')); + configs.push(extendedConfig); + extendingConfig = extendedConfig; + } + return configs; +} + +function merge(extendingObj: { [key: string]: any }, baseObj: { [key: string]: any }) { + Object.keys(baseObj).forEach((prop) => { + if (extendingObj[prop] === undefined) { + extendingObj[prop] = baseObj[prop]; + } else if (isObject(baseObj[prop])) { + extendingObj[prop] = merge(extendingObj[prop], baseObj[prop]); + } + }); + return extendingObj; +} + +function isObject(item: any) { + return item && typeof item === 'object' && !Array.isArray(item); +} + +function checkIfConfigExtends(config?: Config) { + if (config && config.extends) { + config = mergeConfigs(config); + } + return config; +} + export function getConfig(): Config | undefined { const { packageJsonConfig, dojoRcConfig } = parseConfigs(); const hasPackageConfig = typeof packageJsonConfig === 'object'; const hasDojoRcConfig = typeof dojoRcConfig === 'object'; if (!hasDojoRcConfig && hasPackageConfig) { - return packageJsonConfig; + return checkIfConfigExtends(packageJsonConfig); } else { - return dojoRcConfig; + return checkIfConfigExtends(dojoRcConfig); } } diff --git a/tests/unit/configurationHelper.ts b/tests/unit/configurationHelper.ts index 179d346e..4a48d657 100644 --- a/tests/unit/configurationHelper.ts +++ b/tests/unit/configurationHelper.ts @@ -185,6 +185,88 @@ registerSuite('Configuration Helper', { assert.equal(mockFs.readFileSync.firstCall.args[0], dojoRcPath); assert.deepEqual(config, existingConfig); }, + 'Should shallow extend configs correctly'() { + mockFs.existsSync.onCall(0).returns(true); + mockFs.existsSync.onCall(1).returns(false); + mockFs.readFileSync.onCall(0).returns( + JSON.stringify({ + extends: './path/to/dojorc/to/.extend', + 'testGroupName-testCommandName': { + prop: 'config' + } + }) + ); + mockFs.readFileSync.onCall(1).returns( + JSON.stringify({ + 'testGroupName-testCommandName': { + prop: 'config-extended' + } + }) + ); + const config = configurationHelper.sandbox('testGroupName', 'testCommandName').get(); + assert.isTrue(mockFs.readFileSync.calledTwice); + assert.equal(mockFs.readFileSync.firstCall.args[0], dojoRcPath); + assert.deepEqual(config, { + prop: 'config' + }); + }, + 'Supports deep extension of configs correctly'() { + mockFs.existsSync.onCall(0).returns(true); + mockFs.existsSync.onCall(1).returns(false); + mockFs.readFileSync.onCall(0).returns( + JSON.stringify({ + extends: './path/to/dojorc/to/.extend', + 'testGroupName-testCommandName': { + prop: 'config' + } + }) + ); + mockFs.readFileSync.onCall(1).returns( + JSON.stringify({ + extends: './path/to/dojorc/to/.extend1', + 'testGroupName-testCommandName': { + prop: 'config-extended-1', + prop1: 'config-extended-1', + prop3: { + innerProp: 'config-inner-prop', + innerProp1: { + deepProp: 'config-deep-prop', + deepProp1: 'config-deep-prop' + } + } + } + }) + ); + mockFs.readFileSync.onCall(2).returns( + JSON.stringify({ + 'testGroupName-testCommandName': { + prop: 'config-extended-2', + prop2: 'config-extended-2', + prop3: { + innerProp: 'config-inner-prop-1', + innerProp1: { + deepProp: 'config-deep-prop' + } + } + } + }) + ); + const config = configurationHelper.sandbox('testGroupName', 'testCommandName').get(); + assert.equal(mockFs.readFileSync.callCount, 3); + assert.equal(mockFs.readFileSync.firstCall.args[0], dojoRcPath); + assert.deepEqual(config, { + prop: 'config', + prop1: 'config-extended-1', + prop2: 'config-extended-2', + prop3: { + innerProp: 'config-inner-prop', + innerProp1: { + deepProp: 'config-deep-prop', + deepProp1: 'config-deep-prop' + } + } + }); + }, 'Should accept and ignore commandName parameter'() { const newConfig = { foo: 'bar' }; mockFs.readFileSync = sinon.stub().returns(JSON.stringify({ 'testGroupName-testCommandName': {} }));