hono-decks 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +221 -3
- package/README.md +241 -3
- package/dist/adapters/hono-markdown.d.ts +39 -0
- package/dist/adapters/hono-markdown.js +47 -0
- package/dist/advanced.d.ts +8 -1
- package/dist/advanced.js +165 -118
- package/dist/bin.js +356 -86
- package/dist/cli.js +356 -86
- package/dist/file-transaction-NjDxX6Gt.js +80 -0
- package/dist/honox.d.ts +21 -0
- package/dist/honox.js +47 -0
- package/dist/init-vite-EA_L4k2H.js +441 -0
- package/dist/mod.d.ts +8 -1
- package/dist/mod.js +7 -2
- package/dist/node.d.ts +121 -98
- package/dist/node.js +474 -157
- package/dist/vite.d.ts +4 -2
- package/dist/vite.js +441 -53
- package/package.json +15 -2
- package/skills/hono-decks/references/getting-started.md +40 -1
- package/skills/hono-decks/references/integrating.md +12 -0
- package/skills/hono-decks/references/styling.md +10 -0
- package/vite-env.d.ts +7 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
//#region src/node/path-utils.ts
|
|
4
|
+
function dirname$1(path) {
|
|
5
|
+
const normalized = normalizePath(path);
|
|
6
|
+
return normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
|
|
7
|
+
}
|
|
8
|
+
function normalizePath(path) {
|
|
9
|
+
return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
|
|
10
|
+
}
|
|
11
|
+
function normalizeDeckRoot(root) {
|
|
12
|
+
const normalized = normalizeRelativePath(root, "Deck root").replace(/\/$/, "");
|
|
13
|
+
if (normalized === ".") throw new Error("Deck root must be a relative path inside the current working directory");
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
function normalizeRelativePath(path, label) {
|
|
17
|
+
const normalized = normalizePath(path).replace(/\/$/, "");
|
|
18
|
+
const segments = normalized.split("/");
|
|
19
|
+
if (normalized === "" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.includes("..")) throw new Error(`${label} must be a relative path inside the current working directory`);
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/node/file-transaction.ts
|
|
24
|
+
/** Stage every file before publishing; restore the previous files on an I/O error.
|
|
25
|
+
* Individual renames are atomic. This is rollback on failure, not a cross-process
|
|
26
|
+
* or crash-atomic filesystem transaction. Failed rollback retains recovery files.
|
|
27
|
+
*/
|
|
28
|
+
async function publishGeneratedFiles(files, renameFile = rename) {
|
|
29
|
+
const staged = [];
|
|
30
|
+
const published = [];
|
|
31
|
+
let keepRecovery = false;
|
|
32
|
+
try {
|
|
33
|
+
if (new Set(files.map((file) => file.path)).size !== files.length) throw new Error("Generated output paths must be unique.");
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
const parent = dirname$1(file.path);
|
|
36
|
+
await mkdir(parent, { recursive: true });
|
|
37
|
+
const dir = await mkdtemp(join(parent, ".hono-decks-"));
|
|
38
|
+
const entry = {
|
|
39
|
+
path: file.path,
|
|
40
|
+
dir,
|
|
41
|
+
next: join(dir, "next")
|
|
42
|
+
};
|
|
43
|
+
staged.push(entry);
|
|
44
|
+
await writeFile(entry.next, file.content, "utf8");
|
|
45
|
+
let previous;
|
|
46
|
+
try {
|
|
47
|
+
previous = await readFile(file.path);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === "ENOENT") continue;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
entry.backup = join(dir, "previous");
|
|
53
|
+
await writeFile(entry.backup, previous);
|
|
54
|
+
}
|
|
55
|
+
for (const entry of staged) {
|
|
56
|
+
await renameFile(entry.next, entry.path);
|
|
57
|
+
published.push(entry);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const failures = [];
|
|
61
|
+
for (const entry of published.reverse()) try {
|
|
62
|
+
if (entry.backup) await renameFile(entry.backup, entry.path);
|
|
63
|
+
else await rm(entry.path, { force: true });
|
|
64
|
+
} catch (failure) {
|
|
65
|
+
failures.push(failure);
|
|
66
|
+
}
|
|
67
|
+
if (failures.length) {
|
|
68
|
+
keepRecovery = true;
|
|
69
|
+
throw new AggregateError([error, ...failures], `Generated output rollback failed. Recovery files remain in: ${staged.map((entry) => entry.dir).join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
} finally {
|
|
73
|
+
if (!keepRecovery) await Promise.allSettled(staged.map((entry) => rm(entry.dir, {
|
|
74
|
+
recursive: true,
|
|
75
|
+
force: true
|
|
76
|
+
})));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { normalizeRelativePath as i, normalizeDeckRoot as n, normalizePath as r, publishGeneratedFiles as t };
|
package/dist/honox.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
//#region src/honox.d.ts
|
|
3
|
+
/** Keep route discovery in the host's Vite graph; route composition lives here. */
|
|
4
|
+
interface HonoXDecksIntegration<R extends object> {
|
|
5
|
+
routes: Record<string, R>;
|
|
6
|
+
mountPath: string;
|
|
7
|
+
router: Hono<any>;
|
|
8
|
+
}
|
|
9
|
+
/** Preserve HonoX options and mount decks through its normal routing pipeline.
|
|
10
|
+
* Uses structural types so Hono-only consumers do not need to install HonoX.
|
|
11
|
+
*/
|
|
12
|
+
declare function withHonoXDecks<T extends {
|
|
13
|
+
root?: string;
|
|
14
|
+
ROUTES?: Record<string, object>;
|
|
15
|
+
}, R extends object>(options: T, input: HonoXDecksIntegration<R>): T & {
|
|
16
|
+
ROUTES: Record<string, R | NonNullable<T["ROUTES"]>[string] | {
|
|
17
|
+
default: Hono<any>;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { HonoXDecksIntegration, withHonoXDecks };
|
package/dist/honox.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/server/paths.ts
|
|
2
|
+
/** Normalizes a configured mount path to one leading slash and no trailing slash. */
|
|
3
|
+
function normalizeMountPath(value) {
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
if (!trimmed || trimmed === "/") return "/";
|
|
6
|
+
let start = 0;
|
|
7
|
+
let end = trimmed.length;
|
|
8
|
+
while (trimmed[start] === "/") start += 1;
|
|
9
|
+
while (end > start && trimmed[end - 1] === "/") end -= 1;
|
|
10
|
+
return `/${trimmed.slice(start, end)}`;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/honox.ts
|
|
14
|
+
/** Preserve HonoX options and mount decks through its normal routing pipeline.
|
|
15
|
+
* Uses structural types so Hono-only consumers do not need to install HonoX.
|
|
16
|
+
*/
|
|
17
|
+
function withHonoXDecks(options, input) {
|
|
18
|
+
const root = options.root ?? "/app/routes";
|
|
19
|
+
if (root !== "/app/routes") throw new Error("hono-decks: custom HonoX roots require a custom route integration.");
|
|
20
|
+
const routes = options.ROUTES ?? input.routes;
|
|
21
|
+
const mountPath = normalizeMountPath(input.mountPath);
|
|
22
|
+
const mount = mountPath === "/" ? "" : mountPath;
|
|
23
|
+
for (const [file, route] of Object.entries(routes)) {
|
|
24
|
+
const path = file.slice(root.length).replace(/\.[^.]+$/, "").replace(/\([^/]+\)/g, "").replace(/\/index$/, "").replace(/\/+/g, "/");
|
|
25
|
+
const value = route.default;
|
|
26
|
+
if (routesOverlap(path, mount, Boolean(value && typeof value === "object" && "fetch" in value))) throw new Error(`hono-decks: mountPath ${mountPath} conflicts with HonoX route ${file}`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...options,
|
|
30
|
+
ROUTES: {
|
|
31
|
+
...routes,
|
|
32
|
+
[root + mount + "/index.ts"]: { default: input.router }
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function routesOverlap(path, mount, isApp) {
|
|
37
|
+
const route = path.split("/").filter(Boolean);
|
|
38
|
+
const target = mount.split("/").filter(Boolean);
|
|
39
|
+
for (let index = 0; index < Math.min(route.length, target.length); index++) {
|
|
40
|
+
const part = route[index];
|
|
41
|
+
if (part.startsWith("[...")) return true;
|
|
42
|
+
if (!/^\[[^\]]+\]$/.test(part) && part !== target[index]) return false;
|
|
43
|
+
}
|
|
44
|
+
return route.length >= target.length || isApp || route.some((part) => part.startsWith("[..."));
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { withHonoXDecks };
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { t as publishGeneratedFiles } from "./file-transaction-NjDxX6Gt.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { parse } from "@babel/parser";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
//#region src/adapters/void.ts
|
|
10
|
+
/** Dependency policy of the Void preset, outside the deck compiler. */
|
|
11
|
+
const voidDependencies = [
|
|
12
|
+
["void", "void@>=1.1.11 <1.2.0"],
|
|
13
|
+
["vite", "vite@^8.0.16"],
|
|
14
|
+
["hono", "hono@^4.13.13"],
|
|
15
|
+
["markdown-exit", "markdown-exit@^1.3.0"],
|
|
16
|
+
["yaml", "yaml@^2.9.1"],
|
|
17
|
+
["markdown-it-container", "markdown-it-container@^4.0.0"],
|
|
18
|
+
["shiki", "shiki@^4.5.0"],
|
|
19
|
+
["@shikijs/transformers", "@shikijs/transformers@^4.5.0"]
|
|
20
|
+
];
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/node/install-void.ts
|
|
23
|
+
async function installVoidDependencies(cwd, options = {}) {
|
|
24
|
+
if (options.adapter?.startsWith("-")) throw new Error("--adapter must be a package specifier or tarball path.");
|
|
25
|
+
if (existsSync(join(cwd, ".pnp.cjs"))) throw new Error("--install requires the node_modules linker; install dependencies manually for Yarn PnP.");
|
|
26
|
+
const pkg = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
|
|
27
|
+
const declared = pkg.packageManager?.match(/^(npm|pnpm|yarn|bun)@(\d+)/);
|
|
28
|
+
if (pkg.packageManager && !declared) throw new Error("Unsupported packageManager; use npm, pnpm, yarn, or bun.");
|
|
29
|
+
const locks = [
|
|
30
|
+
["npm", "package-lock.json"],
|
|
31
|
+
["npm", "npm-shrinkwrap.json"],
|
|
32
|
+
["pnpm", "pnpm-lock.yaml"],
|
|
33
|
+
["yarn", "yarn.lock"],
|
|
34
|
+
["bun", "bun.lock"],
|
|
35
|
+
["bun", "bun.lockb"]
|
|
36
|
+
].filter(([, file]) => existsSync(join(cwd, file))).map(([manager]) => manager);
|
|
37
|
+
if (!declared && new Set(locks).size > 1) throw new Error("Multiple package-manager lockfiles found; set packageManager before --install.");
|
|
38
|
+
const manager = declared?.[1] ?? locks[0] ?? "npm";
|
|
39
|
+
const modernYarn = manager === "yarn" && (declared ? Number(declared[2]) >= 2 : (await readFile(join(cwd, "yarn.lock"), "utf8").catch(() => "")).includes("__metadata:"));
|
|
40
|
+
const require = createRequire(join(cwd, "package.json"));
|
|
41
|
+
const missing = (id) => {
|
|
42
|
+
try {
|
|
43
|
+
require.resolve(id);
|
|
44
|
+
return false;
|
|
45
|
+
} catch {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const required = voidDependencies;
|
|
50
|
+
for (const [id, spec] of required) {
|
|
51
|
+
if (missing(id)) continue;
|
|
52
|
+
const version = await installedVersion(require.resolve(id), id);
|
|
53
|
+
const minimum = spec.match(/(?:\^|>=)(\d+)\.(\d+)\.(\d+)/);
|
|
54
|
+
if (version) {
|
|
55
|
+
const value = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
56
|
+
const min = minimum.slice(1).map(Number);
|
|
57
|
+
const current = value?.slice(1).map(Number);
|
|
58
|
+
if (!current || current[0] !== min[0] || id === "void" && current[1] !== 1 || compare(current, min) < 0) throw new Error(`Installed ${id}@${version} is incompatible with ${spec}. Update it explicitly before --install.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const specs = required.filter(([id]) => missing(id)).map(([, spec]) => spec);
|
|
62
|
+
let packageDirectory = cwd;
|
|
63
|
+
let hasLocalDecks = false;
|
|
64
|
+
while (true) {
|
|
65
|
+
if (existsSync(join(packageDirectory, "node_modules/hono-decks/package.json"))) {
|
|
66
|
+
hasLocalDecks = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const parent = dirname(packageDirectory);
|
|
70
|
+
if (parent === packageDirectory) break;
|
|
71
|
+
packageDirectory = parent;
|
|
72
|
+
}
|
|
73
|
+
if (!hasLocalDecks) {
|
|
74
|
+
let ownPackage;
|
|
75
|
+
try {
|
|
76
|
+
ownPackage = await readFile(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
ownPackage = await readFile(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf8");
|
|
79
|
+
}
|
|
80
|
+
const requested = pkg.dependencies?.["hono-decks"] ?? pkg.devDependencies?.["hono-decks"] ?? JSON.parse(ownPackage).version;
|
|
81
|
+
specs.push(`hono-decks@${requested}`);
|
|
82
|
+
}
|
|
83
|
+
if (options.adapter || missing("void-plugin-hono-jsx/markdown/compiler")) {
|
|
84
|
+
let adapter = options.adapter ?? "void-plugin-hono-jsx@^0.5.0";
|
|
85
|
+
if (/^\d+\./.test(adapter)) adapter = "void-plugin-hono-jsx@" + adapter;
|
|
86
|
+
else if (modernYarn && (adapter.endsWith(".tgz") || adapter.startsWith("file:"))) adapter = "void-plugin-hono-jsx@file:" + resolve(cwd, adapter.replace(/^file:/, ""));
|
|
87
|
+
specs.push(adapter);
|
|
88
|
+
}
|
|
89
|
+
if (specs.length) {
|
|
90
|
+
const args = manager === "npm" ? [
|
|
91
|
+
"install",
|
|
92
|
+
"--ignore-scripts",
|
|
93
|
+
"--no-audit",
|
|
94
|
+
"--no-fund"
|
|
95
|
+
] : manager === "yarn" ? ["add", ...modernYarn ? ["--mode=skip-build"] : ["--ignore-scripts"]] : ["add", "--ignore-scripts"];
|
|
96
|
+
await (options.run ?? ((command, args, cwd) => new Promise((resolve, reject) => {
|
|
97
|
+
execFile(command, args, { cwd }, (error, _stdout, stderr) => error ? reject(/* @__PURE__ */ new Error(`Dependency installation failed: ${stderr || error.message}`)) : resolve());
|
|
98
|
+
})))(manager, [...args, ...specs], cwd);
|
|
99
|
+
}
|
|
100
|
+
const check = `import { createRequire } from "node:module";
|
|
101
|
+
import { pathToFileURL } from "node:url";
|
|
102
|
+
const require = createRequire(process.cwd() + "/package.json");
|
|
103
|
+
for (const id of ${JSON.stringify(required.map(([id]) => id))}) require.resolve(id);
|
|
104
|
+
const compiler = await import(pathToFileURL(require.resolve("void-plugin-hono-jsx/markdown/compiler")).href);
|
|
105
|
+
if (typeof compiler.compileMarkdownModule !== "function" || typeof compiler.readMarkdownModule !== "function") throw new Error("Missing module compiler API");`;
|
|
106
|
+
await new Promise((resolve, reject) => {
|
|
107
|
+
execFile(process.execPath, [
|
|
108
|
+
"--input-type=module",
|
|
109
|
+
"--eval",
|
|
110
|
+
check
|
|
111
|
+
], { cwd }, (error, _stdout, stderr) => {
|
|
112
|
+
if (error) reject(/* @__PURE__ */ new Error("The installed Hono Markdown adapter lacks the required module compiler API or peers. Use --adapter with a compatible version or tarball. Dependencies may have changed; app integration files were not written. " + stderr));
|
|
113
|
+
else resolve();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function installedVersion(entry, name) {
|
|
118
|
+
let directory = dirname(entry);
|
|
119
|
+
while (true) {
|
|
120
|
+
try {
|
|
121
|
+
const pkg = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
122
|
+
if (pkg.name === name) return pkg.version;
|
|
123
|
+
} catch {}
|
|
124
|
+
const parent = dirname(directory);
|
|
125
|
+
if (parent === directory) return void 0;
|
|
126
|
+
directory = parent;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function compare(a, b) {
|
|
130
|
+
for (let index = 0; index < 3; index++) if (a[index] !== b[index]) return a[index] - b[index];
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/adapters/vite-presets.ts
|
|
135
|
+
/** Initialization policies belong to the host integration, not the compiler. */
|
|
136
|
+
const vitePresets = {
|
|
137
|
+
void: { dependencies: [...voidDependencies.map(([id]) => id), "void-plugin-hono-jsx/markdown/compiler"] },
|
|
138
|
+
hono: {
|
|
139
|
+
dependencies: ["vite", "hono"],
|
|
140
|
+
clientEntryAssetPath: "/_assets/client"
|
|
141
|
+
},
|
|
142
|
+
honox: {
|
|
143
|
+
dependencies: [
|
|
144
|
+
"vite",
|
|
145
|
+
"hono",
|
|
146
|
+
"honox"
|
|
147
|
+
],
|
|
148
|
+
clientEntryAssetPath: "/_assets/client"
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/node/init-vite.ts
|
|
153
|
+
/** Configure an existing Vite app without changing its package scripts. */
|
|
154
|
+
async function initializeVite(cwd, framework, configFile = "hono-decks.config.ts", entry, installation) {
|
|
155
|
+
if (!configFile || configFile.startsWith("/") || configFile.includes("\\") || /^[A-Za-z]:/.test(configFile) || configFile.split("/").includes("..") || !/\.[cm]?[jt]s$/.test(configFile)) throw new Error("--config must be a JS/TS file inside the current working directory.");
|
|
156
|
+
const require = createRequire(join(cwd, "package.json"));
|
|
157
|
+
const dependencies = vitePresets[framework].dependencies;
|
|
158
|
+
for (const dependency of installation ? [] : dependencies) try {
|
|
159
|
+
require.resolve(dependency);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new Error(`${framework} init requires ${dependency}. Install the compatible dependencies first; no files were changed.`);
|
|
162
|
+
}
|
|
163
|
+
const candidates = [
|
|
164
|
+
"ts",
|
|
165
|
+
"mts",
|
|
166
|
+
"js",
|
|
167
|
+
"mjs",
|
|
168
|
+
"cts",
|
|
169
|
+
"cjs"
|
|
170
|
+
].map((ext) => `vite.config.${ext}`).filter((file) => existsSync(join(cwd, file)));
|
|
171
|
+
if (candidates.length !== 1) throw new Error("Expected one existing vite.config.{ts,mts,js,mjs,cts,cjs} in the app root. No files were changed.");
|
|
172
|
+
const viteFile = candidates[0];
|
|
173
|
+
const patched = connectViteConfig(await readFile(join(cwd, viteFile), "utf8"), configFile);
|
|
174
|
+
let sampleSlug = "welcome";
|
|
175
|
+
let suffix = 2;
|
|
176
|
+
while ([
|
|
177
|
+
sampleSlug,
|
|
178
|
+
`${sampleSlug}.md`,
|
|
179
|
+
`${sampleSlug}.mdx`
|
|
180
|
+
].some((path) => existsSync(join(cwd, "decks", path)))) sampleSlug = `welcome-${suffix++}`;
|
|
181
|
+
const files = [
|
|
182
|
+
{
|
|
183
|
+
path: configFile,
|
|
184
|
+
content: `import { defineDecksConfig } from "hono-decks";
|
|
185
|
+
|
|
186
|
+
export default defineDecksConfig({
|
|
187
|
+
mountPath: "/slides",
|
|
188
|
+
build: { root: "decks"${framework === "void" ? ", markdown: \"void\"" : ""} },${vitePresets[framework].clientEntryAssetPath ? `\n router: { clientEntryAssetPath: ${JSON.stringify(vitePresets[framework].clientEntryAssetPath)} },` : ""}
|
|
189
|
+
});
|
|
190
|
+
`
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
path: "src/decks.ts",
|
|
194
|
+
content: `/// <reference types="hono-decks/vite-env" />
|
|
195
|
+
/// <reference types="vite/client" />
|
|
196
|
+
import { decks } from "virtual:hono-decks";
|
|
197
|
+
|
|
198
|
+
export { decks };
|
|
199
|
+
`
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
path: "middleware/hono-decks.ts",
|
|
203
|
+
content: `import { Hono } from "hono";
|
|
204
|
+
import { raw } from "hono/html";
|
|
205
|
+
import { defineMiddleware } from "void";
|
|
206
|
+
import markdownStyle from "void-plugin-hono-jsx/theme-content.css?inline";
|
|
207
|
+
import { decks } from "../src/decks";
|
|
208
|
+
|
|
209
|
+
const app = new Hono();
|
|
210
|
+
const devClient = import.meta.env.DEV ? '<script type="module" src="' + import.meta.env.BASE_URL + '@vite/client"><\/script>' : "";
|
|
211
|
+
app.route(decks.mountPath, decks.router({
|
|
212
|
+
document: { head: raw(\`<style>\${markdownStyle}</style>\${devClient}\`) },
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
export default defineMiddleware(async (c, next) => {
|
|
216
|
+
const mount = decks.mountPath.replace(/\\/$/, "");
|
|
217
|
+
if (c.req.path === mount || c.req.path.startsWith(mount + "/")) {
|
|
218
|
+
return app.fetch(c.req.raw, c.env, c.executionCtx);
|
|
219
|
+
}
|
|
220
|
+
await next();
|
|
221
|
+
});
|
|
222
|
+
`
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
path: `decks/${sampleSlug}/deck.${framework === "void" ? "md" : "mdx"}`,
|
|
226
|
+
content: `${framework === "void" ? "<script>\nimport Counter from \"./Counter.tsx\" with { island: \"load\" };\n<\/script>\n" : ""}---
|
|
227
|
+
title: Welcome
|
|
228
|
+
---
|
|
229
|
+
# Welcome
|
|
230
|
+
|
|
231
|
+
Edit decks/${sampleSlug}/deck.${framework === "void" ? "md" : "mdx"} to write your slides.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
# Next slide
|
|
236
|
+
|
|
237
|
+
Existing .mdx decks work alongside Hono Markdown.
|
|
238
|
+
${framework === "void" ? "\n<Counter />\n" : ""}
|
|
239
|
+
`
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
if (framework === "void") files.push({
|
|
243
|
+
path: `decks/${sampleSlug}/Counter.tsx`,
|
|
244
|
+
content: `import { useState } from "hono/jsx";
|
|
245
|
+
|
|
246
|
+
export default function Counter() {
|
|
247
|
+
const [count, setCount] = useState(0);
|
|
248
|
+
return <button type="button" onClick={() => setCount(count + 1)}>Count: {count}</button>;
|
|
249
|
+
}
|
|
250
|
+
`
|
|
251
|
+
});
|
|
252
|
+
const edits = [];
|
|
253
|
+
if (framework !== "void") {
|
|
254
|
+
files.splice(2, 1);
|
|
255
|
+
files[1].content = files[1].content + `
|
|
256
|
+
import { raw } from "hono/html";
|
|
257
|
+
|
|
258
|
+
export const deckRouter = decks.router({
|
|
259
|
+
document: { head: raw(import.meta.env.DEV ? '<script type="module" src="' + import.meta.env.BASE_URL + '@vite/client"><\/script>' : "") },
|
|
260
|
+
});
|
|
261
|
+
`;
|
|
262
|
+
if (framework === "hono") {
|
|
263
|
+
const entryFile = selectHonoEntry(cwd, entry);
|
|
264
|
+
const appSource = await readFile(join(cwd, entryFile), "utf8");
|
|
265
|
+
edits.push({
|
|
266
|
+
path: entryFile,
|
|
267
|
+
content: connectHonoApp(appSource, relativeModule(entryFile, "src/decks.ts"))
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
if (!existsSync(join(cwd, "app/routes"))) throw new Error("HonoX init expects app/routes. Custom app directories require manual integration. No files were changed.");
|
|
271
|
+
for (const path of [
|
|
272
|
+
"app/routes/slides",
|
|
273
|
+
"app/routes/slides.ts",
|
|
274
|
+
"app/routes/slides.tsx",
|
|
275
|
+
"app/routes/slides.jsx",
|
|
276
|
+
"app/routes/slides.js"
|
|
277
|
+
]) if (existsSync(join(cwd, path))) throw new Error(`Refusing to replace existing HonoX route: ${path}`);
|
|
278
|
+
const serverFile = "app/server.ts";
|
|
279
|
+
const appSource = await readFile(join(cwd, serverFile), "utf8");
|
|
280
|
+
edits.push({
|
|
281
|
+
path: serverFile,
|
|
282
|
+
content: connectHonoXApp(appSource)
|
|
283
|
+
});
|
|
284
|
+
files.push({
|
|
285
|
+
path: "src/honox-decks.ts",
|
|
286
|
+
content: emitHonoXIntegration()
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const file of files) if (existsSync(join(cwd, file.path))) throw new Error(`Refusing to overwrite existing file: ${file.path}`);
|
|
291
|
+
if (new Set(files.map((file) => file.path)).size !== files.length || files.some((file) => file.path === viteFile)) throw new Error("--config conflicts with an integration file. No files were changed.");
|
|
292
|
+
if (installation) {
|
|
293
|
+
if (framework !== "void") throw new Error("--install is supported for Void only.");
|
|
294
|
+
await installVoidDependencies(cwd, installation);
|
|
295
|
+
}
|
|
296
|
+
await publishGeneratedFiles([...[...files, ...edits].map((file) => ({
|
|
297
|
+
...file,
|
|
298
|
+
path: join(cwd, file.path)
|
|
299
|
+
})), {
|
|
300
|
+
path: join(cwd, viteFile),
|
|
301
|
+
content: patched
|
|
302
|
+
}]);
|
|
303
|
+
return sampleSlug;
|
|
304
|
+
}
|
|
305
|
+
function emitHonoXIntegration() {
|
|
306
|
+
return `/// <reference types="vite/client" />
|
|
307
|
+
import type { Env } from "hono";
|
|
308
|
+
import type { ServerOptions } from "honox/server";
|
|
309
|
+
import { withHonoXDecks as connectHonoXDecks } from "hono-decks/honox";
|
|
310
|
+
import { decks, deckRouter } from "./decks";
|
|
311
|
+
|
|
312
|
+
// Same route discovery convention as HonoX. Keep this outside app/routes to
|
|
313
|
+
// avoid importing the integration itself through the route glob.
|
|
314
|
+
const fileRoutes = import.meta.glob([
|
|
315
|
+
"/app/routes/**/*.{ts,tsx,md,mdx}",
|
|
316
|
+
"/app/routes/.well-known/**/*.{ts,tsx,md,mdx}",
|
|
317
|
+
"!/app/routes/**/_*.{ts,tsx,md,mdx}",
|
|
318
|
+
"!/app/routes/**/-*.{ts,tsx,md,mdx}",
|
|
319
|
+
"!/app/routes/**/$*.{ts,tsx,md,mdx}",
|
|
320
|
+
"!/app/routes/**/*.test.{ts,tsx}",
|
|
321
|
+
"!/app/routes/**/*.spec.{ts,tsx}",
|
|
322
|
+
"!/app/routes/**/-*/**/*",
|
|
323
|
+
], { eager: true }) as NonNullable<ServerOptions["ROUTES"]>;
|
|
324
|
+
|
|
325
|
+
export function withHonoXDecks<E extends Env>(options: ServerOptions<E> = {}): ServerOptions<E> {
|
|
326
|
+
return connectHonoXDecks(options, { routes: fileRoutes, mountPath: decks.mountPath, router: deckRouter });
|
|
327
|
+
}
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
/** Preserve createApp options while adding a config-driven route to HonoX. */
|
|
331
|
+
function connectHonoXApp(source) {
|
|
332
|
+
const ast = parse(source, {
|
|
333
|
+
sourceType: "module",
|
|
334
|
+
plugins: ["typescript", "jsx"]
|
|
335
|
+
});
|
|
336
|
+
const unsupported = () => /* @__PURE__ */ new Error("Cannot safely connect this HonoX server. Wrap createApp options with withHonoXDecks from src/honox-decks. No files were changed.");
|
|
337
|
+
if (ast.program.interpreter) throw unsupported();
|
|
338
|
+
const constructors = ast.program.body.flatMap((node) => node.type === "ImportDeclaration" && node.source.value === "honox/server" ? node.specifiers.flatMap((item) => item.type === "ImportSpecifier" && item.imported.type === "Identifier" && item.imported.name === "createApp" ? [item.local.name] : []) : []);
|
|
339
|
+
const exported = ast.program.body.find((node) => node.type === "ExportDefaultDeclaration");
|
|
340
|
+
if (!exported || exported.type !== "ExportDefaultDeclaration" || exported.declaration.type !== "CallExpression" || exported.declaration.callee.type !== "Identifier" || !constructors.includes(exported.declaration.callee.name) || exported.declaration.arguments.length > 1) throw unsupported();
|
|
341
|
+
const call = exported.declaration;
|
|
342
|
+
const arg = call.arguments[0];
|
|
343
|
+
if (arg?.type === "SpreadElement" || call.end == null || call.callee.end == null) throw unsupported();
|
|
344
|
+
let alias = "withHonoXDecks";
|
|
345
|
+
while (source.includes(alias)) alias = "_" + alias;
|
|
346
|
+
const start = arg?.start ?? call.end - 1;
|
|
347
|
+
const end = arg?.end ?? start;
|
|
348
|
+
if (start == null || end == null) throw unsupported();
|
|
349
|
+
return `import { withHonoXDecks as ${alias} } from "../src/honox-decks";\n` + source.slice(0, start) + alias + "(" + source.slice(start, end) + ")" + source.slice(end);
|
|
350
|
+
}
|
|
351
|
+
/** Wrap only the default export expression, preserving app settings and comments. */
|
|
352
|
+
function connectViteConfig(source, configFile) {
|
|
353
|
+
const ast = parse(source, {
|
|
354
|
+
sourceType: "module",
|
|
355
|
+
plugins: ["typescript", "jsx"]
|
|
356
|
+
});
|
|
357
|
+
if (ast.program.interpreter) throw new Error("Vite config with a shebang requires manual integration. No files were changed.");
|
|
358
|
+
if (ast.program.body.some((node) => node.type === "ImportDeclaration" && node.source.value === "hono-decks/vite")) throw new Error("Vite config already imports hono-decks/vite. Keep one honoDecks() registration; no files were changed.");
|
|
359
|
+
const exported = ast.program.body.find((node) => node.type === "ExportDefaultDeclaration");
|
|
360
|
+
if (!exported || exported.type !== "ExportDefaultDeclaration" || exported.declaration.start == null || exported.declaration.end == null || exported.declaration.type === "FunctionDeclaration" || exported.declaration.type === "ClassDeclaration") throw new Error("Cannot safely wrap this Vite config. Add honoDecks() from hono-decks/vite to its plugins. No files were changed.");
|
|
361
|
+
let alias = "withHonoDecks";
|
|
362
|
+
while (source.includes(alias)) alias = "_" + alias;
|
|
363
|
+
const options = configFile === "hono-decks.config.ts" ? "" : `, { configFile: ${JSON.stringify(configFile)} }`;
|
|
364
|
+
const { start, end } = exported.declaration;
|
|
365
|
+
return `import { withHonoDecks as ${alias} } from "hono-decks/vite";\n` + source.slice(0, start) + `${alias}(` + source.slice(start, end) + options + ")" + source.slice(end);
|
|
366
|
+
}
|
|
367
|
+
function selectHonoEntry(cwd, entry) {
|
|
368
|
+
if (entry) {
|
|
369
|
+
if (entry.startsWith("/") || entry.includes("\\") || /^[A-Za-z]:/.test(entry) || entry.split("/").includes("..") || !/\.[cm]?[jt]sx?$/.test(entry)) throw new Error("--entry must be a JS/TS file inside the current working directory.");
|
|
370
|
+
return entry;
|
|
371
|
+
}
|
|
372
|
+
const candidates = [
|
|
373
|
+
"src/index.ts",
|
|
374
|
+
"src/index.tsx",
|
|
375
|
+
"src/index.js",
|
|
376
|
+
"src/index.mts",
|
|
377
|
+
"src/index.mjs",
|
|
378
|
+
"src/app.ts",
|
|
379
|
+
"src/app.tsx",
|
|
380
|
+
"src/app.js"
|
|
381
|
+
].filter((path) => existsSync(join(cwd, path)));
|
|
382
|
+
if (candidates.length !== 1) throw new Error("Cannot identify one Hono entry. Use --framework hono --entry path/to/app.ts. No files were changed.");
|
|
383
|
+
return candidates[0];
|
|
384
|
+
}
|
|
385
|
+
function relativeModule(from, to) {
|
|
386
|
+
const path = relative(from.includes("/") ? from.slice(0, from.lastIndexOf("/")) : ".", to).replaceAll("\\", "/").replace(/\.[cm]?[jt]sx?$/, "");
|
|
387
|
+
return path.startsWith(".") ? path : "./" + path;
|
|
388
|
+
}
|
|
389
|
+
/** Append after app middleware; refuse earlier routes that could consume /slides. */
|
|
390
|
+
function connectHonoApp(source, facadeImport) {
|
|
391
|
+
const ast = parse(source, {
|
|
392
|
+
sourceType: "module",
|
|
393
|
+
plugins: ["typescript", "jsx"]
|
|
394
|
+
});
|
|
395
|
+
const unsupported = () => /* @__PURE__ */ new Error("Cannot safely connect this Hono entry. Export a const app = new Hono(), or mount decks.router() manually before conflicting routes. No files were changed.");
|
|
396
|
+
if (ast.program.interpreter) throw unsupported();
|
|
397
|
+
const exported = ast.program.body.find((node) => node.type === "ExportDefaultDeclaration");
|
|
398
|
+
if (!exported || exported.type !== "ExportDefaultDeclaration" || exported.declaration.type !== "Identifier" || exported.start == null) throw unsupported();
|
|
399
|
+
if (ast.program.body.slice(ast.program.body.indexOf(exported) + 1).some((node) => node.type !== "TSInterfaceDeclaration" && node.type !== "TSTypeAliasDeclaration" && !(node.type === "ExportNamedDeclaration" && node.exportKind === "type"))) throw unsupported();
|
|
400
|
+
const appName = exported.declaration.name;
|
|
401
|
+
const constructors = ast.program.body.flatMap((node) => node.type === "ImportDeclaration" && node.source.value === "hono" ? node.specifiers.flatMap((item) => item.type === "ImportSpecifier" && item.imported.type === "Identifier" && item.imported.name === "Hono" ? [item.local.name] : []) : []);
|
|
402
|
+
const declaration = ast.program.body.find((node) => node.type === "VariableDeclaration" && node.kind === "const" && node.declarations.some((item) => item.id.type === "Identifier" && item.id.name === appName && item.init?.type === "NewExpression" && item.init.callee.type === "Identifier" && constructors.includes(item.init.callee.name)));
|
|
403
|
+
if (!declaration || declaration.end == null || declaration.end > exported.start) throw unsupported();
|
|
404
|
+
function checkRoutes(value) {
|
|
405
|
+
if (!value || typeof value !== "object") return;
|
|
406
|
+
const node = value;
|
|
407
|
+
if (node.type === "CallExpression") {
|
|
408
|
+
const call = value;
|
|
409
|
+
if (call.callee.type === "MemberExpression") {
|
|
410
|
+
let receiver = call.callee.object;
|
|
411
|
+
while (receiver?.type === "CallExpression") receiver = receiver.callee.object;
|
|
412
|
+
const property = call.callee.property;
|
|
413
|
+
const method = property?.name ?? (typeof property?.value === "string" ? property.value : void 0);
|
|
414
|
+
if (receiver?.type === "Identifier" && receiver.name === appName && method && [
|
|
415
|
+
"get",
|
|
416
|
+
"post",
|
|
417
|
+
"put",
|
|
418
|
+
"patch",
|
|
419
|
+
"delete",
|
|
420
|
+
"options",
|
|
421
|
+
"head",
|
|
422
|
+
"all",
|
|
423
|
+
"on",
|
|
424
|
+
"route"
|
|
425
|
+
].includes(method)) {
|
|
426
|
+
const path = call.arguments[method === "on" ? 1 : 0];
|
|
427
|
+
if (!path || path.type !== "StringLiteral" || !path.value || path.value.includes(":") || path.value.includes("{") || path.value === "*" || path.value === "/*" || method === "route" && path.value === "/" || path.value.startsWith("/slides") || path.value.includes("*") && "/slides/".startsWith(path.value.split("*")[0])) throw unsupported();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const child of Object.values(node)) if (Array.isArray(child)) child.forEach(checkRoutes);
|
|
432
|
+
else if (child && typeof child === "object") checkRoutes(child);
|
|
433
|
+
}
|
|
434
|
+
checkRoutes(ast.program);
|
|
435
|
+
let alias = "_honoDecks";
|
|
436
|
+
while (source.includes(alias)) alias = "_" + alias;
|
|
437
|
+
const routerAlias = alias + "Router";
|
|
438
|
+
return `import { decks as ${alias}, deckRouter as ${routerAlias} } from ${JSON.stringify(facadeImport)};\n` + source.slice(0, exported.start) + `${appName}.route(${alias}.mountPath, ${routerAlias});\n\n` + source.slice(exported.start);
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
export { initializeVite };
|
package/dist/mod.d.ts
CHANGED
|
@@ -130,6 +130,8 @@ interface CompiledDeck {
|
|
|
130
130
|
meta: DeckFrontmatter;
|
|
131
131
|
themeStyle?: string;
|
|
132
132
|
themeSourcePath?: string;
|
|
133
|
+
/** Omit generated client code for explicitly static compiled decks. */
|
|
134
|
+
clientEntryRequired?: boolean;
|
|
133
135
|
slides: CompiledSlide[];
|
|
134
136
|
assets: AssetRef[];
|
|
135
137
|
componentRegistry?: Record<string, SlideComponentInput>;
|
|
@@ -461,9 +463,14 @@ type DecksRouterOverrides<E extends Env = any> = Partial<Omit<DecksRouterOptions
|
|
|
461
463
|
type DecksRouterConfig<E extends Env = any> = Omit<DecksRouterOverrides<E>, "source">;
|
|
462
464
|
/** File-system settings consumed by the CLI. */
|
|
463
465
|
interface DecksBuildConfig {
|
|
466
|
+
/** Build-only Markdown adapter reference. "void" retains the compatible preset. */
|
|
467
|
+
markdown?: "void" | {
|
|
468
|
+
adapter: string;
|
|
469
|
+
options?: Record<string, unknown>;
|
|
470
|
+
};
|
|
464
471
|
/** Deck source directory. @default "decks" */
|
|
465
472
|
root?: string;
|
|
466
|
-
/**
|
|
473
|
+
/** CLI output directory. Vite keeps modules in memory. @default "src/generated" */
|
|
467
474
|
outDir?: string;
|
|
468
475
|
/** Optional deterministic Open Graph metadata cache. */
|
|
469
476
|
ogpCacheFile?: string;
|
package/dist/mod.js
CHANGED
|
@@ -567,6 +567,7 @@ function renderViewerScript(nonce) {
|
|
|
567
567
|
}
|
|
568
568
|
|
|
569
569
|
function handleKeydown(event) {
|
|
570
|
+
if (event.defaultPrevented) return;
|
|
570
571
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "p" && printPath) {
|
|
571
572
|
event.preventDefault();
|
|
572
573
|
const printUrl = new URL(printPath, window.location.href);
|
|
@@ -574,6 +575,10 @@ function renderViewerScript(nonce) {
|
|
|
574
575
|
window.location.assign(printUrl);
|
|
575
576
|
return;
|
|
576
577
|
}
|
|
578
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
579
|
+
const target = event.target;
|
|
580
|
+
if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input,textarea,select,button,a[href],[role=textbox],[role=button],[role=slider]"))) return;
|
|
581
|
+
if (["ArrowRight", "ArrowLeft", " "].includes(event.key)) event.preventDefault();
|
|
577
582
|
if (event.key === "ArrowRight" || event.key === " ") sendCommand("next");
|
|
578
583
|
if (event.key === "ArrowLeft") sendCommand("previous");
|
|
579
584
|
if (event.key === "f") void toggleViewerFullscreen();
|
|
@@ -629,7 +634,7 @@ const VIEWER_ASPECT_RATIO = "16/9";
|
|
|
629
634
|
function embeddedViewerStyle() {
|
|
630
635
|
const root = "[data-hono-decks-viewer][data-hono-decks-embed]";
|
|
631
636
|
return `${root}{color-scheme:dark;background:#050816;color:#eef2ff;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;display:grid;gap:12px;width:100%;min-width:0;box-sizing:border-box}
|
|
632
|
-
${root} *:focus-visible{outline:
|
|
637
|
+
${root} *:focus-visible{outline:2px solid currentColor;outline-offset:2px}
|
|
633
638
|
${root} .hono-decks-viewer-shell{position:relative;display:grid;grid-template-rows:minmax(0,1fr);place-items:center;width:100%;min-width:0;min-height:0;box-sizing:border-box}
|
|
634
639
|
${root} .hono-decks-viewer-stage{display:grid;place-items:center;width:100%;min-width:0;min-height:0;container-type:inline-size}
|
|
635
640
|
${root} .hono-decks-viewport{width:100%;aspect-ratio:${VIEWER_ASPECT_RATIO};position:relative;overflow:hidden;touch-action:pan-y}
|
|
@@ -642,7 +647,7 @@ ${root} .hono-decks-mobile-navigation-next{right:.25rem}
|
|
|
642
647
|
${root} .hono-decks-mobile-navigation:focus-visible{outline:2px solid currentColor;outline-offset:-4px}
|
|
643
648
|
${root} .hono-decks-mobile-navigation:active{background:rgba(15,23,42,.28);color:#eef2ff;opacity:1}
|
|
644
649
|
${root} .hono-decks-mobile-navigation .hono-decks-control-icon{width:22px;height:22px}
|
|
645
|
-
${root} .hono-decks-viewport>[data-hono-decks-position]{position:absolute;left:50%;bottom:8px;z-index:3;transform:translateX(-50%);border:0;background:
|
|
650
|
+
${root} .hono-decks-viewport>[data-hono-decks-position]{position:absolute;left:50%;bottom:8px;z-index:3;transform:translateX(-50%);border:0;border-radius:4px;background:rgba(15,23,42,.9);color:#eef2ff;padding:4px 6px;font:inherit;font-size:12px;line-height:1;opacity:1;pointer-events:none;white-space:nowrap}
|
|
646
651
|
${root} .hono-decks-viewer-controls{position:absolute;left:50%;bottom:max(.25rem,env(safe-area-inset-bottom,0));display:flex;gap:8px;align-items:center;justify-content:center;max-width:calc(100% - 1rem);min-width:0;z-index:4;transform:translateX(-50%)}
|
|
647
652
|
${root} .hono-decks-viewer-controls [data-hono-decks-navigation-control="previous"],${root} .hono-decks-viewer-controls [data-hono-decks-navigation-control="next"],${root} .hono-decks-viewer-controls [data-hono-decks-position]{position:absolute;visibility:hidden;pointer-events:none}
|
|
648
653
|
${root} .hono-decks-viewer-controls button,${root} .hono-decks-viewer-controls a,${root} .hono-decks-viewer-controls span{border:1px solid rgba(148,163,184,.32);border-radius:8px;background:rgba(15,23,42,.78);color:inherit;padding:8px 10px;font:inherit;font-size:14px}
|