diff --git a/apps/website/docs/react/api/create-dom-renderer.md b/apps/website/docs/react/api/create-dom-renderer.md index e62abf42a..de6d56213 100644 --- a/apps/website/docs/react/api/create-dom-renderer.md +++ b/apps/website/docs/react/api/create-dom-renderer.md @@ -22,6 +22,16 @@ function App(props) { } ``` +### classNameHashSalt + +A salt that will be added for generated hashed classes. Check [microsoft/griffel#453](https://github.com/microsoft/griffel/issues/453) for an example use case. + +:::caution Use with caution + +There cannot be more than **one single** hash salt in the same application (bundle) + +::: + ### compareMediaQueries A function with the same signature as sort functions in e.g. `Array.prototype.sort` for dynamically sorting media queries. Maps over an array of media query strings. diff --git a/change/@griffel-core-6d8b0e4c-e53b-469f-aff5-fbc1505643d5.json b/change/@griffel-core-6d8b0e4c-e53b-469f-aff5-fbc1505643d5.json new file mode 100644 index 000000000..dab7a9def --- /dev/null +++ b/change/@griffel-core-6d8b0e4c-e53b-469f-aff5-fbc1505643d5.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add \"classNameHashSalt\" option", + "packageName": "@griffel/core", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/core/src/makeResetStyles.test.ts b/packages/core/src/makeResetStyles.test.ts index bd4b0744e..c7ba77f06 100644 --- a/packages/core/src/makeResetStyles.test.ts +++ b/packages/core/src/makeResetStyles.test.ts @@ -72,4 +72,18 @@ describe('makeResetStyles', () => { } `); }); + + describe('classNameHashSalt', () => { + it('applies a salt to the hash', () => { + const rendererWithSalt = createDOMRenderer(document, { classNameHashSalt: 'salt' }); + + const resultWithSalt = makeResetStyles({ color: 'red' })({ dir: 'ltr', renderer }); + const resultWithoutSalt = makeResetStyles({ color: 'red' })({ dir: 'ltr', renderer: rendererWithSalt }); + + expect(resultWithSalt).toMatchInlineSnapshot(`"rtokvmb"`); + expect(resultWithoutSalt).toMatchInlineSnapshot(`"r1fkucf3"`); + + expect(resultWithSalt).not.toBe(resultWithoutSalt); + }); + }); }); diff --git a/packages/core/src/makeResetStyles.ts b/packages/core/src/makeResetStyles.ts index 2de5158c3..7ce62ddd4 100644 --- a/packages/core/src/makeResetStyles.ts +++ b/packages/core/src/makeResetStyles.ts @@ -17,12 +17,31 @@ export function makeResetStyles(styles: GriffelResetStyle, factory: GriffelInser let rtlClassName: string | null = null; let cssRules: CSSRulesByBucket | string[] | null = null; + let classNameHashSalt: string; function computeClassName(options: MakeResetStylesOptions): string { const { dir, renderer } = options; if (ltrClassName === null) { - [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles); + [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles, renderer.classNameHashSalt); + + if (process.env.NODE_ENV !== 'production') { + if (renderer.classNameHashSalt) { + if (classNameHashSalt !== renderer.classNameHashSalt) { + console.error( + [ + '@griffel/core:', + '\n\n', + 'A provided renderer has different "classNameHashSalt".', + 'This is not supported and WILL cause issues with classnames generation.', + 'Ensure that all renderers created with "createDOMRenderer()" have the same "classNameHashSalt".', + ].join(' '), + ); + } + + classNameHashSalt = renderer.classNameHashSalt; + } + } } insertStyles(renderer, Array.isArray(cssRules) ? { r: cssRules! } : cssRules!); diff --git a/packages/core/src/makeStyles.test.ts b/packages/core/src/makeStyles.test.ts index 1b768d7da..f43b3fae2 100644 --- a/packages/core/src/makeStyles.test.ts +++ b/packages/core/src/makeStyles.test.ts @@ -277,6 +277,23 @@ describe('makeStyles', () => { `); }); + describe('classNameHashSalt', () => { + it('applies a salt to the hash', () => { + const rendererWithSalt = createDOMRenderer(document, { classNameHashSalt: 'salt' }); + + const computeClassesWithSalt = makeStyles({ root: { color: 'red' } }); + const computeClassesWithoutSalt = makeStyles({ root: { color: 'red' } }); + + const resultWithSalt = computeClassesWithSalt({ dir: 'ltr', renderer }).root; + const resultWithoutSalt = computeClassesWithoutSalt({ dir: 'ltr', renderer: rendererWithSalt }).root; + + expect(resultWithSalt).toMatchInlineSnapshot(`"___afhpfp0 fe3e8s9"`); + expect(resultWithoutSalt).toMatchInlineSnapshot(`"___eoxc7a0 fl2dfm4"`); + + expect(resultWithSalt).not.toBe(resultWithoutSalt); + }); + }); + it.each<'test' | 'development'>(['test', 'development'])( 'in non-production mode, hashes include debug information', env => { diff --git a/packages/core/src/makeStyles.ts b/packages/core/src/makeStyles.ts index 94958cc15..04a93ab21 100644 --- a/packages/core/src/makeStyles.ts +++ b/packages/core/src/makeStyles.ts @@ -27,11 +27,31 @@ export function makeStyles( sourceURL = getSourceURLfromError(); } + let classNameHashSalt: string; + function computeClasses(options: MakeStylesOptions): Record { const { dir, renderer } = options; if (classesMapBySlot === null) { - [classesMapBySlot, cssRules] = resolveStyleRulesForSlots(stylesBySlots); + [classesMapBySlot, cssRules] = resolveStyleRulesForSlots(stylesBySlots, renderer.classNameHashSalt); + + if (process.env.NODE_ENV !== 'production') { + if (renderer.classNameHashSalt) { + if (classNameHashSalt !== renderer.classNameHashSalt) { + console.error( + [ + '@griffel/core:', + '\n\n', + 'A provided renderer has different "classNameHashSalt".', + 'This is not supported and WILL cause issues with classnames generation.', + 'Ensure that all renderers created with "createDOMRenderer()" have the same "classNameHashSalt".', + ].join(' '), + ); + } + + classNameHashSalt = renderer.classNameHashSalt; + } + } } const isLTR = dir === 'ltr'; diff --git a/packages/core/src/renderer/createDOMRenderer.ts b/packages/core/src/renderer/createDOMRenderer.ts index 322a18b1f..ad6e60bd2 100644 --- a/packages/core/src/renderer/createDOMRenderer.ts +++ b/packages/core/src/renderer/createDOMRenderer.ts @@ -7,6 +7,14 @@ import { safeInsertRule } from './safeInsertRule'; let lastIndex = 0; export interface CreateDOMRendererOptions { + /** + * A salt that will be added for hashed classes. Should be the same for all renderers in the same application + * (bundle). + * + * @see https://github.com/microsoft/griffel/issues/453 + */ + classNameHashSalt?: string; + /** * If specified, a renderer will insert created style tags after this element. */ @@ -50,12 +58,14 @@ export function createDOMRenderer( options: CreateDOMRendererOptions = {}, ): GriffelRenderer { const { + classNameHashSalt, unstable_filterCSSRule, insertionPoint, styleElementAttributes, compareMediaQueries = defaultCompareMediaQueries, } = options; const renderer: GriffelRenderer = { + classNameHashSalt, insertionCache: {}, stylesheets: {}, styleElementAttributes: Object.freeze(styleElementAttributes), diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 69293fc67..8e5d319e0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -20,6 +20,11 @@ export interface IsomorphicStyleSheet { export interface GriffelRenderer { id: string; + /** + * @private + */ + classNameHashSalt?: string; + /** * @private */