poveste 0.1.2 → 0.2.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 (92) hide show
  1. package/dist/node/bin.js +3 -1
  2. package/dist/node/build.d.ts.map +1 -1
  3. package/dist/node/build.js +23 -5
  4. package/dist/node/builtin-plugins/pinceau-tokens.js +35 -35
  5. package/dist/node/builtin-plugins/tailwind-tokens.js +38 -38
  6. package/dist/node/builtin-plugins/vanilla-support/RenderStory.d.ts.map +1 -1
  7. package/dist/node/builtin-plugins/vanilla-support/RenderStory.js +7 -4
  8. package/dist/node/builtin-plugins/vanilla-support/types.d.ts +6 -5
  9. package/dist/node/builtin-plugins/vanilla-support/types.d.ts.map +1 -1
  10. package/dist/node/commands/preview.d.ts +2 -0
  11. package/dist/node/commands/preview.d.ts.map +1 -1
  12. package/dist/node/commands/preview.js +6 -3
  13. package/dist/node/config.d.ts.map +1 -1
  14. package/dist/node/config.js +1 -0
  15. package/dist/node/context.js +2 -2
  16. package/dist/node/dom/env.d.ts.map +1 -1
  17. package/dist/node/dom/env.js +26 -1
  18. package/dist/node/markdown.js +1 -1
  19. package/dist/node/preview.d.ts +7 -1
  20. package/dist/node/preview.d.ts.map +1 -1
  21. package/dist/node/preview.js +22 -39
  22. package/dist/node/style-isolation/entry-css-merger.d.ts +9 -0
  23. package/dist/node/style-isolation/entry-css-merger.d.ts.map +1 -0
  24. package/dist/node/style-isolation/entry-css-merger.js +128 -0
  25. package/dist/node/style-isolation/global-styles.d.ts +8 -0
  26. package/dist/node/style-isolation/global-styles.d.ts.map +1 -0
  27. package/dist/node/style-isolation/global-styles.js +31 -0
  28. package/dist/node/style-isolation/index.d.ts +6 -0
  29. package/dist/node/style-isolation/index.d.ts.map +1 -0
  30. package/dist/node/style-isolation/index.js +5 -0
  31. package/dist/node/style-isolation/scope-chrome.d.ts +6 -0
  32. package/dist/node/style-isolation/scope-chrome.d.ts.map +1 -0
  33. package/dist/node/style-isolation/scope-chrome.js +37 -0
  34. package/dist/node/style-isolation/selectors.d.ts +7 -0
  35. package/dist/node/style-isolation/selectors.d.ts.map +1 -0
  36. package/dist/node/style-isolation/selectors.js +15 -0
  37. package/dist/node/style-isolation/transforms.d.ts +11 -0
  38. package/dist/node/style-isolation/transforms.d.ts.map +1 -0
  39. package/dist/node/style-isolation/transforms.js +75 -0
  40. package/dist/node/style-isolation/vite-plugin.d.ts +15 -0
  41. package/dist/node/style-isolation/vite-plugin.d.ts.map +1 -0
  42. package/dist/node/style-isolation/vite-plugin.js +62 -0
  43. package/dist/node/util/head.d.ts +3 -0
  44. package/dist/node/util/head.d.ts.map +1 -0
  45. package/dist/node/util/head.js +10 -0
  46. package/dist/node/virtual/markdown.js +1 -1
  47. package/dist/node/virtual/noop.d.ts +2 -1
  48. package/dist/node/virtual/noop.d.ts.map +1 -1
  49. package/dist/node/virtual/noop.js +7 -1
  50. package/dist/node/virtual/resolved-generated-global-setup.js +3 -3
  51. package/dist/node/virtual/resolved-markdown-files.js +2 -2
  52. package/dist/node/virtual/util.d.ts +15 -0
  53. package/dist/node/virtual/util.d.ts.map +1 -1
  54. package/dist/node/virtual/util.js +18 -0
  55. package/dist/node/virtual/vite-plugin.js +1 -1
  56. package/dist/node/vite.d.ts +1 -1
  57. package/dist/node/vite.d.ts.map +1 -1
  58. package/dist/node/vite.js +51 -9
  59. package/package.json +7 -5
  60. package/src/node/__tests__/dom-env.spec.ts +38 -0
  61. package/src/node/__tests__/head.spec.ts +32 -0
  62. package/src/node/__tests__/virtual-setup.spec.ts +51 -0
  63. package/src/node/bin.ts +3 -1
  64. package/src/node/build.ts +31 -5
  65. package/src/node/builtin-plugins/pinceau-tokens.ts +35 -35
  66. package/src/node/builtin-plugins/tailwind-tokens.ts +38 -38
  67. package/src/node/builtin-plugins/vanilla-support/RenderStory.ts +8 -5
  68. package/src/node/builtin-plugins/vanilla-support/types.ts +3 -1
  69. package/src/node/commands/preview.ts +8 -3
  70. package/src/node/config.ts +1 -0
  71. package/src/node/context.ts +2 -2
  72. package/src/node/dom/env.ts +26 -1
  73. package/src/node/markdown.ts +1 -1
  74. package/src/node/preview.ts +30 -47
  75. package/src/node/style-isolation/__tests__/entry-css-merger.spec.ts +126 -0
  76. package/src/node/style-isolation/__tests__/transforms.spec.ts +92 -0
  77. package/src/node/style-isolation/__tests__/vite-plugin.spec.ts +39 -0
  78. package/src/node/style-isolation/entry-css-merger.ts +133 -0
  79. package/src/node/style-isolation/global-styles.ts +37 -0
  80. package/src/node/style-isolation/index.ts +5 -0
  81. package/src/node/style-isolation/scope-chrome.ts +42 -0
  82. package/src/node/style-isolation/selectors.ts +16 -0
  83. package/src/node/style-isolation/transforms.ts +90 -0
  84. package/src/node/style-isolation/vite-plugin.ts +75 -0
  85. package/src/node/util/head.ts +12 -0
  86. package/src/node/virtual/markdown.ts +1 -1
  87. package/src/node/virtual/noop.ts +9 -1
  88. package/src/node/virtual/resolved-generated-global-setup.ts +3 -3
  89. package/src/node/virtual/resolved-markdown-files.ts +2 -2
  90. package/src/node/virtual/util.ts +22 -0
  91. package/src/node/virtual/vite-plugin.ts +1 -1
  92. package/src/node/vite.ts +57 -9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../../../src/node/util/head.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAQtG"}
