poveste 0.11.0 → 0.12.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Fast and beautiful interactive component playgrounds
4
4
 
5
5
  Poveste builds a browsable *book* of stories from your components, powered by
6
- [Vite](https://vite.dev). It is a community-maintained successor to
6
+ [Vite](https://vite.dev). It is a community-maintained fork of
7
7
  [histoire](https://github.com/histoire-dev/histoire).
8
8
 
9
9
  ## Install
@@ -41,7 +41,7 @@ Run `npx poveste --help` for the full list of options.
41
41
 
42
42
  ## Requirements
43
43
 
44
- Node `>=26` and Vite `^8.0.0`. See
44
+ Node `^22.22.2 || ^24.15.0 || >=26.0.0` and Vite `^8.0.0`. See
45
45
  [supported versions](https://poveste.dev/guide/getting-started.html#supported-versions) for
46
46
  what proves each range.
47
47
 
@@ -299,12 +299,11 @@ export async function build(ctx) {
299
299
  }
300
300
  function generateBaseHtml(head, body, ctx) {
301
301
  return `<!DOCTYPE html>
302
- <html>
302
+ <html lang="${ctx.config.theme.lang ?? 'en'}">
303
303
  <head>
304
304
  <title>${ctx.config.theme.title}</title>
305
305
  <meta charset="utf-8">
306
306
  <meta name="viewport" content="width=device-width,initial-scale=1">
307
- <meta name="description" content="">
308
307
  <style>html,body{margin:0;padding:0}</style>
309
308
  ${head}
310
309
  </head>
@@ -0,0 +1 @@
1
+ export declare const css = ".__pvt-shade {\n height: 80px;\n border-radius: 4px;\n}\n\n.__pvt-text {\n font-size: 4rem;\n display: flex;\n align-items: flex-end;\n}\n\n.__pvt-border {\n border-style: solid;\n border-width: 2px;\n}\n\n.__pvt-spaces {\n background-color: rgb(113 113 122 / 0.1);\n width: min-content;\n}\n\n.__pvt-margin {\n border: dashed 1px rgb(113 113 122 / 0.5);\n width: min-content;\n}\n\n.__pvt-spaces-box,\n.__pvt-margin-box {\n width: 5rem;\n height: 5rem;\n background-color: rgb(113 113 122 / 0.5);\n}\n\n.__pvt-spaces,\n.__pvt-spaces-box,\n.__pvt-margin,\n.__pvt-margin-box,\n.__pvt-drop-shadow {\n border-radius: 4px;\n}\n\n.__pvt-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.__pvt-drop-shadow {\n width: 8rem;\n height: 8rem;\n background: white;\n margin-bottom: 0.5rem;\n}\n\n.__pvt-drop-shadow {\n background: #4e4e57;\n}\n\n.__pvt-border-radius {\n width: 8rem;\n height: 8rem;\n background-color: rgb(113 113 122 / 0.5);\n}\n\n.__pvt-border-width {\n width: 8rem;\n height: 8rem;\n border-color: rgb(113 113 122 / 0.5);\n background-color: rgb(113 113 122 / 0.1);\n}\n\n.__pvt-width {\n background-color: rgb(113 113 122 / 0.1);\n}\n\n.__pvt-width-box,\n.__pvt-height {\n background-color: rgb(113 113 122 / 0.5);\n}\n\n.__pvt-width-box {\n height: 5rem;\n}";
@@ -0,0 +1,85 @@
1
+ // Injected as `style.css` next to the generated story file, so it styles
2
+ // the Pinceau token stories without reaching the book's own stylesheet.
3
+ export const css = `.__pvt-shade {
4
+ height: 80px;
5
+ border-radius: 4px;
6
+ }
7
+
8
+ .__pvt-text {
9
+ font-size: 4rem;
10
+ display: flex;
11
+ align-items: flex-end;
12
+ }
13
+
14
+ .__pvt-border {
15
+ border-style: solid;
16
+ border-width: 2px;
17
+ }
18
+
19
+ .__pvt-spaces {
20
+ background-color: rgb(113 113 122 / 0.1);
21
+ width: min-content;
22
+ }
23
+
24
+ .__pvt-margin {
25
+ border: dashed 1px rgb(113 113 122 / 0.5);
26
+ width: min-content;
27
+ }
28
+
29
+ .__pvt-spaces-box,
30
+ .__pvt-margin-box {
31
+ width: 5rem;
32
+ height: 5rem;
33
+ background-color: rgb(113 113 122 / 0.5);
34
+ }
35
+
36
+ .__pvt-spaces,
37
+ .__pvt-spaces-box,
38
+ .__pvt-margin,
39
+ .__pvt-margin-box,
40
+ .__pvt-drop-shadow {
41
+ border-radius: 4px;
42
+ }
43
+
44
+ .__pvt-truncate {
45
+ overflow: hidden;
46
+ text-overflow: ellipsis;
47
+ white-space: nowrap;
48
+ }
49
+
50
+ .__pvt-drop-shadow {
51
+ width: 8rem;
52
+ height: 8rem;
53
+ background: white;
54
+ margin-bottom: 0.5rem;
55
+ }
56
+
57
+ .__pvt-drop-shadow {
58
+ background: #4e4e57;
59
+ }
60
+
61
+ .__pvt-border-radius {
62
+ width: 8rem;
63
+ height: 8rem;
64
+ background-color: rgb(113 113 122 / 0.5);
65
+ }
66
+
67
+ .__pvt-border-width {
68
+ width: 8rem;
69
+ height: 8rem;
70
+ border-color: rgb(113 113 122 / 0.5);
71
+ background-color: rgb(113 113 122 / 0.1);
72
+ }
73
+
74
+ .__pvt-width {
75
+ background-color: rgb(113 113 122 / 0.1);
76
+ }
77
+
78
+ .__pvt-width-box,
79
+ .__pvt-height {
80
+ background-color: rgb(113 113 122 / 0.5);
81
+ }
82
+
83
+ .__pvt-width-box {
84
+ height: 5rem;
85
+ }`;
@@ -1,5 +1,6 @@
1
1
  import { join } from 'pathe';
2
2
  import { getInjectedImport } from '../util/vendors.js';
3
+ import { css } from './pinceau-tokens-style.js';
3
4
  export function pinceauTokens(_options = {}) {
4
5
  const themePath = join(process.cwd(), 'node_modules/.vite/pinceau/index.js');
5
6
  async function generate(api) {
@@ -421,86 +422,3 @@ export default {
421
422
  ],
422
423
  }`;
423
424
  }
424
- const css = `.__pvt-shade {
425
- height: 80px;
426
- border-radius: 4px;
427
- }
428
-
429
- .__pvt-text {
430
- font-size: 4rem;
431
- display: flex;
432
- align-items: flex-end;
433
- }
434
-
435
- .__pvt-border {
436
- border-style: solid;
437
- border-width: 2px;
438
- }
439
-
440
- .__pvt-spaces {
441
- background-color: rgb(113 113 122 / 0.1);
442
- width: min-content;
443
- }
444
-
445
- .__pvt-margin {
446
- border: dashed 1px rgb(113 113 122 / 0.5);
447
- width: min-content;
448
- }
449
-
450
- .__pvt-spaces-box,
451
- .__pvt-margin-box {
452
- width: 5rem;
453
- height: 5rem;
454
- background-color: rgb(113 113 122 / 0.5);
455
- }
456
-
457
- .__pvt-spaces,
458
- .__pvt-spaces-box,
459
- .__pvt-margin,
460
- .__pvt-margin-box,
461
- .__pvt-drop-shadow {
462
- border-radius: 4px;
463
- }
464
-
465
- .__pvt-truncate {
466
- overflow: hidden;
467
- text-overflow: ellipsis;
468
- white-space: nowrap;
469
- }
470
-
471
- .__pvt-drop-shadow {
472
- width: 8rem;
473
- height: 8rem;
474
- background: white;
475
- margin-bottom: 0.5rem;
476
- }
477
-
478
- .__pvt-drop-shadow {
479
- background: #4e4e57;
480
- }
481
-
482
- .__pvt-border-radius {
483
- width: 8rem;
484
- height: 8rem;
485
- background-color: rgb(113 113 122 / 0.5);
486
- }
487
-
488
- .__pvt-border-width {
489
- width: 8rem;
490
- height: 8rem;
491
- border-color: rgb(113 113 122 / 0.5);
492
- background-color: rgb(113 113 122 / 0.1);
493
- }
494
-
495
- .__pvt-width {
496
- background-color: rgb(113 113 122 / 0.1);
497
- }
498
-
499
- .__pvt-width-box,
500
- .__pvt-height {
501
- background-color: rgb(113 113 122 / 0.5);
502
- }
503
-
504
- .__pvt-width-box {
505
- height: 5rem;
506
- }`;
@@ -13,15 +13,26 @@ let _runner;
13
13
  // per story broke re-collection.
14
14
  let _domEnv;
15
15
  let _rpc;
16
+ // A virtual module is cached under the id the plugin resolved it to, not the one
17
+ // it was asked for, so both forms have to go.
18
+ function invalidate(file) {
19
+ _moduleCache.delete(file);
20
+ _moduleCache.delete(`\0${file}`);
21
+ }
16
22
  // Cleanup module cache
17
23
  parentPort.on('message', (message) => {
18
24
  if (message?.kind === 'hst:invalidate') {
19
- _moduleCache.delete(message.file);
25
+ invalidate(message.file);
20
26
  }
21
27
  });
22
28
  export default async (payload) => {
23
29
  const startTime = performance.now();
24
30
  process.env.HST_COLLECT = 'true';
31
+ // Here rather than broadcast from the main thread: the pool dispatches tasks
32
+ // on a different port from `broadcastMessage`, and nothing orders the two, so
33
+ // a busy worker could start the task first and read its own stale cache. A
34
+ // story being re-executed is being re-read by definition (#557).
35
+ invalidate(payload.storyFile.moduleId);
25
36
  // Before any module runs: an externalised dep reads these at import time.
26
37
  for (const [key, value] of Object.entries(payload.defineGlobals ?? {})) {
27
38
  ;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Type checks over the config options that are load-bearing.
3
+ *
4
+ * Nothing validated the config, so a wrong type surfaced wherever the value was
5
+ * first used — `outDir: 42` became `TypeError: input.replace is not a function`
6
+ * inside `pathe`, naming neither Poveste, nor the option, nor the config file
7
+ * (#324).
8
+ *
9
+ * Hand-written rather than a schema library: `poveste` already ships 31 runtime
10
+ * dependencies (#306), and this needs to name a key and a received type, not
11
+ * model the whole surface. Only options whose wrong type fails *inside someone
12
+ * else's code* are listed — the rest fail legibly on their own.
13
+ */
14
+ export type ConfigKind = 'string' | 'boolean' | 'number' | 'string[]' | 'array' | 'object' | 'function';
15
+ /** What the value actually is, in the words the message uses. */
16
+ export declare function describeKind(value: unknown): string;
17
+ export declare function configProblems(config: unknown, configFile: string): string[];
@@ -0,0 +1,95 @@
1
+ const RULES = [
2
+ { path: 'outDir', kind: 'string' },
3
+ { path: 'storyMatch', kind: 'string[]' },
4
+ { path: 'storyIgnored', kind: 'string[]' },
5
+ { path: 'plugins', kind: 'array' },
6
+ { path: 'setupFile', kind: ['string', 'object'] },
7
+ { path: 'setupCode', kind: 'string[]' },
8
+ { path: 'globalStyles', kind: 'string[]' },
9
+ { path: 'isolateStyles', kind: 'boolean' },
10
+ { path: 'autoApplyContrastColor', kind: 'boolean' },
11
+ { path: 'defaultBackgroundColor', kind: 'string' },
12
+ { path: 'sandboxDarkClass', kind: 'string' },
13
+ { path: 'responsivePresets', kind: 'array' },
14
+ { path: 'backgroundPresets', kind: 'array' },
15
+ { path: 'theme.title', kind: 'string' },
16
+ { path: 'theme.lang', kind: 'string' },
17
+ { path: 'theme.darkClass', kind: 'string' },
18
+ { path: 'theme.favicon', kind: 'string' },
19
+ { path: 'theme.logo.square', kind: 'string' },
20
+ { path: 'theme.logo.light', kind: 'string' },
21
+ { path: 'theme.logo.dark', kind: 'string' },
22
+ { path: 'theme.logoHref', kind: 'string' },
23
+ { path: 'tree.file', kind: ['string', 'function'] },
24
+ { path: 'tree.order', kind: ['string', 'function'] },
25
+ { path: 'vite', kind: ['object', 'function'] },
26
+ ];
27
+ /** What the value actually is, in the words the message uses. */
28
+ export function describeKind(value) {
29
+ if (Array.isArray(value)) {
30
+ return value.every(item => typeof item === 'string') ? 'string[]' : 'array';
31
+ }
32
+ return typeof value;
33
+ }
34
+ function matches(value, kind) {
35
+ switch (kind) {
36
+ case 'string[]':
37
+ return Array.isArray(value) && value.every(item => typeof item === 'string');
38
+ case 'array':
39
+ return Array.isArray(value);
40
+ case 'object':
41
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
42
+ case 'string':
43
+ return typeof value === 'string';
44
+ case 'boolean':
45
+ return typeof value === 'boolean';
46
+ case 'number':
47
+ return typeof value === 'number';
48
+ case 'function':
49
+ return typeof value === 'function';
50
+ default:
51
+ return false;
52
+ }
53
+ }
54
+ function read(config, path) {
55
+ let current = config;
56
+ for (const key of path.split('.')) {
57
+ if (current == null || typeof current !== 'object' || !(key in current)) {
58
+ return { present: false, value: undefined };
59
+ }
60
+ current = current[key];
61
+ }
62
+ return { present: true, value: current };
63
+ }
64
+ /**
65
+ * The value echoed back, short enough to be worth reading.
66
+ *
67
+ * JSON rather than `String(value)`: an object stringifies to `[object Object]`,
68
+ * which tells the reader nothing about what they wrote.
69
+ */
70
+ function preview(value) {
71
+ let text;
72
+ try {
73
+ text = JSON.stringify(value) ?? String(value);
74
+ }
75
+ catch {
76
+ text = String(value);
77
+ }
78
+ return text.length > 40 ? `${text.slice(0, 40)}…` : text;
79
+ }
80
+ export function configProblems(config, configFile) {
81
+ const problems = [];
82
+ for (const { path, kind } of RULES) {
83
+ const { present, value } = read(config, path);
84
+ // An absent or explicitly-undefined option falls back to the default.
85
+ if (!present || value === undefined) {
86
+ continue;
87
+ }
88
+ const kinds = Array.isArray(kind) ? kind : [kind];
89
+ if (kinds.some(one => matches(value, one))) {
90
+ continue;
91
+ }
92
+ problems.push(`${configFile}: \`${path}\` must be ${kinds.join(' or ')}, received ${describeKind(value)} (${preview(value)})`);
93
+ }
94
+ return problems;
95
+ }
@@ -7,6 +7,7 @@ import pc from 'picocolors';
7
7
  import { mergeConfig as mergeViteConfig, resolveConfig as resolveViteConfig, } from 'vite';
8
8
  import { vanillaSupport } from './builtin-plugins/vanilla-support/plugin.js';
9
9
  import { defaultColors } from './colors.js';
10
+ import { configProblems } from './config-validation.js';
10
11
  import { findUp } from './util/find-up.js';
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  export function getDefaultConfig() {
@@ -38,6 +39,7 @@ export function getDefaultConfig() {
38
39
  defaultColorScheme: 'auto',
39
40
  storeColorScheme: true,
40
41
  darkClass: 'dark',
42
+ lang: 'en',
41
43
  },
42
44
  responsivePresets: [
43
45
  {
@@ -164,6 +166,16 @@ export async function loadConfigFile(configFile) {
164
166
  }
165
167
  catch (e) {
166
168
  console.error(pc.red(`Error while loading ${configFile}`));
169
+ const missing = e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND'
170
+ ? (String(e.message).match(/Cannot find module '([^']+)'/) ?? [])[1]
171
+ : undefined;
172
+ if (missing && !process.env.DEBUG) {
173
+ // The stack is all module-loader and jiti frames, which diagnose neither
174
+ // an uninstalled package nor a typo (#324). `DEBUG=1` keeps it.
175
+ console.error(pc.red(`Cannot find module '${missing}'`));
176
+ console.error(pc.dim('Is it installed, and is the name spelt correctly? Run with DEBUG=1 for the full stack.'));
177
+ throw new Error(`Cannot find module '${missing}' imported from ${configFile}`);
178
+ }
167
179
  throw e;
168
180
  }
169
181
  }
@@ -237,17 +249,37 @@ export const mergeConfig = createDefu((obj, key, value) => {
237
249
  return true;
238
250
  }
239
251
  });
252
+ /** Print every problem, then stop before anything consumes the value. */
253
+ function reportConfigProblems(config, configFile) {
254
+ const problems = configProblems(config, configFile);
255
+ if (problems.length === 0) {
256
+ return;
257
+ }
258
+ for (const problem of problems) {
259
+ console.error(pc.red(problem));
260
+ }
261
+ throw new Error(`Invalid Poveste config: ${problems.length} problem${problems.length === 1 ? '' : 's'} above.`);
262
+ }
240
263
  export async function resolveConfig(cwd = process.cwd(), mode, configFile) {
241
264
  let result;
242
265
  const resolvedConfigFile = resolveConfigFile(cwd, configFile);
243
266
  if (resolvedConfigFile) {
244
267
  result = await loadConfigFile(resolvedConfigFile);
268
+ // Before the merge, so the message quotes what the user actually wrote
269
+ // rather than a value defaults supplied, and before anything consumes it —
270
+ // the whole point is not reaching `pathe` with a number (#324).
271
+ reportConfigProblems(result, path.relative(cwd, resolvedConfigFile) || resolvedConfigFile);
245
272
  }
246
273
  const viteConfig = await resolveViteConfig({}, 'serve');
247
274
  // The `histoire` key is our own deprecation, and reading it is the whole
248
275
  // point — it is what makes poveste drop-in for an existing histoire config.
249
276
  // eslint-disable-next-line ts/no-deprecated
250
277
  const vitePovesteConfig = (viteConfig.poveste ?? viteConfig.histoire ?? {});
278
+ // The other place a config is written, and validating only `poveste.config.ts`
279
+ // left it out: `examples/svelte5` and `examples/sveltekit` have no such file
280
+ // and configure poveste entirely under this key, so half the reference books
281
+ // took the unvalidated path (#324).
282
+ reportConfigProblems(vitePovesteConfig, viteConfig.configFile ? path.relative(cwd, viteConfig.configFile) : 'the vite config');
251
283
  const preUserConfig = mergeConfig(result, vitePovesteConfig);
252
284
  const processedDefaultConfig = await processDefaultConfig(getDefaultConfig(), preUserConfig, mode, cwd);
253
285
  return resolveConfigPlugins(mergeConfig(preUserConfig, processedDefaultConfig), mode);
@@ -1,7 +1,9 @@
1
+ import type { ServerMarkdownFile } from '@poveste/shared';
1
2
  import type { Plugin as VitePlugin } from 'vite';
2
3
  import type { Context } from './context.js';
3
4
  import MarkdownIt from 'markdown-it';
4
5
  export declare function onMarkdownListChange(handler: () => unknown): void;
6
+ export declare function onMarkdownFileChange(handler: (file: ServerMarkdownFile) => unknown): void;
5
7
  export declare function createMarkdownRenderer(ctx: Context): Promise<MarkdownIt>;
6
8
  export declare function createMarkdownPlugins(ctx: Context): Promise<VitePlugin<any>[]>;
7
9
  export declare function createMarkdownFilesWatcher(ctx: Context): Promise<{
@@ -21,6 +21,24 @@ function notifyMarkdownListChange() {
21
21
  handler();
22
22
  }
23
23
  }
24
+ /**
25
+ * One markdown file's *content* changed, as distinct from the list of files
26
+ * changing.
27
+ *
28
+ * The rendered html reaches the client as its own virtual module,
29
+ * `virtual:md:<id>`, so invalidating the list module is not enough to repaint an
30
+ * open docs pane — the list is the same, and only that file's module is stale
31
+ * (#370). The module already carries the `import.meta.hot.accept` half.
32
+ */
33
+ const onMarkdownFileChangeHandlers = [];
34
+ export function onMarkdownFileChange(handler) {
35
+ onMarkdownFileChangeHandlers.push(handler);
36
+ }
37
+ function notifyMarkdownFileChange(file) {
38
+ for (const handler of onMarkdownFileChangeHandlers) {
39
+ handler(file);
40
+ }
41
+ }
24
42
  /**
25
43
  * Shared between the markdown Vite plugin and the markdown file watcher, which
26
44
  * both build a renderer — a `poveste build` reaches this three times. The
@@ -132,6 +150,21 @@ export async function createMarkdownPlugins(ctx) {
132
150
  });
133
151
  return plugins;
134
152
  }
153
+ /**
154
+ * The story a standalone `.story.md` gets: no sibling story file, so its story
155
+ * is derived from frontmatter rather than collected from source.
156
+ */
157
+ function derivedStoryCode(frontmatter) {
158
+ return `export default ${JSON.stringify({
159
+ id: frontmatter.id,
160
+ title: frontmatter.title,
161
+ icon: frontmatter.icon ?? 'carbon:document-blank',
162
+ iconColor: frontmatter.iconColor,
163
+ group: frontmatter.group,
164
+ docsOnly: true,
165
+ variants: [],
166
+ })}`;
167
+ }
135
168
  export async function createMarkdownFilesWatcher(ctx) {
136
169
  const md = await createMarkdownRendererWithPlugins(ctx);
137
170
  const watcher = chokidar.watch('.', {
@@ -169,15 +202,7 @@ export async function createMarkdownFilesWatcher(ctx) {
169
202
  ctx.markdownFiles.push(file);
170
203
  if (!isRelatedToStory) {
171
204
  const storyRelativePath = relativePath.replace(/\.md$/, '.js');
172
- const storyFile = addStory(storyRelativePath, `export default ${JSON.stringify({
173
- id: frontmatter.id,
174
- title: frontmatter.title,
175
- icon: frontmatter.icon ?? 'carbon:document-blank',
176
- iconColor: frontmatter.iconColor,
177
- group: frontmatter.group,
178
- docsOnly: true,
179
- variants: [],
180
- })}`);
205
+ const storyFile = addStory(storyRelativePath, derivedStoryCode(frontmatter));
181
206
  file.storyFile = storyFile;
182
207
  storyFile.markdownFile = file;
183
208
  notifyStoryChange(storyFile);
@@ -206,12 +231,50 @@ export async function createMarkdownFilesWatcher(ctx) {
206
231
  notifyMarkdownListChange();
207
232
  }
208
233
  }
234
+ /**
235
+ * Chokidar emits `change` for an edit to an existing file, and nothing
236
+ * handled it: `addFile` is the only path that reads and renders markdown, and
237
+ * it ran on `add` alone, so an edit was watched, delivered and dropped (#370).
238
+ *
239
+ * Story files get away with the same two-handler shape because they travel
240
+ * through Vite's module graph and Vite updates them. Markdown does not —
241
+ * `addFile` reads and renders in Node, outside the graph.
242
+ */
243
+ function updateFile(relativePath) {
244
+ const file = ctx.markdownFiles.find(file => file.relativePath === relativePath);
245
+ if (!file) {
246
+ // Not tracked yet — a file that did not match when it was added, say.
247
+ addFile(relativePath);
248
+ return;
249
+ }
250
+ const { data: frontmatter, content } = matter(fs.readFileSync(file.absolutePath, 'utf8'));
251
+ file.frontmatter = frontmatter;
252
+ file.content = content;
253
+ // Rewritten in place rather than by removing and re-adding the story:
254
+ // `moduleId` is derived from the path, so a re-added story carries the id
255
+ // the client already imported and nothing would tell it to look again
256
+ // (#539).
257
+ if (!file.isRelatedToStory && file.storyFile) {
258
+ file.storyFile.moduleCode = derivedStoryCode(frontmatter);
259
+ }
260
+ file.html = md.render(content, {
261
+ file: file.absolutePath,
262
+ });
263
+ notifyMarkdownFileChange(file);
264
+ if (file.storyFile) {
265
+ notifyStoryChange(file.storyFile);
266
+ }
267
+ notifyMarkdownListChange();
268
+ }
209
269
  async function stop() {
210
270
  await watcher.close();
211
271
  }
212
272
  watcher
213
273
  .on('add', (relativePath) => {
214
274
  addFile(relativePath);
275
+ })
276
+ .on('change', (relativePath) => {
277
+ updateFile(relativePath);
215
278
  })
216
279
  .on('unlink', (relativePath) => {
217
280
  removeFile(relativePath);
@@ -4,7 +4,7 @@ import { createServer as createViteServer, mergeConfig as mergeViteConfig } from
4
4
  import { useCollectStories } from './collect/index.js';
5
5
  import { hmrPortFor } from './commands/port.js';
6
6
  import { useModuleLoader } from './load.js';
7
- import { createMarkdownFilesWatcher, onMarkdownListChange } from './markdown.js';
7
+ import { createMarkdownFilesWatcher, onMarkdownFileChange, onMarkdownListChange } from './markdown.js';
8
8
  import { DevEventPluginApi, DevPluginApi } from './plugin.js';
9
9
  import { onStoryChange, onStoryListChange, watchStories } from './stories.js';
10
10
  import { wrapLogError } from './util/log.js';
@@ -192,6 +192,11 @@ export async function createServer(ctx, options = {}) {
192
192
  onMarkdownListChange(() => {
193
193
  invalidateModule(VirtualFiles.RESOLVED_MARKDOWN_FILES);
194
194
  });
195
+ // The list module is unchanged when a file is merely edited — the stale one is
196
+ // that file's own module, which is what carries the rendered html (#370).
197
+ onMarkdownFileChange((file) => {
198
+ invalidateModule(`/__resolved__virtual:md:${file.id}`);
199
+ });
195
200
  async function close() {
196
201
  for (const cb of pluginOnCleanups) {
197
202
  await wrapLogError('plugin.onDev.onCleanup', () => cb());
package/dist/node/vite.js CHANGED
@@ -207,12 +207,11 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
207
207
  res.statusCode = 200;
208
208
  let html = `
209
209
  <!DOCTYPE html>
210
- <html>
210
+ <html lang="${ctx.config.theme?.lang ?? 'en'}">
211
211
  <head>
212
212
  <title></title>
213
213
  <meta charset="utf-8">
214
214
  <meta name="viewport" content="width=device-width,initial-scale=1">
215
- <meta name="description" content="">
216
215
  </head>
217
216
  <body>
218
217
  <div id="app"></div>
@@ -245,13 +244,12 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
245
244
  res.statusCode = 200;
246
245
  let html = `
247
246
  <!DOCTYPE html>
248
- <html>
247
+ <html lang="${ctx.config.theme?.lang ?? 'en'}">
249
248
  <head>
250
249
  <title></title>
251
250
  <link rel="icon" href=""/>
252
251
  <meta charset="utf-8">
253
252
  <meta name="viewport" content="width=device-width,initial-scale=1">
254
- <meta name="description" content="">
255
253
  ${ctx.config.theme?.favicon ? `<link rel="icon" type="${lookupMime(ctx.config.theme.favicon)}" href="${server.config.base}${ctx.config.theme.favicon}"/>` : ''}
256
254
  <style>html,body{margin:0;padding:0}</style>
257
255
  </head>
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.11.0",
5
- "description": "Interactive component playgrounds for Vue, Svelte and Nuxt — a maintained, drop-in successor to histoire",
4
+ "version": "0.12.1",
5
+ "description": "Interactive component playgrounds for Vue, Nuxt, Svelte, SvelteKit and Quasar — a drop-in fork of histoire, with Quasar support histoire does not ship.",
6
6
  "author": {
7
- "name": "Sorin Gitlan"
7
+ "name": "Sorin Gitlan",
8
+ "url": "https://x.com/50rayn"
8
9
  },
9
10
  "license": "MIT",
10
11
  "homepage": "https://poveste.dev",
@@ -72,6 +73,10 @@
72
73
  },
73
74
  "dependencies": {
74
75
  "@akryum/tinypool": "^0.3.1",
76
+ "@poveste/app": "^0.12.1",
77
+ "@poveste/controls": "^0.12.1",
78
+ "@poveste/shared": "^0.12.1",
79
+ "@poveste/vendors": "^0.12.1",
75
80
  "@types/markdown-it": "^14.1.2",
76
81
  "birpc": "^0.2.19",
77
82
  "change-case": "^5.4.4",
@@ -97,11 +102,7 @@
97
102
  "shiki": "^3.0.0",
98
103
  "sirv": "^3.0.0",
99
104
  "unhead": "^3.1.0",
100
- "vite-node": "^6.0.0",
101
- "@poveste/app": "^0.11.0",
102
- "@poveste/controls": "^0.11.0",
103
- "@poveste/shared": "^0.11.0",
104
- "@poveste/vendors": "^0.11.0"
105
+ "vite-node": "^6.0.0"
105
106
  },
106
107
  "devDependencies": {
107
108
  "@types/fs-extra": "^11.0.4",