@hh.ru/magritte-internal-default-props-context 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ export interface BoundDefaultPropsContextProps<P> {
3
+ props: Partial<P>;
4
+ /**
5
+ * Если `true`, заданные здесь пропсы форсируются: по пересекающимся ключам они побеждают
6
+ * любой вложенный (внутренний) провайдер, а не наоборот. Между несколькими `important`
7
+ * побеждает самый внешний. Влияет только на ключи, перечисленные в `props`.
8
+ * Явные пропсы на самом компоненте по-прежнему перекрывают любой дефолт.
9
+ * @default false
10
+ */
11
+ important?: boolean;
12
+ children?: ReactNode;
13
+ }
14
+ /**
15
+ * Результат `createDefaultPropsContext`: провайдер с забинженным типом пропсов и хук для чтения.
16
+ */
17
+ export interface DefaultPropsContextBundle<P> {
18
+ /** Провайдер дефолтных пропсов — тип пропсов забинжен фабрикой. */
19
+ DefaultPropsContext: FC<BoundDefaultPropsContextProps<P>>;
20
+ /** Хук для чтения смёрженных дефолтов из ближайшего провайдера этого бандла. */
21
+ useDefaultProps: () => Partial<P>;
22
+ }
23
+ /**
24
+ * Фабрика, создающая изолированный `DefaultPropsContext` и хук чтения, параметризованные типом пропсов `P`.
25
+ *
26
+ * Контекст создаётся один раз на вызов фабрики и живёт в замыкании модуля компонента, поэтому провайдер
27
+ * и читающий хук гарантированно делят один и тот же объект контекста, даже если центральный пакет
28
+ * задублирован по версиям в дереве зависимостей. Каждый компонент создаёт свой бандл в своём пакете
29
+ * и экспортирует наружу только провайдер, оставляя хук внутренней деталью реализации.
30
+ *
31
+ * Мерж дефолтов выполняется в провайдере (один раз на провайдер), поэтому потребитель читает готовое
32
+ * значение со стабильной ссылкой между рендерами.
33
+ *
34
+ * Вызывать строго один раз на уровне модуля (не внутри рендера) — иначе на каждый рендер будет новый контекст.
35
+ *
36
+ * @param name — имя компонента для `displayName` провайдера в React DevTools (необязательно).
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * // в пакете иконок, внутренний модуль
41
+ * const { DefaultPropsContext: IconDefaultPropsContext, useDefaultProps: useIconDefaultProps } =
42
+ * createDefaultPropsContext<IconProps>('Icon');
43
+ *
44
+ * export { IconDefaultPropsContext };
45
+ *
46
+ * // Icon.tsx
47
+ * const Icon: FC<IconProps> = (props) => {
48
+ * const defaults = useIconDefaultProps();
49
+ * const { size = 'medium', ...rest } = { ...defaults, ...props };
50
+ * // ...
51
+ * };
52
+ * ```
53
+ */
54
+ export declare const createDefaultPropsContext: <P>(name?: string) => DefaultPropsContextBundle<P>;
package/index.js ADDED
@@ -0,0 +1,103 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { createContext, useContext, useMemo, useRef } from 'react';
3
+
4
+ const shallowEqual = (a, b) => {
5
+ if (a === b) {
6
+ return true;
7
+ }
8
+ let countA = 0;
9
+ for (const key in a) {
10
+ if (!Object.is(a[key], b[key])) {
11
+ return false;
12
+ }
13
+ countA += 1;
14
+ }
15
+ for (const _ in b) {
16
+ countA -= 1;
17
+ if (countA < 0) {
18
+ return false;
19
+ }
20
+ }
21
+ return true;
22
+ };
23
+ const useShallowMemo = (value) => {
24
+ const ref = useRef(value);
25
+ if (!shallowEqual(ref.current, value)) {
26
+ ref.current = value;
27
+ }
28
+ return ref.current;
29
+ };
30
+ const EMPTY_DEFAULTS = { values: {}, importantKeys: new Set() };
31
+ /**
32
+ * Инкрементальная свёртка «родитель + пропсы провайдера» сверху вниз.
33
+ *
34
+ * Для каждого ключа провайдера:
35
+ * - если ключ уже зафиксирован `important` снаружи — пропускаем (внешний `important` побеждает);
36
+ * - иначе записываем значение (обычный внутренний так перекрывает обычный внешний, т.к. идём сверху вниз);
37
+ * - если провайдер `important` — дополнительно фиксируем ключ, чтобы внутренние провайдеры его не перебили.
38
+ *
39
+ * Эквивалентно полному обходу цепочки на чтении, но считается один раз на провайдер, а не на потребителя.
40
+ */
41
+ const foldDefaults = (parent, props, important) => {
42
+ const values = { ...parent.values };
43
+ let importantKeys = parent.importantKeys;
44
+ for (const key in props) {
45
+ // Ключ, уже зафиксированный important-провайдером снаружи, не трогаем — внешний important побеждает.
46
+ if (!parent.importantKeys.has(key)) {
47
+ values[key] = props[key];
48
+ if (important) {
49
+ if (importantKeys === parent.importantKeys) {
50
+ importantKeys = new Set(parent.importantKeys);
51
+ }
52
+ importantKeys.add(key);
53
+ }
54
+ }
55
+ }
56
+ return { values, importantKeys };
57
+ };
58
+ /**
59
+ * Фабрика, создающая изолированный `DefaultPropsContext` и хук чтения, параметризованные типом пропсов `P`.
60
+ *
61
+ * Контекст создаётся один раз на вызов фабрики и живёт в замыкании модуля компонента, поэтому провайдер
62
+ * и читающий хук гарантированно делят один и тот же объект контекста, даже если центральный пакет
63
+ * задублирован по версиям в дереве зависимостей. Каждый компонент создаёт свой бандл в своём пакете
64
+ * и экспортирует наружу только провайдер, оставляя хук внутренней деталью реализации.
65
+ *
66
+ * Мерж дефолтов выполняется в провайдере (один раз на провайдер), поэтому потребитель читает готовое
67
+ * значение со стабильной ссылкой между рендерами.
68
+ *
69
+ * Вызывать строго один раз на уровне модуля (не внутри рендера) — иначе на каждый рендер будет новый контекст.
70
+ *
71
+ * @param name — имя компонента для `displayName` провайдера в React DevTools (необязательно).
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * // в пакете иконок, внутренний модуль
76
+ * const { DefaultPropsContext: IconDefaultPropsContext, useDefaultProps: useIconDefaultProps } =
77
+ * createDefaultPropsContext<IconProps>('Icon');
78
+ *
79
+ * export { IconDefaultPropsContext };
80
+ *
81
+ * // Icon.tsx
82
+ * const Icon: FC<IconProps> = (props) => {
83
+ * const defaults = useIconDefaultProps();
84
+ * const { size = 'medium', ...rest } = { ...defaults, ...props };
85
+ * // ...
86
+ * };
87
+ * ```
88
+ */
89
+ const createDefaultPropsContext = (name) => {
90
+ const BoundContext = createContext(EMPTY_DEFAULTS);
91
+ const DefaultPropsContext = ({ props, important = false, children }) => {
92
+ const parent = useContext(BoundContext);
93
+ const stableProps = useShallowMemo(props);
94
+ const resolved = useMemo(() => foldDefaults(parent, stableProps, important), [parent, stableProps, important]);
95
+ return jsx(BoundContext.Provider, { value: resolved, children: children });
96
+ };
97
+ DefaultPropsContext.displayName = name ? `${name}DefaultPropsContext` : 'DefaultPropsContext';
98
+ const useDefaultProps = () => useContext(BoundContext).values;
99
+ return { DefaultPropsContext, useDefaultProps };
100
+ };
101
+
102
+ export { createDefaultPropsContext };
103
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["src/index.tsx"],"sourcesContent":["import { createContext, useContext, useMemo, useRef, type FC, type ReactNode } from 'react';\n\nconst shallowEqual = (a: Record<string, unknown>, b: Record<string, unknown>): boolean => {\n if (a === b) {\n return true;\n }\n let countA = 0;\n for (const key in a) {\n if (!Object.is(a[key], b[key])) {\n return false;\n }\n countA += 1;\n }\n for (const _ in b) {\n countA -= 1;\n if (countA < 0) {\n return false;\n }\n }\n return true;\n};\n\nconst useShallowMemo = <T extends Record<string, unknown>>(value: T): T => {\n const ref = useRef(value);\n if (!shallowEqual(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n};\n\n/**\n * Свёрнутые дефолты для поддерева. `values` — итоговые значения для потребителя,\n * `importantKeys` — ключи, уже зафиксированные `important`-провайдером выше по дереву\n * (нужны, чтобы вложенный провайдер мог продолжить свёртку с правильным приоритетом).\n */\ntype ResolvedDefaults = {\n values: Record<string, unknown>;\n importantKeys: ReadonlySet<string>;\n};\n\nconst EMPTY_DEFAULTS: ResolvedDefaults = { values: {}, importantKeys: new Set() };\n\n/**\n * Инкрементальная свёртка «родитель + пропсы провайдера» сверху вниз.\n *\n * Для каждого ключа провайдера:\n * - если ключ уже зафиксирован `important` снаружи — пропускаем (внешний `important` побеждает);\n * - иначе записываем значение (обычный внутренний так перекрывает обычный внешний, т.к. идём сверху вниз);\n * - если провайдер `important` — дополнительно фиксируем ключ, чтобы внутренние провайдеры его не перебили.\n *\n * Эквивалентно полному обходу цепочки на чтении, но считается один раз на провайдер, а не на потребителя.\n */\nconst foldDefaults = (\n parent: ResolvedDefaults,\n props: Record<string, unknown>,\n important: boolean\n): ResolvedDefaults => {\n const values = { ...parent.values };\n let importantKeys = parent.importantKeys;\n\n for (const key in props) {\n // Ключ, уже зафиксированный important-провайдером снаружи, не трогаем — внешний important побеждает.\n if (!parent.importantKeys.has(key)) {\n values[key] = props[key];\n if (important) {\n if (importantKeys === parent.importantKeys) {\n importantKeys = new Set(parent.importantKeys);\n }\n (importantKeys as Set<string>).add(key);\n }\n }\n }\n\n return { values, importantKeys };\n};\n\nexport interface BoundDefaultPropsContextProps<P> {\n props: Partial<P>;\n /**\n * Если `true`, заданные здесь пропсы форсируются: по пересекающимся ключам они побеждают\n * любой вложенный (внутренний) провайдер, а не наоборот. Между несколькими `important`\n * побеждает самый внешний. Влияет только на ключи, перечисленные в `props`.\n * Явные пропсы на самом компоненте по-прежнему перекрывают любой дефолт.\n * @default false\n */\n important?: boolean;\n children?: ReactNode;\n}\n\n/**\n * Результат `createDefaultPropsContext`: провайдер с забинженным типом пропсов и хук для чтения.\n */\nexport interface DefaultPropsContextBundle<P> {\n /** Провайдер дефолтных пропсов — тип пропсов забинжен фабрикой. */\n DefaultPropsContext: FC<BoundDefaultPropsContextProps<P>>;\n /** Хук для чтения смёрженных дефолтов из ближайшего провайдера этого бандла. */\n useDefaultProps: () => Partial<P>;\n}\n\n/**\n * Фабрика, создающая изолированный `DefaultPropsContext` и хук чтения, параметризованные типом пропсов `P`.\n *\n * Контекст создаётся один раз на вызов фабрики и живёт в замыкании модуля компонента, поэтому провайдер\n * и читающий хук гарантированно делят один и тот же объект контекста, даже если центральный пакет\n * задублирован по версиям в дереве зависимостей. Каждый компонент создаёт свой бандл в своём пакете\n * и экспортирует наружу только провайдер, оставляя хук внутренней деталью реализации.\n *\n * Мерж дефолтов выполняется в провайдере (один раз на провайдер), поэтому потребитель читает готовое\n * значение со стабильной ссылкой между рендерами.\n *\n * Вызывать строго один раз на уровне модуля (не внутри рендера) — иначе на каждый рендер будет новый контекст.\n *\n * @param name — имя компонента для `displayName` провайдера в React DevTools (необязательно).\n *\n * @example\n * ```tsx\n * // в пакете иконок, внутренний модуль\n * const { DefaultPropsContext: IconDefaultPropsContext, useDefaultProps: useIconDefaultProps } =\n * createDefaultPropsContext<IconProps>('Icon');\n *\n * export { IconDefaultPropsContext };\n *\n * // Icon.tsx\n * const Icon: FC<IconProps> = (props) => {\n * const defaults = useIconDefaultProps();\n * const { size = 'medium', ...rest } = { ...defaults, ...props };\n * // ...\n * };\n * ```\n */\nexport const createDefaultPropsContext = <P,>(name?: string): DefaultPropsContextBundle<P> => {\n const BoundContext = createContext<ResolvedDefaults>(EMPTY_DEFAULTS);\n\n const DefaultPropsContext: FC<BoundDefaultPropsContextProps<P>> = ({ props, important = false, children }) => {\n const parent = useContext(BoundContext);\n const stableProps = useShallowMemo(props as Record<string, unknown>);\n\n const resolved = useMemo(() => foldDefaults(parent, stableProps, important), [parent, stableProps, important]);\n\n return <BoundContext.Provider value={resolved}>{children}</BoundContext.Provider>;\n };\n DefaultPropsContext.displayName = name ? `${name}DefaultPropsContext` : 'DefaultPropsContext';\n\n const useDefaultProps = (): Partial<P> => useContext(BoundContext).values as Partial<P>;\n\n return { DefaultPropsContext, useDefaultProps };\n};\n"],"names":["_jsx"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@hh.ru/magritte-internal-default-props-context",
3
+ "version": "1.0.1",
4
+ "main": "index.js",
5
+ "types": "index.d.ts",
6
+ "sideEffects": [
7
+ "index.css"
8
+ ],
9
+ "scripts": {
10
+ "build": "yarn root:build $(pwd)",
11
+ "build-test-branch": "yarn root:build-test-branch $(pwd)",
12
+ "prepack": "yarn root:prepack $(pwd)",
13
+ "postpublish": "yarn root:postpublish $(pwd)",
14
+ "stylelint-check": "yarn root:stylelint-check $(pwd)",
15
+ "eslint-check": "yarn root:eslint-check $(pwd)",
16
+ "ts-config": "yarn root:ts-config $(pwd)",
17
+ "ts-check": "yarn root:ts-check $(pwd)",
18
+ "test": "yarn root:test $(pwd)",
19
+ "watch": "yarn root:watch $(pwd)"
20
+ },
21
+ "peerDependencies": {
22
+ "react": ">=18.2.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "gitHead": "fa01b9128d8cb502e80c61cd964d7a8a3a279957"
28
+ }