poveste 0.2.0 → 0.3.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 (35) hide show
  1. package/dist/node/build.js +0 -3
  2. package/dist/node/builtin-plugins/tailwind-tokens.d.ts +6 -1
  3. package/dist/node/builtin-plugins/tailwind-tokens.d.ts.map +1 -1
  4. package/dist/node/builtin-plugins/tailwind-tokens.js +169 -33
  5. package/dist/node/builtin-plugins/vanilla-support/MountStory.d.ts +3 -3
  6. package/dist/node/builtin-plugins/vanilla-support/RenderStory.d.ts +5 -5
  7. package/dist/node/collect/index.d.ts.map +1 -1
  8. package/dist/node/collect/index.js +1 -3
  9. package/dist/node/config.d.ts +2 -18
  10. package/dist/node/config.d.ts.map +1 -1
  11. package/dist/node/config.js +3 -0
  12. package/dist/node/dom/env.d.ts.map +1 -1
  13. package/dist/node/dom/env.js +10 -6
  14. package/dist/node/index.d.ts +0 -7
  15. package/dist/node/index.d.ts.map +1 -1
  16. package/dist/node/markdown.d.ts.map +1 -1
  17. package/dist/node/markdown.js +21 -3
  18. package/dist/node/stories.js +1 -1
  19. package/dist/node/style-isolation/entry-css-merger.d.ts.map +1 -1
  20. package/dist/node/style-isolation/entry-css-merger.js +8 -7
  21. package/dist/node/vite.js +1 -1
  22. package/package.json +10 -12
  23. package/src/node/__tests__/dom-env.spec.ts +19 -1
  24. package/src/node/build.ts +2 -6
  25. package/src/node/builtin-plugins/tailwind-tokens.ts +190 -34
  26. package/src/node/collect/index.ts +1 -3
  27. package/src/node/config.ts +3 -0
  28. package/src/node/dom/env.ts +10 -6
  29. package/src/node/index.ts +0 -8
  30. package/src/node/markdown.ts +25 -3
  31. package/src/node/stories.ts +1 -1
  32. package/src/node/style-isolation/__tests__/entry-css-merger.spec.ts +37 -18
  33. package/src/node/style-isolation/entry-css-merger.ts +10 -9
  34. package/src/node/vite.ts +4 -4
  35. package/tsconfig.json +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "A fast story book powered by Vite",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -38,7 +38,7 @@
38
38
  "histoire": "./bin.mjs"
39
39
  },
40
40
  "peerDependencies": {
41
- "vite": "^7.0.0 || ^8.0.0"
41
+ "vite": "^8.0.0"
42
42
  },
43
43
  "dependencies": {
44
44
  "@akryum/tinypool": "^0.3.1",
@@ -53,7 +53,7 @@
53
53
  "globby": "^14.0.2",
54
54
  "gray-matter": "^4.0.3",
55
55
  "jiti": "^2.4.1",
56
- "jsdom": "^27.4.0",
56
+ "jsdom": "^30.0.1",
57
57
  "lightningcss": "^1.32.0",
58
58
  "markdown-it": "^14.1.0",
59
59
  "markdown-it-anchor": "^9.2.0",
@@ -67,20 +67,18 @@
67
67
  "shiki": "^3.0.0",
68
68
  "sirv": "^3.0.0",
69
69
  "unhead": "^3.1.0",
70
- "vite-node": "3.2.4",
71
- "@poveste/app": "^0.2.0",
72
- "@poveste/controls": "^0.2.0",
73
- "@poveste/shared": "^0.2.0",
74
- "@poveste/vendors": "^0.2.0"
70
+ "vite-node": "^6.0.0",
71
+ "@poveste/app": "^0.3.0",
72
+ "@poveste/controls": "^0.3.0",
73
+ "@poveste/shared": "^0.3.0",
74
+ "@poveste/vendors": "^0.3.0"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/fs-extra": "^11.0.4",
78
78
  "@types/node": "^22.10.1",
79
- "esbuild": "^0.27.2",
80
- "rollup": "^4.28.0",
81
79
  "typescript": "5.6.3",
82
- "vite": "^7.3.0",
83
- "vitest": "4.0.16"
80
+ "vite": "^8.2.0",
81
+ "vitest": "^4.1.10"
84
82
  },
