@tschk/moonshine-svelte 0.0.0-stage → 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/src/prop.ts ADDED
@@ -0,0 +1,64 @@
1
+ /** Compiler-facing prop cells; binding declaration checks belong to the compiler. */
2
+ import { untrack } from "@tschk/moonshine";
3
+ import { peekComponentContext } from "@tschk/moonshine-dom/component";
4
+ import { createDerived } from "./derived";
5
+ import { createState } from "./state";
6
+
7
+ export function createProp<T>(
8
+ props: object,
9
+ key: PropertyKey,
10
+ options: {
11
+ bindable?: boolean;
12
+ fallback?: () => T;
13
+ mode?: "dom" | "ssr";
14
+ } = {},
15
+ ): { get value(): T; set value(value: T) } {
16
+ const mode = options.mode ?? peekComponentContext()?.mode ?? "dom";
17
+ const setter = options.bindable
18
+ ? Object.getOwnPropertyDescriptor(props, key)?.set
19
+ : undefined;
20
+ // Svelte 5.57 rune defaults are lazy derived values, independent of local
21
+ // prop overrides and reused across defined/undefined parent phases.
22
+ const fallbackCell = options.fallback
23
+ ? createDerived(options.fallback)
24
+ : undefined;
25
+ function fallback(): T {
26
+ return fallbackCell?.value as T;
27
+ }
28
+ const initial = untrack(() => Reflect.get(props, key));
29
+ if (initial === undefined && options.fallback) {
30
+ const value = fallback();
31
+ if (setter) {
32
+ if (mode === "dom")
33
+ throw new Error(
34
+ `Bound prop ${String(key)} cannot be undefined when it has a fallback`,
35
+ );
36
+ if (value !== undefined)
37
+ untrack(() => Reflect.apply(setter, props, [value]));
38
+ }
39
+ }
40
+ function read(): T {
41
+ const value = Reflect.get(props, key);
42
+ if (value === undefined) return fallback();
43
+ return value as T;
44
+ }
45
+ if (setter)
46
+ return {
47
+ get value() {
48
+ return read();
49
+ },
50
+ set value(value: T) {
51
+ untrack(() => Reflect.apply(setter, props, [value]));
52
+ },
53
+ };
54
+ const local = createDerived(read);
55
+ return {
56
+ get value() {
57
+ return local.value;
58
+ },
59
+ set value(value: T) {
60
+ const next = options.bindable ? createState(value).value : value;
61
+ local.value = next;
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ renderSchedulerKey,
3
+ type RenderScheduler,
4
+ } from "@tschk/moonshine-dom/render-scheduler";
5
+ import {
6
+ isRawSnippet,
7
+ getRawSnippetFactory,
8
+ type RawSnippet,
9
+ } from "./raw-snippet";
10
+ import { insertedNode } from "@tschk/moonshine-dom/dom";
11
+ import { rawHTML } from "@tschk/moonshine-dom/html";
12
+ import {
13
+ withComponentConsumer,
14
+ type ComponentContext,
15
+ } from "@tschk/moonshine-dom/component";
16
+ import {
17
+ createRoot,
18
+ getOwner,
19
+ onCleanup,
20
+ onBeforeDispose,
21
+ runWithOwner,
22
+ untrack,
23
+ type Owner,
24
+ } from "@tschk/moonshine-dom/reactive";
25
+ type Cell<T> = { readonly value: T };
26
+ export function invokeRawDOM<P extends unknown[]>(
27
+ snippet: RawSnippet<P>,
28
+ cells: { [K in keyof P]: Cell<P[K]> },
29
+ frame: ComponentContext,
30
+ initial: boolean,
31
+ consumerScheduler: RenderScheduler | undefined,
32
+ ) {
33
+ const factory = getRawSnippetFactory(snippet);
34
+ const invocation = getOwner();
35
+ if (!invocation || invocation.disposed || frame.owner.disposed)
36
+ throw Error("Raw snippet requires live authored consumer");
37
+ const lifetime = initial ? frame.owner : invocation;
38
+ let work: Owner | undefined,
39
+ stopWork: () => void = () => {},
40
+ element: Element | undefined,
41
+ done = false,
42
+ branchClosed = false,
43
+ returned: (() => void) | undefined;
44
+ const activate = <T>(fn: () => T): T =>
45
+ withComponentConsumer(frame, () => untrack(fn));
46
+ const finish = () => {
47
+ branchClosed = true;
48
+ element?.remove();
49
+ const cleanup = returned;
50
+ returned = undefined;
51
+ try {
52
+ cleanup?.();
53
+ } finally {
54
+ if (!done) stopWork();
55
+ }
56
+ };
57
+ onCleanup(finish);
58
+ const result = createRoot((stop) => {
59
+ stopWork = stop;
60
+ work = getOwner()!;
61
+ if (initial) {
62
+ // Initial setup effects belong to the consumer's post group, not this branch.
63
+ work.context = new Map(work.context);
64
+ if (consumerScheduler)
65
+ work.context.set(renderSchedulerKey, consumerScheduler);
66
+ else work.context.delete(renderSchedulerKey);
67
+ runWithOwner(lifetime, () => onCleanup(stopWork));
68
+ }
69
+ const rendered = activate(() =>
70
+ factory(
71
+ ...(cells.map((cell) => () => cell.value) as {
72
+ [K in keyof P]: () => P[K];
73
+ }),
74
+ ),
75
+ );
76
+ const html = activate(() => rendered.render().trim()),
77
+ nodes = rawHTML(() => html)();
78
+ if (nodes.length !== 1 || !(nodes[0] instanceof Element))
79
+ throw TypeError("Raw snippet render requires one element");
80
+ element = nodes[0];
81
+ onBeforeDispose(() => element?.remove());
82
+ Object.defineProperty(element, insertedNode, {
83
+ configurable: true,
84
+ value: () => {
85
+ if (invocation.disposed || lifetime.disposed || work!.disposed) {
86
+ element!.remove();
87
+ stopWork();
88
+ return;
89
+ }
90
+ if (!initial) {
91
+ work!.parent = lifetime;
92
+ lifetime.children.add(work!);
93
+ }
94
+ try {
95
+ const cleanup = runWithOwner(work!, () =>
96
+ activate(() => rendered.setup?.(element!)),
97
+ );
98
+ done = true;
99
+ if (typeof cleanup === "function") {
100
+ if (branchClosed || invocation.disposed || lifetime.disposed)
101
+ cleanup();
102
+ else returned = cleanup as () => void;
103
+ }
104
+ } catch (error) {
105
+ try {
106
+ stopWork();
107
+ } catch {}
108
+ throw error;
109
+ }
110
+ },
111
+ });
112
+ return element;
113
+ });
114
+ return result;
115
+ }
116
+ /** Internal @render dispatch: ordinary compiled snippets keep their cell ABI. */
117
+ export function invokeSnippetDOM<P extends unknown[]>(
118
+ snippet:
119
+ | RawSnippet<P>
120
+ | ((
121
+ ...cells: { [K in keyof P]: Cell<P[K]> }
122
+ ) => import("@tschk/moonshine-dom/dom").DOMChild),
123
+ cells: { [K in keyof P]: Cell<P[K]> },
124
+ consumer: ComponentContext,
125
+ initial: boolean,
126
+ consumerScheduler: RenderScheduler | undefined,
127
+ ): import("@tschk/moonshine-dom/dom").DOMChild {
128
+ if (isRawSnippet(snippet))
129
+ return invokeRawDOM(
130
+ snippet as RawSnippet<P>,
131
+ cells,
132
+ consumer,
133
+ initial,
134
+ consumerScheduler,
135
+ );
136
+ return (
137
+ snippet as (
138
+ ...cells: { [K in keyof P]: Cell<P[K]> }
139
+ ) => import("@tschk/moonshine-dom/dom").DOMChild
140
+ )(...cells);
141
+ }
142
+
143
+ /** Internal compiler bridge: one queue/commit marker per rendered snippet slot. */
144
+ export function createRawSnippetSlot() {
145
+ const pending = new Set<() => void>(),
146
+ marker = document.createComment("");
147
+ function drain() {
148
+ let failed = false,
149
+ failure: unknown;
150
+ const stops = [...pending];
151
+ pending.clear();
152
+ for (const stop of stops)
153
+ try {
154
+ untrack(stop);
155
+ } catch (error) {
156
+ if (!failed) failure = error;
157
+ failed = true;
158
+ }
159
+ if (failed) throw failure;
160
+ }
161
+ onCleanup(drain);
162
+ return {
163
+ deferRemoved(stop: () => void) {
164
+ pending.add(stop);
165
+ },
166
+ render(fn: () => import("@tschk/moonshine-dom/dom").DOMChild) {
167
+ let value: import("@tschk/moonshine-dom/dom").DOMChild;
168
+ try {
169
+ value = fn();
170
+ } catch (error) {
171
+ try {
172
+ drain();
173
+ } catch {}
174
+ throw error;
175
+ }
176
+ if (value instanceof Node) {
177
+ const setup = (value as Node & { [insertedNode]?: () => void })[
178
+ insertedNode
179
+ ];
180
+ if (setup)
181
+ Object.defineProperty(value, insertedNode, {
182
+ configurable: true,
183
+ value() {
184
+ let failed = false,
185
+ failure: unknown;
186
+ try {
187
+ setup();
188
+ } catch (error) {
189
+ failure = error;
190
+ failed = true;
191
+ }
192
+ try {
193
+ drain();
194
+ } catch (error) {
195
+ if (!failed) failure = error;
196
+ failed = true;
197
+ }
198
+ if (failed) throw failure;
199
+ },
200
+ });
201
+ }
202
+ return value;
203
+ },
204
+ commit(
205
+ value: import("@tschk/moonshine-dom/dom").DOMChild,
206
+ ): import("@tschk/moonshine-dom/dom").DOMChild {
207
+ Object.defineProperty(marker, insertedNode, {
208
+ configurable: true,
209
+ value: drain,
210
+ });
211
+ return [value, marker];
212
+ },
213
+ };
214
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ isRawSnippet,
3
+ getRawSnippetFactory,
4
+ type RawSnippet,
5
+ } from "./raw-snippet";
6
+ import { rawHTML } from "@tschk/moonshine-dom/ssr";
7
+ import {
8
+ withComponentConsumer,
9
+ type ComponentContext,
10
+ } from "@tschk/moonshine-dom/component";
11
+ import {
12
+ untrack,
13
+ getOwner,
14
+ runWithOwner,
15
+ onCleanup,
16
+ } from "@tschk/moonshine-dom/reactive";
17
+ import {
18
+ captureSSRRenderAttempt,
19
+ isSSRRenderAccepted,
20
+ } from "@tschk/moonshine-dom/ssr-attempt";
21
+ import { rawSSRRegistrar, type Registrar } from "./raw-ssr-lifecycle";
22
+ /** Internal invocation ABI: explicitly supplied authored consumer; source values, not DOM cells. */
23
+ export function invokeRawSSR<P extends unknown[]>(
24
+ snippet: RawSnippet<P>,
25
+ values: P,
26
+ consumer: ComponentContext,
27
+ ) {
28
+ const factory = getRawSnippetFactory(snippet);
29
+ const owner = getOwner();
30
+ if (!owner || owner.disposed) throw Error("Live raw SSR invocation required");
31
+ const consumerAttempt = runWithOwner(consumer.owner, captureSSRRenderAttempt),
32
+ context = owner.context;
33
+ owner.context = new Map(context);
34
+ owner.context.set(rawSSRRegistrar, {
35
+ consumer,
36
+ register(fn) {
37
+ runWithOwner(consumer.owner, () =>
38
+ onCleanup(() => {
39
+ if (isSSRRenderAccepted(consumerAttempt)) fn();
40
+ }),
41
+ );
42
+ },
43
+ } satisfies Registrar);
44
+ try {
45
+ return withComponentConsumer(consumer, () =>
46
+ untrack(() =>
47
+ rawHTML(
48
+ factory(
49
+ ...(values.map((value) => () => value) as {
50
+ [K in keyof P]: () => P[K];
51
+ }),
52
+ )
53
+ .render()
54
+ .trim(),
55
+ ),
56
+ ),
57
+ );
58
+ } finally {
59
+ owner.context = context;
60
+ }
61
+ }
62
+ /** Internal @render dispatch: ordinary server snippets keep source value arguments. */
63
+ export function invokeSnippetSSR<P extends unknown[]>(
64
+ snippet:
65
+ | RawSnippet<P>
66
+ | ((...values: P) => import("@tschk/moonshine-dom/ssr").SSRChild),
67
+ values: P,
68
+ consumer: ComponentContext,
69
+ ): import("@tschk/moonshine-dom/ssr").SSRChild {
70
+ if (isRawSnippet(snippet))
71
+ return invokeRawSSR(snippet as RawSnippet<P>, values, consumer);
72
+ return (
73
+ snippet as (...values: P) => import("@tschk/moonshine-dom/ssr").SSRChild
74
+ )(...values);
75
+ }
@@ -0,0 +1,36 @@
1
+ const rawSnippet = /* @__PURE__ */ Symbol("moonshine raw snippet");
2
+ export type RawSnippetFactory<P extends unknown[]> = (
3
+ ...getters: { [K in keyof P]: () => P[K] }
4
+ ) => { render(): string; setup?: (element: Element) => unknown };
5
+ /** Tagged source callable; direct execution rejects until @render is compiler-lowered. */
6
+ export interface RawSnippet<P extends unknown[] = []> {
7
+ (...values: number extends P["length"] ? never : P): unknown;
8
+ readonly [rawSnippet]: RawSnippetFactory<P>;
9
+ }
10
+ export function createRawSnippet<P extends unknown[]>(
11
+ factory: RawSnippetFactory<P>,
12
+ ): RawSnippet<P> {
13
+ if (typeof factory !== "function")
14
+ throw new TypeError("Raw snippet requires a factory");
15
+ const snippet = (
16
+ ..._values: number extends P["length"] ? never : P
17
+ ): never => {
18
+ throw new Error("Raw snippets require compiler-lowered @render invocation");
19
+ };
20
+ return Object.assign(snippet, { [rawSnippet]: factory });
21
+ }
22
+ /** Internal compiler adapter validation, not a source invocation API. */
23
+ export function getRawSnippetFactory<P extends unknown[]>(
24
+ snippet: RawSnippet<P>,
25
+ ): RawSnippetFactory<P> {
26
+ if (!snippet || typeof snippet[rawSnippet] !== "function")
27
+ throw new TypeError("Expected native raw snippet descriptor");
28
+ return snippet[rawSnippet];
29
+ }
30
+ /** Internal dynamic @render discrimination; ordinary source snippets are untagged. */
31
+ export function isRawSnippet(value: unknown): value is RawSnippet<unknown[]> {
32
+ return (
33
+ typeof value === "function" &&
34
+ typeof (value as RawSnippet<unknown[]>)[rawSnippet] === "function"
35
+ );
36
+ }
@@ -0,0 +1,8 @@
1
+ import type { ComponentContext } from "@tschk/moonshine-dom/component";
2
+ export const rawSSRRegistrar = /* @__PURE__ */ Symbol(
3
+ "raw SSR consumer lifecycle",
4
+ );
5
+ export type Registrar = {
6
+ consumer: ComponentContext;
7
+ register(fn: () => void): void;
8
+ };
@@ -0,0 +1,19 @@
1
+ /** Shallow compiler cell: assignments track; nested values retain their identity. */
2
+ import { createSignal, untrack } from "@tschk/moonshine-dom/reactive";
3
+ import { readHistory, recordHistory, withHistoryMutation } from "./history";
4
+
5
+ export function createRawState<T>(initial: T): { value: T } {
6
+ const [read, write] = createSignal(initial);
7
+ return {
8
+ get value() {
9
+ return readHistory(read, "value", read);
10
+ },
11
+ set value(next: T) {
12
+ withHistoryMutation(() => {
13
+ const previous = untrack(read);
14
+ if (previous !== next) recordHistory(read, "value", previous, next);
15
+ write(() => next);
16
+ });
17
+ },
18
+ };
19
+ }
package/src/runes.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** Compiler-facing native state and post-DOM effects; syntax lowering is Crepus's. */
2
+ export { createState, stateSnapshot } from "./state";
3
+ export { createRuneEffect, createRuneRoot, runeTracking } from "./effect";
4
+ export { createDerived } from "./derived";
5
+ export { createProp } from "./prop";
6
+ export { createRawState } from "./raw-state";
7
+ export { spreadProps } from "./spread-props";
8
+ export {
9
+ createPrivateState,
10
+ createPrivateRawState,
11
+ } from "./private-class-state";
@@ -0,0 +1,31 @@
1
+ import type {
2
+ ComponentExports,
3
+ ComponentProps,
4
+ MountOptions,
5
+ UnmountOptions,
6
+ } from "./mount-types";
7
+ export type {
8
+ ComponentView,
9
+ ComponentFactory,
10
+ ComponentProps,
11
+ ComponentExports,
12
+ MountOptions,
13
+ UnmountOptions,
14
+ } from "./mount-types";
15
+ function unavailable(name: string): never {
16
+ throw new Error(
17
+ `lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`,
18
+ );
19
+ }
20
+ export function mount<C extends (...args: never[]) => unknown>(
21
+ _component: C & ([ComponentExports<C>] extends [never] ? never : unknown),
22
+ _options: MountOptions<ComponentProps<C>>,
23
+ ): ComponentExports<C> {
24
+ return unavailable("mount");
25
+ }
26
+ export function unmount(
27
+ _instance: object,
28
+ _options?: UnmountOptions,
29
+ ): Promise<void> {
30
+ return unavailable("unmount");
31
+ }
@@ -0,0 +1,29 @@
1
+ /** Server bindings publish final local values only after successful rendering. */
2
+ import { untrack } from "@tschk/moonshine";
3
+
4
+ export function createServerProp<T>(
5
+ props: object,
6
+ key: PropertyKey,
7
+ fallback?: () => T,
8
+ ): { value: T; commit(): void } {
9
+ let value = untrack(() => {
10
+ const initial = Reflect.get(props, key);
11
+ return initial === undefined ? fallback?.() : initial;
12
+ }) as T;
13
+ return {
14
+ get value() {
15
+ return value;
16
+ },
17
+ set value(next: T) {
18
+ value = next;
19
+ },
20
+ commit() {
21
+ untrack(() => {
22
+ if (Reflect.get(props, key) !== undefined || value === undefined)
23
+ return;
24
+ const setter = Object.getOwnPropertyDescriptor(props, key)?.set;
25
+ if (setter) Reflect.apply(setter, props, [value]);
26
+ });
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,113 @@
1
+ /** Compiler-owned synchronous SSR subscriptions; completion is explicit after serialization. */
2
+ import {
3
+ getOwner,
4
+ onAfterDispose,
5
+ untrack,
6
+ } from "@tschk/moonshine-dom/reactive";
7
+ import { subscribeTo } from "./store-internal";
8
+ import type { StoreSource } from "./store-binding";
9
+ export type ServerStoreReference<T> = {
10
+ get(store: StoreSource<T> | null | undefined): T | undefined;
11
+ releaseIfChanged<S extends StoreSource<T> | null | undefined>(store: S): S;
12
+ };
13
+ export type ServerStoreReferences = {
14
+ create<T>(): ServerStoreReference<T>;
15
+ finish(): void;
16
+ };
17
+ const noop = () => {};
18
+ export function createServerStoreReferences(): ServerStoreReferences {
19
+ const owner = getOwner();
20
+ if (!owner || owner.disposed)
21
+ throw Error("Server store references require a live native owner");
22
+ const accessed: Array<() => void> = [];
23
+ let closed = false;
24
+ function finish() {
25
+ if (closed) return;
26
+ closed = true;
27
+ let failed = false,
28
+ failure: unknown;
29
+ for (const release of accessed.splice(0)) {
30
+ try {
31
+ release();
32
+ } catch (error) {
33
+ if (!failed) failure = error;
34
+ failed = true;
35
+ }
36
+ }
37
+ if (failed) throw failure;
38
+ }
39
+ // Resource cleanup runs for failed/discarded attempts too; it is not public onDestroy.
40
+ onAfterDispose(finish);
41
+ return {
42
+ finish,
43
+ create<T>(): ServerStoreReference<T> {
44
+ if (closed || owner.disposed)
45
+ throw Error("Cannot create a server store reference after completion");
46
+ let store: StoreSource<T> | null = null,
47
+ current: T | undefined,
48
+ seen = false,
49
+ releasing = false,
50
+ stop: () => void = noop;
51
+ function release() {
52
+ const previous = stop;
53
+ stop = noop;
54
+ const outer = releasing;
55
+ releasing = true;
56
+ try {
57
+ previous();
58
+ } finally {
59
+ releasing = outer;
60
+ }
61
+ }
62
+ function temporary(
63
+ next: StoreSource<T> | null | undefined,
64
+ ): T | undefined {
65
+ if (next == null) return undefined;
66
+ let value: T | undefined;
67
+ const unsubscribe = untrack(() =>
68
+ subscribeTo(next, (v) => (value = v)),
69
+ );
70
+ unsubscribe();
71
+ return value;
72
+ }
73
+ return {
74
+ releaseIfChanged(next) {
75
+ return next;
76
+ },
77
+ get(next) {
78
+ const identity = next ?? null;
79
+ if (seen && store === identity) return current;
80
+ // Late/new reads cannot leak subscriptions from public destroy callbacks.
81
+ if (releasing || closed || (owner.disposed && !owner.disposing))
82
+ return temporary(next);
83
+ if (!seen) {
84
+ seen = true;
85
+ accessed.push(release);
86
+ }
87
+ release();
88
+ if (closed || (owner.disposed && !owner.disposing))
89
+ return temporary(next);
90
+ store = identity;
91
+ current = undefined;
92
+ if (next) {
93
+ let active = true,
94
+ unsubscribe: (() => void) | undefined;
95
+ stop = () => {
96
+ if (!active) return;
97
+ active = false;
98
+ unsubscribe?.();
99
+ };
100
+ unsubscribe = untrack(() =>
101
+ subscribeTo(next, (value) => {
102
+ if (active) current = value;
103
+ }),
104
+ );
105
+ if (!active || closed || (owner.disposed && !owner.disposing))
106
+ unsubscribe();
107
+ }
108
+ return current;
109
+ },
110
+ };
111
+ },
112
+ };
113
+ }