@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.
- package/dist/create-vue-adapter.d.ts +11 -0
- package/dist/create-vue-adapter.d.ts.map +1 -0
- package/dist/create-vue-adapter.js +53 -0
- package/dist/create-vue-adapter.test.d.ts +2 -0
- package/dist/create-vue-adapter.test.d.ts.map +1 -0
- package/dist/create-vue-adapter.test.js +81 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +29 -0
|
@@ -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 @@
|
|
|
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
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|