Skip to content

Commit

Permalink
chore: Add nonce support to styling (#2629)
Browse files Browse the repository at this point in the history
Add a way to create an Emotion instance with initialization options, including a `nonce` or custom `stylis` plugins. Before, the Emotion instance created by `@emotion/css` was used. Now `@emotion/css` is used as a fallback if an instance hasn't been created by the time a style utility is called.

Fixes: #2628 

This change adds the following opt-in features:
- Change the default Emotion cache instance used by Canvas Kit and the application
- Allow use of `injectGlobal` from `@workday/canvas-kit-styling` that uses the shared instance.

[category:Infrastructure]

Release Note:
This change does not introduce any breaking changes, but creating a custom Emotion instance can introduce a breaking change. A custom instance should only be used if all instances of Canvas Kit on the page are above the version this change is released in and no application code is imported directly from `@emotion/css`. This change updates all internal Canvas Kit styling to use the Emotion instance created in `@workday/canvas-kit-styling`. If no custom instance is created, the one created by `@emotion/css` will be used. If the default instance is used, there should be no breaking changes, but everyone should update their application code to use styling functions from `@workday/canvas-kit-styling` and not `@emotion/css`. SSR using `@emotion/css` is unaffected since server to client hydration only cares about the cache key ("css") and the style's hash, which should be the same even with a custom cache instance.
  • Loading branch information
NicholasBoll committed Mar 6, 2024
1 parent 8f32464 commit 75fe178
Show file tree
Hide file tree
Showing 12 changed files with 282 additions and 52 deletions.
4 changes: 2 additions & 2 deletions modules/react/common/lib/CanvasProvider.tsx
Expand Up @@ -3,8 +3,7 @@ import {Theme, ThemeProvider, CacheProvider} from '@emotion/react';
import {InputProvider} from './InputProvider';
import {defaultCanvasTheme, PartialEmotionCanvasTheme, useTheme} from './theming';
import {brand} from '@workday/canvas-tokens-web';
import {cache} from '@emotion/css';
import {createStyles} from '@workday/canvas-kit-styling';
import {createStyles, getCache} from '@workday/canvas-kit-styling';

export interface CanvasProviderProps {
theme?: PartialEmotionCanvasTheme;
Expand Down Expand Up @@ -89,6 +88,7 @@ export const CanvasProvider = ({
...props
}: CanvasProviderProps & React.HTMLAttributes<HTMLElement>) => {
const elemProps = useCanvasThemeToCssVars(theme, props);
const cache = getCache();
return (
<CacheProvider value={cache}>
<ThemeProvider theme={theme as Theme}>
Expand Down
14 changes: 3 additions & 11 deletions modules/react/layout/lib/utils/mergeStyles.ts
@@ -1,4 +1,3 @@
import {css} from '@emotion/css';
import {CSToPropsInput, handleCsProp} from '@workday/canvas-kit-styling';
import {boxStyleFn} from '../Box';
import {backgroundStyleFnConfigs} from './background';
Expand Down Expand Up @@ -71,20 +70,13 @@ export function mergeStyles<T extends {}>(
return result;
}, {});

// We need to determine if style props have been used. If they have, we need to merge all the CSS
// classes into a single class name in the order that the class names are listed. This variable
// will collect the CSS class name created by Emotion if we detect style props.
let stylePropsClassName = '';
let styles = {};

// We have style props. We need to create style and merge with our `csToProps` to get the correct
// merging order for styles
if (shouldRuntimeMergeStyles) {
const styles = boxStyleFn(styleProps);
stylePropsClassName = css(styles);
styles = boxStyleFn(styleProps);
}

return handleCsProp(elemProps, [localCs, stylePropsClassName]) as Omit<
T,
'cs' | keyof CommonStyleProps
>;
return handleCsProp(elemProps, [localCs, styles]) as Omit<T, 'cs' | keyof CommonStyleProps>;
}
15 changes: 7 additions & 8 deletions modules/react/layout/spec/mergeStyles.spec.tsx
@@ -1,18 +1,17 @@
import React from 'react';

import {cache} from '@emotion/css';
import {jsx, CacheProvider} from '@emotion/react';
import {jsx} from '@emotion/react';
import styled from '@emotion/styled';
import {render as rtlRender, screen} from '@testing-library/react';

// We need to force Emotion's cache wrapper to use the cache from `@emotion/css` for tests to pass
const CacheWrapper = props => <CacheProvider value={cache} {...props} />;
// @ts-ignore We want the types to be the same, but I don't care to fix the error
const render: typeof rtlRender = (ui, options) =>
rtlRender(ui, {wrapper: CacheWrapper, ...options});

import {mergeStyles} from '@workday/canvas-kit-react/layout';
import {createStyles} from '@workday/canvas-kit-styling';
import {CanvasProvider} from '@workday/canvas-kit-react/common';

// We need to force Emotion's cache wrapper to use the cache from `@workday/canvas-kit-styling`
// @ts-ignore We want the types to be the same, but I don't care to fix the error
const render: typeof rtlRender = (ui, options) =>
rtlRender(ui, {wrapper: CanvasProvider, ...options});

describe('mergeStyles', () => {
const padding = {
Expand Down
1 change: 0 additions & 1 deletion modules/react/package.json
Expand Up @@ -44,7 +44,6 @@
"react": ">=16.14"
},
"dependencies": {
"@emotion/css": "^11.7.1",
"@emotion/is-prop-valid": "^1.1.1",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
Expand Down
2 changes: 2 additions & 0 deletions modules/styling-transform/lib/styleTransform.ts
Expand Up @@ -11,6 +11,7 @@ import {handlePx2Rem} from './utils/handlePx2Rem';
import {handleCssVar} from './utils/handleCssVar';
import {Config, NodeTransformer, ObjectTransform, TransformerContext} from './utils/types';
import {handleKeyframes} from './utils/handleKeyframes';
import {handleInjectGlobal} from './utils/handleInjectGlobal';

export type NestedStyleObject = {[key: string]: string | NestedStyleObject};

Expand Down Expand Up @@ -44,6 +45,7 @@ const defaultTransformers = [
handleCreateVars,
handleCreateStyles,
handleCreateStencil,
handleInjectGlobal,
];

export default function styleTransformer(
Expand Down
56 changes: 56 additions & 0 deletions modules/styling-transform/lib/utils/handleInjectGlobal.ts
@@ -0,0 +1,56 @@
import ts from 'typescript';

import {isImportedFromStyling} from './isImportedFromStyling';
import {parseObjectToStaticValue} from './parseObjectToStaticValue';
import {compileCSS, createStyleObjectNode, serializeStyles} from './createStyleObjectNode';
import {NestedStyleObject, NodeTransformer, TransformerContext} from './types';
import {parseNodeToStaticValue} from './parseNodeToStaticValue';

export const handleInjectGlobal: NodeTransformer = (node, context) => {
const {checker, getFileName} = context;

if (
ts.isTaggedTemplateExpression(node) &&
ts.isIdentifier(node.tag) &&
node.tag.text === 'injectGlobal' &&
isImportedFromStyling(node.tag, checker)
) {
const fileName = getFileName(node.getSourceFile()?.fileName || context.fileName);
const styleObj = parseNodeToStaticValue(node.template, context).toString(); //?

return ts.factory.createCallExpression(node.tag, undefined, [
createStyleReplacementNode(styleObj, fileName, context),
]);
}

if (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'injectGlobal' &&
isImportedFromStyling(node.expression, checker)
) {
if (ts.isObjectLiteralExpression(node.arguments[0])) {
const fileName = getFileName(node.expression.getSourceFile()?.fileName || context.fileName);
const styleObj = parseObjectToStaticValue(node.arguments[0], context);

return ts.factory.updateCallExpression(node, node.expression, undefined, [
createStyleReplacementNode(styleObj, fileName, context),
]);
}
}

return;
};

function createStyleReplacementNode(
styleObj: NestedStyleObject | string,
fileName: string,
{styles}: TransformerContext
) {
const serialized = serializeStyles(styleObj); //?
const styleOutput = compileCSS(serialized.styles);
styles[fileName] = styles[fileName] || [];
styles[fileName].push(styleOutput);

return createStyleObjectNode(serialized.styles, serialized.name);
}
76 changes: 76 additions & 0 deletions modules/styling-transform/spec/utils/handleInjectGlobal.spec.ts
@@ -0,0 +1,76 @@
import {
createProgramFromSource,
withDefaultContext,
transform,
} from '@workday/canvas-kit-styling-transform/testing';
import {compileCSS} from '../../lib/utils/createStyleObjectNode';

describe('handleInjectGlobal', () => {
it('should transform an object using injectGlobal', () => {
const program = createProgramFromSource(`
import {injectGlobal} from '@workday/canvas-kit-styling';
injectGlobal({
'*': {
padding: 10
}
})
`);

const result = transform(program, 'test.ts');

expect(result).toContain('styles: "*{padding:10px;}');
});

it('should transform a TemplateString using injectGlobal', () => {
const program = createProgramFromSource(`
import {injectGlobal} from '@workday/canvas-kit-styling';
injectGlobal\`
* {
padding: 10px;
}
\`
`);

const result = transform(program, 'test.ts');

expect(result).toMatch(
/injectGlobal\({ name: "[a-z0-9]+", styles: ".+\*.+{.+padding: 10px;.+}/
);
});

it('should add global styles to the compiled CSS from an object', () => {
const program = createProgramFromSource(`
import {injectGlobal} from '@workday/canvas-kit-styling';
injectGlobal({
'*': {
padding: 10
}
})
`);

const styles = {};
transform(program, 'test.ts', withDefaultContext(program.getTypeChecker(), {styles}));

expect(styles['test.css']).toContainEqual(compileCSS('* { padding: 10px; }'));
});

it('should add global styles to the compiled CSS from a template string', () => {
const program = createProgramFromSource(`
import {injectGlobal} from '@workday/canvas-kit-styling';
injectGlobal\`
* {
padding: 10px;
}
\`
`);

const styles = {};
transform(program, 'test.ts', withDefaultContext(program.getTypeChecker(), {styles}));

expect(styles['test.css']).toContainEqual(compileCSS('* { padding: 10px; }'));
});
});

0 comments on commit 75fe178

Please sign in to comment.