You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
5.3 KiB
166 lines
5.3 KiB
import hash from '@emotion/hash';
|
|
import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext';
|
|
import type Theme from '../theme/Theme';
|
|
import useGlobalCache from './useGlobalCache';
|
|
import { flattenToken, token2key } from '../util';
|
|
import type { Ref } from 'vue';
|
|
import { ref, computed } from 'vue';
|
|
|
|
const EMPTY_OVERRIDE = {};
|
|
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
// nuxt generate when NODE_ENV is prerender
|
|
const isPrerender = process.env.NODE_ENV === 'prerender';
|
|
|
|
// 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 = !isProduction && !isPrerender ? 'css-dev-only-do-not-override' : 'css';
|
|
|
|
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;
|
|
}
|
|
|
|
const tokenKeys = new Map<string, number>();
|
|
function recordCleanToken(tokenKey: string) {
|
|
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) + 1);
|
|
}
|
|
|
|
function removeStyleTags(key: string, instanceId: string) {
|
|
if (typeof document !== 'undefined') {
|
|
const styles = document.querySelectorAll(`style[${ATTR_TOKEN}="${key}"]`);
|
|
|
|
styles.forEach(style => {
|
|
if ((style as any)[CSS_IN_JS_INSTANCE] === instanceId) {
|
|
style.parentNode?.removeChild(style);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const TOKEN_THRESHOLD = 0;
|
|
|
|
// Remove will check current keys first
|
|
function cleanTokenStyle(tokenKey: string, instanceId: string) {
|
|
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) - 1);
|
|
|
|
const tokenKeyList = Array.from(tokenKeys.keys());
|
|
const cleanableKeyList = tokenKeyList.filter(key => {
|
|
const count = tokenKeys.get(key) || 0;
|
|
|
|
return count <= 0;
|
|
});
|
|
|
|
// Should keep tokens under threshold for not to insert style too often
|
|
if (tokenKeyList.length - cleanableKeyList.length > TOKEN_THRESHOLD) {
|
|
cleanableKeyList.forEach(key => {
|
|
removeStyleTags(key, instanceId);
|
|
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
|
|
* @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: Ref<Theme<any, any>>,
|
|
tokens: Ref<Partial<DesignToken>[]>,
|
|
option: Ref<Option<DerivativeToken, DesignToken>> = ref({}),
|
|
) {
|
|
const style = useStyleInject();
|
|
|
|
// Basic - We do basic cache here
|
|
const mergedToken = computed(() => Object.assign({}, ...tokens.value));
|
|
const tokenStr = computed(() => flattenToken(mergedToken.value));
|
|
const overrideTokenStr = computed(() => flattenToken(option.value.override || EMPTY_OVERRIDE));
|
|
|
|
const cachedToken = useGlobalCache<[DerivativeToken & { _tokenKey: string }, string]>(
|
|
'token',
|
|
computed(() => [
|
|
option.value.salt || '',
|
|
theme.value.id,
|
|
tokenStr.value,
|
|
overrideTokenStr.value,
|
|
]),
|
|
() => {
|
|
const {
|
|
salt = '',
|
|
override = EMPTY_OVERRIDE,
|
|
formatToken,
|
|
getComputedToken: compute,
|
|
} = option.value;
|
|
const mergedDerivativeToken = compute
|
|
? compute(mergedToken.value, override, theme.value)
|
|
: getComputedToken(mergedToken.value, override, theme.value, formatToken);
|
|
|
|
// Optimize for `useStyleRegister` performance
|
|
const tokenKey = token2key(mergedDerivativeToken, salt);
|
|
mergedDerivativeToken._tokenKey = tokenKey;
|
|
recordCleanToken(tokenKey);
|
|
|
|
const hashId = `${hashPrefix}-${hash(tokenKey)}`;
|
|
mergedDerivativeToken._hashId = hashId; // Not used
|
|
return [mergedDerivativeToken, hashId];
|
|
},
|
|
cache => {
|
|
// Remove token will remove all related style
|
|
cleanTokenStyle(cache[0]._tokenKey, style.value?.cache.instanceId);
|
|
},
|
|
);
|
|
|
|
return cachedToken;
|
|
}
|