diff --git a/__tests__/__properties/paddings.1.json b/__tests__/__properties/paddings.1.json new file mode 100644 index 000000000..4ca03cfcc --- /dev/null +++ b/__tests__/__properties/paddings.1.json @@ -0,0 +1,24 @@ +{ + "size": { + "padding": { + "tiny": { + "value": "3" + }, + "small": { + "value": "5" + }, + "base": { + "value": "10" + }, + "large": { + "value": "15" + }, + "xl": { + "value": "20" + }, + "xxl": { + "value": "30" + } + } + } +} \ No newline at end of file diff --git a/__tests__/extend.test.js b/__tests__/extend.test.js index c9ca204dd..afd940f88 100644 --- a/__tests__/extend.test.js +++ b/__tests__/extend.test.js @@ -15,6 +15,15 @@ var helpers = require('./__helpers'); var StyleDictionary = require('../index'); var _ = require('lodash'); +function traverseObj(obj, fn) { + for (let key in obj) { + fn.apply(this, [obj, key, obj[key]]); + if (obj[key] && typeof obj[key] === 'object') { + traverseObj(obj[key], fn); + } + } +} + var test_props = { size: { padding: { @@ -24,7 +33,6 @@ var test_props = { }; describe('extend', () => { - describe('method signature', () => { it('should accept a string as a path to a JSON file', () => { var StyleDictionaryExtended = StyleDictionary.extend(__dirname + '/__configs/test.json'); @@ -77,17 +85,31 @@ describe('extend', () => { it('should build the properties object if an include is given', () => { var StyleDictionaryExtended = StyleDictionary.extend({ - "include": [__dirname + "/__properties/paddings.json"] + include: [__dirname + "/__properties/paddings.json"] }); - expect(StyleDictionaryExtended.properties).toEqual(helpers.fileToJSON(__dirname + "/__properties/paddings.json")); + var output = helpers.fileToJSON(__dirname + "/__properties/paddings.json"); + traverseObj(output, (obj) => { + if (obj.value && !obj.filePath) { + obj.filePath = __dirname + "/__properties/paddings.json"; + obj.isSource = false; + } + }); + expect(StyleDictionaryExtended.properties).toEqual(output); }); it('should override existing properties if include is given', () => { var StyleDictionaryExtended = StyleDictionary.extend({ properties: test_props, - include: [__dirname + "/__properties/paddings.json"] + "include": [__dirname + "/__properties/paddings.json"] }); - expect(StyleDictionaryExtended.properties).toEqual(helpers.fileToJSON(__dirname + "/__properties/paddings.json")); + var output = helpers.fileToJSON(__dirname + "/__properties/paddings.json"); + traverseObj(output, (obj) => { + if (obj.value && !obj.filePath) { + obj.filePath = __dirname + "/__properties/paddings.json"; + obj.isSource = false; + } + }); + expect(StyleDictionaryExtended.properties).toEqual(output); }); it('should update properties if there are includes', () => { @@ -106,7 +128,6 @@ describe('extend', () => { }); }); - describe('source', () => { it('should throw if source isnt an array', () => { expect( @@ -125,7 +146,14 @@ describe('extend', () => { var StyleDictionaryExtended = StyleDictionary.extend({ "source": [__dirname + "/__properties/paddings.json"] }); - expect(StyleDictionaryExtended.properties).toEqual(helpers.fileToJSON(__dirname + "/__properties/paddings.json")); + var output = helpers.fileToJSON(__dirname + "/__properties/paddings.json"); + traverseObj(output, (obj) => { + if (obj.value && !obj.filePath) { + obj.filePath = __dirname + "/__properties/paddings.json"; + obj.isSource = true; + } + }); + expect(StyleDictionaryExtended.properties).toEqual(output); }); it('should override existing properties source is given', () => { @@ -133,26 +161,39 @@ describe('extend', () => { properties: test_props, source: [__dirname + "/__properties/paddings.json"] }); - expect(StyleDictionaryExtended.properties).toEqual(helpers.fileToJSON(__dirname + "/__properties/paddings.json")); + var output = helpers.fileToJSON(__dirname + "/__properties/paddings.json"); + traverseObj(output, (obj) => { + if (obj.value && !obj.filePath) { + obj.filePath = __dirname + "/__properties/paddings.json"; + obj.isSource = true; + } + }); + expect(StyleDictionaryExtended.properties).toEqual(output); }); }); - // This is to allow style dictionaries to depend on other style dictionaries and // override properties. Useful for skinning it('should not throw a collision error if a source file collides with an include', () => { var StyleDictionaryExtended = StyleDictionary.extend({ - include: [__dirname + "/__properties/paddings.json"], + include: [__dirname + "/__properties/paddings.1.json"], source: [__dirname + "/__properties/paddings.json"], log: 'error' }); - expect(StyleDictionaryExtended.properties).toEqual(helpers.fileToJSON(__dirname + "/__properties/paddings.json")); + var output = helpers.fileToJSON(__dirname + "/__properties/paddings.json"); + traverseObj(output, (obj) => { + if (obj.value && !obj.filePath) { + obj.filePath = __dirname + "/__properties/paddings.json"; + obj.isSource = true; + } + }); + expect(StyleDictionaryExtended.properties).toEqual(output); }); it('should throw a error if the collision is in source files and log is set to error', () => { expect( StyleDictionary.extend.bind(null, { - source: [__dirname + "/__properties/paddings.json", __dirname + "/__properties/paddings.json"], + source: [__dirname + "/__properties/paddings.1.json", __dirname + "/__properties/paddings.json"], log: 'error' }) ).toThrow('Collisions detected'); @@ -171,7 +212,7 @@ describe('extend', () => { var StyleDictionaryExtended = StyleDictionary.extend(__dirname + '/__configs/test.json5'); expect(StyleDictionaryExtended).toHaveProperty('platforms.web'); }); - + it('should allow for chained extends and not mutate the original', function() { var StyleDictionary1 = StyleDictionary.extend({ foo: 'bar' diff --git a/lib/extend.js b/lib/extend.js index 2829014e2..8eec404a7 100644 --- a/lib/extend.js +++ b/lib/extend.js @@ -103,7 +103,7 @@ function extend(opts) { if (!_.isArray(options.include)) throw new Error('include must be an array'); - to_ret.properties = combineJSON(options.include, true); + to_ret.properties = combineJSON(options.include, true, null, false); to_ret.include = null; // We don't want to carry over include references } diff --git a/lib/transform/propertySetup.js b/lib/transform/propertySetup.js index 4f6876a38..f9d317251 100644 --- a/lib/transform/propertySetup.js +++ b/lib/transform/propertySetup.js @@ -37,8 +37,13 @@ function propertySetup(property, name, path) { // Only do this once if (!property.original) { // Initial property setup - // Keep the original object properties; we will key off of them in the transforms - to_ret.original = _.clone(property); + // Keep the original object properties like it was in file (whitout additional data) + // so we can key off them in the transforms + var to_ret_original = _.clone(property); + delete to_ret_original.filePath; + delete to_ret_original.isSource; + + to_ret.original = to_ret_original; // Copy the name - it will be our base name to transform to_ret.name = to_ret.name || name || ''; // Create an empty attributes object that we can transform on it later diff --git a/lib/utils/combineJSON.js b/lib/utils/combineJSON.js index 3c5f2b672..05304557d 100644 --- a/lib/utils/combineJSON.js +++ b/lib/utils/combineJSON.js @@ -19,6 +19,15 @@ var glob = require('glob'), path = require('path'), resolveCwd = require('resolve-cwd'); +function traverseObj(obj, fn) { + for (let key in obj) { + fn.apply(null, [obj, key, obj[key]]); + if (obj[key] && typeof obj[key] === 'object') { + traverseObj(obj[key], fn); + } + } +} + /** * Takes an array of json files and merges * them together. Optionally does a deep extend. @@ -26,9 +35,10 @@ var glob = require('glob'), * @param {String[]} arr - Array of paths to json (or node modules that export objects) files * @param {Boolean} [deep=false] - If it should perform a deep merge * @param {Function} collision - A function to be called when a name collision happens that isn't a normal deep merge of objects + * @param {Boolean} [source=true] - If json files are "sources", tag properties * @returns {Object} */ -function combineJSON(arr, deep, collision) { +function combineJSON(arr, deep, collision, source) { var i, files = [], to_ret = {}; @@ -39,17 +49,26 @@ function combineJSON(arr, deep, collision) { for (i = 0; i < files.length; i++) { var resolvedPath = resolveCwd(path.isAbsolute(files[i]) ? files[i] : './' + files[i]); - var file_content; + var file_content = {}; try { // This delete force require(resolvedPath) to take the latest version of the file. It's handfull when using the node package along chokidar. delete require.cache[resolvedPath] - file_content = require(resolvedPath); + file_content = deepExtend([file_content, require(resolvedPath)]); } catch (e) { e.message = 'Failed to load or parse JSON or JS Object: ' + e.message; throw e; } + // Add some side data on each property to make filtering easier + traverseObj(file_content, (obj) => { + if (obj.value && !obj.filePath) { + obj.filePath = resolvedPath; + + obj.isSource = source || source === undefined ? true : false; + } + }); + if (deep) { deepExtend([to_ret, file_content], collision); } else {