@sohan_fahad/wilt 0.1.2
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/LICENSE +21 -0
- package/README.md +250 -0
- package/bin/commands/add.ts +155 -0
- package/bin/commands/generate.ts +74 -0
- package/bin/commands/new.ts +457 -0
- package/bin/create-wilt-app.ts +3 -0
- package/bin/generators/module.generator.ts +266 -0
- package/bin/utils/config.ts +46 -0
- package/bin/utils/paths.ts +32 -0
- package/bin/utils/wrangler.ts +216 -0
- package/bin/wilt.ts +79 -0
- package/dist/lib/bin/create-wilt-app.js +413 -0
- package/dist/lib/bin/create-wilt-app.js.map +1 -0
- package/dist/lib/bin/wilt.js +1151 -0
- package/dist/lib/bin/wilt.js.map +1 -0
- package/dist/lib/chunk-EUXUH3YW.js +15 -0
- package/dist/lib/chunk-EUXUH3YW.js.map +1 -0
- package/dist/lib/chunk-FIEODUMV.js +234 -0
- package/dist/lib/chunk-FIEODUMV.js.map +1 -0
- package/dist/lib/chunk-MOVXD653.cjs +234 -0
- package/dist/lib/chunk-MOVXD653.cjs.map +1 -0
- package/dist/lib/chunk-ZBDE64SD.cjs +15 -0
- package/dist/lib/chunk-ZBDE64SD.cjs.map +1 -0
- package/dist/lib/config.cjs +10 -0
- package/dist/lib/config.cjs.map +1 -0
- package/dist/lib/config.d.cts +13 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.js +10 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/index.cjs +974 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +255 -0
- package/dist/lib/index.d.ts +255 -0
- package/dist/lib/index.js +974 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/middleware/index.cjs +10 -0
- package/dist/lib/middleware/index.cjs.map +1 -0
- package/dist/lib/middleware/index.d.cts +18 -0
- package/dist/lib/middleware/index.d.ts +18 -0
- package/dist/lib/middleware/index.js +10 -0
- package/dist/lib/middleware/index.js.map +1 -0
- package/package.json +78 -0
- package/src/wilt/README.md +285 -0
- package/src/wilt/config.ts +14 -0
- package/src/wilt/context/execution-context.ts +36 -0
- package/src/wilt/context/index.ts +1 -0
- package/src/wilt/decorators/core/exception-filters.decorator.ts +24 -0
- package/src/wilt/decorators/core/index.ts +6 -0
- package/src/wilt/decorators/core/injectable.decorator.ts +41 -0
- package/src/wilt/decorators/core/optional.decorator.ts +9 -0
- package/src/wilt/decorators/core/set-metadata.decorator.ts +20 -0
- package/src/wilt/decorators/core/use-guards.decorator.ts +14 -0
- package/src/wilt/decorators/core/use-interceptors.decorator.ts +16 -0
- package/src/wilt/decorators/http/controller.decorator.ts +230 -0
- package/src/wilt/decorators/http/header.decorator.ts +11 -0
- package/src/wilt/decorators/http/http-code.decorator.ts +8 -0
- package/src/wilt/decorators/http/index.ts +6 -0
- package/src/wilt/decorators/http/redirect.decorator.ts +13 -0
- package/src/wilt/decorators/http/route-mapping.decorator.ts +22 -0
- package/src/wilt/decorators/http/route-params.decorator.ts +60 -0
- package/src/wilt/decorators/index.ts +3 -0
- package/src/wilt/decorators/modules/global.decorator.ts +8 -0
- package/src/wilt/decorators/modules/index.ts +2 -0
- package/src/wilt/decorators/modules/module.decorator.ts +16 -0
- package/src/wilt/exceptions/http-exception.ts +17 -0
- package/src/wilt/exceptions/http-exceptions.ts +85 -0
- package/src/wilt/exceptions/index.ts +2 -0
- package/src/wilt/index.ts +11 -0
- package/src/wilt/injector/index.ts +1 -0
- package/src/wilt/injector/injector.ts +103 -0
- package/src/wilt/injector/module-compiler.ts +48 -0
- package/src/wilt/injector/module.factory.ts +74 -0
- package/src/wilt/interfaces/core/filter.interface.ts +5 -0
- package/src/wilt/interfaces/core/guard.interface.ts +5 -0
- package/src/wilt/interfaces/core/index.ts +5 -0
- package/src/wilt/interfaces/core/interceptor.interface.ts +9 -0
- package/src/wilt/interfaces/core/lifecycle.interface.ts +19 -0
- package/src/wilt/interfaces/core/pipe.interface.ts +9 -0
- package/src/wilt/interfaces/http/index.ts +1 -0
- package/src/wilt/interfaces/http/response.interface.ts +27 -0
- package/src/wilt/interfaces/index.ts +3 -0
- package/src/wilt/interfaces/modules/index.ts +1 -0
- package/src/wilt/interfaces/modules/module.interface.ts +17 -0
- package/src/wilt/middleware/error-handler.middleware.ts +63 -0
- package/src/wilt/middleware/index.ts +2 -0
- package/src/wilt/middleware/request-logger.middleware.ts +17 -0
- package/src/wilt/pipes/index.ts +3 -0
- package/src/wilt/pipes/validate.pipe.ts +79 -0
- package/src/wilt/pipes/zod-query.pipe.ts +42 -0
- package/src/wilt/pipes/zod-validate.pipe.ts +49 -0
- package/src/wilt/services/index.ts +1 -0
- package/src/wilt/services/reflector.service.ts +24 -0
- package/src/wilt/utils/apply-decorators.util.ts +17 -0
- package/src/wilt/utils/forward-ref.util.ts +14 -0
- package/src/wilt/utils/index.ts +22 -0
- package/src/wilt/utils/logger.util.ts +189 -0
- package/src/wilt/utils/response.util.ts +72 -0
|
@@ -0,0 +1,1151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/commands/new.ts
|
|
4
|
+
import { mkdirSync, writeFileSync, existsSync } from "fs";
|
|
5
|
+
import { join, resolve } from "path";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
function pascal(name) {
|
|
8
|
+
return name.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
function kebab(name) {
|
|
11
|
+
return name.replace(/([A-Z])/g, "-$1").replace(/^-/, "").replace(/_/g, "-").toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
function packageJsonTemplate(name) {
|
|
14
|
+
return JSON.stringify(
|
|
15
|
+
{
|
|
16
|
+
name: kebab(name),
|
|
17
|
+
version: "0.0.1",
|
|
18
|
+
private: true,
|
|
19
|
+
type: "module",
|
|
20
|
+
scripts: {
|
|
21
|
+
dev: "vite dev --host --port 4001",
|
|
22
|
+
"dev:cf": "wrangler dev --port 4001",
|
|
23
|
+
build: "vite build",
|
|
24
|
+
deploy: "vite build && wrangler deploy",
|
|
25
|
+
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
|
|
26
|
+
test: "vitest --run",
|
|
27
|
+
"test:watch": "vitest --watch",
|
|
28
|
+
"db:gen": "drizzle-kit generate",
|
|
29
|
+
"db:migrate": "wrangler d1 migrations apply DB --local",
|
|
30
|
+
"db:migrate:remote": "wrangler d1 migrations apply DB --remote",
|
|
31
|
+
check: "tsc --noEmit",
|
|
32
|
+
wilt: "wilt"
|
|
33
|
+
},
|
|
34
|
+
dependencies: {
|
|
35
|
+
hono: "^4.12.0",
|
|
36
|
+
wilt: "^0.1.0",
|
|
37
|
+
"reflect-metadata": "^0.2.2",
|
|
38
|
+
tsyringe: "^4.10.0",
|
|
39
|
+
zod: "^4.0.0",
|
|
40
|
+
"drizzle-orm": "^0.45.0",
|
|
41
|
+
ulid: "^3.0.0"
|
|
42
|
+
},
|
|
43
|
+
devDependencies: {
|
|
44
|
+
"@cloudflare/vite-plugin": "^1.36.0",
|
|
45
|
+
"@cloudflare/workers-types": "^4.0.0",
|
|
46
|
+
"@cloudflare/vitest-pool-workers": "^0.16.0",
|
|
47
|
+
"@sohan_fahad/wilt": "^0.1.0",
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"drizzle-kit": "^0.31.0",
|
|
50
|
+
typescript: "^5.0.0",
|
|
51
|
+
vite: "^6.0.0",
|
|
52
|
+
vitest: "^4.1.0",
|
|
53
|
+
tsx: "^4.0.0",
|
|
54
|
+
wrangler: "^4.0.0"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
null,
|
|
58
|
+
2
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function wranglerTemplate(name) {
|
|
62
|
+
return `{
|
|
63
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
64
|
+
"name": "${kebab(name)}",
|
|
65
|
+
"main": "./src/index.ts",
|
|
66
|
+
"compatibility_date": "2025-06-17",
|
|
67
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
68
|
+
"vars": {
|
|
69
|
+
"MODE": "development"
|
|
70
|
+
},
|
|
71
|
+
"observability": { "enabled": true },
|
|
72
|
+
"d1_databases": [
|
|
73
|
+
{
|
|
74
|
+
"binding": "DB",
|
|
75
|
+
"database_name": "REPLACE_WITH_YOUR_D1_NAME",
|
|
76
|
+
"database_id": "00000000-0000-0000-0000-000000000001",
|
|
77
|
+
"migrations_dir": "migrations",
|
|
78
|
+
"preview_database_id": "00000000-0000-0000-0000-000000000002"
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"dev": { "port": 4001 }
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
function tsconfigTemplate() {
|
|
86
|
+
return `{
|
|
87
|
+
"compilerOptions": {
|
|
88
|
+
"target": "ESNext",
|
|
89
|
+
"module": "ESNext",
|
|
90
|
+
"moduleResolution": "Bundler",
|
|
91
|
+
"strict": true,
|
|
92
|
+
"skipLibCheck": true,
|
|
93
|
+
"experimentalDecorators": true,
|
|
94
|
+
"emitDecoratorMetadata": true,
|
|
95
|
+
"lib": ["ESNext", "DOM"],
|
|
96
|
+
"types": ["vite/client", "@cloudflare/workers-types"],
|
|
97
|
+
"baseUrl": ".",
|
|
98
|
+
"paths": {
|
|
99
|
+
"@app/*": ["src/*"]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"include": ["src/**/*", "worker-configuration.d.ts"],
|
|
103
|
+
"exclude": ["node_modules", "dist"]
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
function viteConfigTemplate() {
|
|
108
|
+
return `import { defineConfig } from "vite";
|
|
109
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
110
|
+
import { fileURLToPath, URL } from "node:url";
|
|
111
|
+
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
plugins: [cloudflare()],
|
|
114
|
+
resolve: {
|
|
115
|
+
alias: {
|
|
116
|
+
"@app": fileURLToPath(new URL("./src", import.meta.url)),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
function workerTypesTemplate() {
|
|
123
|
+
return `// Generated by \`pnpm cf-typegen\`. Do not edit manually.
|
|
124
|
+
/// <reference types="@cloudflare/vitest-pool-workers/types" />
|
|
125
|
+
interface CloudflareBindings {
|
|
126
|
+
DB: D1Database;
|
|
127
|
+
MODE: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
declare namespace Cloudflare {
|
|
131
|
+
interface Env extends CloudflareBindings {
|
|
132
|
+
TEST_MIGRATIONS: string;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
function vitestConfigTemplate() {
|
|
138
|
+
return `import path from "path";
|
|
139
|
+
import { defineConfig } from "vitest/config";
|
|
140
|
+
import { cloudflarePool, cloudflareTest, readD1Migrations } from "@cloudflare/vitest-pool-workers";
|
|
141
|
+
|
|
142
|
+
export default defineConfig(async () => {
|
|
143
|
+
const migrations = await readD1Migrations("./migrations");
|
|
144
|
+
const poolOptions = {
|
|
145
|
+
wrangler: { configPath: "./wrangler.jsonc" },
|
|
146
|
+
miniflare: {
|
|
147
|
+
bindings: { TEST_MIGRATIONS: JSON.stringify(migrations) },
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
plugins: [cloudflareTest(poolOptions)],
|
|
152
|
+
resolve: {
|
|
153
|
+
alias: { "@app": path.resolve("./src") },
|
|
154
|
+
},
|
|
155
|
+
test: {
|
|
156
|
+
pool: cloudflarePool(poolOptions),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
function indexTemplate() {
|
|
163
|
+
return `import "reflect-metadata";
|
|
164
|
+
import { app } from "./app.module";
|
|
165
|
+
|
|
166
|
+
export default {
|
|
167
|
+
async fetch(request: Request, env: CloudflareBindings, ctx: ExecutionContext): Promise<Response> {
|
|
168
|
+
return app.fetch(request, env, ctx);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
function appModuleTemplate(name) {
|
|
174
|
+
const p = pascal(name);
|
|
175
|
+
return `import { cors } from "hono/cors";
|
|
176
|
+
import { secureHeaders } from "hono/secure-headers";
|
|
177
|
+
|
|
178
|
+
import { Module, createModule } from "wilt";
|
|
179
|
+
import { requestLogger } from "wilt/middleware";
|
|
180
|
+
import { HelloModule } from "./modules/app/hello/hello.module";
|
|
181
|
+
|
|
182
|
+
@Module({
|
|
183
|
+
imports: [HelloModule],
|
|
184
|
+
})
|
|
185
|
+
export class AppModule {}
|
|
186
|
+
|
|
187
|
+
const app = createModule(AppModule, {
|
|
188
|
+
middlewares: [cors(), secureHeaders(), requestLogger],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
app.get("/", (c) => c.json({ success: true, status: "healthy", timestamp: new Date().toISOString() }));
|
|
192
|
+
app.get("/health", (c) => c.json({ success: true, status: "healthy", timestamp: new Date().toISOString() }));
|
|
193
|
+
|
|
194
|
+
export { app };
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
function helloModuleTemplate() {
|
|
198
|
+
return `import { Module } from "wilt";
|
|
199
|
+
import { HelloController } from "./hello.controller";
|
|
200
|
+
import { HelloService } from "./hello.service";
|
|
201
|
+
|
|
202
|
+
@Module({
|
|
203
|
+
controllers: [HelloController],
|
|
204
|
+
providers: [HelloService],
|
|
205
|
+
})
|
|
206
|
+
export class HelloModule {}
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
function helloControllerTemplate() {
|
|
210
|
+
return `import type { Context } from "hono";
|
|
211
|
+
import { Controller, Get, Inject } from "wilt";
|
|
212
|
+
import { ResponseUtil } from "wilt";
|
|
213
|
+
import { HelloService } from "./hello.service";
|
|
214
|
+
|
|
215
|
+
@Controller("/hello")
|
|
216
|
+
export class HelloController {
|
|
217
|
+
constructor(@Inject(HelloService) private helloService: HelloService) {}
|
|
218
|
+
|
|
219
|
+
@Get()
|
|
220
|
+
async greet(c: Context) {
|
|
221
|
+
const message = this.helloService.greet();
|
|
222
|
+
return ResponseUtil.success(c, { message });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
function helloServiceTemplate() {
|
|
228
|
+
return `import { Injectable } from "wilt";
|
|
229
|
+
|
|
230
|
+
@Injectable()
|
|
231
|
+
export class HelloService {
|
|
232
|
+
greet() {
|
|
233
|
+
return "Hello from Wilt!";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
function dbSchemaTemplate() {
|
|
239
|
+
return `// Register all your entity tables here so Drizzle can build relations and
|
|
240
|
+
// drizzle-kit can generate migrations from a single source of truth.
|
|
241
|
+
export const schema = {};
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
function dbConnectionTemplate() {
|
|
245
|
+
return `import { drizzle } from "drizzle-orm/d1";
|
|
246
|
+
import type { Context } from "hono";
|
|
247
|
+
import { schema } from "./schema";
|
|
248
|
+
export function getDb(c: Context) {
|
|
249
|
+
return drizzle(c.env.DB, { schema: schema });
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
function gitignoreTemplate() {
|
|
254
|
+
return `dist/
|
|
255
|
+
node_modules/
|
|
256
|
+
.wrangler
|
|
257
|
+
.env
|
|
258
|
+
.env.production
|
|
259
|
+
.dev.vars
|
|
260
|
+
.DS_Store
|
|
261
|
+
coverage/
|
|
262
|
+
*.log
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
function wiltConfigTemplate() {
|
|
266
|
+
return `import { defineConfig } from "wilt/config";
|
|
267
|
+
|
|
268
|
+
export default defineConfig({
|
|
269
|
+
// Directory where generated modules are placed (relative to project root)
|
|
270
|
+
modulesDir: "src/modules/app",
|
|
271
|
+
|
|
272
|
+
// Default files scaffolded by \`wilt generate module <name>\`
|
|
273
|
+
// Remove "test" to skip test files, or pass --no-test at the CLI
|
|
274
|
+
generate: {
|
|
275
|
+
files: ["module", "controller", "service", "dto", "entity", "test"],
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
function envExampleTemplate() {
|
|
281
|
+
return `CLOUDFLARE_ACCOUNT_ID=
|
|
282
|
+
CLOUDFLARE_TOKEN=
|
|
283
|
+
|
|
284
|
+
CLOUDFLARE_DATABASE_ID=
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
function drizzleConfigTemplate() {
|
|
288
|
+
return `import { defineConfig } from "drizzle-kit";
|
|
289
|
+
|
|
290
|
+
export default defineConfig({
|
|
291
|
+
schema: "./src/database/schema.ts",
|
|
292
|
+
out: "./migrations",
|
|
293
|
+
dialect: "sqlite",
|
|
294
|
+
driver: "d1-http",
|
|
295
|
+
dbCredentials: {
|
|
296
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
|
|
297
|
+
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
|
|
298
|
+
token: process.env.CLOUDFLARE_TOKEN!,
|
|
299
|
+
},
|
|
300
|
+
verbose: true,
|
|
301
|
+
strict: true,
|
|
302
|
+
});
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
function migrationTemplate() {
|
|
306
|
+
return `-- Initial migration
|
|
307
|
+
-- Add your tables here
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
function migrationMetaTemplate(name) {
|
|
311
|
+
return JSON.stringify(
|
|
312
|
+
{
|
|
313
|
+
version: "5",
|
|
314
|
+
dialect: "sqlite",
|
|
315
|
+
entries: [
|
|
316
|
+
{
|
|
317
|
+
idx: 0,
|
|
318
|
+
version: "5",
|
|
319
|
+
when: Date.now(),
|
|
320
|
+
tag: "0000_initial",
|
|
321
|
+
breakpoints: true
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
null,
|
|
326
|
+
2
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
function write(path, content) {
|
|
330
|
+
writeFileSync(path, content, "utf-8");
|
|
331
|
+
console.log(` \u2714 ${path.replace(process.cwd() + "/", "")}`);
|
|
332
|
+
}
|
|
333
|
+
function dir(path) {
|
|
334
|
+
mkdirSync(path, { recursive: true });
|
|
335
|
+
}
|
|
336
|
+
function runNew(args) {
|
|
337
|
+
const [name] = args;
|
|
338
|
+
if (!name) {
|
|
339
|
+
console.error(" \u2717 Usage: wilt new <project-name>");
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
const projectDir = resolve(process.cwd(), name);
|
|
343
|
+
if (existsSync(projectDir)) {
|
|
344
|
+
console.error(` \u2717 Directory already exists: ${projectDir}`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
console.log(`
|
|
348
|
+
Creating Wilt app "${name}"...
|
|
349
|
+
`);
|
|
350
|
+
dir(join(projectDir, "src/modules/app/hello"));
|
|
351
|
+
dir(join(projectDir, "src/database"));
|
|
352
|
+
dir(join(projectDir, "migrations/meta"));
|
|
353
|
+
write(join(projectDir, "package.json"), packageJsonTemplate(name));
|
|
354
|
+
write(join(projectDir, "wrangler.jsonc"), wranglerTemplate(name));
|
|
355
|
+
write(join(projectDir, "tsconfig.json"), tsconfigTemplate());
|
|
356
|
+
write(join(projectDir, "vite.config.ts"), viteConfigTemplate());
|
|
357
|
+
write(join(projectDir, "vitest.config.ts"), vitestConfigTemplate());
|
|
358
|
+
write(join(projectDir, "worker-configuration.d.ts"), workerTypesTemplate());
|
|
359
|
+
write(join(projectDir, "wilt.config.ts"), wiltConfigTemplate());
|
|
360
|
+
write(join(projectDir, ".gitignore"), gitignoreTemplate());
|
|
361
|
+
write(join(projectDir, ".env.example"), envExampleTemplate());
|
|
362
|
+
write(join(projectDir, "drizzle.config.ts"), drizzleConfigTemplate());
|
|
363
|
+
write(join(projectDir, "src/index.ts"), indexTemplate());
|
|
364
|
+
write(join(projectDir, "src/app.module.ts"), appModuleTemplate(name));
|
|
365
|
+
write(join(projectDir, "src/modules/app/hello/hello.module.ts"), helloModuleTemplate());
|
|
366
|
+
write(join(projectDir, "src/modules/app/hello/hello.controller.ts"), helloControllerTemplate());
|
|
367
|
+
write(join(projectDir, "src/modules/app/hello/hello.service.ts"), helloServiceTemplate());
|
|
368
|
+
write(join(projectDir, "src/database/schema.ts"), dbSchemaTemplate());
|
|
369
|
+
write(join(projectDir, "src/database/connection.ts"), dbConnectionTemplate());
|
|
370
|
+
write(join(projectDir, "migrations/0000_initial.sql"), migrationTemplate());
|
|
371
|
+
write(join(projectDir, "migrations/meta/_journal.json"), migrationMetaTemplate(name));
|
|
372
|
+
console.log("\n Installing dependencies...\n");
|
|
373
|
+
try {
|
|
374
|
+
const pm = detectPackageManager();
|
|
375
|
+
execSync(`${pm} install`, { cwd: projectDir, stdio: "inherit" });
|
|
376
|
+
} catch {
|
|
377
|
+
console.log(" \u26A0 Could not auto-install. Run `pnpm install` manually.\n");
|
|
378
|
+
}
|
|
379
|
+
console.log(`
|
|
380
|
+
\u2714 Project created!
|
|
381
|
+
|
|
382
|
+
Next steps:
|
|
383
|
+
cd ${name}
|
|
384
|
+
cp .env.example .env # fill in Cloudflare credentials
|
|
385
|
+
# Edit wrangler.jsonc: set database_name + database_id
|
|
386
|
+
pnpm db:migrate # apply local D1 migrations
|
|
387
|
+
pnpm dev # http://localhost:4001
|
|
388
|
+
|
|
389
|
+
Try it:
|
|
390
|
+
curl http://localhost:4001/health
|
|
391
|
+
curl http://localhost:4001/hello
|
|
392
|
+
|
|
393
|
+
Generate a new module:
|
|
394
|
+
pnpm wilt generate module posts
|
|
395
|
+
`);
|
|
396
|
+
}
|
|
397
|
+
function detectPackageManager() {
|
|
398
|
+
try {
|
|
399
|
+
execSync("pnpm --version", { stdio: "ignore" });
|
|
400
|
+
return "pnpm";
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
execSync("bun --version", { stdio: "ignore" });
|
|
405
|
+
return "bun";
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
return "npm";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// bin/generators/module.generator.ts
|
|
412
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
413
|
+
import { join as join3 } from "path";
|
|
414
|
+
|
|
415
|
+
// bin/utils/paths.ts
|
|
416
|
+
import { join as join2 } from "path";
|
|
417
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
418
|
+
var PROJECT_ROOT = process.cwd();
|
|
419
|
+
var SRC_DIR = join2(PROJECT_ROOT, "src");
|
|
420
|
+
var MODULES_DIR = join2(SRC_DIR, "modules");
|
|
421
|
+
var APP_MODULES_DIR = join2(MODULES_DIR, "app");
|
|
422
|
+
var WRANGLER_JSONC = join2(PROJECT_ROOT, "wrangler.jsonc");
|
|
423
|
+
var APP_MODULE_FILE = join2(SRC_DIR, "app.module.ts");
|
|
424
|
+
function wiltImportPath() {
|
|
425
|
+
return "wilt";
|
|
426
|
+
}
|
|
427
|
+
function ensureDir(dir2) {
|
|
428
|
+
if (!existsSync2(dir2)) {
|
|
429
|
+
mkdirSync2(dir2, { recursive: true });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// bin/generators/module.generator.ts
|
|
434
|
+
function toPascalCase(name) {
|
|
435
|
+
return name.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
436
|
+
}
|
|
437
|
+
function toCamelCase(name) {
|
|
438
|
+
const pascal2 = toPascalCase(name);
|
|
439
|
+
return pascal2.charAt(0).toLowerCase() + pascal2.slice(1);
|
|
440
|
+
}
|
|
441
|
+
function toKebabCase(name) {
|
|
442
|
+
return name.replace(/([A-Z])/g, "-$1").replace(/^-/, "").replace(/_/g, "-").toLowerCase();
|
|
443
|
+
}
|
|
444
|
+
function pluralize(name) {
|
|
445
|
+
return name.endsWith("s") ? name : name + "s";
|
|
446
|
+
}
|
|
447
|
+
function moduleTemplate(name, wiltPath) {
|
|
448
|
+
const pascal2 = toPascalCase(name);
|
|
449
|
+
const kebab2 = toKebabCase(name);
|
|
450
|
+
return `import { Module } from "${wiltPath}";
|
|
451
|
+
import { ${pascal2}Controller } from "./${kebab2}.controller";
|
|
452
|
+
import { ${pascal2}Service } from "./${kebab2}.service";
|
|
453
|
+
|
|
454
|
+
@Module({
|
|
455
|
+
controllers: [${pascal2}Controller],
|
|
456
|
+
providers: [${pascal2}Service],
|
|
457
|
+
exports: [${pascal2}Service],
|
|
458
|
+
})
|
|
459
|
+
export class ${pascal2}Module {}
|
|
460
|
+
`;
|
|
461
|
+
}
|
|
462
|
+
function entityTemplate(name) {
|
|
463
|
+
const camel = toCamelCase(name);
|
|
464
|
+
const kebab2 = toKebabCase(name);
|
|
465
|
+
const pascal2 = toPascalCase(name);
|
|
466
|
+
const entityVar = pluralize(camel);
|
|
467
|
+
const tableName = pluralize(kebab2);
|
|
468
|
+
return `import { sqliteTable, integer } from "drizzle-orm/sqlite-core";
|
|
469
|
+
|
|
470
|
+
export const ${entityVar} = sqliteTable("${tableName}", {
|
|
471
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
472
|
+
createdAt: integer("created_at", { mode: "timestamp" })
|
|
473
|
+
.$defaultFn(() => new Date())
|
|
474
|
+
.notNull(),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
export type ${pascal2} = typeof ${entityVar}.$inferSelect;
|
|
478
|
+
export type New${pascal2} = typeof ${entityVar}.$inferInsert;
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
function controllerTemplate(name, wiltPath) {
|
|
482
|
+
const pascal2 = toPascalCase(name);
|
|
483
|
+
const camel = toCamelCase(name);
|
|
484
|
+
const kebab2 = toKebabCase(name);
|
|
485
|
+
return `import type { Context } from "hono";
|
|
486
|
+
import { Controller, Get, Inject } from "${wiltPath}";
|
|
487
|
+
import { ResponseUtil } from "${wiltPath}";
|
|
488
|
+
import { ${pascal2}Service } from "./${kebab2}.service";
|
|
489
|
+
|
|
490
|
+
@Controller("/${kebab2}")
|
|
491
|
+
export class ${pascal2}Controller {
|
|
492
|
+
constructor(@Inject(${pascal2}Service) private ${camel}Service: ${pascal2}Service) {}
|
|
493
|
+
|
|
494
|
+
@Get()
|
|
495
|
+
async getAll(c: Context) {
|
|
496
|
+
const data = await this.${camel}Service.findAll();
|
|
497
|
+
return ResponseUtil.success(c, data);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
`;
|
|
501
|
+
}
|
|
502
|
+
function serviceTemplate(name, wiltPath) {
|
|
503
|
+
const pascal2 = toPascalCase(name);
|
|
504
|
+
return `import { Injectable } from "${wiltPath}";
|
|
505
|
+
import { createDatabase } from "../../../database/connection";
|
|
506
|
+
|
|
507
|
+
@Injectable()
|
|
508
|
+
export class ${pascal2}Service {
|
|
509
|
+
|
|
510
|
+
private getDatabase(env: CloudflareBindings) {
|
|
511
|
+
return createDatabase(env.DB);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async findAll() {
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
`;
|
|
519
|
+
}
|
|
520
|
+
function testTemplate(name) {
|
|
521
|
+
const kebab2 = toKebabCase(name);
|
|
522
|
+
const pascal2 = toPascalCase(name);
|
|
523
|
+
const table = pluralize(kebab2.replace(/-/g, "_"));
|
|
524
|
+
return `import { describe, it, expect, beforeAll, afterEach } from "vitest";
|
|
525
|
+
import { SELF, env, applyD1Migrations } from "cloudflare:test";
|
|
526
|
+
import { ${pascal2}Service } from "./${kebab2}.service";
|
|
527
|
+
|
|
528
|
+
const BASE = "http://localhost";
|
|
529
|
+
const service = new ${pascal2}Service();
|
|
530
|
+
|
|
531
|
+
beforeAll(async () => {
|
|
532
|
+
await applyD1Migrations(env.DB, JSON.parse(env.TEST_MIGRATIONS));
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
afterEach(async () => {
|
|
536
|
+
await env.DB.prepare("DELETE FROM ${table}").run();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe("${pascal2}Service", () => {
|
|
540
|
+
it("should be defined", () => {
|
|
541
|
+
expect(service).toBeDefined();
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
describe("GET /${kebab2}", () => {
|
|
546
|
+
it("returns an empty list", async () => {
|
|
547
|
+
const res = await SELF.fetch(\`\${BASE}/${kebab2}\`);
|
|
548
|
+
expect(res.status).toBe(200);
|
|
549
|
+
const { data } = await res.json() as { data: unknown[] };
|
|
550
|
+
expect(data).toEqual([]);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
`;
|
|
554
|
+
}
|
|
555
|
+
function dtoTemplate(name) {
|
|
556
|
+
const pascal2 = toPascalCase(name);
|
|
557
|
+
return `import { z } from "zod";
|
|
558
|
+
|
|
559
|
+
export const Create${pascal2}Dto = z.object({
|
|
560
|
+
// TODO: define your fields
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
export const Update${pascal2}Dto = Create${pascal2}Dto.partial();
|
|
564
|
+
|
|
565
|
+
export type Create${pascal2}DtoType = z.infer<typeof Create${pascal2}Dto>;
|
|
566
|
+
export type Update${pascal2}DtoType = z.infer<typeof Update${pascal2}Dto>;
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
function generateModule(name, options = {}) {
|
|
570
|
+
const kebab2 = toKebabCase(name);
|
|
571
|
+
const pascal2 = toPascalCase(name);
|
|
572
|
+
const basedir = options.modulesDir ?? APP_MODULES_DIR;
|
|
573
|
+
const moduleDir = options.dir ?? join3(basedir, kebab2);
|
|
574
|
+
let filesToGen = options.files ?? options.defaultFiles ?? ["module", "controller", "service", "dto", "entity", "test"];
|
|
575
|
+
if (options.noTest) filesToGen = filesToGen.filter((f) => f !== "test");
|
|
576
|
+
if (existsSync3(moduleDir)) {
|
|
577
|
+
console.error(` \u2717 Directory already exists: ${moduleDir}`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
ensureDir(moduleDir);
|
|
581
|
+
const wiltPath = wiltImportPath();
|
|
582
|
+
const hasEntity = filesToGen.includes("entity");
|
|
583
|
+
const fileMap = {
|
|
584
|
+
module: moduleTemplate(name, wiltPath),
|
|
585
|
+
controller: controllerTemplate(name, wiltPath),
|
|
586
|
+
service: serviceTemplate(name, wiltPath),
|
|
587
|
+
dto: dtoTemplate(name),
|
|
588
|
+
entity: entityTemplate(name),
|
|
589
|
+
test: testTemplate(name)
|
|
590
|
+
};
|
|
591
|
+
const extMap = {
|
|
592
|
+
module: `${kebab2}.module.ts`,
|
|
593
|
+
controller: `${kebab2}.controller.ts`,
|
|
594
|
+
service: `${kebab2}.service.ts`,
|
|
595
|
+
dto: `${kebab2}.dto.ts`,
|
|
596
|
+
entity: `${kebab2}.entity.ts`,
|
|
597
|
+
test: `${kebab2}.test.ts`
|
|
598
|
+
};
|
|
599
|
+
for (const file of filesToGen) {
|
|
600
|
+
const filePath = join3(moduleDir, extMap[file]);
|
|
601
|
+
writeFileSync2(filePath, fileMap[file], "utf-8");
|
|
602
|
+
console.log(` \u2714 Created ${filePath.replace(process.cwd() + "/", "")}`);
|
|
603
|
+
}
|
|
604
|
+
const relModuleDir = moduleDir.replace(process.cwd() + "/", "");
|
|
605
|
+
const entityLine = hasEntity ? `
|
|
606
|
+
2. Add the entity to src/database/schema.ts:
|
|
607
|
+
|
|
608
|
+
import { ${pluralize(toCamelCase(name))} } from "@app/${relModuleDir.replace(/^src\//, "")}/${kebab2}.entity";
|
|
609
|
+
|
|
610
|
+
export const schema = {
|
|
611
|
+
...existing,
|
|
612
|
+
${pluralize(toCamelCase(name))},
|
|
613
|
+
};
|
|
614
|
+
` : "";
|
|
615
|
+
console.log(`
|
|
616
|
+
Next steps \u2014
|
|
617
|
+
|
|
618
|
+
1. Register the module in src/app.module.ts:
|
|
619
|
+
|
|
620
|
+
import { ${pascal2}Module } from "./${relModuleDir.replace(/^src\//, "")}/${kebab2}.module";
|
|
621
|
+
|
|
622
|
+
@Module({
|
|
623
|
+
imports: [..., ${pascal2}Module],
|
|
624
|
+
})
|
|
625
|
+
export class AppModule {}
|
|
626
|
+
${entityLine} Then run \`pnpm db:gen\` to generate a migration.
|
|
627
|
+
`);
|
|
628
|
+
}
|
|
629
|
+
function generateService(name, dir2, modulesDir) {
|
|
630
|
+
const kebab2 = toKebabCase(name);
|
|
631
|
+
const basedir = modulesDir ?? APP_MODULES_DIR;
|
|
632
|
+
const targetDir = dir2 ?? join3(basedir, kebab2);
|
|
633
|
+
const wiltPath = wiltImportPath();
|
|
634
|
+
const filePath = join3(targetDir, `${kebab2}.service.ts`);
|
|
635
|
+
if (existsSync3(filePath)) {
|
|
636
|
+
console.error(` \u2717 File already exists: ${filePath}`);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
ensureDir(targetDir);
|
|
640
|
+
writeFileSync2(filePath, serviceTemplate(name, wiltPath), "utf-8");
|
|
641
|
+
console.log(` \u2714 Created ${filePath.replace(process.cwd() + "/", "")}`);
|
|
642
|
+
}
|
|
643
|
+
function generateController(name, dir2, modulesDir) {
|
|
644
|
+
const kebab2 = toKebabCase(name);
|
|
645
|
+
const basedir = modulesDir ?? APP_MODULES_DIR;
|
|
646
|
+
const targetDir = dir2 ?? join3(basedir, kebab2);
|
|
647
|
+
const wiltPath = wiltImportPath();
|
|
648
|
+
const filePath = join3(targetDir, `${kebab2}.controller.ts`);
|
|
649
|
+
if (existsSync3(filePath)) {
|
|
650
|
+
console.error(` \u2717 File already exists: ${filePath}`);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
ensureDir(targetDir);
|
|
654
|
+
writeFileSync2(filePath, controllerTemplate(name, wiltPath), "utf-8");
|
|
655
|
+
console.log(` \u2714 Created ${filePath.replace(process.cwd() + "/", "")}`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// bin/utils/config.ts
|
|
659
|
+
import { existsSync as existsSync4 } from "fs";
|
|
660
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
661
|
+
import { createJiti } from "jiti";
|
|
662
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
663
|
+
let userConfig = {};
|
|
664
|
+
for (const name of ["wilt.config.ts", "wilt.config.js", "wilt.config.mjs"]) {
|
|
665
|
+
const configPath = join4(cwd, name);
|
|
666
|
+
if (existsSync4(configPath)) {
|
|
667
|
+
try {
|
|
668
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
669
|
+
const mod = await jiti.import(configPath);
|
|
670
|
+
userConfig = mod ?? {};
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
modulesDir: resolve2(cwd, userConfig.modulesDir ?? "src/modules/app"),
|
|
678
|
+
srcDir: resolve2(cwd, userConfig.srcDir ?? "src"),
|
|
679
|
+
generate: {
|
|
680
|
+
files: userConfig.generate?.files ?? ["module", "controller", "service", "dto", "entity", "test"]
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// bin/commands/generate.ts
|
|
686
|
+
var USAGE = `
|
|
687
|
+
Usage:
|
|
688
|
+
pnpm wilt generate module <name> [--path <dir>] [--no-test]
|
|
689
|
+
pnpm wilt generate service <name> [--path <dir>]
|
|
690
|
+
pnpm wilt generate controller <name> [--path <dir>]
|
|
691
|
+
|
|
692
|
+
Aliases:
|
|
693
|
+
g m \u2192 generate module
|
|
694
|
+
g s \u2192 generate service
|
|
695
|
+
g c \u2192 generate controller
|
|
696
|
+
|
|
697
|
+
Flags:
|
|
698
|
+
--no-test Skip generating the .test.ts file for a module
|
|
699
|
+
|
|
700
|
+
Examples:
|
|
701
|
+
pnpm wilt generate module payment
|
|
702
|
+
pnpm wilt g m payment
|
|
703
|
+
pnpm wilt generate module payment --no-test
|
|
704
|
+
pnpm wilt generate service payment --path src/modules/app/payment
|
|
705
|
+
`.trim();
|
|
706
|
+
function parsePath(args) {
|
|
707
|
+
const idx = args.indexOf("--path");
|
|
708
|
+
if (idx === -1) return { args };
|
|
709
|
+
const path = args[idx + 1];
|
|
710
|
+
const cleaned = args.filter((_, i) => i !== idx && i !== idx + 1);
|
|
711
|
+
return { args: cleaned, path };
|
|
712
|
+
}
|
|
713
|
+
function parseNoTest(args) {
|
|
714
|
+
const idx = args.indexOf("--no-test");
|
|
715
|
+
if (idx === -1) return { args, noTest: false };
|
|
716
|
+
return { args: args.filter((_, i) => i !== idx), noTest: true };
|
|
717
|
+
}
|
|
718
|
+
async function runGenerate(args) {
|
|
719
|
+
const { args: withoutPath, path: targetPath } = parsePath(args);
|
|
720
|
+
const { args: cleanArgs, noTest } = parseNoTest(withoutPath);
|
|
721
|
+
const [subcommand, name] = cleanArgs;
|
|
722
|
+
if (!subcommand || !name) {
|
|
723
|
+
console.error(` \u2717 Missing arguments.
|
|
724
|
+
|
|
725
|
+
${USAGE}`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
const config = await loadConfig();
|
|
729
|
+
const sub = subcommand.toLowerCase();
|
|
730
|
+
if (sub === "module" || sub === "m") {
|
|
731
|
+
console.log(`
|
|
732
|
+
Generating module "${name}"...
|
|
733
|
+
`);
|
|
734
|
+
generateModule(name, {
|
|
735
|
+
dir: targetPath,
|
|
736
|
+
modulesDir: config.modulesDir,
|
|
737
|
+
defaultFiles: config.generate.files,
|
|
738
|
+
noTest
|
|
739
|
+
});
|
|
740
|
+
} else if (sub === "service" || sub === "s") {
|
|
741
|
+
console.log(`
|
|
742
|
+
Generating service "${name}"...
|
|
743
|
+
`);
|
|
744
|
+
generateService(name, targetPath, config.modulesDir);
|
|
745
|
+
} else if (sub === "controller" || sub === "c") {
|
|
746
|
+
console.log(`
|
|
747
|
+
Generating controller "${name}"...
|
|
748
|
+
`);
|
|
749
|
+
generateController(name, targetPath, config.modulesDir);
|
|
750
|
+
} else {
|
|
751
|
+
console.error(` \u2717 Unknown generate subcommand: "${subcommand}"
|
|
752
|
+
|
|
753
|
+
${USAGE}`);
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// bin/utils/wrangler.ts
|
|
759
|
+
import { readFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
760
|
+
function stripComments(text) {
|
|
761
|
+
return text.split("\n").map((line) => line.replace(/\s*\/\/.*$/, "")).join("\n");
|
|
762
|
+
}
|
|
763
|
+
function readWrangler() {
|
|
764
|
+
const raw = readFileSync(WRANGLER_JSONC, "utf-8");
|
|
765
|
+
return JSON.parse(stripComments(raw));
|
|
766
|
+
}
|
|
767
|
+
function writeWrangler(config) {
|
|
768
|
+
const header = `{
|
|
769
|
+
"$schema": "node_modules/wrangler/config-schema.json",`;
|
|
770
|
+
const body = JSON.stringify(config, null, 2);
|
|
771
|
+
const bodyWithoutSchema = body.replace(/^\{/, "").replace(/\s+"?\$schema"?\s*:\s*"[^"]*",?\n/, "\n");
|
|
772
|
+
writeFileSync3(WRANGLER_JSONC, header + bodyWithoutSchema, "utf-8");
|
|
773
|
+
}
|
|
774
|
+
function addD1(binding, databaseName) {
|
|
775
|
+
const config = readWrangler();
|
|
776
|
+
if (!config.d1_databases) config.d1_databases = [];
|
|
777
|
+
const alreadyExists = config.d1_databases.some(
|
|
778
|
+
(db) => db.binding === binding
|
|
779
|
+
);
|
|
780
|
+
if (alreadyExists) {
|
|
781
|
+
console.error(` \u2717 D1 binding "${binding}" already exists in wrangler.jsonc`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
config.d1_databases.push({
|
|
785
|
+
binding,
|
|
786
|
+
database_name: databaseName,
|
|
787
|
+
database_id: "00000000-0000-0000-0000-000000000000",
|
|
788
|
+
migrations_dir: "migrations"
|
|
789
|
+
});
|
|
790
|
+
writeWrangler(config);
|
|
791
|
+
console.log(` \u2714 Added D1 binding "${binding}" (database: ${databaseName})`);
|
|
792
|
+
console.log(` ! Remember to replace database_id with your actual D1 database ID.`);
|
|
793
|
+
}
|
|
794
|
+
function addR2(binding, bucketName) {
|
|
795
|
+
const config = readWrangler();
|
|
796
|
+
if (!config.r2_buckets) config.r2_buckets = [];
|
|
797
|
+
if (config.r2_buckets.some((b) => b.binding === binding)) {
|
|
798
|
+
console.error(` \u2717 R2 binding "${binding}" already exists in wrangler.jsonc`);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
config.r2_buckets.push({
|
|
802
|
+
binding,
|
|
803
|
+
bucket_name: bucketName,
|
|
804
|
+
preview_bucket_name: `${bucketName}-preview`
|
|
805
|
+
});
|
|
806
|
+
writeWrangler(config);
|
|
807
|
+
console.log(` \u2714 Added R2 binding "${binding}" (bucket: ${bucketName})`);
|
|
808
|
+
}
|
|
809
|
+
function addKv(binding) {
|
|
810
|
+
const config = readWrangler();
|
|
811
|
+
if (!config.kv_namespaces) config.kv_namespaces = [];
|
|
812
|
+
if (config.kv_namespaces.some((k) => k.binding === binding)) {
|
|
813
|
+
console.error(` \u2717 KV binding "${binding}" already exists in wrangler.jsonc`);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
config.kv_namespaces.push({
|
|
817
|
+
binding,
|
|
818
|
+
id: "00000000000000000000000000000000"
|
|
819
|
+
});
|
|
820
|
+
writeWrangler(config);
|
|
821
|
+
console.log(` \u2714 Added KV namespace binding "${binding}"`);
|
|
822
|
+
console.log(` ! Remember to replace id with your actual KV namespace ID.`);
|
|
823
|
+
}
|
|
824
|
+
function addQueue(binding, queueName) {
|
|
825
|
+
const config = readWrangler();
|
|
826
|
+
if (!config.queues) config.queues = { producers: [], consumers: [] };
|
|
827
|
+
if (!config.queues.producers) config.queues.producers = [];
|
|
828
|
+
if (!config.queues.consumers) config.queues.consumers = [];
|
|
829
|
+
if (config.queues.producers.some((p) => p.binding === binding)) {
|
|
830
|
+
console.error(` \u2717 Queue producer binding "${binding}" already exists in wrangler.jsonc`);
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
config.queues.producers.push({ binding, queue: queueName });
|
|
834
|
+
config.queues.consumers.push({
|
|
835
|
+
queue: queueName,
|
|
836
|
+
max_batch_size: 10,
|
|
837
|
+
max_batch_timeout: 10,
|
|
838
|
+
max_retries: 3
|
|
839
|
+
});
|
|
840
|
+
writeWrangler(config);
|
|
841
|
+
console.log(` \u2714 Added Queue binding "${binding}" (queue: ${queueName})`);
|
|
842
|
+
}
|
|
843
|
+
function addAi(binding) {
|
|
844
|
+
const config = readWrangler();
|
|
845
|
+
if (config.ai) {
|
|
846
|
+
console.error(` \u2717 AI binding already exists in wrangler.jsonc`);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
config.ai = { binding };
|
|
850
|
+
writeWrangler(config);
|
|
851
|
+
console.log(` \u2714 Added AI binding "${binding}"`);
|
|
852
|
+
}
|
|
853
|
+
function addDurableObject(binding, className) {
|
|
854
|
+
const config = readWrangler();
|
|
855
|
+
if (!config.durable_objects) config.durable_objects = { bindings: [] };
|
|
856
|
+
if (!config.durable_objects.bindings) config.durable_objects.bindings = [];
|
|
857
|
+
if (config.durable_objects.bindings.some((b) => b.name === binding)) {
|
|
858
|
+
console.error(` \u2717 Durable Object binding "${binding}" already exists in wrangler.jsonc`);
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
config.durable_objects.bindings.push({
|
|
862
|
+
name: binding,
|
|
863
|
+
class_name: className
|
|
864
|
+
});
|
|
865
|
+
if (!config.migrations) config.migrations = [];
|
|
866
|
+
const existingClasses = config.migrations.flatMap(
|
|
867
|
+
(m) => m.new_sqlite_classes ?? m.new_classes ?? []
|
|
868
|
+
);
|
|
869
|
+
if (!existingClasses.includes(className)) {
|
|
870
|
+
const nextTag = `v${config.migrations.length + 1}`;
|
|
871
|
+
config.migrations.push({
|
|
872
|
+
tag: nextTag,
|
|
873
|
+
new_sqlite_classes: [className]
|
|
874
|
+
});
|
|
875
|
+
console.log(` ! Added migration entry "${nextTag}" for ${className}.`);
|
|
876
|
+
}
|
|
877
|
+
writeWrangler(config);
|
|
878
|
+
console.log(` \u2714 Added Durable Object binding "${binding}" (class: ${className})`);
|
|
879
|
+
}
|
|
880
|
+
function addVectorize(binding, indexName, dimensions = 1536) {
|
|
881
|
+
const config = readWrangler();
|
|
882
|
+
if (!config.vectorize) config.vectorize = [];
|
|
883
|
+
if (config.vectorize.some((v) => v.binding === binding)) {
|
|
884
|
+
console.error(` \u2717 Vectorize binding "${binding}" already exists in wrangler.jsonc`);
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
config.vectorize.push({
|
|
888
|
+
binding,
|
|
889
|
+
index_name: indexName,
|
|
890
|
+
dimensions,
|
|
891
|
+
metric: "cosine"
|
|
892
|
+
});
|
|
893
|
+
writeWrangler(config);
|
|
894
|
+
console.log(` \u2714 Added Vectorize binding "${binding}" (index: ${indexName}, dimensions: ${dimensions})`);
|
|
895
|
+
console.log(` ! Create the index with: wrangler vectorize create ${indexName} --dimensions=${dimensions} --metric=cosine`);
|
|
896
|
+
}
|
|
897
|
+
function addBrowser(binding) {
|
|
898
|
+
const config = readWrangler();
|
|
899
|
+
if (config.browser) {
|
|
900
|
+
console.error(` \u2717 Browser binding already exists in wrangler.jsonc`);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
config.browser = { binding };
|
|
904
|
+
writeWrangler(config);
|
|
905
|
+
console.log(` \u2714 Added Browser rendering binding "${binding}"`);
|
|
906
|
+
}
|
|
907
|
+
function addHyperdrive(binding, connectionString) {
|
|
908
|
+
const config = readWrangler();
|
|
909
|
+
if (!config.hyperdrive) config.hyperdrive = [];
|
|
910
|
+
if (config.hyperdrive.some((h) => h.binding === binding)) {
|
|
911
|
+
console.error(` \u2717 Hyperdrive binding "${binding}" already exists in wrangler.jsonc`);
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
config.hyperdrive.push({
|
|
915
|
+
binding,
|
|
916
|
+
id: "00000000000000000000000000000000",
|
|
917
|
+
localConnectionString: connectionString
|
|
918
|
+
});
|
|
919
|
+
writeWrangler(config);
|
|
920
|
+
console.log(` \u2714 Added Hyperdrive binding "${binding}"`);
|
|
921
|
+
console.log(` ! Remember to replace id with your actual Hyperdrive config ID.`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// bin/commands/add.ts
|
|
925
|
+
var USAGE2 = `
|
|
926
|
+
Usage:
|
|
927
|
+
pnpm wilt add d1 <BINDING_NAME> <database-name>
|
|
928
|
+
pnpm wilt add r2 <BINDING_NAME> <bucket-name>
|
|
929
|
+
pnpm wilt add kv <BINDING_NAME>
|
|
930
|
+
pnpm wilt add queue <BINDING_NAME> <queue-name>
|
|
931
|
+
pnpm wilt add ai <BINDING_NAME>
|
|
932
|
+
pnpm wilt add durable-object <BINDING_NAME> <ClassName>
|
|
933
|
+
pnpm wilt add vectorize <BINDING_NAME> <index-name> [dimensions]
|
|
934
|
+
pnpm wilt add browser <BINDING_NAME>
|
|
935
|
+
pnpm wilt add hyperdrive <BINDING_NAME> <connection-string>
|
|
936
|
+
|
|
937
|
+
Examples:
|
|
938
|
+
pnpm wilt add d1 PAYMENTS_DB payments-db
|
|
939
|
+
pnpm wilt add r2 ASSETS_BUCKET my-assets
|
|
940
|
+
pnpm wilt add kv SESSION_KV
|
|
941
|
+
pnpm wilt add queue MAIL_QUEUE mailer
|
|
942
|
+
pnpm wilt add ai AI
|
|
943
|
+
pnpm wilt add durable-object CHAT_ROOM ChatRoom
|
|
944
|
+
pnpm wilt add vectorize VECTOR_IDX my-index 1536
|
|
945
|
+
pnpm wilt add browser BROWSER
|
|
946
|
+
pnpm wilt add hyperdrive HYPERDRIVE postgres://user:pass@host/db
|
|
947
|
+
`.trim();
|
|
948
|
+
function runAdd(args) {
|
|
949
|
+
const [service, ...rest] = args;
|
|
950
|
+
if (!service) {
|
|
951
|
+
console.error(` \u2717 Missing service type.
|
|
952
|
+
|
|
953
|
+
${USAGE2}`);
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
const svc = service.toLowerCase();
|
|
957
|
+
switch (svc) {
|
|
958
|
+
case "d1": {
|
|
959
|
+
const [binding, dbName] = rest;
|
|
960
|
+
if (!binding || !dbName) {
|
|
961
|
+
console.error(` \u2717 Usage: pnpm wilt add d1 <BINDING_NAME> <database-name>`);
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
console.log(`
|
|
965
|
+
Adding D1 binding to wrangler.jsonc...
|
|
966
|
+
`);
|
|
967
|
+
addD1(binding, dbName);
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case "r2": {
|
|
971
|
+
const [binding, bucketName] = rest;
|
|
972
|
+
if (!binding || !bucketName) {
|
|
973
|
+
console.error(` \u2717 Usage: pnpm wilt add r2 <BINDING_NAME> <bucket-name>`);
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
console.log(`
|
|
977
|
+
Adding R2 binding to wrangler.jsonc...
|
|
978
|
+
`);
|
|
979
|
+
addR2(binding, bucketName);
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
case "kv": {
|
|
983
|
+
const [binding] = rest;
|
|
984
|
+
if (!binding) {
|
|
985
|
+
console.error(` \u2717 Usage: pnpm wilt add kv <BINDING_NAME>`);
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
console.log(`
|
|
989
|
+
Adding KV namespace binding to wrangler.jsonc...
|
|
990
|
+
`);
|
|
991
|
+
addKv(binding);
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
case "queue": {
|
|
995
|
+
const [binding, queueName] = rest;
|
|
996
|
+
if (!binding || !queueName) {
|
|
997
|
+
console.error(` \u2717 Usage: pnpm wilt add queue <BINDING_NAME> <queue-name>`);
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
console.log(`
|
|
1001
|
+
Adding Queue binding to wrangler.jsonc...
|
|
1002
|
+
`);
|
|
1003
|
+
addQueue(binding, queueName);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
case "ai": {
|
|
1007
|
+
const [binding] = rest;
|
|
1008
|
+
if (!binding) {
|
|
1009
|
+
console.error(` \u2717 Usage: pnpm wilt add ai <BINDING_NAME>`);
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
console.log(`
|
|
1013
|
+
Adding AI binding to wrangler.jsonc...
|
|
1014
|
+
`);
|
|
1015
|
+
addAi(binding);
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
case "durable-object":
|
|
1019
|
+
case "do": {
|
|
1020
|
+
const [binding, className] = rest;
|
|
1021
|
+
if (!binding || !className) {
|
|
1022
|
+
console.error(` \u2717 Usage: pnpm wilt add durable-object <BINDING_NAME> <ClassName>`);
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
console.log(`
|
|
1026
|
+
Adding Durable Object binding to wrangler.jsonc...
|
|
1027
|
+
`);
|
|
1028
|
+
addDurableObject(binding, className);
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
case "vectorize":
|
|
1032
|
+
case "vector": {
|
|
1033
|
+
const [binding, indexName, rawDimensions] = rest;
|
|
1034
|
+
if (!binding || !indexName) {
|
|
1035
|
+
console.error(` \u2717 Usage: pnpm wilt add vectorize <BINDING_NAME> <index-name> [dimensions]`);
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
const dimensions = rawDimensions ? parseInt(rawDimensions, 10) : 1536;
|
|
1039
|
+
console.log(`
|
|
1040
|
+
Adding Vectorize binding to wrangler.jsonc...
|
|
1041
|
+
`);
|
|
1042
|
+
addVectorize(binding, indexName, dimensions);
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
case "browser": {
|
|
1046
|
+
const [binding] = rest;
|
|
1047
|
+
if (!binding) {
|
|
1048
|
+
console.error(` \u2717 Usage: pnpm wilt add browser <BINDING_NAME>`);
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
console.log(`
|
|
1052
|
+
Adding Browser rendering binding to wrangler.jsonc...
|
|
1053
|
+
`);
|
|
1054
|
+
addBrowser(binding);
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
case "hyperdrive": {
|
|
1058
|
+
const [binding, connectionString] = rest;
|
|
1059
|
+
if (!binding || !connectionString) {
|
|
1060
|
+
console.error(` \u2717 Usage: pnpm wilt add hyperdrive <BINDING_NAME> <connection-string>`);
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
console.log(`
|
|
1064
|
+
Adding Hyperdrive binding to wrangler.jsonc...
|
|
1065
|
+
`);
|
|
1066
|
+
addHyperdrive(binding, connectionString);
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
default: {
|
|
1070
|
+
console.error(` \u2717 Unknown service type: "${service}"
|
|
1071
|
+
|
|
1072
|
+
${USAGE2}`);
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// bin/wilt.ts
|
|
1079
|
+
var BANNER = `
|
|
1080
|
+
\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
1081
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D
|
|
1082
|
+
\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
1083
|
+
\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
1084
|
+
\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551
|
|
1085
|
+
\u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D
|
|
1086
|
+
Wilt CLI \u2014 Cloudflare Workers Framework
|
|
1087
|
+
`.trim();
|
|
1088
|
+
var HELP = `
|
|
1089
|
+
${BANNER}
|
|
1090
|
+
|
|
1091
|
+
Usage:
|
|
1092
|
+
pnpm wilt <command> [subcommand] [args...]
|
|
1093
|
+
|
|
1094
|
+
Commands:
|
|
1095
|
+
new Create a new Wilt project
|
|
1096
|
+
generate (g) Scaffold module, service, or controller files
|
|
1097
|
+
add Add a Cloudflare service binding to wrangler.jsonc
|
|
1098
|
+
help Show this help message
|
|
1099
|
+
|
|
1100
|
+
Generate subcommands:
|
|
1101
|
+
module (m) Scaffold a full module (module + controller + service + dto)
|
|
1102
|
+
service (s) Scaffold a service file
|
|
1103
|
+
controller (c) Scaffold a controller file
|
|
1104
|
+
|
|
1105
|
+
Add subcommands:
|
|
1106
|
+
d1 D1 database binding
|
|
1107
|
+
r2 R2 bucket binding
|
|
1108
|
+
kv KV namespace binding
|
|
1109
|
+
queue Queue producer + consumer
|
|
1110
|
+
ai Workers AI binding
|
|
1111
|
+
durable-object Durable Object binding + migration
|
|
1112
|
+
vectorize Vectorize index binding
|
|
1113
|
+
browser Browser rendering binding
|
|
1114
|
+
hyperdrive Hyperdrive binding
|
|
1115
|
+
|
|
1116
|
+
Examples:
|
|
1117
|
+
pnpm wilt new my-app
|
|
1118
|
+
pnpm wilt generate module payment
|
|
1119
|
+
pnpm wilt g m payment
|
|
1120
|
+
pnpm wilt add d1 PAYMENTS_DB payments-db
|
|
1121
|
+
pnpm wilt add r2 ASSETS_BUCKET my-assets
|
|
1122
|
+
pnpm wilt add kv SESSION_KV
|
|
1123
|
+
pnpm wilt add queue MAIL_QUEUE mailer
|
|
1124
|
+
pnpm wilt add ai AI
|
|
1125
|
+
pnpm wilt add durable-object CHAT_ROOM ChatRoom
|
|
1126
|
+
`.trim();
|
|
1127
|
+
async function main() {
|
|
1128
|
+
const args = process.argv.slice(2);
|
|
1129
|
+
const [command, ...rest] = args;
|
|
1130
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
1131
|
+
console.log(`
|
|
1132
|
+
${HELP}
|
|
1133
|
+
`);
|
|
1134
|
+
process.exit(0);
|
|
1135
|
+
}
|
|
1136
|
+
const cmd = command.toLowerCase();
|
|
1137
|
+
if (cmd === "new" || cmd === "n") {
|
|
1138
|
+
runNew(rest);
|
|
1139
|
+
} else if (cmd === "generate" || cmd === "g") {
|
|
1140
|
+
await runGenerate(rest);
|
|
1141
|
+
} else if (cmd === "add") {
|
|
1142
|
+
runAdd(rest);
|
|
1143
|
+
} else {
|
|
1144
|
+
console.error(` \u2717 Unknown command: "${command}"
|
|
1145
|
+
`);
|
|
1146
|
+
console.log(HELP);
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
main();
|
|
1151
|
+
//# sourceMappingURL=wilt.js.map
|