void-plugin-hono-jsx 0.1.0-alpha.2 → 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.
- package/CHANGELOG.md +59 -0
- package/README.md +73 -20
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/dist/browser/client.d.ts +2 -1
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/client.js +76 -22
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/document-head.d.ts +1 -1
- package/dist/browser/document-head.d.ts.map +1 -1
- package/dist/browser/document-head.js +22 -1
- package/dist/browser/document-head.js.map +1 -1
- package/dist/browser/form-view.d.ts +10 -0
- package/dist/browser/form-view.d.ts.map +1 -0
- package/dist/browser/form-view.js +16 -0
- package/dist/browser/form-view.js.map +1 -0
- package/dist/browser/form.d.ts +28 -12
- package/dist/browser/form.d.ts.map +1 -1
- package/dist/browser/form.js +84 -23
- package/dist/browser/form.js.map +1 -1
- package/dist/browser/markdown.d.ts +15 -0
- package/dist/browser/markdown.d.ts.map +1 -0
- package/dist/browser/markdown.js +6 -0
- package/dist/browser/markdown.js.map +1 -0
- package/dist/browser/navigation.d.ts +13 -0
- package/dist/browser/navigation.d.ts.map +1 -0
- package/dist/browser/navigation.js +83 -0
- package/dist/browser/navigation.js.map +1 -0
- package/dist/browser/pages.d.ts +33 -3
- package/dist/browser/pages.d.ts.map +1 -1
- package/dist/browser/pages.js +76 -13
- package/dist/browser/pages.js.map +1 -1
- package/dist/plugins/compatibility.d.ts +12 -1
- package/dist/plugins/compatibility.d.ts.map +1 -1
- package/dist/plugins/compatibility.js +65 -22
- package/dist/plugins/compatibility.js.map +1 -1
- package/dist/plugins/hono-islands.d.ts +1 -1
- package/dist/plugins/hono-islands.d.ts.map +1 -1
- package/dist/plugins/hono-islands.js +156 -24
- package/dist/plugins/hono-islands.js.map +1 -1
- package/dist/plugins/hono-markdown.d.ts +4 -0
- package/dist/plugins/hono-markdown.d.ts.map +1 -0
- package/dist/plugins/hono-markdown.js +91 -0
- package/dist/plugins/hono-markdown.js.map +1 -0
- package/dist/plugins/hono-pages.d.ts +10 -2
- package/dist/plugins/hono-pages.d.ts.map +1 -1
- package/dist/plugins/hono-pages.js +55 -18
- package/dist/plugins/hono-pages.js.map +1 -1
- package/dist/plugins/markdown-api.d.ts +10 -0
- package/dist/plugins/markdown-api.d.ts.map +1 -0
- package/dist/plugins/markdown-api.js +13 -0
- package/dist/plugins/markdown-api.js.map +1 -0
- package/dist/plugins/markdown-compiler.d.ts +43 -0
- package/dist/plugins/markdown-compiler.d.ts.map +1 -0
- package/dist/plugins/markdown-compiler.js +91 -0
- package/dist/plugins/markdown-compiler.js.map +1 -0
- package/dist/plugins/resolve-component.d.ts +3 -0
- package/dist/plugins/resolve-component.d.ts.map +1 -0
- package/dist/plugins/resolve-component.js +15 -0
- package/dist/plugins/resolve-component.js.map +1 -0
- package/dist/plugins/runtime-paths.d.ts +1 -0
- package/dist/plugins/runtime-paths.d.ts.map +1 -1
- package/dist/plugins/runtime-paths.js +2 -0
- package/dist/plugins/runtime-paths.js.map +1 -1
- package/dist/server/form-view.d.ts +10 -0
- package/dist/server/form-view.d.ts.map +1 -0
- package/dist/server/form-view.js +16 -0
- package/dist/server/form-view.js.map +1 -0
- package/dist/server/form.d.ts +28 -12
- package/dist/server/form.d.ts.map +1 -1
- package/dist/server/form.js +84 -23
- package/dist/server/form.js.map +1 -1
- package/dist/server/markdown.d.ts +15 -0
- package/dist/server/markdown.d.ts.map +1 -0
- package/dist/server/markdown.js +6 -0
- package/dist/server/markdown.js.map +1 -0
- package/dist/server/pages.d.ts +33 -3
- package/dist/server/pages.d.ts.map +1 -1
- package/dist/server/pages.js +76 -13
- package/dist/server/pages.js.map +1 -1
- package/package.json +24 -6
- package/src/plugins/compatibility.ts +56 -19
- package/src/plugins/hono-islands.ts +121 -23
- package/src/plugins/hono-markdown.ts +78 -0
- package/src/plugins/hono-pages.ts +54 -17
- package/src/plugins/markdown-api.ts +17 -0
- package/src/plugins/markdown-compiler.ts +77 -0
- package/src/plugins/resolve-component.ts +12 -0
- package/src/plugins/runtime-paths.ts +2 -0
- package/src/runtime/client.tsx +66 -24
- package/src/runtime/document-head.ts +20 -1
- package/src/runtime/form-view.tsx +23 -0
- package/src/runtime/form.ts +82 -41
- package/src/runtime/markdown.ts +7 -0
- package/src/runtime/navigation.ts +72 -0
- package/src/runtime/pages.tsx +77 -15
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "void-plugin-hono-jsx",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Experimental Hono JSX Pages adapter for Void
|
|
5
|
+
"description": "Experimental Hono JSX Pages adapter for Void",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"engines": {
|
|
@@ -28,12 +28,22 @@
|
|
|
28
28
|
"./form": {
|
|
29
29
|
"types": "./dist/server/form.d.ts",
|
|
30
30
|
"default": "./dist/server/form.js"
|
|
31
|
+
},
|
|
32
|
+
"./markdown/plugin": {
|
|
33
|
+
"types": "./dist/plugins/hono-markdown.d.ts",
|
|
34
|
+
"import": "./dist/plugins/hono-markdown.js"
|
|
35
|
+
},
|
|
36
|
+
"./markdown": {
|
|
37
|
+
"types": "./dist/server/markdown.d.ts",
|
|
38
|
+
"default": "./dist/server/markdown.js"
|
|
31
39
|
}
|
|
32
40
|
},
|
|
33
41
|
"peerDependencies": {
|
|
34
|
-
"hono": "4.13.13",
|
|
35
|
-
"void": "0.26.1",
|
|
36
|
-
"vite": "8.
|
|
42
|
+
"hono": ">=4.13.13 <5.0.0",
|
|
43
|
+
"void": ">=0.26.1 <0.28.0",
|
|
44
|
+
"vite": ">=8.0.16 <9.0.0",
|
|
45
|
+
"markdown-exit": ">=1.3.0 <2.0.0",
|
|
46
|
+
"yaml": ">=2.9.1 <3.0.0"
|
|
37
47
|
},
|
|
38
48
|
"repository": {
|
|
39
49
|
"type": "git",
|
|
@@ -55,5 +65,13 @@
|
|
|
55
65
|
"jsx",
|
|
56
66
|
"vite",
|
|
57
67
|
"ssr"
|
|
58
|
-
]
|
|
68
|
+
],
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"markdown-exit": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"yaml": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
59
77
|
}
|
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { version as helperViteVersion } from 'vite';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
export const supportedFrameworkRanges = {
|
|
7
|
+
void: '>=0.26.1 <0.28.0',
|
|
8
|
+
hono: '>=4.13.13 <5.0.0',
|
|
9
|
+
vite: '>=8.0.16 <9.0.0',
|
|
10
|
+
};
|
|
11
|
+
export const supportedViteRange = supportedFrameworkRanges.vite;
|
|
12
|
+
|
|
13
|
+
// Only stable releases are admitted. Void is pre-1.0, so its next minor
|
|
14
|
+
// requires explicit validation; Hono and Vite use their current major lines.
|
|
15
|
+
export function supportsFramework(name: keyof typeof supportedFrameworkRanges, version: unknown): boolean {
|
|
16
|
+
if (typeof version !== 'string') return false;
|
|
17
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
18
|
+
if (!match) return false;
|
|
19
|
+
const [major, minor, patch] = match.slice(1).map(Number);
|
|
20
|
+
if (![major, minor, patch].every(Number.isSafeInteger)) return false;
|
|
21
|
+
if (name === 'void') return major === 0 && ((minor === 26 && patch >= 1) || minor === 27);
|
|
22
|
+
if (name === 'hono') return major === 4 && (minor > 13 || (minor === 13 && patch >= 13));
|
|
23
|
+
return major === 8 && (minor > 0 || patch >= 16);
|
|
24
|
+
}
|
|
25
|
+
export const supportsVite = (version: unknown): boolean => supportsFramework('vite', version);
|
|
26
|
+
|
|
27
|
+
type PackageInfo = { version: string; path: string };
|
|
28
|
+
function packageInfo(name: string, require: NodeRequire): PackageInfo {
|
|
29
|
+
let directory = dirname(require.resolve(name));
|
|
30
|
+
while (true) {
|
|
31
|
+
const path = join(directory, 'package.json');
|
|
32
|
+
if (existsSync(path)) {
|
|
33
|
+
const metadata: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
34
|
+
if (typeof metadata === 'object' && metadata !== null && 'name' in metadata && (metadata.name === name || (name === 'vite' && metadata.name === '@voidzero-dev/vite-plus-core'))) {
|
|
35
|
+
if (!('version' in metadata) || typeof metadata.version !== 'string') throw new Error(`Hono Pages: ${name} package version is missing (${path}).`);
|
|
36
|
+
return { version: metadata.version, path };
|
|
20
37
|
}
|
|
21
|
-
const parent = dirname(directory);
|
|
22
|
-
if (parent === directory) throw new Error(`Hono Pages: cannot locate ${name} package metadata.`);
|
|
23
|
-
directory = parent;
|
|
24
38
|
}
|
|
25
|
-
|
|
39
|
+
const parent = dirname(directory);
|
|
40
|
+
if (parent === directory) throw new Error(`Hono Pages: cannot locate ${name} package metadata from ${require.resolve(name)}.`);
|
|
41
|
+
directory = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function describe(name: string, require: NodeRequire): string {
|
|
45
|
+
try { const info = packageInfo(name, require); return `${info.path} (metadata ${info.version})`; }
|
|
46
|
+
catch { return `not resolved from ${require.resolve.paths(name)?.join(', ') ?? 'this context'}`; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function assertSupportedFrameworks({ root, viteVersion }: { root: string; viteVersion: unknown }) {
|
|
50
|
+
const adapter = createRequire(import.meta.url);
|
|
51
|
+
const project = createRequire(join(root, 'package.json'));
|
|
52
|
+
const diagnostic = () => `Running Vite from hook context: ${typeof viteVersion === 'string' ? viteVersion : 'unavailable'}. Adapter helper Vite: ${helperViteVersion}; package.json: ${describe('vite', adapter)}. Project Vite package.json: ${describe('vite', project)}. Package paths are diagnostics, not the running Vite identity.`;
|
|
53
|
+
if (!supportsVite(viteVersion)) {
|
|
54
|
+
throw new Error(`Hono Pages: running Vite ${typeof viteVersion === 'string' ? viteVersion : 'unavailable'} is unsupported; expected ${supportedViteRange}. ${diagnostic()} Update the CLI/launcher that executes Vite. For bundled launchers such as vp, installing project vite alone does not update the running Vite. For standard Vite, install vite@^8.3.4 and run vite directly.`);
|
|
55
|
+
}
|
|
56
|
+
if (!supportsVite(helperViteVersion)) {
|
|
57
|
+
throw new Error(`Hono Pages: adapter helper Vite ${helperViteVersion} is unsupported; expected ${supportedViteRange}. ${diagnostic()} Inspect the adapter's resolved peer copy; install vite@^8.3.4 and refresh the adapter's peer resolution/lockfile if a stale copy remains.`);
|
|
58
|
+
}
|
|
59
|
+
for (const name of ['void', 'hono'] as const) {
|
|
60
|
+
const expected = supportedFrameworkRanges[name];
|
|
61
|
+
const info = packageInfo(name, adapter);
|
|
62
|
+
if (!supportsFramework(name, info.version)) throw new Error(`Hono Pages: ${name} ${info.version} is unsupported; install ${name}@"${expected}". Resolved package.json: ${info.path}. Project package.json: ${describe(name, project)}. ${diagnostic()}`);
|
|
26
63
|
}
|
|
27
64
|
}
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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.
|
|
65
|
-
throw new Error('Hono islands:
|
|
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(
|
|
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 =
|
|
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':
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,31 +1,57 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { normalizePath } from 'vite';
|
|
3
|
-
import { pagesRuntimePath, clientRuntimePath, publicRuntimeModules } from './runtime-paths.ts';
|
|
4
|
+
import { pagesRuntimePath, clientRuntimePath, markdownRuntimePath, publicRuntimeModules } from './runtime-paths.ts';
|
|
4
5
|
import { generateComponentManifest, resolvePagesDir, resolveStaticPageDataBuildOptions, scanPages, type PageScanResult } from 'void/pages';
|
|
5
6
|
import type { Plugin } from 'vite';
|
|
7
|
+
import { parseCacheFor, type CacheForInput } from 'void/pages-prefetch';
|
|
6
8
|
import { honoJsx } from './hono-jsx.ts';
|
|
7
9
|
import { honoIslands } from './hono-islands.ts';
|
|
10
|
+
import { markdownApi, markdownDiagnostic, type HonoMarkdownApi } from './markdown-api.ts';
|
|
8
11
|
import { assertSupportedFrameworks } from './compatibility.ts';
|
|
9
12
|
|
|
10
13
|
const rendererId = 'virtual:void-pages-renderer';
|
|
11
14
|
const clientId = 'virtual:void-pages-client';
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
|
|
39
|
+
/** Experimental Pages contract verified against Void 0.26.1 and 0.27.0. */
|
|
40
|
+
export function voidHonoJsx(options: HonoJsxOptions = {}): Plugin[] {
|
|
41
|
+
const navigationOptions = clientOptions(options);
|
|
15
42
|
const root = process.cwd();
|
|
16
43
|
const pagesDir = resolvePagesDir(root);
|
|
17
44
|
const staticData = resolveStaticPageDataBuildOptions(root);
|
|
18
45
|
let scan: PageScanResult | undefined;
|
|
46
|
+
let markdown: HonoMarkdownApi | undefined;
|
|
19
47
|
|
|
20
48
|
async function rescan() {
|
|
21
49
|
scan = await scanPages(root, { pagesDir });
|
|
22
50
|
for (const layout of [...scan.layouts, ...scan.namedLayouts]) {
|
|
23
|
-
|
|
51
|
+
assertComponentFile(layout.filePath, 'layout');
|
|
24
52
|
}
|
|
25
53
|
for (const page of scan.pages) {
|
|
26
|
-
|
|
27
|
-
throw new Error(`Hono Pages: only standard TSX/JSX pages are supported: ${page.componentPath}.`);
|
|
28
|
-
}
|
|
54
|
+
assertComponentFile(page.componentPath, 'page', Boolean(markdown));
|
|
29
55
|
if (staticData.enabled && page.methods.some((method) => method === 'action' || method === 'actions')) {
|
|
30
56
|
throw new Error(`Hono Pages: actions require a server and cannot be used with static output: ${page.componentPath}.`);
|
|
31
57
|
}
|
|
@@ -36,13 +62,19 @@ export function voidHonoPages(): Plugin[] {
|
|
|
36
62
|
return [honoIslands(), honoJsx(), {
|
|
37
63
|
name: 'void-hono:pages',
|
|
38
64
|
enforce: 'pre',
|
|
39
|
-
config() {
|
|
40
|
-
assertSupportedFrameworks();
|
|
65
|
+
config(config) {
|
|
66
|
+
assertSupportedFrameworks({ root: resolve(config.root ?? root), viteVersion: this?.meta?.viteVersion });
|
|
41
67
|
return {
|
|
42
68
|
resolve: { dedupe: ['hono'] },
|
|
43
69
|
build: { manifest: true, rollupOptions: { input: { 'pages-client': clientId } } },
|
|
44
70
|
};
|
|
45
71
|
},
|
|
72
|
+
configResolved: {
|
|
73
|
+
order: 'pre',
|
|
74
|
+
handler(config) {
|
|
75
|
+
markdown = markdownApi(config.plugins);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
46
78
|
configEnvironment() {
|
|
47
79
|
const modules = Object.keys(publicRuntimeModules);
|
|
48
80
|
const packages = [...new Set(modules.map((id) => id.slice(0, id.lastIndexOf('/'))))];
|
|
@@ -74,7 +106,7 @@ export function voidHonoPages(): Plugin[] {
|
|
|
74
106
|
},
|
|
75
107
|
resolveId(id) {
|
|
76
108
|
if (id === rendererId || id === clientId) return `\0${id}`;
|
|
77
|
-
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 };
|
|
78
110
|
const runtime = publicRuntimeModules[id];
|
|
79
111
|
if (runtime) return { id: realpathSync(this.environment.config.consumer === 'client' ? runtime.client : runtime.server), external: false };
|
|
80
112
|
},
|
|
@@ -83,7 +115,7 @@ export function voidHonoPages(): Plugin[] {
|
|
|
83
115
|
const currentScan = scan ?? await rescan();
|
|
84
116
|
const serverSnapshot = id === `\0${rendererId}` && this.environment.mode === 'dev';
|
|
85
117
|
const manifestScan = id === `\0${clientId}`
|
|
86
|
-
? { ...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) }
|
|
87
119
|
: serverSnapshot ? { ...currentScan, pages: currentScan.pages.filter((page) => page.renderMode !== 'client') } : currentScan;
|
|
88
120
|
let manifest = generateComponentManifest(manifestScan, pagesDir);
|
|
89
121
|
if (serverSnapshot) {
|
|
@@ -101,10 +133,13 @@ export function voidHonoPages(): Plugin[] {
|
|
|
101
133
|
}
|
|
102
134
|
if (id === `\0${clientId}` && !currentScan.pages.some((page) => !page.island)) return 'export {};';
|
|
103
135
|
if (id === `\0${rendererId}`) {
|
|
136
|
+
const markdownScriptIds = [...(markdown?.getClientScripts().keys() ?? [])];
|
|
104
137
|
return `
|
|
105
138
|
import { jsx } from 'hono/jsx';
|
|
106
|
-
import {
|
|
107
|
-
|
|
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)};
|
|
108
143
|
import { serializePageData } from 'void/pages-server';
|
|
109
144
|
import { renderHeadToString, renderHtmlAttrs, renderBodyAttrs } from 'void/pages-head';
|
|
110
145
|
${manifest}
|
|
@@ -112,7 +147,7 @@ export async function renderPage(pageObj, assetTags) {
|
|
|
112
147
|
if (${JSON.stringify(staticData.enabled)} && pageObj.deferredKeys) throw new Error('Hono Pages: deferred props cannot be statically prerendered; remove defer().');
|
|
113
148
|
const load = components[pageObj.component];
|
|
114
149
|
if (!load) throw new Error('Hono Pages: unknown page component ' + pageObj.component);
|
|
115
|
-
const { default: Page } = await load();
|
|
150
|
+
const { default: Page, frontmatter = {}, headings = [] } = await load();
|
|
116
151
|
if (typeof Page !== 'function') throw new Error('Hono Pages: a page must export a component function.');
|
|
117
152
|
let content = jsx(Page, pageObj.props);
|
|
118
153
|
const ids = layoutTree[pageObj.component] ?? [];
|
|
@@ -121,21 +156,23 @@ export async function renderPage(pageObj, assetTags) {
|
|
|
121
156
|
if (typeof Layout !== 'function') throw new Error('Hono Pages: a layout must export a component function.');
|
|
122
157
|
content = jsx(Layout, { children: content });
|
|
123
158
|
}
|
|
124
|
-
|
|
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();
|
|
125
162
|
const head = pageObj.head;
|
|
126
163
|
const body = pageObj.deferredKeys ? assetTags.body.replaceAll('<script type="module"', '<script async type="module"') : assetTags.body;
|
|
127
164
|
return '<!doctype html><html' + (head ? renderHtmlAttrs(head) : '') + '><head>'
|
|
128
165
|
+ (head ? renderHeadToString(head) : '') + assetTags.css + assetTags.preloads
|
|
129
166
|
+ '</head><body' + (head ? renderBodyAttrs(head) : '') + '>'
|
|
130
167
|
+ '<script id="__VOID_PAGE_DATA__" type="application/json">' + serializePageData(pageObj) + '</script>'
|
|
131
|
-
+
|
|
168
|
+
+ appHtml + body + '</body></html>';
|
|
132
169
|
}
|
|
133
170
|
`;
|
|
134
171
|
}
|
|
135
172
|
return `
|
|
136
173
|
import { startHonoPages } from ${JSON.stringify(clientRuntimePath)};
|
|
137
174
|
${manifest}
|
|
138
|
-
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)});
|
|
139
176
|
`;
|
|
140
177
|
},
|
|
141
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
|
+
}
|