diff --git a/package.json b/package.json index 99761fa..83b53e1 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.0.10", - "rc-util": "^5.34.1", + "rc-util": "^5.35.0", "stylis": "^4.0.13" }, "devDependencies": { diff --git a/src/hooks/useStyleRegister/index.tsx b/src/hooks/useStyleRegister/index.tsx index 67fd85f..d8f0c54 100644 --- a/src/hooks/useStyleRegister/index.tsx +++ b/src/hooks/useStyleRegister/index.tsx @@ -367,10 +367,16 @@ export default function useStyleRegister( layer?: string; nonce?: string | (() => string); clientOnly?: boolean; + /** + * Tell cssinjs the insert order of style. + * It's useful when you need to insert style + * before other style to overwrite for the same selector priority. + */ + order?: number; }, styleFn: () => CSSInterpolation, ) { - const { token, path, hashId, layer, nonce, clientOnly } = info; + const { token, path, hashId, layer, nonce, clientOnly, order = 0 } = info; const { autoClear, mock, @@ -399,6 +405,7 @@ export default function useStyleRegister( styleId: string, effectStyle: Record, clientOnly: boolean | undefined, + order: number, ] >( 'style', @@ -411,7 +418,14 @@ export default function useStyleRegister( if (existPath(cachePath)) { const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath); if (inlineCacheStyleStr) { - return [inlineCacheStyleStr, tokenKey, styleHash, {}, clientOnly]; + return [ + inlineCacheStyleStr, + tokenKey, + styleHash, + {}, + clientOnly, + order, + ]; } } @@ -429,7 +443,7 @@ export default function useStyleRegister( const styleStr = normalizeStyle(parsedStyle); const styleId = uniqueHash(fullPath, styleStr); - return [styleStr, tokenKey, styleId, effectStyle, clientOnly]; + return [styleStr, tokenKey, styleId, effectStyle, clientOnly, order]; }, // Remove cache if no need @@ -446,6 +460,7 @@ export default function useStyleRegister( mark: ATTR_MARK, prepend: 'queue', attachTo: container, + priority: order, }; const nonceStr = typeof nonce === 'function' ? nonce() : nonce; @@ -547,43 +562,65 @@ export function extractStyle(cache: Cache, plain = false) { } // ====================== Fill Style ====================== - styleKeys.forEach((key) => { - const cachePath = key.slice(matchPrefix.length).replace(/%/g, '|'); - - const [styleStr, tokenKey, styleId, effectStyle, clientOnly]: [ - string, - string, - string, - Record, - boolean, - ] = cache.cache.get(key)![1]; - - // Skip client only style - if (clientOnly) { - return; - } + type OrderStyle = [order: number, style: string]; + + const orderStyles: OrderStyle[] = styleKeys + .map((key) => { + const cachePath = key.slice(matchPrefix.length).replace(/%/g, '|'); + + const [styleStr, tokenKey, styleId, effectStyle, clientOnly, order]: [ + string, + string, + string, + Record, + boolean, + number, + ] = cache.cache.get(key)![1]; + + // Skip client only style + if (clientOnly) { + return null! as OrderStyle; + } - // ====================== Style ====================== - styleText += toStyleStr(styleStr, tokenKey, styleId); + // ====================== Style ====================== + // Used for rc-util + const sharedAttrs = { + 'data-rc-order': 'prependQueue', + 'data-rc-priority': `${order}`, + }; - // Save cache path with hash mapping - cachePathMap[cachePath] = styleId; + let keyStyleText = toStyleStr(styleStr, tokenKey, styleId, sharedAttrs); - // =============== Create effect style =============== - if (effectStyle) { - Object.keys(effectStyle).forEach((effectKey) => { - // Effect style can be reused - if (!effectStyles[effectKey]) { - effectStyles[effectKey] = true; - styleText += toStyleStr( - normalizeStyle(effectStyle[effectKey]), - tokenKey, - `_effect-${effectKey}`, - ); - } - }); - } - }); + // Save cache path with hash mapping + cachePathMap[cachePath] = styleId; + + // =============== Create effect style =============== + if (effectStyle) { + Object.keys(effectStyle).forEach((effectKey) => { + // Effect style can be reused + if (!effectStyles[effectKey]) { + effectStyles[effectKey] = true; + keyStyleText += toStyleStr( + normalizeStyle(effectStyle[effectKey]), + tokenKey, + `_effect-${effectKey}`, + sharedAttrs, + ); + } + }); + } + + const ret: OrderStyle = [order, keyStyleText]; + + return ret; + }) + .filter((o) => o); + + orderStyles + .sort((o1, o2) => o1[0] - o2[0]) + .forEach(([, style]) => { + styleText += style; + }); // ==================== Fill Cache Path ==================== styleText += toStyleStr( diff --git a/tests/server.spec.tsx b/tests/server.spec.tsx index 56eb887..e5cded8 100644 --- a/tests/server.spec.tsx +++ b/tests/server.spec.tsx @@ -115,7 +115,7 @@ describe('SSR', () => { '
:R1:
:Ra:
:R3:
', ); expect(style).toEqual( - '', + '', ); expect(plainStyle).toEqual( '.box{background-color:#1890ff;}.data-ant-cssinjs-cache-path{content:"u4cay0|.box:gn1jfq";}', @@ -241,7 +241,7 @@ describe('SSR', () => { const style = extractStyle(cache); expect(style).toEqual( - '', + '', ); }); @@ -330,7 +330,7 @@ describe('SSR', () => { expect(html).toEqual('
'); expect(style).toEqual( - '', + '', ); }); @@ -386,4 +386,59 @@ describe('SSR', () => { getStyleAndHash.mockRestore(); }); + + it('ssr keep order', () => { + const createComponent = (name: string, order?: number) => { + const OrderDefault = ({ children }: { children?: React.ReactNode }) => { + const [token] = useCacheToken(theme, [baseToken]); + + const wrapSSR = useStyleRegister( + { theme, token, path: [name], order }, + () => ({ + [`.${name}`]: { + backgroundColor: token.primaryColor, + }, + }), + ); + + return wrapSSR(
{children}
); + }; + + return OrderDefault; + }; + + const Order0 = createComponent('order0', 0); + const Order1 = createComponent('order1', 1); + const Order2 = createComponent('order2', 2); + + const cache = createCache(); + + renderToString( + + + + + , + ); + + const style = extractStyle(cache); + const holder = document.createElement('div'); + holder.innerHTML = style; + const styles = Array.from(holder.querySelectorAll('style')); + + expect(styles[0].getAttribute('data-rc-priority')).toEqual('0'); + expect(styles[1].getAttribute('data-rc-priority')).toEqual('1'); + expect(styles[2].getAttribute('data-rc-priority')).toEqual('2'); + + // Pure style + const pureStyle = extractStyle(cache, true); + expect(pureStyle).toEqual( + [ + `.order0{background-color:#1890ff;}`, + `.order1{background-color:#1890ff;}`, + `.order2{background-color:#1890ff;}`, + `.data-ant-cssinjs-cache-path{content:"u4cay0|order1:1qekw6y;u4cay0|order0:1r3sam0;u4cay0|order2:1at78yk";}`, + ].join(''), + ); + }); });