{
23 | const $descriptor = createDescriptor(name).withMeta() as ComponentDescriptor;
24 | const $deps = createDepsDescriptor($descriptor, Object(deps));
25 |
26 | $descriptor.deps = $deps;
27 | $descriptor.props = props;
28 |
29 | $descriptor.createComponent = (render) => createComponent($descriptor, render);
30 | $descriptor.createTheme = (rules) => createThemeFor($descriptor, rules);
31 | $descriptor.createDeps = (deps) => createDepsBy($deps, deps as any);
32 | $descriptor.createStrictDeps = (deps) => createStrictDepsBy($deps, deps as any);
33 |
34 | return $descriptor;
35 | }
36 |
37 | export function createComponent<
38 | D extends ComponentDescriptor,
39 | >(
40 | $descriptor: D,
41 | render: ComponentRender,
42 | ): (props: D['meta']) => ReactElement {
43 | const $deps = $descriptor.deps;
44 |
45 | function Component(props: D['meta']) {
46 | const entry: EnvContextProps = {
47 | deps: null,
48 | theme: null,
49 | depsInjection: null,
50 | props,
51 | };
52 |
53 | return withEnvScope($descriptor, entry, () => {
54 | const ctx = getEnvContext();
55 | const overrides = ctx !== null && ctx.deps !== null ? ctx.deps.overrides : null;
56 |
57 | if (overrides !== null && overrides.has($descriptor)) {
58 | const override = getDescriptorOverride(overrides.get($descriptor)!, ctx!);
59 |
60 | if (override !== null && override.value !== Component) {
61 | return override.value(props);
62 | }
63 | }
64 |
65 | const theme = useTheme($descriptor, props, ctx);
66 | const deps = $deps.use(props, ctx);
67 |
68 | return render(
69 | createElement,
70 | props,
71 | {theme, Slot} as any,
72 | deps as any,
73 | );
74 | });
75 | }
76 |
77 | Component.$descriptor = $descriptor;
78 | Component.displayName = $descriptor.name;
79 |
80 | return Component;
81 | }
82 |
83 | type SlotProps = {
84 | name: string;
85 | value: any;
86 | children: React.ReactNode;
87 | is?: SlotElement | [SlotElement, SlotElement];
88 | }
89 |
90 | function Slot({name, children, value, is}: SlotProps) {
91 | if (name == null) {
92 | name = 'children';
93 | }
94 |
95 | const envScope = getActiveEnvScope();
96 | const scopeProps = (envScope !== null && envScope.ctx !== null) ? envScope.ctx.props : null;
97 | const content = scopeProps != null ? scopeProps[name] : undefined;
98 |
99 | if (is) {
100 | if (!is.hasOwnProperty('type')) {
101 | is = is[0] === null ? null : (is[0] || is[1] || null);
102 | }
103 | } else {
104 | is = null;
105 | }
106 |
107 | // Слот не нужен, так сказали свыше!
108 | if (content === null) {
109 | return null;
110 | }
111 |
112 | // Переопределение слота
113 | if (content !== undefined) {
114 | // Это перегрузка
115 | if (typeof content === 'function') {
116 | const result = content(children, value);
117 |
118 | // Слот не нужен
119 | if (result === null) {
120 | return null;
121 | } else if (result !== undefined) {
122 | return wrap(result, is);
123 | }
124 | } else {
125 | return wrap(content, is);
126 | }
127 | }
128 |
129 | // Значение есть, но оно именно null
130 | if (children === null) {
131 | return null;
132 | } else if (children === undefined) {
133 | return is; // возвращаем оборачиващий элемент
134 | }
135 |
136 | if (typeof children === 'function') {
137 | return wrap(children(value), is);
138 | }
139 |
140 | return wrap(children, is);
141 | }
142 |
143 | function wrap(content: any, is: any) {
144 | if (is !== null) {
145 | content = content && cloneElement(
146 | is as React.ReactElement,
147 | is.props,
148 | content,
149 | );
150 | }
151 |
152 | return content;
153 | }
154 |
--------------------------------------------------------------------------------
/component/component.types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createElement,
3 | } from 'react';
4 | import {
5 | DescriptorWithMeta,
6 | DescriptorWithMetaMap,
7 | LikeFragment,
8 | LikeComponent,
9 | ToIntersect,
10 | CleanObject,
11 | Meta,
12 | GetMeta,
13 | } from '../core.types';
14 | import {
15 | DepsDescriptor,
16 | DepsMap,
17 | DepsMapBy,
18 | } from '../deps';
19 | import {
20 | Theme,
21 | ThemeRules,
22 | } from '../theme/theme.types';
23 |
24 | export interface ComponentDescriptor<
25 | N extends string,
26 | P extends object,
27 | DM extends DescriptorWithMetaMap,
28 | > extends DescriptorWithMeta {
29 | deps: DepsDescriptor;
30 | props: P;
31 |
32 | createComponent(render: ComponentRender): LikeComponent;
33 | createTheme>(rules: ThemeRules): Theme;
34 | createDeps(deps: Partial>): DepsMap
35 | createStrictDeps(deps: DepsMapBy): DepsMap;
36 | }
37 |
38 | type GetTheme = P extends {theme?: infer P} ? P : never;
39 | type GetThemeSpec
= P extends {theme?: Theme} ? TS : never;
40 | type GetDeps = P extends {deps?: infer D} ? D : never;
41 |
42 | export type SlotProp = null | (S extends (value: infer V) => infer R
43 | ? SlotWithValue | R
44 | : SlotWithoutValue | S
45 | ) | Meta
46 |
47 | type SlotWithValue = (parent: S, value: V) => R
48 | type SlotWithoutValue = (parent: S) => S
49 |
50 | type SlotPropTypeInfer
= ToIntersect
extends Meta ? S : never;
51 |
52 | export type GetSlotsSpec = CleanObject<{
53 | [K in keyof P]-?: SlotPropTypeInfer
;
54 | }>
55 |
56 | type SlotComponentProps = (
57 | N extends 'children'
58 | ? {name?: N}
59 | : {name: N}
60 | ) & (
61 | T extends (value: infer V) => any
62 | ? {value: V; children?: T; } // {(value) => ...}
63 | : {children?: T;} // ...
64 | ) & ({
65 | is?: SlotElement | [SlotElement | undefined, SlotElement | undefined];
66 | })
67 |
68 | export type SlotComponent = (
69 | (props: {
70 | [N in keyof S]: SlotComponentProps;
71 | }[keyof S]) => LikeFragment
72 | );
73 |
74 | export type ComponentRender> = (
75 | jsx: typeof createElement,
76 | props: D['meta'],
77 | env: {
78 | theme: GetTheme;
79 | Slot: SlotComponent>;
80 | },
81 | deps: GetDeps,
82 | ) => LikeFragment;
83 |
84 | type SlotValue = number | string | JSX.Element;
85 |
86 | export type SlotContent = SlotValue | SlotValue[];
87 | export type SlotPropType = SlotContent | ((value: object) => SlotContent);
88 |
89 | export type SlotElement = null | {
90 | type: any;
91 | props: object;
92 | }
--------------------------------------------------------------------------------
/core.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createDescriptor,
3 | } from './core';
4 |
5 | it('createDescriptor', () => {
6 | const $desc = createDescriptor('uniq');
7 | expect($desc.id).toBe('uniq');
8 | expect($desc.id).toBe($desc.name);
9 | });
10 |
--------------------------------------------------------------------------------
/core.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Descriptor,
3 | DescriptorWith,
4 | DescriptorWithMetaMap,
5 | DescriptorWithMeta,
6 | Predicate,
7 | DescriptorOverride,
8 | PredicateFunc,
9 | DescriptorOverrideIndex,
10 | } from './core.types';
11 | import { EnvContextEntry } from './env/env.types';
12 | import { getActiveEnvScope } from './env/env';
13 |
14 | const reservedDescriptors = {} as {
15 | [name:string]: Descriptor;
16 | };
17 |
18 | export function optional(v: T): T | undefined {
19 | return v;
20 | }
21 |
22 | /** @param name — must be unique string constant (don't use interpolation or concatenation) */
23 | export function createDescriptor(name: N): DescriptorWith {
24 | const descriptor: DescriptorWith = {
25 | id: name,
26 | name,
27 | isOptional: false,
28 | optional() {
29 | return Object.create(this, {isOptional: {value: true}});
30 | },
31 | withMeta: () => descriptor as any,
32 | };
33 |
34 | if (reservedDescriptors.hasOwnProperty(name)) {
35 | throw new Error(`[@truekit/core] Cannot redeclare descriptor '${name}'`);
36 | }
37 |
38 | reservedDescriptors[name] = descriptor;
39 |
40 | return descriptor;
41 | }
42 |
43 | export function createDescriptorWithMetaMap(map: T): T {
44 | return map;
45 | }
46 |
47 | export function createPredicate(predicate?: Predicate): PredicateFunc {
48 | let fn: PredicateFunc = null;
49 |
50 | if (typeof predicate === 'function') {
51 | fn = predicate;
52 | } else if (predicate != null) {
53 | const keys = Object.keys(predicate);
54 | const length = keys.length;
55 |
56 | fn = (props: T) => {
57 | let idx = length;
58 | while (idx--) {
59 | const key = keys[idx]
60 | if (props[key] !== predicate[key]) {
61 | return false;
62 | }
63 | }
64 | return true;
65 | };
66 | }
67 |
68 | return fn;
69 | }
70 |
71 | export function createDescriptorOverride<
72 | D extends DescriptorWithMeta
73 | >(
74 | Target: D,
75 | specificPath: DescriptorWithMeta[],
76 | predicate?: Predicate,
77 | ): DescriptorOverride {
78 | return {
79 | xpath: specificPath.concat(Target),
80 | predicate: createPredicate(predicate),
81 | };
82 | }
83 |
84 | export function createDescriptorOverrideIndex<
85 | T extends DescriptorOverride,
86 | >(overrides: T[]): DescriptorOverrideIndex {
87 | return overrides.reduce((index, override) => {
88 | const xpath = override.xpath.slice();
89 | const target = xpath.pop();
90 |
91 | index.set(
92 | target,
93 | (index.get(target) || [])
94 | .concat({
95 | ...override,
96 | xpath,
97 | })
98 | .sort(descriptorOverrideComparator)
99 | );
100 |
101 | return index;
102 | }, new Map);
103 | }
104 |
105 | function descriptorOverrideComparator(a: DescriptorOverride, b: DescriptorOverride) {
106 | return getDescriptorOverrideWeight(b) - getDescriptorOverrideWeight(a);
107 | }
108 |
109 | function getDescriptorOverrideWeight(v: DescriptorOverride): number {
110 | return v.xpath.length + +(v.predicate !== null);
111 | }
112 |
113 | export function getDescriptorOverride(
114 | list: T[],
115 | ctx: EnvContextEntry,
116 | ): T | null {
117 | let override: T | null = null;
118 |
119 | for (let i = 0, n = list.length; i < n; i++) {
120 | override = list[i];
121 |
122 | const {
123 | xpath,
124 | predicate,
125 | } = override;
126 | const scope = getActiveEnvScope();
127 | let cursor = scope;
128 |
129 | XPATH: for (let x = 0, xn = xpath.length; x < xn; x++) {
130 | const descr = xpath[x];
131 |
132 | while (cursor) {
133 | cursor = cursor.parent;
134 |
135 | if (cursor === null || cursor.ctx === ctx) {
136 | override = null;
137 | break XPATH;
138 | }
139 |
140 | if (cursor.owner === descr) {
141 | break;
142 | }
143 | }
144 | }
145 |
146 | if (override !== null && (predicate === null || (
147 | scope !== null
148 | && scope.ctx !== null
149 | && scope.ctx.props !== null
150 | && predicate(scope.ctx.props)
151 | ))) {
152 | return override;
153 | }
154 | }
155 |
156 | return override;
157 | }
--------------------------------------------------------------------------------
/core.types.ts:
--------------------------------------------------------------------------------
1 | export type Omit = Pick>
2 | export type PartialBy = Omit & Partial>
3 |
4 | export type ToIntersect =
5 | (U extends any ? (inp: U) => void : never) extends ((out: infer I) => void)
6 | ? I
7 | : never
8 | ;
9 |
10 | export type CastIntersect = Cast, Y>;
11 |
12 | export type ArrayInfer = T extends (infer U)[] ? U : never;
13 | export type FunctionInfer = F extends (...args: infer A) => infer R ? [A, R] : never;
14 | export type FirstArgInfer = F extends (first: infer F) => any ? F : never;
15 |
16 | export type Optional = T | undefined;
17 | export type IsOptional = ToIntersect extends undefined ? true : false;
18 | export type NonOptional = IsOptional extends true ? NonNullable : T;
19 |
20 | export type FlattenObject = T extends object ? {[K in keyof T]: T[K]} : never;
21 | export type OptionalObject = T extends object ? ToIntersect<{
22 | [K in keyof T]: IsOptional extends true
23 | ? {[X in K]?: T[K]}
24 | : {[X in K]: T[K]}
25 | }[keyof T]> : never;
26 |
27 | export type CleanObject = CastIntersect<{
28 | [K in keyof T]: T[K] extends never ? never : {[X in K]: T[K]}
29 | }[keyof T], object>;
30 |
31 | export type Head = T extends [any, ...any[]]
32 | ? T[0]
33 | : never
34 | ;
35 |
36 | export type Tail =
37 | ((...args: T) => any) extends ((_: any, ...tail: infer TT) => any)
38 | ? TT
39 | : []
40 | ;
41 |
42 | export type HasTail = T extends ([] | [any]) ? false : true;
43 |
44 | export type Last = {
45 | 0: Last>
46 | 1: Head
47 | }[HasTail extends true ? 0 : 1];
48 |
49 | export type Length = T['length'];
50 |
51 | export type Prepend =
52 | ((head: E, ...args: T) => any) extends ((...args: infer U) => any)
53 | ? U
54 | : T
55 | ;
56 |
57 | export type Cast = X extends Y ? X : Y;
58 |
59 | export interface Descriptor {
60 | readonly id: ID;
61 | readonly name: ID;
62 | readonly isOptional: boolean;
63 | optional(): this | undefined;
64 | }
65 |
66 | export type DescriptorWithMeta = Descriptor & {meta: M};
67 |
68 | export type DescriptorWith = Descriptor & {
69 | withMeta: () => DescriptorWithMeta;
70 | }
71 |
72 | export type DescriptorWithMetaMap = {
73 | [key:string]: DescriptorWithMeta | undefined;
74 | }
75 |
76 | export const __meta__ = Symbol('__meta__');
77 |
78 | export type Meta = {
79 | [__meta__]?: T;
80 | }
81 |
82 | export type GetMeta = {
83 | [K in keyof T]-?: K extends symbol ? T[K] : never;
84 | }[keyof T]
85 |
86 | export type Predicate = ((props: T) => boolean) | Partial | null | undefined
87 | export type PredicateFunc = ((props: T) => boolean) | null
88 | export type DescriptorOverride = {
89 | xpath: DescriptorWithMeta[];
90 | predicate: PredicateFunc