@poveste/plugin-vue 0.19.1 → 0.21.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.
Files changed (62) hide show
  1. package/README.md +7 -0
  2. package/dist/bundled/client/app/MountStory.js +32 -30
  3. package/dist/bundled/client/app/RenderStory.js +39 -44
  4. package/dist/bundled/client/app/Story.js +62 -60
  5. package/dist/bundled/client/app/global-components.js +5 -3
  6. package/dist/bundled/client/app/host.js +9 -5
  7. package/dist/bundled/client/app/util.js +46 -33
  8. package/dist/bundled/client/server/Story.js +13 -0
  9. package/dist/bundled/client/server/collect.js +41 -0
  10. package/dist/bundled/client/server/run.js +10 -39
  11. package/dist/bundled/client/server.js +1 -2
  12. package/dist/bundled/testing/index.js +168 -0
  13. package/dist/client/app/Story.d.ts +19 -1
  14. package/dist/client/app/Variant.d.ts +2 -2
  15. package/dist/client/app/global-components.d.ts +1 -0
  16. package/dist/client/app/host.d.ts +8 -0
  17. package/dist/client/app/util.d.ts +28 -0
  18. package/dist/client/server/Story.d.ts +21 -3
  19. package/dist/client/server/collect.d.ts +14 -0
  20. package/dist/client-dependencies.d.ts +2 -0
  21. package/dist/client-dependencies.js +2 -0
  22. package/dist/docgen/events.d.ts +9 -0
  23. package/dist/docgen/events.js +45 -0
  24. package/dist/docgen/extractor.d.ts +7 -0
  25. package/dist/docgen/extractor.js +60 -0
  26. package/dist/docgen/generic.d.ts +5 -0
  27. package/dist/docgen/generic.js +19 -0
  28. package/dist/docgen/meta.d.ts +12 -0
  29. package/dist/docgen/meta.js +76 -0
  30. package/dist/docgen/slots.d.ts +6 -0
  31. package/dist/docgen/slots.js +21 -0
  32. package/dist/index.node.d.ts +2 -0
  33. package/dist/index.node.js +24 -11
  34. package/dist/testing/index.d.ts +28 -0
  35. package/dist/testing/vite.d.ts +18 -0
  36. package/dist/testing/vite.js +171 -0
  37. package/dist/util/project-vue.d.ts +1 -0
  38. package/dist/util/project-vue.js +12 -0
  39. package/dist/util/vue-plugin.d.ts +10 -0
  40. package/dist/util/vue-plugin.js +28 -0
  41. package/package.json +33 -15
  42. package/src/client/app/MountStory.ts +2 -0
  43. package/src/client/app/RenderStory.ts +2 -1
  44. package/src/client/app/Story.ts +15 -3
  45. package/src/client/app/global-components.ts +4 -0
  46. package/src/client/app/host.ts +21 -2
  47. package/src/client/app/util.ts +65 -0
  48. package/src/client/server/Story.ts +15 -2
  49. package/src/client/server/collect.ts +83 -0
  50. package/src/client/server/run.ts +3 -63
  51. package/src/client-dependencies.ts +2 -0
  52. package/src/docgen/events.ts +55 -0
  53. package/src/docgen/extractor.ts +67 -0
  54. package/src/docgen/generic.ts +20 -0
  55. package/src/docgen/meta.ts +93 -0
  56. package/src/docgen/slots.ts +33 -0
  57. package/src/index.node.ts +27 -12
  58. package/src/testing/index.ts +286 -0
  59. package/src/testing/vite.ts +194 -0
  60. package/src/util/project-vue.ts +13 -0
  61. package/src/util/vue-plugin.ts +32 -0
  62. package/testing.d.ts +2 -0
