@poveste/plugin-vue 0.18.3 → 0.19.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/dist/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/bundled/client/app/host.js +11 -10
- package/dist/commands/generate-story.client.js +3 -1
- package/dist/index.node.js +27 -0
- package/dist/util/vapor.d.ts +30 -0
- package/dist/util/vapor.js +59 -0
- package/package.json +10 -7
- package/src/client/app/host.ts +9 -0
- package/src/commands/generate-story.client.ts +3 -1
- package/src/index.node.ts +29 -0
- package/src/util/vapor.ts +65 -0
- package/dist/util/list-components.spec.d.ts +0 -1
- package/dist/util/list-components.spec.js +0 -46
- package/src/client/__tests__/codegen-attrs.spec.ts +0 -149
- package/src/client/__tests__/context.spec.ts +0 -50
- package/src/client/app/__tests__/auto-props.spec.ts +0 -49
- package/src/client/app/__tests__/preview-variants.spec.ts +0 -76
- package/src/client/app/__tests__/state-sync.spec.ts +0 -498
- package/src/client/app/__tests__/writable-binding.spec.ts +0 -39
- package/src/client/server/__tests__/nesting-errors.spec.ts +0 -48
- package/src/util/list-components.spec.ts +0 -54
|
@@ -5,9 +5,10 @@ import { RouterLinkStub as r } from "./RouterLinkStub.js";
|
|
|
5
5
|
import { Suspense as i, createApp as a, defineComponent as o, h as s } from "vue";
|
|
6
6
|
import { getSetupHook as c, reportStoryError as l } from "@poveste/shared";
|
|
7
7
|
import * as u from "virtual:$poveste-generated-global-setup";
|
|
8
|
-
import
|
|
8
|
+
import d from "virtual:$poveste-plugin-vue/vapor-interop";
|
|
9
|
+
import * as f from "virtual:$poveste-setup";
|
|
9
10
|
//#region src/client/app/host.ts
|
|
10
|
-
var
|
|
11
|
+
var p = o({
|
|
11
12
|
name: "PreviewHostRoot",
|
|
12
13
|
props: {
|
|
13
14
|
story: {
|
|
@@ -23,14 +24,14 @@ var f = o({
|
|
|
23
24
|
return t(e.renderContext), () => e.story.file ? s(e.story.file.component, { story: e.story }) : null;
|
|
24
25
|
}
|
|
25
26
|
});
|
|
26
|
-
function
|
|
27
|
+
function m(e) {
|
|
27
28
|
let t = null, o = null;
|
|
28
29
|
async function c() {
|
|
29
30
|
let c = [];
|
|
30
31
|
o = document.createElement("div"), e.el.appendChild(o), t = a({
|
|
31
32
|
name: e.name,
|
|
32
33
|
render: () => {
|
|
33
|
-
let t = s(
|
|
34
|
+
let t = s(p, {
|
|
34
35
|
story: e.getStory(),
|
|
35
36
|
renderContext: e.renderContext
|
|
36
37
|
});
|
|
@@ -48,7 +49,7 @@ function p(e) {
|
|
|
48
49
|
storyId: e.getStory()?.id,
|
|
49
50
|
variantId: e.getVariant()?.id
|
|
50
51
|
}), console.error(t);
|
|
51
|
-
}, n(t), t.component("RouterLink", r), await
|
|
52
|
+
}, d && t.use(d), n(t), t.component("RouterLink", r), await h({
|
|
52
53
|
app: t,
|
|
53
54
|
story: e.getStory(),
|
|
54
55
|
variant: e.getVariant() ?? void 0,
|
|
@@ -60,23 +61,23 @@ function p(e) {
|
|
|
60
61
|
function u() {
|
|
61
62
|
t?.unmount(), t = null, o &&= (o.parentNode?.removeChild(o), null);
|
|
62
63
|
}
|
|
63
|
-
function
|
|
64
|
+
function f() {
|
|
64
65
|
t?._instance?.proxy?.$forceUpdate();
|
|
65
66
|
}
|
|
66
67
|
return {
|
|
67
68
|
mount: c,
|
|
68
69
|
unmount: u,
|
|
69
|
-
forceUpdate:
|
|
70
|
+
forceUpdate: f
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
|
-
async function
|
|
73
|
+
async function h(t) {
|
|
73
74
|
let n = c(u, e);
|
|
74
75
|
n && await n(t);
|
|
75
|
-
let r = c(
|
|
76
|
+
let r = c(f, e);
|
|
76
77
|
if (r && await r(t), typeof t.variant?.setupApp == "function") {
|
|
77
78
|
let e = t.variant.setupApp;
|
|
78
79
|
await e(t);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
//#endregion
|
|
82
|
-
export {
|
|
83
|
+
export { m as createPreviewHost };
|
|
@@ -14,7 +14,9 @@ export default {
|
|
|
14
14
|
label: 'File name',
|
|
15
15
|
type: 'text',
|
|
16
16
|
required: true,
|
|
17
|
-
|
|
17
|
+
// A name, not a path: both halves join it to the component's own directory,
|
|
18
|
+
// and a path here was joined twice into one that does not exist (#1176).
|
|
19
|
+
defaultValue: answers => answers['component']?.split('/').pop()?.replace(/\.vue$/, '.story.vue'),
|
|
18
20
|
},
|
|
19
21
|
],
|
|
20
22
|
clientAction: (params) => {
|
package/dist/index.node.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import generateStoryCommand from './commands/generate-story.server.js';
|
|
2
5
|
import { VUE_SETUP_HOOK_NAMES } from './setup-hooks.js';
|
|
3
6
|
import { listComponentFiles } from './util/list-components.js';
|
|
7
|
+
import { VAPOR_INTEROP_ID, vaporInteropModule, withoutVaporPlugin } from './util/vapor.js';
|
|
4
8
|
export function HstVue() {
|
|
5
9
|
return {
|
|
6
10
|
name: '@poveste/plugin-vue',
|
|
@@ -19,6 +23,20 @@ export function HstVue() {
|
|
|
19
23
|
return {
|
|
20
24
|
vite: {
|
|
21
25
|
plugins: [
|
|
26
|
+
withoutVaporPlugin(),
|
|
27
|
+
(() => {
|
|
28
|
+
let root = process.cwd();
|
|
29
|
+
return {
|
|
30
|
+
name: 'poveste-plugin-vue:vapor-interop',
|
|
31
|
+
configResolved(config) {
|
|
32
|
+
root = config.root;
|
|
33
|
+
},
|
|
34
|
+
resolveId: id => id === VAPOR_INTEROP_ID ? `\0${VAPOR_INTEROP_ID}` : undefined,
|
|
35
|
+
load(id) {
|
|
36
|
+
return id === `\0${VAPOR_INTEROP_ID}` ? vaporInteropModule(projectVueVersion(root)) : undefined;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
})(),
|
|
22
40
|
{
|
|
23
41
|
name: 'poveste-plugin-vue',
|
|
24
42
|
enforce: 'post',
|
|
@@ -54,4 +72,13 @@ export function HstVue() {
|
|
|
54
72
|
},
|
|
55
73
|
};
|
|
56
74
|
}
|
|
75
|
+
function projectVueVersion(root) {
|
|
76
|
+
try {
|
|
77
|
+
const manifest = createRequire(join(root, 'package.json')).resolve('vue/package.json');
|
|
78
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).version;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
57
84
|
export * from './helpers.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* An SFC with `vapor` removed from its `<script>` and `<template>` tags, or
|
|
4
|
+
* undefined when it has none. Only the opening tags: the attribute does nothing
|
|
5
|
+
* anywhere else, and a template may well contain the word.
|
|
6
|
+
*/
|
|
7
|
+
export declare function withoutVapor(code: string): string | undefined;
|
|
8
|
+
export declare const VAPOR_INTEROP_ID = "virtual:$poveste-plugin-vue/vapor-interop";
|
|
9
|
+
/**
|
|
10
|
+
* Whether a Vue of this version exports `vaporInteropPlugin`: 3.6 and later,
|
|
11
|
+
* prereleases included. Only the project knows its Vue, so the story app gets
|
|
12
|
+
* the plugin through `VAPOR_INTEROP_ID` rather than importing it: a named import
|
|
13
|
+
* fails to link against 3.5, and reading it off the namespace either warns in
|
|
14
|
+
* every 3.5 build or keeps all of Vue in the bundle (#1112).
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasVaporInterop(version: string | undefined): boolean;
|
|
17
|
+
export declare function vaporInteropModule(vueVersion: string | undefined): string;
|
|
18
|
+
/**
|
|
19
|
+
* Collection imports every story file in Node, where Vue 3.6's entry has no
|
|
20
|
+
* Vapor runtime, so a `<script setup vapor>` component failed with "does not
|
|
21
|
+
* provide an export named 'defineVaporComponent'" and took its whole story out
|
|
22
|
+
* of the book (#1153). Collection only reads metadata and stubs every component
|
|
23
|
+
* but `Story` and `Variant`, so the component is compiled as an ordinary one
|
|
24
|
+
* there. The browser still gets it as Vapor.
|
|
25
|
+
*
|
|
26
|
+
* A story file is compiled without it in the browser too. Under a Vapor parent,
|
|
27
|
+
* `Story` and `Variant` get Vapor blocks for slots rather than vnodes, and the
|
|
28
|
+
* preview never left "Loading..." (#1164). The components it imports stay Vapor.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withoutVaporPlugin(): Plugin;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An SFC with `vapor` removed from its `<script>` and `<template>` tags, or
|
|
3
|
+
* undefined when it has none. Only the opening tags: the attribute does nothing
|
|
4
|
+
* anywhere else, and a template may well contain the word.
|
|
5
|
+
*/
|
|
6
|
+
export function withoutVapor(code) {
|
|
7
|
+
const stripped = code.replace(/<(?:script|template)\b[^>]*>/g, tag => tag.replace(/\svapor(?=[\s/>])/, ''));
|
|
8
|
+
return stripped === code ? undefined : stripped;
|
|
9
|
+
}
|
|
10
|
+
export const VAPOR_INTEROP_ID = 'virtual:$poveste-plugin-vue/vapor-interop';
|
|
11
|
+
/**
|
|
12
|
+
* Whether a Vue of this version exports `vaporInteropPlugin`: 3.6 and later,
|
|
13
|
+
* prereleases included. Only the project knows its Vue, so the story app gets
|
|
14
|
+
* the plugin through `VAPOR_INTEROP_ID` rather than importing it: a named import
|
|
15
|
+
* fails to link against 3.5, and reading it off the namespace either warns in
|
|
16
|
+
* every 3.5 build or keeps all of Vue in the bundle (#1112).
|
|
17
|
+
*/
|
|
18
|
+
export function hasVaporInterop(version) {
|
|
19
|
+
const match = /^(\d+)\.(\d+)\./.exec(version ?? '');
|
|
20
|
+
if (!match) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const [major, minor] = [Number(match[1]), Number(match[2])];
|
|
24
|
+
return major > 3 || (major === 3 && minor >= 6);
|
|
25
|
+
}
|
|
26
|
+
export function vaporInteropModule(vueVersion) {
|
|
27
|
+
return hasVaporInterop(vueVersion)
|
|
28
|
+
? `export { vaporInteropPlugin as default } from 'vue'\n`
|
|
29
|
+
: `export default undefined\n`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Collection imports every story file in Node, where Vue 3.6's entry has no
|
|
33
|
+
* Vapor runtime, so a `<script setup vapor>` component failed with "does not
|
|
34
|
+
* provide an export named 'defineVaporComponent'" and took its whole story out
|
|
35
|
+
* of the book (#1153). Collection only reads metadata and stubs every component
|
|
36
|
+
* but `Story` and `Variant`, so the component is compiled as an ordinary one
|
|
37
|
+
* there. The browser still gets it as Vapor.
|
|
38
|
+
*
|
|
39
|
+
* A story file is compiled without it in the browser too. Under a Vapor parent,
|
|
40
|
+
* `Story` and `Variant` get Vapor blocks for slots rather than vnodes, and the
|
|
41
|
+
* preview never left "Loading..." (#1164). The components it imports stay Vapor.
|
|
42
|
+
*/
|
|
43
|
+
export function withoutVaporPlugin() {
|
|
44
|
+
let isStoryFile = () => false;
|
|
45
|
+
return {
|
|
46
|
+
name: 'poveste-plugin-vue:without-vapor',
|
|
47
|
+
enforce: 'pre',
|
|
48
|
+
configResolved(config) {
|
|
49
|
+
const api = config.plugins.find(plugin => plugin.name === 'poveste-vite-plugin')?.api;
|
|
50
|
+
isStoryFile = api?.isStoryFile ?? isStoryFile;
|
|
51
|
+
},
|
|
52
|
+
transform(code, id) {
|
|
53
|
+
if (!id.endsWith('.vue') || !(this.meta.poveste?.isCollecting || isStoryFile(id))) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return withoutVapor(code);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
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.19.1",
|
|
5
5
|
"description": "Poveste plugin for Vue.js support",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan",
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
"main": "dist/index.js",
|
|
61
61
|
"types": "dist/index.node.d.ts",
|
|
62
62
|
"files": [
|
|
63
|
+
"!src/**/*.spec.ts",
|
|
64
|
+
"!src/**/__tests__",
|
|
63
65
|
"client.d.ts",
|
|
64
66
|
"collect.d.ts",
|
|
65
67
|
"components.d.ts",
|
|
@@ -70,13 +72,13 @@
|
|
|
70
72
|
"node": ">=22.22.2"
|
|
71
73
|
},
|
|
72
74
|
"peerDependencies": {
|
|
73
|
-
"poveste": "^0.
|
|
75
|
+
"poveste": "^0.19.1",
|
|
74
76
|
"vue": "^3.5.43"
|
|
75
77
|
},
|
|
76
78
|
"dependencies": {
|
|
77
|
-
"@poveste/controls": "^0.
|
|
78
|
-
"@poveste/shared": "^0.
|
|
79
|
-
"@poveste/vendors": "^0.
|
|
79
|
+
"@poveste/controls": "^0.19.1",
|
|
80
|
+
"@poveste/shared": "^0.19.1",
|
|
81
|
+
"@poveste/vendors": "^0.19.1",
|
|
80
82
|
"change-case": "^5.4.4",
|
|
81
83
|
"ignore": "^7.0.10",
|
|
82
84
|
"launch-editor": "^2.9.1",
|
|
@@ -86,11 +88,12 @@
|
|
|
86
88
|
"devDependencies": {
|
|
87
89
|
"@types/node": "^26.6.3",
|
|
88
90
|
"concurrently": "^10.0.5",
|
|
89
|
-
"poveste": "0.
|
|
91
|
+
"poveste": "0.19.1",
|
|
90
92
|
"typescript": "6.0.3",
|
|
91
93
|
"vite": "^8.3.1",
|
|
92
94
|
"vitest": "^5.0.2",
|
|
93
|
-
"vue": "^3.5.43"
|
|
95
|
+
"vue": "^3.5.43",
|
|
96
|
+
"vue-vapor-rc": "npm:vue@3.6.0-rc.10"
|
|
94
97
|
},
|
|
95
98
|
"scripts": {
|
|
96
99
|
"build": "rimraf dist && vite build && tsc -d -P tsconfig.build.json && pnpm run build:types",
|
package/src/client/app/host.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { getSetupHook, reportStoryError } from '@poveste/shared'
|
|
|
6
6
|
// @ts-expect-error virtual module id
|
|
7
7
|
import * as generatedSetup from 'virtual:$poveste-generated-global-setup'
|
|
8
8
|
// @ts-expect-error virtual module id
|
|
9
|
+
import vaporInteropPlugin from 'virtual:$poveste-plugin-vue/vapor-interop'
|
|
10
|
+
// @ts-expect-error virtual module id
|
|
9
11
|
import * as setup from 'virtual:$poveste-setup'
|
|
10
12
|
import {
|
|
11
13
|
createApp,
|
|
@@ -99,6 +101,13 @@ export function createPreviewHost(options: PreviewHostOptions) {
|
|
|
99
101
|
console.error(err)
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
// From Vue 3.6, a Vapor component inside a VDOM tree mounts only through this
|
|
105
|
+
// plugin, and without it the preview throws `reading 'mount'` (#1112).
|
|
106
|
+
// Undefined on an older Vue.
|
|
107
|
+
if (vaporInteropPlugin) {
|
|
108
|
+
app.use(vaporInteropPlugin)
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
registerGlobalComponents(app)
|
|
103
112
|
app.component('RouterLink', RouterLinkStub)
|
|
104
113
|
|
|
@@ -16,7 +16,9 @@ export default {
|
|
|
16
16
|
label: 'File name',
|
|
17
17
|
type: 'text',
|
|
18
18
|
required: true,
|
|
19
|
-
|
|
19
|
+
// A name, not a path: both halves join it to the component's own directory,
|
|
20
|
+
// and a path here was joined twice into one that does not exist (#1176).
|
|
21
|
+
defaultValue: answers => answers['component']?.split('/').pop()?.replace(/\.vue$/, '.story.vue'),
|
|
20
22
|
},
|
|
21
23
|
],
|
|
22
24
|
clientAction: (params) => {
|
package/src/index.node.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Plugin } from 'poveste'
|
|
2
2
|
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
3
7
|
import generateStoryCommand from './commands/generate-story.server.js'
|
|
4
8
|
import { VUE_SETUP_HOOK_NAMES } from './setup-hooks.js'
|
|
5
9
|
import { listComponentFiles } from './util/list-components.js'
|
|
10
|
+
import { VAPOR_INTEROP_ID, vaporInteropModule, withoutVaporPlugin } from './util/vapor.js'
|
|
6
11
|
|
|
7
12
|
export function HstVue(): Plugin {
|
|
8
13
|
return {
|
|
@@ -24,6 +29,20 @@ export function HstVue(): Plugin {
|
|
|
24
29
|
return {
|
|
25
30
|
vite: {
|
|
26
31
|
plugins: [
|
|
32
|
+
withoutVaporPlugin(),
|
|
33
|
+
(() => {
|
|
34
|
+
let root = process.cwd()
|
|
35
|
+
return {
|
|
36
|
+
name: 'poveste-plugin-vue:vapor-interop',
|
|
37
|
+
configResolved(config) {
|
|
38
|
+
root = config.root
|
|
39
|
+
},
|
|
40
|
+
resolveId: id => id === VAPOR_INTEROP_ID ? `\0${VAPOR_INTEROP_ID}` : undefined,
|
|
41
|
+
load(id) {
|
|
42
|
+
return id === `\0${VAPOR_INTEROP_ID}` ? vaporInteropModule(projectVueVersion(root)) : undefined
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
})(),
|
|
27
46
|
{
|
|
28
47
|
name: 'poveste-plugin-vue',
|
|
29
48
|
enforce: 'post',
|
|
@@ -63,4 +82,14 @@ export function HstVue(): Plugin {
|
|
|
63
82
|
}
|
|
64
83
|
}
|
|
65
84
|
|
|
85
|
+
function projectVueVersion(root: string): string | undefined {
|
|
86
|
+
try {
|
|
87
|
+
const manifest = createRequire(join(root, 'package.json')).resolve('vue/package.json')
|
|
88
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).version
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
export * from './helpers.js'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An SFC with `vapor` removed from its `<script>` and `<template>` tags, or
|
|
5
|
+
* undefined when it has none. Only the opening tags: the attribute does nothing
|
|
6
|
+
* anywhere else, and a template may well contain the word.
|
|
7
|
+
*/
|
|
8
|
+
export function withoutVapor(code: string): string | undefined {
|
|
9
|
+
const stripped = code.replace(/<(?:script|template)\b[^>]*>/g, tag => tag.replace(/\svapor(?=[\s/>])/, ''))
|
|
10
|
+
return stripped === code ? undefined : stripped
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VAPOR_INTEROP_ID = 'virtual:$poveste-plugin-vue/vapor-interop'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether a Vue of this version exports `vaporInteropPlugin`: 3.6 and later,
|
|
17
|
+
* prereleases included. Only the project knows its Vue, so the story app gets
|
|
18
|
+
* the plugin through `VAPOR_INTEROP_ID` rather than importing it: a named import
|
|
19
|
+
* fails to link against 3.5, and reading it off the namespace either warns in
|
|
20
|
+
* every 3.5 build or keeps all of Vue in the bundle (#1112).
|
|
21
|
+
*/
|
|
22
|
+
export function hasVaporInterop(version: string | undefined): boolean {
|
|
23
|
+
const match = /^(\d+)\.(\d+)\./.exec(version ?? '')
|
|
24
|
+
if (!match) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
const [major, minor] = [Number(match[1]), Number(match[2])]
|
|
28
|
+
return major > 3 || (major === 3 && minor >= 6)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function vaporInteropModule(vueVersion: string | undefined): string {
|
|
32
|
+
return hasVaporInterop(vueVersion)
|
|
33
|
+
? `export { vaporInteropPlugin as default } from 'vue'\n`
|
|
34
|
+
: `export default undefined\n`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Collection imports every story file in Node, where Vue 3.6's entry has no
|
|
39
|
+
* Vapor runtime, so a `<script setup vapor>` component failed with "does not
|
|
40
|
+
* provide an export named 'defineVaporComponent'" and took its whole story out
|
|
41
|
+
* of the book (#1153). Collection only reads metadata and stubs every component
|
|
42
|
+
* but `Story` and `Variant`, so the component is compiled as an ordinary one
|
|
43
|
+
* there. The browser still gets it as Vapor.
|
|
44
|
+
*
|
|
45
|
+
* A story file is compiled without it in the browser too. Under a Vapor parent,
|
|
46
|
+
* `Story` and `Variant` get Vapor blocks for slots rather than vnodes, and the
|
|
47
|
+
* preview never left "Loading..." (#1164). The components it imports stay Vapor.
|
|
48
|
+
*/
|
|
49
|
+
export function withoutVaporPlugin(): Plugin {
|
|
50
|
+
let isStoryFile: (id: string) => boolean = () => false
|
|
51
|
+
return {
|
|
52
|
+
name: 'poveste-plugin-vue:without-vapor',
|
|
53
|
+
enforce: 'pre',
|
|
54
|
+
configResolved(config) {
|
|
55
|
+
const api = config.plugins.find(plugin => plugin.name === 'poveste-vite-plugin')?.api
|
|
56
|
+
isStoryFile = api?.isStoryFile ?? isStoryFile
|
|
57
|
+
},
|
|
58
|
+
transform(code, id) {
|
|
59
|
+
if (!id.endsWith('.vue') || !((this.meta as any).poveste?.isCollecting || isStoryFile(id))) {
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
return withoutVapor(code)
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { listComponentFiles } from './list-components.js';
|
|
6
|
-
const roots = [];
|
|
7
|
-
function tree(files) {
|
|
8
|
-
const root = mkdtempSync(join(tmpdir(), 'list-components-'));
|
|
9
|
-
roots.push(root);
|
|
10
|
-
for (const [path, content] of Object.entries(files)) {
|
|
11
|
-
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
12
|
-
writeFileSync(join(root, path), content);
|
|
13
|
-
}
|
|
14
|
-
return root;
|
|
15
|
-
}
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
for (const root of roots.splice(0)) {
|
|
18
|
-
rmSync(root, { recursive: true, force: true });
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
// Listing honoured .gitignore through globby's `gitignore: true`, and tinyglobby
|
|
22
|
-
// has no such option, so the filtering is ours now (#839).
|
|
23
|
-
describe('listComponentFiles', () => {
|
|
24
|
-
it('leaves out components git ignores, from the root .gitignore and a nested one', async () => {
|
|
25
|
-
const root = tree({
|
|
26
|
-
'.git/HEAD': 'ref: refs/heads/main\n',
|
|
27
|
-
'.gitignore': 'generated/\n',
|
|
28
|
-
'app/.gitignore': 'scratch/*\n!scratch/Kept.vue\n',
|
|
29
|
-
'app/src/Button.vue': '',
|
|
30
|
-
'app/src/generated/Output.vue': '',
|
|
31
|
-
'app/scratch/Draft.vue': '',
|
|
32
|
-
'app/scratch/Kept.vue': '',
|
|
33
|
-
});
|
|
34
|
-
const files = await listComponentFiles('', [], 10, join(root, 'app'));
|
|
35
|
-
expect(files.sort()).toEqual(['scratch/Kept.vue', 'src/Button.vue']);
|
|
36
|
-
});
|
|
37
|
-
it('still skips node_modules and the patterns it is given', async () => {
|
|
38
|
-
const root = tree({
|
|
39
|
-
'.git/HEAD': 'ref: refs/heads/main\n',
|
|
40
|
-
'src/Button.vue': '',
|
|
41
|
-
'src/Button.story.vue': '',
|
|
42
|
-
'node_modules/pkg/Dep.vue': '',
|
|
43
|
-
});
|
|
44
|
-
expect(await listComponentFiles('', ['**/*.story.vue'], 10, root)).toEqual(['src/Button.vue']);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { Variant } from '@poveste/shared'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { h } from 'vue'
|
|
4
|
-
import { delimitAttr, generateSourceCode } from '../codegen.js'
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
* The source pane shows the template that produced the variant, so a handler
|
|
8
|
-
* has to appear as its author wrote it. The pass this replaces escaped `'` for
|
|
9
|
-
* a JavaScript string and rewrote `"` into `'` — neither of which is the
|
|
10
|
-
* attribute the result lands in — and each half reached the reader alone (#602).
|
|
11
|
-
*
|
|
12
|
-
* A case carrying both quote characters is the one that matters: each half
|
|
13
|
-
* fails on its own, so a fixture with only one of them passes a half-fix.
|
|
14
|
-
*/
|
|
15
|
-
describe('delimitAttr', () => {
|
|
16
|
-
it('leaves code with no quotes in it alone, under the usual delimiter', () => {
|
|
17
|
-
expect(delimitAttr(['onClick'])).toEqual({ quote: '"', lines: ['onClick'] })
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
// Was shown as `() => alert('hi')` — the reader's double quotes rewritten.
|
|
21
|
-
it('takes the other delimiter rather than rewriting a double quote', () => {
|
|
22
|
-
expect(delimitAttr(['() => alert("hi")'])).toEqual({
|
|
23
|
-
quote: '\'',
|
|
24
|
-
lines: ['() => alert("hi")'],
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
// Was shown as `() => alert(\'hi\')`, backslashes and all.
|
|
29
|
-
it('leaves a single quote unescaped, since the attribute is not a JS string', () => {
|
|
30
|
-
expect(delimitAttr(['() => alert(\'hi\')'])).toEqual({
|
|
31
|
-
quote: '"',
|
|
32
|
-
lines: ['() => alert(\'hi\')'],
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// Both characters, so whichever delimiter is chosen has to be escaped inside
|
|
37
|
-
// it. Two doubles against one single here, so the single is the cheaper one.
|
|
38
|
-
it('escapes the quote that appears less often when the code carries both', () => {
|
|
39
|
-
expect(delimitAttr(['() => console.log("it\'s here")'])).toEqual({
|
|
40
|
-
quote: '\'',
|
|
41
|
-
lines: ['() => console.log("it's here")'],
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// Four singles against two doubles, so the double is the cheaper one here.
|
|
46
|
-
it('escapes the double quote instead when that is the rarer one', () => {
|
|
47
|
-
expect(delimitAttr(['() => alert(\'a\', \'b\', "c")'])).toEqual({
|
|
48
|
-
quote: '"',
|
|
49
|
-
lines: ['() => alert(\'a\', \'b\', "c")'],
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
// The alert CodeQL raised: the escaping pass never escaped a backslash, so
|
|
54
|
-
// `() => alert('it\'s')` went in carrying one and came out carrying four.
|
|
55
|
-
it('passes a backslash through untouched', () => {
|
|
56
|
-
const written = '() => alert(\'it\\\'s\')'
|
|
57
|
-
|
|
58
|
-
expect(delimitAttr([written]).lines[0]).toBe(written)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('reads the whole value when deciding, not one line of it', () => {
|
|
62
|
-
expect(delimitAttr(['() => {', ' alert("hi")', '}']).quote).toBe('\'')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('escapes on every line, not only the one that decided it', () => {
|
|
66
|
-
expect(delimitAttr(['() => {', ' alert("a")', ' alert(\'b\')', ' alert(\'c\')', '}']).lines).toEqual([
|
|
67
|
-
'() => {',
|
|
68
|
-
' alert("a")',
|
|
69
|
-
' alert(\'b\')',
|
|
70
|
-
' alert(\'c\')',
|
|
71
|
-
'}',
|
|
72
|
-
])
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
/*
|
|
77
|
-
* `delimitAttr` could be right and unused, so these drive the generator itself.
|
|
78
|
-
*
|
|
79
|
-
* The handler states its own source rather than being written as one. A `.vue`
|
|
80
|
-
* ships to the browser untransformed, but this spec does not: esbuild rewrote
|
|
81
|
-
* `alert('it\'s')` in the fixture to `alert("it's")` before `toString()` ever
|
|
82
|
-
* saw it, so a handler written here is not the text the generator receives.
|
|
83
|
-
* Overriding `toString` is what makes the input exact.
|
|
84
|
-
*/
|
|
85
|
-
const WITH_DOUBLE = '() => alert("hi")'
|
|
86
|
-
const WITH_SINGLE = '() => alert(\'hi\')'
|
|
87
|
-
const WITH_BOTH = '() => console.log("it\'s here")'
|
|
88
|
-
const WITH_BACKSLASH = String.raw`() => alert('it\'s')`
|
|
89
|
-
|
|
90
|
-
function handlerWritten(source: string): () => void {
|
|
91
|
-
const handler = (): void => {}
|
|
92
|
-
handler.toString = () => source
|
|
93
|
-
return handler
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function variantWith(source: string): Variant {
|
|
97
|
-
return {
|
|
98
|
-
state: {},
|
|
99
|
-
slots: () => ({ default: () => h('button', { onClick: handlerWritten(source) }, 'go') }),
|
|
100
|
-
} as unknown as Variant
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
describe('generateSourceCode, for an event handler', () => {
|
|
104
|
-
it('shows a double quote the author wrote, under the other delimiter', async () => {
|
|
105
|
-
expect(await generateSourceCode(variantWith(WITH_DOUBLE))).toContain(`@click='${WITH_DOUBLE}'`)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('shows a single quote unescaped, under the usual delimiter', async () => {
|
|
109
|
-
expect(await generateSourceCode(variantWith(WITH_SINGLE))).toContain(`@click="${WITH_SINGLE}"`)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('escapes only the rarer quote when the handler carries both', async () => {
|
|
113
|
-
expect(await generateSourceCode(variantWith(WITH_BOTH)))
|
|
114
|
-
.toContain('@click=\'() => console.log("it's here")\'')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('shows a backslash the author wrote exactly as written', async () => {
|
|
118
|
-
expect(await generateSourceCode(variantWith(WITH_BACKSLASH))).toContain(WITH_BACKSLASH)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
// The alert itself: one backslash in, four out.
|
|
122
|
-
it('does not multiply the backslash it was given', async () => {
|
|
123
|
-
const source = await generateSourceCode(variantWith(WITH_BACKSLASH))
|
|
124
|
-
|
|
125
|
-
expect(source.match(/\\/g)).toHaveLength(1)
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
/*
|
|
130
|
-
* A string prop is the other way a `"` reaches an attribute, and it took the
|
|
131
|
-
* delimiter fix later than the handlers did — the element below rendered its
|
|
132
|
-
* directives correctly and mangled this one.
|
|
133
|
-
*/
|
|
134
|
-
describe('generateSourceCode, for a string prop', () => {
|
|
135
|
-
function variantWithTitle(title: string): Variant {
|
|
136
|
-
return {
|
|
137
|
-
state: {},
|
|
138
|
-
slots: () => ({ default: () => h('div', { title }, 'go') }),
|
|
139
|
-
} as unknown as Variant
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
it('leaves a plain value under the usual delimiter', async () => {
|
|
143
|
-
expect(await generateSourceCode(variantWithTitle('a calm title'))).toContain('title="a calm title"')
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('takes the other delimiter rather than closing the attribute early', async () => {
|
|
147
|
-
expect(await generateSourceCode(variantWithTitle('He said "hi"'))).toContain(`title='He said "hi"'`)
|
|
148
|
-
})
|
|
149
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
})
|