@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 +298 -2
- package/package.json +57 -4
- package/src/app.ts +97 -0
- package/src/compiler-sfc.ts +1 -0
- package/src/context.ts +59 -0
- package/src/error-context.ts +57 -0
- package/src/errors.ts +24 -0
- package/src/events.ts +86 -0
- package/src/framework.ts +1 -0
- package/src/index.ts +42 -0
- package/src/lifecycle.ts +59 -0
- package/src/normalize.ts +111 -0
- package/src/props.ts +471 -0
- package/src/reactive.ts +390 -0
- package/src/render.ts +365 -0
- package/src/shims.d.ts +17 -0
- package/src/slots.ts +82 -0
- package/src/text.ts +56 -0
- package/src/vue.ts +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,299 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@tschk/moonshine-vue`
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|