@vitus-labs/core
Version:
Core and utility functions for vitus-labs packages
569 lines (558 loc) • 15.1 kB
JavaScript
import { cloneElement, createContext, createElement, isValidElement, useMemo, useRef } from "react";
import { Fragment, jsx } from "react/jsx-runtime";
import { isFragment, isMemo, isValidElementType } from "react-is";
//#region src/compose.ts
/**
* Right-to-left function composition.
* `compose(f, g, h)(x)` === `f(g(h(x)))`.
*
* Used throughout the system to build HOC chains —
* `compose(attrsHoc, userHoc1, userHoc2)(Component)`.
*/
const compose = (...fns) => (p) => fns.reduceRight((acc, cur) => cur(acc), p);
//#endregion
//#region src/config.ts
const notConfigured = (_name) => {
throw new Error("[@vitus-labs/core] CSS engine not configured. Call init() with a connector before rendering.\n\n import { init } from '@vitus-labs/core'\n import * as connector from '@vitus-labs/connector-styler'\n init(connector)\n");
};
const createStyledDelegate = (self) => {
const createLazy = (tag, options, strings, values) => {
let Real = null;
const Lazy = (props) => {
if (!Real) {
const engine = self._styled;
if (!engine) return notConfigured("styled");
Real = options ? engine(tag, options)(strings, ...values) : engine(tag)(strings, ...values);
}
return createElement(Real, props);
};
Lazy.displayName = `styled(${typeof tag === "string" ? tag : tag.displayName || tag.name || "Component"})`;
return Lazy;
};
const factory = (tag, options) => {
return (strings, ...values) => {
if (self._styled) return options ? self._styled(tag, options)(strings, ...values) : self._styled(tag)(strings, ...values);
return createLazy(tag, options, strings, values);
};
};
return new Proxy(factory, { get(_target, prop) {
if (prop === "prototype" || prop === "$$typeof") return void 0;
return (strings, ...values) => {
if (self._styled) return self._styled[prop](strings, ...values);
return createLazy(prop, void 0, strings, values);
};
} });
};
/**
* Singleton configuration that bridges the UI system with the chosen
* CSS-in-JS engine. All packages reference `config.css`, `config.styled`,
* etc. — the engine is swapped via `init()` with a connector.
*
* The `css` and `styled` properties are **stable delegate functions** that
* can be safely destructured at module level (`const { styled, css } = config`).
* They internally read the latest engine reference set by `init()`.
*
* When destructured before `init()` is called:
* - `css` returns a thunk (function) that resolves at render time
* - `styled` returns a lazy component that creates the real component on first render
*/
var Configuration = class {
_css = null;
_styled = null;
_provider = null;
_keyframes = null;
_createGlobalStyle = null;
_useTheme = null;
component = "div";
textComponent = "span";
createMediaQueries = void 0;
/**
* Stable CSS delegate. When the engine is available, delegates immediately.
* When not (module load time before init), returns a thunk that resolves
* at render time — all CSS-in-JS engines treat functions as interpolations.
*/
css = (strings, ...values) => {
if (this._css) return this._css(strings, ...values);
return (() => {
const engine = this._css;
if (!engine) return notConfigured("css");
return engine(strings, ...values);
});
};
/**
* Stable styled delegate (Proxy). Supports `styled(tag)` and `styled.div`.
* When the engine is not yet available, returns a lazy component
* that creates the real styled component on first render.
*/
styled;
/** The external ThemeProvider from the configured engine. */
get ExternalProvider() {
return this._provider;
}
/** Keyframes factory from the configured engine, or null. */
get keyframes() {
return this._keyframes ?? null;
}
/** Global style factory from the configured engine, or null. */
get createGlobalStyle() {
return this._createGlobalStyle ?? null;
}
/** Theme hook from the configured engine, or null. */
get useTheme() {
return this._useTheme ?? null;
}
constructor() {
this.styled = createStyledDelegate(this);
}
/**
* Initialize or swap the CSS-in-JS engine. Must be called before any
* component renders (typically at the app entry point).
*
* @example
* ```typescript
* import { init } from '@vitus-labs/core'
* import * as connector from '@vitus-labs/connector-styler'
* init(connector)
* ```
*/
init = (props) => {
if (props.css) this._css = props.css;
if (props.styled) this._styled = props.styled;
if (props.provider) this._provider = props.provider;
if (props.keyframes) this._keyframes = props.keyframes;
if (props.createGlobalStyle) this._createGlobalStyle = props.createGlobalStyle;
if (props.useTheme) this._useTheme = props.useTheme;
if (props.component) this.component = props.component;
if (props.textComponent) this.textComponent = props.textComponent;
if (props.createMediaQueries) this.createMediaQueries = props.createMediaQueries;
};
};
const config = new Configuration();
const { init } = config;
//#endregion
//#region src/isEmpty.ts
const isEmpty = (param) => {
if (!param) return true;
if (typeof param !== "object") return true;
if (Array.isArray(param)) return param.length === 0;
return Object.keys(param).length === 0;
};
//#endregion
//#region src/isEqual.ts
const isArrayEqual = (a, b, seen) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (!isEqualInner(a[i], b[i], seen)) return false;
return true;
};
const isObjectEqual = (a, b, seen) => {
const aKeys = Object.keys(a);
if (aKeys.length !== Object.keys(b).length) return false;
for (const key of aKeys) {
if (!Object.hasOwn(b, key)) return false;
if (!isEqualInner(a[key], b[key], seen)) return false;
}
return true;
};
const isEqualInner = (a, b, seen) => {
if (Object.is(a, b)) return true;
if (typeof a !== typeof b || a == null || b == null || typeof a !== "object") return false;
const aObj = a;
const bObj = b;
let bSet = seen.get(aObj);
if (bSet !== void 0) {
if (bSet.has(bObj)) return true;
bSet.add(bObj);
} else {
bSet = /* @__PURE__ */ new WeakSet();
bSet.add(bObj);
seen.set(aObj, bSet);
}
if (Array.isArray(a)) return Array.isArray(b) && isArrayEqual(a, b, seen);
if (Array.isArray(b)) return false;
return isObjectEqual(a, b, seen);
};
const isEqual = (a, b) => isEqualInner(a, b, /* @__PURE__ */ new WeakMap());
//#endregion
//#region src/useStableValue.ts
/**
* Returns a referentially stable version of `value`. The returned reference
* only changes when the value is no longer deeply equal to the previous one.
*
* Use this to stabilize object/array props before passing them as hook
* dependencies, preventing unnecessary recalculations in useMemo/useEffect.
*
* Based on the useDeepCompareMemoize pattern from use-deep-compare.
*/
const useStableValue = (value) => {
const ref = useRef(value);
if (!isEqual(ref.current, value)) ref.current = value;
return ref.current;
};
//#endregion
//#region src/context.tsx
/**
* Internal React context shared across all @vitus-labs packages.
* Carries the theme object plus any extra provider props.
*/
const context = createContext({});
const VitusLabsProvider = context.Provider;
/**
* Dual-layer provider that feeds both the internal VitusLabs context
* and an optional external styling provider (e.g. styled-components'
* ThemeProvider). When no theme is supplied, renders children directly.
*/
const Provider = ({ theme, children, ...props }) => {
const ExternalProvider = useMemo(() => config.ExternalProvider, []);
const context = useStableValue({
theme,
...props
});
if (isEmpty(theme) || !theme) return /* @__PURE__ */ jsx(Fragment, { children });
if (ExternalProvider) return /* @__PURE__ */ jsx(VitusLabsProvider, {
value: context,
children: /* @__PURE__ */ jsx(ExternalProvider, {
theme,
children
})
});
return /* @__PURE__ */ jsx(VitusLabsProvider, {
value: context,
children
});
};
//#endregion
//#region src/hoistNonReactStatics.ts
const REACT_STATICS = {
childContextTypes: true,
contextType: true,
contextTypes: true,
defaultProps: true,
displayName: true,
getDefaultProps: true,
getDerivedStateFromError: true,
getDerivedStateFromProps: true,
mixins: true,
propTypes: true,
type: true
};
const KNOWN_STATICS = {
name: true,
length: true,
prototype: true,
caller: true,
callee: true,
arguments: true,
arity: true
};
const FORWARD_REF_STATICS = {
$$typeof: true,
render: true,
defaultProps: true,
displayName: true,
propTypes: true
};
const MEMO_STATICS = {
$$typeof: true,
compare: true,
defaultProps: true,
displayName: true,
propTypes: true,
type: true
};
const TYPE_STATICS = {};
TYPE_STATICS[Symbol.for("react.forward_ref")] = FORWARD_REF_STATICS;
TYPE_STATICS[Symbol.for("react.memo")] = MEMO_STATICS;
const getStatics = (component) => {
if (isMemo(component)) return MEMO_STATICS;
return TYPE_STATICS[component.$$typeof] || REACT_STATICS;
};
const hoistNonReactStatics = (target, source, excludeList) => {
if (typeof source === "string") return target;
const proto = Object.getPrototypeOf(source);
if (proto && proto !== Object.prototype) hoistNonReactStatics(target, proto, excludeList);
const keys = [...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)];
const targetStatics = getStatics(target);
const sourceStatics = getStatics(source);
for (const key of keys) {
const k = key;
if (KNOWN_STATICS[k] || excludeList?.[k] || sourceStatics[k] || targetStatics[k]) continue;
const descriptor = Object.getOwnPropertyDescriptor(source, key);
if (descriptor) try {
Object.defineProperty(target, key, descriptor);
} catch {}
}
return target;
};
//#endregion
//#region src/html/htmlTags.ts
const HTML_TAGS = [
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"bdi",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"label",
"legend",
"li",
"main",
"map",
"mark",
"meter",
"nav",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"section",
"select",
"small",
"source",
"span",
"strong",
"sub",
"summary",
"sup",
"svg",
"table",
"tbody",
"td",
"template",
"textarea",
"tfoot",
"th",
"thead",
"time",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr"
];
const HTML_TEXT_TAGS = [
"abbr",
"b",
"bdi",
"bdo",
"big",
"blockquote",
"cite",
"code",
"del",
"div",
"dl",
"dt",
"em",
"figcaption",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"i",
"ins",
"kbd",
"label",
"legend",
"li",
"p",
"pre",
"q",
"rp",
"rt",
"s",
"small",
"span",
"strong",
"sub",
"summary",
"sup",
"time",
"u"
];
//#endregion
//#region src/render.ts
const render = (content, attachProps) => {
if (!content) return null;
const render = (child) => attachProps ? createElement(child, attachProps) : createElement(child);
const t = typeof content;
if (t === "string" || t === "number" || t === "boolean" || t === "bigint") return content;
if (Array.isArray(content) || isFragment(content)) return content;
if (isValidElementType(content)) return render(content);
if (isValidElement(content)) {
if (isEmpty(attachProps)) return content;
return cloneElement(content, attachProps);
}
return content;
};
//#endregion
//#region src/utils.ts
const omit = (obj, keys) => {
if (obj == null) return {};
if (!keys) return { ...obj };
const keysSet = keys instanceof Set ? keys : new Set(keys);
if (keysSet.size === 0) return { ...obj };
const result = {};
for (const key in obj) if (Object.hasOwn(obj, key) && !keysSet.has(key)) result[key] = obj[key];
return result;
};
const pick = (obj, keys) => {
if (obj == null) return {};
if (!keys || keys.length === 0) return { ...obj };
const result = {};
for (const key of keys) {
const k = key;
if (Object.hasOwn(obj, k)) result[k] = obj[k];
}
return result;
};
const PATH_RE = /[^.[\]]+/g;
/** Split a dot/bracket path string into individual key tokens. */
const parsePath = (path) => {
if (Array.isArray(path)) return path;
return path.match(PATH_RE) ?? [];
};
const isUnsafeKey = (key) => key === "__proto__" || key === "prototype" || key === "constructor";
const get = (obj, path, defaultValue) => {
const keys = parsePath(path);
let result = obj;
for (const key of keys) {
if (result == null || isUnsafeKey(key)) return defaultValue;
result = result[key];
}
return result === void 0 ? defaultValue : result;
};
const set = (obj, path, value) => {
const keys = parsePath(path);
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (isUnsafeKey(key)) return obj;
const nextKey = keys[i + 1];
if (isUnsafeKey(nextKey)) return obj;
if (current[key] == null) current[key] = /^\d+$/.test(nextKey) ? [] : {};
current = current[key];
}
const lastKey = keys[keys.length - 1];
if (lastKey != null && !isUnsafeKey(lastKey)) current[lastKey] = value;
return obj;
};
const throttle = (fn, wait = 0, options) => {
const leading = options?.leading !== false;
const trailing = options?.trailing !== false;
let lastCallTime;
let timeoutId;
let lastArgs;
const invoke = (args) => {
lastCallTime = Date.now();
fn(...args);
};
const startTrailingTimer = (args, delay) => {
lastArgs = args;
if (timeoutId !== void 0) return;
timeoutId = setTimeout(() => {
timeoutId = void 0;
if (lastArgs) {
invoke(lastArgs);
lastArgs = void 0;
}
}, delay);
};
const throttled = (...args) => {
const now = Date.now();
const elapsed = lastCallTime === void 0 ? wait : now - lastCallTime;
if (elapsed >= wait) if (leading) invoke(args);
else {
lastCallTime = now;
if (trailing) startTrailingTimer(args, wait);
}
else if (trailing) startTrailingTimer(args, wait - elapsed);
};
throttled.cancel = () => {
if (timeoutId !== void 0) {
clearTimeout(timeoutId);
timeoutId = void 0;
}
lastArgs = void 0;
lastCallTime = void 0;
};
return throttled;
};
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
const merge = (target, ...sources) => {
for (const source of sources) {
if (source == null) continue;
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
const targetVal = target[key];
const sourceVal = source[key];
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) target[key] = merge({ ...targetVal }, sourceVal);
else target[key] = sourceVal;
}
}
return target;
};
//#endregion
export { HTML_TAGS, HTML_TEXT_TAGS, Provider, compose, config, context, get, hoistNonReactStatics, init, isEmpty, isEqual, merge, omit, pick, render, set, throttle, useStableValue };
//# sourceMappingURL=index.js.map