@rmc-toolkit/vue 0.2.0

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.
@@ -0,0 +1,11 @@
1
+ import type * as VueNamespace from "vue";
2
+ import { type RuntimeHostObservableOptions, type RuntimeHostStatus } from "@rmc-toolkit/core";
3
+ export type UseRuntimeHostOptions = Omit<RuntimeHostObservableOptions, "target">;
4
+ export type UseRuntimeHostResult = {
5
+ target: VueNamespace.Ref<Element | null>;
6
+ status: VueNamespace.Ref<RuntimeHostStatus>;
7
+ };
8
+ export declare const createVueAdapter: (Vue: typeof VueNamespace) => {
9
+ useRuntimeHost(path: () => string, options: UseRuntimeHostOptions): UseRuntimeHostResult;
10
+ };
11
+ //# sourceMappingURL=create-vue-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-vue-adapter.d.ts","sourceRoot":"","sources":["../src/create-vue-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,YAAY,MAAM,KAAK,CAAC;AACzC,OAAO,EAGL,KAAK,4BAA4B,EACjC,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,4BAA4B,EAAE,QAAQ,CAAC,CAAC;AAEjF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,YAAY,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACzC,MAAM,EAAE,YAAY,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;CAC7C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,KAAK,OAAO,YAAY,KACvB;IACD,cAAc,CAAC,IAAI,EAAE,MAAM,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,oBAAoB,CAAC;CA4D1F,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { createRuntimeHostObservable, } from "@rmc-toolkit/core";
2
+ export const createVueAdapter = (Vue) => {
3
+ const useRuntimeHost = (path, options) => {
4
+ const target = Vue.ref(null);
5
+ const status = Vue.ref({ type: "idle" });
6
+ let observable = null;
7
+ let unsubscribe = null;
8
+ Vue.onMounted(() => {
9
+ if (!target.value) {
10
+ return;
11
+ }
12
+ observable = createRuntimeHostObservable({ ...options, target: target.value });
13
+ // Assigning .value here from an externally-triggered callback (not
14
+ // during a component render or a computed/effect run) is safe by
15
+ // design: a ref's setter just updates its value and notifies its
16
+ // subscribers, and doesn't require an active reactive tracking
17
+ // context. No nextTick() or other synchronization is needed, the same
18
+ // way it wouldn't be for a WebSocket message handler updating a ref.
19
+ unsubscribe = observable.subscribe((next) => {
20
+ status.value = next;
21
+ });
22
+ // Forward the initial path now that the observable exists. Reading the
23
+ // observable's own getSnapshot() first would also work, but calling
24
+ // next() unconditionally here mirrors the watcher below and guarantees
25
+ // the first navigation happens exactly once, right after creation.
26
+ observable.next(path());
27
+ });
28
+ // Deliberately a separate watcher rather than folded into onMounted: the
29
+ // watch callback registered here has no `{ immediate: true }`, so it
30
+ // never fires synchronously during setup — it only fires on a later
31
+ // reactive change, which cannot happen before mount completes and
32
+ // onMounted's callback has already run and assigned `observable`. Do not
33
+ // add `{ immediate: true }` to this watch call — that would let it fire
34
+ // during setup, before observable is assigned, and (once mount does
35
+ // complete) would also race a duplicate next(path()) against the one in
36
+ // onMounted above.
37
+ Vue.watch(path, (newPath) => {
38
+ observable?.next(newPath);
39
+ });
40
+ Vue.onUnmounted(() => {
41
+ unsubscribe?.();
42
+ unsubscribe = null;
43
+ // Fire-and-forget: destroy() returns a Promise, but onUnmounted has no
44
+ // way to keep the component alive until it settles, and callers of
45
+ // useRuntimeHost have no handle to await it either. This matches
46
+ // createReactAdapter's cleanup effect, which discards the same Promise.
47
+ void observable?.destroy();
48
+ observable = null;
49
+ });
50
+ return { target, status };
51
+ };
52
+ return { useRuntimeHost };
53
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=create-vue-adapter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-vue-adapter.test.d.ts","sourceRoot":"","sources":["../src/create-vue-adapter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,81 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, test, vi } from "vitest";
3
+ import * as Vue from "vue";
4
+ import { createApp, h } from "vue";
5
+ const observers = new Set();
6
+ let currentStatus = { type: "idle" };
7
+ const mockNext = vi.fn((path) => {
8
+ currentStatus = { type: "loading", path };
9
+ observers.forEach((observer) => observer(currentStatus));
10
+ currentStatus = { type: "ready", path };
11
+ observers.forEach((observer) => observer(currentStatus));
12
+ });
13
+ const mockDestroy = vi.fn(async () => { });
14
+ const mockCreateRuntimeHostObservable = vi.fn((_options) => ({
15
+ next: mockNext,
16
+ destroy: mockDestroy,
17
+ subscribe: (observer) => {
18
+ observers.add(observer);
19
+ return () => observers.delete(observer);
20
+ },
21
+ getSnapshot: () => currentStatus,
22
+ }));
23
+ vi.mock("@rmc-toolkit/core", () => ({
24
+ createRuntimeHostObservable: (options) => mockCreateRuntimeHostObservable(options),
25
+ }));
26
+ const { createVueAdapter } = await import("./create-vue-adapter.js");
27
+ const manifest = { namespace: "@acme", assetsOrigin: "https://assets.example.com" };
28
+ describe("createVueAdapter", () => {
29
+ test("creates one observable on mount, forwards the initial path to next(), and surfaces status", async () => {
30
+ mockCreateRuntimeHostObservable.mockClear();
31
+ mockNext.mockClear();
32
+ mockDestroy.mockClear();
33
+ currentStatus = { type: "idle" };
34
+ const { useRuntimeHost } = createVueAdapter(Vue);
35
+ const currentPath = Vue.ref("/search");
36
+ const TestComponent = {
37
+ setup() {
38
+ return useRuntimeHost(() => currentPath.value, { manifest });
39
+ },
40
+ render() {
41
+ return h("div", { ref: "target" }, this.status.type);
42
+ },
43
+ };
44
+ const container = document.createElement("div");
45
+ document.body.appendChild(container);
46
+ const app = createApp(TestComponent);
47
+ app.mount(container);
48
+ await Vue.nextTick();
49
+ expect(mockCreateRuntimeHostObservable).toHaveBeenCalledTimes(1);
50
+ expect(mockNext).toHaveBeenCalledWith("/search");
51
+ expect(container.textContent).toBe("ready");
52
+ app.unmount();
53
+ expect(mockDestroy).toHaveBeenCalledTimes(1);
54
+ });
55
+ test("forwards a new path to next() when the reactive path source changes", async () => {
56
+ mockCreateRuntimeHostObservable.mockClear();
57
+ mockNext.mockClear();
58
+ currentStatus = { type: "idle" };
59
+ const { useRuntimeHost } = createVueAdapter(Vue);
60
+ const currentPath = Vue.ref("/search");
61
+ const TestComponent = {
62
+ setup() {
63
+ return useRuntimeHost(() => currentPath.value, { manifest });
64
+ },
65
+ render() {
66
+ return h("div", { ref: "target" });
67
+ },
68
+ };
69
+ const container = document.createElement("div");
70
+ document.body.appendChild(container);
71
+ const app = createApp(TestComponent);
72
+ app.mount(container);
73
+ await Vue.nextTick();
74
+ currentPath.value = "/cart";
75
+ await Vue.nextTick();
76
+ expect(mockCreateRuntimeHostObservable).toHaveBeenCalledTimes(1);
77
+ expect(mockNext).toHaveBeenNthCalledWith(1, "/search");
78
+ expect(mockNext).toHaveBeenNthCalledWith(2, "/cart");
79
+ app.unmount();
80
+ });
81
+ });
@@ -0,0 +1,3 @@
1
+ export { createVueAdapter } from "./create-vue-adapter.js";
2
+ export type { UseRuntimeHostOptions, UseRuntimeHostResult, } from "./create-vue-adapter.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EACV,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createVueAdapter } from "./create-vue-adapter.js";
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@rmc-toolkit/vue",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "peerDependencies": {
20
+ "@rmc-toolkit/core": "0.2.0",
21
+ "vue": ">=3"
22
+ },
23
+ "dependencies": {
24
+ "@rmc-toolkit/core": "0.2.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -b"
28
+ }
29
+ }