@canofold/vite 0.3.1 → 0.3.3

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
@@ -15,4 +15,14 @@ export default defineConfig({
15
15
 
16
16
  Run `canofold dev`; Vite is mounted on the same development server and port.
17
17
 
18
+ Canofold and Vite share one HTTP server, and a development session creates only one long-lived Vite server. Vite's module graph watches components, styles, and Demo dependencies and delivers their HMR updates; Canofold continues to rebuild Markdown and Canofold configuration inputs.
19
+
20
+ No documentation-specific Vite configuration is required. `@canofold/vite` reuses the project's existing `vite.config`, plugins, aliases, React resolution, TypeScript/JSX settings, and CSS pipeline. Demos should import components through the package name or aliases the project already uses. Component CSS belongs in the component or Demo dependency graph, not in Canofold's site-level `styles` list.
21
+
22
+ For a standard component library with a package name and one `build.lib.entry`, Canofold resolves the package name to that source entry when no same-name alias exists, so the entry does not need to be repeated as an alias. Multi-entry libraries and workspaces are not inferred and keep using their existing exports or aliases.
23
+
24
+ Production uses one Vite graph for the Markdown browser enhancer, Demo runtime, and project components. The component project owns React and React DOM resolution, while Vite deduplicates them and splits Demo JavaScript and CSS on demand instead of placing every example in the entry chunk. That graph emits a lightweight Markdown entry and a Demo entry: pages without Demos do not statically load React for native Markdown behavior, while Demo pages reuse the enhancer from the Demo entry.
25
+
26
+ Use `demos.setup` only for shared providers, themes, internationalization, or routing context. Ordinary components do not need it.
27
+
18
28
  Demos render inline by default. `sandbox="iframe"` places trusted local demo code in a restricted iframe for DOM and global-style isolation; it is not a security boundary for untrusted code.
package/README.zh-CN.md CHANGED
@@ -13,4 +13,14 @@ export default defineConfig({
13
13
  })
14
14
  ```
15
15
 
16
+ 运行 `canofold dev` 即可。Canofold 与 Vite 共用一个 HTTP Server 和端口;同一开发会话只创建一个 Vite Server,组件、样式和 Demo 依赖由 Vite module graph 监听并通过 HMR 更新,Markdown 与 Canofold 配置仍由 Canofold 重建。
17
+
18
+ 无需为文档单独维护 Vite 配置。`@canofold/vite` 会复用项目现有的 `vite.config`、插件、alias、React、TypeScript/JSX 与 CSS 配置。Demo 应像应用代码一样从组件包名或项目已有 alias 导入组件,组件 CSS 由组件或 Demo 自己 import;不要把每个组件样式登记到 Canofold 的 `styles`。
19
+
20
+ 对于 `package.json` 有包名且 `build.lib.entry` 只有一个入口的标准组件库,Canofold 会在没有同名自定义 alias 时自动把包名解析到该源码入口,因此不需要把同一入口再写一遍 alias。多入口库和 workspace 不做推断,继续使用项目已有的 exports 或 alias。
21
+
22
+ 生产构建中,Markdown 浏览器增强、Demo 运行时和项目组件进入同一 Vite 构建图。React 与 React DOM 由组件项目解析并去重,Demo 与 CSS 按需分块,不会把全部示例和样式无条件放进入口。同一构建图会输出轻量 Markdown 入口和 Demo 入口:没有 Demo 的页面不会因为原生 Markdown 交互而静态加载 React,有 Demo 的页面则复用 Demo 入口中的 Markdown 增强能力。
23
+
24
+ `demos.setup` 只用于所有 Demo 共享的 Provider、主题、国际化或路由环境;普通组件不需要设置。
25
+
16
26
  Demo 默认渲染在文档页面中。`sandbox="iframe"` 会把可信的本地 Demo 放进受限 iframe,隔离 DOM 与全局样式;它不是运行不可信代码的安全边界。
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/index.ts
2
+ import { createHash } from "crypto";
2
3
  import { realpathSync } from "fs";
3
- import { readFile, writeFile } from "fs/promises";
4
+ import { access, readFile, readdir } from "fs/promises";
4
5
  import { extname, isAbsolute, join, relative, resolve, sep } from "path";
6
+ import { fileURLToPath } from "url";
5
7
  import {
6
8
  build as viteBuild,
7
9
  createServer as createViteServer,
@@ -11,16 +13,16 @@ import {
11
13
  } from "vite";
12
14
 
13
15
  // src/runtime.ts
14
- function demoRuntimeSource(registryEntries, setupImport, styleUrls = []) {
16
+ function demoRuntimeSource(registryEntries, setupImport, markdownClientSpecifier) {
15
17
  const imports = registryEntries.join("\n");
16
18
  const setup = setupImport ?? "const CanofoldDemoSetup = null;";
17
19
  return `${imports}
18
20
  ${setup}
21
+ export { enhanceMarkdown } from ${JSON.stringify(markdownClientSpecifier)};
19
22
  import React from 'react';
20
23
  import { createRoot } from 'react-dom/client';
21
24
 
22
25
  const registry = new Map(CANOFOLD_DEMO_REGISTRY);
23
- const iframeStyleUrls = ${JSON.stringify(styleUrls)};
24
26
  const roots = new Set();
25
27
  const frameObservers = new Set();
26
28
  let eventController;
@@ -34,15 +36,16 @@ class DemoBoundary extends React.Component {
34
36
  }
35
37
  }
36
38
 
37
- function componentFor(id) {
38
- const value = registry.get(id);
39
+ async function componentFor(id) {
40
+ const load = registry.get(id);
41
+ const value = load && await load();
39
42
  const component = value && (value.default || value.Demo);
40
43
  if (!component) throw new Error('Demo module ' + id + ' must export a default React component.');
41
44
  return component;
42
45
  }
43
46
 
44
- export function mountDemo(element, id, failedLabel) {
45
- const Demo = componentFor(id);
47
+ export async function mountDemo(element, id, failedLabel) {
48
+ const Demo = await componentFor(id);
46
49
  const content = React.createElement(Demo);
47
50
  const wrapped = CanofoldDemoSetup ? React.createElement(CanofoldDemoSetup, null, content) : content;
48
51
  const root = createRoot(element);
@@ -53,8 +56,7 @@ export function mountDemo(element, id, failedLabel) {
53
56
 
54
57
  function iframeDocument(id, failedLabel) {
55
58
  const script = \`import(\${JSON.stringify(import.meta.url)}).then(({ mountDemo }) => mountDemo(document.getElementById('root'),\${JSON.stringify(id)},\${JSON.stringify(failedLabel)})).catch((error) => { console.error('[Canofold demo iframe]', error); const root = document.getElementById('root'); root.setAttribute('role', 'alert'); root.textContent = \${JSON.stringify(failedLabel)}; });\`;
56
- const styles = iframeStyleUrls.map((href) => '<link rel="stylesheet" href="' + href.replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '">').join('');
57
- return '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' + styles + '<style>html,body,#root{min-height:100%;margin:0}body{display:grid;place-items:center;padding:24px;box-sizing:border-box;font-family:system-ui,sans-serif}</style></head><body><div id="root"></div><script type="module">' + script.replace(/<\\/script/gi, '<\\\\/script') + '<\\/script></body></html>';
59
+ return '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>html,body,#root{min-height:100%;margin:0}body{display:grid;place-items:center;padding:24px;box-sizing:border-box;font-family:system-ui,sans-serif}</style></head><body><div id="root"></div><script type="module">' + script.replace(/<\\/script/gi, '<\\\\/script') + '<\\/script></body></html>';
58
60
  }
59
61
 
60
62
  function fitFrame(frame) {
@@ -86,7 +88,7 @@ function bindSourceToggle(button, signal) {
86
88
  button.addEventListener('click', () => setExpanded(button.getAttribute('aria-expanded') !== 'true'), { signal });
87
89
  }
88
90
 
89
- function standaloneDemo() {
91
+ async function standaloneDemo() {
90
92
  const id = new URL(window.location.href).searchParams.get('canofold-demo');
91
93
  if (!id || !registry.has(id)) return false;
92
94
  const preview = Array.from(document.querySelectorAll('[data-cf-demo-preview]')).find(
@@ -98,13 +100,13 @@ function standaloneDemo() {
98
100
  root.setAttribute('data-cf-demo-id', id);
99
101
  document.documentElement.setAttribute('data-cf-demo-standalone', '');
100
102
  document.body.replaceChildren(root);
101
- mountDemo(root, id, failed);
103
+ await mountDemo(root, id, failed);
102
104
  return true;
103
105
  }
104
106
 
105
107
  export async function bootstrapDemos() {
106
108
  window.__canofoldDemoDispose?.();
107
- if (standaloneDemo()) {
109
+ if (await standaloneDemo()) {
108
110
  window.__canofoldDemoDispose = () => {
109
111
  roots.forEach((root) => root.unmount());
110
112
  roots.clear();
@@ -115,7 +117,7 @@ export async function bootstrapDemos() {
115
117
  document.documentElement.removeAttribute('data-cf-demo-standalone');
116
118
  eventController = new AbortController();
117
119
  document.querySelectorAll('[data-cf-demo-source-toggle]').forEach((button) => bindSourceToggle(button, eventController.signal));
118
- document.querySelectorAll('[data-cf-demo-preview]').forEach((preview) => {
120
+ await Promise.all(Array.from(document.querySelectorAll('[data-cf-demo-preview]')).map(async (preview) => {
119
121
  const id = preview.getAttribute('data-cf-demo-id');
120
122
  if (!id) return;
121
123
  const loading = preview.getAttribute('data-cf-demo-loading-label') || 'Loading example\u2026';
@@ -135,14 +137,14 @@ export async function bootstrapDemos() {
135
137
  preview.replaceChildren(frame);
136
138
  } else {
137
139
  preview.textContent = '';
138
- mountDemo(preview, id, failed);
140
+ await mountDemo(preview, id, failed);
139
141
  }
140
142
  } catch (error) {
141
143
  console.error('[Canofold demo]', error);
142
144
  preview.textContent = failed;
143
145
  preview.setAttribute('data-cf-demo-error', '');
144
146
  }
145
- });
147
+ }));
146
148
  window.__canofoldDemoDispose = () => {
147
149
  eventController?.abort();
148
150
  eventController = undefined;
@@ -165,6 +167,21 @@ if (import.meta.hot) {
165
167
  // src/index.ts
166
168
  var PUBLIC_CLIENT_ID = "virtual:canofold-demo-client";
167
169
  var RESOLVED_CLIENT_ID = `\0${PUBLIC_CLIENT_ID}`;
170
+ var PUBLIC_MARKDOWN_CLIENT_ID = "virtual:canofold-markdown-client";
171
+ var RESOLVED_MARKDOWN_CLIENT_ID = `\0${PUBLIC_MARKDOWN_CLIENT_ID}`;
172
+ var PACKAGE_MODULE_PATH = fileURLToPath(import.meta.url);
173
+ var PROJECT_METADATA_FILES = [
174
+ "package.json",
175
+ "pnpm-lock.yaml",
176
+ "package-lock.json",
177
+ "yarn.lock",
178
+ "bun.lock",
179
+ "bun.lockb"
180
+ ];
181
+ var devSessions = /* @__PURE__ */ new WeakMap();
182
+ function jsonForJavaScript(value) {
183
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
184
+ }
168
185
  function baseUrl(basePath, path) {
169
186
  const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
170
187
  return `${prefix}/${path.replace(/^\//, "")}`;
@@ -191,6 +208,21 @@ function canonicalPath(path) {
191
208
  return resolve(path);
192
209
  }
193
210
  }
211
+ async function projectMetadataPaths(projectRoot) {
212
+ const candidates = PROJECT_METADATA_FILES.map((file) => join(projectRoot, file));
213
+ const existing = await Promise.all(
214
+ candidates.map(async (path) => {
215
+ try {
216
+ await access(path);
217
+ return canonicalPath(path);
218
+ } catch (error) {
219
+ if (error.code === "ENOENT") return void 0;
220
+ throw error;
221
+ }
222
+ })
223
+ );
224
+ return existing.filter((path) => Boolean(path));
225
+ }
194
226
  function isProjectSource(cwd, path) {
195
227
  const projectRelative = relative(canonicalPath(cwd), canonicalPath(path));
196
228
  return projectRelative !== ".." && !projectRelative.startsWith(`..${sep}`) && !isAbsolute(projectRelative) && !projectRelative.split(sep).some((segment) => segment === "node_modules" || segment === ".canofold");
@@ -273,30 +305,78 @@ async function resolvedSetup(server, setup, cwd) {
273
305
  dependencyPaths: await collectLocalDependencies(server, modulePath, cwd)
274
306
  };
275
307
  }
308
+ function hasPackageAlias(config, packageName) {
309
+ const alias = config.resolve?.alias;
310
+ if (!alias) return false;
311
+ if (!Array.isArray(alias)) return Object.hasOwn(alias, packageName);
312
+ return alias.some(
313
+ ({ find }) => typeof find === "string" ? find === packageName : new RegExp(find.source, find.flags).test(packageName)
314
+ );
315
+ }
316
+ function singleLibraryEntry(config) {
317
+ const library = config.build?.lib;
318
+ if (!library) return void 0;
319
+ const { entry } = library;
320
+ if (typeof entry === "string") return entry;
321
+ if (Array.isArray(entry)) return entry.length === 1 ? entry[0] : void 0;
322
+ const entries = Object.values(entry);
323
+ return entries.length === 1 ? entries[0] : void 0;
324
+ }
325
+ async function inferredPackageAlias(config, projectRoot) {
326
+ const entry = singleLibraryEntry(config);
327
+ if (!entry) return void 0;
328
+ try {
329
+ const manifest = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf8"));
330
+ if (typeof manifest.name !== "string" || manifest.name.length === 0) return void 0;
331
+ if (hasPackageAlias(config, manifest.name)) return void 0;
332
+ return {
333
+ find: new RegExp(`^${manifest.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
334
+ replacement: canonicalPath(resolve(projectRoot, entry))
335
+ };
336
+ } catch (error) {
337
+ if (error.code === "ENOENT") return void 0;
338
+ throw error;
339
+ }
340
+ }
276
341
  function virtualClientPlugin({
342
+ projectRoot,
277
343
  getDemos,
278
- getSetup,
279
- styleUrls = []
344
+ getSetup
280
345
  }) {
281
346
  return {
282
347
  name: "canofold-demo-client",
283
348
  enforce: "post",
349
+ async config(config) {
350
+ const alias = await inferredPackageAlias(config, projectRoot);
351
+ return alias ? { resolve: { alias: [alias] } } : void 0;
352
+ },
284
353
  resolveId(id) {
285
- return id === PUBLIC_CLIENT_ID ? RESOLVED_CLIENT_ID : void 0;
354
+ if (id === PUBLIC_CLIENT_ID) return RESOLVED_CLIENT_ID;
355
+ if (id === PUBLIC_MARKDOWN_CLIENT_ID) return RESOLVED_MARKDOWN_CLIENT_ID;
356
+ return void 0;
286
357
  },
287
- load(id) {
358
+ async load(id) {
359
+ if (id === RESOLVED_MARKDOWN_CLIENT_ID) {
360
+ const markdownClient = await this.resolve("@canofold/markdown/client", PACKAGE_MODULE_PATH, {
361
+ skipSelf: true
362
+ });
363
+ if (!markdownClient || markdownClient.external) {
364
+ throw new Error("@canofold/vite could not resolve its @canofold/markdown client dependency");
365
+ }
366
+ return `export { enhanceMarkdown } from ${jsonForJavaScript(normalizePath(markdownClient.id))};`;
367
+ }
288
368
  if (id !== RESOLVED_CLIENT_ID) return void 0;
289
369
  const entries = [];
290
370
  const registry = [];
291
- getDemos().forEach((demo, index) => {
292
- const binding = `CanofoldDemo${index}`;
293
- entries.push(`import * as ${binding} from ${JSON.stringify(normalizePath(demo.modulePath))};`);
294
- registry.push(`[${JSON.stringify(demo.id)}, ${binding}]`);
371
+ getDemos().forEach((demo) => {
372
+ registry.push(
373
+ `[${jsonForJavaScript(demo.id)}, () => import(${jsonForJavaScript(normalizePath(demo.modulePath))})]`
374
+ );
295
375
  });
296
376
  entries.push(`const CANOFOLD_DEMO_REGISTRY = [${registry.join(",")}];`);
297
377
  const setup = getSetup();
298
- const setupImport = setup ? `import CanofoldDemoSetup from ${JSON.stringify(setup)};` : void 0;
299
- return demoRuntimeSource(entries, setupImport, styleUrls);
378
+ const setupImport = setup ? `import CanofoldDemoSetup from ${jsonForJavaScript(setup)};` : void 0;
379
+ return demoRuntimeSource(entries, setupImport, PUBLIC_MARKDOWN_CLIENT_ID);
300
380
  }
301
381
  };
302
382
  }
@@ -308,6 +388,9 @@ function inlineConfig(cwd, basePath, options, plugins) {
308
388
  base: basePath,
309
389
  appType: "custom",
310
390
  plugins: Array.isArray(plugins) ? plugins : [plugins],
391
+ // The dev server ships native ESM to the current browser. Avoid asking
392
+ // esbuild to lower dependency syntax that its transformer cannot lower.
393
+ optimizeDeps: { esbuildOptions: { target: "esnext" } },
311
394
  resolve: { dedupe: ["react", "react-dom"] }
312
395
  };
313
396
  }
@@ -320,15 +403,22 @@ function demoBuildOptions(context) {
320
403
  outDir: join(context.outputRoot, "assets/canofold-demos"),
321
404
  emptyOutDir: true,
322
405
  copyPublicDir: false,
323
- cssCodeSplit: false,
406
+ cssCodeSplit: true,
324
407
  rollupOptions: {
325
408
  external: () => false,
326
- input: PUBLIC_CLIENT_ID,
409
+ // Canofold loads both browser entries with dynamic import() and consumes
410
+ // their named exports. Vite's application default may otherwise remove
411
+ // entry exports that are not referenced inside the bundle.
412
+ preserveEntrySignatures: "strict",
413
+ input: {
414
+ index: PUBLIC_CLIENT_ID,
415
+ markdown: PUBLIC_MARKDOWN_CLIENT_ID
416
+ },
327
417
  output: {
328
418
  format: "es",
329
- inlineDynamicImports: true,
330
- entryFileNames: "index.js",
331
- assetFileNames: (asset) => asset.names?.some((name) => name.endsWith(".css")) ? "styles.css" : "[name][extname]"
419
+ entryFileNames: "[name].js",
420
+ chunkFileNames: "chunks/[name]-[hash].js",
421
+ assetFileNames: "assets/[name]-[hash][extname]"
332
422
  }
333
423
  }
334
424
  };
@@ -341,12 +431,20 @@ async function isolatedBuildConfig(context, options, plugin, build) {
341
431
  configFile,
342
432
  root
343
433
  );
344
- const sharedConfig = { ...loaded?.config ?? {} };
434
+ let sharedConfig = { ...loaded?.config ?? {} };
435
+ const packageAlias = await inferredPackageAlias(sharedConfig, root);
436
+ if (packageAlias) {
437
+ sharedConfig = mergeConfig(sharedConfig, { resolve: { alias: [packageAlias] } });
438
+ }
345
439
  delete sharedConfig.build;
346
440
  delete sharedConfig.server;
347
441
  delete sharedConfig.preview;
348
442
  const engineConfig = {
349
443
  ...inlineConfig(context.cwd, context.basePath, { ...options, configFile: false }, plugin),
444
+ // The demo bundle is emitted below the site's own asset directory. Vite 8
445
+ // resolves preload dependencies from `base`, so using the site root here
446
+ // would incorrectly request demo chunks from `/chunks` and `/assets`.
447
+ base: baseUrl(context.basePath, "/assets/canofold-demos/"),
350
448
  mode: "production",
351
449
  define: {
352
450
  "process.env.NODE_ENV": JSON.stringify("production")
@@ -354,17 +452,23 @@ async function isolatedBuildConfig(context, options, plugin, build) {
354
452
  esbuild: {
355
453
  jsxDev: false
356
454
  },
357
- build
455
+ build: {
456
+ // Markdown browser modules are published as ES2022. Keep that baseline
457
+ // when a component project does not declare its own Vite target.
458
+ target: loaded?.config.build?.target ?? "es2022",
459
+ ...build
460
+ }
358
461
  };
359
462
  return mergeConfig(sharedConfig, engineConfig);
360
463
  }
361
464
  async function prepareDemos(context, options) {
465
+ const projectRoot = canonicalPath(options.root ? resolve(context.cwd, options.root) : context.cwd);
362
466
  let demos = [];
363
467
  let setup;
364
468
  const plugin = virtualClientPlugin({
469
+ projectRoot,
365
470
  getDemos: () => demos,
366
- getSetup: () => setup,
367
- styleUrls: context.mode === "build" ? [baseUrl(context.basePath, "/assets/canofold-demos/styles.css")] : []
471
+ getSetup: () => setup
368
472
  });
369
473
  const server = await createViteServer({
370
474
  ...inlineConfig(context.cwd, context.basePath, options, plugin),
@@ -384,6 +488,7 @@ async function prepareDemos(context, options) {
384
488
  dependencyPaths: [
385
489
  .../* @__PURE__ */ new Set([
386
490
  ...server.config.configFileDependencies.map((path) => canonicalPath(path)),
491
+ ...await projectMetadataPaths(projectRoot),
387
492
  ...resolved?.dependencyPaths ?? []
388
493
  ])
389
494
  ],
@@ -393,6 +498,119 @@ async function prepareDemos(context, options) {
393
498
  await server.close();
394
499
  }
395
500
  }
501
+ function devSessionKey(context, options) {
502
+ return JSON.stringify({
503
+ cwd: canonicalPath(context.cwd),
504
+ outputRoot: canonicalPath(context.outputRoot),
505
+ basePath: context.basePath,
506
+ root: options.root ?? null,
507
+ configFile: options.configFile ?? null
508
+ });
509
+ }
510
+ async function configDependencySignature(paths) {
511
+ const hash = createHash("sha256");
512
+ for (const path of [...paths].map(canonicalPath).sort()) {
513
+ hash.update(path);
514
+ try {
515
+ hash.update(await readFile(path));
516
+ } catch (error) {
517
+ if (error.code !== "ENOENT") throw error;
518
+ hash.update("\0missing");
519
+ }
520
+ }
521
+ return hash.digest("hex");
522
+ }
523
+ async function devEnvironmentSignature(server, projectRoot) {
524
+ return configDependencySignature([
525
+ ...server.config.configFileDependencies,
526
+ ...await projectMetadataPaths(projectRoot)
527
+ ]);
528
+ }
529
+ function isGeneratedPath(context, path) {
530
+ const projectRelative = relative(canonicalPath(context.cwd), canonicalPath(path));
531
+ const outputRelative = relative(canonicalPath(context.outputRoot), canonicalPath(path));
532
+ return projectRelative.split(sep).some((segment) => segment === "node_modules" || segment === ".git" || segment === ".canofold") || outputRelative !== ".." && !outputRelative.startsWith(`..${sep}`) && !isAbsolute(outputRelative);
533
+ }
534
+ async function prepareDevDemos(context, options) {
535
+ if (!context.server) {
536
+ throw new Error("@canofold/vite requires the shared Canofold HTTP server in dev mode");
537
+ }
538
+ const key = devSessionKey(context, options);
539
+ const projectRoot = canonicalPath(options.root ? resolve(context.cwd, options.root) : context.cwd);
540
+ let session = devSessions.get(context.server);
541
+ const environmentChanged = session?.key === key && await devEnvironmentSignature(session.server, projectRoot) !== session.environmentSignature;
542
+ if (environmentChanged && session) {
543
+ await session.server.restart(true);
544
+ session.environmentSignature = await devEnvironmentSignature(session.server, projectRoot);
545
+ }
546
+ if (session?.key !== key) {
547
+ if (session) await session.server.close();
548
+ const state = { demos: [] };
549
+ const plugin = virtualClientPlugin({
550
+ projectRoot,
551
+ getDemos: () => state.demos,
552
+ getSetup: () => state.setup
553
+ });
554
+ const server = await createViteServer({
555
+ ...inlineConfig(context.cwd, context.basePath, options, plugin),
556
+ server: {
557
+ middlewareMode: true,
558
+ hmr: { server: context.server },
559
+ watch: { ignored: (path) => isGeneratedPath(context, path) }
560
+ }
561
+ });
562
+ session = {
563
+ key,
564
+ environmentSignature: await devEnvironmentSignature(server, projectRoot),
565
+ server,
566
+ plugin,
567
+ state
568
+ };
569
+ devSessions.set(context.server, session);
570
+ }
571
+ const wasInitialized = session.state.demos.length > 0 || session.state.setup !== void 0;
572
+ const previousSignature = JSON.stringify([
573
+ session.state.demos.map((demo) => [demo.id, demo.modulePath]),
574
+ session.state.setup
575
+ ]);
576
+ const demos = await Promise.all(
577
+ context.demos.map((reference) => resolveDemo(session.server, reference, context.cwd))
578
+ );
579
+ const setup = await resolvedSetup(session.server, context.setup, context.cwd);
580
+ session.state.demos = demos;
581
+ session.state.setup = setup?.id;
582
+ const nextSignature = JSON.stringify([
583
+ session.state.demos.map((demo) => [demo.id, demo.modulePath]),
584
+ session.state.setup
585
+ ]);
586
+ if (wasInitialized && previousSignature !== nextSignature) {
587
+ const module = session.server.moduleGraph.getModuleById(RESOLVED_CLIENT_ID);
588
+ if (module) session.server.moduleGraph.invalidateModule(module);
589
+ session.server.ws.send({ type: "full-reload" });
590
+ }
591
+ return {
592
+ demos,
593
+ setup: session.state.setup,
594
+ dependencyPaths: [
595
+ .../* @__PURE__ */ new Set([
596
+ ...session.server.config.configFileDependencies.map((path) => canonicalPath(path)),
597
+ ...await projectMetadataPaths(projectRoot),
598
+ ...setup?.dependencyPaths ?? []
599
+ ])
600
+ ],
601
+ plugin: session.plugin
602
+ };
603
+ }
604
+ async function outputPathsUnder(root, prefix) {
605
+ const entries = await readdir(root, { withFileTypes: true });
606
+ const paths = await Promise.all(
607
+ entries.map(async (entry) => {
608
+ const relativePath = `${prefix}/${entry.name}`;
609
+ return entry.isDirectory() ? outputPathsUnder(join(root, entry.name), relativePath) : [relativePath];
610
+ })
611
+ );
612
+ return paths.flat().sort();
613
+ }
396
614
  function vite(options = {}) {
397
615
  return {
398
616
  id: "@canofold/vite",
@@ -402,63 +620,46 @@ function vite(options = {}) {
402
620
  configFile: options.configFile ?? null
403
621
  },
404
622
  async prepare(context) {
405
- const prepared = await prepareDemos(context, options);
623
+ const prepared = context.mode === "dev" ? await prepareDevDemos(context, options) : await prepareDemos(context, options);
406
624
  if (context.mode === "build") {
407
625
  const build = demoBuildOptions(context);
408
626
  await viteBuild(await isolatedBuildConfig(context, options, prepared.plugin, build));
409
- await writeFile(join(context.outputRoot, "assets/canofold-demos/styles.css"), "", {
410
- flag: "a"
411
- });
412
627
  }
628
+ const clientUrl = context.mode === "build" ? baseUrl(context.basePath, "/assets/canofold-demos/index.js") : baseUrl(context.basePath, `/@id/__x00__${PUBLIC_CLIENT_ID}`);
629
+ const markdownClientUrl = context.mode === "build" ? baseUrl(context.basePath, "/assets/canofold-demos/markdown.js") : baseUrl(context.basePath, `/@id/__x00__${PUBLIC_MARKDOWN_CLIENT_ID}`);
413
630
  return {
414
- clientUrl: context.mode === "build" ? baseUrl(context.basePath, "/assets/canofold-demos/index.js") : baseUrl(context.basePath, `/@id/__x00__${PUBLIC_CLIENT_ID}`),
415
- ...context.mode === "build" ? { styleUrls: [baseUrl(context.basePath, "/assets/canofold-demos/styles.css")] } : {},
631
+ clientUrl,
632
+ markdownClientUrl,
416
633
  demos: Object.fromEntries(prepared.demos.map((demo) => [demo.id, demo])),
417
634
  dependencyPaths: prepared.dependencyPaths,
418
635
  ...context.mode === "build" ? {
419
- outputPaths: ["assets/canofold-demos/index.js", "assets/canofold-demos/styles.css"]
636
+ outputPaths: await outputPathsUnder(
637
+ join(context.outputRoot, "assets/canofold-demos"),
638
+ "assets/canofold-demos"
639
+ )
420
640
  } : {}
421
641
  };
422
642
  },
423
643
  async startDev(context) {
424
- const currentDemos = () => context.getDemos();
425
- let setup;
426
- const plugin = virtualClientPlugin({ getDemos: currentDemos, getSetup: () => setup });
427
- const server = await createViteServer({
428
- ...inlineConfig(context.cwd, context.basePath, options, plugin),
429
- server: {
430
- middlewareMode: true,
431
- hmr: { server: context.server },
432
- watch: { ignored: (path) => context.shouldIgnorePath(path) }
433
- }
434
- });
435
- setup = (await resolvedSetup(server, context.setup, context.cwd))?.id;
436
- let demoSignature = JSON.stringify(
437
- currentDemos().map((demo) => [demo.id, normalizePath(demo.modulePath)])
438
- );
644
+ const session = devSessions.get(context.server);
645
+ if (!session) {
646
+ throw new Error("@canofold/vite dev session was not prepared on the shared HTTP server");
647
+ }
648
+ const { server } = session;
439
649
  return {
440
650
  middleware(request, response, next) {
441
651
  server.middlewares(request, response, next);
442
652
  },
443
653
  handlesFile(path) {
444
654
  const absolutePath = canonicalPath(path);
445
- const isEntry = currentDemos().some((demo) => canonicalPath(demo.modulePath) === absolutePath);
446
- if (isEntry || setup && canonicalPath(filePathFromResolvedId(setup)) === absolutePath) {
447
- return false;
448
- }
449
655
  return Boolean(server.moduleGraph.getModulesByFile(absolutePath)?.size);
450
656
  },
451
- update() {
452
- const nextSignature = JSON.stringify(
453
- currentDemos().map((demo) => [demo.id, normalizePath(demo.modulePath)])
454
- );
455
- if (nextSignature === demoSignature) return;
456
- demoSignature = nextSignature;
457
- const module = server.moduleGraph.getModuleById(RESOLVED_CLIENT_ID);
458
- if (module) server.moduleGraph.invalidateModule(module);
459
- server.ws.send({ type: "full-reload" });
460
- },
461
- close: () => server.close()
657
+ async close() {
658
+ if (devSessions.get(context.server) === session) {
659
+ devSessions.delete(context.server);
660
+ }
661
+ await server.close();
662
+ }
462
663
  };
463
664
  }
464
665
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canofold/vite",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Official Vite demo engine for interactive Canofold component documentation.",
5
5
  "license": "MIT",
6
6
  "author": "Canofold Contributors",
@@ -41,19 +41,24 @@
41
41
  "access": "public",
42
42
  "provenance": true
43
43
  },
44
+ "dependencies": {
45
+ "@canofold/markdown": "0.3.3"
46
+ },
44
47
  "peerDependencies": {
45
- "canofold": "^0.3.1",
48
+ "canofold": "^0.3.3",
46
49
  "react": "^18.2.0 || ^19.0.0",
47
50
  "react-dom": "^18.2.0 || ^19.0.0",
48
51
  "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
49
52
  },
50
53
  "devDependencies": {
51
- "@types/node": "^22.10.0",
54
+ "@types/node": "^22.20.3",
55
+ "react": "^19.3.0",
56
+ "react-dom": "^19.3.0",
52
57
  "tsup": "^8.3.5",
53
58
  "typescript": "^6.0.3",
54
59
  "vite": "^6.4.3",
55
60
  "vitest": "^4.1.11",
56
- "canofold": "0.3.1"
61
+ "canofold": "0.3.3"
57
62
  },
58
63
  "scripts": {
59
64
  "build": "tsup",