void-plugin-hono-jsx 0.1.0-alpha.3 → 0.1.0-alpha.4

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 (90) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +41 -12
  3. package/THIRD_PARTY_NOTICES.md +2 -2
  4. package/dist/browser/client.d.ts +2 -1
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/client.js +76 -22
  7. package/dist/browser/client.js.map +1 -1
  8. package/dist/browser/document-head.d.ts +1 -1
  9. package/dist/browser/document-head.d.ts.map +1 -1
  10. package/dist/browser/document-head.js +22 -1
  11. package/dist/browser/document-head.js.map +1 -1
  12. package/dist/browser/form-view.d.ts +10 -0
  13. package/dist/browser/form-view.d.ts.map +1 -0
  14. package/dist/browser/form-view.js +16 -0
  15. package/dist/browser/form-view.js.map +1 -0
  16. package/dist/browser/form.d.ts +28 -12
  17. package/dist/browser/form.d.ts.map +1 -1
  18. package/dist/browser/form.js +84 -23
  19. package/dist/browser/form.js.map +1 -1
  20. package/dist/browser/markdown.d.ts +15 -0
  21. package/dist/browser/markdown.d.ts.map +1 -0
  22. package/dist/browser/markdown.js +6 -0
  23. package/dist/browser/markdown.js.map +1 -0
  24. package/dist/browser/navigation.d.ts +13 -0
  25. package/dist/browser/navigation.d.ts.map +1 -0
  26. package/dist/browser/navigation.js +83 -0
  27. package/dist/browser/navigation.js.map +1 -0
  28. package/dist/browser/pages.d.ts +33 -3
  29. package/dist/browser/pages.d.ts.map +1 -1
  30. package/dist/browser/pages.js +76 -13
  31. package/dist/browser/pages.js.map +1 -1
  32. package/dist/plugins/hono-islands.d.ts +1 -1
  33. package/dist/plugins/hono-islands.d.ts.map +1 -1
  34. package/dist/plugins/hono-islands.js +156 -24
  35. package/dist/plugins/hono-islands.js.map +1 -1
  36. package/dist/plugins/hono-markdown.d.ts +4 -0
  37. package/dist/plugins/hono-markdown.d.ts.map +1 -0
  38. package/dist/plugins/hono-markdown.js +91 -0
  39. package/dist/plugins/hono-markdown.js.map +1 -0
  40. package/dist/plugins/hono-pages.d.ts +9 -1
  41. package/dist/plugins/hono-pages.d.ts.map +1 -1
  42. package/dist/plugins/hono-pages.js +51 -15
  43. package/dist/plugins/hono-pages.js.map +1 -1
  44. package/dist/plugins/markdown-api.d.ts +10 -0
  45. package/dist/plugins/markdown-api.d.ts.map +1 -0
  46. package/dist/plugins/markdown-api.js +13 -0
  47. package/dist/plugins/markdown-api.js.map +1 -0
  48. package/dist/plugins/markdown-compiler.d.ts +43 -0
  49. package/dist/plugins/markdown-compiler.d.ts.map +1 -0
  50. package/dist/plugins/markdown-compiler.js +91 -0
  51. package/dist/plugins/markdown-compiler.js.map +1 -0
  52. package/dist/plugins/resolve-component.d.ts +3 -0
  53. package/dist/plugins/resolve-component.d.ts.map +1 -0
  54. package/dist/plugins/resolve-component.js +15 -0
  55. package/dist/plugins/resolve-component.js.map +1 -0
  56. package/dist/plugins/runtime-paths.d.ts +1 -0
  57. package/dist/plugins/runtime-paths.d.ts.map +1 -1
  58. package/dist/plugins/runtime-paths.js +2 -0
  59. package/dist/plugins/runtime-paths.js.map +1 -1
  60. package/dist/server/form-view.d.ts +10 -0
  61. package/dist/server/form-view.d.ts.map +1 -0
  62. package/dist/server/form-view.js +16 -0
  63. package/dist/server/form-view.js.map +1 -0
  64. package/dist/server/form.d.ts +28 -12
  65. package/dist/server/form.d.ts.map +1 -1
  66. package/dist/server/form.js +84 -23
  67. package/dist/server/form.js.map +1 -1
  68. package/dist/server/markdown.d.ts +15 -0
  69. package/dist/server/markdown.d.ts.map +1 -0
  70. package/dist/server/markdown.js +6 -0
  71. package/dist/server/markdown.js.map +1 -0
  72. package/dist/server/pages.d.ts +33 -3
  73. package/dist/server/pages.d.ts.map +1 -1
  74. package/dist/server/pages.js +76 -13
  75. package/dist/server/pages.js.map +1 -1
  76. package/package.json +21 -3
  77. package/src/plugins/hono-islands.ts +121 -23
  78. package/src/plugins/hono-markdown.ts +78 -0
  79. package/src/plugins/hono-pages.ts +50 -14
  80. package/src/plugins/markdown-api.ts +17 -0
  81. package/src/plugins/markdown-compiler.ts +77 -0
  82. package/src/plugins/resolve-component.ts +12 -0
  83. package/src/plugins/runtime-paths.ts +2 -0
  84. package/src/runtime/client.tsx +66 -24
  85. package/src/runtime/document-head.ts +20 -1
  86. package/src/runtime/form-view.tsx +23 -0
  87. package/src/runtime/form.ts +82 -41
  88. package/src/runtime/markdown.ts +7 -0
  89. package/src/runtime/navigation.ts +72 -0
  90. package/src/runtime/pages.tsx +77 -15
@@ -1,30 +1,44 @@
1
1
  import { readFileSync, realpathSync } from 'node:fs';
2
2
  import { join, relative } from 'node:path';
3
3
  import { normalizePath, parseSync, type Plugin } from 'vite';
4
- import { scanPages, resolvePagesDir, type PageScanResult } from 'void/pages';
5
- import { findIslandImports, editIslandSource, resolveSpecifier, computePagesNeedingClientJS, ISLANDS_RENDERER_ID, ISLANDS_CLIENT_ENTRY_ID } from 'void/pages-islands-plugin';
4
+ import { scanPages, resolveEffectiveLayoutChain, resolvePagesDir, type PageScanResult } from 'void/pages';
5
+ import { findIslandImports, editIslandSource, computePagesNeedingClientJS, ISLANDS_RENDERER_ID, ISLANDS_CLIENT_ENTRY_ID } from 'void/pages-islands-plugin';
6
6
 
