@tschk/moonshine-vue 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/README.md CHANGED
@@ -1,3 +1,299 @@
1
- # Temporary Holding Version
1
+ # `@tschk/moonshine-vue`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The legacy `/vue` and `/compiler-sfc` entries re-export Vue as a peer.
4
+ At runtime, the root bridges and independent `/reactive` entry depend only on Moonshine's
5
+ signal kernel and native DOM package. They import no Vue, React, WASM, or Crepus
6
+ runtime. Vue remains an optional peer for the two legacy entries.
7
+
8
+ Strict TypeScript checking of the root entry requires installing
9
+ `@tschk/moonshine-framework`, even when importing only a bridge: the root also
10
+ exports `crepusRenderer` with the framework's `Renderer` contract. The framework
11
+ remains optional for runtime imports and bridge-only browser/Bun/Node bundles.
12
+ This requirement does not apply to the signal-only `@tschk/moonshine` core entry.
13
+
14
+ The native entry is a bounded composition API implementation. It is not a
15
+ replacement for Vue's component/compiler ABI: do not alias `vue` wholesale onto
16
+ this package or execute existing Vue compiler output against it.
17
+
18
+ ```ts
19
+ import { fromMoonshineSignal } from "@tschk/moonshine-vue";
20
+ import { ref } from "@tschk/moonshine-vue/reactive";
21
+ ```
22
+
23
+ | Subpath | Provides |
24
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------- |
25
+ | `.` | signal bridges, lazy native SSR compatibility renderer |
26
+ | `./vue` | `vue` runtime re-exports |
27
+ | `./compiler-sfc` | `vue/compiler-sfc` re-exports |
28
+ | `./reactive` | native refs/`shallowRef`, `toRef`/`toRefs`/`unref`/`isRef`, `reactive`/`isReactive`, `computed`, synchronous `watch` |
29
+ | `./context` | native component `provide`, `inject`, `hasInjectionContext` |
30
+ | `./text` | native DOM and SSR interpolation conversion |
31
+ | `./props` | compiler-facing shallow declared props and owned parent refresh |
32
+ | `./events` | compiler-facing typed component callbacks, callback arrays and per-instance once delivery |
33
+ | `./normalize` | native class/style normalization and compiler-facing DOM/SSR policies |
34
+
35
+ `./framework` exports the same lazy `crepusRenderer` compatibility name as the
36
+ root. Its optional `@tschk/moonshine-framework` peer is needed only when rendering.
37
+ Pages must use explicit `@tschk/moonshine-dom/ssr` server factories. Existing
38
+ React IR pages need to be ported; this facade does not render React components.
39
+ The compatibility root renderer loads source files under Bun. Bundled deployments use
40
+ `@tschk/moonshine-dom/framework-native` with their bundled module table.
41
+
42
+ Legacy peer: `vue` >= 3, needed only for `/vue` and `/compiler-sfc`.
43
+
44
+ Native `watch` requires `{ flush: "sync" }`; component scheduling, deep watches,
45
+ and Vue collection proxies are not implemented by this entry. Plain objects and
46
+ arrays are reactive; nested refs unwrap on object properties and retain their
47
+ identity at integer array entries.
48
+ Each watcher run can register multiple cleanup callbacks. Reruns and stops drain
49
+ them in registration order, attempt all callbacks after a failure, and preserve
50
+ the first error. A cleanup that stops its watcher prevents another callback run.
51
+
52
+ ### Optional native watch errors
53
+
54
+ `./errors` exports `withComponentErrorHandler(handler, setup)`, which requires a
55
+ live native owner and scopes work created synchronously by `setup`. It handles
56
+ function-source getter errors, `watch` callbacks, `watchSyncEffect` bodies and each
57
+ registered watcher cleanup. The handler runs synchronously and untracked with the
58
+ original thrown value and `{ component, phase }`. The finite phase labels are
59
+ `"watch getter"`, `"watch callback"`, `"watch effect"` and `"watch cleanup"`.
60
+ `component` is the native frame or `undefined`, not a Vue public instance.
61
+ Getter/effect bodies keep their normal tracking; handled getter errors yield
62
+ `undefined`, and handled cleanup errors allow peers and the next run to continue.
63
+ Source reads do not restart recursively when getter reporting writes the source;
64
+ callback feedback remains synchronous and reentrant.
65
+
66
+ Returned promises with callable `then` and `catch` report rejection through the
67
+ captured handler/frame, including after owner disposal. A then-only object is
68
+ ignored. A throwing `.catch` method escapes its caller. A handler that throws
69
+ logs its secondary error once; genuine reentrant watch errors invoke it again.
70
+
71
+ Without this scope, existing native throwing and `catchError` behavior remains.
72
+ Other error phases retain their existing handling. This is not Vue's app
73
+ configuration or `onErrorCaptured`, and adds no watcher or component frame.
74
+
75
+ `toRef` preserves existing native refs, creates live writable property aliases,
76
+ and wraps getters in readonly refs without caching their results. Its optional
77
+ property default applies only to `undefined`; an alias does not make a plain
78
+ source object reactive. Function-valued properties and
79
+ defaults stay literal. `unref` unwraps native refs once and never calls functions.
80
+ `isRef` recognizes only this entry's native refs, including computed/getter refs;
81
+ it does not accept Vue host refs or objects with a spoofed `__v_isRef` flag.
82
+
83
+ `shallowRef` stores raw values unchanged and tracks replacement of `.value` using
84
+ `Object.is` equality. It preserves existing native refs, existing reactive proxies,
85
+ and literal function values. Nested raw mutations do not notify; an already
86
+ reactive proxy keeps its own tracking. This supports external snapshots without
87
+ deep conversion. `triggerRef` is not implemented.
88
+
89
+ Native computed refs are lazy and cached, propagate previous successful values,
90
+ and keep writable setters. Stopping an owner releases its watchers without
91
+ freezing separately retained computed refs.
92
+
93
+ The local Crepuscularity compiler now lowers the bounded typed setup counter
94
+ through real DOM/SSR module graphs without Vue. It supports imported native
95
+ ref/computed helpers, stable function handlers, interpolation and live props;
96
+ strict public prop types and independent instances are verified. Inline and
97
+ imported finite prop interfaces support shallow local writes/deletes and
98
+ coalesced parent refresh. Scoped/module styles, entities, unsupported whitespace, dynamic
99
+ handlers, non-finite defaults and unsupported Boolean prop variants still diagnose. Nested Vue render
100
+ scheduling, effectful parent getter timing, general component composition and
101
+ the VNode ABI remain incomplete. These compiler changes are local and unreleased.
102
+
103
+ Native `./context` provides `provide`, `inject`, `hasInjectionContext`, and typed
104
+ `InjectionKey` symbols during synchronous native component initialization. Injection
105
+ searches ancestors, never the same component's own providers. Nearest ancestors
106
+ shadow outer values, including explicitly provided `undefined`; refs stay refs.
107
+ Defaults are values unless the third argument is `true`, which invokes a missing
108
+ value's factory untracked. Factories have no Vue public-instance `this` receiver.
109
+ Outside initialization, `provide`/`inject` throw; `hasInjectionContext` returns false.
110
+ This supports DOM and SSR component scopes, not Vue app-level injection or
111
+ `app.runWithContext`, and does not establish full SFC compiler compatibility.
112
+
113
+ ### Optional native lifecycle
114
+
115
+ `@tschk/moonshine-vue/lifecycle` exports `onMounted`, `onBeforeUnmount`, and
116
+ `onUnmounted`. Register synchronously during native component initialization, or
117
+ register `onUnmounted` inside an `onMounted` or `onBeforeUnmount` callback.
118
+ SSR skips all three. Mounted hooks run after synchronous children and DOM commits,
119
+ with pending component references published first. Disposed components skip queued
120
+ mounts. Before-unmount hooks run parent first with DOM and watchers still active;
121
+ unmounted hooks run child first after DOM removal and reactive teardown. Callbacks
122
+ are untracked; returned cleanup functions are ignored. Mounted callbacks flush
123
+ pending references first, recheck that the owner is live, then invoke the hook
124
+ with its component context. Mounted and before-unmount callbacks restore the previous
125
+ context even when they throw. Registration from an already disposed owner,
126
+ including an unmounted callback, is rejected.
127
+
128
+ This optional entry has no Vue dependency. It does not provide Vue public-instance
129
+ receivers, app lifecycle, update hooks, async component/Suspense scheduling, or
130
+ full SFC compatibility. Crepus lowers named/aliased imports of these three hooks
131
+ in the accepted authored source slice. Permanent DOM/SSR fixtures verify nested
132
+ registration, connected mount/before phases, disconnected child-first cleanup,
133
+ two instances and server callback suppression. Template refs and Vue's full
134
+ error-handler policy remain unproven. A failed native mounted hook tears down
135
+ its owner; the reference can retain the subtree until unmount.
136
+
137
+ Callback failures propagate after pending peers and disposal finish. Native
138
+ exported-instance reference cleanup remains deferred; Vue template-ref lowering
139
+ is still pending.
140
+
141
+ A watch started under an already disposed native owner throws. Ownerless watches
142
+ remain manually stoppable.
143
+
144
+ `./reactive` also exports `watchSyncEffect(effect)`, returning a stop function.
145
+ The effect runs immediately and tracks synchronous reads, including conditional
146
+ sources. Its supplied cleanup registrar accepts multiple callbacks, including
147
+ late registrations while active; registrations after stopping are ignored.
148
+ Cleanup runs untracked before reruns and on stop. Writes made by the effect or
149
+ its cleanup do not recursively invoke that same effect; cleanup writes remain
150
+ visible to the following run. Native owners stop their effects on disposal;
151
+ ownerless effects require manual stopping. SSR sync effects run and stop with the
152
+ request owner. Promise continuations are not tracked.
153
+
154
+ Cleanup attempts every registered callback and propagates the first failure;
155
+ failed effect runs release their registered cleanup while preserving the effect
156
+ error. This follows native error handling rather than Vue's app error handler.
157
+ This entry does not supply scheduled `watchEffect`, pause/resume handles, or a
158
+ Vue component render scheduler.
159
+
160
+ `./reactive` exports `toRefs(object)` for destructuring linked properties without
161
+ losing live reads or writes. It snapshots enumerable string keys (including
162
+ inherited keys), omits symbols and preserves array length and holes. Properties
163
+ added later need `toRef(object, key)`. Existing native refs retain their identity.
164
+ Getter/setter descriptors keep their source receiver; writes to getter-only
165
+ properties throw. This does not implement Vue readonly-proxy warnings or SFC
166
+ compilation.
167
+
168
+ `./text` exports `toDisplayString` for DOM interpolation and
169
+ `toDisplayStringSSR` for SSR. Both preserve Vue's conversion of nullish values,
170
+ native refs, arrays, objects, maps, sets and symbols without invoking function
171
+ values. DOM conversion can return `undefined` when an object's `toJSON` does;
172
+ SSR converts that result to the literal string `"undefined"`. These helpers
173
+ produce text, and the renderer handles HTML escaping. This optional entry adds
174
+ no host dependency and does not establish Vue SFC or VNode compatibility.
175
+
176
+ Reactive object and array property updates use `Object.is`: writing negative
177
+ zero after positive zero notifies, while repeating NaN does not. Nested values
178
+ and accessor setter receivers retain this policy. This is specific to the Vue
179
+ entry; native `createMutable` keeps strict equality by default.
180
+
181
+ `./props` exports compiler-facing `createProps(source, declaredKeys)` and
182
+ `createServerProps(source, declaredKeys)`. Initial props are shallow snapshots
183
+ with own optional keys, preserving nested objects and functions. DOM local
184
+ writes/deletes are reactive; later parent changes coalesce in one owned
185
+ microtask, and unchanged final parent values preserve local overrides. Nested
186
+ props refreshes drain in that same microtask in setup order, so a queued child
187
+ sees its parent's final snapshot. Disposal drops pending refreshes. Server
188
+ snapshots remain independent per request.
189
+ The compiler resolves finite declared keys through its existing safe module
190
+ resolver; unresolved shapes diagnose rather than guessing keys.
191
+
192
+ Imported PascalCase child components now have real CLI DOM/SSR source proof for
193
+ explicit props-only calls. Parent and child snapshots settle in one props
194
+ microtask in either enqueue order while retaining child state, nodes and focus.
195
+ The permanent source gate includes two independent instances and fresh escaped
196
+ SSR requests. Static default slots have the separate source proof below. Typed component events now have separate real CLI DOM/SSR and production browser proof; the permanent source gate includes these cases. Attrs fallthrough remains unsupported. The bounded lifecycle imports above have separate source proof.
197
+ Native disposal removes retained-node
198
+ listeners; Vue can still invoke a listener on a saved detached button.
199
+
200
+ This props-only queue does not implement Vue's nested renderer jobs, `nextTick`
201
+ or immediate DOM timing. Parent getters are observed synchronously; local DOM
202
+ effects retain native timing. Setter failures preserve the first error and
203
+ allow sibling refresh/retry, deliberately avoiding the reference's cleanup bugs.
204
+
205
+ Assigning a reactive child back into a parent preserves its identity and raw
206
+ storage, including array entries and mutation methods. Deep refs, shallow refs,
207
+ and reactive properties all use `Object.is` replacement equality.
208
+
209
+ Plain unscoped Vue CSS now travels as inventoried build assets through the normal
210
+ Bun stylesheet output and SSR route links. Repeated component instances share
211
+ the stylesheet; whitespace-only blocks emit no assets. The 458-module source
212
+ gate covers unchanged CSS bytes, strict generated types, browser colors and
213
+ fresh escaped SSR. No Vue runtime is imported for this path.
214
+
215
+ Scoped/module CSS, preprocessors, external styles, URLs, imports, escapes and
216
+ `v-bind` CSS still reject. The graph reader uses Bun's CSS parser and reads only
217
+ the declared inventory; this does not claim PostCSS validation parity.
218
+
219
+ The optional compiler-private `./slots` entry supports implicit static default
220
+ content and static default outlets. Authored source tests verify repeated
221
+ outlets, lazy fallback, child consumer injection, live parent values, child prop
222
+ refresh, retained leaf state/focus and independent instances through DOM and SSR.
223
+ DOM content owns one attached scope for cache lifetime and failed-factory
224
+ rollback; it adds no component frame. Ordinary Vue modules import no slot helper.
225
+
226
+ The shared permanent gate covers 734 generated modules and all 728 emitted
227
+ TypeScript modules under both strict checkers using frozen combined
228
+ compiler `82e517ca`. Named/scoped/dynamic slots still reject.
229
+ Native granular updates skip unrelated fallback function calls that Vue
230
+ component jobs repeat; those call-count traces deliberately differ. This does
231
+ not supply Vue's component render scheduler or promise full ABI compatibility.
232
+
233
+ The optional `/events` helper lowers inline typed tuple-map `defineEmits` declarations and stable parent `@event` handlers, including `.once`. Declared props exclude event callbacks. DOM callbacks stop with their component owner; SSR callbacks remain callable after rendering, matching the bounded reference cases. Native DOM updates and error routing retain Moonshine semantics. Event validators, model events, arbitrary event names and attrs fallthrough still diagnose. This does not establish Vue scheduler or compiler ABI compatibility.
234
+
235
+ Declared component class/style props now retain literal versus expression provenance in Crepus. The optional `/normalize` entry handles nested class values, static declaration strings, dynamic styles and reactive object cloning with separate DOM/SSR policies. Fresh actual Vue 3.5.13 comparisons pass Unicode whitespace, falsy values, constant aliases, proxy identity, narrow typed styles, retained local state/focus, cleanup and two fresh SSR renders. Intrinsic dynamic class/style, undeclared fallthrough and other directives remain separate work. Merged `__proto__` style keys are safe own data properties; this intentionally differs from the reference prototype setter.
236
+
237
+ Boolean-bearing finite `defineProps` types now use compiler-only
238
+ `createBooleanProps` / `createServerBooleanProps` from `/props`; ordinary props
239
+ use their original exports and omit the resolver from bundles. The compiler
240
+ retains Boolean/String union order, distinguishes absence from explicit
241
+ undefined, accepts proven declared ASCII hyphen aliases and precasts empty/name
242
+ literals without widening public prop types. Pure optional Boolean setup values
243
+ are internally required; mixed unions remain optional. Fresh source graphs pass
244
+ both strict checkers for both targets and actual Vue 3.5.13 Chromium/SSR comparisons.
245
+
246
+ Duplicate canonical/alias bindings, ambiguous Boolean intersections, literal
247
+ hyphen declarations mixed with casts, Boolean id/class/style declarations,
248
+ non-ASCII nonempty name-literal casts remain explicit gaps.
249
+ Finite defaults have separate shallow readonly typing; full Vue render jobs and broader macro typing remain incomplete.
250
+
251
+ ### Compiler-facing defaults
252
+
253
+ The optional `/props` entry includes `createDefaultProps`,
254
+ `createServerDefaultProps`, `DefaultValues`, and `withVueScheduler` for native
255
+ compiler integration. The finite default tuples specify literal values versus
256
+ factories. Factories receive each instance's stable raw props object, run in its
257
+ consumer context, and cache successful results including `undefined`. Setup
258
+ props are shallow readonly in TypeScript; the caller's props remain optional
259
+ where declared. These helpers do not install runtime readonly proxies.
260
+
261
+ The component carrier gates render work while defaults prepare. Successful
262
+ preparation refreshes that component's render callbacks; failed preparation
263
+ keeps its previous DOM until a later successful parent props commit. Independent
264
+ child component groups remain live. It reuses existing native owners and DOM
265
+ controls and guards feedback at 1,000 render or post waves. Ordinary native DOM
266
+ rendering remains synchronous without the carrier.
267
+
268
+ Finite typed source `withDefaults` now has real CLI, strict caller, Chromium and
269
+ fresh SSR acceptance, including ordered factories, cached undefined, Boolean
270
+ fallbacks and failed refresh recovery. Spreads, accessors, setup-value captures,
271
+ nested macros and unresolved default shapes still diagnose. Outer casts and
272
+ satisfies constraints retain their type checks.
273
+
274
+ This is a compiler-private contract, not Vue's complete component job or app
275
+ error policy. Render-expression evaluation and fallback, props-job error
276
+ attribution, and full Vue lifecycle timing remain separate work. The optional `/errors` scope handles watch phases; it does not
277
+ turn a default factory failure into an app error handler call.
278
+
279
+ ### Optional native application mount
280
+
281
+ `@tschk/moonshine-vue/app` exports `createApp(component, props)` for native
282
+ component factories, including Crepus-compiled Vue components. Props retain the
283
+ factory's public type. The returned native application has only `mount(target)`
284
+ and `unmount()`; `mount` returns `void`, not a Vue public instance.
285
+
286
+ `mount` accepts a selector, Element or ShadowRoot in the current DOM realm,
287
+ clears its existing children and mounts one owned root. A private container lease
288
+ rejects another app before clearing and remains held until teardown finishes.
289
+ Invalid targets leave
290
+ DOM unchanged and allow a corrected attempt. Once rendering starts, a failed or
291
+ successful attempt is terminal; mounting again throws. `unmount` is idempotent;
292
+ before mounting it is a no-op, and during synchronous mounting it requests
293
+ cleanup when that mount finishes. Failed setup/mounting and rejected promise or
294
+ VNode results release owned work. Accessor children retain native tracking.
295
+
296
+ This entry adds no Vue scheduler group, VNode renderer, Options API, application
297
+ configuration, public instance, runtime template compiler or hydration. Use native
298
+ factories and native DOM children; the separate legacy `/vue` and `/compiler-sfc`
299
+ entries and their optional peer are unchanged.
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-vue",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.4",
4
+ "description": "Native composition helpers and signal bridges with optional legacy Vue runtime/compiler entries",
5
+ "license": "ISC",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./vue": "./src/vue.ts",
12
+ "./reactive": "./src/reactive.ts",
13
+ "./compiler-sfc": "./src/compiler-sfc.ts",
14
+ "./framework": "./src/framework.ts",
15
+ "./context": "./src/context.ts",
16
+ "./lifecycle": "./src/lifecycle.ts",
17
+ "./text": "./src/text.ts",
18
+ "./props": "./src/props.ts",
19
+ "./slots": "./src/slots.ts",
20
+ "./events": "./src/events.ts",
21
+ "./normalize": "./src/normalize.ts",
22
+ "./errors": "./src/errors.ts",
23
+ "./app": "./src/app.ts"
24
+ },
25
+ "dependencies": {
26
+ "@tschk/moonshine": "^0.4.4",
27
+ "@tschk/moonshine-dom": "^0.4.4"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "7.0.2"
31
+ },
32
+ "peerDependencies": {
33
+ "vue": ">=3",
34
+ "@tschk/moonshine-framework": "^0.4.4"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "vue": {
38
+ "optional": true
39
+ },
40
+ "@tschk/moonshine-framework": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "files": [
45
+ "src",
46
+ "README.md"
47
+ ],
48
+ "keywords": [
49
+ "moonshine",
50
+ "vue",
51
+ "adapter"
52
+ ],
53
+ "scripts": {
54
+ "format": "bunx prettier --check .",
55
+ "lint": "bunx oxlint .",
56
+ "typecheck": "bunx tsc --noEmit",
57
+ "test": "bun test"
58
+ }
59
+ }
package/src/app.ts ADDED
@@ -0,0 +1,97 @@
1
+ import { h, render, type DOMChild } from "@tschk/moonshine-dom/dom";
2
+
3
+ export interface NativeApplication {
4
+ mount(target: string | Element | ShadowRoot): void;
5
+ unmount(): void;
6
+ }
7
+
8
+ const leases = /* @__PURE__ */ new WeakMap<
9
+ Element | ShadowRoot,
10
+ NativeApplication
11
+ >();
12
+
13
+ // Validate async/VNode results without evaluating accessor children ahead of insert.
14
+ function content(value: DOMChild): DOMChild {
15
+ if (typeof value === "function") return () => content(value());
16
+ if (Array.isArray(value)) return value.map(content);
17
+ if (value !== null && typeof value === "object" && !(value instanceof Node)) {
18
+ if (value instanceof Promise)
19
+ void Promise.prototype.then.call(value, undefined, () => {});
20
+ throw new TypeError(
21
+ "Native applications require DOM children, not promises or VNodes",
22
+ );
23
+ }
24
+ return value;
25
+ }
26
+
27
+ /** Mount a native compiled/component factory; this does not execute Vue VNodes. */
28
+ export function createApp<P extends object>(
29
+ component: (props: P) => DOMChild,
30
+ props: P,
31
+ ): NativeApplication {
32
+ if (typeof component !== "function")
33
+ throw new TypeError("Native applications require a component factory");
34
+ if (props === null || typeof props !== "object" || Array.isArray(props))
35
+ throw new TypeError("Native application props must be an object");
36
+ let state: "new" | "mounting" | "mounted" | "stopped" = "new";
37
+ let stop: (() => void) | undefined;
38
+ const stopped = () => state === "stopped";
39
+ const unmount = () => {
40
+ if (state === "new" || state === "stopped") return;
41
+ state = "stopped";
42
+ const dispose = stop;
43
+ stop = undefined;
44
+ dispose?.();
45
+ };
46
+ const app: NativeApplication = {
47
+ mount(target) {
48
+ if (state !== "new")
49
+ throw new Error("Native application cannot be mounted again");
50
+ if (typeof document === "undefined")
51
+ throw new Error("Native application mount requires a DOM");
52
+ const container =
53
+ typeof target === "string" ? document.querySelector(target) : target;
54
+ if (!(container instanceof Element) && !(container instanceof ShadowRoot))
55
+ throw new TypeError(
56
+ "Native application mount target was not found or is invalid",
57
+ );
58
+ if (leases.has(container))
59
+ throw new Error("Native application mount target is already owned");
60
+ const release = () => {
61
+ if (leases.get(container) === app) leases.delete(container);
62
+ };
63
+ // Clearing and disposal can synchronously reenter another app mount.
64
+ leases.set(container, app);
65
+ state = "mounting";
66
+ try {
67
+ container.replaceChildren();
68
+ if (stopped()) {
69
+ release();
70
+ return;
71
+ }
72
+ const dispose = render(
73
+ () => h((input: P) => content(component(input)), props),
74
+ container,
75
+ );
76
+ const finish = () => {
77
+ try {
78
+ dispose();
79
+ } finally {
80
+ release();
81
+ }
82
+ };
83
+ if (stopped()) finish();
84
+ else {
85
+ stop = finish;
86
+ state = "mounted";
87
+ }
88
+ } catch (error) {
89
+ release();
90
+ state = "stopped";
91
+ throw error;
92
+ }
93
+ },
94
+ unmount,
95
+ };
96
+ return app;
97
+ }
@@ -0,0 +1 @@
1
+ export { parse, compileScript, compileTemplate } from "vue/compiler-sfc";
package/src/context.ts ADDED
@@ -0,0 +1,59 @@
1
+ /** Native component injection; no Vue app or public-instance runtime. */
2
+ import {
3
+ getComponentContext,
4
+ peekComponentContext,
5
+ setContext,
6
+ type ComponentContext,
7
+ } from "@tschk/moonshine-dom/component";
8
+ import { untrack } from "@tschk/moonshine-dom/reactive";
9
+
10
+ declare const injectionType: unique symbol;
11
+ export type InjectionKey<T> = symbol & { readonly [injectionType]?: T };
12
+ const providesKey = Symbol("moonshine.vue.provides");
13
+ type Provides = {
14
+ frame: ComponentContext;
15
+ parent?: Provides;
16
+ values: Map<string | symbol, unknown>;
17
+ };
18
+ function layer(frame: ComponentContext): Provides | undefined {
19
+ return frame.contexts.get(providesKey) as Provides | undefined;
20
+ }
21
+ export function provide<T>(key: InjectionKey<T> | string, value: T): void {
22
+ const frame = getComponentContext();
23
+ let current = layer(frame);
24
+ if (current?.frame !== frame) {
25
+ current = { frame, parent: current, values: new Map() };
26
+ setContext(providesKey, current);
27
+ }
28
+ current.values.set(key, value);
29
+ }
30
+ export function inject<T>(key: InjectionKey<T> | string): T | undefined;
31
+ export function inject<T>(
32
+ key: InjectionKey<T> | string,
33
+ defaultValue: T,
34
+ treatDefaultAsFactory?: false,
35
+ ): T;
36
+ export function inject<T>(
37
+ key: InjectionKey<T> | string,
38
+ defaultValue: () => T,
39
+ treatDefaultAsFactory: true,
40
+ ): T;
41
+ export function inject<T>(
42
+ key: InjectionKey<T> | string,
43
+ defaultValue?: T | (() => T),
44
+ treatDefaultAsFactory = false,
45
+ ): T | undefined {
46
+ const frame = getComponentContext();
47
+ let current = layer(frame);
48
+ if (current?.frame === frame) current = current.parent;
49
+ for (; current; current = current.parent) {
50
+ if (current.values.has(key)) return current.values.get(key) as T;
51
+ }
52
+ return treatDefaultAsFactory && typeof defaultValue === "function"
53
+ ? untrack(() => (defaultValue as () => T)())
54
+ : (defaultValue as T | undefined);
55
+ }
56
+ export function hasInjectionContext(): boolean {
57
+ const frame = peekComponentContext();
58
+ return !!frame?.active && !frame.owner.disposed;
59
+ }
@@ -0,0 +1,57 @@
1
+ /** Private callback policy; no Vue app/public-instance ABI. */
2
+ import {
3
+ peekComponentContext,
4
+ type ComponentContext,
5
+ } from "@tschk/moonshine-dom/component";
6
+ import { getOwner, untrack } from "@tschk/moonshine-dom/reactive";
7
+ export type WatchErrorPhase =
8
+ "watch getter" | "watch callback" | "watch effect" | "watch cleanup";
9
+ export type ComponentErrorInfo = {
10
+ readonly component: ComponentContext | undefined;
11
+ readonly phase: WatchErrorPhase;
12
+ };
13
+ export type ComponentErrorHandler = (
14
+ error: unknown,
15
+ info: ComponentErrorInfo,
16
+ ) => void;
17
+ export type ErrorDispatcher = (error: unknown, phase: WatchErrorPhase) => void;
18
+ export const componentErrorKey = /* @__PURE__ */ Symbol(
19
+ "moonshine.vue.callback-error",
20
+ );
21
+ export function captureWatchErrorHandler(): ErrorDispatcher | undefined {
22
+ const handler = getOwner()?.context?.get(componentErrorKey) as
23
+ ComponentErrorHandler | undefined;
24
+ if (!handler) return;
25
+ const component = peekComponentContext();
26
+ return (error, phase) => {
27
+ try {
28
+ untrack(() => handler(error, { component, phase }));
29
+ } catch (secondary) {
30
+ console.error(secondary);
31
+ }
32
+ };
33
+ }
34
+ export function invokeWatch<T>(
35
+ report: ErrorDispatcher,
36
+ phase: WatchErrorPhase,
37
+ fn: () => T,
38
+ ): T | undefined {
39
+ let result: T;
40
+ try {
41
+ result = fn();
42
+ } catch (error) {
43
+ report(error, phase);
44
+ return;
45
+ }
46
+ // Attach outside the user-call guard: a throwing .catch method must escape.
47
+ if (
48
+ result !== null &&
49
+ (typeof result === "object" || typeof result === "function") &&
50
+ typeof (result as { then?: unknown }).then === "function" &&
51
+ typeof (result as { catch?: unknown }).catch === "function"
52
+ )
53
+ (
54
+ result as unknown as { catch(reject: (error: unknown) => void): unknown }
55
+ ).catch((error) => report(error, phase));
56
+ return result;
57
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** Explicit native error scope; does not construct a Vue application. */
2
+ import { createComponentScope, getOwner } from "@tschk/moonshine-dom/reactive";
3
+ import { componentErrorKey, type ComponentErrorHandler } from "./error-context";
4
+ export type {
5
+ ComponentErrorHandler,
6
+ ComponentErrorInfo,
7
+ WatchErrorPhase,
8
+ } from "./error-context";
9
+ export function withComponentErrorHandler<T>(
10
+ handler: ComponentErrorHandler,
11
+ setup: () => T,
12
+ ): T {
13
+ if (typeof handler !== "function" || typeof setup !== "function")
14
+ throw new TypeError("Error handler and setup must be functions");
15
+ const owner = getOwner();
16
+ if (!owner || owner.disposed)
17
+ throw Error("Component error scope requires a live owner");
18
+ return createComponentScope(() => {
19
+ const scope = getOwner()!;
20
+ scope.context = new Map(scope.context);
21
+ scope.context.set(componentErrorKey, handler);
22
+ return setup();
23
+ });
24
+ }