@sigrea/vue 0.5.0 → 0.6.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sigrea
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -33,7 +33,7 @@
33
33
  npm install @sigrea/vue @sigrea/core vue
34
34
  ```
35
35
 
36
- Requires Vue 3.4+ and Node.js 20 or later.
36
+ Requires Vue 3.4+ and Node.js 24 or later.
37
37
 
38
38
  ## Quick Start
39
39
 
@@ -163,11 +163,11 @@ const model = useDeepSignal(profile);
163
163
 
164
164
  ```ts
165
165
  function useSignal<T>(
166
- signal: Signal<T> | ReadonlySignal<T>
166
+ signal: Signal<T> | ReadonlySignal<T> | Computed<T>
167
167
  ): DeepReadonly<ShallowRef<T>>
168
168
  ```
169
169
 
170
- Subscribes to a signal or computed value and returns a readonly Vue ref that updates when the signal changes. The subscription is cleaned up when the component unmounts.
170
+ Subscribes to a signal or computed value and returns a readonly Vue ref that updates when the signal changes. The subscription is cleaned up when the component unmounts, or after server rendering, including any `onServerPrefetch()` work, completes.
171
171
 
172
172
  ### useComputed
173
173
 
@@ -175,7 +175,7 @@ Subscribes to a signal or computed value and returns a readonly Vue ref that upd
175
175
  function useComputed<T>(source: Computed<T>): DeepReadonly<ShallowRef<T>>
176
176
  ```
177
177
 
178
- Subscribes to a computed value and returns a readonly Vue ref that updates when the computed value changes. The subscription is cleaned up when the component unmounts.
178
+ Subscribes to a computed value and returns a readonly Vue ref that updates when the computed value changes. The subscription is cleaned up when the component unmounts, or after server rendering, including any `onServerPrefetch()` work, completes.
179
179
 
180
180
  ### useDeepSignal
181
181
 
@@ -183,7 +183,7 @@ Subscribes to a computed value and returns a readonly Vue ref that updates when
183
183
  function useDeepSignal<T extends object>(signal: DeepSignal<T>): ShallowRef<T>
