chore: update cssinjs

pull/6941/head
tangjinzhou 2023-09-09 23:31:44 +08:00
parent 2b8f2adcbf
commit d0f7c34497
9 changed files with 405 additions and 82 deletions

View File

@ -1,16 +1,20 @@
export type KeyType = string | number; export type KeyType = string | number;
type ValueType = [number, any]; // [times, realValue] type ValueType = [number, any]; // [times, realValue]
const SPLIT = '%';
class Entity { class Entity {
instanceId: string;
constructor(instanceId: string) {
this.instanceId = instanceId;
}
/** @private Internal cache map. Do not access this directly */ /** @private Internal cache map. Do not access this directly */
cache = new Map<string, ValueType>(); cache = new Map<string, ValueType>();
get(keys: KeyType[] | string): ValueType | null { get(keys: KeyType[] | string): ValueType | null {
return this.cache.get(Array.isArray(keys) ? keys.join('%') : keys) || null; return this.cache.get(Array.isArray(keys) ? keys.join(SPLIT) : keys) || null;
} }
update(keys: KeyType[] | string, valueFn: (origin: ValueType | null) => ValueType | null) { update(keys: KeyType[] | string, valueFn: (origin: ValueType | null) => ValueType | null) {
const path = Array.isArray(keys) ? keys.join('%') : keys; const path = Array.isArray(keys) ? keys.join(SPLIT) : keys;
const prevValue = this.cache.get(path)!; const prevValue = this.cache.get(path)!;
const nextValue = valueFn(prevValue); const nextValue = valueFn(prevValue);

View File

@ -7,20 +7,22 @@ import { arrayType, booleanType, objectType, someType, stringType, withInstall }
import initDefaultProps from '../props-util/initDefaultProps'; import initDefaultProps from '../props-util/initDefaultProps';
export const ATTR_TOKEN = 'data-token-hash'; export const ATTR_TOKEN = 'data-token-hash';
export const ATTR_MARK = 'data-css-hash'; export const ATTR_MARK = 'data-css-hash';
export const ATTR_DEV_CACHE_PATH = 'data-dev-cache-path'; export const ATTR_CACHE_PATH = 'data-cache-path';
// Mark css-in-js instance in style element // Mark css-in-js instance in style element
export const CSS_IN_JS_INSTANCE = '__cssinjs_instance__'; export const CSS_IN_JS_INSTANCE = '__cssinjs_instance__';
export const CSS_IN_JS_INSTANCE_ID = Math.random().toString(12).slice(2);
export function createCache() { export function createCache() {
const cssinjsInstanceId = Math.random().toString(12).slice(2);
// Tricky SSR: Move all inline style to the head.
// PS: We do not recommend tricky mode.
if (typeof document !== 'undefined' && document.head && document.body) { if (typeof document !== 'undefined' && document.head && document.body) {
const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || []; const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || [];
const { firstChild } = document.head; const { firstChild } = document.head;
Array.from(styles).forEach(style => { Array.from(styles).forEach(style => {
(style as any)[CSS_IN_JS_INSTANCE] = (style as any)[CSS_IN_JS_INSTANCE] = (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId;
(style as any)[CSS_IN_JS_INSTANCE] || CSS_IN_JS_INSTANCE_ID;
// Not force move if no head // Not force move if no head
document.head.insertBefore(style, firstChild); document.head.insertBefore(style, firstChild);
@ -31,7 +33,7 @@ export function createCache() {
Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach(style => { Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach(style => {
const hash = style.getAttribute(ATTR_MARK)!; const hash = style.getAttribute(ATTR_MARK)!;
if (styleHash[hash]) { if (styleHash[hash]) {
if ((style as any)[CSS_IN_JS_INSTANCE] === CSS_IN_JS_INSTANCE_ID) { if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) {
style.parentNode?.removeChild(style); style.parentNode?.removeChild(style);
} }
} else { } else {
@ -40,7 +42,7 @@ export function createCache() {
}); });
} }
return new CacheEntity(); return new CacheEntity(cssinjsInstanceId);
} }
export type HashPriority = 'low' | 'high'; export type HashPriority = 'low' | 'high';

View File

@ -1,5 +1,5 @@
import hash from '@emotion/hash'; import hash from '@emotion/hash';
import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, CSS_IN_JS_INSTANCE_ID } from '../StyleContext'; import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext';
import type Theme from '../theme/Theme'; import type Theme from '../theme/Theme';
import useGlobalCache from './useGlobalCache'; import useGlobalCache from './useGlobalCache';
import { flattenToken, token2key } from '../util'; import { flattenToken, token2key } from '../util';
@ -12,7 +12,7 @@ const EMPTY_OVERRIDE = {};
// This helps developer not to do style override directly on the hash id. // 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 hashPrefix = process.env.NODE_ENV !== 'production' ? 'css-dev-only-do-not-override' : 'css';
export interface Option<DerivativeToken> { export interface Option<DerivativeToken, DesignToken> {
/** /**
* Generate token with salt. * Generate token with salt.
* This is used to generate different hashId even same derivative token for different version. * This is used to generate different hashId even same derivative token for different version.
@ -30,6 +30,18 @@ export interface Option<DerivativeToken> {
* It's ok to useMemo outside but this has better cache strategy. * It's ok to useMemo outside but this has better cache strategy.
*/ */
formatToken?: (mergedToken: any) => DerivativeToken; 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;
} }
const tokenKeys = new Map<string, number>(); const tokenKeys = new Map<string, number>();
@ -37,20 +49,22 @@ function recordCleanToken(tokenKey: string) {
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) + 1); tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) + 1);
} }
function removeStyleTags(key: string) { function removeStyleTags(key: string, instanceId: string) {
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
const styles = document.querySelectorAll(`style[${ATTR_TOKEN}="${key}"]`); const styles = document.querySelectorAll(`style[${ATTR_TOKEN}="${key}"]`);
styles.forEach(style => { styles.forEach(style => {
if ((style as any)[CSS_IN_JS_INSTANCE] === CSS_IN_JS_INSTANCE_ID) { if ((style as any)[CSS_IN_JS_INSTANCE] === instanceId) {
style.parentNode?.removeChild(style); style.parentNode?.removeChild(style);
} }
}); });
} }
} }
const TOKEN_THRESHOLD = 0;
// Remove will check current keys first // Remove will check current keys first
function cleanTokenStyle(tokenKey: string) { function cleanTokenStyle(tokenKey: string, instanceId: string) {
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) - 1); tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) - 1);
const tokenKeyList = Array.from(tokenKeys.keys()); const tokenKeyList = Array.from(tokenKeys.keys());
@ -60,14 +74,37 @@ function cleanTokenStyle(tokenKey: string) {
return count <= 0; return count <= 0;
}); });
if (cleanableKeyList.length < tokenKeyList.length) { // Should keep tokens under threshold for not to insert style too often
if (tokenKeyList.length - cleanableKeyList.length > TOKEN_THRESHOLD) {
cleanableKeyList.forEach(key => { cleanableKeyList.forEach(key => {
removeStyleTags(key); removeStyleTags(key, instanceId);
tokenKeys.delete(key); tokenKeys.delete(key);
}); });
} }
} }
export const getComputedToken = <DerivativeToken = object, DesignToken = DerivativeToken>(
originToken: DesignToken,
overrideToken: object,
theme: Theme<any, any>,
format?: (token: DesignToken) => DerivativeToken,
) => {
const derivativeToken = theme.getDerivativeToken(originToken);
// Merge with override
let mergedDerivativeToken = {
...derivativeToken,
...overrideToken,
};
// Format if needed
if (format) {
mergedDerivativeToken = format(mergedDerivativeToken);
}
return mergedDerivativeToken;
};
/** /**
* Cache theme derivative token as global shared one * Cache theme derivative token as global shared one
* @param theme Theme entity * @param theme Theme entity
@ -78,8 +115,10 @@ function cleanTokenStyle(tokenKey: string) {
export default function useCacheToken<DerivativeToken = object, DesignToken = DerivativeToken>( export default function useCacheToken<DerivativeToken = object, DesignToken = DerivativeToken>(
theme: Ref<Theme<any, any>>, theme: Ref<Theme<any, any>>,
tokens: Ref<Partial<DesignToken>[]>, tokens: Ref<Partial<DesignToken>[]>,
option: Ref<Option<DerivativeToken>> = ref({}), option: Ref<Option<DerivativeToken, DesignToken>> = ref({}),
) { ) {
const style = useStyleInject();
// Basic - We do basic cache here // Basic - We do basic cache here
const mergedToken = computed(() => Object.assign({}, ...tokens.value)); const mergedToken = computed(() => Object.assign({}, ...tokens.value));
const tokenStr = computed(() => flattenToken(mergedToken.value)); const tokenStr = computed(() => flattenToken(mergedToken.value));
@ -94,19 +133,15 @@ export default function useCacheToken<DerivativeToken = object, DesignToken = De
overrideTokenStr.value, overrideTokenStr.value,
]), ]),
() => { () => {
const { salt = '', override = EMPTY_OVERRIDE, formatToken } = option.value; const {
const derivativeToken = theme.value.getDerivativeToken(mergedToken.value); salt = '',
override = EMPTY_OVERRIDE,
// Merge with override formatToken,
let mergedDerivativeToken = { getComputedToken: compute,
...derivativeToken, } = option.value;
...override, const mergedDerivativeToken = compute
}; ? compute(mergedToken.value, override, theme.value)
: getComputedToken(mergedToken.value, override, theme.value, formatToken);
// Format if needed
if (formatToken) {
mergedDerivativeToken = formatToken(mergedDerivativeToken);
}
// Optimize for `useStyleRegister` performance // Optimize for `useStyleRegister` performance
const tokenKey = token2key(mergedDerivativeToken, salt); const tokenKey = token2key(mergedDerivativeToken, salt);
@ -120,7 +155,7 @@ export default function useCacheToken<DerivativeToken = object, DesignToken = De
}, },
cache => { cache => {
// Remove token will remove all related style // Remove token will remove all related style
cleanTokenStyle(cache[0]._tokenKey); cleanTokenStyle(cache[0]._tokenKey, style.value?.cache.instanceId);
}, },
); );

View File

@ -16,7 +16,8 @@ if (
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'production' &&
typeof module !== 'undefined' && typeof module !== 'undefined' &&
module && module &&
(module as any).hot (module as any).hot &&
typeof window !== 'undefined'
) { ) {
const win = window as any; const win = window as any;
if (typeof win.webpackHotUpdate === 'function') { if (typeof win.webpackHotUpdate === 'function') {

View File

@ -0,0 +1,91 @@
import canUseDom from '../../../../_util/canUseDom';
import { ATTR_MARK } from '../../StyleContext';
export const ATTR_CACHE_MAP = 'data-ant-cssinjs-cache-path';
/**
* This marks style from the css file.
* Which means not exist in `<style />` tag.
*/
export const CSS_FILE_STYLE = '_FILE_STYLE__';
export function serialize(cachePathMap: Record<string, string>) {
return Object.keys(cachePathMap)
.map(path => {
const hash = cachePathMap[path];
return `${path}:${hash}`;
})
.join(';');
}
let cachePathMap: Record<string, string>;
let fromCSSFile = true;
/**
* @private Test usage only. Can save remove if no need.
*/
export function reset(mockCache?: Record<string, string>, fromFile = true) {
cachePathMap = mockCache!;
fromCSSFile = fromFile;
}
export function prepare() {
if (!cachePathMap) {
cachePathMap = {};
if (canUseDom()) {
const div = document.createElement('div');
div.className = ATTR_CACHE_MAP;
div.style.position = 'fixed';
div.style.visibility = 'hidden';
div.style.top = '-9999px';
document.body.appendChild(div);
let content = getComputedStyle(div).content || '';
content = content.replace(/^"/, '').replace(/"$/, '');
// Fill data
content.split(';').forEach(item => {
const [path, hash] = item.split(':');
cachePathMap[path] = hash;
});
// Remove inline record style
const inlineMapStyle = document.querySelector(`style[${ATTR_CACHE_MAP}]`);
if (inlineMapStyle) {
fromCSSFile = false;
inlineMapStyle.parentNode?.removeChild(inlineMapStyle);
}
document.body.removeChild(div);
}
}
}
export function existPath(path: string) {
prepare();
return !!cachePathMap[path];
}
export function getStyleAndHash(path: string): [style: string | null, hash: string] {
const hash = cachePathMap[path];
let styleStr: string | null = null;
if (hash && canUseDom()) {
if (fromCSSFile) {
styleStr = CSS_FILE_STYLE;
} else {
const style = document.querySelector(`style[${ATTR_MARK}="${cachePathMap[path]}"]`);
if (style) {
styleStr = style.innerHTML;
} else {
// Clean up since not exist anymore
delete cachePathMap[path];
}
}
}
return [styleStr, hash];
}

