@poveste/plugin-vue 0.15.0 → 0.16.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/README.md +1 -1
- package/components.d.ts +4 -0
- package/dist/bundled/client/app/MountStory.js +5 -0
- package/dist/bundled/client/app/Story.js +58 -57
- package/dist/bundled/client/app/Variant.js +42 -41
- package/dist/bundled/client/app/context.js +5 -0
- package/dist/bundled/client/app/global-components.js +1 -1
- package/dist/bundled/client/app/render-context.js +7 -7
- package/dist/bundled/client/app/util.js +32 -27
- package/dist/bundled/client/context.js +20 -0
- package/dist/bundled/client/server/Story.js +28 -27
- package/dist/bundled/client/server/Variant.js +13 -17
- package/dist/bundled/client/server/context.js +5 -0
- package/dist/bundled/client/server/run.js +27 -27
- package/dist/client/app/MountStory.d.ts +9 -0
- package/dist/client/app/context.d.ts +9 -0
- package/dist/client/app/render-context.d.ts +15 -0
- package/dist/client/app/util.d.ts +32 -0
- package/dist/client/context.d.ts +40 -0
- package/dist/client/server/context.d.ts +7 -0
- package/package.json +13 -13
- package/src/client/__tests__/context.spec.ts +50 -0
- package/src/client/app/MountStory.ts +6 -0
- package/src/client/app/Story.ts +9 -4
- package/src/client/app/Variant.ts +10 -5
- package/src/client/app/__tests__/state-sync.spec.ts +113 -2
- package/src/client/app/__tests__/writable-binding.spec.ts +39 -0
- package/src/client/app/context.ts +17 -0
- package/src/client/app/render-context.ts +25 -5
- package/src/client/app/util.ts +88 -3
- package/src/client/context.ts +66 -0
- package/src/client/server/Story.ts +7 -7
- package/src/client/server/Variant.ts +13 -17
- package/src/client/server/__tests__/nesting-errors.spec.ts +48 -0
- package/src/client/server/context.ts +18 -0
- package/src/client/server/run.ts +7 -4
|
@@ -1,44 +1,44 @@
|
|
|
1
1
|
import { VUE_SETUP_HOOK_NAMES as e } from "../../setup-hooks.js";
|
|
2
|
-
import t from "./
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import * as
|
|
2
|
+
import { addStoryContext as t, storyFileContext as n } from "./context.js";
|
|
3
|
+
import r from "./Story.js";
|
|
4
|
+
import i from "./Variant.js";
|
|
5
|
+
import { createApp as a, h as o } from "vue";
|
|
6
|
+
import { getSetupHook as s } from "@poveste/shared";
|
|
7
|
+
import * as c from "virtual:$poveste-generated-global-setup";
|
|
8
|
+
import * as l from "virtual:$poveste-setup";
|
|
8
9
|
//#region src/client/server/run.ts
|
|
9
|
-
async function
|
|
10
|
-
let { default:
|
|
10
|
+
async function u({ file: u, storyData: d, el: f }) {
|
|
11
|
+
let { default: p } = await import(/* @vite-ignore */
|
|
11
12
|
/* @vite-ignore */
|
|
12
|
-
|
|
13
|
-
),
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
hstStoryFile: c
|
|
13
|
+
u.moduleId
|
|
14
|
+
), m = a({
|
|
15
|
+
setup() {
|
|
16
|
+
t.provide((e) => {
|
|
17
|
+
d.push(e);
|
|
18
|
+
}), n.provide(u);
|
|
19
19
|
},
|
|
20
20
|
render() {
|
|
21
|
-
return
|
|
21
|
+
return o(p);
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
app:
|
|
24
|
+
m.component("Story", r), m.component("Variant", i);
|
|
25
|
+
let h = {
|
|
26
|
+
app: m,
|
|
27
27
|
story: void 0,
|
|
28
28
|
variant: void 0,
|
|
29
29
|
addWrapper: () => {}
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
let
|
|
33
|
-
if (
|
|
30
|
+
}, g = s(c, e);
|
|
31
|
+
g && await g(h);
|
|
32
|
+
let _ = s(l, e);
|
|
33
|
+
if (_ && await _(h), m.mount(f), p.doc) {
|
|
34
34
|
let e = document.createElement("div");
|
|
35
|
-
e.innerHTML =
|
|
35
|
+
e.innerHTML = p.doc;
|
|
36
36
|
let t = e.textContent;
|
|
37
|
-
|
|
37
|
+
d.forEach((e) => {
|
|
38
38
|
e.docsText = t;
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
m.unmount();
|
|
42
42
|
}
|
|
43
43
|
//#endregion
|
|
44
|
-
export {
|
|
44
|
+
export { u as run };
|
|
@@ -9,6 +9,10 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
9
9
|
type: StringConstructor;
|
|
10
10
|
default: null;
|
|
11
11
|
};
|
|
12
|
+
syncState: {
|
|
13
|
+
type: BooleanConstructor;
|
|
14
|
+
default: boolean;
|
|
15
|
+
};
|
|
12
16
|
}>, {
|
|
13
17
|
el: import("vue").Ref<HTMLDivElement | undefined, HTMLDivElement | undefined>;
|
|
14
18
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -20,7 +24,12 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
20
24
|
type: StringConstructor;
|
|
21
25
|
default: null;
|
|
22
26
|
};
|
|
27
|
+
syncState: {
|
|
28
|
+
type: BooleanConstructor;
|
|
29
|
+
default: boolean;
|
|
30
|
+
};
|
|
23
31
|
}>> & Readonly<{}>, {
|
|
32
|
+
syncState: boolean;
|
|
24
33
|
targetVariantId: string;
|
|
25
34
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
26
35
|
export default _default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue';
|
|
2
|
+
/** The story a variant belongs to. Read with `?.` throughout. */
|
|
3
|
+
export declare const storyContext: import("../context.js").Context<ComputedRef<any>>;
|
|
4
|
+
/**
|
|
5
|
+
* The story's implicit state, absent when the story has none. A variant skips
|
|
6
|
+
* its state sync rather than failing, which is behaviour rather than an
|
|
7
|
+
* oversight — this is the type saying so.
|
|
8
|
+
*/
|
|
9
|
+
export declare const implicitStateContext: import("../context.js").Context<() => any>;
|
|
@@ -9,10 +9,25 @@ export interface PreviewRenderContext {
|
|
|
9
9
|
* app realm, where the story view needs all of them mounted.
|
|
10
10
|
*/
|
|
11
11
|
targetVariantId?: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Whether this mount pass owns the live state sync for its variants.
|
|
14
|
+
*
|
|
15
|
+
* False in a sandbox realm, where the render tree beside it already syncs the
|
|
16
|
+
* same `variant.state` against the same story's bindings. Two syncs over one
|
|
17
|
+
* state is two deep watchers walking the whole graph on every edit, and a
|
|
18
|
+
* story holding a large one pays for both: #964 measured the sandbox running
|
|
19
|
+
* three syncs where the app realm runs one.
|
|
20
|
+
*
|
|
21
|
+
* The app realm keeps it. Its mount pass is the only thing that fills
|
|
22
|
+
* `variant.state` there — with no controls slot nothing renders, so nothing
|
|
23
|
+
* else would — and that is what the controls panel builds from.
|
|
24
|
+
*/
|
|
25
|
+
syncState?: boolean;
|
|
12
26
|
externalState: Variant['state'] | null;
|
|
13
27
|
nextVariantIndex: {
|
|
14
28
|
value: number;
|
|
15
29
|
};
|
|
16
30
|
}
|
|
17
31
|
export declare function provideRenderContext(value: PreviewRenderContext): void;
|
|
32
|
+
/** Null outside a preview, which several call sites read with `?.`. */
|
|
18
33
|
export declare function useRenderContext(): PreviewRenderContext | null;
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { ComponentInternalInstance } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* A value Vue has marked raw is carried by reference rather than rebuilt.
|
|
4
|
+
*
|
|
5
|
+
* Both sides of this bridge live in one realm, so the reference is the useful
|
|
6
|
+
* thing to hand over: the baseline then holds the same identity the next pass
|
|
7
|
+
* reads, `isEquivalent` settles it by `Object.is`, and nothing walks whatever
|
|
8
|
+
* is behind it. Rebuilding it was ten of the twenty traversals a keystroke
|
|
9
|
+
* cost (#957).
|
|
10
|
+
*
|
|
11
|
+
* The bridge that leaves this realm cannot do the same — see `toRawDeep` in
|
|
12
|
+
* `poveste-app`, where the mark does not survive the crossing and the key is
|
|
13
|
+
* dropped instead.
|
|
14
|
+
*/
|
|
2
15
|
/**
|
|
3
16
|
* Using external/user Vue
|
|
4
17
|
*/
|
|
@@ -17,4 +30,23 @@ export declare function syncStateBundledAndExternal(bundledState: Record<string,
|
|
|
17
30
|
stop(): void;
|
|
18
31
|
};
|
|
19
32
|
/** The component being set up. Throws outside `setup`, where Vue returns null. */
|
|
33
|
+
/**
|
|
34
|
+
* Whether a `<script setup>` binding can be story state a control drives.
|
|
35
|
+
*
|
|
36
|
+
* A readonly ref cannot: the write-back is refused, so the two sides of the
|
|
37
|
+
* bridge never agree on it and the sync re-walks forever (#959). `useTemplateRef`
|
|
38
|
+
* returns one — in dev builds only, which is why a built book is quiet and the
|
|
39
|
+
* mode story authors work in is not.
|
|
40
|
+
*
|
|
41
|
+
* It has to be asked of the ref rather than of what the ref holds. The guard
|
|
42
|
+
* beside this one tests the value, and a value test cannot classify a template
|
|
43
|
+
* ref at all: at setup time it holds `null`, and what it will hold later is not
|
|
44
|
+
* knowable from here. Readonly-ness is a property of the ref itself and is
|
|
45
|
+
* settled by the time the binding is registered.
|
|
46
|
+
*
|
|
47
|
+
* A `computed` is caught by the same rule and for the same reason — it is a
|
|
48
|
+
* derived value, and a control that cannot write is not a control. Before this
|
|
49
|
+
* it appeared in the panel and warned when written.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isWritableBinding(value: unknown): boolean;
|
|
20
52
|
export declare function useInstance(name: string): ComponentInternalInstance;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A context keyed by a symbol, with its provide and its inject in one place.
|
|
3
|
+
*
|
|
4
|
+
* Six of these used to be string keys carrying `any`, and two of the six were
|
|
5
|
+
* the same string meaning two different things in two realms (#981). A symbol
|
|
6
|
+
* cannot collide and the type travels with it.
|
|
7
|
+
*
|
|
8
|
+
* `inject` and `injectOptional` are separate functions rather than one call with a
|
|
9
|
+
* default, because "nobody provided this" and "this is allowed to be absent"
|
|
10
|
+
* are different questions that used to arrive as the same `undefined` — and the
|
|
11
|
+
* first one is what an author hits when they nest a component wrongly.
|
|
12
|
+
*
|
|
13
|
+
* Reka's `createContext` is this shape and is already a dependency of the
|
|
14
|
+
* controls package. It is not one of this package's, and this is fifteen lines,
|
|
15
|
+
* so it is written here rather than taking a dependency for it.
|
|
16
|
+
*/
|
|
17
|
+
export interface Context<T> {
|
|
18
|
+
provide: (value: T) => void;
|
|
19
|
+
/** Throws, naming the provider, when nothing provided it. */
|
|
20
|
+
inject: (consumer: string) => T;
|
|
21
|
+
/** `null` when nothing provided it, which the caller is expected to handle. */
|
|
22
|
+
injectOptional: () => T | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `description` is what a symbol shows in a stack; `provider` is the component
|
|
26
|
+
* a reader has to go and add, and is the only part of the error they can act on.
|
|
27
|
+
*
|
|
28
|
+
* The key comes from the global registry rather than being minted here, because
|
|
29
|
+
* this module is evaluated more than once. A collection worker's module graph is
|
|
30
|
+
* invalidated while a story is mid-run, so `provide` and `inject` can each hold
|
|
31
|
+
* their own instance of this file — a plain `Symbol()` then differs between them
|
|
32
|
+
* and the injection misses, which reads as a `<Variant>` outside its `<Story>`.
|
|
33
|
+
* Every description here is already unique, so the registry cannot re-collide
|
|
34
|
+
* the way the string keys in #981 did.
|
|
35
|
+
*
|
|
36
|
+
* Absence is read as `null` rather than `undefined` so that a provider passing
|
|
37
|
+
* `undefined` on purpose is still a provider. None does today, and this is the
|
|
38
|
+
* cheaper of the two mistakes to have made.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createContext<T>(description: string, provider: string): Context<T>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ServerStory, ServerStoryFile, ServerVariant } from '@poveste/shared';
|
|
2
|
+
/** The file being collected, provided by `run.ts` rather than by a component. */
|
|
3
|
+
export declare const storyFileContext: import("../context.js").Context<ServerStoryFile>;
|
|
4
|
+
/** Optional: a `<Story>` collected outside a run has nowhere to report to. */
|
|
5
|
+
export declare const addStoryContext: import("../context.js").Context<(story: ServerStory) => void>;
|
|
6
|
+
export declare const serverStoryContext: import("../context.js").Context<ServerStory>;
|
|
7
|
+
export declare const addVariantContext: import("../context.js").Context<(variant: ServerVariant) => void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poveste/plugin-vue",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"description": "Poveste plugin for Vue.js support",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan",
|
|
@@ -67,30 +67,30 @@
|
|
|
67
67
|
"src"
|
|
68
68
|
],
|
|
69
69
|
"engines": {
|
|
70
|
-
"node": "
|
|
70
|
+
"node": ">=22.22.2"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"poveste": "^0.
|
|
74
|
-
"vue": "^3.5.
|
|
73
|
+
"poveste": "^0.16.0",
|
|
74
|
+
"vue": "^3.5.43"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@poveste/controls": "^0.
|
|
78
|
-
"@poveste/shared": "^0.
|
|
79
|
-
"@poveste/vendors": "^0.
|
|
77
|
+
"@poveste/controls": "^0.16.0",
|
|
78
|
+
"@poveste/shared": "^0.16.0",
|
|
79
|
+
"@poveste/vendors": "^0.16.0",
|
|
80
80
|
"change-case": "^5.4.4",
|
|
81
|
-
"ignore": "^7.0.
|
|
81
|
+
"ignore": "^7.0.10",
|
|
82
82
|
"launch-editor": "^2.9.1",
|
|
83
83
|
"pathe": "^2.0.3",
|
|
84
84
|
"tinyglobby": "^0.2.17"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@types/node": "^
|
|
87
|
+
"@types/node": "^26.6.3",
|
|
88
88
|
"concurrently": "^10.0.5",
|
|
89
|
-
"poveste": "0.
|
|
89
|
+
"poveste": "0.16.0",
|
|
90
90
|
"typescript": "6.0.3",
|
|
91
|
-
"vite": "^8.
|
|
92
|
-
"vitest": "^5.0.
|
|
93
|
-
"vue": "^3.5.
|
|
91
|
+
"vite": "^8.3.1",
|
|
92
|
+
"vitest": "^5.0.2",
|
|
93
|
+
"vue": "^3.5.43"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"build": "rimraf dist && vite build && tsc -d -P tsconfig.build.json && pnpm run build:types",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A collection worker evaluates the context modules more than once — its graph is
|
|
5
|
+
* invalidated while a story is mid-run — so the provider and the consumer can each
|
|
6
|
+
* hold their own instance of one file. The key is what has to survive that: minted
|
|
7
|
+
* per instance it differs between them, the injection misses, and the reader is
|
|
8
|
+
* told their `<Variant>` is outside a `<Story>`.
|
|
9
|
+
*
|
|
10
|
+
* Vue's own `provide`/`inject` are stubbed because they need a mounted component
|
|
11
|
+
* and this package's tests have no DOM; the key they are handed is the subject.
|
|
12
|
+
*/
|
|
13
|
+
const provided: unknown[] = []
|
|
14
|
+
const injected: unknown[] = []
|
|
15
|
+
|
|
16
|
+
vi.mock('vue', () => ({
|
|
17
|
+
provide: (key: unknown) => {
|
|
18
|
+
provided.push(key)
|
|
19
|
+
},
|
|
20
|
+
inject: (key: unknown) => {
|
|
21
|
+
injected.push(key)
|
|
22
|
+
return null
|
|
23
|
+
},
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
const { createContext } = await import('../context.js')
|
|
27
|
+
|
|
28
|
+
function keyOf(description: string) {
|
|
29
|
+
createContext(description, '<Story>').provide('anything')
|
|
30
|
+
return provided.at(-1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('createContext', () => {
|
|
34
|
+
it('gives two instances of the same context one key', () => {
|
|
35
|
+
expect(keyOf('poveste-test-context')).toBe(keyOf('poveste-test-context'))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('keeps two contexts apart when their descriptions differ', () => {
|
|
39
|
+
// What #981 was: one key meaning two things.
|
|
40
|
+
expect(keyOf('poveste-test-one')).not.toBe(keyOf('poveste-test-two'))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('injects under the key it provides under', () => {
|
|
44
|
+
const context = createContext<string>('poveste-test-round-trip', '<Story>')
|
|
45
|
+
context.provide('the story file')
|
|
46
|
+
context.injectOptional()
|
|
47
|
+
|
|
48
|
+
expect(injected.at(-1)).toBe(provided.at(-1))
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -26,6 +26,11 @@ export default _defineComponent({
|
|
|
26
26
|
type: String,
|
|
27
27
|
default: null,
|
|
28
28
|
},
|
|
29
|
+
|
|
30
|
+
syncState: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
29
34
|
},
|
|
30
35
|
|
|
31
36
|
setup(props) {
|
|
@@ -35,6 +40,7 @@ export default _defineComponent({
|
|
|
35
40
|
slotName: 'default',
|
|
36
41
|
currentVariant: null,
|
|
37
42
|
targetVariantId: props.targetVariantId,
|
|
43
|
+
syncState: props.syncState,
|
|
38
44
|
externalState: null,
|
|
39
45
|
nextVariantIndex: {
|
|
40
46
|
value: 0,
|
package/src/client/app/Story.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { Story } from '@poveste/shared'
|
|
|
2
2
|
import type { PropType, VNode } from 'vue'
|
|
3
3
|
import type { PreviewRenderContext } from './render-context.js'
|
|
4
4
|
import { omitInheritStoryProps } from '@poveste/shared'
|
|
5
|
-
import { cloneVNode, computed, defineComponent, h, isRef,
|
|
5
|
+
import { cloneVNode, computed, defineComponent, h, isRef, reactive, useAttrs } from 'vue'
|
|
6
|
+
import { implicitStateContext, storyContext } from './context.js'
|
|
6
7
|
import { useRenderContext } from './render-context.js'
|
|
7
|
-
import { useInstance } from './util.js'
|
|
8
|
+
import { isWritableBinding, useInstance } from './util.js'
|
|
8
9
|
import Variant from './Variant.js'
|
|
9
10
|
|
|
10
11
|
export default defineComponent({
|
|
@@ -35,7 +36,7 @@ export default defineComponent({
|
|
|
35
36
|
|
|
36
37
|
const renderContext = useRenderContext()
|
|
37
38
|
const story = computed(() => attrs.story)
|
|
38
|
-
provide(
|
|
39
|
+
storyContext.provide(story)
|
|
39
40
|
|
|
40
41
|
const storyComponent: any = vm.parent
|
|
41
42
|
const implicitState = reactive<Record<string, any>>({})
|
|
@@ -56,6 +57,10 @@ export default defineComponent({
|
|
|
56
57
|
return
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
if (!isWritableBinding(value)) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
Object.defineProperty(implicitState, key, {
|
|
60
65
|
enumerable: true,
|
|
61
66
|
configurable: true,
|
|
@@ -90,7 +95,7 @@ export default defineComponent({
|
|
|
90
95
|
for (const key in storyComponent.data) {
|
|
91
96
|
addImplicitState(storyComponent.data, key)
|
|
92
97
|
}
|
|
93
|
-
provide(
|
|
98
|
+
implicitStateContext.provide(() => implicitState)
|
|
94
99
|
|
|
95
100
|
function updateStory() {
|
|
96
101
|
if (props.meta) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Variant } from '@poveste/shared'
|
|
2
|
-
import type {
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
3
|
import type { PreviewRenderContext } from './render-context.js'
|
|
4
4
|
import { applyState, autoPropsStateKeys } from '@poveste/shared'
|
|
5
|
-
import { computed, defineComponent,
|
|
5
|
+
import { computed, defineComponent, onBeforeUnmount, useAttrs } from 'vue'
|
|
6
6
|
import { syncVariantAutoProps } from './auto-props.js'
|
|
7
|
+
import { implicitStateContext, storyContext } from './context.js'
|
|
7
8
|
import { useRenderContext } from './render-context.js'
|
|
8
9
|
import { syncStateBundledAndExternal, toRawDeep, useInstance } from './util.js'
|
|
9
10
|
|
|
@@ -56,8 +57,8 @@ export default defineComponent({
|
|
|
56
57
|
variant?: Variant
|
|
57
58
|
}
|
|
58
59
|
const vm = useInstance('Variant')
|
|
59
|
-
const story =
|
|
60
|
-
const implicitState =
|
|
60
|
+
const story = storyContext.injectOptional()
|
|
61
|
+
const implicitState = implicitStateContext.injectOptional()
|
|
61
62
|
const renderContext = useRenderContext()
|
|
62
63
|
let lastPropsTypesSnapshot: string
|
|
63
64
|
let renderVariant: Variant | undefined
|
|
@@ -84,7 +85,11 @@ export default defineComponent({
|
|
|
84
85
|
applyState(mountVariant.value.state, toRawDeep(state))
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
// `syncState` is false in a sandbox realm, where the render tree beside this
|
|
89
|
+
// one already syncs the same `variant.state` against the same story's
|
|
90
|
+
// bindings. Both were live, so an edit walked the graph twice over for one
|
|
91
|
+
// copy of it (#964).
|
|
92
|
+
if (renderContext?.mode !== 'render' && renderContext?.syncState !== false && mountVariant.value && implicitState) {
|
|
88
93
|
mountStateSync = syncStateBundledAndExternal(mountVariant.value.state, implicitState(), autoPropsStateKeys)
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { autoPropsStateKeys } from '@poveste/shared'
|
|
2
|
-
import { nextTick as bundledNextTick, reactive as bundledReactive } from '@poveste/vendors/vue'
|
|
2
|
+
import { nextTick as bundledNextTick, reactive as bundledReactive, ref as bundledRef } from '@poveste/vendors/vue'
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
|
-
import { nextTick, reactive, watch } from 'vue'
|
|
4
|
+
import { markRaw, nextTick, reactive, ref, watch } from 'vue'
|
|
5
5
|
import { syncStateBundledAndExternal } from '../util.js'
|
|
6
6
|
|
|
7
7
|
// These run against both Vue copies for real: `@poveste/vendors/vue` is the one
|
|
@@ -41,6 +41,82 @@ describe('syncStateBundledAndExternal', () => {
|
|
|
41
41
|
sync.stop()
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
// Each side's walker recurses with its own copy's `isRef`, and the bundled
|
|
45
|
+
// one used to hand its whole subtree to the external walker — so below the
|
|
46
|
+
// first level, bundled state was walked by the wrong copy of Vue. Nothing
|
|
47
|
+
// observable came of it, because `isRef` reads `__v_isRef` rather than an
|
|
48
|
+
// identity and either copy answers for the other's ref; this pins the
|
|
49
|
+
// recursion either way, which is coverage the bridge did not have.
|
|
50
|
+
it('unwraps a ref nested below the first level, on both sides', async () => {
|
|
51
|
+
const { bundled, external, sync } = setup({ count: 0 })
|
|
52
|
+
await settle()
|
|
53
|
+
|
|
54
|
+
bundled.deep = { inner: { value: bundledRef('from bundled') } }
|
|
55
|
+
await settle()
|
|
56
|
+
expect(external.deep.inner.value).toBe('from bundled')
|
|
57
|
+
|
|
58
|
+
external.other = { inner: { value: ref('from external') } }
|
|
59
|
+
await settle()
|
|
60
|
+
expect(bundled.other.inner.value).toBe('from external')
|
|
61
|
+
|
|
62
|
+
sync.stop()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Both walkers rebuilt every object from its own enumerable keys, so a `Date`
|
|
66
|
+
// crossed as `{}` — and the baseline's `isEquivalent`, which refuses to
|
|
67
|
+
// compare anything that is not plain precisely so two distinct `Date`s are
|
|
68
|
+
// never called equal, was handed two empty plain objects instead (#977).
|
|
69
|
+
it('carries a Date across, rather than emptying it', async () => {
|
|
70
|
+
const { bundled, external, sync } = setup({ count: 0 })
|
|
71
|
+
await settle()
|
|
72
|
+
|
|
73
|
+
external.at = new Date(1790071200000)
|
|
74
|
+
await settle()
|
|
75
|
+
|
|
76
|
+
expect(bundled.at).toBeInstanceOf(Date)
|
|
77
|
+
expect(bundled.at.getTime()).toBe(1790071200000)
|
|
78
|
+
|
|
79
|
+
sync.stop()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('carries a Map and a class instance the same way', async () => {
|
|
83
|
+
class Point {
|
|
84
|
+
constructor(public x: number, public y: number) {}
|
|
85
|
+
get len() { return Math.hypot(this.x, this.y) }
|
|
86
|
+
}
|
|
87
|
+
const { bundled, external, sync } = setup({ count: 0 })
|
|
88
|
+
await settle()
|
|
89
|
+
|
|
90
|
+
external.m = new Map([['a', 1]])
|
|
91
|
+
external.p = new Point(3, 4)
|
|
92
|
+
await settle()
|
|
93
|
+
|
|
94
|
+
expect(bundled.m).toBeInstanceOf(Map)
|
|
95
|
+
expect(bundled.m.get('a')).toBe(1)
|
|
96
|
+
// In one realm the reference crosses, so the prototype survives too — which
|
|
97
|
+
// the sandbox bridge cannot promise and does not (#977).
|
|
98
|
+
expect(bundled.p.len).toBe(5)
|
|
99
|
+
|
|
100
|
+
sync.stop()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// The half the walker was destroying upstream of: two different dates must
|
|
104
|
+
// not settle as equivalent, or an edit is dropped rather than mirrored.
|
|
105
|
+
it('mirrors a change from one Date to another', async () => {
|
|
106
|
+
const { bundled, external, sync } = setup({ count: 0 })
|
|
107
|
+
await settle()
|
|
108
|
+
|
|
109
|
+
external.at = new Date(1790071200000)
|
|
110
|
+
await settle()
|
|
111
|
+
|
|
112
|
+
external.at = new Date(0)
|
|
113
|
+
await settle()
|
|
114
|
+
|
|
115
|
+
expect(bundled.at.getTime()).toBe(0)
|
|
116
|
+
|
|
117
|
+
sync.stop()
|
|
118
|
+
})
|
|
119
|
+
|
|
44
120
|
it('stops mirroring once stopped', async () => {
|
|
45
121
|
const { bundled, external, sync } = setup({ count: 0 })
|
|
46
122
|
await settle()
|
|
@@ -385,3 +461,38 @@ describe('keys the sync is told to omit', () => {
|
|
|
385
461
|
sync.stop()
|
|
386
462
|
})
|
|
387
463
|
})
|
|
464
|
+
|
|
465
|
+
describe('a binding Vue has marked raw', () => {
|
|
466
|
+
// Not `setup()` above: that clones its argument, and `structuredClone` is
|
|
467
|
+
// exactly what a mark does not survive.
|
|
468
|
+
function setupWithGraph() {
|
|
469
|
+
const graph = markRaw({ nodes: { deep: 1 } })
|
|
470
|
+
const bundled = bundledReactive<Record<string, any>>({ text: '', graph })
|
|
471
|
+
const external = reactive<Record<string, any>>({ text: '' })
|
|
472
|
+
const sync = syncStateBundledAndExternal(bundled, external)
|
|
473
|
+
return { bundled, external, sync, graph }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
it('crosses between the two Vues as itself, not as a rebuilt copy', async () => {
|
|
477
|
+
const { external, sync, graph } = setupWithGraph()
|
|
478
|
+
await settle()
|
|
479
|
+
|
|
480
|
+
// Both sides live in one realm, so the reference is the useful thing to
|
|
481
|
+
// hand over — and handing it over is what lets the compare settle on
|
|
482
|
+
// identity instead of walking whatever is behind it (#957).
|
|
483
|
+
expect(external.graph).toBe(graph)
|
|
484
|
+
sync.stop()
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('stays that object over later passes, and does not hold up the keys beside it', async () => {
|
|
488
|
+
const { bundled, external, sync, graph } = setupWithGraph()
|
|
489
|
+
await settle()
|
|
490
|
+
|
|
491
|
+
bundled.text = 'a'
|
|
492
|
+
await settle()
|
|
493
|
+
|
|
494
|
+
expect(external.graph).toBe(graph)
|
|
495
|
+
expect(external.text).toBe('a')
|
|
496
|
+
sync.stop()
|
|
497
|
+
})
|
|
498
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { computed, readonly, ref, shallowRef, useTemplateRef } from 'vue'
|
|
3
|
+
import { isWritableBinding } from '../util.js'
|
|
4
|
+
|
|
5
|
+
// `useTemplateRef` returns a readonly ref in a dev build and a writable one in a
|
|
6
|
+
// production build, so this file is the only place the defect in #959 can be
|
|
7
|
+
// caught: vitest runs the dev build, and the e2e suites serve built books where
|
|
8
|
+
// the bug does not exist.
|
|
9
|
+
|
|
10
|
+
describe('isWritableBinding', () => {
|
|
11
|
+
it('rejects the ref useTemplateRef returns, which is what loops the sync', () => {
|
|
12
|
+
expect(isWritableBinding(useTemplateRef('el'))).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('rejects a readonly ref however it was made', () => {
|
|
16
|
+
expect(isWritableBinding(readonly(ref(0)))).toBe(false)
|
|
17
|
+
expect(isWritableBinding(readonly(shallowRef(null)))).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Same rule, and the reason is the same: a control that cannot write is not a
|
|
21
|
+
// control. Called out because it is a behaviour change beyond the defect.
|
|
22
|
+
it('rejects a computed, which could never take the write either', () => {
|
|
23
|
+
expect(isWritableBinding(computed(() => 1))).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// The shape every existing story uses for a template ref, which has to keep
|
|
27
|
+
// working — a story that writes its own state through one still syncs.
|
|
28
|
+
it('accepts the pre-3.5 ref(null) form', () => {
|
|
29
|
+
expect(isWritableBinding(ref(null))).toBe(true)
|
|
30
|
+
expect(isWritableBinding(shallowRef(null))).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('accepts ordinary bindings, ref and plain alike', () => {
|
|
34
|
+
expect(isWritableBinding(ref(0))).toBe(true)
|
|
35
|
+
expect(isWritableBinding({ a: 1 })).toBe(true)
|
|
36
|
+
expect(isWritableBinding('text')).toBe(true)
|
|
37
|
+
expect(isWritableBinding(undefined)).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue'
|
|
2
|
+
import { createContext } from '../context.js'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* App mode's contexts. Both are optional, and both were guarded by hand before
|
|
6
|
+
* being able to say so (#981).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The story a variant belongs to. Read with `?.` throughout. */
|
|
10
|
+
export const storyContext = createContext<ComputedRef<any>>('poveste-app-story', '<Story>')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The story's implicit state, absent when the story has none. A variant skips
|
|
14
|
+
* its state sync rather than failing, which is behaviour rather than an
|
|
15
|
+
* oversight — this is the type saying so.
|
|
16
|
+
*/
|
|
17
|
+
export const implicitStateContext = createContext<() => any>('poveste-app-implicit-state', '<Story>')
|