tss-react
Version:
Type safe CSS-in-JS API heavily inspired by react-jss
269 lines (268 loc) • 13.9 kB
JavaScript
/* eslint-disable @typescript-eslint/ban-types */
import { createUseCache } from "./makeStyles";
import { createUseCssAndCx } from "./cssAndCx";
import { assert } from "./tools/assert";
import { objectFromEntries } from "./tools/polyfills/Object.fromEntries";
import { objectKeys } from "./tools/objectKeys";
import { typeGuard } from "./tools/typeGuard";
import { getDependencyArrayRef } from "./tools/getDependencyArrayRef";
import { mergeClasses } from "./mergeClasses";
import { isSSR } from "./tools/isSSR";
export function createTss(params) {
counter = 0;
nestedSelectorUsageTrackRecord.splice(0, nestedSelectorUsageTrackRecord.length);
const { useContext, usePlugin, cache: cacheProvidedAtInception } = params;
const { useCache } = createUseCache({ cacheProvidedAtInception });
const { useCssAndCx } = createUseCssAndCx({ useCache });
const usePluginDefault = ({ classes, cx, css }) => ({ classes, cx, css });
const tss = createTss_internal({
useContext,
useCache,
useCssAndCx,
"usePlugin": usePlugin !== null && usePlugin !== void 0 ? usePlugin : usePluginDefault,
"name": undefined,
"doesUseNestedSelectors": false
});
return { tss };
}
let counter = 0;
const nestedSelectorUsageTrackRecord = [];
function createTss_internal(params) {
const { useContext, useCache, useCssAndCx, usePlugin, name, doesUseNestedSelectors } = params;
return {
"withParams": () => createTss_internal({ ...params }),
"withName": nameOrWrappedName => createTss_internal({
...params,
"name": typeof nameOrWrappedName !== "object"
? nameOrWrappedName
: Object.keys(nameOrWrappedName)[0]
}),
"withNestedSelectors": () => createTss_internal({
...params,
"doesUseNestedSelectors": true
}),
"create": (cssObjectByRuleNameOrGetCssObjectByRuleName) => {
// NOTE: Not isomorphic. Not guaranteed to be the same on client and server.
// Do not attempt to 'simplify' the code without taking this fact into account.
const idOfUseStyles = `x${counter++}`;
// NOTE: Cleanup for hot module reloading.
if (name !== undefined) {
// eslint-disable-next-line no-constant-condition
while (true) {
const wrap = nestedSelectorUsageTrackRecord.find(wrap => wrap.name === name);
if (wrap === undefined) {
break;
}
nestedSelectorUsageTrackRecord.splice(nestedSelectorUsageTrackRecord.indexOf(wrap), 1);
}
}
const getCssObjectByRuleName = typeof cssObjectByRuleNameOrGetCssObjectByRuleName ===
"function"
? cssObjectByRuleNameOrGetCssObjectByRuleName
: () => cssObjectByRuleNameOrGetCssObjectByRuleName;
return function useStyles(params) {
var _a, _b, _c;
const { classesOverrides, ...paramsAndPluginParams } = (params !== null && params !== void 0 ? params : {});
const context = useContext();
const { css, cx } = useCssAndCx();
const cache = useCache();
const getClasses = () => {
const refClassesCache = {};
// @ts-expect-error: Type safety non achievable.
const cssObjectByRuleName = getCssObjectByRuleName({
...params,
...context,
...(!doesUseNestedSelectors
? {}
: {
"classes": typeof Proxy === "undefined"
? {}
: new Proxy({}, {
"get": (_target, ruleName) => {
/* prettier-ignore */
if (typeof ruleName === "symbol") {
assert(false);
}
if (isSSR &&
name === undefined) {
throw new Error([
`tss-react: In SSR setups, in order to use nested selectors, you must also give a unique name to the useStyle function.`,
`Solution: Use tss.withName("ComponentName").withNestedSelectors<...>()... to set a name.`
].join("\n"));
}
update_nested_selector_usage_track_record: {
if (name === undefined) {
break update_nested_selector_usage_track_record;
}
/* prettier-ignore */
let wrap = nestedSelectorUsageTrackRecord.find(wrap => wrap.name === name && wrap.idOfUseStyles === idOfUseStyles);
/* prettier-ignore */
if (wrap === undefined) {
/* prettier-ignore */
wrap = { name, idOfUseStyles, "nestedSelectorRuleNames": new Set() };
/* prettier-ignore */
nestedSelectorUsageTrackRecord.push(wrap);
}
/* prettier-ignore */
wrap.nestedSelectorRuleNames.add(ruleName);
}
detect_potential_conflicts: {
if (name === undefined) {
break detect_potential_conflicts;
}
const hasPotentialConflict = nestedSelectorUsageTrackRecord.find(wrap => wrap.name ===
name &&
wrap.idOfUseStyles !==
idOfUseStyles &&
wrap.nestedSelectorRuleNames.has(ruleName)) !== undefined;
if (!hasPotentialConflict) {
break detect_potential_conflicts;
}
throw new Error([
`tss-react: There are in your codebase two different useStyles named "${name}" that`,
`both use use the nested selector ${ruleName}.\n`,
`This may lead to CSS class name collisions, causing nested selectors to target elements outside of the intended scope.\n`,
`Solution: Ensure each useStyles using nested selectors has a unique name.\n`,
`Use: tss.withName("UniqueName").withNestedSelectors<...>()...`
].join(" "));
}
/* prettier-ignore */
return (refClassesCache[ruleName] = `${cache.key}-${name !== undefined ? name : idOfUseStyles}-${ruleName}-ref`);
}
})
})
});
let classes = objectFromEntries(objectKeys(cssObjectByRuleName).map(ruleName => {
const cssObject = cssObjectByRuleName[ruleName];
if (!cssObject.label) {
cssObject.label = `${name !== undefined ? `${name}-` : ""}${ruleName}`;
}
return [
ruleName,
`${css(cssObject)}${typeGuard(ruleName, ruleName in refClassesCache)
? ` ${refClassesCache[ruleName]}`
: ""}`
];
}));
objectKeys(refClassesCache).forEach(ruleName => {
if (ruleName in classes) {
return;
}
classes[ruleName] =
refClassesCache[ruleName];
});
classes = mergeClasses(classes, classesOverrides, cx);
return classes;
};
const classes = runGetClassesOrUseCache({
cache,
cssObjectByRuleNameOrGetCssObjectByRuleName,
"classesOverridesRef": getDependencyArrayRef(classesOverrides),
"paramsAndPluginParamsRef": getDependencyArrayRef(paramsAndPluginParams),
idOfUseStyles,
context,
getClasses
});
// @ts-expect-error: Type safety non achievable.
const pluginResultWrap = usePlugin({
classes,
css,
cx,
idOfUseStyles,
name,
...context,
...paramsAndPluginParams
});
return {
"classes": (_a = pluginResultWrap.classes) !== null && _a !== void 0 ? _a : classes,
"css": (_b = pluginResultWrap.css) !== null && _b !== void 0 ? _b : css,
"cx": (_c = pluginResultWrap.cx) !== null && _c !== void 0 ? _c : cx,
...context
};
};
}
};
}
const mapCache = new WeakMap();
function runGetClassesOrUseCache(params) {
const { cache, cssObjectByRuleNameOrGetCssObjectByRuleName, classesOverridesRef, paramsAndPluginParamsRef, idOfUseStyles, context, getClasses } = params;
use_cache: {
const mapCache_in = mapCache.get(cache);
if (mapCache_in === undefined) {
break use_cache;
}
const mapCache_in_in = mapCache_in.get(cssObjectByRuleNameOrGetCssObjectByRuleName);
if (mapCache_in_in === undefined) {
break use_cache;
}
const mapCache_in_in_in = mapCache_in_in.get(classesOverridesRef);
if (mapCache_in_in_in === undefined) {
break use_cache;
}
const arr = mapCache_in_in_in.get(paramsAndPluginParamsRef);
if (arr === undefined) {
break use_cache;
}
const entry = arr.find(({ context: context_i }) => {
if (context_i === context) {
return true;
}
if (objectKeys(context_i).length !== objectKeys(context).length) {
return false;
}
for (const key in context_i) {
if (getDependencyArrayRef(context_i[key]) !==
getDependencyArrayRef(context[key])) {
return false;
}
}
return true;
});
if (entry === undefined) {
break use_cache;
}
if ((entry === null || entry === void 0 ? void 0 : entry.idOfUseStyles) !== idOfUseStyles) {
arr.splice(arr.indexOf(entry), 1);
break use_cache;
}
return entry.result;
}
const result = getClasses();
{
if (!mapCache.has(cache)) {
mapCache.set(cache, new WeakMap());
}
const mapCache_in = mapCache.get(cache);
assert(mapCache_in !== undefined);
if (!mapCache_in.has(cssObjectByRuleNameOrGetCssObjectByRuleName)) {
mapCache_in.set(cssObjectByRuleNameOrGetCssObjectByRuleName, new Map());
}
const mapCache_in_in = mapCache_in.get(cssObjectByRuleNameOrGetCssObjectByRuleName);
assert(mapCache_in_in !== undefined);
if (!mapCache_in_in.has(classesOverridesRef)) {
if (mapCache_in_in.size > 200) {
mapCache_in_in.clear();
}
mapCache_in_in.set(classesOverridesRef, new Map());
}
const mapCache_in_in_in = mapCache_in_in.get(classesOverridesRef);
assert(mapCache_in_in_in !== undefined);
if (!mapCache_in_in_in.has(paramsAndPluginParamsRef)) {
clear_cache: {
const threshold = typeof paramsAndPluginParamsRef === "string" ? 257 : 5;
if (mapCache_in_in_in.size < threshold) {
break clear_cache;
}
mapCache_in_in_in.clear();
}
mapCache_in_in_in.set(paramsAndPluginParamsRef, []);
}
let arr = mapCache_in_in_in.get(paramsAndPluginParamsRef);
assert(arr !== undefined);
if (arr.length > 5) {
arr = [];
}
arr.push({ idOfUseStyles, context, result });
}
return result;
}