Skip to content

Commit

Permalink
chore: make deepExtend types better with generics
Browse files Browse the repository at this point in the history
  • Loading branch information
jorenbroekema committed Dec 19, 2023
1 parent f939576 commit 38d0e08
Showing 7 changed files with 28 additions and 40 deletions.
6 changes: 2 additions & 4 deletions lib/StyleDictionary.js
Original file line number Diff line number Diff line change
@@ -176,7 +176,7 @@ export default class StyleDictionary extends Register {
// grab the inline tokens, ones either defined in the configuration object
// or that already exist from extending another style dictionary instance
// with `tokens` keys
inlineTokens = /** @type {Tokens} */ (deepExtend([{}, this.tokens || {}]));
inlineTokens = deepExtend([{}, this.tokens || {}]);

// Update tokens with includes from dependencies
if (this.options.include) {
@@ -217,9 +217,7 @@ export default class StyleDictionary extends Register {
}

// Merge inline, include, and source tokens
const unprocessedTokens = /** @type {Tokens} */ (
deepExtend([{}, inlineTokens, includeTokens, sourceTokens])
);
const unprocessedTokens = deepExtend([{}, inlineTokens, includeTokens, sourceTokens]);
this.tokens = await preprocess(unprocessedTokens, this.preprocessors);
this.hasInitializedResolve(null);

16 changes: 5 additions & 11 deletions lib/transform/config.js
Original file line number Diff line number Diff line change
@@ -21,7 +21,6 @@ import GroupMessages from '../utils/groupMessages.js';
* @typedef {import('../../types/File.d.ts').File} File
* @typedef {import('../../types/Filter.d.ts').Matcher} Matcher
* @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig
* @typedef {import('../../types/Config.d.ts').Obj} Obj
*/

const MISSING_TRANSFORM_ERRORS = GroupMessages.GROUP.MissingRegisterTransformErrors;
@@ -150,11 +149,12 @@ None of ${transform_warnings} match the name of a registered transform.
};

/**
* @param {Obj} matchObj
* @param {{[key: string]: unknown}} matchObj
*/
const matches = function (matchObj) {
let cloneObj = { ...matchObj }; // shallow clone, structuredClone not suitable because obj can contain "Function()"
let matchesFn = /** @param {Obj} inputObj */ (inputObj) => matchFn(inputObj, cloneObj);
let matchesFn = /** @param {{[key: string]: unknown}} inputObj */ (inputObj) =>
matchFn(inputObj, cloneObj);
return matchesFn;
};
ext.filter = matches(file.filter);
@@ -180,14 +180,8 @@ None of ${transform_warnings} match the name of a registered transform.
throw new Error('Please supply a format for file: ' + JSON.stringify(file));
}

// Some nasty hacks here because for some reason Obj and File types don't have enough overlap
// to be typecasted, but in this case we know File is a type of nested Object that can be
// deep extended just fine...
const _ext = /** @type {Obj} */ (/** @type {unknown} */ (ext));
const _file = /** @type {Obj} */ (/** @type {unknown} */ (file));

// typecast back to File now that it's extended.
const extended = /** @type {File} */ (/** @type {unknown} */ (deepExtend([{}, _file, _ext])));
// destination is a required prop so we have to prefill it here, or it breaks return type
const extended = deepExtend([{ destination: '' }, file, ext]);

return extended;
});
4 changes: 1 addition & 3 deletions lib/transform/tokenSetup.js
Original file line number Diff line number Diff line change
@@ -12,7 +12,6 @@
*/

import isPlainObject from 'is-plain-obj';
import deepExtend from '../utils/deepExtend.js';

/**
* @typedef {import('../../types/DesignToken.d.ts').DesignToken} Token
@@ -42,8 +41,7 @@ export default function tokenSetup(token, name, path) {
// Initial token setup
// Keep the original object tokens like it was in file (whitout additional data)
// so we can key off them in the transforms
to_ret = /** @type {Token|TransformedToken} */ (deepExtend([{}, token]));
let to_ret_original = deepExtend([{}, token]);
let to_ret_original = structuredClone(token);
delete to_ret_original.filePath;
delete to_ret_original.isSource;

