Skip to content

Commit edd48c4

Browse files
committed
Add nonce support for css-variables and injectCSPNonce helper
- Extract injectCSPNonce helper function to unify nonce injection logic - Add nonce parameter support to useCacheToken hook with type definition - Add nonce parameter support to useCSSVarRegister hook - Refactor useStyleRegister to use new injectCSPNonce helper - Add comprehensive tests for nonce functionality (string and function types) This provides better CSP security by allowing nonce configuration for style elements.
1 parent d1cbe07 commit edd48c4

File tree

5 files changed

+133
-13
lines changed

5 files changed

+133
-13
lines changed

src/hooks/useCSSVarRegister.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import StyleContext, {
55
ATTR_TOKEN,
66
CSS_IN_JS_INSTANCE,
77
} from '../StyleContext';
8-
import { isClientSide, toStyleStr } from '../util';
8+
import { injectCSPNonce, isClientSide, toStyleStr } from '../util';
99
import type { TokenWithCSSVar } from '../util/css-variables';
1010
import { transformToken } from '../util/css-variables';
1111
import type { ExtractStyle } from './useGlobalCache';
@@ -31,10 +31,11 @@ const useCSSVarRegister = <V, T extends Record<string, V>>(
3131
scope?: string | string[];
3232
token: any;
3333
hashId?: string;
34+
nonce?: string | (() => string);
3435
},
3536
fn: () => T,
3637
) => {
37-
const { key, prefix, unitless, ignore, token, hashId, scope } = config;
38+
const { key, prefix, unitless, ignore, token, hashId, scope, nonce } = config;
3839
const {
3940
cache: { instanceId },
4041
container,
@@ -70,12 +71,16 @@ const useCSSVarRegister = <V, T extends Record<string, V>>(
7071
if (!cssVarsStr) {
7172
return;
7273
}
73-
const style = updateCSS(cssVarsStr, styleId, {
74+
const mergedCSSConfig: Parameters<typeof updateCSS>[2] = {
7475
mark: ATTR_MARK,
7576
prepend: 'queue',
7677
attachTo: container,
7778
priority: -999,
78-
});
79+
};
80+
81+
injectCSPNonce(mergedCSSConfig, nonce);
82+
83+
const style = updateCSS(cssVarsStr, styleId, mergedCSSConfig);
7984

8085
(style as any)[CSS_IN_JS_INSTANCE] = instanceId;
8186

src/hooks/useCacheToken.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import StyleContext, {
77
CSS_IN_JS_INSTANCE,
88
} from '../StyleContext';
99
import type Theme from '../theme/Theme';
10-
import { flattenToken, memoResult, token2key, toStyleStr } from '../util';
10+
import { flattenToken, injectCSPNonce, memoResult, token2key, toStyleStr } from '../util';
1111
import { transformToken } from '../util/css-variables';
1212
import type { ExtractStyle } from './useGlobalCache';
1313
import useGlobalCache from './useGlobalCache';
@@ -68,6 +68,12 @@ export interface Option<DerivativeToken, DesignToken> {
6868
/** Key for current theme. Useful for customizing and should be unique */
6969
key: string;
7070
};
71+
72+
/**
73+
* CSP nonce for style element.
74+
* Can be a string or a function that returns a string.
75+
*/
76+
nonce?: string | (() => string);
7177
}
7278

7379
const tokenKeys = new Map<string, number>();
@@ -168,6 +174,7 @@ export default function useCacheToken<
168174
formatToken,
169175
getComputedToken: compute,
170176
cssVar,
177+
nonce,
171178
} = option;
172179

173180
// Basic - We do basic cache here
@@ -219,12 +226,16 @@ export default function useCacheToken<
219226
if (!cssVarsStr) {
220227
return;
221228
}
222-
const style = updateCSS(cssVarsStr, hash(`css-var-${themeKey}`), {
229+
const mergedCSSConfig: Parameters<typeof updateCSS>[2] = {
223230
mark: ATTR_MARK,
224231
prepend: 'queue',
225232
attachTo: container,
226233
priority: -999,
227-
});
234+
};
235+
236+
injectCSPNonce(mergedCSSConfig, nonce);
237+
238+
const style = updateCSS(cssVarsStr, hash(`css-var-${themeKey}`), mergedCSSConfig);
228239

229240
(style as any)[CSS_IN_JS_INSTANCE] = instanceId;
230241

src/hooks/useStyleRegister.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import StyleContext, {
1515
ATTR_MARK,
1616
CSS_IN_JS_INSTANCE,
1717
} from '../StyleContext';
18-
import { isClientSide, isNonNullable, toStyleStr, where } from '../util';
18+
import { injectCSPNonce, isClientSide, isNonNullable, toStyleStr, where } from '../util';
1919
import {
2020
CSS_FILE_STYLE,
2121
existPath,
@@ -463,11 +463,7 @@ export default function useStyleRegister(
463463
priority,
464464
};
465465

466-
const nonceStr = typeof nonce === 'function' ? nonce() : nonce;
467-
468-
if (nonceStr) {
469-
mergedCSSConfig.csp = { nonce: nonceStr };
470-
}
466+
injectCSPNonce(mergedCSSConfig, nonce);
471467

472468
// ================= Split Effect Style =================
473469
// We will split effectStyle here since @layer should be at the top level

src/util/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,18 @@ export function where(options?: {
204204
export const isNonNullable = <T>(val: T): val is NonNullable<T> => {
205205
return val !== undefined && val !== null;
206206
};
207+
208+
export type Nonce = string | (() => string);
209+
210+
/**
211+
* Get nonce value and inject it into CSS config if available.
212+
*/
213+
export function injectCSPNonce<T extends { csp?: { nonce?: string } }>(
214+
config: T,
215+
nonce: Nonce | undefined,
216+
) {
217+
const nonceStr = typeof nonce === 'function' ? nonce() : nonce;
218+
if (nonceStr) {
219+
config.csp = { nonce: nonceStr };
220+
}
221+
}

tests/css-variables.spec.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
useCSSVarRegister,
1515
useStyleRegister,
1616
} from '../src';
17+
import { ATTR_TOKEN } from '../src/StyleContext';
1718

1819
export interface DesignToken {
1920
primaryColor: string;
@@ -580,4 +581,96 @@ describe('CSS Variables', () => {
580581
/\.orange\.box,\s*\.orange\.container\{/,
581582
);
582583
});
584+
585+
describe('nonce', () => {
586+
function testCacheTokenNonce(
587+
name: string,
588+
nonce: string | (() => string),
589+
) {
590+
it(`useCacheToken - ${name}`, () => {
591+
const CacheTokenNonceBox = () => {
592+
useCacheToken<DerivativeToken, DesignToken>(
593+
theme,
594+
[defaultDesignToken],
595+
{
596+
salt: '',
597+
cssVar: {
598+
prefix: 'rc',
599+
key: 'nonce-cache-token',
600+
},
601+
nonce,
602+
},
603+
);
604+
605+
return <div />;
606+
};
607+
608+
render(
609+
<StyleProvider cache={createCache()}>
610+
<CacheTokenNonceBox />
611+
</StyleProvider>,
612+
);
613+
614+
const styles = Array.from(document.head.querySelectorAll('style'));
615+
const cacheTokenStyle = styles.find((style) =>
616+
style.getAttribute(ATTR_TOKEN) === 'nonce-cache-token',
617+
);
618+
expect(cacheTokenStyle).toBeDefined();
619+
expect(cacheTokenStyle!.nonce).toBe('bamboo');
620+
});
621+
}
622+
623+
testCacheTokenNonce('string', 'bamboo');
624+
testCacheTokenNonce('function', () => 'bamboo');
625+
626+
function testCSSVarRegisterNonce(
627+
name: string,
628+
nonce: string | (() => string),
629+
) {
630+
it(`useCSSVarRegister - ${name}`, () => {
631+
const CSSVarNonceBox = () => {
632+
const [token, hashId, realToken] = useCacheToken<
633+
DerivativeToken,
634+
DesignToken
635+
>(theme, [defaultDesignToken], {
636+
salt: '',
637+
cssVar: {
638+
prefix: 'rc',
639+
key: 'nonce-css-var',
640+
},
641+
});
642+
643+
useCSSVarRegister(
644+
{
645+
path: ['NonceBox'],
646+
key: 'nonce-css-var',
647+
token: realToken,
648+
prefix: 'rc-nonce',
649+
hashId,
650+
nonce,
651+
},
652+
() => ({ testColor: '#ff0000' }),
653+
);
654+
655+
return <div />;
656+
};
657+
658+
render(
659+
<StyleProvider cache={createCache()}>
660+
<CSSVarNonceBox />
661+
</StyleProvider>,
662+
);
663+
664+
const styles = Array.from(document.head.querySelectorAll('style'));
665+
const cssVarStyle = styles.find((style) =>
666+
style.textContent?.includes('--rc-nonce-test-color'),
667+
);
668+
expect(cssVarStyle).toBeDefined();
669+
expect(cssVarStyle!.nonce).toBe('bamboo');
670+
});
671+
}
672+
673+
testCSSVarRegisterNonce('string', 'bamboo');
674+
testCSSVarRegisterNonce('function', () => 'bamboo');
675+
});
583676
});

0 commit comments

Comments
 (0)