From 38d0e08d738e6d6f1e14c1b0bc80ceee0ec2a3b6 Mon Sep 17 00:00:00 2001 From: jorenbroekema Date: Tue, 19 Dec 2023 13:18:13 +0100 Subject: [PATCH] chore: make deepExtend types better with generics --- lib/StyleDictionary.js | 6 ++---- lib/transform/config.js | 16 +++++----------- lib/transform/tokenSetup.js | 4 +--- lib/utils/combineJSON.js | 7 ++----- lib/utils/createFormatArgs.js | 11 ++++------- lib/utils/deepExtend.js | 20 ++++++++++++++------ types/Config.d.ts | 4 ---- 7 files changed, 28 insertions(+), 40 deletions(-) diff --git a/lib/StyleDictionary.js b/lib/StyleDictionary.js index 9ce06337d..e04cf0bbb 100644 --- a/lib/StyleDictionary.js +++ b/lib/StyleDictionary.js @@ -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); diff --git a/lib/transform/config.js b/lib/transform/config.js index a8830c6d6..6a107882d 100644 --- a/lib/transform/config.js +++ b/lib/transform/config.js @@ -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; }); diff --git a/lib/transform/tokenSetup.js b/lib/transform/tokenSetup.js index 05f5a1794..6f93c0dfb 100644 --- a/lib/transform/tokenSetup.js +++ b/lib/transform/tokenSetup.js @@ -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; diff --git a/lib/utils/combineJSON.js b/lib/utils/combineJSON.js index c41ae94f2..f47dccc9c 100644 --- a/lib/utils/combineJSON.js +++ b/lib/utils/combineJSON.js @@ -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) { diff --git a/lib/utils/createFormatArgs.js b/lib/utils/createFormatArgs.js index 7c85965f3..e4580200e 100644 --- a/lib/utils/createFormatArgs.js +++ b/lib/utils/createFormatArgs.js @@ -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} */ ({ 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, so we just typecast it. + file = /** @type {File} */ (deepExtend([{}, fileOptsTakenFromPlatform, file])); return { dictionary, diff --git a/lib/utils/deepExtend.js b/lib/utils/deepExtend.js index df998bbba..b24deddfe 100644 --- a/lib/utils/deepExtend.js +++ b/lib/utils/deepExtend.js @@ -16,21 +16,29 @@ 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} objects - An array of JS objects + * @template {Object} T - Generic type T extends from "Object", to be maximally permissive + * @param {Array} 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] || {}; @@ -38,7 +46,7 @@ export default function deepExtend(objects, collision, 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; } diff --git a/types/Config.d.ts b/types/Config.d.ts index dff8fbce2..c552b97d7 100644 --- a/types/Config.d.ts +++ b/types/Config.d.ts @@ -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;