@tschk/moonshine-solid 0.4.0 → 0.4.4
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/README.md +259 -23
- package/package.json +22 -8
- package/src/children.ts +30 -0
- package/src/cleanup-finalization.ts +91 -0
- package/src/cleanup-policy.ts +39 -0
- package/src/component-factory.ts +62 -0
- package/src/context.ts +53 -0
- package/src/control.ts +6 -0
- package/src/dom.ts +3 -0
- package/src/dynamic.ts +90 -0
- package/src/events.ts +185 -0
- package/src/framework-native.ts +1 -0
- package/src/framework.ts +32 -115
- package/src/h.ts +2 -4
- package/src/index.ts +20 -246
- package/src/ir.ts +213 -0
- package/src/lazy.ts +62 -0
- package/src/portal-h.ts +42 -0
- package/src/portal.ts +56 -0
- package/src/props.ts +152 -0
- package/src/reactive.ts +1 -0
- package/src/solid.ts +198 -16
- package/src/ssr.ts +41 -0
- package/src/store.ts +7 -4
package/src/props.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** Independent Solid-style prop composition; no compiler or host proxy ABI. */
|
|
2
|
+
import { createMemo } from "@tschk/moonshine-dom/reactive";
|
|
3
|
+
|
|
4
|
+
type Source =
|
|
5
|
+
object | (() => object | null | undefined | false) | null | undefined | false;
|
|
6
|
+
type Resolved<T> = T extends () => infer R
|
|
7
|
+
? Extract<R, object>
|
|
8
|
+
: Extract<T, object>;
|
|
9
|
+
type Merge<T extends readonly unknown[]> = T extends readonly [
|
|
10
|
+
infer A,
|
|
11
|
+
...infer B,
|
|
12
|
+
]
|
|
13
|
+
? Omit<Resolved<A>, keyof Merge<B>> & Merge<B>
|
|
14
|
+
: {};
|
|
15
|
+
type Split<T, K extends readonly (readonly (keyof T)[])[]> = [
|
|
16
|
+
...{ [I in keyof K]: Pick<T, K[I][number]> },
|
|
17
|
+
Omit<T, K[number][number]>,
|
|
18
|
+
];
|
|
19
|
+
const views = new WeakSet<object>();
|
|
20
|
+
type Reader = () => object;
|
|
21
|
+
const empty = Object.freeze({});
|
|
22
|
+
function view(
|
|
23
|
+
read: (key: PropertyKey) => unknown,
|
|
24
|
+
has: (key: PropertyKey) => boolean,
|
|
25
|
+
keys: () => (string | symbol)[],
|
|
26
|
+
): object {
|
|
27
|
+
const result = new Proxy(
|
|
28
|
+
{},
|
|
29
|
+
{
|
|
30
|
+
get: (_, key) => read(key),
|
|
31
|
+
has: (_, key) => has(key),
|
|
32
|
+
ownKeys: keys,
|
|
33
|
+
set: () => true,
|
|
34
|
+
deleteProperty: () => true,
|
|
35
|
+
getOwnPropertyDescriptor: (_, key) => ({
|
|
36
|
+
configurable: true,
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: () => read(key),
|
|
39
|
+
set: () => {},
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
views.add(result);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export function mergeProps<T extends readonly Source[]>(
|
|
47
|
+
...sources: T
|
|
48
|
+
): Merge<T> {
|
|
49
|
+
const live = sources.some(
|
|
50
|
+
(source) => typeof source === "function" || (!!source && views.has(source)),
|
|
51
|
+
);
|
|
52
|
+
if (live) {
|
|
53
|
+
const readers: Reader[] = sources.map((source) => {
|
|
54
|
+
if (typeof source === "function") {
|
|
55
|
+
const memo = createMemo(
|
|
56
|
+
source as () => object | null | undefined | false,
|
|
57
|
+
);
|
|
58
|
+
return () => memo() || empty;
|
|
59
|
+
}
|
|
60
|
+
return () => source || empty;
|
|
61
|
+
});
|
|
62
|
+
return view(
|
|
63
|
+
(key) => {
|
|
64
|
+
for (let index = readers.length - 1; index >= 0; index--) {
|
|
65
|
+
const source = readers[index]!();
|
|
66
|
+
const value = Reflect.get(source, key);
|
|
67
|
+
if (value !== undefined) return value;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
(key) => readers.some((read) => key in read()),
|
|
71
|
+
() => [...new Set(readers.flatMap((read) => Object.keys(read())))],
|
|
72
|
+
) as Merge<T>;
|
|
73
|
+
}
|
|
74
|
+
const names = [
|
|
75
|
+
...new Set(
|
|
76
|
+
sources
|
|
77
|
+
.toReversed()
|
|
78
|
+
.flatMap((source) =>
|
|
79
|
+
source ? Object.getOwnPropertyNames(source).toReversed() : [],
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
].toReversed();
|
|
83
|
+
const result: Record<string, unknown> = {};
|
|
84
|
+
for (const key of names) {
|
|
85
|
+
if (key === "__proto__" || key === "constructor") continue;
|
|
86
|
+
const descriptors = sources.map((source) =>
|
|
87
|
+
source ? Object.getOwnPropertyDescriptor(source, key) : undefined,
|
|
88
|
+
);
|
|
89
|
+
let selected = descriptors.length - 1;
|
|
90
|
+
while (
|
|
91
|
+
selected >= 0 &&
|
|
92
|
+
(!descriptors[selected] ||
|
|
93
|
+
(!descriptors[selected]!.get &&
|
|
94
|
+
descriptors[selected]!.value === undefined))
|
|
95
|
+
)
|
|
96
|
+
selected--;
|
|
97
|
+
const descriptor = descriptors[selected];
|
|
98
|
+
if (descriptor && descriptor.get) {
|
|
99
|
+
Object.defineProperty(result, key, {
|
|
100
|
+
configurable: true,
|
|
101
|
+
enumerable: true,
|
|
102
|
+
get() {
|
|
103
|
+
for (let index = selected; index >= 0; index--) {
|
|
104
|
+
const desc = descriptors[index];
|
|
105
|
+
if (!desc) continue;
|
|
106
|
+
const value = desc.get ? desc.get.call(sources[index]) : desc.value;
|
|
107
|
+
if (value !== undefined) return value;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
} else result[key] = descriptor ? descriptor.value : undefined;
|
|
112
|
+
}
|
|
113
|
+
return result as Merge<T>;
|
|
114
|
+
}
|
|
115
|
+
export function splitProps<
|
|
116
|
+
T extends object,
|
|
117
|
+
K extends readonly (readonly (keyof T)[])[],
|
|
118
|
+
>(props: T, ...groups: K): Split<T, K> {
|
|
119
|
+
if (views.has(props)) {
|
|
120
|
+
const blocked = new Set<PropertyKey>(groups.flat());
|
|
121
|
+
const parts = groups.map((group) =>
|
|
122
|
+
view(
|
|
123
|
+
(key) =>
|
|
124
|
+
group.includes(key as keyof T) ? Reflect.get(props, key) : undefined,
|
|
125
|
+
(key) => group.includes(key as keyof T) && key in props,
|
|
126
|
+
() =>
|
|
127
|
+
group
|
|
128
|
+
.filter((key) => key in props)
|
|
129
|
+
.map((key) => (typeof key === "symbol" ? key : String(key))),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
parts.push(
|
|
133
|
+
view(
|
|
134
|
+
(key) => (blocked.has(key) ? undefined : Reflect.get(props, key)),
|
|
135
|
+
(key) => !blocked.has(key) && key in props,
|
|
136
|
+
() => Object.keys(props).filter((key) => !blocked.has(key)),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
return parts as Split<T, K>;
|
|
140
|
+
}
|
|
141
|
+
const parts = Array.from({ length: groups.length + 1 }, () => ({}));
|
|
142
|
+
for (const key of Object.getOwnPropertyNames(props)) {
|
|
143
|
+
const index = groups.findIndex((group) => group.includes(key as keyof T));
|
|
144
|
+
const descriptor = Object.getOwnPropertyDescriptor(props, key)!;
|
|
145
|
+
Object.defineProperty(
|
|
146
|
+
parts[index < 0 ? groups.length : index]!,
|
|
147
|
+
key,
|
|
148
|
+
descriptor,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return parts as Split<T, K>;
|
|
152
|
+
}
|
package/src/reactive.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@tschk/moonshine-dom/reactive";
|
package/src/solid.ts
CHANGED
|
@@ -1,25 +1,207 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { children as resolveChildren } from "./children";
|
|
2
|
+
import { peekComponentContext } from "@tschk/moonshine-dom/component";
|
|
3
|
+
/** Native source-level helpers; Solid compiler instructions are not supported. */
|
|
4
|
+
import {
|
|
5
|
+
ErrorBoundary as nativeErrorBoundary,
|
|
6
|
+
For as nativeFor,
|
|
7
|
+
Index as nativeIndex,
|
|
8
|
+
Show as nativeShow,
|
|
9
|
+
Match as nativeMatch,
|
|
10
|
+
Switch as nativeSwitch,
|
|
11
|
+
type MatchBranch,
|
|
12
|
+
} from "@tschk/moonshine-dom/control";
|
|
13
|
+
import {
|
|
14
|
+
createMemo,
|
|
15
|
+
createComponentScope,
|
|
16
|
+
untrack,
|
|
17
|
+
type Accessor,
|
|
18
|
+
} from "@tschk/moonshine-dom/reactive";
|
|
8
19
|
export {
|
|
9
|
-
|
|
10
|
-
Index,
|
|
11
|
-
Match,
|
|
12
|
-
Show,
|
|
13
|
-
Switch,
|
|
20
|
+
catchError,
|
|
14
21
|
batch,
|
|
15
22
|
createEffect,
|
|
16
23
|
createMemo,
|
|
17
|
-
createResource,
|
|
18
|
-
createRoot,
|
|
19
24
|
createSignal,
|
|
20
25
|
on,
|
|
21
26
|
onCleanup,
|
|
22
27
|
onMount,
|
|
23
28
|
untrack,
|
|
24
|
-
} from "
|
|
25
|
-
export type { Accessor,
|
|
29
|
+
} from "@tschk/moonshine-dom/reactive";
|
|
30
|
+
export type { Accessor, Setter } from "@tschk/moonshine-dom/reactive";
|
|
31
|
+
export { createResource } from "@tschk/moonshine-dom/resource";
|
|
32
|
+
export type { Resource } from "@tschk/moonshine-dom/resource";
|
|
33
|
+
|
|
34
|
+
type ListProps<T, U> = {
|
|
35
|
+
readonly each: readonly T[] | false | null | undefined;
|
|
36
|
+
readonly fallback?: U;
|
|
37
|
+
};
|
|
38
|
+
function withFallback<U>(
|
|
39
|
+
mapped: Accessor<U[]>,
|
|
40
|
+
fallback: () => U | undefined,
|
|
41
|
+
): Accessor<U[] | U | undefined> {
|
|
42
|
+
const selected = nativeShow<boolean, U | Accessor<U[]> | undefined>({
|
|
43
|
+
when: () => mapped().length > 0,
|
|
44
|
+
children: () => mapped,
|
|
45
|
+
fallback,
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
const value = selected();
|
|
49
|
+
return value === mapped ? mapped() : (value as U | undefined);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function For<T, U>(
|
|
53
|
+
props: ListProps<T, U> & {
|
|
54
|
+
children: (item: T, index: Accessor<number>) => U;
|
|
55
|
+
},
|
|
56
|
+
): Accessor<U[] | U | undefined> {
|
|
57
|
+
const items = createMemo(() => props.each || []);
|
|
58
|
+
return withFallback(
|
|
59
|
+
nativeFor({
|
|
60
|
+
each: items,
|
|
61
|
+
children: (item, index) => props.children(item, index),
|
|
62
|
+
}),
|
|
63
|
+
() => props.fallback,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
export function Index<T, U>(
|
|
67
|
+
props: ListProps<T, U> & {
|
|
68
|
+
children: (item: Accessor<T>, index: number) => U;
|
|
69
|
+
},
|
|
70
|
+
): Accessor<U[] | U | undefined> {
|
|
71
|
+
const items = createMemo(() => props.each || []);
|
|
72
|
+
return withFallback(
|
|
73
|
+
nativeIndex({
|
|
74
|
+
each: items,
|
|
75
|
+
children: (item, index) => props.children(item, index),
|
|
76
|
+
}),
|
|
77
|
+
() => props.fallback,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
type ConditionalProps<T, U, Keyed extends boolean = false> = {
|
|
81
|
+
readonly when: T;
|
|
82
|
+
readonly children:
|
|
83
|
+
| U
|
|
84
|
+
| ((
|
|
85
|
+
value: Keyed extends true ? NonNullable<T> : Accessor<NonNullable<T>>,
|
|
86
|
+
) => U);
|
|
87
|
+
readonly fallback?: U;
|
|
88
|
+
readonly keyed?: Keyed;
|
|
89
|
+
};
|
|
90
|
+
function conditional<T, U>(
|
|
91
|
+
props: ConditionalProps<T, U, boolean>,
|
|
92
|
+
value: any,
|
|
93
|
+
): U {
|
|
94
|
+
const child = props.children;
|
|
95
|
+
return typeof child === "function" && child.length > 0
|
|
96
|
+
? (child as (value: any) => U)(value)
|
|
97
|
+
: (child as U);
|
|
98
|
+
}
|
|
99
|
+
function keyedBranch<T, U>(
|
|
100
|
+
props: ConditionalProps<T, U, boolean>,
|
|
101
|
+
): Accessor<MatchBranch<T, U>> {
|
|
102
|
+
return createMemo(() => {
|
|
103
|
+
const value = props.when;
|
|
104
|
+
return nativeMatch({
|
|
105
|
+
when: () => value,
|
|
106
|
+
children: () => conditional(props, value),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const keyedBranches = /* @__PURE__ */ new WeakMap<
|
|
111
|
+
object,
|
|
112
|
+
Accessor<MatchBranch<any, any>>
|
|
113
|
+
>();
|
|
114
|
+
export function Show<T, U>(
|
|
115
|
+
props: ConditionalProps<T, U, true> & { readonly keyed: true },
|
|
116
|
+
): Accessor<U | undefined>;
|
|
117
|
+
export function Show<T, U>(
|
|
118
|
+
props: ConditionalProps<T, U>,
|
|
119
|
+
): Accessor<U | undefined>;
|
|
120
|
+
export function Show<T, U>(
|
|
121
|
+
props: ConditionalProps<T, U, boolean>,
|
|
122
|
+
): Accessor<U | undefined> {
|
|
123
|
+
return createComponentScope(() => {
|
|
124
|
+
if (props.keyed) {
|
|
125
|
+
const condition = createMemo(() => props.when);
|
|
126
|
+
return createMemo(() => {
|
|
127
|
+
const value = condition();
|
|
128
|
+
return value
|
|
129
|
+
? untrack(() => conditional(props, value))
|
|
130
|
+
: props.fallback;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return nativeShow({
|
|
134
|
+
when: () => props.when,
|
|
135
|
+
children: (value) => conditional(props, value),
|
|
136
|
+
fallback: () => props.fallback,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export function Match<T, U>(
|
|
141
|
+
props: ConditionalProps<T, U, true> & { readonly keyed: true },
|
|
142
|
+
): MatchBranch<T, U>;
|
|
143
|
+
export function Match<T, U>(props: ConditionalProps<T, U>): MatchBranch<T, U>;
|
|
144
|
+
export function Match<T, U>(
|
|
145
|
+
props: ConditionalProps<T, U, boolean>,
|
|
146
|
+
): MatchBranch<T, U> {
|
|
147
|
+
if (props.keyed) {
|
|
148
|
+
const branch = keyedBranch(props);
|
|
149
|
+
const initial = untrack(branch);
|
|
150
|
+
keyedBranches.set(initial, branch);
|
|
151
|
+
return initial;
|
|
152
|
+
}
|
|
153
|
+
return nativeMatch({
|
|
154
|
+
when: () => props.when,
|
|
155
|
+
children: (value) => conditional(props, value),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
type SwitchChild<U> =
|
|
159
|
+
| MatchBranch<any, U>
|
|
160
|
+
| boolean
|
|
161
|
+
| readonly SwitchChild<U>[]
|
|
162
|
+
| (() => SwitchChild<U>);
|
|
163
|
+
export function Switch<U>(props: {
|
|
164
|
+
readonly children: SwitchChild<U>;
|
|
165
|
+
readonly fallback?: U;
|
|
166
|
+
}): Accessor<U | undefined> {
|
|
167
|
+
return createComponentScope(() => {
|
|
168
|
+
const resolved = resolveChildren<any>(() => props.children);
|
|
169
|
+
return nativeSwitch({
|
|
170
|
+
children: () => {
|
|
171
|
+
const value = resolved();
|
|
172
|
+
const branches = Array.isArray(value) ? value : [value];
|
|
173
|
+
return branches
|
|
174
|
+
.filter((branch) => typeof branch !== "boolean")
|
|
175
|
+
.map((branch) => keyedBranches.get(branch)?.() ?? branch);
|
|
176
|
+
},
|
|
177
|
+
fallback: () => props.fallback,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
export { createContext, useContext, type Context } from "./context";
|
|
182
|
+
|
|
183
|
+
export { children, type ResolvedChildren, type ChildValue } from "./children";
|
|
184
|
+
|
|
185
|
+
export function ErrorBoundary<T>(props: {
|
|
186
|
+
readonly children: T;
|
|
187
|
+
readonly fallback: T | ((error: Error, reset: () => void) => T);
|
|
188
|
+
}): Accessor<T | undefined> {
|
|
189
|
+
if (peekComponentContext()?.mode === "ssr")
|
|
190
|
+
throw new Error("Native Solid ErrorBoundary does not support SSR");
|
|
191
|
+
return nativeErrorBoundary({
|
|
192
|
+
children: () => props.children,
|
|
193
|
+
get fallback() {
|
|
194
|
+
const fallback = props.fallback;
|
|
195
|
+
return (error: Error, reset: () => void) =>
|
|
196
|
+
typeof fallback === "function" && fallback.length
|
|
197
|
+
? (fallback as (error: Error, reset: () => void) => T)(error, reset)
|
|
198
|
+
: (fallback as T);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { mergeProps, splitProps } from "./props";
|
|
204
|
+
|
|
205
|
+
export { lazy } from "./lazy";
|
|
206
|
+
|
|
207
|
+
export { createRoot } from "./cleanup-policy";
|
package/src/ssr.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export * from "@tschk/moonshine-dom/ssr";
|
|
2
|
+
import { h, type SSRChild } from "@tschk/moonshine-dom/ssr";
|
|
3
|
+
import { createIrRenderer } from "./ir";
|
|
4
|
+
export type { CrepusNode, CrepusIr, RenderCrepusOptions } from "./ir";
|
|
5
|
+
export const { renderCrepusNodeSolid, renderCrepusIrSolid } =
|
|
6
|
+
/* @__PURE__ */ createIrRenderer<SSRChild>(h);
|
|
7
|
+
|
|
8
|
+
/** Portals have no server output and never evaluate their children. */
|
|
9
|
+
export function Portal(_props: {
|
|
10
|
+
children?: unknown;
|
|
11
|
+
mount?: unknown;
|
|
12
|
+
isSVG?: boolean;
|
|
13
|
+
useShadow?: boolean;
|
|
14
|
+
}): undefined {}
|
|
15
|
+
|
|
16
|
+
import { splitProps } from "./props";
|
|
17
|
+
import type { DynamicComponent, DynamicProps } from "./dynamic";
|
|
18
|
+
|
|
19
|
+
/** Server selection is evaluated once; no browser runtime is imported. */
|
|
20
|
+
export function createDynamic<T extends DynamicComponent<SSRChild>>(
|
|
21
|
+
component: () => T | undefined,
|
|
22
|
+
props: DynamicProps<NoInfer<T>>,
|
|
23
|
+
): SSRChild {
|
|
24
|
+
const value = component();
|
|
25
|
+
if (typeof value === "function")
|
|
26
|
+
return h(value, props as Record<string, unknown>);
|
|
27
|
+
if (typeof value !== "string") return undefined;
|
|
28
|
+
const attributes = Object.fromEntries(
|
|
29
|
+
Object.keys(props as object).map((key) => [
|
|
30
|
+
key,
|
|
31
|
+
() => Reflect.get(props as object, key, props),
|
|
32
|
+
]),
|
|
33
|
+
);
|
|
34
|
+
return h(value, attributes);
|
|
35
|
+
}
|
|
36
|
+
export function Dynamic<T extends DynamicComponent<SSRChild>>(
|
|
37
|
+
props: { component: T | undefined } & DynamicProps<NoInfer<T>>,
|
|
38
|
+
): SSRChild {
|
|
39
|
+
const [, rest] = splitProps(props, ["component"]);
|
|
40
|
+
return createDynamic(() => props.component, rest as DynamicProps<T>);
|
|
41
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/** Independent native structured state on the Moonshine reactive graph. */
|
|
2
|
+
export {
|
|
3
|
+
createStore,
|
|
4
|
+
createMutable,
|
|
5
|
+
produce,
|
|
6
|
+
reconcile,
|
|
7
|
+
} from "@tschk/moonshine-dom/store";
|