@nilejs/cli 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -5
- package/dist/index.js +308 -18
- package/package.json +2 -1
- package/template/README.md +29 -4
- package/template/package.json +2 -1
- package/template/src/services/services.config.ts +5 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nilejs/cli)
|
|
4
4
|
|
|
5
|
-
CLI for scaffolding and generating [Nile](https://
|
|
5
|
+
CLI for scaffolding and generating [Nile](https://github.com/nile-js/nile/tree/main/packages/nile) backend projects.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -60,12 +60,12 @@ The scaffolded project includes:
|
|
|
60
60
|
|
|
61
61
|
### `nile generate service <name>`
|
|
62
62
|
|
|
63
|
-
Alias: `nile
|
|
63
|
+
Alias: `nile gen service <name>`
|
|
64
64
|
|
|
65
65
|
Generate a new service directory under `src/services/` with a demo action and barrel export. Run this from the project root.
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
nile
|
|
68
|
+
nile gen service users
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
Creates:
|
|
@@ -80,12 +80,12 @@ After creating the files, the CLI asks whether to auto-register the service in `
|
|
|
80
80
|
|
|
81
81
|
### `nile generate action <service-name> <action-name>`
|
|
82
82
|
|
|
83
|
-
Alias: `nile
|
|
83
|
+
Alias: `nile gen action <service-name> <action-name>`
|
|
84
84
|
|
|
85
85
|
Generate a new action file in an existing service directory.
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
nile
|
|
88
|
+
nile gen action users get-user
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
Creates `src/services/users/get-user.ts` with:
|
|
@@ -114,6 +114,43 @@ export const getUserAction: Action = createAction({
|
|
|
114
114
|
|
|
115
115
|
Kebab-case names are converted to camelCase for variables and PascalCase for types.
|
|
116
116
|
|
|
117
|
+
### `nile generate schema`
|
|
118
|
+
|
|
119
|
+
Alias: `nile gen schema`
|
|
120
|
+
|
|
121
|
+
Extract Zod validation schemas from your action definitions and generate TypeScript files with schema exports and inferred types.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
nile gen schema
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The command auto-detects `src/services/services.config.ts`. If the file is not found, it prompts for the path. It spawns a `bun` subprocess to import your services config, extracts JSON Schema from each action's validation field, and converts them back to Zod code strings.
|
|
128
|
+
|
|
129
|
+
Output (default `src/generated/`):
|
|
130
|
+
|
|
131
|
+
- `schemas.ts` — named Zod schema exports (`tasksCreateSchema`, `tasksUpdateSchema`, etc.)
|
|
132
|
+
- `types.ts` — TypeScript types via `z.infer` (`TasksCreatePayload`, `TasksUpdatePayload`, etc.)
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
|
|
136
|
+
| Flag | Description |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `-e, --entry <path>` | Path to services config (auto-detected by default) |
|
|
139
|
+
| `-o, --output <path>` | Output directory (default: `src/generated`) |
|
|
140
|
+
|
|
141
|
+
Actions without a `validation` schema are skipped and listed in the CLI output.
|
|
142
|
+
|
|
143
|
+
Requires [Bun](https://bun.sh) installed for the extraction subprocess.
|
|
144
|
+
|
|
145
|
+
### Quick Reference
|
|
146
|
+
|
|
147
|
+
| Command | Description |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `nile new <name>` | Scaffold a new project |
|
|
150
|
+
| `nile gen service <name>` | Add a service with a demo action |
|
|
151
|
+
| `nile gen action <service> <name>` | Add an action to an existing service |
|
|
152
|
+
| `nile gen schema` | Generate Zod schemas and TypeScript types |
|
|
153
|
+
|
|
117
154
|
## Generated Project Structure
|
|
118
155
|
|
|
119
156
|
```
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { resolve as resolve6 } from "node:path";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/commands/generate-action.ts
|
|
@@ -130,8 +131,9 @@ var generateActionCommand = async (serviceName, actionName) => {
|
|
|
130
131
|
outro();
|
|
131
132
|
};
|
|
132
133
|
|
|
133
|
-
// src/commands/generate-
|
|
134
|
-
import {
|
|
134
|
+
// src/commands/generate-schema.ts
|
|
135
|
+
import { execFileSync } from "node:child_process";
|
|
136
|
+
import { resolve as resolve3 } from "node:path";
|
|
135
137
|
|
|
136
138
|
// src/utils/prompt.ts
|
|
137
139
|
import { createInterface } from "node:readline";
|
|
@@ -153,15 +155,289 @@ var confirmPrompt = (question, defaultYes = true) => {
|
|
|
153
155
|
});
|
|
154
156
|
});
|
|
155
157
|
};
|
|
158
|
+
var inputPrompt = (question) => {
|
|
159
|
+
const rl = createInterface({
|
|
160
|
+
input: process.stdin,
|
|
161
|
+
output: process.stdout
|
|
162
|
+
});
|
|
163
|
+
return new Promise((resolve2) => {
|
|
164
|
+
rl.question(` ${question} `, (answer) => {
|
|
165
|
+
rl.close();
|
|
166
|
+
resolve2(answer.trim());
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
};
|
|
156
170
|
|
|
157
|
-
// src/
|
|
171
|
+
// src/utils/schema-codegen.ts
|
|
172
|
+
import { jsonSchemaToZod } from "json-schema-to-zod";
|
|
173
|
+
var toSchemaExportName = (service, action) => {
|
|
174
|
+
const pascalAction = action.charAt(0).toUpperCase() + action.slice(1);
|
|
175
|
+
return `${toCamelCase2(service)}${pascalAction}Schema`;
|
|
176
|
+
};
|
|
177
|
+
var toTypeExportName = (service, action) => {
|
|
178
|
+
const pascalService = toCamelCase2(service).charAt(0).toUpperCase() + toCamelCase2(service).slice(1);
|
|
179
|
+
const pascalAction = action.charAt(0).toUpperCase() + action.slice(1);
|
|
180
|
+
return `${pascalService}${pascalAction}Payload`;
|
|
181
|
+
};
|
|
158
182
|
var toCamelCase2 = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
183
|
+
var generateHeader = () => {
|
|
184
|
+
const timestamp = new Date().toISOString();
|
|
185
|
+
return [
|
|
186
|
+
`// Auto-generated by @nilejs/cli on ${timestamp}`,
|
|
187
|
+
"// Do not edit manually. Re-run `nile generate schema` to update.",
|
|
188
|
+
""
|
|
189
|
+
].join(`
|
|
190
|
+
`);
|
|
191
|
+
};
|
|
192
|
+
var buildSchemaEntries = (extraction) => {
|
|
193
|
+
const entries = [];
|
|
194
|
+
for (const [serviceName, actions] of Object.entries(extraction.schemas)) {
|
|
195
|
+
for (const [actionName, jsonSchema] of Object.entries(actions)) {
|
|
196
|
+
const zodCode = jsonSchemaToZod(jsonSchema, {
|
|
197
|
+
noImport: true
|
|
198
|
+
});
|
|
199
|
+
entries.push({
|
|
200
|
+
serviceName,
|
|
201
|
+
actionName,
|
|
202
|
+
exportName: toSchemaExportName(serviceName, actionName),
|
|
203
|
+
typeName: toTypeExportName(serviceName, actionName),
|
|
204
|
+
zodCode
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return entries;
|
|
209
|
+
};
|
|
210
|
+
var generateSchemasFile = (extraction) => {
|
|
211
|
+
const entries = buildSchemaEntries(extraction);
|
|
212
|
+
if (entries.length === 0) {
|
|
213
|
+
return `${generateHeader()}
|
|
214
|
+
// No action schemas found.
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
const lines = [generateHeader(), `import { z } from "zod";`, ""];
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
lines.push(`export const ${entry.exportName} = ${entry.zodCode};`);
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
if (extraction.skipped.length > 0) {
|
|
223
|
+
lines.push(`// Skipped (no validation): ${extraction.skipped.join(", ")}`);
|
|
224
|
+
lines.push("");
|
|
225
|
+
}
|
|
226
|
+
return lines.join(`
|
|
227
|
+
`);
|
|
228
|
+
};
|
|
229
|
+
var generateTypesFile = (extraction) => {
|
|
230
|
+
const entries = buildSchemaEntries(extraction);
|
|
231
|
+
if (entries.length === 0) {
|
|
232
|
+
return `${generateHeader()}
|
|
233
|
+
// No action types found.
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
const schemaImports = entries.map((e) => e.exportName);
|
|
237
|
+
const lines = [
|
|
238
|
+
generateHeader(),
|
|
239
|
+
`import type { z } from "zod";`,
|
|
240
|
+
"import type {",
|
|
241
|
+
...schemaImports.map((name) => ` ${name},`),
|
|
242
|
+
`} from "./schemas.js";`,
|
|
243
|
+
""
|
|
244
|
+
];
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
lines.push(`export type ${entry.typeName} = z.infer<typeof ${entry.exportName}>;`);
|
|
247
|
+
}
|
|
248
|
+
lines.push("");
|
|
249
|
+
lines.push("/** Map of all services and their action payloads for @nilejs/client */");
|
|
250
|
+
lines.push("export type ServicePayloads = {");
|
|
251
|
+
const serviceMap = {};
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (!serviceMap[entry.serviceName]) {
|
|
254
|
+
serviceMap[entry.serviceName] = [];
|
|
255
|
+
}
|
|
256
|
+
serviceMap[entry.serviceName].push(` "${entry.actionName}": ${entry.typeName};`);
|
|
257
|
+
}
|
|
258
|
+
for (const [service, actions] of Object.entries(serviceMap)) {
|
|
259
|
+
lines.push(` "${service}": {`);
|
|
260
|
+
lines.push(...actions);
|
|
261
|
+
lines.push(" };");
|
|
262
|
+
}
|
|
263
|
+
lines.push("};");
|
|
264
|
+
lines.push("");
|
|
265
|
+
return lines.join(`
|
|
266
|
+
`);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// src/utils/schema-extractor.ts
|
|
270
|
+
import { execFile } from "node:child_process";
|
|
271
|
+
import { unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
272
|
+
import { resolve as resolve2 } from "node:path";
|
|
273
|
+
var TEMP_SCRIPT_NAME = ".nile-schema-extract.ts";
|
|
274
|
+
var LEADING_SLASH = /^\//;
|
|
275
|
+
var TS_EXTENSION = /\.ts$/;
|
|
276
|
+
var buildExtractionScript = (configPath) => {
|
|
277
|
+
return `import z from "zod";
|
|
278
|
+
import { services } from "${configPath}";
|
|
279
|
+
|
|
280
|
+
const schemas: Record<string, Record<string, unknown>> = {};
|
|
281
|
+
const skipped: string[] = [];
|
|
282
|
+
|
|
283
|
+
for (const service of services) {
|
|
284
|
+
const serviceSchemas: Record<string, unknown> = {};
|
|
285
|
+
|
|
286
|
+
for (const action of service.actions) {
|
|
287
|
+
if (!action.validation) {
|
|
288
|
+
skipped.push(\`\${service.name}.\${action.name}\`);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const jsonSchema = z.toJSONSchema(action.validation, { unrepresentable: "any" });
|
|
294
|
+
serviceSchemas[action.name] = jsonSchema;
|
|
295
|
+
} catch {
|
|
296
|
+
skipped.push(\`\${service.name}.\${action.name}\`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (Object.keys(serviceSchemas).length > 0) {
|
|
301
|
+
schemas[service.name] = serviceSchemas;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(JSON.stringify({ schemas, skipped }));
|
|
306
|
+
`;
|
|
307
|
+
};
|
|
308
|
+
var resolveImportPath = (configPath, projectRoot) => {
|
|
309
|
+
const relative = configPath.replace(projectRoot, "").replace(LEADING_SLASH, "./").replace(TS_EXTENSION, "");
|
|
310
|
+
return relative.startsWith("./") ? relative : `./${relative}`;
|
|
311
|
+
};
|
|
312
|
+
var extractSchemas = (configPath, projectRoot) => {
|
|
313
|
+
const importPath = resolveImportPath(configPath, projectRoot);
|
|
314
|
+
const scriptContent = buildExtractionScript(importPath);
|
|
315
|
+
const tempScriptPath = resolve2(projectRoot, TEMP_SCRIPT_NAME);
|
|
316
|
+
return new Promise((resolve3, reject) => {
|
|
317
|
+
writeFile2(tempScriptPath, scriptContent, "utf-8").then(() => {
|
|
318
|
+
execFile("bun", ["run", TEMP_SCRIPT_NAME], { cwd: projectRoot, timeout: 30000 }, (err, stdout, stderr) => {
|
|
319
|
+
unlink(tempScriptPath).catch(() => {});
|
|
320
|
+
if (err) {
|
|
321
|
+
const message = stderr?.trim() || err.message;
|
|
322
|
+
reject(new Error(`Schema extraction failed:
|
|
323
|
+
${message}`));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const result = JSON.parse(stdout.trim());
|
|
328
|
+
resolve3(result);
|
|
329
|
+
} catch {
|
|
330
|
+
reject(new Error(`Failed to parse extraction output. Raw output:
|
|
331
|
+
${stdout.trim()}`));
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}).catch((writeErr) => {
|
|
335
|
+
reject(new Error(`Failed to write temp extraction script: ${writeErr.message}`));
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
var DEFAULT_CONFIG_PATH = "src/services/services.config.ts";
|
|
340
|
+
var findServicesConfig = (projectRoot) => {
|
|
341
|
+
const defaultPath = resolve2(projectRoot, DEFAULT_CONFIG_PATH);
|
|
342
|
+
return pathExists(defaultPath) ? defaultPath : null;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/commands/generate-schema.ts
|
|
346
|
+
var DEFAULT_OUTPUT_DIR = "src/generated";
|
|
347
|
+
var LEADING_SLASH2 = /^\//;
|
|
348
|
+
var resolveConfigPath = async (projectRoot, entryFlag) => {
|
|
349
|
+
if (entryFlag) {
|
|
350
|
+
const absolute2 = resolve3(projectRoot, entryFlag);
|
|
351
|
+
if (!pathExists(absolute2)) {
|
|
352
|
+
error(`Config file not found: ${entryFlag}`);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return absolute2;
|
|
356
|
+
}
|
|
357
|
+
const detected = findServicesConfig(projectRoot);
|
|
358
|
+
if (detected) {
|
|
359
|
+
success("Found services config at src/services/services.config.ts");
|
|
360
|
+
return detected;
|
|
361
|
+
}
|
|
362
|
+
warn("Could not find src/services/services.config.ts");
|
|
363
|
+
const userPath = await inputPrompt("Enter the path to your services config file:");
|
|
364
|
+
if (!userPath) {
|
|
365
|
+
error("No config path provided.");
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const absolute = resolve3(projectRoot, userPath);
|
|
369
|
+
if (!pathExists(absolute)) {
|
|
370
|
+
error(`Config file not found: ${userPath}`);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return absolute;
|
|
374
|
+
};
|
|
375
|
+
var checkBunAvailable = () => {
|
|
376
|
+
try {
|
|
377
|
+
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
378
|
+
return true;
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var generateSchemaCommand = async (options) => {
|
|
384
|
+
const projectRoot = process.cwd();
|
|
385
|
+
brand();
|
|
386
|
+
if (!checkBunAvailable()) {
|
|
387
|
+
error("Bun is required for schema extraction but was not found.");
|
|
388
|
+
hint("Install bun: https://bun.sh");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
const configPath = await resolveConfigPath(projectRoot, options.entry);
|
|
392
|
+
if (!configPath) {
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
const spinner = createSpinner("Extracting action schemas...");
|
|
396
|
+
let extraction;
|
|
397
|
+
try {
|
|
398
|
+
extraction = await extractSchemas(configPath, projectRoot);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
spinner.stop("Extraction failed");
|
|
401
|
+
console.log("");
|
|
402
|
+
error(err instanceof Error ? err.message : String(err));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const schemaCount = Object.values(extraction.schemas).reduce((sum, actions) => sum + Object.keys(actions).length, 0);
|
|
406
|
+
if (schemaCount === 0) {
|
|
407
|
+
spinner.stop("No action schemas found");
|
|
408
|
+
warn("None of the actions have validation schemas defined.");
|
|
409
|
+
hint("Add a Zod validation schema to your actions to generate types.");
|
|
410
|
+
outro();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
spinner.stop(`Extracted ${schemaCount} action schema${schemaCount > 1 ? "s" : ""}`);
|
|
414
|
+
const outputDir = resolve3(projectRoot, options.output ?? DEFAULT_OUTPUT_DIR);
|
|
415
|
+
await ensureDir(outputDir);
|
|
416
|
+
const schemasContent = generateSchemasFile(extraction);
|
|
417
|
+
const typesContent = generateTypesFile(extraction);
|
|
418
|
+
const schemasPath = resolve3(outputDir, "schemas.ts");
|
|
419
|
+
const typesPath = resolve3(outputDir, "types.ts");
|
|
420
|
+
await writeFileSafe(schemasPath, schemasContent);
|
|
421
|
+
success(`Generated ${schemasPath.replace(projectRoot, "").replace(LEADING_SLASH2, "")}`);
|
|
422
|
+
await writeFileSafe(typesPath, typesContent);
|
|
423
|
+
success(`Generated ${typesPath.replace(projectRoot, "").replace(LEADING_SLASH2, "")}`);
|
|
424
|
+
if (extraction.skipped.length > 0) {
|
|
425
|
+
console.log("");
|
|
426
|
+
hint(`Skipped ${extraction.skipped.length} action(s) without validation:`);
|
|
427
|
+
hint(extraction.skipped.join(", "));
|
|
428
|
+
}
|
|
429
|
+
outro();
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/commands/generate-service.ts
|
|
433
|
+
import { resolve as resolve4 } from "node:path";
|
|
434
|
+
var toCamelCase3 = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
159
435
|
var toPascalCase2 = (str) => {
|
|
160
|
-
const camel =
|
|
436
|
+
const camel = toCamelCase3(str);
|
|
161
437
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
162
438
|
};
|
|
163
439
|
var generateActionContent2 = (serviceName) => {
|
|
164
|
-
const camel =
|
|
440
|
+
const camel = toCamelCase3(serviceName);
|
|
165
441
|
const pascal = toPascalCase2(serviceName);
|
|
166
442
|
return `import { type Action, createAction } from "@nilejs/nile";
|
|
167
443
|
import { Ok } from "slang-ts";
|
|
@@ -194,7 +470,7 @@ var generateConfigSnippet = (serviceName) => {
|
|
|
194
470
|
const serviceEntry = ` {
|
|
195
471
|
name: "${serviceName}",
|
|
196
472
|
description: "${pascal} service",
|
|
197
|
-
actions:
|
|
473
|
+
actions: [sample${pascal}Action],
|
|
198
474
|
},`;
|
|
199
475
|
return `${importLine}
|
|
200
476
|
|
|
@@ -220,7 +496,7 @@ ${importLine}`);
|
|
|
220
496
|
const serviceEntry = ` {
|
|
221
497
|
name: "${serviceName}",
|
|
222
498
|
description: "${pascal} service",
|
|
223
|
-
actions:
|
|
499
|
+
actions: [sample${pascal}Action],
|
|
224
500
|
},`;
|
|
225
501
|
const closingIndex = content.lastIndexOf("];");
|
|
226
502
|
if (closingIndex === -1) {
|
|
@@ -235,13 +511,13 @@ ${importLine}`);
|
|
|
235
511
|
}
|
|
236
512
|
};
|
|
237
513
|
var generateServiceCommand = async (serviceName) => {
|
|
238
|
-
const servicesDir =
|
|
514
|
+
const servicesDir = resolve4(process.cwd(), "src/services");
|
|
239
515
|
if (!pathExists(servicesDir)) {
|
|
240
516
|
error("Could not find src/services/ directory.");
|
|
241
517
|
hint("Make sure you're in a Nile project root.");
|
|
242
518
|
process.exit(1);
|
|
243
519
|
}
|
|
244
|
-
const serviceDir =
|
|
520
|
+
const serviceDir = resolve4(servicesDir, serviceName);
|
|
245
521
|
if (pathExists(serviceDir)) {
|
|
246
522
|
error(`Service "${serviceName}" already exists at src/services/${serviceName}/`);
|
|
247
523
|
process.exit(1);
|
|
@@ -249,10 +525,10 @@ var generateServiceCommand = async (serviceName) => {
|
|
|
249
525
|
brand();
|
|
250
526
|
const spinner = createSpinner(`Creating service ${serviceName}...`);
|
|
251
527
|
await ensureDir(serviceDir);
|
|
252
|
-
await writeFileSafe(
|
|
253
|
-
await writeFileSafe(
|
|
528
|
+
await writeFileSafe(resolve4(serviceDir, "sample.ts"), generateActionContent2(serviceName));
|
|
529
|
+
await writeFileSafe(resolve4(serviceDir, "index.ts"), generateBarrelContent(serviceName));
|
|
254
530
|
spinner.stop(`Service created at src/services/${serviceName}/`);
|
|
255
|
-
const configPath =
|
|
531
|
+
const configPath = resolve4(servicesDir, "services.config.ts");
|
|
256
532
|
if (!pathExists(configPath)) {
|
|
257
533
|
warn("Could not find services.config.ts");
|
|
258
534
|
console.log("");
|
|
@@ -281,22 +557,22 @@ ${generateConfigSnippet(serviceName)}
|
|
|
281
557
|
};
|
|
282
558
|
|
|
283
559
|
// src/commands/new.ts
|
|
284
|
-
import { resolve as
|
|
560
|
+
import { resolve as resolve5 } from "node:path";
|
|
285
561
|
import { fileURLToPath } from "node:url";
|
|
286
562
|
var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
|
|
287
563
|
var resolveTemplateDir = () => {
|
|
288
|
-
const devPath =
|
|
564
|
+
const devPath = resolve5(__dirname2, "../../template");
|
|
289
565
|
if (pathExists(devPath)) {
|
|
290
566
|
return devPath;
|
|
291
567
|
}
|
|
292
|
-
const distPath =
|
|
568
|
+
const distPath = resolve5(__dirname2, "../template");
|
|
293
569
|
if (pathExists(distPath)) {
|
|
294
570
|
return distPath;
|
|
295
571
|
}
|
|
296
572
|
throw new Error("Template directory not found. The CLI package may be corrupted.");
|
|
297
573
|
};
|
|
298
574
|
var newCommand = async (projectName) => {
|
|
299
|
-
const targetDir =
|
|
575
|
+
const targetDir = resolve5(process.cwd(), projectName);
|
|
300
576
|
if (pathExists(targetDir)) {
|
|
301
577
|
error(`Directory "${projectName}" already exists.`);
|
|
302
578
|
process.exit(1);
|
|
@@ -315,18 +591,32 @@ var newCommand = async (projectName) => {
|
|
|
315
591
|
spinner.stop("Project ready.");
|
|
316
592
|
success(`Created ${projectName}`);
|
|
317
593
|
console.log("");
|
|
318
|
-
hint(
|
|
594
|
+
hint(`cd ${projectName}`);
|
|
319
595
|
hint("bun install");
|
|
320
596
|
hint("cp .env.example .env");
|
|
321
597
|
hint("bun run dev");
|
|
598
|
+
console.log("");
|
|
599
|
+
success("Generate with the CLI:");
|
|
600
|
+
hint("nile g service <name> Add a new service");
|
|
601
|
+
hint("nile g action <service> <name> Add an action to a service");
|
|
602
|
+
hint("nile g schema Generate Zod schemas & types");
|
|
322
603
|
outro();
|
|
323
604
|
};
|
|
324
605
|
|
|
325
606
|
// src/index.ts
|
|
607
|
+
var ensureDepsInstalled = () => {
|
|
608
|
+
const nodeModulesPath = resolve6(process.cwd(), "node_modules");
|
|
609
|
+
if (!pathExists(nodeModulesPath)) {
|
|
610
|
+
error("Dependencies not installed.");
|
|
611
|
+
hint("Run 'bun install' first, then try again.");
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
326
615
|
var program = new Command;
|
|
327
616
|
program.name("nile").description("CLI for the Nile backend framework").version("0.0.1");
|
|
328
617
|
program.command("new").argument("<project-name>", "Name of the project to create").description("Scaffold a new Nile project").action(newCommand);
|
|
329
|
-
var generate = program.command("generate").alias("
|
|
618
|
+
var generate = program.command("generate").alias("gen").description("Generate services and actions").hook("preAction", ensureDepsInstalled);
|
|
330
619
|
generate.command("service").argument("<name>", "Service name (kebab-case recommended)").description("Generate a new service with a demo action").action(generateServiceCommand);
|
|
331
620
|
generate.command("action").argument("<service-name>", "Name of the existing service").argument("<action-name>", "Name of the action to create").description("Generate a new action in an existing service").action(generateActionCommand);
|
|
621
|
+
generate.command("schema").option("-e, --entry <path>", "Path to services config file").option("-o, --output <path>", "Output directory for generated files").description("Generate Zod schemas and TypeScript types from action validations").action(generateSchemaCommand);
|
|
332
622
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nilejs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "CLI for scaffolding and generating Nile backend projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"commander": "^13.1.0",
|
|
19
|
+
"json-schema-to-zod": "^2.7.0",
|
|
19
20
|
"picocolors": "^1.1.1"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
package/template/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{projectName}}
|
|
2
2
|
|
|
3
|
-
Built with [Nile](https://
|
|
3
|
+
Built with [Nile](https://github.com/nile-js/nile).
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -17,6 +17,9 @@ The server starts at `http://localhost:8000`. PGLite creates an embedded Postgre
|
|
|
17
17
|
| Script | Description |
|
|
18
18
|
|---|---|
|
|
19
19
|
| `bun run dev` | Start the development server |
|
|
20
|
+
| `bun run gen service <name>` | Generate a new service |
|
|
21
|
+
| `bun run gen action <service> <name>` | Generate a new action |
|
|
22
|
+
| `bun run gen schema` | Generate Zod schemas and TypeScript types |
|
|
20
23
|
| `bun run db:generate` | Generate Drizzle migrations from schema changes |
|
|
21
24
|
| `bun run db:push` | Push schema changes directly to the database |
|
|
22
25
|
| `bun run db:studio` | Open Drizzle Studio to browse your data |
|
|
@@ -77,7 +80,7 @@ curl -X POST http://localhost:8000/api/services \
|
|
|
77
80
|
Generate a new service with the CLI:
|
|
78
81
|
|
|
79
82
|
```bash
|
|
80
|
-
|
|
83
|
+
bun run gen service users
|
|
81
84
|
```
|
|
82
85
|
|
|
83
86
|
Or manually create a directory under `src/services/` with action files and register it in `src/services/services.config.ts`.
|
|
@@ -87,11 +90,33 @@ Or manually create a directory under `src/services/` with action files and regis
|
|
|
87
90
|
Generate a new action in an existing service:
|
|
88
91
|
|
|
89
92
|
```bash
|
|
90
|
-
|
|
93
|
+
bun run gen action users get-user
|
|
91
94
|
```
|
|
92
95
|
|
|
93
96
|
Each action file exports a single action created with `createAction`, which takes a Zod validation schema and a handler function that returns `Ok(data)` or `Err(message)`.
|
|
94
97
|
|
|
98
|
+
## Generating Schemas & Types
|
|
99
|
+
|
|
100
|
+
Extract Zod validation schemas from your actions and generate TypeScript types:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bun run gen schema
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This auto-detects `src/services/services.config.ts`, reads validation schemas from all actions, and outputs two files:
|
|
107
|
+
|
|
108
|
+
- `src/generated/schemas.ts` — named Zod schema exports
|
|
109
|
+
- `src/generated/types.ts` — inferred TypeScript types via `z.infer`
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
|
|
113
|
+
| Flag | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `-e, --entry <path>` | Path to services config (auto-detected by default) |
|
|
116
|
+
| `-o, --output <path>` | Output directory (default: `src/generated`) |
|
|
117
|
+
|
|
118
|
+
Actions without a validation schema are skipped and listed in the CLI output.
|
|
119
|
+
|
|
95
120
|
## Database
|
|
96
121
|
|
|
97
122
|
This project ships with [PGLite](https://electric-sql.com/product/pglite) for zero-setup local development. For production, you can swap it for Postgres, MySQL, SQLite, or any other database supported by Drizzle. Update `src/db/client.ts` with your connection and driver.
|
|
@@ -101,7 +126,7 @@ See the [Drizzle getting started guide](https://orm.drizzle.team/docs/get-starte
|
|
|
101
126
|
## Tech Stack
|
|
102
127
|
|
|
103
128
|
- **Runtime:** [Bun](https://bun.sh)
|
|
104
|
-
- **Framework:** [@nilejs/nile](https://
|
|
129
|
+
- **Framework:** [@nilejs/nile](https://github.com/nile-js/nile/tree/main/packages/nile)
|
|
105
130
|
- **Database:** [PGLite](https://electric-sql.com/product/pglite) (embedded Postgres)
|
|
106
131
|
- **ORM:** [Drizzle](https://orm.drizzle.team)
|
|
107
132
|
- **Validation:** [Zod](https://zod.dev)
|
package/template/package.json
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
"private": true,
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "bun run src/index.ts",
|
|
8
|
+
"gen": "bunx @nilejs/cli generate",
|
|
8
9
|
"db:generate": "bunx drizzle-kit generate",
|
|
9
10
|
"db:push": "bunx drizzle-kit push",
|
|
10
11
|
"db:studio": "bunx drizzle-kit studio"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
|
-
"@nilejs/nile": "
|
|
14
|
+
"@nilejs/nile": "latest",
|
|
14
15
|
"@electric-sql/pglite": "^0.2.5",
|
|
15
16
|
"drizzle-orm": "^0.39.2",
|
|
16
17
|
"pino": "^10.3.1",
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createServices, type Services } from "@nilejs/nile";
|
|
2
2
|
import { createTaskAction } from "./tasks/create";
|
|
3
3
|
import { deleteTaskAction } from "./tasks/delete";
|
|
4
4
|
import { getTaskAction } from "./tasks/get";
|
|
5
5
|
import { listTaskAction } from "./tasks/list";
|
|
6
6
|
import { updateTaskAction } from "./tasks/update";
|
|
7
7
|
|
|
8
|
-
export const services: Services = [
|
|
8
|
+
export const services: Services = createServices([
|
|
9
9
|
{
|
|
10
10
|
name: "tasks",
|
|
11
11
|
description: "Task management with CRUD operations",
|
|
12
12
|
meta: { version: "1.0.0" },
|
|
13
|
-
actions:
|
|
13
|
+
actions: [
|
|
14
14
|
createTaskAction,
|
|
15
15
|
listTaskAction,
|
|
16
16
|
getTaskAction,
|
|
17
17
|
updateTaskAction,
|
|
18
18
|
deleteTaskAction,
|
|
19
|
-
]
|
|
19
|
+
],
|
|
20
20
|
},
|
|
21
|
-
];
|
|
21
|
+
]);
|