1
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
import type { TokenWithCSSVar } from '../util/css-variables';
|
||||
import type { ExtractStyle } from './useGlobalCache';
|
||||
export declare const CSS_VAR_PREFIX = "cssVar";
|
||||
type CSSVarCacheValue<V, T extends Record<string, V> = Record<string, V>> = [
|
||||
cssVarToken: TokenWithCSSVar<V, T>,
|
||||
cssVarStr: string,
|
||||
styleId: string,
|
||||
cssVarKey: string
|
||||
];
|
||||
declare const useCSSVarRegister: <V, T extends Record<string, V>>(config: {
|
||||
path: string[];
|
||||
key: string;
|
||||
prefix?: string;
|
||||
unitless?: Record<string, boolean>;
|
||||
ignore?: Record<string, boolean>;
|
||||
scope?: string | string[];
|
||||
token: any;
|
||||
hashId?: string;
|
||||
nonce?: string | (() => string);
|
||||
}, fn: () => T) => CSSVarCacheValue<V, T>;
|
||||
export declare const extract: ExtractStyle<CSSVarCacheValue<any>>;
|
||||
export default useCSSVarRegister;
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { removeCSS, updateCSS } from "@rc-component/util/es/Dom/dynamicCSS";
|
||||
import { useContext } from 'react';
|
||||
import StyleContext, { ATTR_MARK, ATTR_TOKEN, CSS_IN_JS_INSTANCE } from "../StyleContext";
|
||||
import { injectCSPNonce, isClientSide, toStyleStr } from "../util";
|
||||
import { transformToken } from "../util/css-variables";
|
||||
import useGlobalCache from "./useGlobalCache";
|
||||
import { uniqueHash } from "./useStyleRegister";
|
||||
export const CSS_VAR_PREFIX = 'cssVar';
|
||||
const useCSSVarRegister = (config, fn) => {
|
||||
const {
|
||||
key,
|
||||
prefix,
|
||||
unitless,
|
||||
ignore,
|
||||
token,
|
||||
hashId,
|
||||
scope,
|
||||
nonce
|
||||
} = config;
|
||||
const {
|
||||
cache: {
|
||||
instanceId
|
||||
},
|
||||
container,
|
||||
hashPriority
|
||||
} = useContext(StyleContext);
|
||||
const {
|
||||
_tokenKey: tokenKey
|
||||
} = token;
|
||||
const scopeKey = Array.isArray(scope) ? scope.join('@@') : scope;
|
||||
const stylePath = [...config.path, key, scopeKey, tokenKey];
|
||||
const cache = useGlobalCache(CSS_VAR_PREFIX, stylePath, () => {
|
||||
const originToken = fn();
|
||||
const [mergedToken, cssVarsStr] = transformToken(originToken, key, {
|
||||
prefix,
|
||||
unitless,
|
||||
ignore,
|
||||
scope,
|
||||
hashPriority,
|
||||
hashCls: hashId
|
||||
});
|
||||
const styleId = uniqueHash(stylePath, cssVarsStr);
|
||||
return [mergedToken, cssVarsStr, styleId, key];
|
||||
}, ([,, styleId]) => {
|
||||
if (isClientSide) {
|
||||
removeCSS(styleId, {
|
||||
mark: ATTR_MARK,
|
||||
attachTo: container
|
||||
});
|
||||
}
|
||||
}, ([, cssVarsStr, styleId]) => {
|
||||
if (!cssVarsStr) {
|
||||
return;
|
||||
}
|
||||
let mergedCSSConfig = {
|
||||
mark: ATTR_MARK,
|
||||
prepend: 'queue',
|
||||
attachTo: container,
|
||||
priority: -999
|
||||
};
|
||||
mergedCSSConfig = injectCSPNonce(mergedCSSConfig, nonce);
|
||||
const style = updateCSS(cssVarsStr, styleId, mergedCSSConfig);
|
||||
style[CSS_IN_JS_INSTANCE] = instanceId;
|
||||
|
||||
// Used for `useCacheToken` to remove on batch when token removed
|
||||
style.setAttribute(ATTR_TOKEN, key);
|
||||
});
|
||||
return cache;
|
||||
};
|
||||
export const extract = (cache, effectStyles, options) => {
|
||||
const [, styleStr, styleId, cssVarKey] = cache;
|
||||
const {
|
||||
plain
|
||||
} = options || {};
|
||||
if (!styleStr) {
|
||||
return null;
|
||||
}
|
||||
const order = -999;
|
||||
|
||||
// ====================== Style ======================
|
||||
// Used for @rc-component/util
|
||||
const sharedAttrs = {
|
||||
'data-rc-order': 'prependQueue',
|
||||
'data-rc-priority': `${order}`
|
||||
};
|
||||
const styleText = toStyleStr(styleStr, cssVarKey, styleId, sharedAttrs, plain);
|
||||
return [order, styleId, styleText];
|
||||
};
|
||||
export default useCSSVarRegister;
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import type Theme from '../theme/Theme';
|
||||
import type { ExtractStyle } from './useGlobalCache';
|
||||
export interface Option<DerivativeToken, DesignToken> {
|
||||
/**
|
||||
* Generate token with salt.
|
||||
* This is used to generate different hashId even same derivative token for different version.
|
||||
*/
|
||||
salt?: string;
|
||||
override?: object;
|
||||
/**
|
||||
* Format token as you need. Such as:
|
||||
*
|
||||
* - rename token
|
||||
* - merge token
|
||||
* - delete token
|
||||
*
|
||||
* This should always be the same since it's one time process.
|
||||
* It's ok to useMemo outside but this has better cache strategy.
|
||||
*/
|
||||
formatToken?: (mergedToken: any) => DerivativeToken;
|
||||
/**
|
||||
* Get final token with origin token, override token and theme.
|
||||
* The parameters do not contain formatToken since it's passed by user.
|
||||
* @param origin The original token.
|
||||
* @param override Extra tokens to override.
|
||||
* @param theme Theme instance. Could get derivative token by `theme.getDerivativeToken`
|
||||
*/
|
||||
getComputedToken?: (origin: DesignToken, override: object, theme: Theme<any, any>) => DerivativeToken;
|
||||
/**
|
||||
* Transform token to css variables.
|
||||
*/
|
||||
cssVar: {
|
||||
hashed?: boolean;
|
||||
/** Prefix for css variables */
|
||||
prefix?: string;
|
||||
/** Tokens that should not be appended with unit */
|
||||
unitless?: Record<string, boolean>;
|
||||
/** Tokens that should not be transformed to css variables */
|
||||
ignore?: Record<string, boolean>;
|
||||
/** Tokens that preserves origin value */
|
||||
preserve?: Record<string, boolean>;
|
||||
/** Key for current theme. Useful for customizing and should be unique */
|
||||
key: string;
|
||||
};
|
||||
/**
|
||||
* CSP nonce for style element.
|
||||
* Can be a string or a function that returns a string.
|
||||
*/
|
||||
nonce?: string | (() => string);
|
||||
}
|
||||
export declare const getComputedToken: <DerivativeToken = object, DesignToken = DerivativeToken>(originToken: DesignToken, overrideToken: object, theme: Theme<any, any>, format?: ((token: DesignToken) => DerivativeToken) | undefined) => any;
|
||||
export declare const TOKEN_PREFIX = "token";
|
||||
type TokenCacheValue<DerivativeToken> = [
|
||||
token: DerivativeToken,
|
||||
hashId: string,
|
||||
realToken: DerivativeToken,
|
||||
cssVarStr: string,
|
||||
cssVarKey: string
|
||||
];
|
||||
/**
|
||||
* Cache theme derivative token as global shared one
|
||||
* @param theme Theme entity
|
||||
* @param tokens List of tokens, used for cache. Please do not dynamic generate object directly
|
||||
* @param option Additional config
|
||||
* @returns Call Theme.getDerivativeToken(tokenObject) to get token
|
||||
*/
|
||||
export default function useCacheToken<DerivativeToken = object, DesignToken = DerivativeToken>(theme: Theme<any, any>, tokens: Partial<DesignToken>[], option: Option<DerivativeToken, DesignToken>): TokenCacheValue<DerivativeToken>;
|
||||
export declare const extract: ExtractStyle<TokenCacheValue<any>>;
|
||||
export {};
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import hash from '@emotion/hash';
|
||||
import { updateCSS } from "@rc-component/util/es/Dom/dynamicCSS";
|
||||
import { useContext } from 'react';
|
||||
import StyleContext, { ATTR_MARK, ATTR_TOKEN, CSS_IN_JS_INSTANCE } from "../StyleContext";
|
||||
import { flattenToken, injectCSPNonce, memoResult, token2key, toStyleStr } from "../util";
|
||||
import { transformToken } from "../util/css-variables";
|
||||
import useGlobalCache from "./useGlobalCache";
|
||||
const EMPTY_OVERRIDE = {};
|
||||
|
||||
// Generate different prefix to make user selector break in production env.
|
||||
// This helps developer not to do style override directly on the hash id.
|
||||
const hashPrefix = process.env.NODE_ENV !== 'production' ? 'css-dev-only-do-not-override' : 'css';
|
||||
const tokenKeys = new Map();
|
||||
function recordCleanToken(tokenKey) {
|
||||
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) + 1);
|
||||
}
|
||||
function removeStyleTags(key, instanceId) {
|
||||
if (typeof document !== 'undefined') {
|
||||
const styles = document.querySelectorAll(`style[${ATTR_TOKEN}="${key}"]`);
|
||||
styles.forEach(style => {
|
||||
if (style[CSS_IN_JS_INSTANCE] === instanceId) {
|
||||
style.parentNode?.removeChild(style);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const TOKEN_THRESHOLD = -1;
|
||||
|
||||
// Remove will check current keys first
|
||||
function cleanTokenStyle(tokenKey, instanceId) {
|
||||
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) - 1);
|
||||
const cleanableKeyList = new Set();
|
||||
tokenKeys.forEach((value, key) => {
|
||||
if (value <= 0) cleanableKeyList.add(key);
|
||||
});
|
||||
|
||||
// Should keep tokens under threshold for not to insert style too often
|
||||
if (tokenKeys.size - cleanableKeyList.size > TOKEN_THRESHOLD) {
|
||||
cleanableKeyList.forEach(key => {
|
||||
removeStyleTags(key, instanceId);
|
||||
tokenKeys.delete(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
export const getComputedToken = (originToken, overrideToken, theme, format) => {
|
||||
const derivativeToken = theme.getDerivativeToken(originToken);
|
||||
|
||||
// Merge with override
|
||||
let mergedDerivativeToken = {
|
||||
...derivativeToken,
|
||||
...overrideToken
|
||||
};
|
||||
|
||||
// Format if needed
|
||||
if (format) {
|
||||
mergedDerivativeToken = format(mergedDerivativeToken);
|
||||
}
|
||||
return mergedDerivativeToken;
|
||||
};
|
||||
export const TOKEN_PREFIX = 'token';
|
||||
/**
|
||||
* Cache theme derivative token as global shared one
|
||||
* @param theme Theme entity
|
||||
* @param tokens List of tokens, used for cache. Please do not dynamic generate object directly
|
||||
* @param option Additional config
|
||||
* @returns Call Theme.getDerivativeToken(tokenObject) to get token
|
||||
*/
|
||||
export default function useCacheToken(theme, tokens, option) {
|
||||
const {
|
||||
cache: {
|
||||
instanceId
|
||||
},
|
||||
container,
|
||||
hashPriority
|
||||
} = useContext(StyleContext);
|
||||
const {
|
||||
salt = '',
|
||||
override = EMPTY_OVERRIDE,
|
||||
formatToken,
|
||||
getComputedToken: compute,
|
||||
cssVar,
|
||||
nonce
|
||||
} = option;
|
||||
|
||||
// Basic - We do basic cache here
|
||||
const mergedToken = memoResult(() => Object.assign({}, ...tokens), tokens);
|
||||
const tokenStr = flattenToken(mergedToken);
|
||||
const overrideTokenStr = flattenToken(override);
|
||||
const cssVarStr = flattenToken(cssVar);
|
||||
const cachedToken = useGlobalCache(TOKEN_PREFIX, [salt, theme.id, tokenStr, overrideTokenStr, cssVarStr], () => {
|
||||
const mergedDerivativeToken = compute ? compute(mergedToken, override, theme) : getComputedToken(mergedToken, override, theme, formatToken);
|
||||
const actualToken = {
|
||||
...mergedDerivativeToken
|
||||
};
|
||||
|
||||
// Optimize for `useStyleRegister` performance
|
||||
const mergedSalt = `${salt}_${cssVar.prefix}`;
|
||||
const hashId = hash(mergedSalt);
|
||||
const hashCls = `${hashPrefix}-${hashId}`;
|
||||
actualToken._tokenKey = token2key(actualToken, mergedSalt);
|
||||
|
||||
// Replace token value with css variables
|
||||
const [tokenWithCssVar, cssVarsStr] = transformToken(mergedDerivativeToken, cssVar.key, {
|
||||
prefix: cssVar.prefix,
|
||||
ignore: cssVar.ignore,
|
||||
unitless: cssVar.unitless,
|
||||
preserve: cssVar.preserve,
|
||||
hashPriority,
|
||||
hashCls: cssVar.hashed ? hashCls : undefined
|
||||
});
|
||||
tokenWithCssVar._hashId = hashId;
|
||||
recordCleanToken(cssVar.key);
|
||||
return [tokenWithCssVar, hashCls, actualToken, cssVarsStr, cssVar.key];
|
||||
}, ([,,,, themeKey]) => {
|
||||
// Remove token will remove all related style
|
||||
cleanTokenStyle(themeKey, instanceId);
|
||||
}, ([,,, cssVarsStr, themeKey]) => {
|
||||
if (!cssVarsStr) {
|
||||
return;
|
||||
}
|
||||
let mergedCSSConfig = {
|
||||
mark: ATTR_MARK,
|
||||
prepend: 'queue',
|
||||
attachTo: container,
|
||||
priority: -999
|
||||
};
|
||||
mergedCSSConfig = injectCSPNonce(mergedCSSConfig, nonce);
|
||||
const style = updateCSS(cssVarsStr, hash(`css-var-${themeKey}`), mergedCSSConfig);
|
||||
style[CSS_IN_JS_INSTANCE] = instanceId;
|
||||
|
||||
// Used for `useCacheToken` to remove on batch when token removed
|
||||
style.setAttribute(ATTR_TOKEN, themeKey);
|
||||
});
|
||||
return cachedToken;
|
||||
}
|
||||
export const extract = (cache, effectStyles, options) => {
|
||||
const [,, realToken, styleStr, cssVarKey] = cache;
|
||||
const {
|
||||
plain
|
||||
} = options || {};
|
||||
if (!styleStr) {
|
||||
return null;
|
||||
}
|
||||
const styleId = realToken._tokenKey;
|
||||
const order = -999;
|
||||
|
||||
// ====================== Style ======================
|
||||
// Used for @rc-component/util
|
||||
const sharedAttrs = {
|
||||
'data-rc-order': 'prependQueue',
|
||||
'data-rc-priority': `${order}`
|
||||
};
|
||||
const styleText = toStyleStr(styleStr, cssVarKey, styleId, sharedAttrs, plain);
|
||||
return [order, styleId, styleText];
|
||||
};
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { DependencyList } from 'react';
|
||||
declare const useEffectCleanupRegister: (deps?: DependencyList) => (fn: () => void) => void;
|
||||
export default useEffectCleanupRegister;
|
||||
Generated
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
import { warning } from "@rc-component/util/es/warning";
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// DO NOT register functions in useEffect cleanup function, or functions that registered will never be called.
|
||||
const useEffectCleanupRegister = deps => {
|
||||
const effectCleanups = [];
|
||||
let cleanupFlag = false;
|
||||
function register(fn) {
|
||||
if (cleanupFlag) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
warning(false, '[Ant Design CSS-in-JS] You are registering a cleanup function after unmount, which will not have any effect.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
effectCleanups.push(fn);
|
||||
}
|
||||
useEffect(() => {
|
||||
// Compatible with strict mode
|
||||
cleanupFlag = false;
|
||||
return () => {
|
||||
cleanupFlag = true;
|
||||
if (effectCleanups.length) {
|
||||
effectCleanups.forEach(fn => fn());
|
||||
}
|
||||
};
|
||||
}, deps);
|
||||
return register;
|
||||
};
|
||||
export default useEffectCleanupRegister;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { type KeyType } from '../Cache';
|
||||
export type ExtractStyle<CacheValue> = (cache: CacheValue, effectStyles: Record<string, boolean>, options?: {
|
||||
plain?: boolean;
|
||||
autoPrefix?: boolean;
|
||||
}) => [order: number, styleId: string, style: string] | null;
|
||||
export default function useGlobalCache<CacheType>(prefix: string, keyPath: KeyType[], cacheFn: () => CacheType, onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void, onCacheEffect?: (cachedValue: CacheType) => void): CacheType;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import * as React from 'react';
|
||||
import { useInsertionEffect } from 'react';
|
||||
import { pathKey } from "../Cache";
|
||||
import StyleContext from "../StyleContext";
|
||||
import useHMR from "./useHMR";
|
||||
const effectMap = new Map();
|
||||
export default function useGlobalCache(prefix, keyPath, cacheFn, onCacheRemove,
|
||||
// Add additional effect trigger by `useInsertionEffect`
|
||||
onCacheEffect) {
|
||||
const {
|
||||
cache: globalCache
|
||||
} = React.useContext(StyleContext);
|
||||
const fullPath = [prefix, ...keyPath];
|
||||
const fullPathStr = pathKey(fullPath);
|
||||
const HMRUpdate = useHMR();
|
||||
const buildCache = updater => {
|
||||
globalCache.opUpdate(fullPathStr, prevCache => {
|
||||
const [times = 0, cache] = prevCache || [undefined, undefined];
|
||||
|
||||
// HMR should always ignore cache since developer may change it
|
||||
let tmpCache = cache;
|
||||
if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) {
|
||||
onCacheRemove?.(tmpCache, HMRUpdate);
|
||||
tmpCache = null;
|
||||
}
|
||||
const mergedCache = tmpCache || cacheFn();
|
||||
const data = [times, mergedCache];
|
||||
|
||||
// Call updater if need additional logic
|
||||
return updater ? updater(data) : data;
|
||||
});
|
||||
};
|
||||
|
||||
// Create cache
|
||||
React.useMemo(() => {
|
||||
buildCache();
|
||||
}, /* eslint-disable react-hooks/exhaustive-deps */
|
||||
[fullPathStr]
|
||||
/* eslint-enable */);
|
||||
let cacheEntity = globalCache.opGet(fullPathStr);
|
||||
|
||||
// HMR clean the cache but not trigger `useMemo` again
|
||||
// Let's fallback of this
|
||||
// ref https://github.com/ant-design/cssinjs/issues/127
|
||||
if (process.env.NODE_ENV !== 'production' && !cacheEntity) {
|
||||
buildCache();
|
||||
cacheEntity = globalCache.opGet(fullPathStr);
|
||||
}
|
||||
const cacheContent = cacheEntity[1];
|
||||
|
||||
// Remove if no need anymore
|
||||
useInsertionEffect(() => {
|
||||
buildCache(([times, cache]) => [times + 1, cache]);
|
||||
if (!effectMap.has(fullPathStr)) {
|
||||
onCacheEffect?.(cacheContent);
|
||||
effectMap.set(fullPathStr, true);
|
||||
|
||||
// 微任务清理缓存,可以认为是单次 batch render 中只触发一次 effect
|
||||
Promise.resolve().then(() => {
|
||||
effectMap.delete(fullPathStr);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
globalCache.opUpdate(fullPathStr, prevCache => {
|
||||
const [times = 0, cache] = prevCache || [];
|
||||
const nextCount = times - 1;
|
||||
if (nextCount === 0) {
|
||||
onCacheRemove?.(cache, false);
|
||||
effectMap.delete(fullPathStr);
|
||||
return null;
|
||||
}
|
||||
return [times - 1, cache];
|
||||
});
|
||||
};
|
||||
}, [fullPathStr]);
|
||||
return cacheContent;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare function useProdHMR(): boolean;
|
||||
declare const _default: typeof useProdHMR;
|
||||
export default _default;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
function useProdHMR() {
|
||||
return false;
|
||||
}
|
||||
let webpackHMR = false;
|
||||
function useDevHMR() {
|
||||
return webpackHMR;
|
||||
}
|
||||
export default process.env.NODE_ENV === 'production' ? useProdHMR : useDevHMR;
|
||||
|
||||
// Webpack `module.hot.accept` do not support any deps update trigger
|
||||
// We have to hack handler to force mark as HRM
|
||||
if (process.env.NODE_ENV !== 'production' && typeof module !== 'undefined' && module && module.hot && typeof window !== 'undefined') {
|
||||
// Use `globalThis` first, and `window` for older browsers
|
||||
// const win = globalThis as any;
|
||||
const win = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : null;
|
||||
if (win && typeof win.webpackHotUpdate === 'function') {
|
||||
const originWebpackHotUpdate = win.webpackHotUpdate;
|
||||
win.webpackHotUpdate = (...args) => {
|
||||
webpackHMR = true;
|
||||
setTimeout(() => {
|
||||
webpackHMR = false;
|
||||
}, 0);
|
||||
return originWebpackHotUpdate(...args);
|
||||
};
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import type * as CSS from 'csstype';
|
||||
import type { Theme, Transformer } from '..';
|
||||
import type Keyframes from '../Keyframes';
|
||||
import type { Linter } from '../linters';
|
||||
import type { HashPriority } from '../StyleContext';
|
||||
import type { ExtractStyle } from './useGlobalCache';
|
||||
declare const SKIP_CHECK = "_skip_check_";
|
||||
declare const MULTI_VALUE = "_multi_value_";
|
||||
export interface LayerConfig {
|
||||
name: string;
|
||||
dependencies?: string[];
|
||||
}
|
||||
export type CSSProperties = Omit<CSS.PropertiesFallback<number | string>, 'animationName'> & {
|
||||
animationName?: CSS.PropertiesFallback<number | string>['animationName'] | Keyframes;
|
||||
};
|
||||
export type CSSPropertiesWithMultiValues = {
|
||||
[K in keyof CSSProperties]: CSSProperties[K] | readonly Extract<CSSProperties[K], string>[] | {
|
||||
[SKIP_CHECK]?: boolean;
|
||||
[MULTI_VALUE]?: boolean;
|
||||
value: CSSProperties[K] | CSSProperties[K][];
|
||||
};
|
||||
};
|
||||
export type CSSPseudos = {
|
||||
[K in CSS.Pseudos]?: CSSObject;
|
||||
};
|
||||
type ArrayCSSInterpolation = readonly CSSInterpolation[];
|
||||
export type InterpolationPrimitive = null | undefined | boolean | number | string | CSSObject;
|
||||
export type CSSInterpolation = InterpolationPrimitive | ArrayCSSInterpolation | Keyframes;
|
||||
export type CSSOthersObject = Record<string, CSSInterpolation>;
|
||||
export interface CSSObject extends CSSPropertiesWithMultiValues, CSSPseudos, CSSOthersObject {
|
||||
}
|
||||
export declare function normalizeStyle(styleStr: string, autoPrefix: boolean): string;
|
||||
export interface ParseConfig {
|
||||
hashId?: string;
|
||||
hashPriority?: HashPriority;
|
||||
layer?: LayerConfig;
|
||||
path?: string;
|
||||
transformers?: Transformer[];
|
||||
linters?: Linter[];
|
||||
}
|
||||
export interface ParseInfo {
|
||||
root?: boolean;
|
||||
injectHash?: boolean;
|
||||
parentSelectors: string[];
|
||||
}
|
||||
export declare const parseStyle: (interpolation: CSSInterpolation, config?: ParseConfig, { root, injectHash, parentSelectors }?: ParseInfo) => [parsedStr: string, effectStyle: Record<string, string>];
|
||||
export declare function uniqueHash(path: (string | number)[], styleStr: string): string;
|
||||
export declare const STYLE_PREFIX = "style";
|
||||
type StyleCacheValue = [
|
||||
styleStr: string,
|
||||
styleId: string,
|
||||
effectStyle: Record<string, string>,
|
||||
clientOnly: boolean | undefined,
|
||||
order: number
|
||||
];
|
||||
/**
|
||||
* Register a style to the global style sheet.
|
||||
*/
|
||||
export default function useStyleRegister(info: {
|
||||
theme: Theme<any, any>;
|
||||
token: any;
|
||||
path: string[];
|
||||
hashId?: string;
|
||||
layer?: LayerConfig;
|
||||
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): void;
|
||||
export declare const extract: ExtractStyle<StyleCacheValue>;
|
||||
export {};
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
import hash from '@emotion/hash';
|
||||
import { removeCSS, updateCSS } from "@rc-component/util/es/Dom/dynamicCSS";
|
||||
import * as React from 'react';
|
||||
// @ts-ignore
|
||||
import unitless from '@emotion/unitless';
|
||||
import { compile, middleware, prefixer, serialize, stringify } from 'stylis';
|
||||
import { contentQuotesLinter, hashedAnimationLinter } from "../linters";
|
||||
import StyleContext, { ATTR_CACHE_PATH, ATTR_MARK, CSS_IN_JS_INSTANCE } from "../StyleContext";
|
||||
import { injectCSPNonce, isClientSide, isNonNullable, toStyleStr, where } from "../util";
|
||||
import { CSS_FILE_STYLE, existPath, getStyleAndHash } from "../util/cacheMapUtil";
|
||||
import useGlobalCache from "./useGlobalCache";
|
||||
const SKIP_CHECK = '_skip_check_';
|
||||
const MULTI_VALUE = '_multi_value_';
|
||||
// ============================================================================
|
||||
// == Parser ==
|
||||
// ============================================================================
|
||||
// Preprocessor style content to browser support one
|
||||
export function normalizeStyle(styleStr, autoPrefix) {
|
||||
const serialized = autoPrefix ? serialize(compile(styleStr), middleware([prefixer, stringify])) : serialize(compile(styleStr), stringify);
|
||||
return serialized.replace(/\{%%%\:[^;];}/g, ';');
|
||||
}
|
||||
function isCompoundCSSProperty(value) {
|
||||
return typeof value === 'object' && value && (SKIP_CHECK in value || MULTI_VALUE in value);
|
||||
}
|
||||
|
||||
// 注入 hash 值
|
||||
function injectSelectorHash(key, hashId, hashPriority = 'high') {
|
||||
if (!hashId) {
|
||||
return key;
|
||||
}
|
||||
const hashSelector = where({
|
||||
hashCls: hashId,
|
||||
hashPriority
|
||||
});
|
||||
|
||||
// 注入 hashId
|
||||
const keys = key.split(',').map(k => {
|
||||
const fullPath = k.trim().split(/\s+/);
|
||||
|
||||
// 如果 Selector 第一个是 HTML Element,那我们就插到它的后面。反之,就插到最前面。
|
||||
let firstPath = fullPath[0] || '';
|
||||
const htmlElement = firstPath.match(/^\w+/)?.[0] || '';
|
||||
firstPath = `${htmlElement}${hashSelector}${firstPath.slice(htmlElement.length)}`;
|
||||
return [firstPath, ...fullPath.slice(1)].join(' ');
|
||||
});
|
||||
return keys.join(',');
|
||||
}
|
||||
// Parse CSSObject to style content
|
||||
export const parseStyle = (interpolation, config = {}, {
|
||||
root,
|
||||
injectHash,
|
||||
parentSelectors
|
||||
} = {
|
||||
root: true,
|
||||
parentSelectors: []
|
||||
}) => {
|
||||
const {
|
||||
hashId,
|
||||
layer,
|
||||
path,
|
||||
hashPriority,
|
||||
transformers = [],
|
||||
linters = []
|
||||
} = config;
|
||||
let styleStr = '';
|
||||
let effectStyle = {};
|
||||
function parseKeyframes(keyframes) {
|
||||
const animationName = keyframes.getName(hashId);
|
||||
if (!effectStyle[animationName]) {
|
||||
const [parsedStr] = parseStyle(keyframes.style, config, {
|
||||
root: false,
|
||||
parentSelectors
|
||||
});
|
||||
effectStyle[animationName] = `@keyframes ${keyframes.getName(hashId)}${parsedStr}`;
|
||||
}
|
||||
}
|
||||
function flattenList(list, fullList = []) {
|
||||
list.forEach(item => {
|
||||
if (Array.isArray(item)) {
|
||||
flattenList(item, fullList);
|
||||
} else if (item) {
|
||||
fullList.push(item);
|
||||
}
|
||||
});
|
||||
return fullList;
|
||||
}
|
||||
const flattenStyleList = flattenList(Array.isArray(interpolation) ? interpolation : [interpolation]);
|
||||
flattenStyleList.forEach(originStyle => {
|
||||
// Only root level can use raw string
|
||||
const style = typeof originStyle === 'string' && !root ? {} : originStyle;
|
||||
if (typeof style === 'string') {
|
||||
styleStr += `${style}\n`;
|
||||
} else if (style._keyframe) {
|
||||
// Keyframe
|
||||
parseKeyframes(style);
|
||||
} else {
|
||||
const mergedStyle = transformers.reduce((prev, trans) => trans?.visit?.(prev) || prev, style);
|
||||
|
||||
// Normal CSSObject
|
||||
Object.keys(mergedStyle).forEach(key => {
|
||||
const value = mergedStyle[key];
|
||||
if (typeof value === 'object' && value && (key !== 'animationName' || !value._keyframe) && !isCompoundCSSProperty(value)) {
|
||||
let subInjectHash = false;
|
||||
|
||||
// 当成嵌套对象来处理
|
||||
let mergedKey = key.trim();
|
||||
// Whether treat child as root. In most case it is false.
|
||||
let nextRoot = false;
|
||||
|
||||
// 拆分多个选择器
|
||||
if ((root || injectHash) && hashId) {
|
||||
if (mergedKey.startsWith('@')) {
|
||||
// 略过媒体查询,交给子节点继续插入 hashId
|
||||
subInjectHash = true;
|
||||
} else if (mergedKey === '&') {
|
||||
// 抹掉 root selector 上的单个 &
|
||||
mergedKey = injectSelectorHash('', hashId, hashPriority);
|
||||
} else {
|
||||
// 注入 hashId
|
||||
mergedKey = injectSelectorHash(key, hashId, hashPriority);
|
||||
}
|
||||
} else if (root && !hashId && (mergedKey === '&' || mergedKey === '')) {
|
||||
// In case of `{ '&': { a: { color: 'red' } } }` or `{ '': { a: { color: 'red' } } }` without hashId,
|
||||
// we will get `&{a:{color:red;}}` or `{a:{color:red;}}` string for stylis to compile.
|
||||
// But it does not conform to stylis syntax,
|
||||
// and finally we will get `{color:red;}` as css, which is wrong.
|
||||
// So we need to remove key in root, and treat child `{ a: { color: 'red' } }` as root.
|
||||
mergedKey = '';
|
||||
nextRoot = true;
|
||||
}
|
||||
const [parsedStr, childEffectStyle] = parseStyle(value, config, {
|
||||
root: nextRoot,
|
||||
injectHash: subInjectHash,
|
||||
parentSelectors: [...parentSelectors, mergedKey]
|
||||
});
|
||||
effectStyle = {
|
||||
...effectStyle,
|
||||
...childEffectStyle
|
||||
};
|
||||
styleStr += `${mergedKey}${parsedStr}`;
|
||||
} else {
|
||||
function appendStyle(cssKey, cssValue) {
|
||||
if (process.env.NODE_ENV !== 'production' && (typeof value !== 'object' || !value?.[SKIP_CHECK])) {
|
||||
[contentQuotesLinter, hashedAnimationLinter, ...linters].forEach(linter => linter(cssKey, cssValue, {
|
||||
path,
|
||||
hashId,
|
||||
parentSelectors
|
||||
}));
|
||||
}
|
||||
|
||||
// 如果是样式则直接插入
|
||||
const styleName = cssKey.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
||||
|
||||
// Auto suffix with px
|
||||
let formatValue = cssValue;
|
||||
if (!unitless[cssKey] && typeof formatValue === 'number' && formatValue !== 0) {
|
||||
formatValue = `${formatValue}px`;
|
||||
}
|
||||
|
||||
// handle animationName & Keyframe value
|
||||
if (cssKey === 'animationName' && cssValue?._keyframe) {
|
||||
parseKeyframes(cssValue);
|
||||
formatValue = cssValue.getName(hashId);
|
||||
}
|
||||
styleStr += `${styleName}:${formatValue};`;
|
||||
}
|
||||
const actualValue = value?.value ?? value;
|
||||
if (typeof value === 'object' && value?.[MULTI_VALUE] && Array.isArray(actualValue)) {
|
||||
actualValue.forEach(item => {
|
||||
appendStyle(key, item);
|
||||
});
|
||||
} else {
|
||||
if (isNonNullable(actualValue)) {
|
||||
appendStyle(key, actualValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!root) {
|
||||
styleStr = `{${styleStr}}`;
|
||||
} else if (layer) {
|
||||
// fixme: https://github.com/thysultan/stylis/pull/339
|
||||
if (styleStr) {
|
||||
styleStr = `@layer ${layer.name} {${styleStr}}`;
|
||||
}
|
||||
if (layer.dependencies) {
|
||||
effectStyle[`@layer ${layer.name}`] = layer.dependencies.map(deps => `@layer ${deps}, ${layer.name};`).join('\n');
|
||||
}
|
||||
}
|
||||
return [styleStr, effectStyle];
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// == Register ==
|
||||
// ============================================================================
|
||||
export function uniqueHash(path, styleStr) {
|
||||
return hash(`${path.join('%')}${styleStr}`);
|
||||
}
|
||||
export const STYLE_PREFIX = 'style';
|
||||
/**
|
||||
* Register a style to the global style sheet.
|
||||
*/
|
||||
export default function useStyleRegister(info, styleFn) {
|
||||
const {
|
||||
path,
|
||||
hashId,
|
||||
layer,
|
||||
nonce,
|
||||
clientOnly,
|
||||
order = 0
|
||||
} = info;
|
||||
const {
|
||||
mock,
|
||||
hashPriority,
|
||||
container,
|
||||
transformers,
|
||||
linters,
|
||||
cache,
|
||||
layer: enableLayer,
|
||||
autoPrefix
|
||||
} = React.useContext(StyleContext);
|
||||
const fullPath = [hashId || ''];
|
||||
if (enableLayer) {
|
||||
fullPath.push('layer');
|
||||
}
|
||||
fullPath.push(...path);
|
||||
|
||||
// Check if need insert style
|
||||
let isMergedClientSide = isClientSide;
|
||||
if (process.env.NODE_ENV !== 'production' && mock !== undefined) {
|
||||
isMergedClientSide = mock === 'client';
|
||||
}
|
||||
useGlobalCache(STYLE_PREFIX, fullPath,
|
||||
// Create cache if needed
|
||||
() => {
|
||||
const cachePath = fullPath.join('|');
|
||||
|
||||
// Get style from SSR inline style directly
|
||||
if (existPath(cachePath)) {
|
||||
const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath);
|
||||
if (inlineCacheStyleStr) {
|
||||
return [inlineCacheStyleStr, styleHash, {}, clientOnly, order];
|
||||
}
|
||||
}
|
||||
|
||||
// Generate style
|
||||
const styleObj = styleFn();
|
||||
const [parsedStyle, effectStyle] = parseStyle(styleObj, {
|
||||
hashId,
|
||||
hashPriority,
|
||||
layer: enableLayer ? layer : undefined,
|
||||
path: path.join('-'),
|
||||
transformers,
|
||||
linters
|
||||
});
|
||||
const styleStr = normalizeStyle(parsedStyle, autoPrefix || false);
|
||||
const styleId = uniqueHash(fullPath, styleStr);
|
||||
return [styleStr, styleId, effectStyle, clientOnly, order];
|
||||
},
|
||||
// Remove cache if no need
|
||||
(cacheValue, fromHMR) => {
|
||||
const [, styleId] = cacheValue;
|
||||
if (fromHMR && isClientSide) {
|
||||
removeCSS(styleId, {
|
||||
mark: ATTR_MARK,
|
||||
attachTo: container
|
||||
});
|
||||
}
|
||||
},
|
||||
// Effect: Inject style here
|
||||
cacheValue => {
|
||||
const [styleStr, styleId, effectStyle,, priority] = cacheValue;
|
||||
if (isMergedClientSide && styleStr !== CSS_FILE_STYLE) {
|
||||
let mergedCSSConfig = {
|
||||
mark: ATTR_MARK,
|
||||
prepend: enableLayer ? false : 'queue',
|
||||
attachTo: container,
|
||||
priority
|
||||
};
|
||||
mergedCSSConfig = injectCSPNonce(mergedCSSConfig, nonce);
|
||||
|
||||
// ================= Split Effect Style =================
|
||||
// We will split effectStyle here since @layer should be at the top level
|
||||
const effectLayerKeys = [];
|
||||
const effectRestKeys = [];
|
||||
Object.keys(effectStyle).forEach(key => {
|
||||
if (key.startsWith('@layer')) {
|
||||
effectLayerKeys.push(key);
|
||||
} else {
|
||||
effectRestKeys.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
// ================= Inject Layer Style =================
|
||||
// Inject layer style
|
||||
effectLayerKeys.forEach(effectKey => {
|
||||
updateCSS(normalizeStyle(effectStyle[effectKey], autoPrefix || false), `_layer-${effectKey}`, {
|
||||
...mergedCSSConfig,
|
||||
prepend: true
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Inject Style ====================
|
||||
// Inject style
|
||||
const style = updateCSS(styleStr, styleId, mergedCSSConfig);
|
||||
style[CSS_IN_JS_INSTANCE] = cache.instanceId;
|
||||
|
||||
// Debug usage. Dev only
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
style.setAttribute(ATTR_CACHE_PATH, fullPath.join('|'));
|
||||
}
|
||||
|
||||
// ================ Inject Effect Style =================
|
||||
// Inject client side effect style
|
||||
effectRestKeys.forEach(effectKey => {
|
||||
updateCSS(normalizeStyle(effectStyle[effectKey], autoPrefix || false), `_effect-${effectKey}`, mergedCSSConfig);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
export const extract = (cache, effectStyles, options) => {
|
||||
const [styleStr, styleId, effectStyle, clientOnly, order] = cache;
|
||||
const {
|
||||
plain,
|
||||
autoPrefix
|
||||
} = options || {};
|
||||
|
||||
// Skip client only style
|
||||
if (clientOnly) {
|
||||
return null;
|
||||
}
|
||||
let keyStyleText = styleStr;
|
||||
|
||||
// ====================== Share ======================
|
||||
// Used for @rc-component/util
|
||||
const sharedAttrs = {
|
||||
'data-rc-order': 'prependQueue',
|
||||
'data-rc-priority': `${order}`
|
||||
};
|
||||
|
||||
// ====================== Style ======================
|
||||
keyStyleText = toStyleStr(styleStr, undefined, styleId, sharedAttrs, plain);
|
||||
|
||||
// =============== Create effect style ===============
|
||||
if (effectStyle) {
|
||||
Object.keys(effectStyle).forEach(effectKey => {
|
||||
// Effect style can be reused
|
||||
if (!effectStyles[effectKey]) {
|
||||
effectStyles[effectKey] = true;
|
||||
const effectStyleStr = normalizeStyle(effectStyle[effectKey], autoPrefix || false);
|
||||
const effectStyleHTML = toStyleStr(effectStyleStr, undefined, `_effect-${effectKey}`, sharedAttrs, plain);
|
||||
if (effectKey.startsWith('@layer')) {
|
||||
keyStyleText = effectStyleHTML + keyStyleText;
|
||||
} else {
|
||||
keyStyleText += effectStyleHTML;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return [order, styleId, keyStyleText];
|
||||
};
|
||||
Reference in New Issue
Block a user