create-honovel 0.0.0-stage → 0.2.1

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 (39) hide show
  1. package/README.md +22 -2
  2. package/bin/create-honovel.mjs +107 -0
  3. package/bin/create-honovel.mjs.map +7 -0
  4. package/bin/create-honovel.mts +117 -0
  5. package/package.json +32 -4
  6. package/template/cloudflare/.vscode/launch.json.template +18 -0
  7. package/template/cloudflare/README.md +46 -0
  8. package/template/cloudflare/index.ts.template +7 -0
  9. package/template/cloudflare/package.json.template +47 -0
  10. package/template/cloudflare/src/database/main/MainDatabase.ts.template +10 -0
  11. package/template/cloudflare/src/database/main/drizzle.config.ts.template +7 -0
  12. package/template/cloudflare/src/database/main/migrations/.gitkeep +0 -0
  13. package/template/cloudflare/src/database/main/migrations/meta/_journal.json +6 -0
  14. package/template/cloudflare/src/database/main/schema/index.ts.template +2 -0
  15. package/template/cloudflare/src/env.ts.template +5 -0
  16. package/template/cloudflare/src/runtime/CloudflareQueue.ts.template +11 -0
  17. package/template/cloudflare/src/runtime/DurableObjectCache.ts.template +11 -0
  18. package/template/cloudflare/wrangler.jsonc +56 -0
  19. package/template/common/.gitignore.template +16 -0
  20. package/template/common/.vscode/settings.json.template +4 -0
  21. package/template/common/agents.md +33 -0
  22. package/template/common/env.example.template +3 -0
  23. package/template/common/src/database/README.md +27 -0
  24. package/template/common/src/decorators.ts.template +65 -0
  25. package/template/common/src/features/health/controllers/HealthController.ts.template +10 -0
  26. package/template/common/src/runtime/CacheLock.ts.template +4 -0
  27. package/template/common/src/runtime/Controller.ts.template +4 -0
  28. package/template/common/tsconfig.build.json.template +5 -0
  29. package/template/common/tsconfig.json.template +12 -0
  30. package/template/nuxt/README.md +103 -0
  31. package/template/nuxt/frontend/app/app.vue.template +20 -0
  32. package/template/nuxt/frontend/app/assets/css/main.css.template +38 -0
  33. package/template/nuxt/frontend/app/pages/about.vue.template +39 -0
  34. package/template/nuxt/frontend/app/pages/index.vue.template +90 -0
  35. package/template/nuxt/frontend/nuxt.config.ts.template +22 -0
  36. package/template/nuxt/frontend/public/honovel-icon.png +0 -0
  37. package/template/nuxt/frontend/tsconfig.json.template +9 -0
  38. package/template/nuxt/package.json.template +31 -0
  39. package/template/nuxt/wrangler.jsonc +11 -0
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # Create Honovel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The default distribution is public on npmjs and requires no installation login.
4
+
5
+ Create a Cloudflare Workers application with an optional Nuxt frontend:
6
+
7
+ ```sh
8
+ npm create honovel@latest my-app
9
+ npm create honovel@latest my-web-app -- --frontend=nuxt
10
+ npm create honovel@latest my-api -- --api-only
11
+ ```
12
+
13
+ The backend is always Cloudflare Workers. Without an explicit option, the installer asks whether to include Nuxt or create an API-only project. Nuxt is the default in non-interactive environments. `--frontend=none` is equivalent to `--api-only`; both `--frontend=value` and `--frontend value` work. Use `--no-install` to scaffold without installing dependencies. Existing directories are never overwritten.
14
+
15
+ The explicit initializer is `npx create-honovel@latest my-app --frontend=nuxt`. The previous `--template=node|cloudflare` runtime selection has been removed.
16
+
17
+ Both project modes include a health endpoint at `/api/health`, the application-level `/api` controller base, an empty D1 database schema, Durable Object cache/cache-backed locks, a Cloudflare Queue adapter and generated Cron Trigger support. No Todo table or initial SQL migration is created. Shared compiler/dev/build helpers ship in `honovel`, without a copied `scripts/` directory.
18
+
19
+ With Nuxt, `npm run dev` starts the backend and frontend; open `http://localhost:5173`. Nuxt proxies API and documentation requests to Wrangler. `npm run build` generates the static frontend into `dist/frontend` and bundles the Worker into `dist/worker`; deployment attaches Workers Static Assets.
20
+
21
+ For API-only projects, `npm run dev` starts only Wrangler at `http://localhost:8788`. There is no `frontend/` directory, Nuxt/Vue/Tailwind dependency, frontend npm script or static assets configuration. Build, typecheck and deployment only process the backend.
22
+
23
+ Releases publish `honovel` and `create-honovel` to the public npm registry, linked to [Honovel/framework](https://github.com/Honovel/framework). The framework includes its Cloudflare adapters under `honovel/cloudflare`.
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawnSync } from "node:child_process";
6
+ import readline from "node:readline/promises";
7
+ const packageRoot = fileURLToPath(new URL("../", import.meta.url));
8
+ const metadata = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
9
+ async function main() {
10
+ const args = process.argv.slice(2);
11
+ if (!args.length || args.includes("--help") || args.includes("-h")) {
12
+ console.log("Usage: npm create honovel@latest <project-name> [--frontend=nuxt|none] [--api-only] [--no-install]\nThe backend is always Cloudflare Workers. If no frontend option is given, the installer asks whether to include Nuxt (the non-interactive default).");
13
+ return;
14
+ }
15
+ const [name, ...options] = args;
16
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) throw new Error("Use a project name containing lowercase letters, digits, hyphens, or underscores.");
17
+ let frontend = "nuxt";
18
+ let frontendSelected = false;
19
+ let install = true;
20
+ for (let index = 0; index < options.length; index++) {
21
+ const option = options[index];
22
+ if (option === "--no-install") {
23
+ install = false;
24
+ } else if (option === "--api-only" || option === "--frontend" || option.startsWith("--frontend=")) {
25
+ if (frontendSelected) throw new Error("Select the frontend only once; do not combine --api-only and --frontend.");
26
+ frontendSelected = true;
27
+ frontend = option === "--api-only" ? "none" : option === "--frontend" ? options[++index] : option.slice("--frontend=".length);
28
+ if (!["nuxt", "none"].includes(frontend)) throw new Error("Supported frontend options: nuxt, none.");
29
+ } else if (option === "--template" || option.startsWith("--template=")) {
30
+ throw new Error("All projects use Cloudflare Workers. Use --frontend=nuxt, --frontend=none or --api-only.");
31
+ } else throw new Error("Unknown option. Supported: --frontend=nuxt|none, --api-only, --no-install");
32
+ }
33
+ if (!frontendSelected) frontend = await chooseFrontend();
34
+ if (name.length > 63) throw new Error("Cloudflare project names must be at most 63 characters long.");
35
+ const target = path.resolve(name);
36
+ if (fs.existsSync(target)) throw new Error(`${name} already exists. Choose a new directory.`);
37
+ fs.mkdirSync(target);
38
+ function copy(directory, destination) {
39
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
40
+ const source = path.join(directory, entry.name);
41
+ const outputName = entry.name === "env.example.template" ? ".env.example" : entry.name.replace(/\.template$/, "");
42
+ const output = path.join(destination, outputName);
43
+ if (entry.isDirectory()) {
44
+ fs.mkdirSync(output, { recursive: true });
45
+ copy(source, output);
46
+ } else {
47
+ if (/\.(png|jpe?g|gif|webp|ico|woff2?)$/i.test(entry.name)) {
48
+ fs.copyFileSync(source, output);
49
+ continue;
50
+ }
51
+ const content = fs.readFileSync(source, "utf8").replaceAll("__PROJECT_NAME__", name).replaceAll("__WORKER_NAME__", name.replaceAll("_", "-")).replaceAll("__HONOVEL_VERSION__", metadata.version);
52
+ if (fs.existsSync(output) && ["package.json", "wrangler.jsonc"].includes(outputName)) {
53
+ const current = JSON.parse(fs.readFileSync(output, "utf8"));
54
+ const overlay = JSON.parse(content);
55
+ const merged = { ...current, ...overlay };
56
+ if (outputName === "package.json") {
57
+ for (const section of ["scripts", "dependencies", "devDependencies", "overrides"]) {
58
+ merged[section] = { ...current[section], ...overlay[section] };
59
+ }
60
+ }
61
+ fs.writeFileSync(output, JSON.stringify(merged, null, 2) + "\n");
62
+ } else fs.writeFileSync(output, content);
63
+ }
64
+ }
65
+ }
66
+ copy(path.join(packageRoot, "template/common"), target);
67
+ copy(path.join(packageRoot, "template/cloudflare"), target);
68
+ if (frontend === "nuxt") copy(path.join(packageRoot, "template/nuxt"), target);
69
+ const envExamplePath = path.join(target, ".env.example");
70
+ const envPath = path.join(target, ".env");
71
+ if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
72
+ const localEnv = fs.readFileSync(envExamplePath, "utf8").replace(/^APP_DEBUG=.*$/m, "APP_DEBUG=true");
73
+ fs.writeFileSync(envPath, localEnv, { flag: "wx" });
74
+ }
75
+ if (install) {
76
+ const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install", "--loglevel=error"], { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
77
+ if (result.error || result.status !== 0) {
78
+ throw new Error(`Project created at ${target}, but npm install failed. Check the npm output above and run npm install there to retry.`);
79
+ }
80
+ }
81
+ console.log(`Created ${target} (Cloudflare Workers, ${frontend === "nuxt" ? "Nuxt frontend" : "API-only"})
82
+
83
+ Next:
84
+ cd ${name}
85
+ ${install ? "" : " npm install\n"} npm run dev`);
86
+ }
87
+ async function chooseFrontend() {
88
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
89
+ console.log("[Honovel] No interactive terminal detected; including Nuxt. Pass --api-only or --frontend=none for an API-only project.");
90
+ return "nuxt";
91
+ }
92
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
93
+ try {
94
+ console.log("\nCloudflare Workers backend \u2014 choose your frontend:\n\n 1) Nuxt frontend (recommended)\n 2) API-only (no frontend)\n");
95
+ const answer = await prompt.question("Select a project setup [1]: ");
96
+ const selected = answer.trim().toLowerCase();
97
+ if (!selected || selected === "1" || selected === "nuxt") return "nuxt";
98
+ if (selected === "2" || selected === "none" || selected === "api-only") return "none";
99
+ throw new Error("Choose 1 for Nuxt or 2 for API-only.");
100
+ } finally {
101
+ prompt.close();
102
+ }
103
+ }
104
+ main().catch((error) => {
105
+ console.error(`[Honovel] ${error.message}`);
106
+ process.exitCode = 1;
107
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["create-honovel.mts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { spawnSync } from 'node:child_process'\nimport readline from 'node:readline/promises'\n\nconst packageRoot = fileURLToPath(new URL('../', import.meta.url))\nconst metadata = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))\n\nasync function main() {\n const args = process.argv.slice(2)\n if (!args.length || args.includes('--help') || args.includes('-h')) {\n console.log('Usage: npm create honovel@latest <project-name> [--frontend=nuxt|none] [--api-only] [--no-install]\\nThe backend is always Cloudflare Workers. If no frontend option is given, the installer asks whether to include Nuxt (the non-interactive default).')\n return\n }\n const [name, ...options] = args\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) throw new Error('Use a project name containing lowercase letters, digits, hyphens, or underscores.')\n let frontend = 'nuxt'\n let frontendSelected = false\n let install = true\n for (let index = 0; index < options.length; index++) {\n const option = options[index]\n if (option === '--no-install') {\n install = false\n } else if (option === '--api-only' || option === '--frontend' || option.startsWith('--frontend=')) {\n if (frontendSelected) throw new Error('Select the frontend only once; do not combine --api-only and --frontend.')\n frontendSelected = true\n frontend = option === '--api-only' ? 'none'\n : option === '--frontend' ? options[++index] : option.slice('--frontend='.length)\n if (!['nuxt', 'none'].includes(frontend)) throw new Error('Supported frontend options: nuxt, none.')\n } else if (option === '--template' || option.startsWith('--template=')) {\n throw new Error('All projects use Cloudflare Workers. Use --frontend=nuxt, --frontend=none or --api-only.')\n } else throw new Error('Unknown option. Supported: --frontend=nuxt|none, --api-only, --no-install')\n }\n if (!frontendSelected) frontend = await chooseFrontend()\n if (name.length > 63) throw new Error('Cloudflare project names must be at most 63 characters long.')\n const target = path.resolve(name)\n if (fs.existsSync(target)) throw new Error(`${name} already exists. Choose a new directory.`)\n fs.mkdirSync(target)\n function copy(directory, destination) {\n for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {\n const source = path.join(directory, entry.name)\n const outputName = entry.name === 'env.example.template'\n ? '.env.example'\n : entry.name.replace(/\\.template$/, '')\n const output = path.join(destination, outputName)\n if (entry.isDirectory()) {\n fs.mkdirSync(output, { recursive: true })\n copy(source, output)\n } else {\n if (/\\.(png|jpe?g|gif|webp|ico|woff2?)$/i.test(entry.name)) {\n fs.copyFileSync(source, output)\n continue\n }\n const content = fs.readFileSync(source, 'utf8')\n .replaceAll('__PROJECT_NAME__', name)\n .replaceAll('__WORKER_NAME__', name.replaceAll('_', '-'))\n .replaceAll('__HONOVEL_VERSION__', metadata.version)\n if (fs.existsSync(output) && ['package.json', 'wrangler.jsonc'].includes(outputName)) {\n const current = JSON.parse(fs.readFileSync(output, 'utf8'))\n const overlay = JSON.parse(content)\n const merged = { ...current, ...overlay }\n if (outputName === 'package.json') {\n for (const section of ['scripts', 'dependencies', 'devDependencies', 'overrides']) {\n merged[section] = { ...current[section], ...overlay[section] }\n }\n }\n fs.writeFileSync(output, JSON.stringify(merged, null, 2) + '\\n')\n } else fs.writeFileSync(output, content)\n }\n }\n }\n copy(path.join(packageRoot, 'template/common'), target)\n copy(path.join(packageRoot, 'template/cloudflare'), target)\n if (frontend === 'nuxt') copy(path.join(packageRoot, 'template/nuxt'), target)\n const envExamplePath = path.join(target, '.env.example')\n const envPath = path.join(target, '.env')\n if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {\n const localEnv = fs.readFileSync(envExamplePath, 'utf8')\n .replace(/^APP_DEBUG=.*$/m, 'APP_DEBUG=true')\n fs.writeFileSync(envPath, localEnv, { flag: 'wx' })\n }\n if (install) {\n // Drizzle Kit 0.31 still declares deprecated loader packages upstream.\n // Keep the scaffold output focused on actionable install/audit information.\n const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '--loglevel=error'], { cwd: target, stdio: 'inherit', shell: process.platform === 'win32' })\n if (result.error || result.status !== 0) {\n throw new Error(`Project created at ${target}, but npm install failed. Check the npm output above and run npm install there to retry.`)\n }\n }\n console.log(`Created ${target} (Cloudflare Workers, ${frontend === 'nuxt' ? 'Nuxt frontend' : 'API-only'})\\n\\nNext:\\n cd ${name}\\n${install ? '' : ' npm install\\n'} npm run dev`)\n}\n\nasync function chooseFrontend() {\n if (!process.stdin.isTTY || !process.stdout.isTTY) {\n console.log('[Honovel] No interactive terminal detected; including Nuxt. Pass --api-only or --frontend=none for an API-only project.')\n return 'nuxt'\n }\n\n const prompt = readline.createInterface({ input: process.stdin, output: process.stdout })\n try {\n console.log('\\nCloudflare Workers backend \u2014 choose your frontend:\\n\\n 1) Nuxt frontend (recommended)\\n 2) API-only (no frontend)\\n')\n const answer = await prompt.question('Select a project setup [1]: ')\n const selected = answer.trim().toLowerCase()\n if (!selected || selected === '1' || selected === 'nuxt') return 'nuxt'\n if (selected === '2' || selected === 'none' || selected === 'api-only') return 'none'\n throw new Error('Choose 1 for Nuxt or 2 for API-only.')\n } finally {\n prompt.close()\n }\n}\n\nmain().catch((error) => {\n console.error(`[Honovel] ${error.message}`)\n process.exitCode = 1\n})\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { spawnSync } from 'node:child_process'
6
+ import readline from 'node:readline/promises'
7
+
8
+ const packageRoot = fileURLToPath(new URL('../', import.meta.url))
9
+ const metadata = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
10
+
11
+ async function main() {
12
+ const args = process.argv.slice(2)
13
+ if (!args.length || args.includes('--help') || args.includes('-h')) {
14
+ console.log('Usage: npm create honovel@latest <project-name> [--frontend=nuxt|none] [--api-only] [--no-install]\nThe backend is always Cloudflare Workers. If no frontend option is given, the installer asks whether to include Nuxt (the non-interactive default).')
15
+ return
16
+ }
17
+ const [name, ...options] = args
18
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) throw new Error('Use a project name containing lowercase letters, digits, hyphens, or underscores.')
19
+ let frontend = 'nuxt'
20
+ let frontendSelected = false
21
+ let install = true
22
+ for (let index = 0; index < options.length; index++) {
23
+ const option = options[index]
24
+ if (option === '--no-install') {
25
+ install = false
26
+ } else if (option === '--api-only' || option === '--frontend' || option.startsWith('--frontend=')) {
27
+ if (frontendSelected) throw new Error('Select the frontend only once; do not combine --api-only and --frontend.')
28
+ frontendSelected = true
29
+ frontend = option === '--api-only' ? 'none'
30
+ : option === '--frontend' ? options[++index] : option.slice('--frontend='.length)
31
+ if (!['nuxt', 'none'].includes(frontend)) throw new Error('Supported frontend options: nuxt, none.')
32
+ } else if (option === '--template' || option.startsWith('--template=')) {
33
+ throw new Error('All projects use Cloudflare Workers. Use --frontend=nuxt, --frontend=none or --api-only.')
34
+ } else throw new Error('Unknown option. Supported: --frontend=nuxt|none, --api-only, --no-install')
35
+ }
36
+ if (!frontendSelected) frontend = await chooseFrontend()
37
+ if (name.length > 63) throw new Error('Cloudflare project names must be at most 63 characters long.')
38
+ const target = path.resolve(name)
39
+ if (fs.existsSync(target)) throw new Error(`${name} already exists. Choose a new directory.`)
40
+ fs.mkdirSync(target)
41
+ function copy(directory, destination) {
42
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
43
+ const source = path.join(directory, entry.name)
44
+ const outputName = entry.name === 'env.example.template'
45
+ ? '.env.example'
46
+ : entry.name.replace(/\.template$/, '')
47
+ const output = path.join(destination, outputName)
48
+ if (entry.isDirectory()) {
49
+ fs.mkdirSync(output, { recursive: true })
50
+ copy(source, output)
51
+ } else {
52
+ if (/\.(png|jpe?g|gif|webp|ico|woff2?)$/i.test(entry.name)) {
53
+ fs.copyFileSync(source, output)
54
+ continue
55
+ }
56
+ const content = fs.readFileSync(source, 'utf8')
57
+ .replaceAll('__PROJECT_NAME__', name)
58
+ .replaceAll('__WORKER_NAME__', name.replaceAll('_', '-'))
59
+ .replaceAll('__HONOVEL_VERSION__', metadata.version)
60
+ if (fs.existsSync(output) && ['package.json', 'wrangler.jsonc'].includes(outputName)) {
61
+ const current = JSON.parse(fs.readFileSync(output, 'utf8'))
62
+ const overlay = JSON.parse(content)
63
+ const merged = { ...current, ...overlay }
64
+ if (outputName === 'package.json') {
65
+ for (const section of ['scripts', 'dependencies', 'devDependencies', 'overrides']) {
66
+ merged[section] = { ...current[section], ...overlay[section] }
67
+ }
68
+ }
69
+ fs.writeFileSync(output, JSON.stringify(merged, null, 2) + '\n')
70
+ } else fs.writeFileSync(output, content)
71
+ }
72
+ }
73
+ }
74
+ copy(path.join(packageRoot, 'template/common'), target)
75
+ copy(path.join(packageRoot, 'template/cloudflare'), target)
76
+ if (frontend === 'nuxt') copy(path.join(packageRoot, 'template/nuxt'), target)
77
+ const envExamplePath = path.join(target, '.env.example')
78
+ const envPath = path.join(target, '.env')
79
+ if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
80
+ const localEnv = fs.readFileSync(envExamplePath, 'utf8')
81
+ .replace(/^APP_DEBUG=.*$/m, 'APP_DEBUG=true')
82
+ fs.writeFileSync(envPath, localEnv, { flag: 'wx' })
83
+ }
84
+ if (install) {
85
+ // Drizzle Kit 0.31 still declares deprecated loader packages upstream.
86
+ // Keep the scaffold output focused on actionable install/audit information.
87
+ const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '--loglevel=error'], { cwd: target, stdio: 'inherit', shell: process.platform === 'win32' })
88
+ if (result.error || result.status !== 0) {
89
+ throw new Error(`Project created at ${target}, but npm install failed. Check the npm output above and run npm install there to retry.`)
90
+ }
91
+ }
92
+ console.log(`Created ${target} (Cloudflare Workers, ${frontend === 'nuxt' ? 'Nuxt frontend' : 'API-only'})\n\nNext:\n cd ${name}\n${install ? '' : ' npm install\n'} npm run dev`)
93
+ }
94
+
95
+ async function chooseFrontend() {
96
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
97
+ console.log('[Honovel] No interactive terminal detected; including Nuxt. Pass --api-only or --frontend=none for an API-only project.')
98
+ return 'nuxt'
99
+ }
100
+
101
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout })
102
+ try {
103
+ console.log('\nCloudflare Workers backend — choose your frontend:\n\n 1) Nuxt frontend (recommended)\n 2) API-only (no frontend)\n')
104
+ const answer = await prompt.question('Select a project setup [1]: ')
105
+ const selected = answer.trim().toLowerCase()
106
+ if (!selected || selected === '1' || selected === 'nuxt') return 'nuxt'
107
+ if (selected === '2' || selected === 'none' || selected === 'api-only') return 'none'
108
+ throw new Error('Choose 1 for Nuxt or 2 for API-only.')
109
+ } finally {
110
+ prompt.close()
111
+ }
112
+ }
113
+
114
+ main().catch((error) => {
115
+ console.error(`[Honovel] ${error.message}`)
116
+ process.exitCode = 1
117
+ })
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "create-honovel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Create a Cloudflare Workers application with an optional Nuxt frontend",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "bin": {
11
+ "create-honovel": "./bin/create-honovel.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "template",
16
+ "README.md"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Honovel/framework.git",
21
+ "directory": "packages/create-honovel"
22
+ },
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org/",
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "esbuild bin/create-honovel.mts --platform=node --format=esm --outfile=bin/create-honovel.mjs",
29
+ "prepack": "npm run build"
30
+ },
31
+ "devDependencies": {
32
+ "esbuild": "^0.28.1"
33
+ }
34
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Honovel Cloudflare (attach)",
6
+ "type": "node",
7
+ "request": "attach",
8
+ "port": 9229,
9
+ "address": "127.0.0.1",
10
+ "cwd": "/",
11
+ "resolveSourceMapLocations": null,
12
+ "attachExistingChildren": false,
13
+ "autoAttachChildProcesses": false,
14
+ "restart": true,
15
+ "sourceMaps": true
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,46 @@
1
+ # __PROJECT_NAME__
2
+
3
+ This Honovel project runs on Cloudflare Workers and is API-only. No frontend directory, frontend dependencies or static assets binding are generated.
4
+
5
+ ```sh
6
+ npm run dev
7
+ ```
8
+
9
+ The API is available at `http://localhost:8788/api/health`. Development Swagger and OpenAPI are served at `/swagger` and `/openapi.json`. Use `npm run dev:debug` to attach an IDE to the Worker inspector on `127.0.0.1:9229`; normal development uses an ephemeral inspector port.
10
+
11
+ ## Application structure
12
+
13
+ `index.ts` creates the Hono app and exports the Durable Object cache class. `src/runtime/Controller.ts` supplies the `/api` prefix. Add feature controllers, DTOs, actions, repositories and services under `src/features/`.
14
+
15
+ ```sh
16
+ npm run honovel -- make:feature users
17
+ npm run honovel -- list
18
+ npm run typecheck
19
+ npm run build
20
+ ```
21
+
22
+ `build` generates binding types, compiles for production and performs a local Wrangler deployment dry run. It writes the Worker bundle to `dist/worker`; it does not upload anything. Production documentation routes are disabled by default.
23
+
24
+ Build/dev helpers are provided by `honovel` through the `honovel` CLI. This project has no copied `scripts/` directory. `npm run dev` launches Wrangler directly without a frontend server.
25
+
26
+ ## Runtime services and database
27
+
28
+ The project selects D1 for SQL, Durable Objects for cache and cache-backed locks, Cloudflare Queues for queued listeners and Cron Triggers for schedules. Application runtime wrappers under `src/runtime/` select those providers through DI.
29
+
30
+ The configured `main` database has an empty schema and no initial SQL migration. Add tables and export them from `src/database/main/schema/index.ts`, then run:
31
+
32
+ ```sh
33
+ npm run db:generate
34
+ npm run db:migrate # local D1
35
+ ```
36
+
37
+ See `src/database/README.md` for an optional table example. Before deployment, create the database named in `wrangler.jsonc`, replace its placeholder `database_id`, run `npm run compile` and apply any migrations with `npm run db:migrate:remote`. `npm run deploy` rejects placeholder IDs before builds or uploads.
38
+
39
+ ```sh
40
+ npm run cf-typegen # after changing bindings
41
+ npm run deploy
42
+ ```
43
+
44
+ Edit `wrangler.jsonc`; `.honovel.wrangler.jsonc` and `.honovel/` are generated files. Keep secrets in local environment files or Wrangler secrets. The generated local `.env` enables `APP_DEBUG=true`; disable debug details in production.
45
+
46
+ Framework documentation: https://honovel.github.io/framework
@@ -0,0 +1,7 @@
1
+ import { createHonoApp } from 'honovel'
2
+ import type { AppEnv } from './src/env'
3
+
4
+ export { HonovelCacheDurableObject } from 'honovel/cloudflare'
5
+
6
+ const app = createHonoApp<AppEnv>()
7
+ export default app
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "imports": {
7
+ "#src/*": "./src/*.ts"
8
+ },
9
+ "engines": {
10
+ "node": ">=22"
11
+ },
12
+ "overrides": {
13
+ "@esbuild-kit/core-utils": {
14
+ "esbuild": "^0.25.12"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "honovel": "honovel",
19
+ "compile": "honovel cloudflare:compile",
20
+ "compile:wrangler": "honovel cloudflare:build",
21
+ "typecheck:backend": "npm run cf-typegen && honovel typecheck",
22
+ "typecheck": "npm run typecheck:backend",
23
+ "db:generate": "honovel db:generate --db=main",
24
+ "db:migrate": "wrangler d1 migrations apply __WORKER_NAME__-main --local",
25
+ "db:migrate:remote": "wrangler d1 migrations apply __WORKER_NAME__-main --remote",
26
+ "build": "npm run cf-typegen && honovel cloudflare:deploy --dry-run --outdir dist/worker",
27
+ "dev:backend": "honovel cloudflare:dev-backend",
28
+ "dev:backend:debug": "honovel cloudflare:dev-backend --debug",
29
+ "dev": "honovel cloudflare:dev-backend",
30
+ "dev:debug": "honovel cloudflare:dev-backend --debug",
31
+ "predeploy": "honovel cloudflare:predeploy",
32
+ "deploy": "npm run cf-typegen && honovel cloudflare:deploy --minify",
33
+ "cf-typegen": "honovel cloudflare:typegen"
34
+ },
35
+ "dependencies": {
36
+ "honovel": "__HONOVEL_VERSION__",
37
+ "hono": "^4.13.17",
38
+ "drizzle-orm": "^0.45.3",
39
+ "zod": "^4.6.7"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.15.0",
43
+ "drizzle-kit": "^0.31.11",
44
+ "typescript": "^6.0.3",
45
+ "wrangler": "4.158.0"
46
+ }
47
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from 'hono'
2
+ import { drizzle } from 'drizzle-orm/d1'
3
+ import { defineDatabase } from 'honovel'
4
+ import type { AppEnv } from '../../env'
5
+ import * as schema from './schema'
6
+
7
+ /** The default D1 database. Replace the placeholder ID in wrangler.jsonc before deploy. */
8
+ export class MainDatabase extends defineDatabase(
9
+ (context: Context<AppEnv>) => drizzle(context.env.DB, { schema }),
10
+ ) {}
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'drizzle-kit'
2
+
3
+ export default defineConfig({
4
+ dialect: 'sqlite',
5
+ schema: './src/database/main/schema/*.ts',
6
+ out: './src/database/main/migrations',
7
+ })
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ ]
6
+ }
@@ -0,0 +1,2 @@
1
+ // Export your application tables here. See ../../README.md for an example.
2
+ export {}
@@ -0,0 +1,5 @@
1
+ export type AppEnv = {
2
+ Bindings: CloudflareBindings & {
3
+ APP_DEBUG?: string
4
+ }
5
+ }
@@ -0,0 +1,11 @@
1
+ import { Context } from 'hono'
2
+ import { Queue, Scope } from 'honovel'
3
+ import { BaseCloudflareQueue } from 'honovel/cloudflare'
4
+ import type { AppEnv } from '../env'
5
+
6
+ @Scope({ provides: Queue })
7
+ export class CloudflareQueue extends BaseCloudflareQueue {
8
+ constructor(context: Context<AppEnv>) {
9
+ super(context.env.HONOVEL_QUEUE)
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ import { Context } from 'hono'
2
+ import { Cache, Scope } from 'honovel'
3
+ import { BaseDurableObjectCache } from 'honovel/cloudflare'
4
+ import type { AppEnv } from '../env'
5
+
6
+ @Scope({ provides: Cache })
7
+ export class DurableObjectCache extends BaseDurableObjectCache {
8
+ constructor(context: Context<AppEnv>) {
9
+ super(context.env.HONOVEL_CACHE)
10
+ }
11
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "name": "__WORKER_NAME__",
4
+ "main": ".honovel/index.js",
5
+ "compatibility_date": "2026-09-21",
6
+ "compatibility_flags": [
7
+ "no_nodejs_compat",
8
+ "no_nodejs_compat_v2"
9
+ ],
10
+ "durable_objects": {
11
+ "bindings": [
12
+ {
13
+ "name": "HONOVEL_CACHE",
14
+ "class_name": "HonovelCacheDurableObject"
15
+ }
16
+ ]
17
+ },
18
+ "migrations": [
19
+ {
20
+ "tag": "v1",
21
+ "new_sqlite_classes": [
22
+ "HonovelCacheDurableObject"
23
+ ]
24
+ }
25
+ ],
26
+ "queues": {
27
+ "producers": [
28
+ {
29
+ "binding": "HONOVEL_QUEUE",
30
+ "queue": "__WORKER_NAME__"
31
+ }
32
+ ],
33
+ "consumers": [
34
+ {
35
+ "queue": "__WORKER_NAME__"
36
+ }
37
+ ]
38
+ },
39
+ "d1_databases": [
40
+ {
41
+ "binding": "DB",
42
+ "database_name": "__WORKER_NAME__-main",
43
+ "database_id": "00000000-0000-0000-0000-000000000000",
44
+ "migrations_dir": "src/database/main/migrations"
45
+ }
46
+ ],
47
+ "build": {
48
+ "command": "honovel cloudflare:build",
49
+ "watch_dir": [
50
+ "src",
51
+ "index.ts",
52
+ "tsconfig.json",
53
+ "tsconfig.build.json"
54
+ ]
55
+ }
56
+ }
@@ -0,0 +1,16 @@
1
+ node_modules/
2
+ .honovel/
3
+ .honovel.wrangler.jsonc
4
+ dist/
5
+ .nuxt/
6
+ .output/
7
+ .wrangler/
8
+ .dev.vars
9
+ .dev.vars.*
10
+ worker-configuration.d.ts
11
+ .env
12
+ .env.*
13
+ !.env.example
14
+ *.db
15
+ *.db-*
16
+ *.log
@@ -0,0 +1,4 @@
1
+ {
2
+ "js/ts.tsdk.path": "./node_modules/typescript/lib",
3
+ "js/ts.experimental.useTsgo": false
4
+ }
@@ -0,0 +1,33 @@
1
+ # Honovel project guide for coding agents
2
+
3
+ This project is a Honovel application generated by `create-honovel`. Read this file before changing framework-specific code.
4
+
5
+ ## Project structure
6
+
7
+ - `index.ts` is the runtime entrypoint. It creates the Hono application and exports it for Cloudflare Workers.
8
+ - `src/features/<feature>/` contains feature code. Keep controllers, DTOs, actions, repositories and services close to the feature that owns them.
9
+ - `src/runtime/Controller.ts` is the application-owned controller base. It adds the `/api` prefix to feature controller routes.
10
+ - `src/database/<name>/` contains a database provider, Drizzle schemas and migrations. Keep schemas and migration history grouped by physical database.
11
+ - `frontend/` contains the optional Nuxt 4 frontend. It exists only when Nuxt was selected during project creation and uses the same origin as the API in production. API-only projects have no frontend directory, dependencies or assets configuration.
12
+ - Runtime and build helpers are provided by the `honovel` CLI from `honovel`; there is no generated `scripts/` directory. `.honovel/` and `dist/` contain generated output; application code belongs in `src/`, `frontend/` and `index.ts`.
13
+
14
+ The project `README.md` describes the selected Nuxt or API-only setup. The backend is always Cloudflare Workers. Check `package.json` and `wrangler.jsonc` before changing deployment or platform code.
15
+
16
+ ## Working conventions
17
+
18
+ - Import Honovel APIs from `honovel` and `honovel/cloudflare`. Do not import application code from `.honovel/`.
19
+ - Use the Honovel CLI for new structure: `npm run honovel -- make:feature <name>` and the other `make:*` commands listed in `README.md`.
20
+ - Controllers handle HTTP input and delegate application work to actions or services. Repositories own database queries.
21
+ - The database schema starts empty. Add/export tables, then generate and apply migrations before using them. Run `npm run typecheck` after framework or TypeScript changes.
22
+ - Keep request-specific state scoped to the request. In Cloudflare Workers, do not store request or binding objects in module-level mutable state.
23
+
24
+ ## Honovel documentation
25
+
26
+ The hosted documentation is the source of truth for framework APIs and runtime behavior:
27
+
28
+ - Index for agents: https://honovel.github.io/framework/llms.txt
29
+ - Complete agent documentation: https://honovel.github.io/framework/llms-full.txt
30
+ - Human-readable documentation: https://honovel.github.io/framework
31
+ - Individual Markdown topics: https://honovel.github.io/framework/raw/<topic>.md
32
+
33
+ When working on Honovel-specific code, consult `/llms.txt` first and load the relevant topic from `/raw/` before implementing framework APIs. Use `/llms-full.txt` when the task spans several parts of the framework or when the topic is unclear. This prevents assuming undocumented APIs from other frameworks or plain Hono.
@@ -0,0 +1,3 @@
1
+ # Expose exception messages and stack traces in HTTP 500 responses.
2
+ # Keep disabled in production.
3
+ APP_DEBUG=false
@@ -0,0 +1,27 @@
1
+ # Databases
2
+
3
+ Keep each database under its own directory here, e.g. `main/MainDatabase.ts`, `main/schema/*.ts`, `main/drizzle.config.ts`, and `main/migrations/`.
4
+
5
+ Nuxt and API-only projects both include a configured `main` D1 database with an empty schema and no initial SQL migration. No Todo table or CRUD feature is created.
6
+
7
+ For example, create `main/schema/todos.ts` with:
8
+
9
+ ```ts
10
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
11
+
12
+ export const todos = sqliteTable('todos', {
13
+ id: integer('id').primaryKey({ autoIncrement: true }),
14
+ title: text('title').notNull(),
15
+ completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
16
+ })
17
+ ```
18
+
19
+ Export it from `main/schema/index.ts` with `export * from './todos'`. Then run `npm run db:generate` and `npm run db:migrate`. Only add the table if your application needs it. Create `<project>-main` and replace the placeholder `database_id` in `wrangler.jsonc` before remote migrations or deployment.
20
+
21
+ Import `MainDatabase` and the tables only in feature repositories. Additional connections can be added as sibling directories under `src/database/`, each with its own provider, schema, Drizzle config, and migrations.
22
+
23
+ Database providers extend `defineDatabase(...)`, are lazy and request-scoped, and should be injected into repositories rather than controllers. Keep long-lived driver clients/pools outside the provider factory where the runtime supports it; do not cache a request-specific Hono context or binding globally.
24
+
25
+ The starter uses the stable Drizzle Kit 0.31 line and overrides its nested esbuild version to a secure release. The upstream Kit package still declares deprecated `@esbuild-kit` loader packages; the initializer suppresses those non-actionable npm deprecation notices during its automatic install. Revisit this workaround when Drizzle Kit 1.0 is stable.
26
+
27
+ Generate migrations with `npm run db:generate`, then apply them through Wrangler using `npm run db:migrate` (local D1) or `npm run db:migrate:remote` (remote D1).
@@ -0,0 +1,65 @@
1
+ import {
2
+ Cache,
3
+ createDecorator,
4
+ TooManyRequestsException,
5
+ } from 'honovel'
6
+
7
+ /**
8
+ * Default API rate limit: 200 requests per minute per client IP.
9
+ *
10
+ * This can be adapted to user-, account-, token-, or route-based limits if needed.
11
+ */
12
+ export const IpRateLimit = createDecorator(
13
+ [Cache],
14
+ async (context, cache, attempts: number) => {
15
+ const forwardedFor = context.req.header('X-Forwarded-For')
16
+ const ip = context.req.header('CF-Connecting-IP')
17
+ ?? forwardedFor?.split(',')[0]?.trim()
18
+ ?? 'unknown'
19
+
20
+ const currentAttempts = await cache.increment(
21
+ 'rate-limit',
22
+ `api:${ip}`,
23
+ 1,
24
+ 60_000,
25
+ )
26
+
27
+ if (currentAttempts > attempts) {
28
+ throw new TooManyRequestsException()
29
+ }
30
+ },
31
+ { cache: false },
32
+ )
33
+
34
+ /**
35
+ * Additional custom decorator examples. Uncomment and adapt these to your application.
36
+ * They can be used on controller parameters, methods, or controllers.
37
+ */
38
+
39
+ /*
40
+ import { JwtPayload } from 'honovel'
41
+ import { UserService } from './services/UserService'
42
+
43
+ export const Header = createDecorator((context, name: string) =>
44
+ context.req.header(name)
45
+ )
46
+
47
+ // You can inject any service or even other decorators to your custom decorators.
48
+ // UserService is request-scoped by default. Replace findById() with the
49
+ // application-specific lookup used by your user model.
50
+ export const CurrentUser = createDecorator(
51
+ [UserService, JwtPayload], // Inject any service or even other decorators here
52
+ async (_context, users, jwt) => users.findById(Number(jwt.sub)),
53
+ )
54
+
55
+ // Example usage:
56
+ //
57
+ // @ApiRateLimit()
58
+ // @Get('/')
59
+ // show(
60
+ // @Header('x-request-id') requestId: string | undefined,
61
+ // @CurrentUser() user: User,
62
+ // ) {
63
+ // return { requestId, user }
64
+ // }
65
+ */
@@ -0,0 +1,10 @@
1
+ import { BaseUrl, Get } from 'honovel'
2
+ import { Controller } from '#src/runtime/Controller'
3
+
4
+ @BaseUrl('/health')
5
+ export class HealthController extends Controller {
6
+ @Get('/')
7
+ show() {
8
+ return { status: 'ok' }
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ import { CacheLock, Lock, Scope } from 'honovel'
2
+
3
+ @Scope({ provides: Lock })
4
+ export class AppLock extends CacheLock {}
@@ -0,0 +1,4 @@
1
+ import { BaseController, BaseUrl } from 'honovel'
2
+
3
+ @BaseUrl('/api')
4
+ export abstract class Controller extends BaseController {}
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": { "rootDir": ".", "outDir": ".honovel", "declaration": false },
4
+ "include": ["index.ts", "src/**/*.ts", "worker-configuration.d.ts"]
5
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "experimentalDecorators": true,
9
+ "lib": ["ESNext", "DOM"],
10
+ "plugins": [{ "name": "honovel/typescript-plugin" }]
11
+ }
12
+ }
@@ -0,0 +1,103 @@
1
+ # __PROJECT_NAME__
2
+
3
+ ```sh
4
+ npm run dev
5
+ ```
6
+
7
+ The starter runs a Nuxt 4 frontend and the Honovel Worker together. Open `http://localhost:5173`; Nuxt proxies `/api`, `/swagger`, and `/openapi.json` to the local Wrangler backend. Honovel prefers backend port 8788 and automatically picks a free port when 8788 is already in use, while the public frontend URL stays fixed on 5173. The sample frontend calls `GET /api/health`. In production, `nuxt generate` prerenders the frontend and Honovel deploys the generated files with the Worker as Cloudflare Static Assets, so frontend assets and API routes share one origin and one deployment.
8
+
9
+ The combined dev command uses scrolling logs with `[Nuxt]` prefixes for frontend output. Nuxt's fullscreen terminal UI is disabled so it cannot overwrite the Worker logs or the app link. Press Ctrl+C to stop both servers.
10
+
11
+ The frontend includes Tailwind CSS through its Vite plugin and a locally bundled Manrope font. Customize colors and typography in `frontend/app/assets/css/main.css` (`@theme`), and use utility classes directly in Vue components. Pages live in `frontend/app/pages/`.
12
+
13
+ The default production rendering mode is SSG. Nuxt prerenders discoverable pages during the build and hydrates them in the browser. Pure CSR is also possible with `ssr: false` in `frontend/nuxt.config.ts`. Request-time SSR and ISR are not supported by the current Honovel starter because no Nuxt/Nitro server runtime is deployed.
14
+
15
+ Wrangler serves the Worker locally with automatic rebuilding when application source changes. `/swagger` and `/openapi.json` are enabled in development. Normal `npm run dev` chooses a free backend port when needed and lets Wrangler use an ephemeral inspector port, so multiple Honovel or Wrangler projects can run locally without hard-coded port collisions.
16
+
17
+ Wrangler executes the Worker in Cloudflare's local `workerd` runtime rather than in the Node process that launched Wrangler. Use `npm run dev` for normal development. When you want to attach an IDE debugger, run `npm run dev:debug`; that pins the Worker inspector to `127.0.0.1:9229` for the included `Honovel Cloudflare (attach)` configuration. Do not launch Wrangler itself through a Node debug terminal, because that debugs Wrangler and Honovel's custom-build Node subprocesses instead of the Worker runtime.
18
+
19
+ `index.ts` creates and exports the Hono app. Honovel compiles routes to `.honovel/index.js`, the `main` entry in `wrangler.jsonc`. Cloudflare invokes the app's `fetch` handler directly; this template has no Node server adapter or HTTP listener.
20
+
21
+ ```sh
22
+ npm run typecheck
23
+ npm run build
24
+ npm run deploy
25
+ ```
26
+
27
+ `build` runs a local Wrangler deployment dry run and writes the production Worker bundle to `dist/`; it does not upload anything. `deploy` publishes the Worker after Cloudflare authentication. Both invoke Honovel's custom build through Wrangler, which selects production mode for deployment and disables documentation by default.
28
+
29
+ The application-level `src/runtime/Controller.ts` extends Honovel's `BaseController` and declares `@BaseUrl('/api')`. Feature controllers add their own segment, so `@BaseUrl('/users')` becomes `/api/users`. Change that one runtime controller if your application uses a different API prefix.
30
+
31
+ The frontend is generated into `frontend/.output/public` and copied to `dist/frontend`. `wrangler.jsonc` configures SPA fallback routing and sends only `/api/*`, `/swagger`, and `/openapi.json` through the Worker first; normal static asset requests are served directly by Cloudflare's static asset layer.
32
+
33
+ `compile` and `typecheck` generate `CloudflareBindings` with `wrangler types`. Add bindings such as D1, KV, or R2 to `wrangler.jsonc`; `src/env.ts` exposes them as `AppEnv`. Keep local secrets in ignored `.dev.vars`, and use `wrangler secret put` for deployed secrets. Node.js is required for tooling only; the Worker runtime does not need `nodejs_compat` for this starter.
34
+
35
+ The custom build uses `honovel cloudflare:build` from `honovel`. It skips compilation when called by `wrangler types`, because Wrangler also invokes custom builds while inspecting the entrypoint for type generation; this prevents recursive builds.
36
+
37
+ Wrangler is pinned to the version tested with this starter. Upgrade that dependency and the compatibility date deliberately when adopting newer Workers features.
38
+
39
+ Generate a feature with `npm run honovel -- make:feature users`. Database schemas, migrations, and provider classes belong under `src/database/<name>/`. Generate CRUD code for an existing table with `make:resource users/User --table=users --database=main`. For additional databases, choose the appropriate Drizzle driver and binding in a database factory.
40
+
41
+ The starter includes `src/database/main/` with a D1 provider and an empty schema. See `src/database/README.md` for an optional table example; generate your first migration after adding tables. Create a D1 database named exactly like the generated `database_name` (for example `my-app-main`) and replace the placeholder `database_id` in `wrangler.jsonc` with its ID. Run `npm run compile` after changing the ID to refresh the generated Wrangler configuration before applying remote migrations. After generating a migration, run `npm run db:migrate` for the local D1 database; use `npm run db:migrate:remote` for the deployed database. Generate new migrations with `npm run db:generate`.
42
+
43
+ `npm run deploy` checks for D1 placeholder IDs before type generation, builds, or uploads and prints the setup commands if a database has not been configured. `npm run build` remains usable with placeholder IDs because it only performs a local dry run. This check does not verify that configured databases exist in your Cloudflare account; Wrangler validates them during deployment.
44
+
45
+ ## Honovel commands
46
+
47
+ ```sh
48
+ npm run honovel -- list
49
+ npm run honovel -- compile # development compile
50
+ npm run honovel -- build # production compile
51
+ npm run honovel -- typecheck
52
+ npm run honovel -- make:feature users
53
+ npm run honovel -- make:resource users/User --table=users --database=main
54
+ npm run honovel -- make:controller users/User
55
+ npm run honovel -- make:repository users/UserRepository
56
+ npm run honovel -- make:service users/UserService
57
+ npm run honovel -- make:action users/CreateUser
58
+ npm run honovel -- make:request users/CreateUserRequest
59
+ npm run honovel -- make:response users/UserResponse
60
+ npm run honovel -- make:middleware Auth
61
+ npm run honovel -- make:migration add-index
62
+ npm run db:generate
63
+ npm run db:migrate # local D1
64
+ npm run db:migrate:remote # deployed D1
65
+ ```
66
+
67
+ `make:feature` creates only the feature structure. `make:resource` generates CRUD code for an existing exported table; it does not create database files. Keep connections and schemas central under `src/database/`, then expose them to features through repositories.
68
+
69
+ ## DI and decorators
70
+
71
+ Honovel resolves concrete, named-exported classes by constructor. Services are request-scoped by default, which is important in Workers because global mutable state can leak between requests. Inject the concrete `MainDatabase` provider into repositories; it creates a typed Drizzle D1 client lazily from the current Hono context.
72
+
73
+ The following example assumes you have added and exported the optional `todos` table described in `src/database/README.md`.
74
+
75
+ ```ts
76
+ import { BaseUrl, Controller, Get } from 'honovel'
77
+ import { MainDatabase } from '../../../database/main/MainDatabase'
78
+ import { todos } from '../../../database/main/schema'
79
+
80
+ @BaseUrl('/todos')
81
+ export class TodoController extends Controller {
82
+ constructor(private readonly database: MainDatabase) { super() }
83
+
84
+ @Get('/')
85
+ list() {
86
+ return this.database.client.select().from(todos)
87
+ }
88
+ }
89
+ ```
90
+
91
+ Use `@Get`, `@Post`, `@Put`, `@Patch`, and `@Delete` for routes, `@BaseUrl` for a controller prefix, and `@Query`/`@Param` for typed route parameters. `@Middleware` attaches middleware. Use `@Scope('transient')` only for stateless, independently-created helpers; database providers must remain request-scoped. Custom decorators can be built with `createDecorator` and explicit DI dependencies. Validation uses `FormRequest(schema)` or standard schemas and keeps request parsing at the route boundary.
92
+
93
+ The published Core includes the Honovel TypeScript language-service plugin. The generated `tsconfig.json` points editors at `honovel/typescript-plugin`; restart the TypeScript server after installing dependencies to get diagnostics for missing routes, DI dependencies, and decorator usage. The plugin is editor tooling only—the compiler remains the authoritative build/typecheck step.
94
+
95
+ Keep `index.ts` as the Worker entrypoint and keep runtime bindings in `AppEnv`. Do not access `globalThis` for request state or cache a request-specific D1/Context object. Use Wrangler secrets for credentials, keep `.dev.vars` local, and commit migrations alongside the database they change.
96
+
97
+ Copy `.env.example` to `.env` for local Wrangler configuration. `APP_DEBUG=false` is the safe default; set it to `true` locally to include the exception name and stack trace in otherwise hidden HTTP 500 responses. Never enable it in production.
98
+
99
+ The framework and Cloudflare adapters are installed together as `honovel`; no framework source is copied into this project. Import framework APIs from `honovel` and Cloudflare adapters from `honovel/cloudflare`. Installation uses the public npm registry and requires no login.
100
+
101
+ Build and runtime helpers are shipped in `honovel` and invoked through the `honovel` CLI. The starter does not copy a `scripts/` directory. `honovel frontend:build` generates the Nuxt frontend; Cloudflare tooling is available as `honovel cloudflare:<command>`.
102
+
103
+ This project was created with `--frontend=nuxt`. For a new API-only project, use `npm create honovel@latest my-api -- --api-only`; its backend remains Cloudflare Workers.
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <a href="#main" class="sr-only fixed left-6 top-4 z-50 rounded bg-ink p-3 text-paper focus:not-sr-only">Skip to content</a>
3
+ <div class="flex min-h-screen flex-col">
4
+ <header class="page-width flex min-h-24 flex-wrap items-center justify-between gap-4 py-5">
5
+ <NuxtLink to="/" aria-label="Honovel home" class="flex items-center gap-3 text-xl font-extrabold tracking-tight">
6
+ <img src="/honovel-icon.png" alt="" width="32" height="32" class="size-8" aria-hidden="true">
7
+ honovel<span class="text-accent">.</span>
8
+ </NuxtLink>
9
+ <nav aria-label="Main navigation" class="flex items-center gap-1 rounded-full border border-line p-1 text-sm">
10
+ <NuxtLink to="/" class="nav-link">Overview</NuxtLink>
11
+ <NuxtLink to="/about" class="nav-link">The stack</NuxtLink>
12
+ </nav>
13
+ </header>
14
+ <NuxtPage />
15
+ <footer class="page-width mt-auto flex flex-wrap items-center justify-between gap-4 border-t border-line py-7 text-xs text-muted">
16
+ <span>__PROJECT_NAME__ <span class="mx-2 text-line">/</span> Built with Honovel</span>
17
+ <span>Nuxt 4 <span class="mx-2">·</span> Vue 3 <span class="mx-2">·</span> TypeScript <span class="mx-2">·</span> Tailwind CSS</span>
18
+ </footer>
19
+ </div>
20
+ </template>
@@ -0,0 +1,38 @@
1
+ @import '@fontsource-variable/manrope';
2
+ @import 'tailwindcss';
3
+
4
+ @theme {
5
+ --font-sans: 'Manrope Variable', sans-serif;
6
+ --color-paper: #f5f3ed;
7
+ --color-ink: #242520;
8
+ --color-muted: #686a60;
9
+ --color-line: #dedfd5;
10
+ --color-accent: #bd3b23;
11
+ }
12
+
13
+ @layer base {
14
+ body { @apply m-0 min-w-80 bg-paper font-sans text-ink antialiased; }
15
+ ::selection { background: #f7c4aa; color: #242520; }
16
+ button, a { -webkit-tap-highlight-color: transparent; }
17
+ button:not(:disabled), a { cursor: pointer; }
18
+ :focus-visible { outline: 2px solid #bd3b23; outline-offset: 5px; }
19
+ }
20
+
21
+ @layer components {
22
+ .nav-link { @apply rounded-full px-5 py-2 transition-colors; }
23
+ .nav-link:hover { @apply bg-white; }
24
+ .nav-link.router-link-exact-active { @apply bg-ink text-paper; }
25
+ .page-width { @apply mx-auto w-full max-w-7xl px-6 sm:px-10 lg:px-16; }
26
+ .eyebrow { @apply font-mono text-xs uppercase tracking-[0.16em] text-muted; }
27
+ .action { @apply inline-flex min-h-12 items-center justify-center gap-5 rounded-full bg-ink px-6 text-sm font-semibold text-paper transition-colors hover:bg-accent disabled:cursor-wait disabled:opacity-60; }
28
+ .text-link { @apply inline-flex min-h-11 items-center gap-3 text-sm font-semibold transition-colors hover:text-accent; }
29
+ }
30
+
31
+ .enter { animation: enter 650ms cubic-bezier(0.2, 0.8, 0.2, 1) both; }
32
+ .enter-late { animation-delay: 110ms; }
33
+ .hero-mark { animation: mark-enter 950ms cubic-bezier(0.2, 0.8, 0.2, 1) both; }
34
+ @keyframes enter { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: translateY(0); } }
35
+ @keyframes mark-enter { from { opacity: 0; transform: rotate(-8deg) scale(0.95); } to { opacity: 1; transform: rotate(0) scale(1); } }
36
+ @media (prefers-reduced-motion: reduce) {
37
+ *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
38
+ }
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ useSeoMeta({
3
+ title: 'The stack · __PROJECT_NAME__',
4
+ description: 'Nuxt and Honovel, connected by design.',
5
+ })
6
+ </script>
7
+
8
+ <template>
9
+ <main id="main" class="page-width flex-1 pb-16 pt-10 lg:pb-24 lg:pt-16">
10
+ <section class="enter">
11
+ <p class="eyebrow mb-7">The stack / Connected by design</p>
12
+ <h1 class="max-w-4xl text-[clamp(3rem,6.5vw,5.5rem)] font-medium leading-[1.05] tracking-[-0.06em]">Two sides.<br />One <span class="text-accent">application.</span></h1>
13
+ <p class="mt-7 max-w-xl text-lg leading-8 text-muted">Nuxt handles the web experience. Honovel handles the API. TypeScript connects the pieces, and Tailwind makes them your own.</p>
14
+ </section>
15
+ <section class="enter enter-late my-12 overflow-hidden rounded-2xl bg-ink p-6 text-paper sm:p-10 lg:my-16" aria-label="Request flow">
16
+ <p class="mb-8 font-mono text-xs uppercase tracking-widest text-[#bfc0b5]">One origin, from browser to backend</p>
17
+ <div class="grid items-center gap-5 md:grid-cols-[1fr_auto_1fr_auto_1fr]">
18
+ <div><span class="font-mono text-xs text-[#bfc0b5]">01</span><h2 class="mt-2 text-2xl font-medium">Nuxt</h2><p class="mt-2 text-sm text-[#bfc0b5]">Pages, SEO, rendering</p></div>
19
+ <span class="text-3xl text-[#ed9b7b]" aria-hidden="true">→</span>
20
+ <div><span class="font-mono text-xs text-[#bfc0b5]">02</span><h2 class="mt-2 text-2xl font-medium">fetch('/api/…')</h2><p class="mt-2 text-sm text-[#bfc0b5]">Same-origin requests</p></div>
21
+ <span class="text-3xl text-[#ed9b7b]" aria-hidden="true">→</span>
22
+ <div><span class="font-mono text-xs text-[#bfc0b5]">03</span><h2 class="mt-2 text-2xl font-medium">Honovel + Hono</h2><p class="mt-2 text-sm text-[#bfc0b5]">Controllers, services, data</p></div>
23
+ </div>
24
+ </section>
25
+ <section class="grid gap-10 lg:grid-cols-[1fr_1.6fr] lg:gap-20">
26
+ <div><p class="eyebrow mb-4">A place for everything</p><h2 class="text-3xl font-medium tracking-tight">Simple foundations.<br />Room to grow.</h2></div>
27
+ <dl>
28
+ <div class="grid gap-2 border-t border-line py-6 sm:grid-cols-[140px_1fr] sm:gap-6"><dt class="font-semibold">Frontend</dt><dd class="text-sm leading-7 text-muted">Pages live in <code class="text-ink">frontend/app/pages</code>. Nuxt provides routing, SEO metadata, SSR and prerendering capabilities.</dd></div>
29
+ <div class="grid gap-2 border-t border-line py-6 sm:grid-cols-[140px_1fr] sm:gap-6"><dt class="font-semibold">API</dt><dd class="text-sm leading-7 text-muted">Backend features live in <code class="text-ink">src/features</code>. Requests under <code class="text-ink">/api</code> reach your Honovel controllers.</dd></div>
30
+ <div class="grid gap-2 border-t border-line py-6 sm:grid-cols-[140px_1fr] sm:gap-6"><dt class="font-semibold">Development</dt><dd class="text-sm leading-7 text-muted"><code class="text-ink">npm run dev</code> starts Nuxt and the backend together. Nuxt proxies API and docs requests while you work.</dd></div>
31
+ <div class="grid gap-2 border-y border-line py-6 sm:grid-cols-[140px_1fr] sm:gap-6"><dt class="font-semibold">Production</dt><dd class="text-sm leading-7 text-muted"><code class="text-ink">npm run build</code> prerenders the frontend and builds the backend. Your chosen runtime serves both from one origin.</dd></div>
32
+ </dl>
33
+ </section>
34
+ <div class="mt-12 flex flex-wrap items-center justify-between gap-6 border-t border-line pt-8">
35
+ <p class="text-xl font-medium tracking-tight">The rest is your story.</p>
36
+ <NuxtLink to="/" class="action">Back to your canvas <span aria-hidden="true">↗</span></NuxtLink>
37
+ </div>
38
+ </main>
39
+ </template>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref } from 'vue'
3
+
4
+ useSeoMeta({
5
+ title: '__PROJECT_NAME__ · Honovel',
6
+ description: 'A Honovel application powered by Nuxt.',
7
+ })
8
+
9
+ const state = ref<'checking' | 'online' | 'offline'>('checking')
10
+ const status = computed(() => ({ checking: 'Checking connection', online: 'API connected', offline: 'API unavailable' })[state.value])
11
+
12
+ async function checkHealth() {
13
+ state.value = 'checking'
14
+ try {
15
+ const response = await fetch('/api/health', { signal: AbortSignal.timeout(5000) })
16
+ if (!response.ok) throw new Error('Health request failed')
17
+ const body = await response.json() as { status?: string }
18
+ state.value = body.status === 'ok' ? 'online' : 'offline'
19
+ } catch {
20
+ state.value = 'offline'
21
+ }
22
+ }
23
+
24
+ onMounted(checkHealth)
25
+ </script>
26
+
27
+ <template>
28
+ <main id="main" class="flex-1">
29
+ <section class="page-width grid items-center gap-10 pb-14 pt-10 lg:grid-cols-[1.3fr_1fr] lg:gap-16 lg:pb-20 lg:pt-16">
30
+ <div class="enter">
31
+ <p class="eyebrow mb-7">__PROJECT_NAME__ / Your next chapter</p>
32
+ <h1 class="text-[clamp(2.6rem,7.5vw,6.5rem)] font-medium leading-[1.02] tracking-[-0.065em]">Hello, builder.<br />Make it <span class="text-accent">yours.</span></h1>
33
+ <p class="mt-7 max-w-md text-base leading-7 text-muted sm:text-lg">A fresh canvas for your next idea. Your Nuxt frontend and Honovel API are ready to work together.</p>
34
+ <div class="mt-9 flex flex-wrap items-center gap-x-7 gap-y-2">
35
+ <NuxtLink to="/about" class="action">Explore the stack <span aria-hidden="true">↗</span></NuxtLink>
36
+ <a href="#start" class="text-link">Start building <span aria-hidden="true">↓</span></a>
37
+ </div>
38
+ </div>
39
+ <div class="hero-mark relative mx-auto flex aspect-square w-full max-w-[380px] items-center justify-center overflow-hidden rounded-full bg-[#e9e8de]" aria-hidden="true">
40
+ <svg viewBox="0 0 400 400" class="size-full">
41
+ <circle cx="200" cy="200" r="160" fill="none" stroke="#cecec0" />
42
+ <circle cx="200" cy="200" r="116" fill="none" stroke="#cecec0" />
43
+ <path d="M0 200h400M200 0v400" stroke="#cecec0" />
44
+ <g transform="rotate(-12 200 200)">
45
+ <path d="M101 94h59v80h80V94h59v212h-59v-80h-80v80h-59z" fill="#bd3b23" />
46
+ <path d="M160 174h80v52h-80z" fill="#a32d1a" />
47
+ </g>
48
+ <circle cx="323" cy="88" r="11" fill="#242520" />
49
+ </svg>
50
+ </div>
51
+ </section>
52
+ <section class="page-width enter enter-late" aria-label="Backend connection">
53
+ <div class="flex flex-wrap items-center justify-between gap-5 border-y border-line py-5">
54
+ <div class="flex items-center gap-3">
55
+ <span class="size-2 rounded-full" :class="state === 'online' ? 'bg-emerald-700' : state === 'offline' ? 'bg-accent' : 'bg-muted'" aria-hidden="true"></span>
56
+ <span role="status" aria-live="polite" class="text-sm font-semibold">{{ status }}</span>
57
+ <code class="hidden text-xs text-muted sm:inline">GET /api/health</code>
58
+ </div>
59
+ <button type="button" class="text-link -my-2" :disabled="state === 'checking'" @click="checkHealth">{{ state === 'checking' ? 'Checking…' : 'Check again' }} <span aria-hidden="true">↻</span></button>
60
+ </div>
61
+ <p v-if="state === 'offline'" class="mt-3 text-sm text-muted">Start the app with <code>npm run dev</code>, then check again.</p>
62
+ </section>
63
+ <section id="start" class="page-width py-14 lg:py-20">
64
+ <div class="mb-9 flex flex-wrap items-end justify-between gap-4">
65
+ <h2 class="text-3xl font-medium tracking-tight">Small start. Endless possibilities.</h2>
66
+ <span class="eyebrow">Three places to begin</span>
67
+ </div>
68
+ <div class="grid gap-8 md:grid-cols-3 md:gap-10">
69
+ <article class="border-t border-line pt-5">
70
+ <span class="eyebrow text-accent">01 / The interface</span>
71
+ <h3 class="mb-3 mt-5 text-xl font-semibold tracking-tight">Find your own look.</h3>
72
+ <p class="text-sm leading-6 text-muted">Edit this page, add another page, and make it feel like you. Nuxt routing and Tailwind are already wired in.</p>
73
+ <code class="mt-5 block break-all text-xs">frontend/app/pages/index.vue</code>
74
+ </article>
75
+ <article class="border-t border-line pt-5">
76
+ <span class="eyebrow text-accent">02 / The backend</span>
77
+ <h3 class="mb-3 mt-5 text-xl font-semibold tracking-tight">Give your idea a voice.</h3>
78
+ <p class="text-sm leading-6 text-muted">Your first endpoint is live. Build on the health controller to add your own API.</p>
79
+ <code class="mt-5 block text-xs">src/features/health/</code>
80
+ </article>
81
+ <article class="border-t border-line pt-5">
82
+ <span class="eyebrow text-accent">03 / The connection</span>
83
+ <h3 class="mb-3 mt-5 text-xl font-semibold tracking-tight">Bring it all together.</h3>
84
+ <p class="text-sm leading-6 text-muted">Call your API with a same-origin request. Nuxt and Honovel take it from there.</p>
85
+ <code class="mt-5 block text-xs">fetch('/api/health')</code>
86
+ </article>
87
+ </div>
88
+ </section>
89
+ </main>
90
+ </template>
@@ -0,0 +1,22 @@
1
+ import tailwindcss from '@tailwindcss/vite'
2
+
3
+ const backendDevUrl = process.env.HONOVEL_BACKEND_DEV_URL || 'http://127.0.0.1:8788'
4
+
5
+ export default defineNuxtConfig({
6
+ compatibilityDate: '2026-10-02',
7
+ app: {
8
+ head: { link: [{ rel: 'icon', type: 'image/png', href: '/honovel-icon.png' }] },
9
+ },
10
+ devtools: { enabled: true },
11
+ css: ['~/assets/css/main.css'],
12
+ vite: {
13
+ plugins: [tailwindcss()],
14
+ server: {
15
+ proxy: {
16
+ '/api': backendDevUrl,
17
+ '/swagger': backendDevUrl,
18
+ '/openapi.json': backendDevUrl,
19
+ },
20
+ },
21
+ },
22
+ })
@@ -0,0 +1,9 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./.nuxt/tsconfig.app.json" },
5
+ { "path": "./.nuxt/tsconfig.server.json" },
6
+ { "path": "./.nuxt/tsconfig.shared.json" },
7
+ { "path": "./.nuxt/tsconfig.node.json" }
8
+ ]
9
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "scripts": {
3
+ "typecheck:frontend": "nuxt prepare frontend && nuxt typecheck frontend",
4
+ "typecheck": "npm run typecheck:backend && npm run typecheck:frontend",
5
+ "build:frontend": "honovel frontend:build",
6
+ "build": "npm run cf-typegen && npm run typecheck:frontend && npm run build:frontend && honovel cloudflare:deploy --dry-run --outdir dist/worker",
7
+ "dev:frontend": "nuxt dev frontend --port 5173 --host localhost --strictPort",
8
+ "dev": "honovel cloudflare:dev",
9
+ "dev:debug": "honovel cloudflare:dev --debug",
10
+ "deploy": "npm run cf-typegen && npm run build:frontend && honovel cloudflare:deploy --minify"
11
+ },
12
+ "dependencies": {
13
+ "@fontsource-variable/manrope": "^5.2.8",
14
+ "nuxt": "^4.6.4",
15
+ "vue": "^3.5.22"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/vite": "^4.1.0",
19
+ "tailwindcss": "^4.1.0",
20
+ "vite": "^7.3.7",
21
+ "vue-tsc": "^3.1.1"
22
+ },
23
+ "overrides": {
24
+ "@vercel/nft": {
25
+ "glob": "^14.0.2"
26
+ },
27
+ "readdir-glob": {
28
+ "minimatch": "^11.0.1"
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "assets": {
3
+ "directory": "./dist/frontend",
4
+ "not_found_handling": "single-page-application",
5
+ "run_worker_first": [
6
+ "/api/*",
7
+ "/swagger",
8
+ "/openapi.json"
9
+ ]
10
+ }
11
+ }