@@ -0,0 +1,286 @@
1
+ import type { ServerStory, ServerStoryFile, Story, StoryFile, Variant } from '@poveste/shared'
2
+ import type { App, Component } from 'vue'
3
+ import type { PreviewRenderContext } from '../client/app/render-context.js'
4
+ // @ts-expect-error virtual module id
5
+ import { environmentApp, environmentReady } from 'virtual:$poveste-testing-environment'
6
+ import { describe, it } from 'vitest'
7
+ import { markRaw, nextTick, reactive } from 'vue'
8
+ import { registerControlComponents } from '../client/app/global-components.js'
9
+ import { createPreviewHost } from '../client/app/host.js'
10
+ import { syncStateBundledAndExternal } from '../client/app/util.js'
11
+ import { collectComponent } from '../client/server/collect.js'
12
+
13
+ export interface TestStoriesOptions {
14
+ /** The outer `describe`'s name. The story file's name by default. */
15
+ name?: string
16
+ /**
17
+ * How long a variant's setup may take to settle, in milliseconds: an
18
+ * `initState` or an async `setup()` still pending then fails the test.
19
+ */
20
+ timeout?: number
21
+ }
22
+
23
+ type StoryComponent = Component & { __file?: string, doc?: string }
24
+
25
+ /**
26
+ * Turns a story file into one test per variant. Each test renders the variant
27
+ * the way the preview does, and fails on what the preview would only log: an
28
+ * error from setup or render, a rejected `initState`, or a `[Vue warn]`.
29
+ *
30
+ * ```ts
31
+ * import { testStories } from '@poveste/plugin-vue/testing'
32
+ * import BaseButton from './BaseButton.story.vue'
33
+ *
34
+ * testStories(BaseButton)
35
+ * ```
36
+ */
37
+ export function testStories(component: StoryComponent, options: TestStoriesOptions = {}): void {
38
+ const name = options.name ?? fileName(component.__file) ?? 'stories'
39
+ const timeout = options.timeout ?? 5000
40
+
41
+ describe(name, async () => {
42
+ await environmentReady()
43
+ const { stories, problems } = await collect(component, name)
44
+
45
+ if (problems.length || !stories.length) {
46
+ it('collects', () => {
47
+ throw failure(`collecting ${name}`, problems.length ? problems : ['it has no <Story>'])
48
+ })
49
+ return
50
+ }
51
+
52
+ for (const story of stories) {
53
+ describe(story.title, () => {
54
+ for (const variant of story.variants) {
55
+ it(variant.title, async () => {
56
+ const found = await renderVariant(story, variant, timeout)
57
+ if (found.length) {
58
+ throw failure(`${story.title} › ${variant.title}`, found)
59
+ }
60
+ })
61
+ }
62
+ })
63
+ }
64
+ })
65
+ }
66
+
67
+ async function collect(component: StoryComponent, name: string) {
68
+ const path = component.__file ?? name
69
+ const base = fileName(path) ?? name
70
+ const file = {
71
+ id: name.replace(/\W+/g, '-').toLowerCase(),
72
+ path,
73
+ moduleId: path,
74
+ // As poveste names it: what a `<Story>` with no `title` is called.
75
+ fileName: base.includes('.') ? base.slice(0, base.indexOf('.')) : base,
76
+ } as ServerStoryFile
77
+ const storyData: ServerStory[] = []
78
+ const problems: unknown[] = []
79
+ const el = document.createElement('div')
80
+
81
+ try {
82
+ await collectComponent(component, { file, storyData, el }, {
83
+ beforeSetup(app) {
84
+ app.config.errorHandler = (err) => {
85
+ problems.push(err)
86
+ }
87
+ app.config.warnHandler = (message, _instance, trace) => {
88
+ problems.push(vueWarning(message, trace))
89
+ }
90
+ },
91
+ afterSetup(app) {
92
+ copyMissing(app._context.components, controlComponents())
93
+ inheritEnvironmentApp(app)
94
+ },
95
+ })
96
+ }
97
+ catch (err) {
98
+ // A setup file that throws, reported as the file's one failing test.
99
+ problems.push(err)
100
+ }
101
+
102
+ const storyFile = {
103
+ id: file.id,
104
+ supportPluginId: 'vue3',
105
+ component: markRaw(component),
106
+ path: [],
107
+ filePath: file.path,
108
+ source: async () => ({ default: '' }),
109
+ } as unknown as StoryFile
110
+
111
+ const stories = storyData.map((data) => {
112
+ const story: Story = {
113
+ ...data,
114
+ file: markRaw(storyFile),
115
+ variants: data.variants.map(variant => toClientVariant(variant as unknown as Variant)),
116
+ slots: () => ({}),
117
+ } as Story
118
+ storyFile.story = story
119
+ return story
120
+ })
121
+ return { stories, problems }
122
+ }
123
+
124
+ function toClientVariant(variant: Variant): Variant {
125
+ return {
126
+ ...variant,
127
+ state: reactive({
128
+ _hPropState: {},
129
+ _hPropDefs: {},
130
+ }),
131
+ setupApp: undefined,
132
+ slots: () => ({}),
133
+ previewReady: false,
134
+ }
135
+ }
136
+
137
+ /**
138
+ * The mount pass a sandbox runs, for the variant's `initState`, then the render
139
+ * pass for its default slot: what the preview shows.
140
+ */
141
+ async function renderVariant(story: Story, variant: Variant, timeout: number): Promise<unknown[]> {
142
+ const found: unknown[] = []
143
+ const onError = (err: unknown) => {
144
+ found.push(err)
145
+ }
146
+ const onWarn = (message: string, trace: string) => {
147
+ found.push(vueWarning(message, trace))
148
+ }
149
+ let resolved = false
150
+ const el = document.createElement('div')
151
+ document.body.appendChild(el)
152
+
153
+ const mountHost = createPreviewHost({
154
+ name: 'StoryTestMount',
155
+ el,
156
+ getStory: () => story,
157
+ getVariant: () => null,
158
+ renderContext: reactive({
159
+ mode: 'mount',
160
+ slotName: 'default',
161
+ currentVariant: null,
162
+ targetVariantId: variant.id,
163
+ externalState: null,
164
+ nextVariantIndex: { value: 0 },
165
+ }) as PreviewRenderContext,
166
+ onError,
167
+ onWarn,
168
+ configureApp: inheritEnvironmentApp,
169
+ })
170
+
171
+ const externalState = reactive<Variant['state']>({})
172
+ const stateSync = syncStateBundledAndExternal(variant.state, externalState)
173
+ const renderHost = createPreviewHost({
174
+ name: 'StoryTestRender',
175
+ el,
176
+ getStory: () => story,
177
+ getVariant: () => variant,
178
+ renderContext: reactive({
179
+ mode: 'render',
180
+ slotName: 'default',
181
+ currentVariant: variant,
182
+ externalState,
183
+ nextVariantIndex: { value: 0 },
184
+ }) as PreviewRenderContext,
185
+ wrapInDiv: true,
186
+ onError,
187
+ onWarn,
188
+ onResolve: () => {
189
+ resolved = true
190
+ },
191
+ configureApp: inheritEnvironmentApp,
192
+ })
193
+
194
+ try {
195
+ await mountHost.mount()
196
+ if (!await settled(() => !!variant.configReady || found.length > 0, timeout)) {
197
+ found.push(new Error(`its setup did not settle within ${timeout} ms`))
198
+ }
199
+ if (!found.length) {
200
+ await renderHost.mount()
201
+ // Until its async setups settle: one that throws after an `await` is caught
202
+ // only once it does.
203
+ if (!await settled(() => resolved || found.length > 0, timeout)) {
204
+ found.push(new Error(`its render did not settle within ${timeout} ms`))
205
+ }
206
+ }
207
+ }
208
+ catch (err) {
209
+ found.push(err)
210
+ }
211
+ finally {
212
+ renderHost.unmount()
213
+ mountHost.unmount()
214
+ stateSync.stop()
215
+ el.remove()
216
+ }
217
+
218
+ return found
219
+ }
220
+
221
+ // The controls' globals, registered without the warning a second registration of
222
+ // a name the book's setup file already took would print.
223
+ function controlComponents(): Record<string, Component> {
224
+ const registry: Record<string, Component> = {}
225
+ registerControlComponents({ component: (name: string, component: Component) => {
226
+ registry[name] = component
227
+ } } as unknown as App)
228
+ return registry
229
+ }
230
+
231
+ /**
232
+ * Gives a story's app the context of the app the test environment boots, so the
233
+ * story's `useNuxtApp()` and the plugins' injections resolve as in `poveste dev`.
234
+ * What `@nuxt/test-utils`' own mounts copy.
235
+ */
236
+ function inheritEnvironmentApp(app: App) {
237
+ const host: App | undefined = environmentApp()
238
+ if (!host) {
239
+ return
240
+ }
241
+ copyMissing(app, host)
242
+ copyMissing(app._context.provides, host._context.provides)
243
+ copyMissing(app.config.globalProperties, host.config.globalProperties)
244
+ copyMissing(app._context.components, host._context.components)
245
+ copyMissing(app._context.directives, host._context.directives)
246
+ }
247
+
248
+ // By descriptor: Nuxt defines `$nuxt` and its injections as getters that aren't
249
+ // enumerable, so a spread or `Object.assign` leaves them behind.
250
+ function copyMissing(target: object, source: object) {
251
+ for (const key of Reflect.ownKeys(source)) {
252
+ if (!(key in target)) {
253
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)!)
254
+ }
255
+ }
256
+ }
257
+
258
+ async function settled(done: () => boolean, timeout: number): Promise<boolean> {
259
+ const deadline = Date.now() + timeout
260
+ do {
261
+ await nextTick()
262
+ await new Promise(resolve => setTimeout(resolve))
263
+ if (done()) {
264
+ return true
265
+ }
266
+ } while (Date.now() < deadline)
267
+ return done()
268
+ }
269
+
270
+ function failure(where: string, found: unknown[]): Error {
271
+ const first = found[0]
272
+ const lines = found.map(item => item instanceof Error ? (item.stack ?? item.message) : String(item))
273
+ const error = new Error(`${where}: ${found.length === 1 ? 'one problem' : `${found.length} problems`}\n\n${lines.join('\n\n')}`)
274
+ if (first instanceof Error) {
275
+ error.cause = first
276
+ }
277
+ return error
278
+ }
279
+
280
+ function vueWarning(message: string, trace: string): string {
281
+ return `[Vue warn]: ${message}${trace ? `\n${trace}` : ''}`
282
+ }
283
+
284
+ function fileName(path: string | undefined): string | undefined {
285
+ return path?.split(/[\\/]/).pop()
286
+ }
@@ -0,0 +1,194 @@
1
+ import type { PovesteConfig } from 'poveste'
2
+ import type { Plugin, UserConfig } from 'vite'
3
+ import { existsSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import { dirname, join, relative, resolve, sep } from 'node:path'
6
+ import { clientConfig, VIRTUAL_IDS } from '@poveste/shared'
7
+ import { getDefaultConfig, loadConfigFile, mergeConfig, resolveConfigFile } from 'poveste'
8
+ import { VUE_SETUP_HOOK_NAMES } from '../setup-hooks.js'
9
+ import { projectVueVersion } from '../util/project-vue.js'
10
+ import { VAPOR_INTEROP_ID, vaporInteropModule } from '../util/vapor.js'
11
+
12
+ const EMPTY_SETUP_ID = '\0poveste-testing:empty-setup'
13
+ const CONFIG_RESOLVED_ID = `\0${VIRTUAL_IDS.config}`
14
+ const ENVIRONMENT_ID = 'virtual:$poveste-testing-environment'
15
+
16
+ // Vitest hands what it finds in `node_modules` to Node, which can't load the
17
+ // `virtual:` modules these packages import, so in a book that installs them no test
18
+ // ran (#1503). The workspace links them, and Vitest inlines a linked package.
19
+ const POVESTE_PACKAGES = [/\/node_modules\/poveste\//, /\/node_modules\/@poveste\//]
20
+
21
+ const EMPTY_SETUP = VUE_SETUP_HOOK_NAMES.map(name => `export const ${name} = undefined`).join('\n')
22
+
23
+ // `logEvent` from `poveste/client` reaches `@poveste/app`, which names these. A
24
+ // test never runs the code that reads them, but Vite's dependency scan follows
25
+ // the imports, and one it can't resolve skips pre-bundling and reloads the
26
+ // browser run partway through.
27
+ const INERT: Record<string, string> = {
28
+ [VIRTUAL_IDS.markdownFiles]: 'export const markdownFiles = []\n',
29
+ [VIRTUAL_IDS.componentDocs]: 'export const componentDocs = {}\n',
30
+ [VIRTUAL_IDS.supportPluginsClient]: 'export const clientSupportPlugins = {}\n',
31
+ }
32
+
33
+ // Nuxt boots in `@nuxt/test-utils`' `beforeAll`, after Vitest has collected the
34
+ // tests, and a story's own setup can call a Nuxt composable while it is being
35
+ // collected. So its own `setupNuxt` runs before collection, into the promise that
36
+ // `beforeAll` awaits, and Nuxt boots once and in full. Both names are internal to
37
+ // `@nuxt/test-utils`: if a version renames them, the story tests fail loudly.
38
+ function nuxtEnvironment(setupNuxt: string) {
39
+ return `import { tryUseNuxtApp } from '#app/nuxt'
40
+ import { setupNuxt } from ${JSON.stringify(setupNuxt)}
41
+
42
+ export async function environmentReady() {
43
+ const win = window
44
+ if (!win.__NUXT_VITEST_ENVIRONMENT__ || tryUseNuxtApp()) {
45
+ return
46
+ }
47
+ win.__NUXT_VITEST_ENVIRONMENT_PROMISE__ ??= setupNuxt()
48
+ await win.__NUXT_VITEST_ENVIRONMENT_PROMISE__
49
+ }
50
+
51
+ export function environmentApp() {
52
+ return tryUseNuxtApp()?.vueApp
53
+ }
54
+ `
55
+ }
56
+
57
+ const NO_ENVIRONMENT = `export async function environmentReady() {}\nexport function environmentApp() {\n return undefined\n}\n`
58
+
59
+ // `@nuxt/test-utils`' own boot, by file: its `exports` don't list it.
60
+ function setupNuxtFile(root: string): string | undefined {
61
+ try {
62
+ const file = join(dirname(createRequire(join(root, 'package.json')).resolve('@nuxt/test-utils/package.json')), 'dist/runtime/shared/nuxt.mjs')
63
+ return existsSync(file) ? file : undefined
64
+ }
65
+ catch {
66
+ return undefined
67
+ }
68
+ }
69
+
70
+ // Root-relative, as `poveste dev` reads it: the docs write `/src/poveste.setup.ts`,
71
+ // which `resolve` takes as absolute, so pre-bundling scanned a file outside the
72
+ // book and the setup file's imports were found late, with a reload (#1507).
73
+ function setupFileOf(config: Partial<PovesteConfig>): string | undefined {
74
+ const setupFile = config.setupFile
75
+ const file = typeof setupFile === 'string' ? setupFile : setupFile && 'browser' in setupFile ? setupFile.browser : undefined
76
+ return file?.replace(/^\/+/, '')
77
+ }
78
+
79
+ export interface PovesteTestingOptions {
80
+ /** The book's poveste config file. Found from the Vite root by default. */
81
+ configFile?: string
82
+ }
83
+
84
+ /**
85
+ * What a story needs in the user's own Vitest to render as `poveste dev` renders
86
+ * it: the book's setup file and the modules Poveste's client imports, with
87
+ * Poveste's own packages inlined so Vitest can serve those, and `vue` and the
88
+ * setup file pre-bundled for browser mode. Nothing else: no alias and no change
89
+ * to how anything resolves, so the test sees the project's Vite config as the
90
+ * project wrote it.
91
+ *
92
+ * Setup code that a plugin generates for `poveste dev`, such as the Nuxt app
93
+ * `@poveste/plugin-nuxt` boots, is left to the test environment, which already
94
+ * provides it: `@nuxt/test-utils`'s `nuxt` environment does in a Nuxt project.
95
+ */
96
+ export function povesteTesting(options: PovesteTestingOptions = {}): Plugin {
97
+ let root = process.cwd()
98
+ let config: Partial<PovesteConfig> = {}
99
+
100
+ return {
101
+ name: 'poveste:testing',
102
+
103
+ async config(user) {
104
+ root = resolve(user.root ?? process.cwd())
105
+ const file = options.configFile ? resolve(root, options.configFile) : resolveConfigFile(root)
106
+ const fromFile = file ? await loadConfigFile(file) : {}
107
+ // As `poveste dev` reads it: the config file, then the Vite config's key.
108
+ const fromVite = ((user as { poveste?: unknown, histoire?: unknown }).poveste
109
+ ?? (user as { histoire?: unknown }).histoire ?? {}) as Partial<PovesteConfig>
110
+ config = mergeConfig(fromFile, fromVite) as Partial<PovesteConfig>
111
+
112
+ // In browser mode Vite pre-bundles what its scan finds. It never sees `vue`,
113
+ // which compiled templates and this package's own bundle import, nor the
114
+ // setup file, which arrives as a virtual module. Each was bundled later,
115
+ // with a reload partway through the first run, and two copies of Vue
116
+ // failed it (#1503).
117
+ const setupFile = setupFileOf(config)
118
+ const optimizeDeps = {
119
+ include: ['vue'],
120
+ ...setupFile ? { entries: [relative(root, resolve(root, setupFile)).split(sep).join('/')] } : {},
121
+ }
122
+ // `true` already inlines everything, and an array merged into it would not.
123
+ if ((user as { test?: { server?: { deps?: { inline?: unknown } } } }).test?.server?.deps?.inline === true) {
124
+ return { optimizeDeps }
125
+ }
126
+ return { optimizeDeps, test: { server: { deps: { inline: POVESTE_PACKAGES } } } } as UserConfig
127
+ },
128
+
129
+ configResolved(resolved) {
130
+ root = resolved.root
131
+ },
132
+
133
+ async resolveId(id, importer) {
134
+ if (id === VIRTUAL_IDS.setup) {
135
+ const file = setupFileOf(config)
136
+ if (file) {
137
+ return this.resolve(resolve(root, file), importer, { skipSelf: true })
138
+ }
139
+ return EMPTY_SETUP_ID
140
+ }
141
+ if (id === VIRTUAL_IDS.generatedGlobalSetup) {
142
+ return EMPTY_SETUP_ID
143
+ }
144
+ if (id === VIRTUAL_IDS.config) {
145
+ return CONFIG_RESOLVED_ID
146
+ }
147
+ if (id === VAPOR_INTEROP_ID) {
148
+ return `\0${VAPOR_INTEROP_ID}`
149
+ }
150
+ if (Object.hasOwn(INERT, id)) {
151
+ return `\0${id}`
152
+ }
153
+ if (id === ENVIRONMENT_ID) {
154
+ return `\0${ENVIRONMENT_ID}`
155
+ }
156
+ return undefined
157
+ },
158
+
159
+ // A story's `<docs>` block, which `poveste dev` renders as markdown. A test
160
+ // renders the variants, not the docs, so it only has to import.
161
+ transform(_code, id) {
162
+ if (id.includes('?vue&type=docs')) {
163
+ return 'export default () => {}'
164
+ }
165
+ return undefined
166
+ },
167
+
168
+ async load(id) {
169
+ if (id === EMPTY_SETUP_ID) {
170
+ return EMPTY_SETUP
171
+ }
172
+ if (id === CONFIG_RESOLVED_ID) {
173
+ return `export const config = ${JSON.stringify(clientConfig({ ...getDefaultConfig(), ...config }))}\nexport const logos = {}\n`
174
+ }
175
+ if (id === `\0${VAPOR_INTEROP_ID}`) {
176
+ return vaporInteropModule(projectVueVersion(root))
177
+ }
178
+ if (id.startsWith('\0') && Object.hasOwn(INERT, id.slice(1))) {
179
+ return INERT[id.slice(1)]
180
+ }
181
+ // The app the test environment boots: Nuxt's, under `@nuxt/test-utils`' `nuxt`
182
+ // environment. `poveste dev` renders a story inside the Nuxt app its plugin
183
+ // boots, and a test does the same. Told by Nuxt's own `#app` alias and the
184
+ // test utils being installed, not by `#imports`, which unimport also serves.
185
+ if (id === `\0${ENVIRONMENT_ID}`) {
186
+ const setupNuxt = setupNuxtFile(root)
187
+ return setupNuxt && await this.resolve('#app/nuxt', resolve(root, 'index.html'))
188
+ ? nuxtEnvironment(setupNuxt)
189
+ : NO_ENVIRONMENT
190
+ }
191
+ return undefined
192
+ },
193
+ }
194
+ }
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { join } from 'node:path'
4
+
5
+ export function projectVueVersion(root: string): string | undefined {
6
+ try {
7
+ const manifest = createRequire(join(root, 'package.json')).resolve('vue/package.json')
8
+ return JSON.parse(readFileSync(manifest, 'utf8')).version
9
+ }
10
+ catch {
11
+ return undefined
12
+ }
13
+ }
@@ -0,0 +1,32 @@
1
+ import type { Plugin as VitePlugin } from 'vite'
2
+ import { existsSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ const QUASAR_CONFIGS = ['quasar.config.js', 'quasar.config.mjs', 'quasar.config.cjs', 'quasar.config.ts']
6
+
7
+ /**
8
+ * What to say when a book's Vite config has no Vue plugin. Without one, Vite
9
+ * answers the first `.vue` file with "Install @vitejs/plugin-vue", which sends a
10
+ * Quasar reader to install a package their project does not need: Quasar brings
11
+ * its own Vue plugin, and `HstQuasar()` is what hands it to Poveste (#1337).
12
+ */
13
+ export function noVuePluginMessage(root: string, exists: (path: string) => boolean = existsSync): string {
14
+ const fix = QUASAR_CONFIGS.some(name => exists(join(root, name)))
15
+ ? 'This is a Quasar project: add `HstQuasar()` from `@poveste/plugin-quasar` to the config\'s `plugins`, which brings Quasar\'s own Vue plugin.'
16
+ : 'Add `@vitejs/plugin-vue` to `vite.config`, or in a Nuxt project `HstNuxt()` from `@poveste/plugin-nuxt` to the config\'s `plugins`.'
17
+ return `[poveste] The Vite config has no Vue plugin, so no \`.vue\` story can be compiled. ${fix}`
18
+ }
19
+
20
+ /** Reports, once per process, a resolved Vite config with no Vue plugin in it. */
21
+ export function requireVuePlugin(): VitePlugin {
22
+ let reported = false
23
+ return {
24
+ name: 'poveste-plugin-vue:require-vue-plugin',
25
+ configResolved(config) {
26
+ if (!reported && !config.plugins.some(plugin => plugin.name === 'vite:vue')) {
27
+ reported = true
28
+ console.error(noVuePluginMessage(config.root))
29
+ }
30
+ },
31
+ }
32
+ }
package/testing.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // https://github.com/microsoft/TypeScript/issues/33079
2
+ export * from './dist/testing/index'