184
184
  ```
185
185
 
186
- Subscribes to a deep signal and returns a mutable Vue ref. Updates to the deep signal trigger reactivity, and the subscription is cleaned up when the component unmounts. Templates unwrap the ref automatically, so accessing nested properties requires no `.value`. In script blocks, use `state.value` to access the underlying object.
186
+ Subscribes to a deep signal and returns a mutable Vue ref. Updates to the deep signal trigger reactivity, and the subscription is cleaned up when the component unmounts, or after server rendering, including any `onServerPrefetch()` work, completes. Templates unwrap the ref automatically, so accessing nested properties requires no `.value`. In script blocks, use `state.value` to access the underlying object.
187
187
 
188
188
  ### useMutableSignal
189
189
 
@@ -204,6 +204,10 @@ function useMolecule<TReturn extends object, TProps extends object | void = void
204
204
 
205
205
  Mounts a molecule factory and returns its MoleculeInstance. Sigrea augments the molecule with lifecycle metadata: `onMount` callbacks run after the component mounts, and `onUnmount` callbacks run before it unmounts.
206
206
 
207
+ **Server Rendering**
208
+
209
+ During server rendering, `useMolecule` creates the molecule instance for the render pass but does not mount it. `onMount`, `watch`, `watchEffect`, `onActivated`, and `onDeactivated` do not run on the server. Unmounted instances created during SSR are disposed automatically in a microtask after server rendering, including any `onServerPrefetch()` work, completes.
210
+
207
211
  **KeepAlive Support**
208
212
 
209
213
  When used inside Vue's `<KeepAlive>`, molecule side effects are automatically managed for optimal resource efficiency:
@@ -264,7 +268,7 @@ createApp(App).mount("#app");
264
268
 
265
269
  ## Development
266
270
 
267
- This repo targets Node.js 20 or later.
271
+ This repo targets Node.js 24 or later.
268
272
 
269
273
  If you use mise:
270
274
 
package/dist/index.cjs CHANGED
@@ -3,6 +3,109 @@
3
3
  const vue = require('vue');
4
4
  const core = require('@sigrea/core');
5
5
 
6
+ const IS_SERVER = typeof window === "undefined";
7
+ const cleanupStates = /* @__PURE__ */ new WeakMap();
8
+ const wrappedSsrRenderComponents = /* @__PURE__ */ new WeakSet();
9
+ function getCleanupState(instance) {
10
+ const existing = cleanupStates.get(instance);
11
+ if (existing !== void 0) {
12
+ return existing;
13
+ }
14
+ const state = {
15
+ cleanups: [],
16
+ registered: false,
17
+ renderWrapped: false,
18
+ flushed: false
19
+ };
20
+ cleanupStates.set(instance, state);
21
+ return state;
22
+ }
23
+ function flushSsrCleanups(instance, state) {
24
+ if (state.flushed) {
25
+ return;
26
+ }
27
+ state.flushed = true;
28
+ cleanupStates.delete(instance);
29
+ const errors = [];
30
+ const cleanups = state.cleanups.splice(0);
31
+ for (const cleanup of cleanups) {
32
+ try {
33
+ cleanup();
34
+ } catch (error) {
35
+ errors.push(error);
36
+ }
37
+ }
38
+ if (errors.length === 1) {
39
+ throw errors[0];
40
+ }
41
+ if (errors.length > 1) {
42
+ throw new AggregateError(errors, "Failed to run SSR cleanups.");
43
+ }
44
+ }
45
+ function queueSsrCleanup(instance, state) {
46
+ queueMicrotask(() => {
47
+ flushSsrCleanups(instance, state);
48
+ });
49
+ }
50
+ function wrapInstanceRender(instance, state) {
51
+ if (state.renderWrapped || state.flushed) {
52
+ return;
53
+ }
54
+ const renderInstance = instance;
55
+ const render = renderInstance.render;
56
+ if (typeof render !== "function") {
57
+ return;
58
+ }
59
+ state.renderWrapped = true;
60
+ renderInstance.render = ((...args) => {
61
+ try {
62
+ return render(...args);
63
+ } finally {
64
+ queueSsrCleanup(instance, state);
65
+ }
66
+ });
67
+ }
68
+ function wrapComponentSsrRender(component) {
69
+ if (wrappedSsrRenderComponents.has(component)) {
70
+ return;
71
+ }
72
+ const ssrRender = component.ssrRender;
73
+ if (typeof ssrRender !== "function") {
74
+ return;
75
+ }
76
+ wrappedSsrRenderComponents.add(component);
77
+ component.ssrRender = ((...args) => {
78
+ const instance = vue.getCurrentInstance();
79
+ const state = instance === null ? void 0 : cleanupStates.get(instance);
80
+ try {
81
+ return ssrRender(...args);
82
+ } finally {
83
+ if (instance !== null && state !== void 0) {
84
+ queueSsrCleanup(instance, state);
85
+ }
86
+ }
87
+ });
88
+ }
89
+ function registerSsrCleanup(cleanup) {
90
+ if (!IS_SERVER) {
91
+ return;
92
+ }
93
+ const instance = vue.getCurrentInstance();
94
+ if (instance === null) {
95
+ return;
96
+ }
97
+ const state = getCleanupState(instance);
98
+ state.cleanups.push(cleanup);
99
+ if (state.registered) {
100
+ return;
101
+ }
102
+ state.registered = true;
103
+ vue.onServerPrefetch(() => {
104
+ wrapInstanceRender(instance, state);
105
+ wrapComponentSsrRender(instance.type);
106
+ });
107
+ }
108
+
6
109
  function useMolecule(molecule, ...args) {
7
110
  if (vue.getCurrentInstance() === null) {
8
111
  throw new Error(
@@ -16,6 +119,15 @@ function useMolecule(molecule, ...args) {
16
119
  const snapshot = props === void 0 ? void 0 : { ...vue.toRaw(props) };
17
120
  const moleculeArgs = snapshot === void 0 ? [] : [snapshot];
18
121
  const instance = molecule(...moleculeArgs);
122
+ const disposeState = { disposed: false };
123
+ const dispose = () => {
124
+ if (disposeState.disposed) {
125
+ return;
126
+ }
127
+ disposeState.disposed = true;
128
+ core.disposeMolecule(instance);
129
+ };
130
+ registerSsrCleanup(dispose);
19
131
  vue.onMounted(() => {
20
132
  core.mountMolecule(instance);
21
133
  });
@@ -29,7 +141,7 @@ function useMolecule(molecule, ...args) {
29
141
  core.unmountMolecule(instance);
30
142
  });
31
143
  vue.onScopeDispose(() => {
32
- core.disposeMolecule(instance);
144
+ dispose();
33
145
  });
34
146
  return instance;
35
147
  }
@@ -50,8 +162,17 @@ function useSnapshot(handler, options) {
50
162
  vue.triggerRef(state);
51
163
  };
52
164
  const unsubscribe = handler.subscribe(update);
53
- vue.onScopeDispose(() => {
165
+ const unsubscribeState = { active: true };
166
+ const cleanup = () => {
167
+ if (!unsubscribeState.active) {
168
+ return;
169
+ }
170
+ unsubscribeState.active = false;
54
171
  unsubscribe();
172
+ };
173
+ registerSsrCleanup(cleanup);
174
+ vue.onScopeDispose(() => {
175
+ cleanup();
55
176
  });
56
177
  if (options?.mode === "mutable") {
57
178
  return state;
package/dist/index.mjs CHANGED
@@ -1,6 +1,109 @@
1
- import { getCurrentInstance, toRaw, onMounted, onActivated, onDeactivated, onBeforeUnmount, onScopeDispose, shallowRef, readonly, triggerRef, computed } from 'vue';
1
+ import { getCurrentInstance, onServerPrefetch, toRaw, onMounted, onActivated, onDeactivated, onBeforeUnmount, onScopeDispose, shallowRef, readonly, triggerRef, computed } from 'vue';
2
2
  import { mountMolecule, unmountMolecule, disposeMolecule, createSignalHandler, createComputedHandler, createDeepSignalHandler } from '@sigrea/core';
3
3
 
4
+ const IS_SERVER = typeof window === "undefined";
5
+ const cleanupStates = /* @__PURE__ */ new WeakMap();
6
+ const wrappedSsrRenderComponents = /* @__PURE__ */ new WeakSet();
7
+ function getCleanupState(instance) {
8
+ const existing = cleanupStates.get(instance);
9
+ if (existing !== void 0) {
10
+ return existing;
11
+ }
12
+ const state = {
13
+ cleanups: [],
14
+ registered: false,
15
+ renderWrapped: false,
16
+ flushed: false
17
+ };
18
+ cleanupStates.set(instance, state);
19
+ return state;
20
+ }
21
+ function flushSsrCleanups(instance, state) {
22
+ if (state.flushed) {
23
+ return;
24
+ }
25
+ state.flushed = true;
26
+ cleanupStates.delete(instance);
27
+ const errors = [];
28
+ const cleanups = state.cleanups.splice(0);
29
+ for (const cleanup of cleanups) {
30
+ try {
31
+ cleanup();
32
+ } catch (error) {
33
+ errors.push(error);
34
+ }
35
+ }
36
+ if (errors.length === 1) {
37
+ throw errors[0];
38
+ }
39
+ if (errors.length > 1) {
40
+ throw new AggregateError(errors, "Failed to run SSR cleanups.");
41
+ }
42
+ }
43
+ function queueSsrCleanup(instance, state) {
44
+ queueMicrotask(() => {
45
+ flushSsrCleanups(instance, state);
46
+ });
47
+ }
48
+ function wrapInstanceRender(instance, state) {
49
+ if (state.renderWrapped || state.flushed) {
50
+ return;
51
+ }
52
+ const renderInstance = instance;
53
+ const render = renderInstance.render;
54
+ if (typeof render !== "function") {
55
+ return;
56
+ }
57
+ state.renderWrapped = true;
58
+ renderInstance.render = ((...args) => {
59
+ try {
60
+ return render(...args);
61
+ } finally {
62
+ queueSsrCleanup(instance, state);
63
+ }
64
+ });
65
+ }
66
+ function wrapComponentSsrRender(component) {
67
+ if (wrappedSsrRenderComponents.has(component)) {
68
+ return;
69
+ }
70
+ const ssrRender = component.ssrRender;
71
+ if (typeof ssrRender !== "function") {
72
+ return;
73
+ }
74
+ wrappedSsrRenderComponents.add(component);
75
+ component.ssrRender = ((...args) => {
76
+ const instance = getCurrentInstance();
77
+ const state = instance === null ? void 0 : cleanupStates.get(instance);
78
+ try {
79
+ return ssrRender(...args);
80
+ } finally {
81
+ if (instance !== null && state !== void 0) {
82
+ queueSsrCleanup(instance, state);
83
+ }
84
+ }
85
+ });
86
+ }
87
+ function registerSsrCleanup(cleanup) {
88
+ if (!IS_SERVER) {
89
+ return;
90
+ }
91
+ const instance = getCurrentInstance();
92
+ if (instance === null) {
93
+ return;
94
+ }
95
+ const state = getCleanupState(instance);
96
+ state.cleanups.push(cleanup);
97
+ if (state.registered) {
98
+ return;
99
+ }
100
+ state.registered = true;
101
+ onServerPrefetch(() => {
102
+ wrapInstanceRender(instance, state);
103
+ wrapComponentSsrRender(instance.type);
104
+ });
105
+ }
106
+
4
107
  function useMolecule(molecule, ...args) {
5
108
  if (getCurrentInstance() === null) {
6
109
  throw new Error(
@@ -14,6 +117,15 @@ function useMolecule(molecule, ...args) {
14
117
  const snapshot = props === void 0 ? void 0 : { ...toRaw(props) };
15
118
  const moleculeArgs = snapshot === void 0 ? [] : [snapshot];
16
119
  const instance = molecule(...moleculeArgs);
120
+ const disposeState = { disposed: false };
121
+ const dispose = () => {
122
+ if (disposeState.disposed) {
123
+ return;
124
+ }
125
+ disposeState.disposed = true;
126
+ disposeMolecule(instance);
127
+ };
128
+ registerSsrCleanup(dispose);
17
129
  onMounted(() => {
18
130
  mountMolecule(instance);
19
131
  });
@@ -27,7 +139,7 @@ function useMolecule(molecule, ...args) {
27
139
  unmountMolecule(instance);
28
140
  });
29
141
  onScopeDispose(() => {
30
- disposeMolecule(instance);
142
+ dispose();
31
143
  });
32
144
  return instance;
33
145
  }
@@ -48,8 +160,17 @@ function useSnapshot(handler, options) {
48
160
  triggerRef(state);
49
161
  };
50
162
  const unsubscribe = handler.subscribe(update);
51
- onScopeDispose(() => {
163
+ const unsubscribeState = { active: true };
164
+ const cleanup = () => {
165
+ if (!unsubscribeState.active) {
166
+ return;
167
+ }
168
+ unsubscribeState.active = false;
52
169
  unsubscribe();
170
+ };
171
+ registerSsrCleanup(cleanup);
172
+ onScopeDispose(() => {
173
+ cleanup();
53
174
  });
54
175
  if (options?.mode === "mutable") {
55
176
  return state;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigrea/vue",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Vue adapter bindings for Sigrea molecule modules.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/sigrea/vue/issues"
18
18
  },
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=24"
21
21
  },
22
22
  "sideEffects": false,
23
23
  "exports": {
@@ -29,16 +29,8 @@
29
29
  },
30
30
  "main": "./dist/index.cjs",
31
31
  "types": "./dist/index.d.ts",
32
- "files": [
33
- "dist"
34
- ],
35
- "keywords": [
36
- "signals",
37
- "reactivity",
38
- "vue",
39
- "molecule",
40
- "typescript"
41
- ],
32
+ "files": ["dist"],
33
+ "keywords": ["signals", "reactivity", "vue", "molecule", "typescript"],
42
34
  "scripts": {
43
35
  "dev": "vite --config playground/vite.config.ts",
44
36
  "build": "unbuild",
@@ -53,12 +45,12 @@
53
45
  "cicheck": "pnpm test && pnpm typecheck && pnpm format:fix"
54
46
  },
55
47
  "peerDependencies": {
56
- "@sigrea/core": "^0.5.0",
48
+ "@sigrea/core": "^0.6.0",
57
49
  "vue": "^3.4.0"
58
50
  },
59
51
  "devDependencies": {
60
52
  "@biomejs/biome": "1.9.4",
61
- "@sigrea/core": "^0.5.0",
53
+ "@sigrea/core": "^0.6.0",
62
54
  "@vitejs/plugin-vue": "^5.1.4",
63
55
  "@vitest/coverage-v8": "^3.2.4",
64
56
  "@vue/test-utils": "^2.4.0",
@@ -74,9 +66,6 @@
74
66
  "vue": "^3.4.0"
75
67
  },
76
68
  "pnpm": {
77
- "onlyBuiltDependencies": [
78
- "lefthook",
79
- "@biomejs/biome"
80
- ]
69
+ "onlyBuiltDependencies": ["lefthook", "@biomejs/biome"]
81
70
  }
82
71
  }