7 changes: 2 additions & 5 deletions lib/utils/combineJSON.js
Original file line number Diff line number Diff line change
@@ -84,18 +84,15 @@ export default async function combineJSON(

// If there is no file_content then no custom parser ran on that file
if (!file_content) {
let parsedFile;
if (['.js', '.mjs'].includes(path.extname(filePath))) {
const fileToImport = path.resolve(
typeof window === 'object' ? '' : process.cwd(),
filePath,
);
parsedFile = (await import(fileToImport)).default;
file_content = (await import(fileToImport)).default;
} else {
parsedFile = JSON5.parse(/** @type {string} */ (fs.readFileSync(filePath, 'utf-8')));
file_content = JSON5.parse(/** @type {string} */ (fs.readFileSync(filePath, 'utf-8')));
}

file_content = /** @type {Tokens} */ (deepExtend([file_content, parsedFile]));
}
} catch (e) {
if (e instanceof Error) {
11 changes: 4 additions & 7 deletions lib/utils/createFormatArgs.js
Original file line number Diff line number Diff line change
@@ -16,7 +16,6 @@ import deepExtend from './deepExtend.js';
/**
* @typedef {import('../../types/DesignToken.js').Dictionary} Dictionary
* @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig
* @typedef {import('../../types/Config.d.ts').Obj} Obj
* @typedef {import('../../types/File.d.ts').File} File
*
@@ -34,13 +33,11 @@ export default function createFormatArgs({ dictionary, platform, file }) {
// This will merge platform and file-level configuration
// where the file configuration takes precedence
const { options } = platform;
const fileOptsTakenFromPlatform = /** @type {Partial<File>} */ ({ options });

// For some reason typescript doesn't allow us to cast File to Obj and vice versa...
const _file = /** @type {Obj} */ (/** @type {unknown} */ (file));
const extended = /** @type {File} */ (
/** @type {unknown} */ (deepExtend([{}, { options }, _file]))
);
file = extended;
// we have to do some typecasting here. We assume that because deepExtends merges objects together, and "file"
// always has the destination prop, then result will be File rather than Partial<File>, so we just typecast it.
file = /** @type {File} */ (deepExtend([{}, fileOptsTakenFromPlatform, file]));

return {
dictionary,
20 changes: 14 additions & 6 deletions lib/utils/deepExtend.js
Original file line number Diff line number Diff line change
@@ -16,29 +16,37 @@ import isPlainObject from 'is-plain-obj';
/**
* @typedef {import('../../types/DesignToken.d.ts').DesignTokens} Tokens
* @typedef {import('../../types/DesignToken.d.ts').TransformedTokens} TransformedTokens
* @typedef {import('../../types/Config.d.ts').Obj} Obj
*/

/**
* TODO: see if we can use deepmerge instead of maintaining our own utility
* Main reason for having our own is that we have a collision function that warns users
* when props from different objects collide, e.g. multiple token files colliding on the same token name
* https://github.com/TehShrike/deepmerge/issues/262 created a feature request
*
* Performs an deep extend on the objects, from right to left.
* @private
* @param {Array<Obj>} objects - An array of JS objects
* @template {Object} T - Generic type T extends from "Object", to be maximally permissive
* @param {Array<T>} objects - An array of JS objects
* @param {Function} [collision] - A function to be called when a merge collision happens.
* @param {string[]} [path] - (for internal use) An array of strings which is the current path down the object when this is called recursively.
* @returns {Obj}
* @returns {T}
*/
export default function deepExtend(objects, collision, path) {
if (objects == null) return {};
// typecast it to T, to circumvent error thrown because technically,
// T could be instantiated with a type that has more limited constraints than Obj
// but this isn't actually a problem for us since we are only merging objects together..
const defaultVal = /** @type {T} */ ({});

if (objects == null) return defaultVal;

let target = objects[0] || {};

path = path || [];

// Handle case when target is a string or something (possible in deep copy)
if (typeof target !== 'object') {
target = {};
target = defaultVal;
}

for (let i = 1; i < objects.length; i++) {
@@ -80,7 +88,7 @@ export default function deepExtend(objects, collision, path) {
// Don't bring in undefined values
} else if (copy !== undefined) {
if (src != null && typeof collision == 'function') {
collision({ target: target, copy: options, path: path, key: name });
collision({ target, copy: options, path, key: name });
}
target[name] = copy;
}
4 changes: 0 additions & 4 deletions types/Config.d.ts
Original file line number Diff line number Diff line change
@@ -20,10 +20,6 @@ import type { Transform } from './Transform.d.ts';
import type { Formatter } from './Format.d.ts';
import type { Action } from './Action.d.ts';

export interface Obj {
[key: string]: unknown | Obj;
}

export interface LocalOptions {
showFileHeader?: boolean;
fileHeader?: string | FileHeader;

0 comments on commit 38d0e08

Please sign in to comment.