85
83
  "scripts": {
86
84
  "build": "rimraf dist && tsc",
@@ -10,8 +10,25 @@ describe('createDomEnv', () => {
10
10
  errorSpy?.mockRestore()
11
11
  })
12
12
 
13
- describe('when a stylesheet contains unparseable directives (e.g. Tailwind v4 @theme)', () => {
13
+ describe('when a stylesheet contains CSS jsdom cannot parse', () => {
14
+ // An unparseable selector is one of the few inputs jsdom 28+ still reports
15
+ // a css-parsing jsdomError for, so it is what keeps this guard honest.
16
+ // Remove the suppression in `env.ts` and this test fails.
14
17
  it('does not log a css-parsing jsdomError', () => {
18
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
19
+ env = createDomEnv()
20
+ const style = env.window.document.createElement('style')
21
+ style.textContent = '.btn[[[ { color: red }'
22
+
23
+ env.window.document.head.appendChild(style)
24
+
25
+ expect(errorSpy).not.toHaveBeenCalled()
26
+ })
27
+
28
+ // The symptom that motivated the suppression. jsdom now drops the unknown
29
+ // at-rule silently and keeps the rest of the sheet, so this passes on its
30
+ // own merits — it is here to catch a regression, not to cover the guard.
31
+ it('parses a Tailwind v4 @theme sheet without complaining', () => {
15
32
  errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
16
33
  env = createDomEnv()
17
34
  const style = env.window.document.createElement('style')
@@ -20,6 +37,7 @@ describe('createDomEnv', () => {
20
37
  env.window.document.head.appendChild(style)
21
38
 
22
39
  expect(errorSpy).not.toHaveBeenCalled()
40
+ expect(env.window.document.styleSheets[0].cssRules).toHaveLength(1)
23
41
  })
24
42
  })
25
43
 
package/src/node/build.ts CHANGED
@@ -3,8 +3,8 @@ import type {
3
3
  ChangeViteConfigCallback,
4
4
  PreviewStoryCallback,
5
5
  } from '@poveste/shared'
6
- import type { RollupOutput } from 'rollup'
7
6
  import type {
7
+ Rolldown,
8
8
  InlineConfig as ViteInlineConfig,
9
9
  Plugin as VitePlugin,
10
10
  } from 'vite'
@@ -197,7 +197,7 @@ export async function build(ctx: Context) {
197
197
  }
198
198
 
199
199
  const results = await viteBuild(buildViteConfig)
200
- const result = Array.isArray(results) ? results[0] : results as RollupOutput
200
+ const result = Array.isArray(results) ? results[0] : results as Rolldown.RolldownOutput
201
201
 
202
202
  function findEntryCss(entryName: string) {
203
203
  return result.output.find(
@@ -303,7 +303,3 @@ async function writeFile(fileName: string, content: string, ctx: Context) {
303
303
  function generateScriptLinks(prefetchScripts: string[], rel: string, ctx: Context) {
304
304
  return prefetchScripts.map(s => `<link rel="${rel}" href="${ctx.resolvedViteConfig.base}${s}" as="script" crossOrigin="anonymous">`).join('')
305
305
  }
306
-
307
- function generateStyleLinks(styleFiles: string[], rel: string, ctx: Context) {
308
- return styleFiles.map(s => `<link rel="${rel}" href="${ctx.resolvedViteConfig.base}${s}" as="style">`).join('')
309
- }
@@ -3,20 +3,35 @@ import { findUp } from '../util/find-up.js'
3
3
  import { getInjectedImport } from '../util/vendors.js'
4
4
 
5
5
  export interface TailwindTokensOptions {
6
- configFile?: string
6
+ /**
7
+ * Path to the CSS entrypoint that imports Tailwind and declares `@theme`.
8
+ * Tailwind v4 is CSS-first, so the design tokens are read from CSS rather
9
+ * than from a JS config. Auto-detected when omitted.
10
+ */
11
+ cssFile?: string
7
12
  }
8
13
 
14
+ const CSS_ENTRY_CANDIDATES = [
15
+ 'src/style.css',
16
+ 'src/styles.css',
17
+ 'src/main.css',
18
+ 'src/index.css',
19
+ 'src/app.css',
20
+ 'src/assets/main.css',
21
+ 'src/assets/style.css',
22
+ 'src/assets/css/main.css',
23
+ // Nuxt conventions
24
+ 'app/assets/css/main.css',
25
+ 'assets/css/main.css',
26
+ // Next.js conventions
27
+ 'styles/globals.css',
28
+ 'app/globals.css',
29
+ 'style.css',
30
+ 'styles.css',
31
+ ]
32
+
9
33
  export function tailwindTokens(options: TailwindTokensOptions = {}): Plugin {
10
- const tailwindConfigFile = options.configFile ?? findUp(process.cwd(), [
11
- 'tailwind.config.js',
12
- 'tailwind.config.cjs',
13
- 'tailwind.config.mjs',
14
- 'tailwind.config.ts',
15
- 'tailwind-config.js',
16
- 'tailwind-config.cjs',
17
- 'tailwind-config.mjs',
18
- 'tailwind-config.ts',
19
- ])
34
+ const tailwindCssFile = options.cssFile ?? findUp(process.cwd(), CSS_ENTRY_CANDIDATES)
20
35
 
21
36
  async function generate(api: PluginApiBase) {
22
37
  try {
@@ -24,11 +39,9 @@ export function tailwindTokens(options: TailwindTokensOptions = {}): Plugin {
24
39
  await api.fs.emptyDir(api.pluginTempDir)
25
40
  api.moduleLoader.clearCache()
26
41
  await api.fs.writeFile(api.path.resolve(api.pluginTempDir, 'style.css'), css)
27
- const tailwindConfig = await api.moduleLoader.loadModule(tailwindConfigFile)
28
- const { default: resolveConfig } = await import('tailwindcss/resolveConfig.js')
29
- const resolvedTailwindConfig = resolveConfig(tailwindConfig.default ?? tailwindConfig)
42
+ const theme = await loadTailwindTheme(api, tailwindCssFile)
30
43
  const storyFile = api.path.resolve(api.pluginTempDir, 'Tailwind.story.js')
31
- await api.fs.writeFile(storyFile, storyTemplate(resolvedTailwindConfig))
44
+ await api.fs.writeFile(storyFile, storyTemplate({ theme }))
32
45
  api.addStoryFile(storyFile)
33
46
  }
34
47
  catch (e) {
@@ -40,7 +53,7 @@ export function tailwindTokens(options: TailwindTokensOptions = {}): Plugin {
40
53
  name: 'builtin:tailwind-tokens',
41
54
 
42
55
  config(config) {
43
- if (tailwindConfigFile) {
56
+ if (tailwindCssFile) {
44
57
  // Add 'design-system' group
45
58
  if (!config.tree) {
46
59
  config.tree = {}
@@ -65,8 +78,8 @@ export function tailwindTokens(options: TailwindTokensOptions = {}): Plugin {
65
78
  },
66
79
 
67
80
  onDev(api, onCleanup) {
68
- if (tailwindConfigFile) {
69
- const watcher = api.watcher.watch(tailwindConfigFile)
81
+ if (tailwindCssFile) {
82
+ const watcher = api.watcher.watch(tailwindCssFile)
70
83
  .on('change', () => generate(api))
71
84
  .on('add', () => generate(api))
72
85
  onCleanup(() => {
@@ -76,13 +89,156 @@ export function tailwindTokens(options: TailwindTokensOptions = {}): Plugin {
76
89
  },
77
90
 
78
91
  async onBuild(api) {
79
- if (tailwindConfigFile) {
92
+ if (tailwindCssFile) {
80
93
  await generate(api)
81
94
  }
82
95
  },
83
96
  }
84
97
  }
85
98
 
99
+ /**
100
+ * Tailwind v4 keeps its theme as flat CSS custom properties rather than a
101
+ * resolved JS config (`resolveConfig` is gone). Load the design system from the
102
+ * project's CSS entrypoint and reshape its variables into the grouped structure
103
+ * the story template renders.
104
+ */
105
+ /**
106
+ * Resolve a stylesheet referenced from CSS. A bare package specifier resolves
107
+ * to the JS entry, so its `style` field is used instead — that is how
108
+ * `@import 'tailwindcss'` reaches `tailwindcss/index.css`.
109
+ */
110
+ async function resolveStylesheet(api: PluginApiBase, id: string, importBase: string) {
111
+ const { createRequire } = await import('node:module')
112
+ const require = createRequire(`${importBase}/`)
113
+
114
+ if (id.startsWith('.')) {
115
+ return api.path.resolve(importBase, id)
116
+ }
117
+ if (id.endsWith('.css')) {
118
+ return require.resolve(id)
119
+ }
120
+ const packageJsonPath = require.resolve(`${id}/package.json`)
121
+ const packageJson = JSON.parse(await api.fs.readFile(packageJsonPath, 'utf8'))
122
+ return api.path.resolve(api.path.dirname(packageJsonPath), packageJson.style ?? 'index.css')
123
+ }
124
+
125
+ async function loadTailwindTheme(api: PluginApiBase, cssFile: string) {
126
+ const { __unstable__loadDesignSystem } = await import('tailwindcss')
127
+
128
+ const base = api.path.dirname(cssFile)
129
+ const designSystem = await __unstable__loadDesignSystem(
130
+ await api.fs.readFile(cssFile, 'utf8'),
131
+ {
132
+ base,
133
+ loadStylesheet: async (id: string, importBase: string) => {
134
+ const resolved = await resolveStylesheet(api, id, importBase)
135
+ return {
136
+ path: resolved,
137
+ base: api.path.dirname(resolved),
138
+ content: await api.fs.readFile(resolved, 'utf8'),
139
+ }
140
+ },
141
+ },
142
+ )
143
+
144
+ const vars = new Map<string, string>(
145
+ [...designSystem.theme.entries()].map(([key, entry]: [string, any]) => [key, entry.value]),
146
+ )
147
+
148
+ /** All values in a `--namespace-*` group, keyed by their remainder. */
149
+ function group(namespace: string, exclude?: RegExp) {
150
+ const out: Record<string, string> = {}
151
+ const prefix = `${namespace}-`
152
+ for (const [key, value] of vars) {
153
+ if (!key.startsWith(prefix)) continue
154
+ if (exclude?.test(key)) continue
155
+ out[key.slice(prefix.length)] = value
156
+ }
157
+ return out
158
+ }
159
+
160
+ /** Colours are rendered as shade sets: `red-500` -> `{ red: { 500: … } }`. */
161
+ function colors() {
162
+ const out: Record<string, any> = {}
163
+ for (const [key, value] of Object.entries(group('--color'))) {
164
+ const match = key.match(/^(.*)-(\d+)$/)
165
+ if (match) {
166
+ out[match[1]] ??= {}
167
+ out[match[1]][match[2]] = value
168
+ }
169
+ else {
170
+ out[key] = value
171
+ }
172
+ }
173
+ return out
174
+ }
175
+
176
+ // v4 generates the spacing scale on demand from a single `--spacing` base,
177
+ // so the conventional steps are reconstructed here for display.
178
+ const SPACING_STEPS = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96]
179
+ function spacing() {
180
+ const unit = vars.get('--spacing') ?? '0.25rem'
181
+ // Multiply out rather than emitting `calc(0.25rem * 4)`, so the story shows
182
+ // the resolved length the way the v3 resolved config did.
183
+ const [, rawAmount, cssUnit] = unit.trim().match(/^([\d.]+)([a-z%]*)$/i) ?? []
184
+ const amount = Number(rawAmount)
185
+ const out: Record<string, string> = {}
186
+ for (const step of SPACING_STEPS) {
187
+ if (step === 0) {
188
+ out['0'] = '0px'
189
+ }
190
+ else {
191
+ out[String(step)] = Number.isFinite(amount)
192
+ ? `${Number((amount * step).toFixed(4))}${cssUnit}`
193
+ : `calc(${unit} * ${step})`
194
+ }
195
+ }
196
+ return out
197
+ }
198
+
199
+ // Fractions and keywords have no theme variables in v4 — they are static
200
+ // utilities — but they belong in the sizing scales the story lists.
201
+ const SIZING_EXTRAS: Record<string, string> = {
202
+ 'auto': 'auto',
203
+ 'full': '100%',
204
+ 'min': 'min-content',
205
+ 'max': 'max-content',
206
+ 'fit': 'fit-content',
207
+ 'px': '1px',
208
+ '1/2': '50%',
209
+ '1/3': '33.333333%',
210
+ '2/3': '66.666667%',
211
+ '1/4': '25%',
212
+ '3/4': '75%',
213
+ '1/5': '20%',
214
+ '2/5': '40%',
215
+ '3/5': '60%',
216
+ '4/5': '80%',
217
+ }
218
+
219
+ const palette = colors()
220
+ const scale = spacing()
221
+
222
+ return {
223
+ backgroundColor: palette,
224
+ textColor: palette,
225
+ borderColor: palette,
226
+ padding: scale,
227
+ margin: scale,
228
+ width: { ...scale, ...SIZING_EXTRAS, screen: '100vw' },
229
+ height: { ...scale, ...SIZING_EXTRAS, screen: '100vh' },
230
+ fontSize: group('--text', /--line-height$|--letter-spacing$|--font-weight$/),
231
+ fontWeight: group('--font-weight'),
232
+ fontFamily: group('--font', /^--font-weight-/),
233
+ letterSpacing: group('--tracking'),
234
+ lineHeight: group('--leading'),
235
+ dropShadow: group('--drop-shadow'),
236
+ borderRadius: group('--radius'),
237
+ // v4 has no border-width namespace; these are the built-in static utilities.
238
+ borderWidth: { DEFAULT: '1px', 0: '0px', 2: '2px', 4: '4px', 8: '8px' },
239
+ }
240
+ }
241
+
86
242
  function storyTemplate(tailwindConfig: any) {
87
243
  return `
88
244
  import 'poveste-style'
@@ -134,7 +290,7 @@ export default {
134
290
  onMount: (api) => mountApp(api, () => Object.entries(config.theme.backgroundColor).map(([key, shades]) => h(HstColorShades, {
135
291
  key,
136
292
  shades: typeof shades === 'object' ? shades : { DEFAULT: shades },
137
- getName: shade => (config.prefix ?? '') + (shade === 'DEFAULT' ? \`bg-\${key}\` : \`bg-\${key}-\${shade}\`),
293
+ getName: shade => '' + (shade === 'DEFAULT' ? \`bg-\${key}\` : \`bg-\${key}-\${shade}\`),
138
294
  search: search.value,
139
295
  }, ({ color}) => h('div', {
140
296
  class: '__pvt-shade',
@@ -157,7 +313,7 @@ export default {
157
313
  onMount: (api) => mountApp(api, () => Object.entries(config.theme.textColor).map(([key, shades]) => h(HstColorShades, {
158
314
  key,
159
315
  shades: typeof shades === 'object' ? shades : { DEFAULT: shades },
160
- getName: shade => (config.prefix ?? '') + (shade === 'DEFAULT' ? \`text-\${key}\` : \`text-\${key}-\${shade}\`),
316
+ getName: shade => '' + (shade === 'DEFAULT' ? \`text-\${key}\` : \`text-\${key}-\${shade}\`),
161
317
  search: search.value,
162
318
  }, ({ color}) => h('div', {
163
319
  class: '__pvt-shade __pvt-text',
@@ -180,7 +336,7 @@ export default {
180
336
  onMount: (api) => mountApp(api, () => Object.entries(config.theme.borderColor).map(([key, shades]) => h(HstColorShades, {
181
337
  key,
182
338
  shades: typeof shades === 'object' ? shades : { DEFAULT: shades },
183
- getName: shade => (config.prefix ?? '') + (shade === 'DEFAULT' ? \`border-\${key}\` : \`border-\${key}-\${shade}\`),
339
+ getName: shade => '' + (shade === 'DEFAULT' ? \`border-\${key}\` : \`border-\${key}-\${shade}\`),
184
340
  search: search.value,
185
341
  }, ({ color}) => h('div', {
186
342
  class: '__pvt-shade __pvt-border',
@@ -202,7 +358,7 @@ export default {
202
358
  icon: 'carbon:area',
203
359
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
204
360
  tokens: config.theme.padding,
205
- getName: key => \`\${config.prefix ?? ''}p-\${key}\`,
361
+ getName: key => \`p-\${key}\`,
206
362
  }, ({ token }) => h('div', {
207
363
  class: '__pvt-padding',
208
364
  style: {
@@ -220,7 +376,7 @@ export default {
220
376
  icon: 'carbon:area',
221
377
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
222
378
  tokens: config.theme.margin,
223
- getName: key => \`\${config.prefix ?? ''}m-\${key}\`,
379
+ getName: key => \`m-\${key}\`,
224
380
  }, ({ token }) => h('div', {
225
381
  class: '__pvt-margin',
226
382
  }, [
@@ -238,7 +394,7 @@ export default {
238
394
  icon: 'carbon:text-font',
239
395
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
240
396
  tokens: config.theme.fontSize,
241
- getName: key => \`\${config.prefix ?? ''}text-\${key}\`,
397
+ getName: key => \`text-\${key}\`,
242
398
  }, ({ token }) => h('div', {
243
399
  class: '__pvt-truncate',
244
400
  style: {
@@ -261,7 +417,7 @@ export default {
261
417
  icon: 'carbon:text-font',
262
418
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
263
419
  tokens: config.theme.fontWeight,
264
- getName: key => \`\${config.prefix ?? ''}font-\${key}\`,
420
+ getName: key => \`font-\${key}\`,
265
421
  }, ({ token }) => h('div', {
266
422
  class: '__pvt-truncate',
267
423
  style: {
@@ -290,7 +446,7 @@ export default {
290
446
  icon: 'carbon:text-font',
291
447
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
292
448
  tokens: config.theme.fontFamily,
293
- getName: key => \`\${config.prefix ?? ''}font-\${key}\`,
449
+ getName: key => \`font-\${key}\`,
294
450
  }, ({ token }) => h('div', {
295
451
  class: '__pvt-truncate',
296
452
  style: {
@@ -319,7 +475,7 @@ export default {
319
475
  icon: 'carbon:text-font',
320
476
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
321
477
  tokens: config.theme.letterSpacing,
322
- getName: key => \`\${config.prefix ?? ''}tracking-\${key}\`,
478
+ getName: key => \`tracking-\${key}\`,
323
479
  }, ({ token }) => h('div', {
324
480
  class: '__pvt-truncate',
325
481
  style: {
@@ -348,7 +504,7 @@ export default {
348
504
  icon: 'carbon:text-font',
349
505
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
350
506
  tokens: config.theme.lineHeight,
351
- getName: key => \`\${config.prefix ?? ''}leading-\${key}\`,
507
+ getName: key => \`leading-\${key}\`,
352
508
  }, ({ token }) => h('div', {
353
509
  style: {
354
510
  lineHeight: token.value,
@@ -370,7 +526,7 @@ export default {
370
526
  icon: 'carbon:shape-except',
371
527
  onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
372
528
  tokens: config.theme.dropShadow,
373
- getName: key => (config.prefix ?? '') + (key === 'DEFAULT' ? 'drop-shadow' : \`drop-shadow-\${key}\`),
529
+ getName: key => '' + (key === 'DEFAULT' ? 'drop-shadow' : \`drop-shadow-\${key}\`),
374
530
  colSize: 180,
375
531
  }, ({ token }) => h('div', {
376
532
  class: '__pvt-drop-shadow',
@@ -385,7 +541,7 @@ export default {
385
541
  icon: 'carbon:condition-wait-point',
386
542
  onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
387
543
  tokens: config.theme.borderRadius,
388
- getName: key => (config.prefix ?? '') + (key === 'DEFAULT' ? 'rounded' : \`rounded-\${key}\`),
544
+ getName: key => '' + (key === 'DEFAULT' ? 'rounded' : \`rounded-\${key}\`),
389
545
  colSize: 180,
390
546
  }, ({ token }) => h('div', {
391
547
  class: '__pvt-border-radius',
@@ -400,7 +556,7 @@ export default {
400
556
  icon: 'carbon:checkbox',
401
557
  onMount: (api) => mountApp(api, () => h(HstTokenGrid, {
402
558
  tokens: config.theme.borderWidth,
403
- getName: key => (config.prefix ?? '') + (key === 'DEFAULT' ? 'border' : \`border-\${key}\`),
559
+ getName: key => '' + (key === 'DEFAULT' ? 'border' : \`border-\${key}\`),
404
560
  colSize: 180,
405
561
  }, ({ token }) => h('div', {
406
562
  class: '__pvt-border-width',
@@ -415,7 +571,7 @@ export default {
415
571
  icon: 'carbon:pan-horizontal',
416
572
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
417
573
  tokens: config.theme.width,
418
- getName: key => (config.prefix ?? '') + (key === 'DEFAULT' ? 'w' : \`w-\${key}\`),
574
+ getName: key => '' + (key === 'DEFAULT' ? 'w' : \`w-\${key}\`),
419
575
  }, ({ token }) => h('div', {
420
576
  class: '__pvt-width',
421
577
  }, [
@@ -433,7 +589,7 @@ export default {
433
589
  icon: 'carbon:pan-vertical',
434
590
  onMount: (api) => mountApp(api, () => h(HstTokenList, {
435
591
  tokens: config.theme.height,
436
- getName: key => (config.prefix ?? '') + (key === 'DEFAULT' ? 'h' : \`h-\${key}\`),
592
+ getName: key => '' + (key === 'DEFAULT' ? 'h' : \`h-\${key}\`),
437
593
  }, ({ token }) => h('div', {
438
594
  class: '__pvt-height',
439
595
  style: {
@@ -127,9 +127,7 @@ export function useCollectStories(options: UseCollectStoriesOptions, ctx: Contex
127
127
  // Default props
128
128
  if (ctx.config.defaultStoryProps) {
129
129
  for (const key in ctx.config.defaultStoryProps) {
130
- if (finalData[key] == null) {
131
- finalData[key] = ctx.config.defaultStoryProps[key]
132
- }
130
+ finalData[key] ??= ctx.config.defaultStoryProps[key]
133
131
  }
134
132
  }
135
133
 
@@ -265,6 +265,9 @@ export async function resolveConfig(cwd: string = process.cwd(), mode: ConfigMod
265
265
  result = await loadConfigFile(resolvedConfigFile)
266
266
  }
267
267
  const viteConfig = await resolveViteConfig({}, 'serve')
268
+ // The `histoire` key is our own deprecation, and reading it is the whole
269
+ // point — it is what makes poveste drop-in for an existing histoire config.
270
+ // eslint-disable-next-line ts/no-deprecated
268
271
  const vitePovesteConfig = (viteConfig.poveste ?? viteConfig.histoire ?? {}) as PovesteConfig
269
272
 
270
273
  const preUserConfig = mergeConfig(result, vitePovesteConfig)
@@ -8,12 +8,16 @@ import { populateGlobal } from './util.js'
8
8
  const JSDOM_ERROR_CSS_PARSING = 'css-parsing'
9
9
  const JSDOM_ERROR_UNHANDLED_EXCEPTION = 'unhandled-exception'
10
10
 
11
- // Directives JSDOM's CSS parser doesn't know — Tailwind v4's `@theme` is the
12
- // common one — make it emit a jsdomError per stylesheet, so collecting stories
13
- // from a Tailwind app prints "Could not parse CSS stylesheet" over and over.
14
- // The parser recovers on its own and keeps the valid rules, so the message is
15
- // pure noise: drop css-parsing errors, mirror jsdom's default forwardTo for
16
- // the rest.
11
+ // CSS jsdom's parser can't make sense of makes it emit a jsdomError per
12
+ // stylesheet, so collecting stories from an app with such a stylesheet prints
13
+ // "Could not parse CSS stylesheet" over and over. The parser recovers on its
14
+ // own and keeps the valid rules, so the message is pure noise: drop
15
+ // css-parsing errors, mirror jsdom's default forwardTo for the rest.
16
+ //
17
+ // jsdom 28 moved to css-tree, which recovers browser-style and stays quiet for
18
+ // most of what jsdom 27 complained about — Tailwind v4's `@theme`, unknown
19
+ // at-rules, unterminated strings. It still reports some inputs (an
20
+ // unparseable selector, for one), so this stays.
17
21
  function createVirtualConsole(): VirtualConsole | undefined {
18
22
  if (!console || !globalThis.console) return undefined
19
23
  const virtualConsole = new VirtualConsole().forwardTo(globalThis.console, { jsdomErrors: 'none' })
package/src/node/index.ts CHANGED
@@ -2,11 +2,3 @@ export { defaultColors } from './colors.js'
2
2
  export * from './config.js'
3
3
  export * from './plugin.js'
4
4
  export * from '@poveste/shared'
5
-
6
- declare module 'rollup' {
7
- interface PluginContextMeta {
8
- poveste: {
9
- isCollecting: boolean
10
- }
11
- }
12
- }
@@ -1,4 +1,5 @@
1
1
  import type { ServerMarkdownFile } from '@poveste/shared'
2
+ import type { Highlighter } from 'shiki'
2
3
  import type { Plugin as VitePlugin } from 'vite'
3
4
  import type { Context } from './context.js'
4
5
  import { kebabCase } from 'change-case'
@@ -28,14 +29,35 @@ function notifyMarkdownListChange() {
28
29
  }
29
30
  }
30
31
 
31
- export async function createMarkdownRenderer(ctx: Context) {
32
- const highlighter = await createHighlighter({
32
+ /**
33
+ * Shared between the markdown Vite plugin and the markdown file watcher, which
34
+ * both build a renderer — a `poveste build` reaches this three times. The
35
+ * themes and langs are constant, and each instance eagerly loads every bundled
36
+ * grammar, so the duplicates are pure waste.
37
+ */
38
+ let highlighterPromise: Promise<Highlighter> | undefined
39
+
40
+ function getHighlighter() {
41
+ highlighterPromise ??= createHighlighter({
33
42
  themes: ['github-dark'],
34
43
  langs: Object.keys(bundledLanguages), // not ideal but markdown-it does not provide async highlight
35
44
  })
36
45
 
46
+ return highlighterPromise
47
+ }
48
+
49
+ export async function createMarkdownRenderer(ctx: Context) {
50
+ const highlighter = await getHighlighter()
51
+
37
52
  const md = new MarkdownIt({
38
- highlight: (code, lang) => `<div class="ptw-relative ptw-not-prose __poveste-code __histoire-code"><div class="ptw-absolute ptw-top-0 ptw-right-0 ptw-text-xs ptw-text-white/40">${lang}</div>${highlighter.codeToHtml(code, { theme: 'github-dark', lang })}</div>`,
53
+ /*
54
+ * Unprefixed utilities: the `ptw-` prefix went away with Tailwind v4, and
55
+ * `not-prose` in particular is matched by the typography plugin's own
56
+ * `[class~="not-prose"]` selector, so a prefixed spelling silently opts the
57
+ * block back *into* prose. These classes are generated because main.pcss
58
+ * `@source`s this file — v4 auto-detection only scans the app package.
59
+ */
60
+ highlight: (code, lang) => `<div class="relative not-prose __poveste-code __histoire-code"><div class="absolute top-0 right-0 text-xs text-white/40">${lang}</div>${highlighter.codeToHtml(code, { theme: 'github-dark', lang })}</div>`,
39
61
  linkify: true,
40
62
  html: true,
41
63
  breaks: false,
@@ -76,7 +76,7 @@ export async function watchStories(newContext: Context) {
76
76
  watcher
77
77
  .on('add', (file) => {
78
78
  const storyFile = addStory(file)
79
- setTimeout(() => notifyStoryChange(storyFile), 100) // Delay in case file renaming fired Add event before Unlink event
79
+ setTimeout(notifyStoryChange, 100, storyFile) // Delay in case file renaming fired Add event before Unlink event
80
80
  })
81
81
  .on('unlink', (file) => {
82
82
  removeStory(file)