View File

@ -3,27 +3,33 @@ import type * as CSS from 'csstype';
// @ts-ignore // @ts-ignore
import unitless from '@emotion/unitless'; import unitless from '@emotion/unitless';
import { compile, serialize, stringify } from 'stylis'; import { compile, serialize, stringify } from 'stylis';
import type { Theme, Transformer } from '..'; import type { Theme, Transformer } from '../..';
import type Cache from '../Cache'; import type Cache from '../../Cache';
import type Keyframes from '../Keyframes'; import type Keyframes from '../../Keyframes';
import type { Linter } from '../linters'; import type { Linter } from '../../linters';
import { contentQuotesLinter, hashedAnimationLinter } from '../linters'; import { contentQuotesLinter, hashedAnimationLinter } from '../../linters';
import type { HashPriority } from '../StyleContext'; import type { HashPriority } from '../../StyleContext';
import { import {
useStyleInject, useStyleInject,
ATTR_DEV_CACHE_PATH, ATTR_CACHE_PATH,
ATTR_MARK, ATTR_MARK,
ATTR_TOKEN, ATTR_TOKEN,
CSS_IN_JS_INSTANCE, CSS_IN_JS_INSTANCE,
CSS_IN_JS_INSTANCE_ID, } from '../../StyleContext';
} from '../StyleContext'; import { supportLayer } from '../../util';
import { supportLayer } from '../util'; import useGlobalCache from '../useGlobalCache';
import useGlobalCache from './useGlobalCache'; import { removeCSS, updateCSS } from '../../../../vc-util/Dom/dynamicCSS';
import canUseDom from '../../canUseDom';
import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { computed } from 'vue';
import type { VueNode } from '../../type'; import type { VueNode } from '../../../type';
import canUseDom from '../../../../_util/canUseDom';
import {
ATTR_CACHE_MAP,
existPath,
getStyleAndHash,
serialize as serializeCacheMap,
} from './cacheMapUtil';
const isClientSide = canUseDom(); const isClientSide = canUseDom();
@ -60,7 +66,7 @@ export interface CSSObject extends CSSPropertiesWithMultiValues, CSSPseudos, CSS
// == Parser == // == Parser ==
// ============================================================================ // ============================================================================
// Preprocessor style content to browser support one // Preprocessor style content to browser support one
export function normalizeStyle(styleStr: string) { export function normalizeStyle(styleStr: string): string {
const serialized = serialize(compile(styleStr), stringify); const serialized = serialize(compile(styleStr), stringify);
return serialized.replace(/\{%%%\:[^;];}/g, ';'); return serialized.replace(/\{%%%\:[^;];}/g, ';');
} }
@ -307,6 +313,14 @@ export default function useStyleRegister(
path: string[]; path: string[];
hashId?: string; hashId?: string;
layer?: string; 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, styleFn: () => CSSInterpolation,
) { ) {
@ -323,14 +337,32 @@ export default function useStyleRegister(
} }
// const [cacheStyle[0], cacheStyle[1], cacheStyle[2]] // const [cacheStyle[0], cacheStyle[1], cacheStyle[2]]
useGlobalCache( useGlobalCache<
[
styleStr: string,
tokenKey: string,
styleId: string,
effectStyle: Record<string, string>,
clientOnly: boolean | undefined,
order: number,
]
>(
'style', 'style',
fullPath, fullPath,
// Create cache if needed // Create cache if needed
() => { () => {
const { path, hashId, layer, nonce, clientOnly, order = 0 } = info.value;
const cachePath = fullPath.value.join('|');
// Get style from SSR inline style directly
if (existPath(cachePath)) {
const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath);
if (inlineCacheStyleStr) {
return [inlineCacheStyleStr, tokenKey.value, styleHash, {}, clientOnly, order];
}
}
const styleObj = styleFn(); const styleObj = styleFn();
const { hashPriority, container, transformers, linters } = styleContext.value; const { hashPriority, container, transformers, linters, cache } = styleContext.value;
const { path, hashId, layer } = info.value;
const [parsedStyle, effectStyle] = parseStyle(styleObj, { const [parsedStyle, effectStyle] = parseStyle(styleObj, {
hashId, hashId,
hashPriority, hashPriority,
@ -343,20 +375,29 @@ export default function useStyleRegister(
const styleId = uniqueHash(fullPath.value, styleStr); const styleId = uniqueHash(fullPath.value, styleStr);
if (isMergedClientSide) { if (isMergedClientSide) {
const style = updateCSS(styleStr, styleId, { const mergedCSSConfig: Parameters<typeof updateCSS>[2] = {
mark: ATTR_MARK, mark: ATTR_MARK,
prepend: 'queue', prepend: 'queue',
attachTo: container, attachTo: container,
}); priority: order,
};
(style as any)[CSS_IN_JS_INSTANCE] = CSS_IN_JS_INSTANCE_ID; const nonceStr = typeof nonce === 'function' ? nonce() : nonce;
if (nonceStr) {
mergedCSSConfig.csp = { nonce: nonceStr };
}
const style = updateCSS(styleStr, styleId, mergedCSSConfig);
(style as any)[CSS_IN_JS_INSTANCE] = cache.instanceId;
// Used for `useCacheToken` to remove on batch when token removed // Used for `useCacheToken` to remove on batch when token removed
style.setAttribute(ATTR_TOKEN, tokenKey.value); style.setAttribute(ATTR_TOKEN, tokenKey.value);
// Dev usage to find which cache path made this easily // Dev usage to find which cache path made this easily
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
style.setAttribute(ATTR_DEV_CACHE_PATH, fullPath.value.join('|')); style.setAttribute(ATTR_CACHE_PATH, fullPath.value.join('|'));
} }
// Inject client side effect style // Inject client side effect style
@ -374,7 +415,7 @@ export default function useStyleRegister(
}); });
} }
return [styleStr, tokenKey.value, styleId]; return [styleStr, tokenKey.value, styleId, effectStyle, clientOnly, order];
}, },
// Remove cache if no need // Remove cache if no need
([, , styleId], fromHMR) => { ([, , styleId], fromHMR) => {
@ -414,20 +455,112 @@ export default function useStyleRegister(
// == SSR == // == SSR ==
// ============================================================================ // ============================================================================
export function extractStyle(cache: Cache, plain = false) { export function extractStyle(cache: Cache, plain = false) {
// prefix with `style` is used for `useStyleRegister` to cache style context const matchPrefix = `style%`;
const styleKeys = Array.from(cache.cache.keys()).filter(key => key.startsWith('style%'));
// const tokenStyles: Record<string, string[]> = {}; // prefix with `style` is used for `useStyleRegister` to cache style context
const styleKeys = Array.from(cache.cache.keys()).filter(key => key.startsWith(matchPrefix));
// Common effect styles like animation
const effectStyles: Record<string, boolean> = {};
// Mapping of cachePath to style hash
const cachePathMap: Record<string, string> = {};
let styleText = ''; let styleText = '';
styleKeys.forEach(key => { function toStyleStr(
const [styleStr, tokenKey, styleId]: [string, string, string] = cache.cache.get(key)![1]; style: string,
tokenKey?: string,
styleId?: string,
customizeAttrs: Record<string, string> = {},
) {
const attrs: Record<string, string | undefined> = {
...customizeAttrs,
[ATTR_TOKEN]: tokenKey,
[ATTR_MARK]: styleId,
};
styleText += plain const attrStr = Object.keys(attrs)
? styleStr .map(attr => {
: `<style ${ATTR_TOKEN}="${tokenKey}" ${ATTR_MARK}="${styleId}">${styleStr}</style>`; const val = attrs[attr];
return val ? `${attr}="${val}"` : null;
})
.filter(v => v)
.join(' ');
return plain ? style : `<style ${attrStr}>${style}</style>`;
}
// ====================== Fill Style ======================
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<string, string>,
boolean,
number,
] = cache.cache.get(key)![1];
// Skip client only style
if (clientOnly) {
return null! as OrderStyle;
}
// ====================== Style ======================
// Used for vc-util
const sharedAttrs = {
'data-vc-order': 'prependQueue',
'data-vc-priority': `${order}`,
};
let keyStyleText = toStyleStr(styleStr, tokenKey, styleId, sharedAttrs);
// 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(
`.${ATTR_CACHE_MAP}{content:"${serializeCacheMap(cachePathMap)}";}`,
undefined,
undefined,
{
[ATTR_CACHE_MAP]: ATTR_CACHE_MAP,
},
);
return styleText; return styleText;
} }

View File

@ -11,6 +11,8 @@ import { createTheme, Theme } from './theme';
import type { Transformer } from './transformers/interface'; import type { Transformer } from './transformers/interface';
import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties'; import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties';
import px2remTransformer from './transformers/px2rem'; import px2remTransformer from './transformers/px2rem';
import { supportLogicProps, supportWhere } from './util';
const cssinjs = { const cssinjs = {
Theme, Theme,
createTheme, createTheme,
@ -68,4 +70,8 @@ export type {
StyleProviderProps, StyleProviderProps,
}; };
export const _experimental = {
supportModernCSS: () => supportWhere() && supportLogicProps(),
};
export default cssinjs; export default cssinjs;

View File

@ -2,17 +2,30 @@ import hash from '@emotion/hash';
import { removeCSS, updateCSS } from '../../vc-util/Dom/dynamicCSS'; import { removeCSS, updateCSS } from '../../vc-util/Dom/dynamicCSS';
import canUseDom from '../canUseDom'; import canUseDom from '../canUseDom';
import { Theme } from './theme';
// Create a cache here to avoid always loop generate
const flattenTokenCache = new WeakMap<any, string>();
export function flattenToken(token: any) { export function flattenToken(token: any) {
let str = ''; let str = flattenTokenCache.get(token) || '';
if (!str) {
Object.keys(token).forEach(key => { Object.keys(token).forEach(key => {
const value = token[key]; const value = token[key];
str += key; str += key;
if (value && typeof value === 'object') { if (value instanceof Theme) {
str += value.id;
} else if (value && typeof value === 'object') {
str += flattenToken(value); str += flattenToken(value);
} else { } else {
str += value; str += value;
} }
}); });
// Put in cache
flattenTokenCache.set(token, str);
}
return str; return str;
} }
@ -23,12 +36,18 @@ export function token2key(token: any, salt: string): string {
return hash(`${salt}_${flattenToken(token)}`); return hash(`${salt}_${flattenToken(token)}`);
} }
const layerKey = `layer-${Date.now()}-${Math.random()}`.replace(/\./g, ''); const randomSelectorKey = `random-${Date.now()}-${Math.random()}`.replace(/\./g, '');
const layerWidth = '903px';
function supportSelector(styleStr: string, handleElement?: (ele: HTMLElement) => void): boolean { // Magic `content` for detect selector support
const checkContent = '_bAmBoO_';
function supportSelector(
styleStr: string,
handleElement: (ele: HTMLElement) => void,
supportCheck?: (ele: HTMLElement) => boolean,
): boolean {
if (canUseDom()) { if (canUseDom()) {
updateCSS(styleStr, layerKey); updateCSS(styleStr, randomSelectorKey);
const ele = document.createElement('div'); const ele = document.createElement('div');
ele.style.position = 'fixed'; ele.style.position = 'fixed';
@ -42,10 +61,12 @@ function supportSelector(styleStr: string, handleElement?: (ele: HTMLElement) =>
ele.style.zIndex = '9999999'; ele.style.zIndex = '9999999';
} }
const support = getComputedStyle(ele).width === layerWidth; const support = supportCheck
? supportCheck(ele)
: getComputedStyle(ele).content?.includes(checkContent);
ele.parentNode?.removeChild(ele); ele.parentNode?.removeChild(ele);
removeCSS(layerKey); removeCSS(randomSelectorKey);
return support; return support;
} }
@ -57,12 +78,41 @@ let canLayer: boolean | undefined = undefined;
export function supportLayer(): boolean { export function supportLayer(): boolean {
if (canLayer === undefined) { if (canLayer === undefined) {
canLayer = supportSelector( canLayer = supportSelector(
`@layer ${layerKey} { .${layerKey} { width: ${layerWidth}!important; } }`, `@layer ${randomSelectorKey} { .${randomSelectorKey} { content: "${checkContent}"!important; } }`,
ele => { ele => {
ele.className = layerKey; ele.className = randomSelectorKey;
}, },
); );
} }
return canLayer!; return canLayer!;
} }
let canWhere: boolean | undefined = undefined;
export function supportWhere(): boolean {
if (canWhere === undefined) {
canWhere = supportSelector(
`:where(.${randomSelectorKey}) { content: "${checkContent}"!important; }`,
ele => {
ele.className = randomSelectorKey;
},
);
}
return canWhere!;
}
let canLogic: boolean | undefined = undefined;
export function supportLogicProps(): boolean {
if (canLogic === undefined) {
canLogic = supportSelector(
`.${randomSelectorKey} { inset-block: 93px !important; }`,
ele => {
ele.className = randomSelectorKey;
},
ele => getComputedStyle(ele).bottom === '93px',
);
}
return canLogic!;
}

View File

@ -15,6 +15,7 @@ interface Options {
csp?: { nonce?: string }; csp?: { nonce?: string };
prepend?: Prepend; prepend?: Prepend;
mark?: string; mark?: string;
priority?: number;
} }
function getMark({ mark }: Options = {}) { function getMark({ mark }: Options = {}) {