7
- /** Narrow adapter for Void's load-on-page-load island convention. */
7
+ import { resolveComponent } from './resolve-component.ts';
8
+
9
+ import { markdownApi, markdownDiagnostic, type HonoMarkdownApi } from './markdown-api.ts';
10
+
11
+ /** Hono mounting through Void's shared island scheduler. */
8
12
  export function honoIslands(): Plugin {
9
13
  const root = process.cwd();
10
14
  const pagesDir = resolvePagesDir(root);
11
15
  const islands = new Map<string, string>();
12
16
  const pageFiles = new Set<string>();
17
+ const graph = new Map<string, Set<string>>();
18
+ const directlyInteractive = new Set<string>();
19
+ const interactivePages = new Set<string>();
20
+ const scriptOnlyPages = new Set<string>();
13
21
  let scan: PageScanResult | undefined;
22
+ let markdown: HonoMarkdownApi | undefined;
14
23
 
15
24
  function islandImports(code: string, filename: string) {
16
25
  const found = findIslandImports(code, filename);
17
26
  const attributed = parseSync(filename, code).program.body.filter((node) => node.type === 'ImportDeclaration' && node.attributes?.some(({ key }) => ('name' in key && key.name === 'island') || ('value' in key && key.value === 'island')));
18
- if (attributed.length !== found.length) throw new Error(`Hono islands: use one default import with exactly one island: 'load' attribute in ${filename}.`);
27
+ if (attributed.length !== found.length) throw new Error(`Hono islands: use one default import with exactly one island attribute using load, visible, idle or media:query in ${filename}.`);
19
28
  return found;
20
29
  }
21
30
 
22
31
  function imports(code: string, filename: string) {
23
32
  const found = islandImports(code, filename);
24
33
  for (const entry of found) {
25
- if (entry.hydrate !== 'load') throw new Error(`Hono islands: unsupported policy '${entry.hydrate}' in ${filename}; use island: 'load'.`);
34
+ if (!['load', 'visible', 'idle'].includes(entry.hydrate)) {
35
+ const query = entry.hydrate.startsWith('media:') ? entry.hydrate.slice(6).trim() : '';
36
+ let balance = 0;
37
+ for (const character of query) { if (character === '(') balance++; if (character === ')') balance--; if (balance < 0) break; }
38
+ if (!query || balance !== 0 || /[{};]/.test(query)) throw new Error(`Hono islands: unsupported policy or invalid media query '${entry.hydrate}' in ${filename}.`);
39
+ }
26
40
  if (!entry.specifier.startsWith('.')) throw new Error(`Hono islands: use a relative TSX/JSX component import in ${filename}.`);
27
- const path = resolveSpecifier(entry.specifier, join(filename, '..'), ['.tsx', '.jsx']);
41
+ const path = resolveComponent(entry.specifier, join(filename, '..'), ['.tsx', '.jsx']);
28
42
  if (!path || !/\.[jt]sx$/.test(path)) throw new Error(`Hono islands: cannot resolve TSX/JSX component ${entry.specifier} in ${filename}.`);
29
43
  const canonical = realpathSync(path);
30
44
  const id = normalizePath(relative(root, canonical));
@@ -35,21 +49,79 @@ export function honoIslands(): Plugin {
35
49
 
36
50
  async function rescan() {
37
51
  scan = await scanPages(root, { pagesDir });
38
- islands.clear();
39
- pageFiles.clear();
52
+ // Never pass Markdown text to the JavaScript static-graph parser.
53
+ for (const file of [...scan.pages.map(page => page.componentPath), ...scan.layouts.map(layout => layout.filePath), ...scan.namedLayouts.map(layout => layout.filePath)]) {
54
+ if (/\.md$/.test(file) && !markdown) throw new Error(`${markdownDiagnostic} ${file}`);
55
+ }
56
+ islands.clear(); pageFiles.clear(); graph.clear(); directlyInteractive.clear(); interactivePages.clear(); scriptOnlyPages.clear();
57
+ const visit = (filename: string) => {
58
+ const canonical = realpathSync(filename);
59
+ if (graph.has(canonical)) return;
60
+ const dependencies = new Set<string>(); graph.set(canonical, dependencies); pageFiles.add(canonical);
61
+ const code = canonical.endsWith('.md') && markdown ? markdown.getGraphSource(canonical) : readFileSync(canonical, 'utf8');
62
+ const found = imports(code, canonical);
63
+ if (found.length) {
64
+ if (!/\.[jt]sx$/.test(canonical) && !(canonical.endsWith('.md') && markdown)) throw new Error('Hono islands: attributed imports require a static TSX/JSX module.');
65
+ directlyInteractive.add(canonical);
66
+ }
67
+ for (const node of parseSync(canonical, code).program.body) {
68
+ const source = node.type === 'ImportDeclaration' ? node.source : node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration' ? node.source : undefined;
69
+ const typeOnly = node.type === 'ImportDeclaration' ? node.importKind === 'type' || (node.specifiers.length > 0 && node.specifiers.every((entry) => entry.type === 'ImportSpecifier' && entry.importKind === 'type')) : (node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') && node.exportKind === 'type';
70
+ if (!source || typeOnly || !source.value.startsWith('.') || found.some((entry) => entry.specifier === source.value)) continue;
71
+ const target = resolveComponent(source.value, join(canonical, '..'), ['.tsx', '.jsx', '.ts', '.js']);
72
+ if (!target || !/\.[jt]sx?$/.test(target)) continue;
73
+ dependencies.add(realpathSync(target)); visit(target);
74
+ }
75
+
76
+ };
77
+ for (const entry of [...scan.pages.filter((page) => page.island).map((page) => page.componentPath), ...scan.layouts.filter((layout) => layout.island).map((layout) => layout.filePath), ...scan.namedLayouts.filter((layout) => layout.island).map((layout) => layout.filePath)]) visit(join(pagesDir, entry));
78
+ const validateInteractive = (filename: string, seen = new Set<string>()) => {
79
+ const canonical = realpathSync(filename);
80
+ if (seen.has(canonical)) return; seen.add(canonical);
81
+ const code = canonical.endsWith('.md') && markdown ? markdown.getGraphSource(canonical) : readFileSync(canonical, 'utf8');
82
+ if (islandImports(code, canonical).length) throw new Error('Hono islands: attributed imports inside an interactive component are unsupported; use ordinary component imports there.');
83
+ for (const node of parseSync(canonical, code).program.body) {
84
+ const source = node.type === 'ImportDeclaration' ? node.source : node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration' ? node.source : undefined;
85
+ const typeOnly = node.type === 'ImportDeclaration' ? node.importKind === 'type' || (node.specifiers.length > 0 && node.specifiers.every((entry) => entry.type === 'ImportSpecifier' && entry.importKind === 'type')) : (node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') && node.exportKind === 'type';
86
+ if (!source || typeOnly || !source.value.startsWith('.')) continue;
87
+ const target = resolveComponent(source.value, join(canonical, '..'), ['.tsx', '.jsx', '.ts', '.js']);
88
+ if (target && /\.[jt]sx?$/.test(target)) validateInteractive(target, seen);
89
+ }
90
+ };
91
+ for (const target of islands.values()) validateInteractive(target);
92
+ for (const page of scan.pages.filter((page) => !page.island)) {
93
+ for (const file of [page.componentPath, ...resolveEffectiveLayoutChain(page, scan.layouts, scan.namedLayouts).map((layout) => layout.filePath)]) validateInteractive(join(pagesDir, file));
94
+ }
95
+ const needsJS = new Set(directlyInteractive);
96
+ let changed = true;
97
+ while (changed) {
98
+ changed = false;
99
+ for (const [file, dependencies] of graph) if (!needsJS.has(file) && [...dependencies].some((target) => needsJS.has(target))) { needsJS.add(file); changed = true; }
100
+ }
40
101
  for (const page of scan.pages.filter((page) => page.island)) {
41
- const filename = realpathSync(join(pagesDir, page.componentPath));
42
- pageFiles.add(filename);
43
- imports(readFileSync(filename, 'utf8'), filename);
102
+ const files = [page.componentPath, ...resolveEffectiveLayoutChain(page, scan.layouts, scan.namedLayouts).map((layout) => layout.filePath)];
103
+ const hasIslands = files.some(file => needsJS.has(realpathSync(join(pagesDir, file))));
104
+ const hasScript = markdown?.getClientScripts().has(page.componentId);
105
+ if (hasIslands || hasScript) interactivePages.add(page.componentId);
106
+ if (hasScript && !hasIslands) scriptOnlyPages.add(page.componentId);
44
107
  }
45
108
  }
46
109
 
47
110
  return {
48
111
  name: 'void-hono:islands',
49
112
  enforce: 'pre',
113
+ configResolved(config) { markdown = markdownApi(config.plugins); },
50
114
  buildStart: rescan,
51
- async hotUpdate({ file }) {
52
- if (normalizePath(file).startsWith(normalizePath(pagesDir) + '/')) await rescan();
115
+ async hotUpdate({ file, modules, timestamp }) {
116
+ const inPages = normalizePath(file).startsWith(normalizePath(pagesDir) + '/');
117
+ if (!inPages && ![...pageFiles, ...islands.values()].some((path) => normalizePath(path) === normalizePath(file))) return;
118
+ await rescan();
119
+ const invalidated = new Set<(typeof modules)[number]>();
120
+ for (const id of [ISLANDS_RENDERER_ID, ISLANDS_CLIENT_ENTRY_ID, 'virtual:void-pages-renderer']) {
121
+ const module = this.environment.moduleGraph.getModuleById(`\0${id}`);
122
+ if (module) this.environment.moduleGraph.invalidateModule(module, invalidated, timestamp, true);
123
+ }
124
+ if (!inPages && this.environment.config.consumer === 'client') this.environment.hot.send({ type: 'full-reload' });
53
125
  },
54
126
  config() {
55
127
  return { build: { rollupOptions: { input: { 'islands-client': ISLANDS_CLIENT_ENTRY_ID } } } };
@@ -61,42 +133,68 @@ export function honoIslands(): Plugin {
61
133
  const filename = id.split('?')[0];
62
134
  if (!/\.[jt]sx$/.test(filename) || filename.includes('/node_modules/')) return;
63
135
  const canonical = realpathSync(filename);
64
- if ((pageFiles.has(canonical) || [...islands.values()].includes(canonical)) && parseSync(filename, code).program.body.some((node) => node.type === 'ImportDeclaration' && node.source.value === 'void-plugin-hono-jsx/form')) {
65
- throw new Error('Hono islands: form helpers are unsupported; use a full Pages component.');
136
+ if ((pageFiles.has(canonical) || [...islands.values()].includes(canonical)) && parseSync(filename, code).program.body.some((node) => node.type === 'ImportDeclaration' && node.importKind !== 'type' && ['void-plugin-hono-jsx/form', 'void-plugin-hono-jsx/pages'].includes(node.source.value) && node.specifiers.some((entry) => entry.type !== 'ImportSpecifier' ? node.source.value.endsWith('/form') : entry.importKind !== 'type' && ['useForm', 'action', 'setActionRouter'].includes('name' in entry.imported ? entry.imported.name : entry.imported.value)))) {
137
+ throw new Error('Hono islands: useForm/action require full Pages; use named useIslandForm/Form imports for islands.');
66
138
  }
67
139
  const found = islandImports(code, filename);
68
140
  if (!found.length) return;
69
- if (!pageFiles.has(realpathSync(filename))) throw new Error('Hono islands: attributed imports are supported only directly in .island.tsx/.island.jsx pages.');
141
+ if (!pageFiles.has(canonical)) throw new Error('Hono islands: attributed imports require a static island page, layout or component graph; use ordinary imports inside interactive components.');
70
142
  imports(code, filename);
143
+ // Void requests static modules in the client environment to collect CSS.
144
+ // Actual interactive entry graphs were validated above; strip attributes
145
+ // for that traversal rather than generating server-only marker wrappers.
146
+ if (this.environment?.config.consumer === 'client') return editIslandSource(code, found.map((entry) => ({ start: entry.start, end: entry.end, text: `import ${entry.importName} from ${entry.specifierRaw};` })));
71
147
  return editIslandSource(code, found.map((entry) => {
72
- const path = resolveSpecifier(entry.specifier, join(filename, '..'), ['.tsx', '.jsx']);
148
+ const path = resolveComponent(entry.specifier, join(filename, '..'), ['.tsx', '.jsx']);
73
149
  if (!path) throw new Error(`Hono islands: missing component ${entry.specifier}.`);
74
150
  const islandId = normalizePath(relative(root, realpathSync(path)));
75
151
  const component = `__honoIslandComponent${entry.start}`;
76
152
  return { start: entry.start, end: entry.end, text: `import ${component} from ${entry.specifierRaw};
77
- const ${entry.importName} = (props) => __honoIslandJsx('div', { 'data-island': ${JSON.stringify(islandId)}, 'data-hydrate': 'load', 'data-props': JSON.stringify(props), children: __honoIslandJsx(${component}, props) });` };
153
+ const ${entry.importName} = (props) => __honoIslandJsx('div', { 'data-island': ${JSON.stringify(islandId)}, 'data-hydrate': ${JSON.stringify(entry.hydrate)}, 'data-props': JSON.stringify(props), children: __honoIslandJsx(${component}, props) });` };
78
154
  }), { prepend: "import { jsx as __honoIslandJsx } from 'hono/jsx/jsx-runtime';\n" });
79
155
  },
80
156
  load(id) {
81
157
  if (id === `\0${ISLANDS_RENDERER_ID}`) {
82
158
  if (!scan) throw new Error('Hono islands: page discovery has not completed.');
83
- const interactive = [...computePagesNeedingClientJS(scan, pagesDir, null)];
159
+ const interactive = [...new Set([...computePagesNeedingClientJS(scan, pagesDir, null), ...interactivePages])];
84
160
  return `import { renderPage } from 'virtual:void-pages-renderer';
85
161
  const interactive = new Set(${JSON.stringify(interactive)});
162
+ const scriptOnly = new Set(${JSON.stringify([...scriptOnlyPages])});
163
+ const markdownScripts = new Set(${JSON.stringify([...(markdown?.getClientScripts().keys() ?? [])])});
86
164
  export function renderIslandPage(page, assets) {
87
165
  if (page.deferred?.length || page.deferredKeys) throw new Error('Hono islands: deferred props are unsupported.');
88
- return renderPage(page, interactive.has(page.component) ? assets : { ...assets, preloads: '', body: '' });
166
+ if (!markdownScripts.has(page.component)) assets = { ...assets,
167
+ body: assets.body.replace(/<script\\b[^>]*src=["'][^"']*(?:void-hono-markdown-client|markdown-client-)[^"']*["'][^>]*>\\s*<\\/script>/g, ''),
168
+ preloads: assets.preloads.replace(/<link\\b[^>]*href=["'][^"']*markdown-client-[^"']*["'][^>]*>/g, ''),
169
+ };
170
+ if (!interactive.has(page.component)) return renderPage(page, { ...assets, preloads: '', body: '' });
171
+ if (scriptOnly.has(page.component)) {
172
+ // Keep the Markdown entry; its dynamic import loads only this page's code.
173
+ // Framework preloads and the island scheduler have no work on this chain.
174
+ const body = assets.body.replace(/<script\\b[^>]*src=["'][^"']*(?:void-islands-client|islands-client-)[^"']*["'][^>]*>\\s*<\\/script>/g, '');
175
+ return renderPage(page, { ...assets, preloads: '', body });
176
+ }
177
+ return renderPage(page, assets);
89
178
  }`;
90
179
  }
91
180
  if (id === `\0${ISLANDS_CLIENT_ENTRY_ID}`) {
92
- if (!islands.size) return 'export {};';
181
+ if (!islands.size || !interactivePages.size) return 'export {};';
93
182
  const loaders = [...islands].map(([name, path]) => `${JSON.stringify(name)}: () => import(${JSON.stringify(path)})`).join(',\n');
94
- return `import { initIslands } from 'void/pages-client';
183
+ return `${markdown ? "import { MarkdownContext } from 'void-plugin-hono-jsx/markdown';" : ''}
184
+ import { initIslands } from 'void/pages-client';
95
185
  import { jsx } from 'hono/jsx/dom/jsx-runtime';
96
186
  import { render } from 'hono/jsx/dom';
187
+ for (const element of document.querySelectorAll('[data-hydrate^="media:"]')) {
188
+ const query = element.getAttribute('data-hydrate').slice(6).trim();
189
+ if (matchMedia(query).media === 'not all' && query !== 'not all') throw new Error('Hono islands: invalid or unsupported media query ' + query);
190
+ }
97
191
  initIslands({ ${loaders} }, (element, module, props) => {
98
192
  if (typeof module.default !== 'function') throw new Error('Hono islands: a component must export a default function.');
99
- render(jsx(module.default, props), element);
193
+ let content = jsx(module.default, props);
194
+ ${markdown ? `const metadata = JSON.parse(document.getElementById('app')?.getAttribute('data-hono-markdown') || '{}');
195
+ const page = JSON.parse(document.getElementById('__VOID_PAGE_DATA__')?.textContent || '{}');
196
+ content = jsx(MarkdownContext.Provider, { value: { frontmatter: metadata.frontmatter ?? {}, headings: metadata.headings ?? [], props: page.props ?? {} }, children: content });` : ''}
197
+ render(content, element);
100
198
  element.setAttribute('data-hono-island', 'mounted');
101
199
  });`;
102
200
  }
@@ -0,0 +1,78 @@
1
+ import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';
2
+ import { join, relative, dirname } from 'node:path';
3
+ import { normalizePath, type Plugin } from 'vite';
4
+ import { resolvePagesDir } from 'void/pages';
5
+ import { resolveComponent } from './resolve-component.ts';
6
+ import { compileMarkdown, extractMarkdown, renderChunks } from './markdown-compiler.ts';
7
+ import type { HonoMarkdownApi } from './markdown-api.ts';
8
+
9
+ const clientId = 'virtual:void-hono-markdown-client';
10
+ const scriptQuery = '?hono-markdown-script';
11
+ const literal = (value: unknown) => `JSON.parse(${JSON.stringify(JSON.stringify(value))})`;
12
+
13
+ /** Optional authored Markdown compiler; independent of @void/md private files. */
14
+ export function voidHonoMarkdown(): Plugin {
15
+ let root = process.cwd();
16
+ let pagesDir = resolvePagesDir(root);
17
+ const scripts = new Map<string, string>();
18
+ const files = new Map<string, string>();
19
+ function rescan() {
20
+ scripts.clear(); files.clear();
21
+ if (!existsSync(pagesDir)) return;
22
+ for (const entry of readdirSync(pagesDir, { recursive: true, withFileTypes: true })) {
23
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
24
+ const file = join(entry.parentPath, entry.name);
25
+ const id = normalizePath(relative(pagesDir, file)).replace(/\.md$/, '').replace(/\.island$/, '').replace(/\/index$/, '').replace(/^index$/, '') || 'index';
26
+ const { clientCode } = extractMarkdown(readFileSync(file, 'utf8'));
27
+ if (clientCode) { scripts.set(id, clientCode); files.set(id, file); }
28
+ }
29
+ }
30
+ const api: HonoMarkdownApi = { adapter: 'hono', clientEntryId: clientId, getClientScripts() { rescan(); return scripts; }, getGraphSource(file) { return extractMarkdown(readFileSync(file, 'utf8')).graphSource; } };
31
+ return {
32
+ // Void's Markdown entry/asset convention is shared with @void/md.
33
+ name: 'void-md', enforce: 'pre', api,
34
+ config() { return { build: { rollupOptions: { input: { 'markdown-client': clientId } } } }; },
35
+ configResolved(config) { if (!config.plugins.some(plugin => plugin.name === 'void-hono:pages')) throw new Error('Hono Markdown: configure voidHonoJsx() before voidHonoMarkdown().'); root = realpathSync(config.root); pagesDir = resolvePagesDir(root); rescan(); },
36
+ buildStart() { rescan(); },
37
+ hotUpdate({ file, timestamp }) {
38
+ if (!file.endsWith('.md') || !normalizePath(file).startsWith(normalizePath(pagesDir) + '/')) return;
39
+ rescan();
40
+ const entry = this.environment.moduleGraph.getModuleById(`\0${clientId}`);
41
+ if (entry) this.environment.moduleGraph.invalidateModule(entry, new Set(), timestamp, true);
42
+ },
43
+ resolveId(id) { if (id === clientId) return `\0${id}`; },
44
+ load(id) {
45
+ if (id.endsWith(scriptQuery)) return extractMarkdown(readFileSync(id.slice(0, -scriptQuery.length), 'utf8')).clientCode || 'export {};';
46
+ if (id !== `\0${clientId}`) return;
47
+ rescan();
48
+ if (!files.size) return 'export {};';
49
+ return `const scripts = {${[...files].map(([id, file]) => `${JSON.stringify(id)}: () => import(${JSON.stringify(normalizePath(file) + scriptQuery)})`).join(',')}};
50
+ const id = document.getElementById('app')?.getAttribute('data-md-script');
51
+ if (id && scripts[id]) await scripts[id]();`;
52
+ },
53
+ transform: {
54
+ filter: { id: /\.md$/ },
55
+ handler(source, id) {
56
+ const result = compileMarkdown(source);
57
+ const imports = result.imports.map((entry, index) => {
58
+ const target = resolveComponent(entry.specifier, dirname(id), ['.tsx', '.jsx']);
59
+ if (!target || !entry.specifier.startsWith('.')) throw new Error(`Hono Markdown: cannot resolve relative TSX/JSX island ${entry.specifier} in ${id}.`);
60
+ return { ...entry, alias: `__island${index}`, islandId: normalizePath(relative(root, realpathSync(target))) };
61
+ });
62
+ const chunks = renderChunks(result.html, new Set(imports.map(entry => entry.importName)));
63
+ const children = chunks.map(chunk => {
64
+ if ('html' in chunk) return `raw(${JSON.stringify(chunk.html)})`;
65
+ const entry = imports.find(entry => entry.importName === chunk.name)!;
66
+ const props = literal(chunk.props);
67
+ return `jsx('div', { 'data-island': ${JSON.stringify(entry.islandId)}, 'data-hydrate': ${JSON.stringify(entry.hydrate)}, 'data-props': JSON.stringify(${props}), children: jsx(${entry.alias}, ${props}) })`;
68
+ });
69
+ return { code: `import { jsx, Fragment } from 'hono/jsx/jsx-runtime';
70
+ import { raw } from 'hono/html';
71
+ ${imports.map(entry => `import ${entry.alias} from ${JSON.stringify(entry.specifier)};`).join('\n')}
72
+ export const frontmatter = ${literal(result.frontmatter)};
73
+ export const headings = ${literal(result.headings)};
74
+ export default function MarkdownPage() { return jsx(Fragment, { children: [${children.join(',')}] }); }`, map: null };
75
+ },
76
+ },
77
+ };
78
+ }
@@ -1,32 +1,57 @@
1
1
  import { realpathSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { normalizePath } from 'vite';
4
- import { pagesRuntimePath, clientRuntimePath, publicRuntimeModules } from './runtime-paths.ts';
4
+ import { pagesRuntimePath, clientRuntimePath, markdownRuntimePath, publicRuntimeModules } from './runtime-paths.ts';
5
5
  import { generateComponentManifest, resolvePagesDir, resolveStaticPageDataBuildOptions, scanPages, type PageScanResult } from 'void/pages';
6
6
  import type { Plugin } from 'vite';
7
+ import { parseCacheFor, type CacheForInput } from 'void/pages-prefetch';
7
8
  import { honoJsx } from './hono-jsx.ts';
8
9
  import { honoIslands } from './hono-islands.ts';
10
+ import { markdownApi, markdownDiagnostic, type HonoMarkdownApi } from './markdown-api.ts';
9
11
  import { assertSupportedFrameworks } from './compatibility.ts';
10
12
 
11
13
  const rendererId = 'virtual:void-pages-renderer';
12
14
  const clientId = 'virtual:void-pages-client';
13
15
 
16
+ function assertComponentFile(file: string, kind: 'page' | 'layout', markdown = false) {
17
+ if (/\.md$/.test(file)) { if (markdown && kind === 'page') return; if (markdown) throw new Error('Hono Markdown: layouts must remain TSX/JSX: ' + file); throw new Error(`${markdownDiagnostic} ${kind}: ${file}`); }
18
+ if (!/\.[jt]sx$/.test(file)) throw new Error(`Hono Pages: only standard TSX/JSX ${kind}s are supported: ${file}.`);
19
+ }
20
+
21
+
22
+ export type HonoJsxOptions = {
23
+ viewTransitions?: boolean;
24
+ prefetch?: { hoverDelay?: number; cacheFor?: CacheForInput };
25
+ };
26
+
27
+ function clientOptions(options: HonoJsxOptions) {
28
+ const viewTransitions = options.viewTransitions ?? false;
29
+ const prefetchHoverDelay = options.prefetch?.hoverDelay ?? 75;
30
+ if (typeof viewTransitions !== 'boolean') throw new Error('Hono Pages: viewTransitions must be a boolean.');
31
+ if (!Number.isFinite(prefetchHoverDelay) || prefetchHoverDelay < 0) throw new Error('Hono Pages: prefetch.hoverDelay must be a finite non-negative number.');
32
+ const prefetchDefaultCacheFor = parseCacheFor(options.prefetch?.cacheFor ?? '30s');
33
+ if (![prefetchDefaultCacheFor.stale, prefetchDefaultCacheFor.expires].every(value => Number.isFinite(value) && value >= 0) || prefetchDefaultCacheFor.stale > prefetchDefaultCacheFor.expires) {
34
+ throw new Error('Hono Pages: prefetch.cacheFor must have finite non-negative durations, with stale <= expires.');
35
+ }
36
+ return { viewTransitions, prefetchHoverDelay, prefetchDefaultCacheFor };
37
+ }
38
+
14
39
  /** Experimental Pages contract verified against Void 0.26.1 and 0.27.0. */
15
- export function voidHonoPages(): Plugin[] {
40
+ export function voidHonoJsx(options: HonoJsxOptions = {}): Plugin[] {
41
+ const navigationOptions = clientOptions(options);
16
42
  const root = process.cwd();
17
43
  const pagesDir = resolvePagesDir(root);
18
44
  const staticData = resolveStaticPageDataBuildOptions(root);
19
45
  let scan: PageScanResult | undefined;
46
+ let markdown: HonoMarkdownApi | undefined;
20
47
 
21
48
  async function rescan() {
22
49
  scan = await scanPages(root, { pagesDir });
23
50
  for (const layout of [...scan.layouts, ...scan.namedLayouts]) {
24
- if (layout.island || !/\.[jt]sx$/.test(layout.filePath)) throw new Error('Hono Pages: only standard TSX/JSX layouts are supported.');
51
+ assertComponentFile(layout.filePath, 'layout');
25
52
  }
26
53
  for (const page of scan.pages) {
27
- if (!/\.[jt]sx$/.test(page.componentPath)) {
28
- throw new Error(`Hono Pages: only standard TSX/JSX pages are supported: ${page.componentPath}.`);
29
- }
54
+ assertComponentFile(page.componentPath, 'page', Boolean(markdown));
30
55
  if (staticData.enabled && page.methods.some((method) => method === 'action' || method === 'actions')) {
31
56
  throw new Error(`Hono Pages: actions require a server and cannot be used with static output: ${page.componentPath}.`);
32
57
  }
@@ -44,6 +69,12 @@ export function voidHonoPages(): Plugin[] {
44
69
  build: { manifest: true, rollupOptions: { input: { 'pages-client': clientId } } },
45
70
  };
46
71
  },
72
+ configResolved: {
73
+ order: 'pre',
74
+ handler(config) {
75
+ markdown = markdownApi(config.plugins);
76
+ },
77
+ },
47
78
  configEnvironment() {
48
79
  const modules = Object.keys(publicRuntimeModules);
49
80
  const packages = [...new Set(modules.map((id) => id.slice(0, id.lastIndexOf('/'))))];
@@ -75,7 +106,7 @@ export function voidHonoPages(): Plugin[] {
75
106
  },
76
107
  resolveId(id) {
77
108
  if (id === rendererId || id === clientId) return `\0${id}`;
78
- if (id === pagesRuntimePath || id === clientRuntimePath) return { id: realpathSync(id), external: false };
109
+ if (id === pagesRuntimePath || id === clientRuntimePath || id === markdownRuntimePath) return { id: realpathSync(id), external: false };
79
110
  const runtime = publicRuntimeModules[id];
80
111
  if (runtime) return { id: realpathSync(this.environment.config.consumer === 'client' ? runtime.client : runtime.server), external: false };
81
112
  },
@@ -84,7 +115,7 @@ export function voidHonoPages(): Plugin[] {
84
115
  const currentScan = scan ?? await rescan();
85
116
  const serverSnapshot = id === `\0${rendererId}` && this.environment.mode === 'dev';
86
117
  const manifestScan = id === `\0${clientId}`
87
- ? { ...currentScan, pages: currentScan.pages.filter((page) => !page.island) }
118
+ ? { ...currentScan, pages: currentScan.pages.filter((page) => !page.island), layouts: currentScan.layouts.filter((layout) => !layout.island), namedLayouts: currentScan.namedLayouts.filter((layout) => !layout.island) }
88
119
  : serverSnapshot ? { ...currentScan, pages: currentScan.pages.filter((page) => page.renderMode !== 'client') } : currentScan;
89
120
  let manifest = generateComponentManifest(manifestScan, pagesDir);
90
121
  if (serverSnapshot) {
@@ -102,10 +133,13 @@ export function voidHonoPages(): Plugin[] {
102
133
  }
103
134
  if (id === `\0${clientId}` && !currentScan.pages.some((page) => !page.island)) return 'export {};';
104
135
  if (id === `\0${rendererId}`) {
136
+ const markdownScriptIds = [...(markdown?.getClientScripts().keys() ?? [])];
105
137
  return `
106
138
  import { jsx } from 'hono/jsx';
107
- import { createSsrRouter } from 'void/pages-client';
108
- import { PageContext, RouterContext } from ${JSON.stringify(pagesRuntimePath)};
139
+ ${markdown ? `import { MarkdownContext } from ${JSON.stringify(markdownRuntimePath)};` : ''}
140
+ const markdownScriptIds = new Set(${JSON.stringify(markdownScriptIds)});
141
+ import { createSsrRouter, idleNavigationState } from 'void/pages-client';
142
+ import { PageContext, RouterContext, NavigationContext } from ${JSON.stringify(pagesRuntimePath)};
109
143
  import { serializePageData } from 'void/pages-server';
110
144
  import { renderHeadToString, renderHtmlAttrs, renderBodyAttrs } from 'void/pages-head';
111
145
  ${manifest}
@@ -113,7 +147,7 @@ export async function renderPage(pageObj, assetTags) {
113
147
  if (${JSON.stringify(staticData.enabled)} && pageObj.deferredKeys) throw new Error('Hono Pages: deferred props cannot be statically prerendered; remove defer().');
114
148
  const load = components[pageObj.component];
115
149
  if (!load) throw new Error('Hono Pages: unknown page component ' + pageObj.component);
116
- const { default: Page } = await load();
150
+ const { default: Page, frontmatter = {}, headings = [] } = await load();
117
151
  if (typeof Page !== 'function') throw new Error('Hono Pages: a page must export a component function.');
118
152
  let content = jsx(Page, pageObj.props);
119
153
  const ids = layoutTree[pageObj.component] ?? [];
@@ -122,21 +156,23 @@ export async function renderPage(pageObj, assetTags) {
122
156
  if (typeof Layout !== 'function') throw new Error('Hono Pages: a layout must export a component function.');
123
157
  content = jsx(Layout, { children: content });
124
158
  }
125
- const markup = await jsx(RouterContext.Provider, { value: createSsrRouter(pageObj.url, pageObj.params), children: jsx(PageContext.Provider, { value: pageObj, children: content }) }).toString();
159
+ ${markdown ? 'content = jsx(MarkdownContext.Provider, { value: { frontmatter, headings, props: pageObj.props }, children: content });' : ''}
160
+ const markup = await jsx(RouterContext.Provider, { value: createSsrRouter(pageObj.url, pageObj.params), children: jsx(PageContext.Provider, { value: pageObj, children: jsx(NavigationContext.Provider, { value: idleNavigationState(), children: content }) }) }).toString();
161
+ const appHtml = await jsx('div', { id: 'app', 'data-md-script': markdownScriptIds.has(pageObj.component) ? pageObj.component : undefined, ${markdown ? "'data-hono-markdown': JSON.stringify({ frontmatter, headings })," : ''} dangerouslySetInnerHTML: { __html: markup } }).toString();
126
162
  const head = pageObj.head;
127
163
  const body = pageObj.deferredKeys ? assetTags.body.replaceAll('<script type="module"', '<script async type="module"') : assetTags.body;
128
164
  return '<!doctype html><html' + (head ? renderHtmlAttrs(head) : '') + '><head>'
129
165
  + (head ? renderHeadToString(head) : '') + assetTags.css + assetTags.preloads
130
166
  + '</head><body' + (head ? renderBodyAttrs(head) : '') + '>'
131
167
  + '<script id="__VOID_PAGE_DATA__" type="application/json">' + serializePageData(pageObj) + '</script>'
132
- + '<div id="app">' + markup + '</div>' + body + '</body></html>';
168
+ + appHtml + body + '</body></html>';
133
169
  }
134
170
  `;
135
171
  }
136
172
  return `
137
173
  import { startHonoPages } from ${JSON.stringify(clientRuntimePath)};
138
174
  ${manifest}
139
- await startHonoPages({ components, layoutTree, routeMeta, matchRoute, staticPageData: ${JSON.stringify(staticData.enabled)}, staticPageDataFastPath: ${JSON.stringify(staticData.fastPath)} });
175
+ await startHonoPages({ components, layoutTree, routeMeta, matchRoute, staticPageData: ${JSON.stringify(staticData.enabled)}, staticPageDataFastPath: ${JSON.stringify(staticData.fastPath)} }, ${JSON.stringify(navigationOptions)});
140
176
  `;
141
177
  },
142
178
  }];
@@ -0,0 +1,17 @@
1
+ import type { Plugin } from 'vite';
2
+
3
+ export const markdownDiagnostic = 'Hono Pages: Markdown requires voidHonoMarkdown() from void-plugin-hono-jsx/markdown/plugin and its optional parser dependencies. See docs/markdown.md.';
4
+ export type HonoMarkdownApi = {
5
+ adapter: 'hono';
6
+ clientEntryId: string;
7
+ getClientScripts(): Map<string, string>;
8
+ getGraphSource(file: string): string;
9
+ };
10
+ export function markdownApi(plugins: readonly Plugin[]): HonoMarkdownApi | undefined {
11
+ const markdown = plugins.filter(plugin => plugin.name === 'void-md');
12
+ if (markdown.length > 1) throw new Error('Hono Pages: configure only one Markdown compiler.');
13
+ const plugin = markdown[0];
14
+ if (!plugin) return;
15
+ if (plugin.api?.adapter !== 'hono') throw new Error('Hono Pages: use voidHonoMarkdown(), not @void/md/plugin; the official compiler does not emit Hono components.');
16
+ return plugin.api as HonoMarkdownApi;
17
+ }
@@ -0,0 +1,77 @@
1
+ import MarkdownExit from 'markdown-exit';
2
+ import { parseSync } from 'vite';
3
+ import { parseDocument } from 'yaml';
4
+ import { findIslandImports, editIslandSource } from 'void/pages-islands-plugin';
5
+
6
+ const unescapeAttribute = new MarkdownExit().utils.unescapeAll;
7
+
8
+ export type Heading = { depth: number; slug: string; text: string };
9
+ export function extractMarkdown(source: string) {
10
+ const match = source.match(/^<script\b[^>]*>([\s\S]*?)<\/script>\s*\n?/);
11
+ const script = match?.[1] ?? '';
12
+ const body = match ? source.slice(match[0].length) : source;
13
+ const imports = findIslandImports(script, 'markdown-script.ts');
14
+ const attributed = parseSync('markdown-script.ts', script).program.body.filter(node => node.type === 'ImportDeclaration' && node.attributes?.some(({ key }) => ('name' in key && key.name === 'island') || ('value' in key && key.value === 'island')));
15
+ if (attributed.length !== imports.length) throw new Error('Hono Markdown: use one default component import with exactly one island attribute.');
16
+ const graphSource = imports.map(entry => script.slice(entry.start, entry.end)).join('\n');
17
+ const clientCode = editIslandSource(script, imports.map(entry => ({ start: entry.start, end: entry.end, text: '' }))).code.trim();
18
+ return { body, script, imports, graphSource, clientCode };
19
+ }
20
+
21
+ export function compileMarkdown(source: string) {
22
+ const extracted = extractMarkdown(source);
23
+ const match = extracted.body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
24
+ let frontmatter: Record<string, unknown> = {};
25
+ if (match) {
26
+ const yaml = parseDocument(match[1], { uniqueKeys: true });
27
+ if (yaml.errors.length) throw new Error('Hono Markdown: invalid frontmatter: ' + yaml.errors.map(error => error.message).join('; '));
28
+ const value: unknown = yaml.toJS({ maxAliasCount: 50 });
29
+ if (value !== null && (typeof value !== 'object' || Array.isArray(value))) throw new Error('Hono Markdown: frontmatter must be a mapping.');
30
+ frontmatter = (value ?? {}) as Record<string, unknown>;
31
+ try { JSON.stringify(frontmatter); } catch { throw new Error('Hono Markdown: frontmatter must be JSON serializable.'); }
32
+ }
33
+ const content = match ? extracted.body.slice(match[0].length) : extracted.body;
34
+ const compiler = new MarkdownExit({ html: true, linkify: true });
35
+ const tokens = compiler.parse(content, {});
36
+ const headings: Heading[] = [];
37
+ const slugs = new Map<string, number>();
38
+ const authoredHtml = tokens.flatMap(token => token.type === 'html_block' || token.type === 'html_inline' ? [token.content] : (token.children ?? []).filter(child => child.type === 'html_inline').map(child => child.content)).join('\n');
39
+ const usedIds = new Set([...authoredHtml.matchAll(/\bid\s*=\s*(?:"([^"]*)"|'([^']*)')/g)].map(match => match[1] ?? match[2]));
40
+ for (let index = 0; index < tokens.length; index++) {
41
+ if (tokens[index].type !== 'heading_open') continue;
42
+ const inline = tokens[index + 1];
43
+ const text = inline.children?.map(token => token.type === 'text' || token.type === 'code_inline' ? token.content : token.type === 'image' ? token.content : '').join('') ?? inline.content;
44
+ const base = text.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, '').trim().replace(/\s+/g, '-') || 'heading';
45
+ let count = slugs.get(base) ?? 0;
46
+ let slug = count ? `${base}-${count}` : base;
47
+ while (usedIds.has(slug)) { count++; slug = `${base}-${count}`; }
48
+ slugs.set(base, count + 1); usedIds.add(slug);
49
+ tokens[index].attrSet('id', slug);
50
+ headings.push({ depth: Number(tokens[index].tag.slice(1)), slug, text });
51
+ }
52
+ return { ...extracted, frontmatter, headings, html: compiler.renderer.render(tokens, compiler.options, {}) };
53
+ }
54
+
55
+ // Component tag splitting follows @void/md 0.27.0's emit-shared convention.
56
+ // Hono raw fragments preserve enclosing HTML across the component boundaries.
57
+ export function renderChunks(html: string, names: Set<string>) {
58
+ const tags = /<([A-Z]\w*)\s*((?:[^>/"']|"[^"]*"|'[^']*')*)\/\s*>|<([A-Z]\w*)\s*((?:[^>/"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\3\s*>/g;
59
+ const chunks: Array<{ html: string } | { name: string; props: Record<string, string | boolean> }> = [];
60
+ let cursor = 0;
61
+ for (const match of html.matchAll(tags)) {
62
+ const name = match[1] ?? match[3];
63
+ if (!names.has(name)) continue;
64
+ if (match[5]?.trim()) throw new Error('Hono Markdown: island tag children are unsupported; pass literal attributes to a self-closing tag.');
65
+ if (match.index > cursor) chunks.push({ html: html.slice(cursor, match.index) });
66
+ const props: Record<string, string | boolean> = Object.create(null);
67
+ const attrs = match[2] ?? match[4] ?? '';
68
+ const attrRe = /(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s]+)))?/g;
69
+ for (const attr of attrs.matchAll(attrRe)) {
70
+ const value = attr[2] ?? attr[3] ?? attr[4];
71
+ props[attr[1]] = value === undefined ? true : unescapeAttribute(value);
72
+ }
73
+ chunks.push({ name, props }); cursor = match.index + match[0].length;
74
+ }
75
+ if (cursor < html.length) chunks.push({ html: html.slice(cursor) });
76
+ return chunks;
77
+ }
@@ -0,0 +1,12 @@
1
+ import { statSync } from 'node:fs';
2
+ import { resolveSpecifier } from 'void/pages-islands-plugin';
3
+
4
+ /** Relative component imports also use Vite's directory/index convention. */
5
+ export function resolveComponent(specifier: string, importerDir: string, extensions: string[]) {
6
+ const target = resolveSpecifier(specifier, importerDir, extensions);
7
+ if (!target) return null;
8
+ if (statSync(target).isFile()) return target;
9
+ if (!statSync(target).isDirectory()) return null;
10
+ const index = resolveSpecifier('./index', target, extensions);
11
+ return index && statSync(index).isFile() ? index : null;
12
+ }
@@ -5,7 +5,9 @@ import { fileURLToPath } from 'node:url';
5
5
  const compiled = new URL(import.meta.url).pathname.endsWith('.js');
6
6
  export const pagesRuntimePath = fileURLToPath(new URL(compiled ? '../server/pages.js' : '../runtime/pages.tsx', import.meta.url));
7
7
  export const clientRuntimePath = fileURLToPath(new URL(compiled ? '../browser/client.js' : '../runtime/client.tsx', import.meta.url));
8
+ export const markdownRuntimePath = fileURLToPath(new URL(compiled ? '../server/markdown.js' : '../runtime/markdown.ts', import.meta.url));
8
9
  export const publicRuntimeModules: Record<string, { server: string; client: string }> = compiled ? {
10
+ 'void-plugin-hono-jsx/markdown': { server: markdownRuntimePath, client: fileURLToPath(new URL('../browser/markdown.js', import.meta.url)) },
9
11
  'void-plugin-hono-jsx/pages': { server: pagesRuntimePath, client: fileURLToPath(new URL('../browser/pages.js', import.meta.url)) },
10
12
  'void-plugin-hono-jsx/form': {
11
13
  server: fileURLToPath(new URL('../server/form.js', import.meta.url)),