@@ -0,0 +1,10 @@
1
+ import { createHead, transformHtmlTemplate } from 'unhead/server';
2
+ export async function applyHeadTransform(html, head) {
3
+ if (!head) {
4
+ return html;
5
+ }
6
+ // disableDefaults: don't inject unhead's `lang="en"` / viewport tags — Poveste's
7
+ // own HTML template already provides viewport, and we shouldn't override it.
8
+ const instance = createHead({ init: [head], disableDefaults: true });
9
+ return await transformHtmlTemplate(instance, html);
10
+ }
@@ -12,7 +12,7 @@ export const relativePath = ${JSON.stringify(relativePath)}
12
12
  if (import.meta.hot) {
13
13
  import.meta.hot.accept(newModule => {
14
14
  if (newModule) {
15
- window.__hst_md_hmr(newModule)
15
+ window.__pvt_md_hmr(newModule)
16
16
  }
17
17
  })
18
18
  }`;
@@ -1,2 +1,3 @@
1
- export declare const noop: () => string;
1
+ import type { Context } from '../context.js';
2
+ export declare function noop(ctx: Context): string;
2
3
  //# sourceMappingURL=noop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"noop.d.ts","sourceRoot":"","sources":["../../../src/node/virtual/noop.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,cAAkC,CAAA"}
1
+ {"version":3,"file":"noop.d.ts","sourceRoot":"","sources":["../../../src/node/virtual/noop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAG5C,wBAAgB,IAAI,CAAC,GAAG,EAAE,OAAO,UAKhC"}
@@ -1 +1,7 @@
1
- export const noop = () => `export default () => {}`;
1
+ import { declareEmptySetupFns } from './util.js';
2
+ export function noop(ctx) {
3
+ return [
4
+ `export default () => {}`,
5
+ declareEmptySetupFns(ctx),
6
+ ].join('\n');
7
+ }
@@ -1,5 +1,5 @@
1
1
  import { GENERATED_SETUP_CODE } from './index.js';
2
- import { ID_SEPARATOR } from './util.js';
2
+ import { declareEmptySetupFns, getSetupFnNames, ID_SEPARATOR } from './util.js';
3
3
  export function resolvedGeneratedGlobalSetup(ctx) {
4
4
  if (ctx.config.setupCode) {
5
5
  return [
@@ -8,7 +8,7 @@ export function resolvedGeneratedGlobalSetup(ctx) {
8
8
  // List
9
9
  `const setupList = [${ctx.config.setupCode.map((c, index) => `setup_${index}`).join(', ')}]`,
10
10
  // Setups
11
- ...ctx.supportPlugins.map(p => p.setupFn).flat().map(fnName => `export async function ${fnName} (payload) {
11
+ ...getSetupFnNames(ctx).map(fnName => `export async function ${fnName} (payload) {
12
12
  for (const setup of setupList) {
13
13
  if (setup?.${fnName}) {
14
14
  await setup.${fnName}(payload)
@@ -18,6 +18,6 @@ export function resolvedGeneratedGlobalSetup(ctx) {
18
18
  ].join('\n');
19
19
  }
20
20
  else {
21
- return '';
21
+ return declareEmptySetupFns(ctx);
22
22
  }
23
23
  }
@@ -4,8 +4,8 @@ export function resolvedMarkdownFiles(ctx) {
4
4
  return `import { reactive } from ${process.env.POVESTE_DEV ? `'vue'` : getInjectedImport('@poveste/vendors/vue')}
5
5
  export const markdownFiles = reactive({${filesJs}})
6
6
  if (import.meta.hot) {
7
- if (!window.__hst_md_hmr) {
8
- window.__hst_md_hmr = (newModule) => {
7
+ if (!window.__pvt_md_hmr) {
8
+ window.__pvt_md_hmr = (newModule) => {
9
9
  markdownFiles[newModule.relativePath] = () => newModule
10
10
  }
11
11
  }
@@ -1,3 +1,18 @@
1
+ import type { Context } from '../context.js';
1
2
  export declare const ID_SEPARATOR = "__-__";
3
+ /**
4
+ * Names of every setup hook contributed by the enabled support plugins, e.g.
5
+ * `setupVanilla`, `setupVue3`, `setupSvelte4`.
6
+ */
7
+ export declare function getSetupFnNames(ctx: Context): string[];
8
+ /**
9
+ * Declares the setup hooks as named exports set to `undefined`.
10
+ *
11
+ * Consumers read them off a namespace import (`generatedSetup.setupVue3`) behind
12
+ * a `typeof === 'function'` guard. Without the declaration Rollup can't resolve
13
+ * the member access statically and warns `"setupVue3" is not exported by ...`
14
+ * in every consumer build.
15
+ */
16
+ export declare function declareEmptySetupFns(ctx: Context): string;
2
17
  export declare const PLUGINS_HAVE_DEV: string[];
3
18
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../src/node/virtual/util.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,UAAU,CAAA;AAEnC,eAAO,MAAM,gBAAgB,UAE5B,CAAA"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../src/node/virtual/util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAE5C,eAAO,MAAM,YAAY,UAAU,CAAA;AAEnC;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,YAE3C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,UAEhD;AAED,eAAO,MAAM,gBAAgB,UAE5B,CAAA"}
@@ -1,4 +1,22 @@
1
1
  export const ID_SEPARATOR = '__-__';
2
+ /**
3
+ * Names of every setup hook contributed by the enabled support plugins, e.g.
4
+ * `setupVanilla`, `setupVue3`, `setupSvelte4`.
5
+ */
6
+ export function getSetupFnNames(ctx) {
7
+ return [...new Set(ctx.supportPlugins.flatMap(p => p.setupFn))];
8
+ }
9
+ /**
10
+ * Declares the setup hooks as named exports set to `undefined`.
11
+ *
12
+ * Consumers read them off a namespace import (`generatedSetup.setupVue3`) behind
13
+ * a `typeof === 'function'` guard. Without the declaration Rollup can't resolve
14
+ * the member access statically and warns `"setupVue3" is not exported by ...`
15
+ * in every consumer build.
16
+ */
17
+ export function declareEmptySetupFns(ctx) {
18
+ return getSetupFnNames(ctx).map(fnName => `export const ${fnName} = undefined`).join('\n');
19
+ }
2
20
  export const PLUGINS_HAVE_DEV = [
3
21
  '@poveste/plugin-vue',
4
22
  ];
@@ -80,7 +80,7 @@ export function createVirtualFilesPlugin(ctx, isServer) {
80
80
  return VirtualFiles.resolvedStories(ctx);
81
81
  }
82
82
  if (id === VirtualFiles.NOOP_ID) {
83
- return VirtualFiles.noop();
83
+ return VirtualFiles.noop(ctx);
84
84
  }
85
85
  if (id === VirtualFiles.RESOLVED_CONFIG_ID) {
86
86
  return VirtualFiles.resolvedConfig(ctx);
@@ -1,6 +1,6 @@
1
1
  import type { InlineConfig } from 'vite';
2
2
  import type { Context } from './context.js';
3
- export declare function mergeHistoireViteConfig(viteConfig: InlineConfig, ctx: Context): Promise<InlineConfig>;
3
+ export declare function mergePovesteViteConfig(viteConfig: InlineConfig, ctx: Context): Promise<InlineConfig>;
4
4
  export interface ViteConfigWithPlugins {
5
5
  viteConfig: InlineConfig;
6
6
  viteConfigFile: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/node/vite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,MAAM,CAAA;AACb,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAe3C,wBAAsB,uBAAuB,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,EAAE,OAAO,yBAmCnF;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,YAAY,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,wBAAsB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA4T9G"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/node/vite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,MAAM,CAAA;AACb,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAsB3C,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,EAAE,OAAO,yBAmClF;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,YAAY,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,wBAAsB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAqW9G"}
package/dist/node/vite.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { createRequire } from 'node:module';
2
3
  import { lookup as lookupMime } from 'mrmime';
3
4
  import { dirname, join, relative } from 'pathe';
@@ -5,9 +6,11 @@ import { loadConfigFromFile as loadViteConfigFromFile, mergeConfig as mergeViteC
5
6
  import { APP_PATH, TEMP_PATH } from './alias.js';
6
7
  import { createMarkdownPlugins } from './markdown.js';
7
8
  import { notifyStoryChange } from './stories.js';
9
+ import { chromeCssScopePlugin, globalStylesPlugin, userCssScopePlugin, } from './style-isolation/index.js';
10
+ import { applyHeadTransform } from './util/head.js';
8
11
  import { createVirtualFilesPlugin } from './virtual/vite-plugin.js';
9
12
  const require = createRequire(import.meta.url);
10
- export async function mergeHistoireViteConfig(viteConfig, ctx) {
13
+ export async function mergePovesteViteConfig(viteConfig, ctx) {
11
14
  if (ctx.config.vite) {
12
15
  const command = ctx.mode === 'dev' ? 'serve' : 'build';
13
16
  const overrides = typeof ctx.config.vite === 'function'
@@ -42,7 +45,7 @@ export async function mergeHistoireViteConfig(viteConfig, ctx) {
42
45
  export async function getViteConfigWithPlugins(isServer, ctx) {
43
46
  const userViteConfigFile = await loadViteConfigFromFile({ command: ctx.mode === 'dev' ? 'serve' : 'build', mode: ctx.mode });
44
47
  const userViteConfig = mergeViteConfig(userViteConfigFile?.config ?? {}, { server: { port: 6006 } });
45
- const inlineConfig = await mergeHistoireViteConfig(userViteConfig, ctx);
48
+ const inlineConfig = await mergePovesteViteConfig(userViteConfig, ctx);
46
49
  const plugins = [];
47
50
  const supportPluginAllowPaths = ctx.supportPlugins.flatMap((plugin) => {
48
51
  const paths = [ctx.root, import.meta.url];
@@ -60,14 +63,40 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
60
63
  }
61
64
  return Array.from(result);
62
65
  });
66
+ // Mirror how Vite resolves `optimizeDeps.include`: walk `node_modules` up from
67
+ // the project root. Deliberately not `require.resolve(dep, { paths: [root] })`
68
+ // — Node always folds NODE_PATH into resolution even when `paths` is given, and
69
+ // pnpm's bin shims point NODE_PATH at the hidden `.pnpm/node_modules` store, so
70
+ // every package would look reachable from every root. Vite ignores NODE_PATH.
71
+ function isResolvableFromRoot(dep) {
72
+ let dir = ctx.root;
73
+ while (true) {
74
+ if (existsSync(join(dir, 'node_modules', dep, 'package.json'))) {
75
+ return true;
76
+ }
77
+ const parent = dirname(dir);
78
+ if (parent === dir) {
79
+ return false;
80
+ }
81
+ dir = parent;
82
+ }
83
+ }
63
84
  function optimizeDeps(deps) {
64
85
  const result = [];
65
86
  for (const dep of deps) {
66
- result.push(dep);
87
+ // The bare specifier is what hoisted installs (npm, yarn, pnpm with
88
+ // `shamefully-hoist`) need — see histoire@a1014ab. Under strict pnpm it is
89
+ // unreachable from the root and Vite only logs "Failed to resolve
90
+ // dependency", so emit it just when the root can actually resolve it.
91
+ if (isResolvableFromRoot(dep)) {
92
+ result.push(dep);
93
+ }
67
94
  try {
95
+ // Absolute path, valid regardless of layout — this is what makes the
96
+ // dep optimizable under strict pnpm.
68
97
  result.push(dirname(require.resolve(`${dep}/package.json`)));
69
98
  }
70
- catch (e) {
99
+ catch {
71
100
  // Noop
72
101
  }
73
102
  }
@@ -97,11 +126,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
97
126
  `${APP_PATH}/bundle-sandbox.js`,
98
127
  ],
99
128
  include: optimizeDeps([
100
- 'flexsearch',
101
129
  'shiki',
102
- // Shiki dependencies
103
- 'vscode-oniguruma',
104
- 'vscode-textmate',
105
130
  ]),
106
131
  exclude: [
107
132
  'poveste',
@@ -141,7 +166,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
141
166
  : {},
142
167
  '__HST_COLLECT__': isServer,
143
168
  },
144
- cacheDir: isServer ? 'node_modules/.hst-vite-server' : 'node_modules/.hst-vite',
169
+ cacheDir: isServer ? 'node_modules/.pvt-vite-server' : 'node_modules/.pvt-vite',
145
170
  };
146
171
  },
147
172
  options() {
@@ -189,6 +214,7 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
189
214
  <script type="module" src="/@fs/${APP_PATH}/bundle-sandbox${process.env.POVESTE_DEV ? '-dev' : ''}.js"></script>
190
215
  </body>
191
216
  </html>`;
217
+ html = await applyHeadTransform(html, ctx.config.head);
192
218
  // Apply Vite HTML transforms. This injects the Vite HMR client, and
193
219
  // also applies HTML transforms from Vite plugins
194
220
  html = await server.transformIndexHtml(req.url, html);
@@ -213,12 +239,14 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
213
239
  <meta name="viewport" content="width=device-width,initial-scale=1">
214
240
  <meta name="description" content="">
215
241
  ${ctx.config.theme?.favicon ? `<link rel="icon" type="${lookupMime(ctx.config.theme.favicon)}" href="${server.config.base}${ctx.config.theme.favicon}"/>` : ''}
242
+ <style>html,body{margin:0;padding:0}</style>
216
243
  </head>
217
244
  <body>
218
245
  <div id="app"></div>
219
246
  <script type="module" src="/@fs/${APP_PATH}/bundle-main${process.env.POVESTE_DEV ? '-dev' : ''}.js"></script>
220
247
  </body>
221
248
  </html>`;
249
+ html = await applyHeadTransform(html, ctx.config.head);
222
250
  // Apply Vite HTML transforms. This injects the Vite HMR client, and
223
251
  // also applies HTML transforms from Vite plugins
224
252
  html = await server.transformIndexHtml(req.url, html);
@@ -232,6 +260,20 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
232
260
  },
233
261
  });
234
262
  plugins.push(createVirtualFilesPlugin(ctx, isServer));
263
+ plugins.push(globalStylesPlugin({
264
+ files: ctx.config.globalStyles ?? [],
265
+ rootDir: ctx.root,
266
+ }));
267
+ // Dev-mode isolation. User CSS is wrapped in `@scope (.__poveste-render-story)`
268
+ // so it cannot leak into chrome (popovers, tooltips, dropdowns). The build
269
+ // path emits stronger per-bundle isolation that ALSO keeps the user's
270
+ // teleporting-component styling working inside the sandbox iframe — in dev
271
+ // that case is a known trade-off, see docs/guide/css.md.
272
+ const isolate = ctx.config.isolateStyles !== false;
273
+ if (ctx.mode === 'dev' && !isServer) {
274
+ plugins.push(userCssScopePlugin({ enabled: isolate, mode: 'wrap' }));
275
+ plugins.push(chromeCssScopePlugin({ enabled: isolate }));
276
+ }
235
277
  // Replace dev flag
236
278
  const flags = {
237
279
  '_ctx.__POVESTE_DEV__': JSON.stringify(ctx.mode === 'dev'),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "description": "A fast story book powered by Vite",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -54,6 +54,7 @@
54
54
  "gray-matter": "^4.0.3",
55
55
  "jiti": "^2.4.1",
56
56
  "jsdom": "^27.4.0",
57
+ "lightningcss": "^1.32.0",
57
58
  "markdown-it": "^14.1.0",
58
59
  "markdown-it-anchor": "^9.2.0",
59
60
  "markdown-it-attrs": "^4.3.0",
@@ -65,11 +66,12 @@
65
66
  "sade": "^1.8.1",
66
67
  "shiki": "^3.0.0",
67
68
  "sirv": "^3.0.0",
69
+ "unhead": "^3.1.0",
68
70
  "vite-node": "3.2.4",
69
- "@poveste/controls": "^0.1.2",
70
- "@poveste/vendors": "^0.1.2",
71
- "@poveste/app": "^0.1.2",
72
- "@poveste/shared": "^0.1.2"
71
+ "@poveste/app": "^0.2.0",
72
+ "@poveste/controls": "^0.2.0",
73
+ "@poveste/shared": "^0.2.0",
74
+ "@poveste/vendors": "^0.2.0"
73
75
  },
74
76
  "devDependencies": {
75
77
  "@types/fs-extra": "^11.0.4",
@@ -0,0 +1,38 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { createDomEnv } from '../dom/env.js'
3
+
4
+ describe('createDomEnv', () => {
5
+ let env: ReturnType<typeof createDomEnv>
6
+ let errorSpy: ReturnType<typeof vi.spyOn>
7
+
8
+ afterEach(() => {
9
+ env?.destroy()
10
+ errorSpy?.mockRestore()
11
+ })
12
+
13
+ describe('when a stylesheet contains unparseable directives (e.g. Tailwind v4 @theme)', () => {
14
+ it('does not log a css-parsing jsdomError', () => {
15
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
16
+ env = createDomEnv()
17
+ const style = env.window.document.createElement('style')
18
+ style.textContent = '@theme { --color-brand: oklch(0.7 0.1 200); }\n.btn { color: var(--color-brand) }'
19
+
20
+ env.window.document.head.appendChild(style)
21
+
22
+ expect(errorSpy).not.toHaveBeenCalled()
23
+ })
24
+ })
25
+
26
+ describe('when a script throws an unhandled exception', () => {
27
+ it('forwards the error to console.error', () => {
28
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
29
+ env = createDomEnv()
30
+ const script = env.window.document.createElement('script')
31
+ script.textContent = 'throw new Error("boom from story script")'
32
+
33
+ env.window.document.head.appendChild(script)
34
+
35
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('boom from story script'))
36
+ })
37
+ })
38
+ })
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { applyHeadTransform } from '../util/head.js'
3
+
4
+ const BASE_HTML = '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body></html>'
5
+
6
+ describe('applyHeadTransform', () => {
7
+ it('returns the html untouched when head is undefined', async () => {
8
+ expect(await applyHeadTransform(BASE_HTML, undefined)).toBe(BASE_HTML)
9
+ })
10
+
11
+ it('injects user-supplied link tags into the head', async () => {
12
+ const out = await applyHeadTransform(BASE_HTML, {
13
+ link: [{ rel: 'preconnect', href: 'https://fonts.googleapis.com' }],
14
+ })
15
+ expect(out).toContain('<link rel="preconnect" href="https://fonts.googleapis.com">')
16
+ })
17
+
18
+ it('injects user-supplied meta tags into the head', async () => {
19
+ const out = await applyHeadTransform(BASE_HTML, {
20
+ meta: [{ name: 'theme-color', content: '#10b981' }],
21
+ })
22
+ expect(out).toContain('<meta name="theme-color" content="#10b981">')
23
+ })
24
+
25
+ it('skips unhead default lang and viewport tags when head is provided', async () => {
26
+ const out = await applyHeadTransform(BASE_HTML, {
27
+ meta: [{ name: 'theme-color', content: '#10b981' }],
28
+ })
29
+ expect(out).not.toContain('lang="en"')
30
+ expect(out).not.toMatch(/<meta[^>]*name="viewport"/)
31
+ })
32
+ })
@@ -0,0 +1,51 @@
1
+ import type { Context } from '../context.js'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { noop } from '../virtual/noop.js'
4
+ import { resolvedGeneratedGlobalSetup } from '../virtual/resolved-generated-global-setup.js'
5
+
6
+ function contextFactory(config: Partial<Context['config']> = {}): Context {
7
+ return {
8
+ config,
9
+ supportPlugins: [
10
+ { setupFn: 'setupVanilla' },
11
+ { setupFn: 'setupVue3' },
12
+ { setupFn: ['setupSvelte4', 'setupSvelte5'] },
13
+ ],
14
+ } as Context
15
+ }
16
+
17
+ const SETUP_FN_NAMES = ['setupVanilla', 'setupVue3', 'setupSvelte4', 'setupSvelte5']
18
+
19
+ // Consumers read these hooks off a namespace import behind a `typeof === 'function'`
20
+ // guard. If the module doesn't declare them, Rollup can't resolve the member access
21
+ // statically and warns `"setupVue3" is not exported by ...` in every consumer build.
22
+ describe('virtual setup modules', () => {
23
+ describe('generated global setup', () => {
24
+ it('should declare every support plugin setup hook when there is no setup code', () => {
25
+ const code = resolvedGeneratedGlobalSetup(contextFactory())
26
+
27
+ for (const fnName of SETUP_FN_NAMES) {
28
+ expect(code).toContain(`export const ${fnName} = undefined`)
29
+ }
30
+ })
31
+
32
+ it('should export a real hook per support plugin when there is setup code', () => {
33
+ const code = resolvedGeneratedGlobalSetup(contextFactory({ setupCode: ['export function setupVue3() {}'] }))
34
+
35
+ for (const fnName of SETUP_FN_NAMES) {
36
+ expect(code).toContain(`export async function ${fnName} (payload)`)
37
+ }
38
+ })
39
+ })
40
+
41
+ describe('noop', () => {
42
+ it('should keep the default export and declare every setup hook', () => {
43
+ const code = noop(contextFactory())
44
+
45
+ expect(code).toContain('export default () => {}')
46
+ for (const fnName of SETUP_FN_NAMES) {
47
+ expect(code).toContain(`export const ${fnName} = undefined`)
48
+ }
49
+ })
50
+ })
51
+ })
package/src/node/bin.ts CHANGED
@@ -18,7 +18,7 @@ program.command('dev')
18
18
  .option('-p, --port <port>', 'Listening port of the server')
19
19
  .option('-c, --config <file>', `[string] use specified config file`)
20
20
  .option('--open', 'Open in your default browser')
21
- .option('--host <host>', '[string] specify hostname')
21
+ .option('--host [host]', '[string] specify hostname (omit value or pass `0.0.0.0` to expose to all network interfaces)')
22
22
  .action(async (options) => {
23
23
  const { devCommand } = await import('./commands/dev.js')
24
24
  return devCommand(options)
@@ -35,6 +35,8 @@ program.command('build')
35
35
  program.command('preview')
36
36
  .describe('preview the built directory')
37
37
  .option('-p, --port <port>', 'Listening port of the server')
38
+ .option('--open', 'Open in your default browser')
39
+ .option('--host [host]', '[string] specify hostname (omit value or pass `0.0.0.0` to expose to all network interfaces)')
38
40
  .action(async (options) => {
39
41
  const { previewCommand } = await import('./commands/preview.js')
40
42
  return previewCommand(options)
package/src/node/build.ts CHANGED
@@ -27,6 +27,12 @@ import { createMarkdownFilesWatcher } from './markdown.js'
27
27
  import { BuildPluginApi } from './plugin.js'
28
28
  import { startPreview } from './preview.js'
29
29
  import { findAllStories } from './stories.js'
30
+ import {
31
+ chromeCssScopePlugin,
32
+ entryCssMergerPlugin,
33
+ userCssScopePlugin,
34
+ } from './style-isolation/index.js'
35
+ import { applyHeadTransform } from './util/head.js'
30
36
  import { getViteConfigWithPlugins } from './vite.js'
31
37
 
32
38
  const PRELOAD_MODULES = [
@@ -169,7 +175,8 @@ export async function build(ctx: Context) {
169
175
  Object.assign(config.build, {
170
176
  outDir: ctx.config.outDir,
171
177
  emptyOutDir: true,
172
- cssCodeSplit: false,
178
+ // Re-merged per-entry by entry-css-merger.
179
+ cssCodeSplit: true,
173
180
  minify: false,
174
181
  // Don't build in SSR mode
175
182
  ssr: false,
@@ -179,6 +186,11 @@ export async function build(ctx: Context) {
179
186
  },
180
187
  })
181
188
 
189
+ const isolate = ctx.config.isolateStyles !== false
190
+ buildViteConfig.plugins.push(userCssScopePlugin({ enabled: isolate }))
191
+ buildViteConfig.plugins.push(chromeCssScopePlugin({ enabled: isolate }))
192
+ buildViteConfig.plugins.push(entryCssMergerPlugin({ isolateStyles: isolate }))
193
+
182
194
  for (const cb of changeViteConfigCallbacks) {
183
195
  console.log('vite config hook', cb)
184
196
  await cb(buildViteConfig)
@@ -187,7 +199,14 @@ export async function build(ctx: Context) {
187
199
  const results = await viteBuild(buildViteConfig)
188
200
  const result = Array.isArray(results) ? results[0] : results as RollupOutput
189
201
 
190
- const styleOutput = result.output.find(o => o.name === 'style.css' && o.type === 'asset')
202
+ function findEntryCss(entryName: string) {
203
+ return result.output.find(
204
+ o => o.type === 'asset' && o.fileName === `${entryName}.css`,
205
+ )
206
+ }
207
+ const mainStyleOutput = findEntryCss('bundle-main')
208
+ ?? result.output.find(o => o.name === 'style.css' && o.type === 'asset')
209
+ const sandboxStyleOutput = findEntryCss('bundle-sandbox') ?? mainStyleOutput
191
210
 
192
211
  // Preload
193
212
  const preloadOutputs = result.output.filter(o => PRELOAD_MODULES.includes(o.name) && o.type === 'chunk')
@@ -199,14 +218,16 @@ export async function build(ctx: Context) {
199
218
 
200
219
  // Index
201
220
  const indexOutput = result.output.find(o => o.name === 'bundle-main' && o.type === 'chunk')
202
- const indexHtml = generateEntryHtml(indexOutput.fileName, styleOutput.fileName, {
221
+ let indexHtml = generateEntryHtml(indexOutput.fileName, mainStyleOutput.fileName, {
203
222
  HEAD: `${preloadHtml}${prefetchHtml}`,
204
223
  }, ctx)
224
+ indexHtml = await applyHeadTransform(indexHtml, ctx.config.head)
205
225
  await writeFile('index.html', indexHtml, ctx)
206
226
 
207
227
  // Sandbox
208
228
  const sandboxOutput = result.output.find(o => o.name === 'bundle-sandbox' && o.type === 'chunk')
209
- const sandboxHtml = generateEntryHtml(sandboxOutput.fileName, styleOutput.fileName, {}, ctx)
229
+ let sandboxHtml = generateEntryHtml(sandboxOutput.fileName, sandboxStyleOutput.fileName, {}, ctx)
230
+ sandboxHtml = await applyHeadTransform(sandboxHtml, ctx.config.head)
210
231
  await writeFile('__sandbox.html', sandboxHtml, ctx)
211
232
 
212
233
  await writeFile('poveste.json', JSON.stringify(getSerializedStoryData(ctx), null, 2), ctx)
@@ -219,7 +240,7 @@ export async function build(ctx: Context) {
219
240
 
220
241
  // Render
221
242
  if (previewStoryCallbacks.length) {
222
- const { baseUrl, close } = await startPreview(null, ctx)
243
+ const { baseUrl, close } = await startPreview({}, ctx)
223
244
  for (const storyFile of ctx.storyFiles) {
224
245
  const story = storyFile.story
225
246
  for (const variant of story.variants) {
@@ -255,6 +276,7 @@ function generateBaseHtml(head: string, body: string, ctx: Context) {
255
276
  <meta charset="utf-8">
256
277
  <meta name="viewport" content="width=device-width,initial-scale=1">
257
278
  <meta name="description" content="">
279
+ <style>html,body{margin:0;padding:0}</style>
258
280
  ${head}
259
281
  </head>
260
282
  <body>
@@ -281,3 +303,7 @@ async function writeFile(fileName: string, content: string, ctx: Context) {
281
303
  function generateScriptLinks(prefetchScripts: string[], rel: string, ctx: Context) {
282
304
  return prefetchScripts.map(s => `<link rel="${rel}" href="${ctx.resolvedViteConfig.base}${s}" as="script" crossOrigin="anonymous">`).join('')
283
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
+ }