Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Remove style-dictionary dependency from React Native package #5848

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/sharp-kiwis-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@aws-amplify/ui-react-native": patch
"@aws-amplify/ui": patch
---

chore: Copy `style-dictionary` functions to ui package and export from there. Remove `style-dictionary` dependency from React Native package.
3 changes: 1 addition & 2 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
"dependencies": {
"@aws-amplify/ui": "6.6.1",
"@aws-amplify/ui-react-core": "3.0.25",
"@aws-amplify/ui-react-core-notifications": "2.0.25",
"style-dictionary": "3.9.1"
"@aws-amplify/ui-react-core-notifications": "2.0.25"
},
"peerDependencies": {
"aws-amplify": "^6.6.0",
Expand Down
31 changes: 13 additions & 18 deletions packages/react-native/src/theme/createTheme.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import deepExtend from 'style-dictionary/lib/utils/deepExtend';
import resolveObject from 'style-dictionary/lib/utils/resolveObject';
import usesReference from 'style-dictionary/lib/utils/references/usesReference';
import { deepExtend, resolveObject, usesReference } from '@aws-amplify/ui';
import { isFunction, setupTokens } from '@aws-amplify/ui';
import {
Theme,
Expand All @@ -15,13 +13,15 @@ import { defaultTheme } from './defaultTheme';
// calling the component theme function with the already resolved base tokens
// OR
// resolving the component theme object
type TokensAndComponents = {
components: Components;
tokens: StrictTokens;
};

const setupComponents = ({
components,
tokens,
}: {
components: Components;
tokens: StrictTokens;
}) => {
}: TokensAndComponents): Components => {
const output = components
? Object.entries(components).reduce(
(acc, [key, value]) => ({
Expand All @@ -32,7 +32,7 @@ const setupComponents = ({
)
: {};

return resolveObject({
return resolveObject<TokensAndComponents>({
...tokens,
components: output,
}).components;
Expand Down Expand Up @@ -103,12 +103,7 @@ export const createTheme = (
): StrictTheme => {
// merge custom `theme` param and `StrictTheme` to get the merged theme.
// `deepExtend` is a Style Dictionary method that performs a deep merge on n objects.
const mergedTheme = deepExtend([
{},
defaultTheme,
theme,
// cast to `StrictTheme` as `deepExtend` returns a generic object
]) as StrictTheme;
const mergedTheme = deepExtend<StrictTheme>([{}, defaultTheme, theme]);

let { tokens: mergedTokens } = mergedTheme;
const { spaceModifier = 1 } = mergedTheme;
Expand All @@ -119,11 +114,11 @@ export const createTheme = (
if (theme?.overrides?.length) {
theme.overrides.forEach((override) => {
if (override?.colorMode === colorMode) {
mergedTokens = deepExtend([
mergedTokens = deepExtend<StrictTheme['tokens']>([
{},
mergedTokens,
override.tokens,
]) as StrictTheme['tokens'];
]);
}
// more overrides in the future could happen here
});
Expand All @@ -132,13 +127,13 @@ export const createTheme = (
// Setup the tokens:
// - each token will have a raw value
// - references to tokens (strings wrapped in curly braces) are replaced by raw values
const tokens = resolveObject(
const tokens = resolveObject<StrictTheme['tokens']>(
setupTokens({
tokens: mergedTokens,
setupToken: ({ token, path }) => {
return setupToken({ token, path, spaceModifier });
},
}) as StrictTheme['tokens']
})
);

let components;
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ const config: Config = {
'!<rootDir>/src/index.ts',
// ignore internal `debugUtils` from coverage thresholds
'!<rootDir>/**/debugUtils.ts',
// ignore coverage for style-dictionary type declaration file
'!<rootDir>/src/theme/types/style-dictionary.d.ts',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!<rootDir>/src/theme/types/style-dictionary.d.ts does not exist anymore

// ignore coverage for copied style-dictionary functions
'!<rootDir>/src/theme/createTheme/resolveObject.ts',
'!<rootDir>/src/utils/references.ts',
'!<rootDir>/src/utils/groupMessages.ts',
Comment on lines +12 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we ignore testing this code? By migrating it in to our code base we are now liable for it

],
coverageThreshold: {
global: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { usesReference } from '../../../utils';
import {
referenceValue,
isDesignToken,
cssValue,
cssNameTransform,
usesReference,
flattenProperties,
deepExtend,
} from '../utils';
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/theme/createTheme/createTheme.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { defaultTheme } from '../defaultTheme';
import { Theme, DefaultTheme, WebTheme, Override } from '../types';
import {
deepExtend,
flattenProperties,
setupToken,
setupTokens,
flattenProperties,
deepExtend,
} from './utils';
import { WebDesignToken } from '../tokens/types/designToken';
import { createComponentCSS } from './createComponentCSS';
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/theme/createTheme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ export { createComponentCSS } from './createComponentCSS';
export { createGlobalCSS } from './createGlobalCSS';
export {
cssNameTransform,
deepExtend,
isDesignToken,
setupTokens,
SetupToken,
isDesignToken,
} from './utils';
export {
createComponentClasses,
ClassNameArgs,
} from './createComponentClasses';
export { resolveObject } from './resolveObject';
169 changes: 169 additions & 0 deletions packages/ui/src/theme/createTheme/resolveObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Internal Style Dictionary methods
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from [email protected] which is specified in the ui package.json

// copied from amzn/style-dictionary with the owner's permission
import {
cloneDeep,
createReferenceRegex,
getName,
getPathFromName,
GroupMessages,
has,
resolveReference,
usesReference,
} from '../../utils';

const PROPERTY_REFERENCE_WARNINGS =
GroupMessages.GROUP.PropertyReferenceWarnings;

let current_context = []; // To maintain the context to be able to test for circular definitions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Singletons scare me, does this need to be one? Same comment for all module scoped data structures

const defaults = {
ignoreKeys: ['original'],
ignorePaths: [],
};
let updated_object, regex, options;

export function resolveObject<T>(object: Record<string, any>): T {
Copy link
Contributor Author

@esauerbo esauerbo Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only change to this file is adding the generic type <T> on resolveObject and traverseObj. This is modeled after the way we copied deepExtend here and resolves type issues where these functions are used in the react native package.

options = Object.assign({}, defaults);

updated_object = cloneDeep(object); // This object will be edited

regex = createReferenceRegex(options);

if (typeof object === 'object') {
current_context = [];
return traverseObj(updated_object) as T;
} else {
throw new Error('Please pass an object in');
}
}

function traverseObj<T>(obj): T {
let key;

for (key in obj) {
if (!has(obj, key)) {
continue;
}

// We want to check for ignoredKeys, this is to
// skip over attributes that should not be
// mutated, like a copy of the original property.
if (options.ignoreKeys && options.ignoreKeys.indexOf(key) !== -1) {
continue;
}

current_context.push(key);
if (typeof obj[key] === 'object') {
traverseObj(obj[key]);
} else {
if (typeof obj[key] === 'string' && obj[key].indexOf('{') > -1) {
obj[key] = compile_value(obj[key], [getName(current_context)]);
}
}
current_context.pop();
}

return obj as T;
}

let foundCirc = {};
function compile_value(value, stack) {
let to_ret = value,
ref;

// Replace the reference inline, but don't replace the whole string because
// references can be part of the value such as "1px solid {color.border.light}"
value.replace(regex, function (match, variable) {
variable = variable.trim();

// Find what the value is referencing
const pathName = getPathFromName(variable, options);
const context = getName(current_context, options);
const refHasValue = pathName[pathName.length - 1] === 'value';

if (refHasValue && options.ignorePaths.indexOf(variable) !== -1) {
return value;
} else if (
!refHasValue &&
options.ignorePaths.indexOf(`${variable}.value`) !== -1
) {
return value;
}

stack.push(variable);

ref = resolveReference(pathName, updated_object);

// If the reference doesn't end in 'value'
// and
// the reference points to someplace that has a `value` attribute
// we should take the '.value' of the reference
// per the W3C draft spec where references do not have .value
// https://design-tokens.github.io/community-group/format/#aliases-references
if (!refHasValue && ref && has(ref, 'value')) {
ref = ref.value;
}

if (typeof ref !== 'undefined') {
if (typeof ref === 'string' || typeof ref === 'number') {
to_ret = value.replace(match, ref);

// Recursive, therefore we can compute multi-layer variables like a = b, b = c, eventually a = c
if (usesReference(to_ret, regex)) {
var reference = to_ret.slice(1, -1);

// Compare to found circular references
if (has(foundCirc, reference)) {
// If the current reference is a member of a circular reference, do nothing
} else if (stack.indexOf(reference) !== -1) {
// If the current stack already contains the current reference, we found a new circular reference
// chop down only the circular part, save it to our circular reference info, and spit out an error

// Get the position of the existing reference in the stack
var stackIndexReference = stack.indexOf(reference);

// Get the portion of the stack that starts at the circular reference and brings you through until the end
var circStack = stack.slice(stackIndexReference);

// For all the references in this list, add them to the list of references that end up in a circular reference
circStack.forEach(function (key) {
foundCirc[key] = true;
});

// Add our found circular reference to the end of the cycle
circStack.push(reference);

// Add circ reference info to our list of warning messages
GroupMessages.add(
PROPERTY_REFERENCE_WARNINGS,
'Circular definition cycle: ' + circStack.join(', ')
);
} else {
to_ret = compile_value(to_ret, stack);
}
}
// if evaluated value is a number and equal to the reference, we want to keep the type
if (typeof ref === 'number' && ref.toString() === to_ret) {
to_ret = ref;
}
} else {
// if evaluated value is not a string or number, we want to keep the type
to_ret = ref;
}
} else {
GroupMessages.add(
PROPERTY_REFERENCE_WARNINGS,
"Reference doesn't exist: " +
context +
' tries to reference ' +
variable +
', which is not defined'
);
to_ret = ref;
}
stack.pop(variable);

return to_ret;
});

return to_ret;
}
37 changes: 2 additions & 35 deletions packages/ui/src/theme/createTheme/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import kebabCase from 'lodash/kebabCase.js';
import { has, isObject, isString } from '../../utils';
import { has, isObject, isString, usesReference } from '../../utils';
import { WebDesignToken } from '../types';
import { ShadowValue } from '../tokens/types/designToken';
import { CSSProperties } from '../components/utils';
Expand Down Expand Up @@ -142,7 +142,7 @@ export function setupTokens({
tokens,
path = [],
setupToken,
}: SetupTokensProps): any {
}: SetupTokensProps): Record<string, any> {
if (has(tokens, 'value')) {
return setupToken({ token: tokens as BaseDesignToken, path });
}
Expand Down Expand Up @@ -269,36 +269,3 @@ export function deepExtend<T>(

return target as T;
}

/**
* Checks if the value uses a value reference.
* @param {string} value
* @returns {boolean} - True, if the value uses a value reference
*/
export function usesReference(value) {
const regex = new RegExp('\\{([^}]+)\\}', 'g');

if (typeof value === 'string') {
return regex.test(value);
}

if (typeof value === 'object') {
let hasReference = false;
// iterate over each property in the object,
// if any element passes the regex test,
// the whole thing should be true
for (const key in value) {
if (has(value, key)) {
const element = value[key];
let reference = usesReference(element);
if (reference) {
hasReference = true;
break;
}
}
}
return hasReference;
}

return false;
}
2 changes: 2 additions & 0 deletions packages/ui/src/theme/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export {
createTheme,
deepExtend,
defineComponentTheme,
createComponentClasses,
createComponentCSS,
createGlobalCSS,
cssNameTransform,
isDesignToken,
resolveObject,
setupTokens,
SetupToken,
} from './createTheme';
Expand Down
Loading
Loading