@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,266 @@
|
|
|
1
|
+
import { writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { APP_MODULES_DIR, wiltImportPath, ensureDir } from "../utils/paths.js";
|
|
4
|
+
|
|
5
|
+
function toPascalCase(name: string): string {
|
|
6
|
+
return name
|
|
7
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
8
|
+
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toCamelCase(name: string): string {
|
|
12
|
+
const pascal = toPascalCase(name);
|
|
13
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toKebabCase(name: string): string {
|
|
17
|
+
return name
|
|
18
|
+
.replace(/([A-Z])/g, "-$1")
|
|
19
|
+
.replace(/^-/, "")
|
|
20
|
+
.replace(/_/g, "-")
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pluralize(name: string): string {
|
|
25
|
+
return name.endsWith("s") ? name : name + "s";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function moduleTemplate(name: string, wiltPath: string): string {
|
|
29
|
+
const pascal = toPascalCase(name);
|
|
30
|
+
const kebab = toKebabCase(name);
|
|
31
|
+
|
|
32
|
+
return `import { Module } from "${wiltPath}";
|
|
33
|
+
import { ${pascal}Controller } from "./${kebab}.controller";
|
|
34
|
+
import { ${pascal}Service } from "./${kebab}.service";
|
|
35
|
+
|
|
36
|
+
@Module({
|
|
37
|
+
controllers: [${pascal}Controller],
|
|
38
|
+
providers: [${pascal}Service],
|
|
39
|
+
exports: [${pascal}Service],
|
|
40
|
+
})
|
|
41
|
+
export class ${pascal}Module {}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function entityTemplate(name: string): string {
|
|
46
|
+
const camel = toCamelCase(name);
|
|
47
|
+
const kebab = toKebabCase(name);
|
|
48
|
+
const pascal = toPascalCase(name);
|
|
49
|
+
const entityVar = pluralize(camel);
|
|
50
|
+
const tableName = pluralize(kebab);
|
|
51
|
+
|
|
52
|
+
return `import { sqliteTable, integer } from "drizzle-orm/sqlite-core";
|
|
53
|
+
|
|
54
|
+
export const ${entityVar} = sqliteTable("${tableName}", {
|
|
55
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
56
|
+
createdAt: integer("created_at", { mode: "timestamp" })
|
|
57
|
+
.$defaultFn(() => new Date())
|
|
58
|
+
.notNull(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export type ${pascal} = typeof ${entityVar}.$inferSelect;
|
|
62
|
+
export type New${pascal} = typeof ${entityVar}.$inferInsert;
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function controllerTemplate(name: string, wiltPath: string): string {
|
|
67
|
+
const pascal = toPascalCase(name);
|
|
68
|
+
const camel = toCamelCase(name);
|
|
69
|
+
const kebab = toKebabCase(name);
|
|
70
|
+
return `import type { Context } from "hono";
|
|
71
|
+
import { Controller, Get, Inject } from "${wiltPath}";
|
|
72
|
+
import { ResponseUtil } from "${wiltPath}";
|
|
73
|
+
import { ${pascal}Service } from "./${kebab}.service";
|
|
74
|
+
|
|
75
|
+
@Controller("/${kebab}")
|
|
76
|
+
export class ${pascal}Controller {
|
|
77
|
+
constructor(@Inject(${pascal}Service) private ${camel}Service: ${pascal}Service) {}
|
|
78
|
+
|
|
79
|
+
@Get()
|
|
80
|
+
async getAll(c: Context) {
|
|
81
|
+
const data = await this.${camel}Service.findAll();
|
|
82
|
+
return ResponseUtil.success(c, data);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function serviceTemplate(name: string, wiltPath: string): string {
|
|
89
|
+
const pascal = toPascalCase(name);
|
|
90
|
+
return `import { Injectable } from "${wiltPath}";
|
|
91
|
+
import { createDatabase } from "../../../database/connection";
|
|
92
|
+
|
|
93
|
+
@Injectable()
|
|
94
|
+
export class ${pascal}Service {
|
|
95
|
+
|
|
96
|
+
private getDatabase(env: CloudflareBindings) {
|
|
97
|
+
return createDatabase(env.DB);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async findAll() {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function testTemplate(name: string): string {
|
|
108
|
+
const kebab = toKebabCase(name);
|
|
109
|
+
const pascal = toPascalCase(name);
|
|
110
|
+
const table = pluralize(kebab.replace(/-/g, "_"));
|
|
111
|
+
return `import { describe, it, expect, beforeAll, afterEach } from "vitest";
|
|
112
|
+
import { SELF, env, applyD1Migrations } from "cloudflare:test";
|
|
113
|
+
import { ${pascal}Service } from "./${kebab}.service";
|
|
114
|
+
|
|
115
|
+
const BASE = "http://localhost";
|
|
116
|
+
const service = new ${pascal}Service();
|
|
117
|
+
|
|
118
|
+
beforeAll(async () => {
|
|
119
|
+
await applyD1Migrations(env.DB, JSON.parse(env.TEST_MIGRATIONS));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(async () => {
|
|
123
|
+
await env.DB.prepare("DELETE FROM ${table}").run();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("${pascal}Service", () => {
|
|
127
|
+
it("should be defined", () => {
|
|
128
|
+
expect(service).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("GET /${kebab}", () => {
|
|
133
|
+
it("returns an empty list", async () => {
|
|
134
|
+
const res = await SELF.fetch(\`\${BASE}/${kebab}\`);
|
|
135
|
+
expect(res.status).toBe(200);
|
|
136
|
+
const { data } = await res.json() as { data: unknown[] };
|
|
137
|
+
expect(data).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function dtoTemplate(name: string): string {
|
|
144
|
+
const pascal = toPascalCase(name);
|
|
145
|
+
return `import { z } from "zod";
|
|
146
|
+
|
|
147
|
+
export const Create${pascal}Dto = z.object({
|
|
148
|
+
// TODO: define your fields
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const Update${pascal}Dto = Create${pascal}Dto.partial();
|
|
152
|
+
|
|
153
|
+
export type Create${pascal}DtoType = z.infer<typeof Create${pascal}Dto>;
|
|
154
|
+
export type Update${pascal}DtoType = z.infer<typeof Update${pascal}Dto>;
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type GeneratableFile = "module" | "controller" | "service" | "dto" | "entity" | "test";
|
|
159
|
+
|
|
160
|
+
export interface GenerateModuleOptions {
|
|
161
|
+
/** Override output directory — defaults to config.modulesDir/<name>/ */
|
|
162
|
+
dir?: string;
|
|
163
|
+
/** Which files to generate — defaults to config.generate.files */
|
|
164
|
+
files?: GeneratableFile[];
|
|
165
|
+
/** Base directory for generated modules (from wilt.config.ts) */
|
|
166
|
+
modulesDir?: string;
|
|
167
|
+
/** Default file list (from wilt.config.ts) */
|
|
168
|
+
defaultFiles?: GeneratableFile[];
|
|
169
|
+
/** Skip generating the test file even if it is in defaultFiles */
|
|
170
|
+
noTest?: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function generateModule(name: string, options: GenerateModuleOptions = {}): void {
|
|
174
|
+
const kebab = toKebabCase(name);
|
|
175
|
+
const pascal = toPascalCase(name);
|
|
176
|
+
const basedir = options.modulesDir ?? APP_MODULES_DIR;
|
|
177
|
+
const moduleDir = options.dir ?? join(basedir, kebab);
|
|
178
|
+
let filesToGen = options.files ?? options.defaultFiles ?? ["module", "controller", "service", "dto", "entity", "test"];
|
|
179
|
+
if (options.noTest) filesToGen = filesToGen.filter((f) => f !== "test");
|
|
180
|
+
|
|
181
|
+
if (existsSync(moduleDir)) {
|
|
182
|
+
console.error(` ✗ Directory already exists: ${moduleDir}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ensureDir(moduleDir);
|
|
187
|
+
|
|
188
|
+
const wiltPath = wiltImportPath();
|
|
189
|
+
const hasEntity = filesToGen.includes("entity");
|
|
190
|
+
|
|
191
|
+
const fileMap: Record<string, string> = {
|
|
192
|
+
module: moduleTemplate(name, wiltPath),
|
|
193
|
+
controller: controllerTemplate(name, wiltPath),
|
|
194
|
+
service: serviceTemplate(name, wiltPath),
|
|
195
|
+
dto: dtoTemplate(name),
|
|
196
|
+
entity: entityTemplate(name),
|
|
197
|
+
test: testTemplate(name),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const extMap: Record<string, string> = {
|
|
201
|
+
module: `${kebab}.module.ts`,
|
|
202
|
+
controller: `${kebab}.controller.ts`,
|
|
203
|
+
service: `${kebab}.service.ts`,
|
|
204
|
+
dto: `${kebab}.dto.ts`,
|
|
205
|
+
entity: `${kebab}.entity.ts`,
|
|
206
|
+
test: `${kebab}.test.ts`,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const file of filesToGen) {
|
|
210
|
+
const filePath = join(moduleDir, extMap[file]);
|
|
211
|
+
writeFileSync(filePath, fileMap[file], "utf-8");
|
|
212
|
+
console.log(` ✔ Created ${filePath.replace(process.cwd() + "/", "")}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const relModuleDir = moduleDir.replace(process.cwd() + "/", "");
|
|
216
|
+
const entityLine = hasEntity
|
|
217
|
+
? `\n 2. Add the entity to src/database/schema.ts:\n\n import { ${pluralize(toCamelCase(name))} } from "@app/${relModuleDir.replace(/^src\//, "")}/${kebab}.entity";\n\n export const schema = {\n ...existing,\n ${pluralize(toCamelCase(name))},\n };\n`
|
|
218
|
+
: "";
|
|
219
|
+
console.log(`
|
|
220
|
+
Next steps —
|
|
221
|
+
|
|
222
|
+
1. Register the module in src/app.module.ts:
|
|
223
|
+
|
|
224
|
+
import { ${pascal}Module } from "./${relModuleDir.replace(/^src\//, "")}/${kebab}.module";
|
|
225
|
+
|
|
226
|
+
@Module({
|
|
227
|
+
imports: [..., ${pascal}Module],
|
|
228
|
+
})
|
|
229
|
+
export class AppModule {}
|
|
230
|
+
${entityLine} Then run \`pnpm db:gen\` to generate a migration.
|
|
231
|
+
`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function generateService(name: string, dir?: string, modulesDir?: string): void {
|
|
235
|
+
const kebab = toKebabCase(name);
|
|
236
|
+
const basedir = modulesDir ?? APP_MODULES_DIR;
|
|
237
|
+
const targetDir = dir ?? join(basedir, kebab);
|
|
238
|
+
const wiltPath = wiltImportPath();
|
|
239
|
+
const filePath = join(targetDir, `${kebab}.service.ts`);
|
|
240
|
+
|
|
241
|
+
if (existsSync(filePath)) {
|
|
242
|
+
console.error(` ✗ File already exists: ${filePath}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ensureDir(targetDir);
|
|
247
|
+
writeFileSync(filePath, serviceTemplate(name, wiltPath), "utf-8");
|
|
248
|
+
console.log(` ✔ Created ${filePath.replace(process.cwd() + "/", "")}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function generateController(name: string, dir?: string, modulesDir?: string): void {
|
|
252
|
+
const kebab = toKebabCase(name);
|
|
253
|
+
const basedir = modulesDir ?? APP_MODULES_DIR;
|
|
254
|
+
const targetDir = dir ?? join(basedir, kebab);
|
|
255
|
+
const wiltPath = wiltImportPath();
|
|
256
|
+
const filePath = join(targetDir, `${kebab}.controller.ts`);
|
|
257
|
+
|
|
258
|
+
if (existsSync(filePath)) {
|
|
259
|
+
console.error(` ✗ File already exists: ${filePath}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
ensureDir(targetDir);
|
|
264
|
+
writeFileSync(filePath, controllerTemplate(name, wiltPath), "utf-8");
|
|
265
|
+
console.log(` ✔ Created ${filePath.replace(process.cwd() + "/", "")}`);
|
|
266
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
import type { GeneratableFile } from "../generators/module.generator.js";
|
|
5
|
+
|
|
6
|
+
export interface WiltConfig {
|
|
7
|
+
modulesDir?: string;
|
|
8
|
+
srcDir?: string;
|
|
9
|
+
generate?: {
|
|
10
|
+
files?: GeneratableFile[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ResolvedWiltConfig {
|
|
15
|
+
modulesDir: string;
|
|
16
|
+
srcDir: string;
|
|
17
|
+
generate: {
|
|
18
|
+
files: GeneratableFile[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadConfig(cwd: string = process.cwd()): Promise<ResolvedWiltConfig> {
|
|
23
|
+
let userConfig: WiltConfig = {};
|
|
24
|
+
|
|
25
|
+
for (const name of ["wilt.config.ts", "wilt.config.js", "wilt.config.mjs"]) {
|
|
26
|
+
const configPath = join(cwd, name);
|
|
27
|
+
if (existsSync(configPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
30
|
+
const mod = await jiti.import(configPath) as WiltConfig;
|
|
31
|
+
userConfig = mod ?? {};
|
|
32
|
+
} catch {
|
|
33
|
+
// fall back to defaults if config can't be loaded
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
modulesDir: resolve(cwd, userConfig.modulesDir ?? "src/modules/app"),
|
|
41
|
+
srcDir: resolve(cwd, userConfig.srcDir ?? "src"),
|
|
42
|
+
generate: {
|
|
43
|
+
files: userConfig.generate?.files ?? ["module", "controller", "service", "dto", "entity", "test"],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
/** Absolute path to the project root (where the CLI is invoked from) */
|
|
5
|
+
export const PROJECT_ROOT = process.cwd();
|
|
6
|
+
|
|
7
|
+
/** Absolute path to src/ */
|
|
8
|
+
export const SRC_DIR = join(PROJECT_ROOT, "src");
|
|
9
|
+
|
|
10
|
+
/** Absolute path to src/modules/ */
|
|
11
|
+
export const MODULES_DIR = join(SRC_DIR, "modules");
|
|
12
|
+
|
|
13
|
+
/** Absolute path to src/modules/app/ (default location for generated modules) */
|
|
14
|
+
export const APP_MODULES_DIR = join(MODULES_DIR, "app");
|
|
15
|
+
|
|
16
|
+
/** Absolute path to wrangler.jsonc */
|
|
17
|
+
export const WRANGLER_JSONC = join(PROJECT_ROOT, "wrangler.jsonc");
|
|
18
|
+
|
|
19
|
+
/** Absolute path to src/app.module.ts */
|
|
20
|
+
export const APP_MODULE_FILE = join(SRC_DIR, "app.module.ts");
|
|
21
|
+
|
|
22
|
+
/** The wilt import used in generated files */
|
|
23
|
+
export function wiltImportPath(): string {
|
|
24
|
+
return "wilt";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Ensure a directory exists, creating it recursively if needed */
|
|
28
|
+
export function ensureDir(dir: string): void {
|
|
29
|
+
if (!existsSync(dir)) {
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { WRANGLER_JSONC } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
type WranglerConfig = Record<string, any>;
|
|
5
|
+
|
|
6
|
+
/** Strip `//` line comments from a JSONC string so JSON.parse can handle it. */
|
|
7
|
+
function stripComments(text: string): string {
|
|
8
|
+
return text
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((line) => line.replace(/\s*\/\/.*$/, ""))
|
|
11
|
+
.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readWrangler(): WranglerConfig {
|
|
15
|
+
const raw = readFileSync(WRANGLER_JSONC, "utf-8");
|
|
16
|
+
return JSON.parse(stripComments(raw));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function writeWrangler(config: WranglerConfig): void {
|
|
20
|
+
const header = `{\n "$schema": "node_modules/wrangler/config-schema.json",`;
|
|
21
|
+
const body = JSON.stringify(config, null, 2);
|
|
22
|
+
|
|
23
|
+
// Remove opening brace and $schema from body (we add them above with the comment header)
|
|
24
|
+
const bodyWithoutSchema = body
|
|
25
|
+
.replace(/^\{/, "")
|
|
26
|
+
.replace(/\s+"?\$schema"?\s*:\s*"[^"]*",?\n/, "\n");
|
|
27
|
+
|
|
28
|
+
writeFileSync(WRANGLER_JSONC, header + bodyWithoutSchema, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Binding mutators ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export function addD1(binding: string, databaseName: string): void {
|
|
34
|
+
const config = readWrangler();
|
|
35
|
+
if (!config.d1_databases) config.d1_databases = [];
|
|
36
|
+
|
|
37
|
+
const alreadyExists = config.d1_databases.some(
|
|
38
|
+
(db: any) => db.binding === binding
|
|
39
|
+
);
|
|
40
|
+
if (alreadyExists) {
|
|
41
|
+
console.error(` ✗ D1 binding "${binding}" already exists in wrangler.jsonc`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
config.d1_databases.push({
|
|
46
|
+
binding,
|
|
47
|
+
database_name: databaseName,
|
|
48
|
+
database_id: "00000000-0000-0000-0000-000000000000",
|
|
49
|
+
migrations_dir: "migrations",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
writeWrangler(config);
|
|
53
|
+
console.log(` ✔ Added D1 binding "${binding}" (database: ${databaseName})`);
|
|
54
|
+
console.log(` ! Remember to replace database_id with your actual D1 database ID.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function addR2(binding: string, bucketName: string): void {
|
|
58
|
+
const config = readWrangler();
|
|
59
|
+
if (!config.r2_buckets) config.r2_buckets = [];
|
|
60
|
+
|
|
61
|
+
if (config.r2_buckets.some((b: any) => b.binding === binding)) {
|
|
62
|
+
console.error(` ✗ R2 binding "${binding}" already exists in wrangler.jsonc`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
config.r2_buckets.push({
|
|
67
|
+
binding,
|
|
68
|
+
bucket_name: bucketName,
|
|
69
|
+
preview_bucket_name: `${bucketName}-preview`,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
writeWrangler(config);
|
|
73
|
+
console.log(` ✔ Added R2 binding "${binding}" (bucket: ${bucketName})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function addKv(binding: string): void {
|
|
77
|
+
const config = readWrangler();
|
|
78
|
+
if (!config.kv_namespaces) config.kv_namespaces = [];
|
|
79
|
+
|
|
80
|
+
if (config.kv_namespaces.some((k: any) => k.binding === binding)) {
|
|
81
|
+
console.error(` ✗ KV binding "${binding}" already exists in wrangler.jsonc`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
config.kv_namespaces.push({
|
|
86
|
+
binding,
|
|
87
|
+
id: "00000000000000000000000000000000",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
writeWrangler(config);
|
|
91
|
+
console.log(` ✔ Added KV namespace binding "${binding}"`);
|
|
92
|
+
console.log(` ! Remember to replace id with your actual KV namespace ID.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function addQueue(binding: string, queueName: string): void {
|
|
96
|
+
const config = readWrangler();
|
|
97
|
+
if (!config.queues) config.queues = { producers: [], consumers: [] };
|
|
98
|
+
if (!config.queues.producers) config.queues.producers = [];
|
|
99
|
+
if (!config.queues.consumers) config.queues.consumers = [];
|
|
100
|
+
|
|
101
|
+
if (config.queues.producers.some((p: any) => p.binding === binding)) {
|
|
102
|
+
console.error(` ✗ Queue producer binding "${binding}" already exists in wrangler.jsonc`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
config.queues.producers.push({ binding, queue: queueName });
|
|
107
|
+
config.queues.consumers.push({
|
|
108
|
+
queue: queueName,
|
|
109
|
+
max_batch_size: 10,
|
|
110
|
+
max_batch_timeout: 10,
|
|
111
|
+
max_retries: 3,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
writeWrangler(config);
|
|
115
|
+
console.log(` ✔ Added Queue binding "${binding}" (queue: ${queueName})`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function addAi(binding: string): void {
|
|
119
|
+
const config = readWrangler();
|
|
120
|
+
|
|
121
|
+
if (config.ai) {
|
|
122
|
+
console.error(` ✗ AI binding already exists in wrangler.jsonc`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
config.ai = { binding };
|
|
127
|
+
writeWrangler(config);
|
|
128
|
+
console.log(` ✔ Added AI binding "${binding}"`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function addDurableObject(binding: string, className: string): void {
|
|
132
|
+
const config = readWrangler();
|
|
133
|
+
if (!config.durable_objects) config.durable_objects = { bindings: [] };
|
|
134
|
+
if (!config.durable_objects.bindings) config.durable_objects.bindings = [];
|
|
135
|
+
|
|
136
|
+
if (config.durable_objects.bindings.some((b: any) => b.name === binding)) {
|
|
137
|
+
console.error(` ✗ Durable Object binding "${binding}" already exists in wrangler.jsonc`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
config.durable_objects.bindings.push({
|
|
142
|
+
name: binding,
|
|
143
|
+
class_name: className,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Also add a migration entry
|
|
147
|
+
if (!config.migrations) config.migrations = [];
|
|
148
|
+
const existingClasses: string[] = config.migrations.flatMap(
|
|
149
|
+
(m: any) => m.new_sqlite_classes ?? m.new_classes ?? []
|
|
150
|
+
);
|
|
151
|
+
if (!existingClasses.includes(className)) {
|
|
152
|
+
const nextTag = `v${config.migrations.length + 1}`;
|
|
153
|
+
config.migrations.push({
|
|
154
|
+
tag: nextTag,
|
|
155
|
+
new_sqlite_classes: [className],
|
|
156
|
+
});
|
|
157
|
+
console.log(` ! Added migration entry "${nextTag}" for ${className}.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
writeWrangler(config);
|
|
161
|
+
console.log(` ✔ Added Durable Object binding "${binding}" (class: ${className})`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function addVectorize(binding: string, indexName: string, dimensions: number = 1536): void {
|
|
165
|
+
const config = readWrangler();
|
|
166
|
+
if (!config.vectorize) config.vectorize = [];
|
|
167
|
+
|
|
168
|
+
if (config.vectorize.some((v: any) => v.binding === binding)) {
|
|
169
|
+
console.error(` ✗ Vectorize binding "${binding}" already exists in wrangler.jsonc`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
config.vectorize.push({
|
|
174
|
+
binding,
|
|
175
|
+
index_name: indexName,
|
|
176
|
+
dimensions,
|
|
177
|
+
metric: "cosine",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
writeWrangler(config);
|
|
181
|
+
console.log(` ✔ Added Vectorize binding "${binding}" (index: ${indexName}, dimensions: ${dimensions})`);
|
|
182
|
+
console.log(` ! Create the index with: wrangler vectorize create ${indexName} --dimensions=${dimensions} --metric=cosine`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function addBrowser(binding: string): void {
|
|
186
|
+
const config = readWrangler();
|
|
187
|
+
|
|
188
|
+
if (config.browser) {
|
|
189
|
+
console.error(` ✗ Browser binding already exists in wrangler.jsonc`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
config.browser = { binding };
|
|
194
|
+
writeWrangler(config);
|
|
195
|
+
console.log(` ✔ Added Browser rendering binding "${binding}"`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function addHyperdrive(binding: string, connectionString: string): void {
|
|
199
|
+
const config = readWrangler();
|
|
200
|
+
if (!config.hyperdrive) config.hyperdrive = [];
|
|
201
|
+
|
|
202
|
+
if (config.hyperdrive.some((h: any) => h.binding === binding)) {
|
|
203
|
+
console.error(` ✗ Hyperdrive binding "${binding}" already exists in wrangler.jsonc`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
config.hyperdrive.push({
|
|
208
|
+
binding,
|
|
209
|
+
id: "00000000000000000000000000000000",
|
|
210
|
+
localConnectionString: connectionString,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
writeWrangler(config);
|
|
214
|
+
console.log(` ✔ Added Hyperdrive binding "${binding}"`);
|
|
215
|
+
console.log(` ! Remember to replace id with your actual Hyperdrive config ID.`);
|
|
216
|
+
}
|
package/bin/wilt.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { runNew } from "./commands/new.js";
|
|
2
|
+
import { runGenerate } from "./commands/generate.js";
|
|
3
|
+
import { runAdd } from "./commands/add.js";
|
|
4
|
+
|
|
5
|
+
const BANNER = `
|
|
6
|
+
██╗ ██╗██╗██╗ ████████╗
|
|
7
|
+
██║ ██║██║██║ ╚══██╔══╝
|
|
8
|
+
██║ █╗ ██║██║██║ ██║
|
|
9
|
+
██║███╗██║██║██║ ██║
|
|
10
|
+
╚███╔███╔╝██║███████╗██║
|
|
11
|
+
╚══╝╚══╝ ╚═╝╚══════╝╚═╝
|
|
12
|
+
Wilt CLI — Cloudflare Workers Framework
|
|
13
|
+
`.trim();
|
|
14
|
+
|
|
15
|
+
const HELP = `
|
|
16
|
+
${BANNER}
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
pnpm wilt <command> [subcommand] [args...]
|
|
20
|
+
|
|
21
|
+
Commands:
|
|
22
|
+
new Create a new Wilt project
|
|
23
|
+
generate (g) Scaffold module, service, or controller files
|
|
24
|
+
add Add a Cloudflare service binding to wrangler.jsonc
|
|
25
|
+
help Show this help message
|
|
26
|
+
|
|
27
|
+
Generate subcommands:
|
|
28
|
+
module (m) Scaffold a full module (module + controller + service + dto)
|
|
29
|
+
service (s) Scaffold a service file
|
|
30
|
+
controller (c) Scaffold a controller file
|
|
31
|
+
|
|
32
|
+
Add subcommands:
|
|
33
|
+
d1 D1 database binding
|
|
34
|
+
r2 R2 bucket binding
|
|
35
|
+
kv KV namespace binding
|
|
36
|
+
queue Queue producer + consumer
|
|
37
|
+
ai Workers AI binding
|
|
38
|
+
durable-object Durable Object binding + migration
|
|
39
|
+
vectorize Vectorize index binding
|
|
40
|
+
browser Browser rendering binding
|
|
41
|
+
hyperdrive Hyperdrive binding
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
pnpm wilt new my-app
|
|
45
|
+
pnpm wilt generate module payment
|
|
46
|
+
pnpm wilt g m payment
|
|
47
|
+
pnpm wilt add d1 PAYMENTS_DB payments-db
|
|
48
|
+
pnpm wilt add r2 ASSETS_BUCKET my-assets
|
|
49
|
+
pnpm wilt add kv SESSION_KV
|
|
50
|
+
pnpm wilt add queue MAIL_QUEUE mailer
|
|
51
|
+
pnpm wilt add ai AI
|
|
52
|
+
pnpm wilt add durable-object CHAT_ROOM ChatRoom
|
|
53
|
+
`.trim();
|
|
54
|
+
|
|
55
|
+
async function main(): Promise<void> {
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
const [command, ...rest] = args;
|
|
58
|
+
|
|
59
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
60
|
+
console.log(`\n${HELP}\n`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cmd = command.toLowerCase();
|
|
65
|
+
|
|
66
|
+
if (cmd === "new" || cmd === "n") {
|
|
67
|
+
runNew(rest);
|
|
68
|
+
} else if (cmd === "generate" || cmd === "g") {
|
|
69
|
+
await runGenerate(rest);
|
|
70
|
+
} else if (cmd === "add") {
|
|
71
|
+
runAdd(rest);
|
|
72
|
+
} else {
|
|
73
|
+
console.error(` ✗ Unknown command: "${command}"\n`);
|
|
74
|
+
console.log(HELP);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main();
|