@nexusts/cli 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/index.js +4169 -0
- package/dist/index.js.map +57 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4169 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __require = import.meta.require;
|
|
20
|
+
|
|
21
|
+
// packages/cli/src/commands/info.ts
|
|
22
|
+
import { resolve } from "path";
|
|
23
|
+
import { colors, logger } from "@nexusts/core/index.js";
|
|
24
|
+
var infoCommand = {
|
|
25
|
+
name: "info",
|
|
26
|
+
aliases: ["i"],
|
|
27
|
+
summary: "Show project configuration",
|
|
28
|
+
description: "Prints the resolved nx.config.ts plus relevant env vars.",
|
|
29
|
+
async run(ctx) {
|
|
30
|
+
logger.heading("Nexus CLI \u2014 Project Info");
|
|
31
|
+
logger.info(colors.bold("Resolved configuration"));
|
|
32
|
+
logger.blank();
|
|
33
|
+
logger.table([
|
|
34
|
+
["routing", String(ctx.config.routing)],
|
|
35
|
+
["view", String(ctx.config.view)],
|
|
36
|
+
["orm", String(ctx.config.orm)],
|
|
37
|
+
["dialect", String(ctx.config.dialect ?? "(none)")],
|
|
38
|
+
["database.driver", String(ctx.config.database.driver)],
|
|
39
|
+
["database.url", String(ctx.config.database.url)],
|
|
40
|
+
["inertia.frontend", String(ctx.config.inertia.frontend)],
|
|
41
|
+
["inertia.ssr", String(ctx.config.inertia.ssr)],
|
|
42
|
+
["inertia.version", String(ctx.config.inertia.version)]
|
|
43
|
+
]);
|
|
44
|
+
logger.blank();
|
|
45
|
+
logger.info(colors.bold("Paths"));
|
|
46
|
+
logger.blank();
|
|
47
|
+
for (const [k, v] of Object.entries(ctx.config.paths)) {
|
|
48
|
+
logger.table([[k, v]]);
|
|
49
|
+
}
|
|
50
|
+
logger.blank();
|
|
51
|
+
logger.info(colors.bold("Environment"));
|
|
52
|
+
logger.blank();
|
|
53
|
+
const envKeys = [
|
|
54
|
+
"NODE_ENV",
|
|
55
|
+
"PORT",
|
|
56
|
+
"NEXUS_DEBUG",
|
|
57
|
+
"NO_COLOR",
|
|
58
|
+
"FORCE_COLOR",
|
|
59
|
+
"NX_ROUTING",
|
|
60
|
+
"NX_VIEW",
|
|
61
|
+
"NX_ORM",
|
|
62
|
+
"NX_DATABASE_DRIVER",
|
|
63
|
+
"NX_DATABASE_URL",
|
|
64
|
+
"NX_INERTIA_FRONTEND",
|
|
65
|
+
"NX_INERTIA_SSR"
|
|
66
|
+
];
|
|
67
|
+
for (const k of envKeys) {
|
|
68
|
+
const v = process.env[k];
|
|
69
|
+
logger.table([[k, v === undefined ? colors.dim("(unset)") : v]]);
|
|
70
|
+
}
|
|
71
|
+
logger.blank();
|
|
72
|
+
logger.info(colors.bold("Working directory"));
|
|
73
|
+
logger.blank();
|
|
74
|
+
logger.info(` ${resolve(ctx.cwd)}`);
|
|
75
|
+
logger.blank();
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var info_default = infoCommand;
|
|
80
|
+
|
|
81
|
+
// packages/cli/src/commands/init.ts
|
|
82
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
83
|
+
import { resolve as resolve2 } from "path";
|
|
84
|
+
import { flagBool, logger as logger2, render, select } from "@nexusts/core/index.js";
|
|
85
|
+
import { parseJsonLoose } from "@nexusts/core/loose-json.js";
|
|
86
|
+
|
|
87
|
+
// packages/cli/src/templates/controller/adonis.ts
|
|
88
|
+
var adonis_default = `
|
|
89
|
+
import { {{ service }} } from '../services/{{ kebab }}.service.js';
|
|
90
|
+
|
|
91
|
+
export class {{ name }}Controller {
|
|
92
|
+
async index() {
|
|
93
|
+
return new {{ service }}().findAll();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async show({ params }: { params: { id: string } }) {
|
|
97
|
+
return new {{ service }}().findOne(Number(params.id));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async create({ body }: { body: any }) {
|
|
101
|
+
return { status: 201, body: new {{ service }}().create(body) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async update({ params, body }: { params: { id: string }; body: any }) {
|
|
105
|
+
return new {{ service }}().update(Number(params.id), body);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async destroy({ params }: { params: { id: string } }) {
|
|
109
|
+
return new {{ service }}().delete(Number(params.id));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const {{ camel }}Controller = new {{ name }}Controller();
|
|
114
|
+
`.trimStart();
|
|
115
|
+
|
|
116
|
+
// packages/cli/src/templates/controller/functional.ts
|
|
117
|
+
var functional_default = `
|
|
118
|
+
import type { Context } from 'hono';
|
|
119
|
+
|
|
120
|
+
export const {{ camel }}Routes = {
|
|
121
|
+
list: async (c: Context) => {
|
|
122
|
+
return c.json([]);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
show: async (c: Context) => {
|
|
126
|
+
const id = c.req.param('id');
|
|
127
|
+
return c.json({ id });
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
create: async (c: Context) => {
|
|
131
|
+
const body = await c.req.json();
|
|
132
|
+
return c.json({ created: body }, 201);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
update: async (c: Context) => {
|
|
136
|
+
const id = c.req.param('id');
|
|
137
|
+
const body = await c.req.json();
|
|
138
|
+
return c.json({ id, body });
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
destroy: async (c: Context) => {
|
|
142
|
+
const id = c.req.param('id');
|
|
143
|
+
return c.json({ removed: id });
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
`.trimStart();
|
|
147
|
+
|
|
148
|
+
// packages/cli/src/templates/controller/nest.ts
|
|
149
|
+
var nest_default = `
|
|
150
|
+
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nexusts/core';
|
|
151
|
+
import { {{ service }} } from '../services/{{ kebab }}.service.js';
|
|
152
|
+
|
|
153
|
+
@Controller('/{{ kebab }}s')
|
|
154
|
+
export class {{ name }}Controller {
|
|
155
|
+
constructor(@Inject({{ service }}) private readonly {{ serviceCamel }}: {{ service }}) {}
|
|
156
|
+
|
|
157
|
+
@Get('/')
|
|
158
|
+
async index() {
|
|
159
|
+
return this.{{ serviceCamel }}.findAll();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@Get('/:id')
|
|
163
|
+
async show(@Param('id') id: string) {
|
|
164
|
+
return this.{{ serviceCamel }}.findOne(Number(id));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@Post('/')
|
|
168
|
+
async create(@Body() body: any) {
|
|
169
|
+
return { status: 201, body: this.{{ serviceCamel }}.create(body) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Put('/:id')
|
|
173
|
+
async update(@Param('id') id: string, @Body() body: any) {
|
|
174
|
+
return this.{{ serviceCamel }}.update(Number(id), body);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Delete('/:id')
|
|
178
|
+
async destroy(@Param('id') id: string) {
|
|
179
|
+
return this.{{ serviceCamel }}.delete(Number(id));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
`.trimStart();
|
|
183
|
+
|
|
184
|
+
// packages/cli/src/templates/crud/controller.ts
|
|
185
|
+
var controller_default = `
|
|
186
|
+
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nexusts/core';
|
|
187
|
+
import { z } from 'zod';
|
|
188
|
+
import { Validate } from '@nexusts/core';
|
|
189
|
+
import { {{ service }} } from '../services/{{ kebab }}.service.js';
|
|
190
|
+
{{#hasInertia}}import { Inertia } from '@nexusts/view/inertia';{{/hasInertia}}
|
|
191
|
+
|
|
192
|
+
const Create{{ name }}Schema = z.object({
|
|
193
|
+
// TODO: define fields
|
|
194
|
+
title: z.string().min(1),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
@Controller('/{{ kebab }}s')
|
|
198
|
+
export class {{ controller }} {
|
|
199
|
+
constructor(
|
|
200
|
+
@Inject({{ service }}) private readonly {{ camel }}Service: {{ service }},
|
|
201
|
+
{{#hasInertia}} @Inject(Inertia.TOKEN) private readonly inertia: Inertia,{{/hasInertia}}
|
|
202
|
+
) {}
|
|
203
|
+
|
|
204
|
+
@Get('/')
|
|
205
|
+
async index() {
|
|
206
|
+
const items = await this.{{ camel }}Service.findAll();
|
|
207
|
+
{{#hasInertia}}
|
|
208
|
+
return this.inertia.render('{{ viewComponent }}', { items });
|
|
209
|
+
{{/hasInertia}}
|
|
210
|
+
{{^hasInertia}}
|
|
211
|
+
return items;
|
|
212
|
+
{{/hasInertia}}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@Get('/:id')
|
|
216
|
+
async show(@Param('id') id: string) {
|
|
217
|
+
const item = await this.{{ camel }}Service.findOne(Number(id));
|
|
218
|
+
{{#hasInertia}}
|
|
219
|
+
return this.inertia.render('{{ viewShowComponent }}', { item });
|
|
220
|
+
{{/hasInertia}}
|
|
221
|
+
{{^hasInertia}}
|
|
222
|
+
return item;
|
|
223
|
+
{{/hasInertia}}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@Post('/')
|
|
227
|
+
@Validate({ body: Create{{ name }}Schema })
|
|
228
|
+
async create(@Body() body: z.infer<typeof Create{{ name }}Schema>) {
|
|
229
|
+
return { status: 201, body: await this.{{ camel }}Service.create(body) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@Put('/:id')
|
|
233
|
+
@Validate({ body: Create{{ name }}Schema.partial() })
|
|
234
|
+
async update(
|
|
235
|
+
@Param('id') id: string,
|
|
236
|
+
@Body() body: Partial<z.infer<typeof Create{{ name }}Schema>>,
|
|
237
|
+
) {
|
|
238
|
+
return await this.{{ camel }}Service.update(Number(id), body);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@Delete('/:id')
|
|
242
|
+
async destroy(@Param('id') id: string) {
|
|
243
|
+
return await this.{{ camel }}Service.delete(Number(id));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
`.trimStart();
|
|
247
|
+
|
|
248
|
+
// packages/cli/src/templates/crud/dto.ts
|
|
249
|
+
var dto_default = `
|
|
250
|
+
import { z } from 'zod';
|
|
251
|
+
|
|
252
|
+
export const Create{{ name }}Dto = z.object({
|
|
253
|
+
// TODO: define fields. Example:
|
|
254
|
+
// title: z.string().min(1).max(200),
|
|
255
|
+
// body: z.string().optional(),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export const Update{{ name }}Dto = Create{{ name }}Dto.partial();
|
|
259
|
+
|
|
260
|
+
export type Create{{ name }} = z.infer<typeof Create{{ name }}Dto>;
|
|
261
|
+
export type Update{{ name }} = z.infer<typeof Update{{ name }}Dto>;
|
|
262
|
+
`.trimStart();
|
|
263
|
+
|
|
264
|
+
// packages/cli/src/templates/crud/module.ts
|
|
265
|
+
var module_default = `
|
|
266
|
+
import { Module } from '@nexusts/core';
|
|
267
|
+
import { {{ controller }} } from '../controllers/{{ kebab }}.controller.js';
|
|
268
|
+
import { {{ service }} } from '../services/{{ kebab }}.service.js';
|
|
269
|
+
{{#hasRepo}}import { {{ repository }} } from '../repositories/{{ kebab }}.repository.js';{{/hasRepo}}
|
|
270
|
+
|
|
271
|
+
@Module({
|
|
272
|
+
controllers: [{{ controller }}],
|
|
273
|
+
providers: [
|
|
274
|
+
{{ service }},
|
|
275
|
+
{{#hasRepo}}{{ repository }},{{/hasRepo}}
|
|
276
|
+
],
|
|
277
|
+
exports: [{{ service }}],
|
|
278
|
+
})
|
|
279
|
+
export class {{ name }}Module {}
|
|
280
|
+
`.trimStart();
|
|
281
|
+
|
|
282
|
+
// packages/cli/src/templates/crud/test.ts
|
|
283
|
+
var test_default = `
|
|
284
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
285
|
+
import { Application } from '@nexusts/core';
|
|
286
|
+
import { {{ name }}Module } from '../{{ kebab }}.module.js';
|
|
287
|
+
|
|
288
|
+
describe('{{ controller }}', () => {
|
|
289
|
+
let app: Application;
|
|
290
|
+
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
app = new Application({{ name }}Module);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('GET /{{ kebab }}s returns an empty list', async () => {
|
|
296
|
+
const res = await app.server.app.request('/{{ kebab }}s');
|
|
297
|
+
expect(res.status).toBe(200);
|
|
298
|
+
const body = await res.json();
|
|
299
|
+
expect(Array.isArray(body) || typeof body === 'object').toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('POST /{{ kebab }}s creates a record', async () => {
|
|
303
|
+
const res = await app.server.app.request('/{{ kebab }}s', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({ title: 'Test' }),
|
|
307
|
+
});
|
|
308
|
+
expect([200, 201]).toContain(res.status);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
`.trimStart();
|
|
312
|
+
|
|
313
|
+
// packages/cli/src/templates/middleware/middleware.ts
|
|
314
|
+
var middleware_default = `
|
|
315
|
+
import { Injectable } from '@nexusts/core';
|
|
316
|
+
import type { Context, Next } from 'hono';
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* {{ name }} middleware \u2014 generated by \`nx make:middleware {{ name }}\`.
|
|
320
|
+
*/
|
|
321
|
+
@Injectable()
|
|
322
|
+
export class {{ name }}Middleware {
|
|
323
|
+
async handle(c: Context, next: Next): Promise<Response> {
|
|
324
|
+
// TODO: pre-handler logic
|
|
325
|
+
const res = await next();
|
|
326
|
+
// TODO: post-handler logic
|
|
327
|
+
return res;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
`.trimStart();
|
|
331
|
+
|
|
332
|
+
// packages/cli/src/templates/migration/drizzle.ts
|
|
333
|
+
var drizzle_default = `
|
|
334
|
+
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* {{ name }} \u2014 migration generated by \`nx make:migration {{ name }}\`.
|
|
338
|
+
* Run with \`bunx drizzle-kit migrate\` or your migration runner.
|
|
339
|
+
*/
|
|
340
|
+
export const {{ snake }} = sqliteTable('{{ tableName }}', {
|
|
341
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
342
|
+
{{ columns }}
|
|
343
|
+
created_at: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
344
|
+
updated_at: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
345
|
+
});
|
|
346
|
+
`.trimStart();
|
|
347
|
+
|
|
348
|
+
// packages/cli/src/templates/migration/sql.ts
|
|
349
|
+
var sql_default = `
|
|
350
|
+
-- {{ timestamp }}_create_{{ snake }}.sql
|
|
351
|
+
-- Generated by \`nx make:migration {{ name }}\`.
|
|
352
|
+
|
|
353
|
+
CREATE TABLE IF NOT EXISTS {{ tableName }} (
|
|
354
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
355
|
+
{{ columns }}
|
|
356
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
357
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
358
|
+
);
|
|
359
|
+
`.trimStart();
|
|
360
|
+
|
|
361
|
+
// packages/cli/src/templates/model/drizzle.ts
|
|
362
|
+
var drizzle_default2 = `
|
|
363
|
+
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* {{ tableName }} \u2014 generated by \`nx make:model {{ name }}\`.
|
|
367
|
+
*/
|
|
368
|
+
export const {{ snake }} = sqliteTable('{{ tableName }}', {
|
|
369
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
370
|
+
{{ columns }}
|
|
371
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
372
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
export type {{ name }} = typeof {{ snake }}.$inferSelect;
|
|
376
|
+
export type New{{ name }} = typeof {{ snake }}.$inferInsert;
|
|
377
|
+
`.trimStart();
|
|
378
|
+
|
|
379
|
+
// packages/cli/src/templates/model/kysely.ts
|
|
380
|
+
var kysely_default = `
|
|
381
|
+
import type { Generated, Insertable, Selectable, Updateable } from 'kysely';
|
|
382
|
+
import { Kysely } from 'kysely';
|
|
383
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
384
|
+
|
|
385
|
+
export interface {{ name }}Table {
|
|
386
|
+
id: Generated<number>;
|
|
387
|
+
{{ columns }}
|
|
388
|
+
created_at: Generated<Date>;
|
|
389
|
+
updated_at: Generated<Date>;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export type {{ name }} = Selectable<{{ name }}Table>;
|
|
393
|
+
export type New{{ name }} = Insertable<{{ name }}Table>;
|
|
394
|
+
export type {{ name }}Update = Updateable<{{ name }}Table>;
|
|
395
|
+
|
|
396
|
+
@Injectable()
|
|
397
|
+
export class {{ name }}Repository {
|
|
398
|
+
constructor(@Inject('DB') private readonly db: Kysely<any>) {}
|
|
399
|
+
|
|
400
|
+
findAll() {
|
|
401
|
+
return this.db.selectFrom('{{ tableName }}').selectAll().execute();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
findOne(id: number) {
|
|
405
|
+
return this.db
|
|
406
|
+
.selectFrom('{{ tableName }}')
|
|
407
|
+
.selectAll()
|
|
408
|
+
.where('id', '=', id)
|
|
409
|
+
.executeTakeFirst();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
create(data: New{{ name }}) {
|
|
413
|
+
return this.db.insertInto('{{ tableName }}').values(data).returningAll().executeTakeFirst();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
update(id: number, data: {{ name }}Update) {
|
|
417
|
+
return this.db
|
|
418
|
+
.updateTable('{{ tableName }}')
|
|
419
|
+
.set(data)
|
|
420
|
+
.where('id', '=', id)
|
|
421
|
+
.returningAll()
|
|
422
|
+
.executeTakeFirst();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
delete(id: number) {
|
|
426
|
+
return this.db.deleteFrom('{{ tableName }}').where('id', '=', id).execute();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
`.trimStart();
|
|
430
|
+
|
|
431
|
+
// packages/cli/src/templates/model/prisma.ts
|
|
432
|
+
var prisma_default = `
|
|
433
|
+
/**
|
|
434
|
+
* {{ name }} \u2014 generated by \`nx make:model {{ name }}\`.
|
|
435
|
+
*
|
|
436
|
+
* Add this block to your schema.prisma file:
|
|
437
|
+
*
|
|
438
|
+
{{ prismaBlock }}
|
|
439
|
+
*/
|
|
440
|
+
|
|
441
|
+
import { PrismaClient } from '@prisma/client';
|
|
442
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
443
|
+
|
|
444
|
+
@Injectable()
|
|
445
|
+
export class {{ name }}Repository {
|
|
446
|
+
constructor(@Inject('PRISMA') private readonly prisma: PrismaClient) {}
|
|
447
|
+
|
|
448
|
+
findAll() {
|
|
449
|
+
return this.prisma.{{ camel }}.findMany();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
findOne(id: number) {
|
|
453
|
+
return this.prisma.{{ camel }}.findUnique({ where: { id } });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
create(data: any) {
|
|
457
|
+
return this.prisma.{{ camel }}.create({ data });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
update(id: number, data: any) {
|
|
461
|
+
return this.prisma.{{ camel }}.update({ where: { id }, data });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
delete(id: number) {
|
|
465
|
+
return this.prisma.{{ camel }}.delete({ where: { id } });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
`.trimStart();
|
|
469
|
+
|
|
470
|
+
// packages/cli/src/templates/module/module.ts
|
|
471
|
+
var module_default2 = `
|
|
472
|
+
import { Module } from '@nexusts/core';
|
|
473
|
+
import { {{ controller }} } from '../controllers/{{ kebab }}.controller.js';
|
|
474
|
+
{{#hasService}}import { {{ service }} } from '../services/{{ kebab }}.service.js';{{/hasService}}
|
|
475
|
+
{{#hasRepo}}import { {{ repository }} } from '../repositories/{{ kebab }}.repository.js';{{/hasRepo}}
|
|
476
|
+
|
|
477
|
+
@Module({
|
|
478
|
+
controllers: [{{ controller }}],
|
|
479
|
+
providers: [
|
|
480
|
+
{{#hasService}}{{ service }},{{/hasService}}
|
|
481
|
+
{{#hasRepo}}{{ repository }},{{/hasRepo}}
|
|
482
|
+
],
|
|
483
|
+
exports: [
|
|
484
|
+
{{#hasService}}{{ service }},{{/hasService}}
|
|
485
|
+
],
|
|
486
|
+
})
|
|
487
|
+
export class {{ name }}Module {}
|
|
488
|
+
`.trimStart();
|
|
489
|
+
|
|
490
|
+
// packages/cli/src/templates/project/nx.config.ts
|
|
491
|
+
var nx_config_default = `/**
|
|
492
|
+
* Nexus project configuration.
|
|
493
|
+
* Run \`nx info\` to see the resolved values.
|
|
494
|
+
*/
|
|
495
|
+
|
|
496
|
+
export default {
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Core
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
/** Routing style used by \`make:controller\` / \`make:crud\`. */
|
|
502
|
+
routing: '{{ routing }}',
|
|
503
|
+
|
|
504
|
+
/** View engine \u2014 \`inertia\`, \`rendu\`, \`edge\`, or \`none\`. */
|
|
505
|
+
view: '{{ view }}',
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Directory searched when a controller returns a view file name
|
|
509
|
+
* (e.g. \`about.html\`). Empty string = inline templates only.
|
|
510
|
+
* Typical: \`'resources/views'\`. On edge runtimes
|
|
511
|
+
* (Cloudflare Workers), leave empty and pass inline strings.
|
|
512
|
+
*/
|
|
513
|
+
viewPaths: '{{ viewPaths }}',
|
|
514
|
+
|
|
515
|
+
/** ORM driver \u2014 \`drizzle\`, \`prisma\`, \`kysely\`, or \`none\`. */
|
|
516
|
+
orm: '{{ orm }}',
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Database
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
database: {
|
|
523
|
+
driver: '{{ dbDriver }}',
|
|
524
|
+
url: process.env.DATABASE_URL ?? '{{ dbUrl }}',
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
// Inertia (only consulted when \`view === 'inertia'\`)
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
inertia: {
|
|
532
|
+
frontend: '{{ inertiaFrontend }}',
|
|
533
|
+
ssr: {{ inertiaSSR }},
|
|
534
|
+
version: '{{ inertiaVersion }}',
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// Paths
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
paths: {
|
|
542
|
+
app: 'app',
|
|
543
|
+
controllers: 'app/controllers',
|
|
544
|
+
services: 'app/services',
|
|
545
|
+
modules: 'app/modules',
|
|
546
|
+
models: 'app/models',
|
|
547
|
+
migrations: 'app/database/migrations',
|
|
548
|
+
seeds: 'db/seeds',
|
|
549
|
+
middleware: 'app/middleware',
|
|
550
|
+
dto: 'app/dto',
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
`;
|
|
554
|
+
|
|
555
|
+
// packages/cli/src/templates/project/drizzle.config.ts
|
|
556
|
+
var drizzle_config_default = `
|
|
557
|
+
import { defineConfig } from "drizzle-kit";
|
|
558
|
+
|
|
559
|
+
export default defineConfig({
|
|
560
|
+
dialect: "{{ dialect }}",
|
|
561
|
+
schema: "./app/models/*.model.ts",
|
|
562
|
+
out: "./drizzle",
|
|
563
|
+
dbCredentials: {
|
|
564
|
+
url: process.env.DATABASE_URL ?? "{{ dbUrl }}",
|
|
565
|
+
},
|
|
566
|
+
verbose: true,
|
|
567
|
+
strict: true,
|
|
568
|
+
});
|
|
569
|
+
`;
|
|
570
|
+
|
|
571
|
+
// packages/cli/src/templates/repository/repository.ts
|
|
572
|
+
var repository_default = `
|
|
573
|
+
import { Injectable } from '@nexusts/core';
|
|
574
|
+
import { DrizzleRepository } from '@nexusts/drizzle';
|
|
575
|
+
import { {{ tableName }} } from '../models/{{ kebab }}.model.js';
|
|
576
|
+
import type { {{ name }}, New{{ name }} } from '../models/{{ kebab }}.model.js';
|
|
577
|
+
|
|
578
|
+
@Injectable()
|
|
579
|
+
export class {{ repository }} extends DrizzleRepository<typeof {{ tableName }}, {{ name }}> {
|
|
580
|
+
constructor() {
|
|
581
|
+
super({{ tableName }});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
`.trimStart();
|
|
585
|
+
|
|
586
|
+
// packages/cli/src/templates/service/service.ts
|
|
587
|
+
var service_default = `
|
|
588
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
589
|
+
{{#hasRepo}}import { {{ repository }} } from '../repositories/{{ kebab }}.repository.js';{{/hasRepo}}
|
|
590
|
+
|
|
591
|
+
@Injectable()
|
|
592
|
+
export class {{ name }}Service {
|
|
593
|
+
constructor({{#hasRepo}}
|
|
594
|
+
@Inject({{ repository }}) private readonly {{ repositoryCamel }}: {{ repository }},
|
|
595
|
+
{{/hasRepo}}) {}
|
|
596
|
+
|
|
597
|
+
async findAll() {
|
|
598
|
+
{{#hasRepo}}return this.{{ repositoryCamel }}.findAll();{{/hasRepo}}
|
|
599
|
+
{{^hasRepo}}return []; // TODO: implement{{/hasRepo}}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async findOne(id: number) {
|
|
603
|
+
{{#hasRepo}}return this.{{ repositoryCamel }}.findOne(id);{{/hasRepo}}
|
|
604
|
+
{{^hasRepo}}return { id }; // TODO: implement{{/hasRepo}}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async create(data: any) {
|
|
608
|
+
{{#hasRepo}}return this.{{ repositoryCamel }}.create(data);{{/hasRepo}}
|
|
609
|
+
{{^hasRepo}}return { id: Date.now(), ...data }; // TODO: implement{{/hasRepo}}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async update(id: number, data: any) {
|
|
613
|
+
{{#hasRepo}}return this.{{ repositoryCamel }}.update(id, data);{{/hasRepo}}
|
|
614
|
+
{{^hasRepo}}return { id, ...data }; // TODO: implement{{/hasRepo}}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async delete(id: number) {
|
|
618
|
+
{{#hasRepo}}return this.{{ repositoryCamel }}.delete(id);{{/hasRepo}}
|
|
619
|
+
{{^hasRepo}}return { removed: id }; // TODO: implement{{/hasRepo}}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
`.trimStart();
|
|
623
|
+
|
|
624
|
+
// packages/cli/src/templates/validator/validator.ts
|
|
625
|
+
var validator_default = `
|
|
626
|
+
import { z } from 'zod';
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Validation schema for {{ name }}.
|
|
630
|
+
* Generated by \`nx make:validator {{ name }}\`.
|
|
631
|
+
*/
|
|
632
|
+
export const {{ name }}Schema = z.object({
|
|
633
|
+
// TODO: define fields. Example:
|
|
634
|
+
// name: z.string().min(1).max(100),
|
|
635
|
+
// email: z.string().email().optional(),
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
export type {{ name }}Input = z.infer<typeof {{ name }}Schema>;
|
|
639
|
+
`.trimStart();
|
|
640
|
+
|
|
641
|
+
// packages/cli/src/templates/index.ts
|
|
642
|
+
var templates = {
|
|
643
|
+
controller: {
|
|
644
|
+
nest: nest_default,
|
|
645
|
+
adonis: adonis_default,
|
|
646
|
+
functional: functional_default
|
|
647
|
+
},
|
|
648
|
+
service: service_default,
|
|
649
|
+
repository: repository_default,
|
|
650
|
+
module: module_default2,
|
|
651
|
+
validator: validator_default,
|
|
652
|
+
middleware: middleware_default,
|
|
653
|
+
model: {
|
|
654
|
+
drizzle: drizzle_default2,
|
|
655
|
+
prisma: prisma_default,
|
|
656
|
+
kysely: kysely_default
|
|
657
|
+
},
|
|
658
|
+
migration: {
|
|
659
|
+
drizzle: drizzle_default,
|
|
660
|
+
sql: sql_default
|
|
661
|
+
},
|
|
662
|
+
crud: {
|
|
663
|
+
controller: controller_default,
|
|
664
|
+
module: module_default,
|
|
665
|
+
dto: dto_default,
|
|
666
|
+
test: test_default
|
|
667
|
+
},
|
|
668
|
+
project: {
|
|
669
|
+
"nx.config.ts": nx_config_default,
|
|
670
|
+
"drizzle.config.ts": drizzle_config_default
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// packages/cli/src/commands/init.ts
|
|
675
|
+
var initCommand = {
|
|
676
|
+
name: "init",
|
|
677
|
+
aliases: ["i"],
|
|
678
|
+
summary: "Initialize nx.config.ts + app scaffold in the current directory",
|
|
679
|
+
description: "Non-destructive scaffold: adds nx.config.ts, app/*, and merges NexusTS into the existing package.json and tsconfig.json. Skips files that already exist (unless --force).",
|
|
680
|
+
examples: [
|
|
681
|
+
"nx init",
|
|
682
|
+
"nx init ./my-existing-app",
|
|
683
|
+
"nx init --style nest --view inertia --orm drizzle --db bun-sqlite",
|
|
684
|
+
"nx init --force"
|
|
685
|
+
],
|
|
686
|
+
flags: [
|
|
687
|
+
{ name: "target", description: "Target directory (default: cwd)" },
|
|
688
|
+
{
|
|
689
|
+
name: "style",
|
|
690
|
+
description: "Routing style (nest|adonis|functional|mixed)"
|
|
691
|
+
},
|
|
692
|
+
{ name: "view", description: "View engine (rendu|edge|eta|inertia|none)" },
|
|
693
|
+
{ name: "orm", description: "ORM driver (drizzle|prisma|kysely|none)" },
|
|
694
|
+
{
|
|
695
|
+
name: "db",
|
|
696
|
+
description: "Database driver (bun-sqlite|node-sqlite|libsql|postgres|mysql|none)"
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "frontend",
|
|
700
|
+
description: "Inertia frontend (react|vue|svelte|solid)"
|
|
701
|
+
},
|
|
702
|
+
{ name: "no-ssr", description: "Disable Inertia SSR" },
|
|
703
|
+
{ name: "force", description: "Overwrite files that already exist" },
|
|
704
|
+
{ name: "no-interaction", description: "Disable interactive prompts" }
|
|
705
|
+
],
|
|
706
|
+
async run(ctx) {
|
|
707
|
+
const interactive = !flagBool(ctx.flags, "no-interaction", false);
|
|
708
|
+
const force = flagBool(ctx.flags, "force", false);
|
|
709
|
+
const target = resolve2(ctx.cwd, ctx.flags["target"] ?? ".");
|
|
710
|
+
if (!existsSync(target)) {
|
|
711
|
+
logger2.error(`Target directory does not exist: ${target}`);
|
|
712
|
+
logger2.info(`Run \`nx new <name>\` to create a fresh project, or \`mkdir -p ${target}\` first.`);
|
|
713
|
+
return 1;
|
|
714
|
+
}
|
|
715
|
+
const routing = ctx.flags["style"] ?? await select("Routing style", ["nest", "adonis", "functional"], {
|
|
716
|
+
interactive,
|
|
717
|
+
default: "nest"
|
|
718
|
+
});
|
|
719
|
+
const view = ctx.flags["view"] ?? await select("View engine", ["rendu", "edge", "eta", "inertia", "none"], {
|
|
720
|
+
interactive,
|
|
721
|
+
default: "rendu"
|
|
722
|
+
});
|
|
723
|
+
const orm = ctx.flags["orm"] ?? await select("ORM driver", ["drizzle", "prisma", "kysely", "none"], {
|
|
724
|
+
interactive,
|
|
725
|
+
default: "drizzle"
|
|
726
|
+
});
|
|
727
|
+
const db = ctx.flags["db"] ?? await select("Database driver", ["bun-sqlite", "node-sqlite", "libsql", "postgres", "mysql", "none"], {
|
|
728
|
+
interactive,
|
|
729
|
+
default: "bun-sqlite"
|
|
730
|
+
});
|
|
731
|
+
const frontend = ctx.flags["frontend"] ?? await select("Inertia frontend", ["react", "vue", "svelte", "solid"], {
|
|
732
|
+
interactive,
|
|
733
|
+
default: "react"
|
|
734
|
+
});
|
|
735
|
+
const ssr = !flagBool(ctx.flags, "no-ssr", false);
|
|
736
|
+
const plan = [
|
|
737
|
+
{ path: "nx.config.ts", mode: "write" },
|
|
738
|
+
{ path: "package.json", mode: "merge-pkg" },
|
|
739
|
+
{ path: "tsconfig.json", mode: "merge-tsconfig" },
|
|
740
|
+
{ path: "public/.gitkeep", mode: "write" },
|
|
741
|
+
{ path: "resources/views/welcome.html", mode: "write" },
|
|
742
|
+
{ path: ".env", mode: "skip" },
|
|
743
|
+
{ path: ".env.local", mode: "skip" },
|
|
744
|
+
{ path: ".gitignore", mode: "skip" },
|
|
745
|
+
{ path: "app/main.ts", mode: "write" },
|
|
746
|
+
{ path: "app/app.module.ts", mode: "write" },
|
|
747
|
+
{ path: "app/controllers/home.controller.ts", mode: "write" },
|
|
748
|
+
{ path: "README.md", mode: "write" }
|
|
749
|
+
];
|
|
750
|
+
const created = [];
|
|
751
|
+
const skipped = [];
|
|
752
|
+
const merged = [];
|
|
753
|
+
mkdirSync(resolve2(target, "app/controllers"), { recursive: true });
|
|
754
|
+
mkdirSync(resolve2(target, "public"), { recursive: true });
|
|
755
|
+
mkdirSync(resolve2(target, "resources/views"), { recursive: true });
|
|
756
|
+
for (const entry of plan) {
|
|
757
|
+
const abs = resolve2(target, entry.path);
|
|
758
|
+
const exists = existsSync(abs);
|
|
759
|
+
if (entry.mode === "merge-pkg") {
|
|
760
|
+
if (exists) {
|
|
761
|
+
mergePackageJson(abs, {
|
|
762
|
+
"@nexusts/core": "*",
|
|
763
|
+
"reflect-metadata": "^0.2.2",
|
|
764
|
+
hono: "^4.6.0",
|
|
765
|
+
zod: "^3.23.8"
|
|
766
|
+
});
|
|
767
|
+
merged.push(entry.path);
|
|
768
|
+
} else {
|
|
769
|
+
writeFileSync(abs, JSON.stringify({
|
|
770
|
+
name: target.split("/").pop() ?? "nexus-app",
|
|
771
|
+
version: "0.1.0",
|
|
772
|
+
type: "module",
|
|
773
|
+
private: true,
|
|
774
|
+
scripts: {
|
|
775
|
+
dev: "bun --hot app/main.ts",
|
|
776
|
+
build: "bun run build.ts",
|
|
777
|
+
start: "bun app/main.ts",
|
|
778
|
+
test: "vitest",
|
|
779
|
+
nx: "nx"
|
|
780
|
+
},
|
|
781
|
+
dependencies: {
|
|
782
|
+
"@nexusts/core": "*",
|
|
783
|
+
"reflect-metadata": "^0.2.2",
|
|
784
|
+
hono: "^4.6.0",
|
|
785
|
+
zod: "^3.23.8"
|
|
786
|
+
}
|
|
787
|
+
}, null, 2));
|
|
788
|
+
created.push(entry.path);
|
|
789
|
+
}
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (entry.mode === "merge-tsconfig") {
|
|
793
|
+
if (exists) {
|
|
794
|
+
mergeTsconfig(abs, {
|
|
795
|
+
experimentalDecorators: true,
|
|
796
|
+
emitDecoratorMetadata: true
|
|
797
|
+
});
|
|
798
|
+
merged.push(entry.path);
|
|
799
|
+
} else {
|
|
800
|
+
writeFileSync(abs, defaultTsconfig());
|
|
801
|
+
created.push(entry.path);
|
|
802
|
+
}
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (exists && !force) {
|
|
806
|
+
skipped.push(entry.path);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const content = renderContent(entry.path, {
|
|
810
|
+
routing,
|
|
811
|
+
view,
|
|
812
|
+
viewPaths: view === "none" ? "" : "resources/views",
|
|
813
|
+
orm,
|
|
814
|
+
dbDriver: db,
|
|
815
|
+
dbUrl: db === "bun-sqlite" || db === "node-sqlite" ? "app.db" : "",
|
|
816
|
+
inertiaFrontend: frontend,
|
|
817
|
+
inertiaSSR: ssr,
|
|
818
|
+
inertiaVersion: "1.0.0",
|
|
819
|
+
targetName: target.split("/").pop() ?? "nexus-app"
|
|
820
|
+
});
|
|
821
|
+
writeFileSync(abs, content);
|
|
822
|
+
created.push(entry.path);
|
|
823
|
+
}
|
|
824
|
+
logger2.success(`initialized NexusTS in ${target}`);
|
|
825
|
+
logger2.blank();
|
|
826
|
+
if (created.length) {
|
|
827
|
+
logger2.heading("Created");
|
|
828
|
+
for (const f of created)
|
|
829
|
+
logger2.info(` + ${f}`);
|
|
830
|
+
}
|
|
831
|
+
if (merged.length) {
|
|
832
|
+
logger2.heading("Merged into existing files");
|
|
833
|
+
for (const f of merged)
|
|
834
|
+
logger2.info(` ~ ${f}`);
|
|
835
|
+
}
|
|
836
|
+
if (skipped.length) {
|
|
837
|
+
logger2.heading("Skipped (already exist; use --force to overwrite)");
|
|
838
|
+
for (const f of skipped)
|
|
839
|
+
logger2.info(` - ${f}`);
|
|
840
|
+
}
|
|
841
|
+
logger2.blank();
|
|
842
|
+
logger2.heading("Next steps");
|
|
843
|
+
logger2.info(` cd ${target === ctx.cwd ? "." : target}`);
|
|
844
|
+
logger2.info(` bun install`);
|
|
845
|
+
logger2.info(` bun run dev`);
|
|
846
|
+
logger2.blank();
|
|
847
|
+
return 0;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
function renderContent(path, ctx) {
|
|
851
|
+
switch (path) {
|
|
852
|
+
case "nx.config.ts":
|
|
853
|
+
return render(templates.project["nx.config.ts"], ctx);
|
|
854
|
+
case "public/.gitkeep":
|
|
855
|
+
return "";
|
|
856
|
+
case "resources/views/welcome.html":
|
|
857
|
+
return `<h1>Welcome to ${ctx.targetName}</h1>
|
|
858
|
+
<p>This is a sample Rendu template.</p>
|
|
859
|
+
<p>Founded <?= year ?>.</p>
|
|
860
|
+
`;
|
|
861
|
+
case ".gitignore":
|
|
862
|
+
return `# NexusTS
|
|
863
|
+
node_modules/
|
|
864
|
+
app.db
|
|
865
|
+
*.db
|
|
866
|
+
.env.local
|
|
867
|
+
dist/
|
|
868
|
+
`;
|
|
869
|
+
case ".env":
|
|
870
|
+
return `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
871
|
+
# NexusTS \u2014 Environment Variables (committed to git)
|
|
872
|
+
#
|
|
873
|
+
# Shared defaults for all environments. Override locally via
|
|
874
|
+
# .env.local (gitignored) or by environment via .env.{NODE_ENV}
|
|
875
|
+
# (e.g. .env.production, .env.development).
|
|
876
|
+
#
|
|
877
|
+
# Uncomment the database config for your driver:
|
|
878
|
+
# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
879
|
+
|
|
880
|
+
# \u2500\u2500 App \u2500\u2500
|
|
881
|
+
NODE_ENV=development
|
|
882
|
+
PORT=3000
|
|
883
|
+
|
|
884
|
+
# \u2500\u2500 Session secret (REQUIRED) \u2500\u2500
|
|
885
|
+
# Generate with: openssl rand -base64 32
|
|
886
|
+
SESSION_SECRET=change-me-in-production
|
|
887
|
+
|
|
888
|
+
# \u2500\u2500 Database: SQLite (default, zero config) \u2500\u2500
|
|
889
|
+
DATABASE_URL=app.db
|
|
890
|
+
|
|
891
|
+
# \u2500\u2500 Database: PostgreSQL \u2500\u2500
|
|
892
|
+
# DATABASE_URL=postgres://user:password@localhost:5432/myapp
|
|
893
|
+
|
|
894
|
+
# \u2500\u2500 Database: MySQL \u2500\u2500
|
|
895
|
+
# DATABASE_URL=mysql://user:password@localhost:3306/myapp
|
|
896
|
+
|
|
897
|
+
# \u2500\u2500 Better Auth (if using nexusjs/auth) \u2500\u2500
|
|
898
|
+
# BETTER_AUTH_SECRET=
|
|
899
|
+
# BETTER_AUTH_URL=http://localhost:3000
|
|
900
|
+
`;
|
|
901
|
+
case ".env.local":
|
|
902
|
+
return `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
903
|
+
# NexusTS \u2014 Local Overrides (DO NOT COMMIT to git)
|
|
904
|
+
#
|
|
905
|
+
# This file is gitignored. Use it for secrets and local
|
|
906
|
+
# configuration that should never be checked in.
|
|
907
|
+
# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
908
|
+
|
|
909
|
+
# Override any value from .env here:
|
|
910
|
+
# DATABASE_URL=postgres://user:password@localhost:5432/myapp
|
|
911
|
+
# SESSION_SECRET=my-local-secret
|
|
912
|
+
`;
|
|
913
|
+
case "app/main.ts":
|
|
914
|
+
return `import 'reflect-metadata';
|
|
915
|
+
import { Application } from '@nexusts/core';
|
|
916
|
+
import { StaticModule } from '@nexusts/static';
|
|
917
|
+
import { AppModule } from './app.module.js';
|
|
918
|
+
|
|
919
|
+
const app = new Application(AppModule);
|
|
920
|
+
// Serve ./public files under /static/*
|
|
921
|
+
app.server.app.use('/static/*', StaticModule.mount({ root: './public', prefix: '/static' }));
|
|
922
|
+
|
|
923
|
+
const port = Number(process.env["PORT"] ?? 3000);
|
|
924
|
+
await app.listen(port);
|
|
925
|
+
console.log("[nexusjs] Listening on http://localhost:" + port);
|
|
926
|
+
`;
|
|
927
|
+
case "app/app.module.ts":
|
|
928
|
+
return `import { Module } from '@nexusts/core';
|
|
929
|
+
import { HomeController } from './controllers/home.controller.js';
|
|
930
|
+
|
|
931
|
+
@Module({
|
|
932
|
+
imports: [],
|
|
933
|
+
controllers: [HomeController],
|
|
934
|
+
})
|
|
935
|
+
export class AppModule {}
|
|
936
|
+
`;
|
|
937
|
+
case "app/controllers/home.controller.ts":
|
|
938
|
+
return `import { Controller, Get } from '@nexusts/core';
|
|
939
|
+
|
|
940
|
+
@Controller('/')
|
|
941
|
+
export class HomeController {
|
|
942
|
+
@Get('/')
|
|
943
|
+
index() {
|
|
944
|
+
return {
|
|
945
|
+
view: 'welcome.html',
|
|
946
|
+
data: { year: new Date().getFullYear() },
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
`;
|
|
951
|
+
case "README.md":
|
|
952
|
+
return `# ${ctx.targetName}
|
|
953
|
+
|
|
954
|
+
A Nexus project.
|
|
955
|
+
|
|
956
|
+
## Run
|
|
957
|
+
|
|
958
|
+
\`\`\`bash
|
|
959
|
+
bun install
|
|
960
|
+
bun run dev
|
|
961
|
+
\`\`\`
|
|
962
|
+
|
|
963
|
+
## Scaffolding
|
|
964
|
+
|
|
965
|
+
\`\`\`bash
|
|
966
|
+
bunx nx make:crud Post
|
|
967
|
+
\`\`\`
|
|
968
|
+
`;
|
|
969
|
+
default:
|
|
970
|
+
throw new Error(`No render template for: ${path}`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function defaultTsconfig() {
|
|
974
|
+
return `{
|
|
975
|
+
"compilerOptions": {
|
|
976
|
+
"target": "ES2022",
|
|
977
|
+
"module": "ESNext",
|
|
978
|
+
"moduleResolution": "bundler",
|
|
979
|
+
"experimentalDecorators": true,
|
|
980
|
+
"emitDecoratorMetadata": true,
|
|
981
|
+
"strict": true,
|
|
982
|
+
"esModuleInterop": true,
|
|
983
|
+
"skipLibCheck": true,
|
|
984
|
+
"types": ["bun-types"]
|
|
985
|
+
},
|
|
986
|
+
"include": ["app/**/*.ts", "nx.config.ts"]
|
|
987
|
+
}
|
|
988
|
+
`;
|
|
989
|
+
}
|
|
990
|
+
function mergePackageJson(path, additions) {
|
|
991
|
+
const raw = readFileSync(path, "utf8");
|
|
992
|
+
const pkg = parseJsonLoose(raw);
|
|
993
|
+
let changed = false;
|
|
994
|
+
if (!pkg["type"]) {
|
|
995
|
+
pkg["type"] = "module";
|
|
996
|
+
changed = true;
|
|
997
|
+
}
|
|
998
|
+
if (!pkg["private"]) {
|
|
999
|
+
pkg["private"] = true;
|
|
1000
|
+
changed = true;
|
|
1001
|
+
}
|
|
1002
|
+
const SCRIPTS = {
|
|
1003
|
+
dev: "bun --hot app/main.ts",
|
|
1004
|
+
build: "bun run build.ts",
|
|
1005
|
+
start: "bun app/main.ts",
|
|
1006
|
+
test: "vitest",
|
|
1007
|
+
nx: "nx"
|
|
1008
|
+
};
|
|
1009
|
+
const existingScripts = pkg["scripts"] ?? {};
|
|
1010
|
+
for (const [k, v] of Object.entries(SCRIPTS)) {
|
|
1011
|
+
if (!(k in existingScripts)) {
|
|
1012
|
+
existingScripts[k] = v;
|
|
1013
|
+
changed = true;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (Object.keys(existingScripts).length > 0) {
|
|
1017
|
+
pkg["scripts"] = existingScripts;
|
|
1018
|
+
}
|
|
1019
|
+
const deps = pkg["dependencies"] ?? {};
|
|
1020
|
+
for (const [k, v] of Object.entries(additions)) {
|
|
1021
|
+
if (!(k in deps)) {
|
|
1022
|
+
deps[k] = v;
|
|
1023
|
+
changed = true;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
pkg["dependencies"] = deps;
|
|
1027
|
+
if (changed) {
|
|
1028
|
+
writeFileSync(path, JSON.stringify(pkg, null, 2) + `
|
|
1029
|
+
`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function mergeTsconfig(path, additions) {
|
|
1033
|
+
const raw = readFileSync(path, "utf8");
|
|
1034
|
+
const cfg = parseJsonLoose(raw);
|
|
1035
|
+
const co = cfg.compilerOptions ?? {};
|
|
1036
|
+
let changed = false;
|
|
1037
|
+
for (const [k, v] of Object.entries(additions)) {
|
|
1038
|
+
if (!(k in co)) {
|
|
1039
|
+
co[k] = v;
|
|
1040
|
+
changed = true;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const inc = cfg.include ?? [];
|
|
1044
|
+
if (!inc.includes("app/**/*.ts")) {
|
|
1045
|
+
inc.push("app/**/*.ts");
|
|
1046
|
+
changed = true;
|
|
1047
|
+
}
|
|
1048
|
+
if (!inc.includes("nx.config.ts")) {
|
|
1049
|
+
inc.push("nx.config.ts");
|
|
1050
|
+
changed = true;
|
|
1051
|
+
}
|
|
1052
|
+
if (changed) {
|
|
1053
|
+
cfg.compilerOptions = co;
|
|
1054
|
+
cfg.include = inc;
|
|
1055
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + `
|
|
1056
|
+
`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
var init_default = initCommand;
|
|
1060
|
+
|
|
1061
|
+
// packages/cli/src/commands/make-auth.ts
|
|
1062
|
+
import { resolve as resolve3 } from "path";
|
|
1063
|
+
import {
|
|
1064
|
+
flagList,
|
|
1065
|
+
logger as logger3,
|
|
1066
|
+
render as render2,
|
|
1067
|
+
writeFile
|
|
1068
|
+
} from "@nexusts/core/index.js";
|
|
1069
|
+
var AUTH_INSTANCE_TEMPLATE = `/**
|
|
1070
|
+
* Better-auth instance \u2014 generated by \`nx make:auth\`.
|
|
1071
|
+
*
|
|
1072
|
+
* Edit \`nx.config.ts\` (\`auth\` section) instead of this file when possible.
|
|
1073
|
+
*/
|
|
1074
|
+
import 'reflect-metadata';
|
|
1075
|
+
import { createAuth } from '@nexusts/auth';
|
|
1076
|
+
|
|
1077
|
+
export const auth = createAuth({
|
|
1078
|
+
{{^jwt}}
|
|
1079
|
+
jwt: { enabled: false },
|
|
1080
|
+
{{/jwt}}
|
|
1081
|
+
{{#jwt}}
|
|
1082
|
+
jwt: { enabled: true },
|
|
1083
|
+
{{/jwt}}
|
|
1084
|
+
{{#passkey}}
|
|
1085
|
+
passkey: {
|
|
1086
|
+
enabled: true,
|
|
1087
|
+
rpName: '{{ passkeyRpName }}',
|
|
1088
|
+
rpId: '{{ passkeyRpId }}',
|
|
1089
|
+
origin: '{{ passkeyOrigin }}',
|
|
1090
|
+
},
|
|
1091
|
+
{{/passkey}}
|
|
1092
|
+
{{#providers}}
|
|
1093
|
+
socialProviders: {
|
|
1094
|
+
{{#entries}}
|
|
1095
|
+
{{ name }}: {
|
|
1096
|
+
clientId: process.env.{{ envVar }}_CLIENT_ID!,
|
|
1097
|
+
clientSecret: process.env.{{ envVar }}_CLIENT_SECRET!,
|
|
1098
|
+
},
|
|
1099
|
+
{{/entries}}
|
|
1100
|
+
},
|
|
1101
|
+
{{/providers}}
|
|
1102
|
+
});
|
|
1103
|
+
`;
|
|
1104
|
+
var ENV_EXAMPLE_TEMPLATE = `# Better Auth \u2014 generated by \`nx make:auth\`.
|
|
1105
|
+
# Generate a secret with: openssl rand -base64 32
|
|
1106
|
+
BETTER_AUTH_SECRET=
|
|
1107
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
1108
|
+
{{#providers}}
|
|
1109
|
+
{{#entries}}
|
|
1110
|
+
{{ envVar }}_CLIENT_ID=
|
|
1111
|
+
{{ envVar }}_CLIENT_SECRET=
|
|
1112
|
+
{{/entries}}
|
|
1113
|
+
{{/providers}}
|
|
1114
|
+
`;
|
|
1115
|
+
var MODULE_UPDATE_HINT = `import { AuthModule } from '@nexusts/auth';
|
|
1116
|
+
// In your AppModule.imports:
|
|
1117
|
+
imports: [AuthModule.forRoot({ /* ... */ })],
|
|
1118
|
+
`;
|
|
1119
|
+
var KNOWN_PROVIDERS = {
|
|
1120
|
+
github: { env: "GITHUB", label: "GitHub" },
|
|
1121
|
+
google: { env: "GOOGLE", label: "Google" },
|
|
1122
|
+
discord: { env: "DISCORD", label: "Discord" },
|
|
1123
|
+
microsoft: { env: "MICROSOFT", label: "Microsoft" },
|
|
1124
|
+
apple: { env: "APPLE", label: "Apple" },
|
|
1125
|
+
facebook: { env: "FACEBOOK", label: "Facebook" },
|
|
1126
|
+
gitlab: { env: "GITLAB", label: "GitLab" },
|
|
1127
|
+
slack: { env: "SLACK", label: "Slack" },
|
|
1128
|
+
twitter: { env: "TWITTER", label: "Twitter" },
|
|
1129
|
+
spotify: { env: "SPOTIFY", label: "Spotify" },
|
|
1130
|
+
linkedin: { env: "LINKEDIN", label: "LinkedIn" }
|
|
1131
|
+
};
|
|
1132
|
+
var makeAuthCommand = {
|
|
1133
|
+
name: "make:auth",
|
|
1134
|
+
aliases: ["auth", "auth-install"],
|
|
1135
|
+
summary: "Scaffold the auth module",
|
|
1136
|
+
description: "Generates app/auth/auth.ts, .env.example, and an auth-ready module skeleton using better-auth.",
|
|
1137
|
+
examples: [
|
|
1138
|
+
"nx make:auth",
|
|
1139
|
+
"nx make:auth --provider github --jwt",
|
|
1140
|
+
"nx make:auth --provider github,google --jwt --passkey --rp-id example.com"
|
|
1141
|
+
],
|
|
1142
|
+
flags: [
|
|
1143
|
+
{
|
|
1144
|
+
name: "provider",
|
|
1145
|
+
description: "Comma-separated OAuth providers (github, google, ...)"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
name: "jwt",
|
|
1149
|
+
description: "Enable the JWT plugin (token + JWKS endpoint)"
|
|
1150
|
+
},
|
|
1151
|
+
{ name: "passkey", description: "Enable the passkey (WebAuthn) plugin" },
|
|
1152
|
+
{ name: "rp-id", description: "Passkey RP ID (defaults to 'localhost')" },
|
|
1153
|
+
{ name: "rp-name", description: "Passkey RP display name" },
|
|
1154
|
+
{
|
|
1155
|
+
name: "origin",
|
|
1156
|
+
description: "Passkey origin (defaults to http://localhost:3000)"
|
|
1157
|
+
}
|
|
1158
|
+
],
|
|
1159
|
+
async run(ctx) {
|
|
1160
|
+
logger3.heading("Scaffolding auth module");
|
|
1161
|
+
const providers = flagList(ctx.flags, "provider");
|
|
1162
|
+
const jwtEnabled = ctx.flags["jwt"] === true;
|
|
1163
|
+
const passkeyEnabled = ctx.flags["passkey"] === true;
|
|
1164
|
+
const rpId = ctx.flags["rp-id"] ?? "localhost";
|
|
1165
|
+
const rpName = ctx.flags["rp-name"] ?? "NexusTS App";
|
|
1166
|
+
const origin = ctx.flags["origin"] ?? "http://localhost:3000";
|
|
1167
|
+
const entries = providers.map((p) => {
|
|
1168
|
+
const known = KNOWN_PROVIDERS[p.toLowerCase()];
|
|
1169
|
+
return {
|
|
1170
|
+
name: p.toLowerCase(),
|
|
1171
|
+
envVar: known?.env ?? p.toUpperCase()
|
|
1172
|
+
};
|
|
1173
|
+
});
|
|
1174
|
+
const authCode = render2(AUTH_INSTANCE_TEMPLATE, {
|
|
1175
|
+
jwt: jwtEnabled,
|
|
1176
|
+
passkey: passkeyEnabled,
|
|
1177
|
+
providers: providers.length > 0,
|
|
1178
|
+
entries,
|
|
1179
|
+
passkeyRpName: rpName,
|
|
1180
|
+
passkeyRpId: rpId,
|
|
1181
|
+
passkeyOrigin: Array.isArray(origin) ? origin.join(",") : origin
|
|
1182
|
+
});
|
|
1183
|
+
const authOut = resolve3(ctx.cwd, "app/auth/auth.ts");
|
|
1184
|
+
if (writeFile(authOut, authCode)) {
|
|
1185
|
+
logger3.success(`created ${authOut}`);
|
|
1186
|
+
} else {
|
|
1187
|
+
logger3.warn(`skipped (exists): ${authOut}`);
|
|
1188
|
+
}
|
|
1189
|
+
const envCode = render2(ENV_EXAMPLE_TEMPLATE, {
|
|
1190
|
+
providers: providers.length > 0,
|
|
1191
|
+
entries
|
|
1192
|
+
});
|
|
1193
|
+
const envOut = resolve3(ctx.cwd, ".env.example");
|
|
1194
|
+
if (writeFile(envOut, envCode, { skipIfExists: true })) {
|
|
1195
|
+
logger3.success(`created ${envOut}`);
|
|
1196
|
+
} else {
|
|
1197
|
+
logger3.warn(`skipped (exists): ${envOut}`);
|
|
1198
|
+
}
|
|
1199
|
+
logger3.blank();
|
|
1200
|
+
logger3.heading("Next steps");
|
|
1201
|
+
logger3.info("1. Add a secret to .env:");
|
|
1202
|
+
logger3.info(" BETTER_AUTH_SECRET=$(openssl rand -base64 32)");
|
|
1203
|
+
logger3.info("2. Wire the module in AppModule:");
|
|
1204
|
+
logger3.info(` ${MODULE_UPDATE_HINT.trim()}`);
|
|
1205
|
+
if (providers.length > 0) {
|
|
1206
|
+
logger3.info(`3. Set up OAuth credentials for: ${providers.join(", ")}`);
|
|
1207
|
+
}
|
|
1208
|
+
if (passkeyEnabled) {
|
|
1209
|
+
logger3.info("4. Configure passkey RP ID + origin for your domain.");
|
|
1210
|
+
}
|
|
1211
|
+
logger3.info("5. Run `bun --hot app/main.ts` to start the server.");
|
|
1212
|
+
logger3.blank();
|
|
1213
|
+
return 0;
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
var make_auth_default = makeAuthCommand;
|
|
1217
|
+
|
|
1218
|
+
// packages/cli/src/commands/make-controller.ts
|
|
1219
|
+
import { resolve as resolve4 } from "path";
|
|
1220
|
+
import { logger as logger4, nameVariants, render as render3, writeFile as writeFile2 } from "@nexusts/core/index.js";
|
|
1221
|
+
var makeControllerCommand = {
|
|
1222
|
+
name: "make:controller",
|
|
1223
|
+
aliases: ["mc", "make-controller"],
|
|
1224
|
+
summary: "Generate a controller class",
|
|
1225
|
+
description: "Generates a controller file under app/controllers/. The routing style is read from nx.config.ts.",
|
|
1226
|
+
examples: [
|
|
1227
|
+
"nx make:controller User",
|
|
1228
|
+
"nx make:controller Post --style nest",
|
|
1229
|
+
"nx make:controller Webhook --style functional"
|
|
1230
|
+
],
|
|
1231
|
+
flags: [
|
|
1232
|
+
{
|
|
1233
|
+
name: "style",
|
|
1234
|
+
description: "Override routing style (nest|adonis|functional)"
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
name: "no-service",
|
|
1238
|
+
description: "Skip injecting a service into the controller"
|
|
1239
|
+
}
|
|
1240
|
+
],
|
|
1241
|
+
async run(ctx) {
|
|
1242
|
+
const name = ctx.positional[0];
|
|
1243
|
+
if (!name) {
|
|
1244
|
+
logger4.error("Usage: nx make:controller <Name>");
|
|
1245
|
+
return 1;
|
|
1246
|
+
}
|
|
1247
|
+
const variants = nameVariants(name);
|
|
1248
|
+
const style = ctx.flags["style"] ?? ctx.config.routing;
|
|
1249
|
+
if (!["nest", "adonis", "functional"].includes(style)) {
|
|
1250
|
+
logger4.error(`Unknown style: ${style}. Allowed: nest, adonis, functional.`);
|
|
1251
|
+
return 1;
|
|
1252
|
+
}
|
|
1253
|
+
const skipService = ctx.flags["no-service"] === true;
|
|
1254
|
+
const serviceName = `${variants.pascal}Service`;
|
|
1255
|
+
const serviceCamel = variants.camel + "Service";
|
|
1256
|
+
const tpl = templates.controller[style];
|
|
1257
|
+
const code = render3(tpl, {
|
|
1258
|
+
name: variants.pascal,
|
|
1259
|
+
camel: variants.camel,
|
|
1260
|
+
kebab: variants.kebab,
|
|
1261
|
+
snake: variants.snake,
|
|
1262
|
+
pascal: variants.pascal,
|
|
1263
|
+
service: serviceName,
|
|
1264
|
+
serviceCamel
|
|
1265
|
+
}).replace(/import .*\n/g, skipService ? (m) => m.includes("services/") ? "" : m : (m) => m);
|
|
1266
|
+
const out = resolve4(ctx.cwd, ctx.config.paths.controllers, `${variants.kebab}.controller.ts`);
|
|
1267
|
+
const ok = writeFile2(out, code, { skipIfExists: false });
|
|
1268
|
+
if (!ok) {
|
|
1269
|
+
logger4.error(`Refusing to overwrite existing file: ${out}`);
|
|
1270
|
+
return 1;
|
|
1271
|
+
}
|
|
1272
|
+
logger4.success(`created ${out}`);
|
|
1273
|
+
logger4.finger(`edit ${variants.kebab}.controller.ts and add to a module.`);
|
|
1274
|
+
return 0;
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
var make_controller_default = makeControllerCommand;
|
|
1278
|
+
|
|
1279
|
+
// packages/cli/src/commands/make-crud.ts
|
|
1280
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1281
|
+
import { dirname, resolve as resolve5 } from "path";
|
|
1282
|
+
import {
|
|
1283
|
+
flagBool as flagBool2,
|
|
1284
|
+
logger as logger5,
|
|
1285
|
+
nameVariants as nameVariants2,
|
|
1286
|
+
render as render4,
|
|
1287
|
+
writeFile as writeFile3
|
|
1288
|
+
} from "@nexusts/core/index.js";
|
|
1289
|
+
|
|
1290
|
+
// packages/cli/src/templates/model/drizzle-dialect.ts
|
|
1291
|
+
function renderDrizzleDialect(dialect) {
|
|
1292
|
+
const spec = DIALECT_SPECS[dialect] ?? DIALECT_SPECS.sqlite;
|
|
1293
|
+
return `
|
|
1294
|
+
import { ${spec.imports.join(", ")} } from '${spec.importPath}';
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* {{ tableName }} \u2014 generated by \`nx make:model {{ name }}\`.
|
|
1298
|
+
*/
|
|
1299
|
+
export const {{ snake }} = ${spec.tableFn}('{{ tableName }}', {
|
|
1300
|
+
id: ${spec.idHelper}('id').primaryKey(${spec.idOpts}),
|
|
1301
|
+
{{ columns }}
|
|
1302
|
+
createdAt: ${spec.tsTimestamp}('created_at'${spec.tsDateMode}).notNull().$defaultFn(() => new Date()),
|
|
1303
|
+
updatedAt: ${spec.tsTimestamp}('updated_at'${spec.tsDateMode}).notNull().$defaultFn(() => new Date()),
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
export type {{ name }} = typeof {{ snake }}.$inferSelect;
|
|
1307
|
+
export type New{{ name }} = typeof {{ snake }}.$inferInsert;
|
|
1308
|
+
`.trimStart();
|
|
1309
|
+
}
|
|
1310
|
+
var DIALECT_SPECS = {
|
|
1311
|
+
postgres: {
|
|
1312
|
+
imports: [
|
|
1313
|
+
"pgTable",
|
|
1314
|
+
"serial",
|
|
1315
|
+
"text",
|
|
1316
|
+
"timestamp",
|
|
1317
|
+
"boolean",
|
|
1318
|
+
"integer",
|
|
1319
|
+
"bigint"
|
|
1320
|
+
],
|
|
1321
|
+
importPath: "drizzle-orm/pg-core",
|
|
1322
|
+
tableFn: "pgTable",
|
|
1323
|
+
idHelper: "serial",
|
|
1324
|
+
idOpts: "",
|
|
1325
|
+
tsTimestamp: "timestamp",
|
|
1326
|
+
tsDateMode: ""
|
|
1327
|
+
},
|
|
1328
|
+
"bun-sqlite": {
|
|
1329
|
+
imports: ["sqliteTable", "integer", "text", "real"],
|
|
1330
|
+
importPath: "drizzle-orm/sqlite-core",
|
|
1331
|
+
tableFn: "sqliteTable",
|
|
1332
|
+
idHelper: "integer",
|
|
1333
|
+
idOpts: "{ autoIncrement: true }",
|
|
1334
|
+
tsTimestamp: "integer",
|
|
1335
|
+
tsDateMode: ", { mode: 'timestamp' }"
|
|
1336
|
+
},
|
|
1337
|
+
sqlite: {
|
|
1338
|
+
imports: ["sqliteTable", "integer", "text", "real"],
|
|
1339
|
+
importPath: "drizzle-orm/sqlite-core",
|
|
1340
|
+
tableFn: "sqliteTable",
|
|
1341
|
+
idHelper: "integer",
|
|
1342
|
+
idOpts: "{ autoIncrement: true }",
|
|
1343
|
+
tsTimestamp: "integer",
|
|
1344
|
+
tsDateMode: ", { mode: 'timestamp' }"
|
|
1345
|
+
},
|
|
1346
|
+
d1: {
|
|
1347
|
+
imports: ["sqliteTable", "integer", "text", "real"],
|
|
1348
|
+
importPath: "drizzle-orm/d1",
|
|
1349
|
+
tableFn: "sqliteTable",
|
|
1350
|
+
idHelper: "integer",
|
|
1351
|
+
idOpts: "{ autoIncrement: true }",
|
|
1352
|
+
tsTimestamp: "integer",
|
|
1353
|
+
tsDateMode: ", { mode: 'timestamp' }"
|
|
1354
|
+
},
|
|
1355
|
+
mysql: {
|
|
1356
|
+
imports: [
|
|
1357
|
+
"mysqlTable",
|
|
1358
|
+
"int",
|
|
1359
|
+
"text",
|
|
1360
|
+
"timestamp",
|
|
1361
|
+
"boolean",
|
|
1362
|
+
"bigint",
|
|
1363
|
+
"double"
|
|
1364
|
+
],
|
|
1365
|
+
importPath: "drizzle-orm/mysql-core",
|
|
1366
|
+
tableFn: "mysqlTable",
|
|
1367
|
+
idHelper: "int",
|
|
1368
|
+
idOpts: "{ autoIncrement: true }",
|
|
1369
|
+
tsTimestamp: "timestamp",
|
|
1370
|
+
tsDateMode: ""
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
function mapDrizzleType(dialect, type) {
|
|
1374
|
+
const t = type.toLowerCase();
|
|
1375
|
+
if (t === "text" || t === "string" || t === "varchar")
|
|
1376
|
+
return "text";
|
|
1377
|
+
if (t === "int" || t === "integer") {
|
|
1378
|
+
return dialect === "mysql" ? "int" : "integer";
|
|
1379
|
+
}
|
|
1380
|
+
if (t === "bigint" || t === "bigintunsigned")
|
|
1381
|
+
return dialect === "mysql" ? "bigint" : "bigint";
|
|
1382
|
+
if (t === "bool" || t === "boolean")
|
|
1383
|
+
return dialect === "mysql" ? "boolean" : "boolean";
|
|
1384
|
+
if (t === "float" || t === "number" || t === "real" || t === "double") {
|
|
1385
|
+
return dialect === "mysql" ? "double" : "real";
|
|
1386
|
+
}
|
|
1387
|
+
if (t === "datetime" || t === "timestamp" || t === "date") {
|
|
1388
|
+
return dialect === "mysql" ? "timestamp" : dialect === "postgres" ? "timestamp" : "integer";
|
|
1389
|
+
}
|
|
1390
|
+
if (t === "json" || t === "jsonb") {
|
|
1391
|
+
return dialect === "mysql" ? "text" : "text";
|
|
1392
|
+
}
|
|
1393
|
+
return "text";
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// packages/cli/src/commands/make-crud.ts
|
|
1397
|
+
var makeCrudCommand = {
|
|
1398
|
+
name: "make:crud",
|
|
1399
|
+
aliases: ["crud", "make-crud", "scaffold"],
|
|
1400
|
+
summary: "Generate a full CRUD scaffold for a resource",
|
|
1401
|
+
description: "Generates controller + service + repository + model + dto + module + test for a single resource, adapted to the project's nx.config.ts.",
|
|
1402
|
+
examples: [
|
|
1403
|
+
"nx make:crud Post",
|
|
1404
|
+
"nx make:crud User --no-views",
|
|
1405
|
+
"nx make:crud Comment --no-repo --no-test"
|
|
1406
|
+
],
|
|
1407
|
+
flags: [
|
|
1408
|
+
{ name: "no-views", description: "Skip Inertia view rendering" },
|
|
1409
|
+
{ name: "no-repo", description: "Skip the repository / model" },
|
|
1410
|
+
{ name: "no-test", description: "Skip generating the test file" },
|
|
1411
|
+
{ name: "style", description: "Override routing style" },
|
|
1412
|
+
{ name: "orm", description: "Override ORM driver" },
|
|
1413
|
+
{
|
|
1414
|
+
name: "dialect",
|
|
1415
|
+
description: "Drizzle dialect (postgres|mysql|sqlite|bun-sqlite|d1). Default: bun-sqlite"
|
|
1416
|
+
}
|
|
1417
|
+
],
|
|
1418
|
+
async run(ctx) {
|
|
1419
|
+
const name = ctx.positional[0];
|
|
1420
|
+
if (!name) {
|
|
1421
|
+
logger5.error("Usage: nx make:crud <Name>");
|
|
1422
|
+
return 1;
|
|
1423
|
+
}
|
|
1424
|
+
const variants = nameVariants2(name);
|
|
1425
|
+
const style = ctx.flags["style"] ?? ctx.config.routing;
|
|
1426
|
+
const orm = ctx.flags["orm"] ?? ctx.config.orm;
|
|
1427
|
+
const dialect = ctx.flags["dialect"] ?? ctx.config.dialect ?? "bun-sqlite";
|
|
1428
|
+
const noRepo = flagBool2(ctx.flags, "no-repo", false) || orm === "none";
|
|
1429
|
+
const noTest = flagBool2(ctx.flags, "no-test", false);
|
|
1430
|
+
const hasInertia = ctx.config.view === "inertia" && !flagBool2(ctx.flags, "no-views", false);
|
|
1431
|
+
const controller = `${variants.pascal}Controller`;
|
|
1432
|
+
const service = `${variants.pascal}Service`;
|
|
1433
|
+
const repository = `${variants.pascal}Repository`;
|
|
1434
|
+
const tableName = variants.pluralSnake;
|
|
1435
|
+
const viewComponent = `${variants.pascal}s/Index`;
|
|
1436
|
+
const viewShowComponent = `${variants.pascal}s/Show`;
|
|
1437
|
+
logger5.heading(`Scaffolding ${variants.pascal} (style=${style}, view=${ctx.config.view}, orm=${orm})`);
|
|
1438
|
+
const written = [];
|
|
1439
|
+
{
|
|
1440
|
+
const code = render4(templates.crud.controller, {
|
|
1441
|
+
name: variants.pascal,
|
|
1442
|
+
camel: variants.camel,
|
|
1443
|
+
kebab: variants.kebab,
|
|
1444
|
+
snake: variants.snake,
|
|
1445
|
+
tableName,
|
|
1446
|
+
service,
|
|
1447
|
+
serviceCamel: variants.camel + "Service",
|
|
1448
|
+
controller,
|
|
1449
|
+
viewComponent,
|
|
1450
|
+
viewShowComponent,
|
|
1451
|
+
hasInertia
|
|
1452
|
+
});
|
|
1453
|
+
const out = resolve5(ctx.cwd, ctx.config.paths.controllers, `${variants.kebab}.controller.ts`);
|
|
1454
|
+
if (!writeFile3(out, code, { skipIfExists: true })) {
|
|
1455
|
+
logger5.warn(`skipped (exists): ${out}`);
|
|
1456
|
+
} else {
|
|
1457
|
+
logger5.success(`created ${out}`);
|
|
1458
|
+
written.push(out);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
{
|
|
1462
|
+
const code = render4(templates.service, {
|
|
1463
|
+
name: variants.pascal,
|
|
1464
|
+
camel: variants.camel,
|
|
1465
|
+
kebab: variants.kebab,
|
|
1466
|
+
hasRepo: !noRepo,
|
|
1467
|
+
repository,
|
|
1468
|
+
repositoryCamel: variants.camel + "Repository"
|
|
1469
|
+
});
|
|
1470
|
+
const out = resolve5(ctx.cwd, ctx.config.paths.services, `${variants.kebab}.service.ts`);
|
|
1471
|
+
if (!writeFile3(out, code, { skipIfExists: true })) {
|
|
1472
|
+
logger5.warn(`skipped (exists): ${out}`);
|
|
1473
|
+
} else {
|
|
1474
|
+
logger5.success(`created ${out}`);
|
|
1475
|
+
written.push(out);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (!noRepo) {
|
|
1479
|
+
if (orm === "drizzle" || orm === "prisma" || orm === "kysely") {
|
|
1480
|
+
let code;
|
|
1481
|
+
if (orm === "drizzle") {
|
|
1482
|
+
const tpl = renderDrizzleDialect(dialect);
|
|
1483
|
+
code = render4(tpl, {
|
|
1484
|
+
name: variants.pascal,
|
|
1485
|
+
camel: variants.camel,
|
|
1486
|
+
kebab: variants.kebab,
|
|
1487
|
+
snake: variants.snake,
|
|
1488
|
+
tableName,
|
|
1489
|
+
columns: renderDrizzleColumns(dialect),
|
|
1490
|
+
prismaBlock: ""
|
|
1491
|
+
});
|
|
1492
|
+
} else {
|
|
1493
|
+
const tpl = templates.model[orm];
|
|
1494
|
+
code = render4(tpl, {
|
|
1495
|
+
name: variants.pascal,
|
|
1496
|
+
camel: variants.camel,
|
|
1497
|
+
kebab: variants.kebab,
|
|
1498
|
+
snake: variants.snake,
|
|
1499
|
+
tableName,
|
|
1500
|
+
columns: renderDefaultColumns(orm),
|
|
1501
|
+
prismaBlock: ""
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
const out = resolve5(ctx.cwd, ctx.config.paths.models, `${variants.kebab}.model.ts`);
|
|
1505
|
+
if (!writeFile3(out, code, { skipIfExists: true })) {
|
|
1506
|
+
logger5.warn(`skipped (exists): ${out}`);
|
|
1507
|
+
} else {
|
|
1508
|
+
logger5.success(`created ${out}`);
|
|
1509
|
+
written.push(out);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
const repoCode = render4(templates.repository, {
|
|
1513
|
+
name: variants.pascal,
|
|
1514
|
+
camel: variants.camel,
|
|
1515
|
+
kebab: variants.kebab,
|
|
1516
|
+
tableName,
|
|
1517
|
+
repository
|
|
1518
|
+
});
|
|
1519
|
+
const repoOut = resolve5(ctx.cwd, `${ctx.config.paths.app}/repositories`, `${variants.kebab}.repository.ts`);
|
|
1520
|
+
mkdirSync2(dirname(repoOut), { recursive: true });
|
|
1521
|
+
if (!writeFile3(repoOut, repoCode, { skipIfExists: true })) {
|
|
1522
|
+
logger5.warn(`skipped (exists): ${repoOut}`);
|
|
1523
|
+
} else {
|
|
1524
|
+
logger5.success(`created ${repoOut}`);
|
|
1525
|
+
written.push(repoOut);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
{
|
|
1529
|
+
const code = render4(templates.crud.dto, {
|
|
1530
|
+
name: variants.pascal,
|
|
1531
|
+
camel: variants.camel,
|
|
1532
|
+
kebab: variants.kebab
|
|
1533
|
+
});
|
|
1534
|
+
const out = resolve5(ctx.cwd, ctx.config.paths.dto, `${variants.kebab}.dto.ts`);
|
|
1535
|
+
if (!writeFile3(out, code, { skipIfExists: true })) {
|
|
1536
|
+
logger5.warn(`skipped (exists): ${out}`);
|
|
1537
|
+
} else {
|
|
1538
|
+
logger5.success(`created ${out}`);
|
|
1539
|
+
written.push(out);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
{
|
|
1543
|
+
const code = render4(templates.crud.module, {
|
|
1544
|
+
name: variants.pascal,
|
|
1545
|
+
camel: variants.camel,
|
|
1546
|
+
kebab: variants.kebab,
|
|
1547
|
+
controller,
|
|
1548
|
+
service,
|
|
1549
|
+
repository,
|
|
1550
|
+
hasRepo: !noRepo
|
|
1551
|
+
});
|
|
1552
|
+
const out = resolve5(ctx.cwd, ctx.config.paths.modules, `${variants.kebab}.module.ts`);
|
|
1553
|
+
if (!writeFile3(out, code, { skipIfExists: true })) {
|
|
1554
|
+
logger5.warn(`skipped (exists): ${out}`);
|
|
1555
|
+
} else {
|
|
1556
|
+
logger5.success(`created ${out}`);
|
|
1557
|
+
written.push(out);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (!noTest) {
|
|
1561
|
+
const code = render4(templates.crud.test, {
|
|
1562
|
+
name: variants.pascal,
|
|
1563
|
+
camel: variants.camel,
|
|
1564
|
+
kebab: variants.kebab,
|
|
1565
|
+
controller,
|
|
1566
|
+
service
|
|
1567
|
+
});
|
|
1568
|
+
const out = resolve5(ctx.cwd, "tests", `${variants.kebab}.test.ts`);
|
|
1569
|
+
if (!writeFile3(out, code, { skipIfExists: true })) {
|
|
1570
|
+
logger5.warn(`skipped (exists): ${out}`);
|
|
1571
|
+
} else {
|
|
1572
|
+
logger5.success(`created ${out}`);
|
|
1573
|
+
written.push(out);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
logger5.blank();
|
|
1577
|
+
logger5.heading("Next steps");
|
|
1578
|
+
logger5.info(`1. Review the generated files:`);
|
|
1579
|
+
for (const f of written)
|
|
1580
|
+
logger5.info(` ${f}`);
|
|
1581
|
+
logger5.info(`2. Add ${variants.pascal}Module to AppModule.imports.`);
|
|
1582
|
+
logger5.info(`3. ${noRepo ? "" : `Run \`bunx drizzle-kit migrate\` (or your migration tool).`}`);
|
|
1583
|
+
logger5.info(`4. Start the dev server: \`bun --hot app/main.ts\`.`);
|
|
1584
|
+
logger5.blank();
|
|
1585
|
+
return 0;
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
function renderDefaultColumns(orm) {
|
|
1589
|
+
if (orm === "drizzle") {
|
|
1590
|
+
return " title: text('title').notNull(),";
|
|
1591
|
+
}
|
|
1592
|
+
if (orm === "kysely") {
|
|
1593
|
+
return " title: string,";
|
|
1594
|
+
}
|
|
1595
|
+
return " title text,";
|
|
1596
|
+
}
|
|
1597
|
+
function renderDrizzleColumns(dialect) {
|
|
1598
|
+
const helper = mapDrizzleType(dialect, "text");
|
|
1599
|
+
return ` title: ${helper}('title').notNull(),`;
|
|
1600
|
+
}
|
|
1601
|
+
var make_crud_default = makeCrudCommand;
|
|
1602
|
+
|
|
1603
|
+
// packages/cli/src/commands/make-listener.ts
|
|
1604
|
+
import { resolve as resolve6 } from "path";
|
|
1605
|
+
import { logger as logger6, nameVariants as nameVariants3, render as render5, writeFile as writeFile4 } from "@nexusts/core/index.js";
|
|
1606
|
+
var LISTENER_TEMPLATE = `
|
|
1607
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
1608
|
+
import { EventService, OnEvent } from '@nexusts/events';
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* {{ name }} listener \u2014 generated by \`nx make:listener {{ name }}\`.
|
|
1612
|
+
*
|
|
1613
|
+
* Register handlers below with \`@OnEvent(pattern)\`. Pair with
|
|
1614
|
+
* \`scanForListeners(this, eventService)\` at boot.
|
|
1615
|
+
*/
|
|
1616
|
+
@Injectable()
|
|
1617
|
+
export class {{ name }}Listener {
|
|
1618
|
+
constructor(@Inject(EventService.TOKEN) private readonly events: EventService) {}
|
|
1619
|
+
|
|
1620
|
+
// TODO: add @OnEvent('your.event') handlers below.
|
|
1621
|
+
|
|
1622
|
+
// @OnEvent('user.created')
|
|
1623
|
+
// async onUserCreated(payload: { userId: string; email: string }) {
|
|
1624
|
+
// this.events; // unused \u2014 remove if you don't need it
|
|
1625
|
+
// }
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
import { scanForListeners } from '@nexusts/events';
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Bootstrap helper \u2014 call this from your main.ts (or wherever you
|
|
1632
|
+
* wire up the application) to register every \`@OnEvent\` handler
|
|
1633
|
+
* on this listener class.
|
|
1634
|
+
*/
|
|
1635
|
+
export async function register{{ name }}(listener: {{ name }}Listener, events: EventService) {
|
|
1636
|
+
return scanForListeners(listener, events);
|
|
1637
|
+
}
|
|
1638
|
+
`.trimStart();
|
|
1639
|
+
var makeListenerCommand = {
|
|
1640
|
+
name: "make:listener",
|
|
1641
|
+
aliases: ["ml", "make-listener"],
|
|
1642
|
+
summary: "Scaffold an event listener class",
|
|
1643
|
+
description: "Generates an @Injectable listener class with example @OnEvent handlers under app/events/listeners/.",
|
|
1644
|
+
examples: ["nx make:listener UserEvents", "nx make:listener OrderEvents"],
|
|
1645
|
+
async run(ctx) {
|
|
1646
|
+
const name = ctx.positional[0];
|
|
1647
|
+
if (!name) {
|
|
1648
|
+
logger6.error("Usage: nx make:listener <Name>");
|
|
1649
|
+
return 1;
|
|
1650
|
+
}
|
|
1651
|
+
const variants = nameVariants3(name);
|
|
1652
|
+
const code = render5(LISTENER_TEMPLATE, {
|
|
1653
|
+
name: variants.pascal,
|
|
1654
|
+
kebab: variants.kebab
|
|
1655
|
+
});
|
|
1656
|
+
const out = resolve6(ctx.cwd, "app/events/listeners", `${variants.kebab}.listener.ts`);
|
|
1657
|
+
if (writeFile4(out, code, { skipIfExists: true })) {
|
|
1658
|
+
logger6.success(`created ${out}`);
|
|
1659
|
+
} else {
|
|
1660
|
+
logger6.warn(`skipped (exists): ${out}`);
|
|
1661
|
+
}
|
|
1662
|
+
logger6.blank();
|
|
1663
|
+
logger6.heading("Next steps");
|
|
1664
|
+
logger6.info("1. Add @OnEvent('your.event') handlers to the class.");
|
|
1665
|
+
logger6.info(`2. Import + register at boot:`);
|
|
1666
|
+
logger6.info(` scanForListeners(listener, events)`);
|
|
1667
|
+
logger6.info(`3. Emit events from anywhere:`);
|
|
1668
|
+
logger6.info(` await events.emit('user.created', { userId: '1' })`);
|
|
1669
|
+
logger6.blank();
|
|
1670
|
+
return 0;
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
var make_listener_default = makeListenerCommand;
|
|
1674
|
+
|
|
1675
|
+
// packages/cli/src/commands/make-middleware.ts
|
|
1676
|
+
import { resolve as resolve7 } from "path";
|
|
1677
|
+
import { logger as logger7, nameVariants as nameVariants4, render as render6, writeFile as writeFile5 } from "@nexusts/core/index.js";
|
|
1678
|
+
var makeMiddlewareCommand = {
|
|
1679
|
+
name: "make:middleware",
|
|
1680
|
+
aliases: ["mwm", "make-middleware"],
|
|
1681
|
+
summary: "Generate a middleware class",
|
|
1682
|
+
description: "Generates an @Injectable() middleware class with a `handle(c, next)` method.",
|
|
1683
|
+
examples: ["nx make:middleware Auth", "nx make:middleware RateLimit"],
|
|
1684
|
+
async run(ctx) {
|
|
1685
|
+
const name = ctx.positional[0];
|
|
1686
|
+
if (!name) {
|
|
1687
|
+
logger7.error("Usage: nx make:middleware <Name>");
|
|
1688
|
+
return 1;
|
|
1689
|
+
}
|
|
1690
|
+
const variants = nameVariants4(name);
|
|
1691
|
+
const code = render6(templates.middleware, {
|
|
1692
|
+
name: variants.pascal
|
|
1693
|
+
});
|
|
1694
|
+
const out = resolve7(ctx.cwd, ctx.config.paths.middleware, `${variants.kebab}.middleware.ts`);
|
|
1695
|
+
writeFile5(out, code);
|
|
1696
|
+
logger7.success(`created ${out}`);
|
|
1697
|
+
logger7.finger(`register with: app.server.app.use('*', new ${variants.pascal}Middleware().handle)`);
|
|
1698
|
+
return 0;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
var make_middleware_default = makeMiddlewareCommand;
|
|
1702
|
+
|
|
1703
|
+
// packages/cli/src/commands/make-migration.ts
|
|
1704
|
+
import { resolve as resolve8 } from "path";
|
|
1705
|
+
import { logger as logger8, nameVariants as nameVariants5, render as render7, writeFile as writeFile6 } from "@nexusts/core/index.js";
|
|
1706
|
+
var makeMigrationCommand = {
|
|
1707
|
+
name: "make:migration",
|
|
1708
|
+
aliases: ["mkm", "make-migration"],
|
|
1709
|
+
summary: "Generate a migration file",
|
|
1710
|
+
description: "Generates a migration under app/database/migrations/. The template is chosen from nx.config.ts's `orm` field. Use --dialect for Drizzle migrations.",
|
|
1711
|
+
examples: [
|
|
1712
|
+
"nx make:migration create_users_table",
|
|
1713
|
+
"nx make:migration add_email_to_users --orm drizzle --dialect postgres",
|
|
1714
|
+
"nx make:migration drop_old_index --orm none"
|
|
1715
|
+
],
|
|
1716
|
+
flags: [
|
|
1717
|
+
{
|
|
1718
|
+
name: "columns",
|
|
1719
|
+
description: "Comma-separated `name:type` pairs (default: title:text)"
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
name: "orm",
|
|
1723
|
+
description: "Override ORM driver (drizzle|prisma|kysely|none)"
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
name: "dialect",
|
|
1727
|
+
description: "Drizzle dialect (postgres|mysql|sqlite|bun-sqlite|d1). Default: bun-sqlite"
|
|
1728
|
+
}
|
|
1729
|
+
],
|
|
1730
|
+
async run(ctx) {
|
|
1731
|
+
const name = ctx.positional[0];
|
|
1732
|
+
if (!name) {
|
|
1733
|
+
logger8.error("Usage: nx make:migration <Name> [--dialect ...]");
|
|
1734
|
+
return 1;
|
|
1735
|
+
}
|
|
1736
|
+
const orm = ctx.flags["orm"] ?? ctx.config.orm;
|
|
1737
|
+
const dialect = ctx.flags["dialect"] ?? ctx.config.dialect ?? "bun-sqlite";
|
|
1738
|
+
const isDrizzle = orm === "drizzle";
|
|
1739
|
+
const useGenericSql = orm === "none" || orm === "prisma" || orm === "kysely";
|
|
1740
|
+
const variants = nameVariants5(name);
|
|
1741
|
+
const tableName = inferTableName(name);
|
|
1742
|
+
const colsFlag = ctx.flags["columns"];
|
|
1743
|
+
const cols = parseColumns(colsFlag ?? "title:text");
|
|
1744
|
+
const drizzleColumns = renderDrizzleColumns2(cols, dialect);
|
|
1745
|
+
const sqlColumns = renderSqlColumns(cols, dialect);
|
|
1746
|
+
let code;
|
|
1747
|
+
let extension;
|
|
1748
|
+
if (isDrizzle) {
|
|
1749
|
+
if (!isValidDialect(dialect)) {
|
|
1750
|
+
logger8.error(`Unsupported drizzle dialect: ${dialect}. Allowed: postgres, mysql, sqlite, bun-sqlite, d1.`);
|
|
1751
|
+
return 1;
|
|
1752
|
+
}
|
|
1753
|
+
code = renderDrizzleDialect(dialect);
|
|
1754
|
+
code = render7(code, {
|
|
1755
|
+
name: variants.pascal,
|
|
1756
|
+
snake: variants.snake,
|
|
1757
|
+
tableName,
|
|
1758
|
+
columns: drizzleColumns,
|
|
1759
|
+
timestamp: formatTimestamp(new Date)
|
|
1760
|
+
});
|
|
1761
|
+
extension = "ts";
|
|
1762
|
+
} else if (useGenericSql) {
|
|
1763
|
+
const tpl = templates.migration.sql;
|
|
1764
|
+
code = render7(tpl, {
|
|
1765
|
+
name: variants.pascal,
|
|
1766
|
+
snake: variants.snake,
|
|
1767
|
+
tableName,
|
|
1768
|
+
columns: sqlColumns,
|
|
1769
|
+
timestamp: formatTimestamp(new Date)
|
|
1770
|
+
});
|
|
1771
|
+
extension = "sql";
|
|
1772
|
+
} else {
|
|
1773
|
+
logger8.error(`Unsupported ORM for migration: ${orm}. Allowed: drizzle, none, prisma, kysely.`);
|
|
1774
|
+
return 1;
|
|
1775
|
+
}
|
|
1776
|
+
const filename = `${formatTimestamp(new Date)}_${variants.snake}.${extension}`;
|
|
1777
|
+
const out = resolve8(ctx.cwd, ctx.config.paths.migrations, filename);
|
|
1778
|
+
writeFile6(out, code);
|
|
1779
|
+
logger8.success(`created ${out}`);
|
|
1780
|
+
if (isDrizzle) {
|
|
1781
|
+
logger8.finger(`run \`nx migrate\` to apply pending migrations.`);
|
|
1782
|
+
} else {
|
|
1783
|
+
logger8.finger(`run \`bunx drizzle-kit migrate\` or your migration tool.`);
|
|
1784
|
+
}
|
|
1785
|
+
return 0;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
function isValidDialect(d) {
|
|
1789
|
+
return ["postgres", "mysql", "sqlite", "bun-sqlite", "d1"].includes(d);
|
|
1790
|
+
}
|
|
1791
|
+
function inferTableName(input) {
|
|
1792
|
+
const m = /^create_(\w+)_table$/.exec(input);
|
|
1793
|
+
if (m)
|
|
1794
|
+
return m[1];
|
|
1795
|
+
const m2 = /^(?:add|remove|drop|alter)_(\w+)_to_(\w+)$/.exec(input);
|
|
1796
|
+
if (m2)
|
|
1797
|
+
return m2[2];
|
|
1798
|
+
return input.toLowerCase().replace(/s$/, "") + "s";
|
|
1799
|
+
}
|
|
1800
|
+
function parseColumns(input) {
|
|
1801
|
+
const list = Array.isArray(input) ? input : input.split(",");
|
|
1802
|
+
return list.map((s) => s.trim()).filter(Boolean).map((c) => {
|
|
1803
|
+
const [name, type = "text"] = c.split(":");
|
|
1804
|
+
return [name, type];
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
function renderSqlColumns(cols, dialect) {
|
|
1808
|
+
return cols.map(([name, type]) => {
|
|
1809
|
+
const sqlType = mapSqlType(type, dialect);
|
|
1810
|
+
const notNull = /NOT NULL/i.test(sqlType) ? "" : " NOT NULL";
|
|
1811
|
+
return ` ${name} ${sqlType}${notNull},`;
|
|
1812
|
+
}).join(`
|
|
1813
|
+
`);
|
|
1814
|
+
}
|
|
1815
|
+
function renderDrizzleColumns2(cols, dialect) {
|
|
1816
|
+
return cols.map(([name, type]) => {
|
|
1817
|
+
const helper = mapDrizzleType(dialect, type);
|
|
1818
|
+
return ` ${name}: ${helper}('${name}'),`;
|
|
1819
|
+
}).join(`
|
|
1820
|
+
`);
|
|
1821
|
+
}
|
|
1822
|
+
function mapSqlType(t, dialect) {
|
|
1823
|
+
const type = t.toLowerCase();
|
|
1824
|
+
switch (type) {
|
|
1825
|
+
case "text":
|
|
1826
|
+
case "string":
|
|
1827
|
+
case "varchar":
|
|
1828
|
+
return dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
|
|
1829
|
+
case "int":
|
|
1830
|
+
case "integer":
|
|
1831
|
+
return dialect === "postgres" ? "INTEGER" : dialect === "mysql" ? "INT" : "INTEGER";
|
|
1832
|
+
case "bigint":
|
|
1833
|
+
case "bigintunsigned":
|
|
1834
|
+
return dialect === "postgres" ? "BIGINT" : "BIGINT UNSIGNED";
|
|
1835
|
+
case "serial":
|
|
1836
|
+
return dialect === "postgres" ? "SERIAL" : "INTEGER AUTO_INCREMENT";
|
|
1837
|
+
case "bool":
|
|
1838
|
+
case "boolean":
|
|
1839
|
+
return dialect === "mysql" ? "BOOLEAN" : "BOOLEAN";
|
|
1840
|
+
case "float":
|
|
1841
|
+
case "number":
|
|
1842
|
+
case "real":
|
|
1843
|
+
case "double":
|
|
1844
|
+
return dialect === "mysql" ? "DOUBLE" : "REAL";
|
|
1845
|
+
case "datetime":
|
|
1846
|
+
case "timestamp":
|
|
1847
|
+
return dialect === "postgres" ? "TIMESTAMP" : dialect === "mysql" ? "DATETIME" : "INTEGER";
|
|
1848
|
+
case "date":
|
|
1849
|
+
return dialect === "mysql" ? "DATE" : "TEXT";
|
|
1850
|
+
case "json":
|
|
1851
|
+
return dialect === "postgres" ? "JSONB" : dialect === "mysql" ? "JSON" : "TEXT";
|
|
1852
|
+
case "jsonb":
|
|
1853
|
+
return dialect === "postgres" ? "JSONB" : "TEXT";
|
|
1854
|
+
default:
|
|
1855
|
+
return "TEXT";
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function formatTimestamp(d) {
|
|
1859
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1860
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` + `_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
1861
|
+
}
|
|
1862
|
+
var make_migration_default = makeMigrationCommand;
|
|
1863
|
+
|
|
1864
|
+
// packages/cli/src/commands/make-model.ts
|
|
1865
|
+
import { resolve as resolve9 } from "path";
|
|
1866
|
+
import {
|
|
1867
|
+
flagList as flagList2,
|
|
1868
|
+
logger as logger9,
|
|
1869
|
+
nameVariants as nameVariants6,
|
|
1870
|
+
render as render8,
|
|
1871
|
+
writeFile as writeFile7
|
|
1872
|
+
} from "@nexusts/core/index.js";
|
|
1873
|
+
var makeModelCommand = {
|
|
1874
|
+
name: "make:model",
|
|
1875
|
+
aliases: ["mmodel", "make-model"],
|
|
1876
|
+
summary: "Generate a model (table schema)",
|
|
1877
|
+
description: "Generates a model file under app/models/. The template is chosen from nx.config.ts's `orm` field (drizzle|prisma|kysely). For drizzle, use --dialect to pick the import path.",
|
|
1878
|
+
examples: [
|
|
1879
|
+
"nx make:model User",
|
|
1880
|
+
'nx make:model User --columns "name:text,email:text"',
|
|
1881
|
+
"nx make:model User --orm drizzle --dialect postgres",
|
|
1882
|
+
"nx make:model Post --orm drizzle --dialect postgres --columns 'title:text,body:text,published:boolean'"
|
|
1883
|
+
],
|
|
1884
|
+
flags: [
|
|
1885
|
+
{
|
|
1886
|
+
name: "columns",
|
|
1887
|
+
description: "Comma-separated `name:type` pairs (default: title:text)"
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
name: "orm",
|
|
1891
|
+
description: "Override ORM driver (drizzle|prisma|kysely)"
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
name: "dialect",
|
|
1895
|
+
description: "Drizzle dialect (postgres|mysql|sqlite|bun-sqlite|d1). Default: bun-sqlite"
|
|
1896
|
+
}
|
|
1897
|
+
],
|
|
1898
|
+
async run(ctx) {
|
|
1899
|
+
const name = ctx.positional[0];
|
|
1900
|
+
if (!name) {
|
|
1901
|
+
logger9.error("Usage: nx make:model <Name> [--columns name:type,...] [--dialect ...]");
|
|
1902
|
+
return 1;
|
|
1903
|
+
}
|
|
1904
|
+
const orm = ctx.flags["orm"] ?? ctx.config.orm;
|
|
1905
|
+
if (orm !== "drizzle" && orm !== "prisma" && orm !== "kysely") {
|
|
1906
|
+
logger9.error(`Unsupported ORM: ${orm}. Allowed: drizzle, prisma, kysely. Use --orm or set "orm" in nx.config.ts.`);
|
|
1907
|
+
return 1;
|
|
1908
|
+
}
|
|
1909
|
+
const variants = nameVariants6(name);
|
|
1910
|
+
const tableName = variants.pluralSnake;
|
|
1911
|
+
const colsFlag = flagList2(ctx.flags, "columns");
|
|
1912
|
+
const columns = colsFlag.length > 0 ? colsFlag : ["title:text"];
|
|
1913
|
+
const columnLines = renderColumns(columns, orm, ctx.flags["dialect"]);
|
|
1914
|
+
const prismaBlock = renderPrismaBlock(variants.pascal, columns);
|
|
1915
|
+
let code;
|
|
1916
|
+
if (orm === "drizzle") {
|
|
1917
|
+
const dialect = ctx.flags["dialect"] ?? ctx.config.dialect ?? "bun-sqlite";
|
|
1918
|
+
if (!isValidDialect2(dialect)) {
|
|
1919
|
+
logger9.error(`Unsupported drizzle dialect: ${dialect}. Allowed: postgres, mysql, sqlite, bun-sqlite, d1.`);
|
|
1920
|
+
return 1;
|
|
1921
|
+
}
|
|
1922
|
+
code = renderDrizzleDialect(dialect);
|
|
1923
|
+
code = render8(code, {
|
|
1924
|
+
name: variants.pascal,
|
|
1925
|
+
camel: variants.camel,
|
|
1926
|
+
kebab: variants.kebab,
|
|
1927
|
+
snake: variants.snake,
|
|
1928
|
+
tableName,
|
|
1929
|
+
columns: columnLines,
|
|
1930
|
+
prismaBlock
|
|
1931
|
+
});
|
|
1932
|
+
} else {
|
|
1933
|
+
const tpl = templates.model[orm];
|
|
1934
|
+
code = render8(tpl, {
|
|
1935
|
+
name: variants.pascal,
|
|
1936
|
+
camel: variants.camel,
|
|
1937
|
+
kebab: variants.kebab,
|
|
1938
|
+
snake: variants.snake,
|
|
1939
|
+
tableName,
|
|
1940
|
+
columns: columnLines,
|
|
1941
|
+
prismaBlock
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
const out = resolve9(ctx.cwd, ctx.config.paths.models, `${variants.kebab}.model.ts`);
|
|
1945
|
+
writeFile7(out, code);
|
|
1946
|
+
logger9.success(`created ${out}`);
|
|
1947
|
+
logger9.finger(`run \`nx make:migration create_${tableName}_table\` to scaffold a migration.`);
|
|
1948
|
+
if (orm === "drizzle") {
|
|
1949
|
+
logger9.finger(`run \`nx migrate\` to apply pending migrations to the database.`);
|
|
1950
|
+
}
|
|
1951
|
+
return 0;
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
function isValidDialect2(d) {
|
|
1955
|
+
return ["postgres", "mysql", "sqlite", "bun-sqlite", "d1"].includes(d);
|
|
1956
|
+
}
|
|
1957
|
+
function renderColumns(cols, orm, dialect) {
|
|
1958
|
+
const flat = cols.flatMap((c) => c.split(",")).map((c) => c.trim()).filter(Boolean);
|
|
1959
|
+
return flat.map((col) => {
|
|
1960
|
+
const [colName, colType = "text"] = col.split(":");
|
|
1961
|
+
const tsName = toCamel(colName);
|
|
1962
|
+
switch (orm) {
|
|
1963
|
+
case "drizzle": {
|
|
1964
|
+
const d = dialect ?? "bun-sqlite";
|
|
1965
|
+
const helper = mapDrizzleType(d, colType);
|
|
1966
|
+
return ` ${tsName}: ${helper}('${colName}'),`;
|
|
1967
|
+
}
|
|
1968
|
+
case "kysely": {
|
|
1969
|
+
const tsType = colType === "text" ? "string" : colType;
|
|
1970
|
+
return ` ${colName}: ${tsType},`;
|
|
1971
|
+
}
|
|
1972
|
+
case "prisma":
|
|
1973
|
+
default:
|
|
1974
|
+
return ` ${colName} ${colType},`;
|
|
1975
|
+
}
|
|
1976
|
+
}).join(`
|
|
1977
|
+
`);
|
|
1978
|
+
}
|
|
1979
|
+
function renderPrismaBlock(modelName, cols) {
|
|
1980
|
+
const fieldLines = cols.map((c) => {
|
|
1981
|
+
const [name, type = "String"] = c.split(":");
|
|
1982
|
+
return ` ${name.padEnd(16)} ${capitalize(type)}`;
|
|
1983
|
+
}).join(`
|
|
1984
|
+
`);
|
|
1985
|
+
return ` * model ${modelName} {
|
|
1986
|
+
* id Int @id @default(autoincrement())
|
|
1987
|
+
${fieldLines.split(`
|
|
1988
|
+
`).map((l) => ` *${l}`).join(`
|
|
1989
|
+
`)}
|
|
1990
|
+
* createdAt DateTime @default(now())
|
|
1991
|
+
* updatedAt DateTime @updatedAt
|
|
1992
|
+
* }`;
|
|
1993
|
+
}
|
|
1994
|
+
function toCamel(s) {
|
|
1995
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1996
|
+
}
|
|
1997
|
+
function capitalize(s) {
|
|
1998
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1999
|
+
}
|
|
2000
|
+
var make_model_default = makeModelCommand;
|
|
2001
|
+
|
|
2002
|
+
// packages/cli/src/commands/make-module.ts
|
|
2003
|
+
import { resolve as resolve10 } from "path";
|
|
2004
|
+
import {
|
|
2005
|
+
flagBool as flagBool3,
|
|
2006
|
+
logger as logger10,
|
|
2007
|
+
nameVariants as nameVariants7,
|
|
2008
|
+
render as render9,
|
|
2009
|
+
writeFile as writeFile8
|
|
2010
|
+
} from "@nexusts/core/index.js";
|
|
2011
|
+
var makeModuleCommand = {
|
|
2012
|
+
name: "make:module",
|
|
2013
|
+
aliases: ["mm", "make-module"],
|
|
2014
|
+
summary: "Generate a feature module",
|
|
2015
|
+
description: "Generates a @Module() class that wires a controller + service (+ optional repository) under app/modules/.",
|
|
2016
|
+
examples: ["nx make:module User", "nx make:module User --no-controller"],
|
|
2017
|
+
flags: [
|
|
2018
|
+
{
|
|
2019
|
+
name: "no-controller",
|
|
2020
|
+
description: "Skip including the controller in the module"
|
|
2021
|
+
},
|
|
2022
|
+
{ name: "no-service", description: "Skip including the service" },
|
|
2023
|
+
{ name: "no-repo", description: "Skip including the repository" }
|
|
2024
|
+
],
|
|
2025
|
+
async run(ctx) {
|
|
2026
|
+
const name = ctx.positional[0];
|
|
2027
|
+
if (!name) {
|
|
2028
|
+
logger10.error("Usage: nx make:module <Name>");
|
|
2029
|
+
return 1;
|
|
2030
|
+
}
|
|
2031
|
+
const variants = nameVariants7(name);
|
|
2032
|
+
const hasController = !flagBool3(ctx.flags, "no-controller", false);
|
|
2033
|
+
const hasService = !flagBool3(ctx.flags, "no-service", false);
|
|
2034
|
+
const hasRepo = !flagBool3(ctx.flags, "no-repo", false) && ctx.config.orm !== "none";
|
|
2035
|
+
const code = render9(templates.module, {
|
|
2036
|
+
name: variants.pascal,
|
|
2037
|
+
kebab: variants.kebab,
|
|
2038
|
+
controller: `${variants.pascal}Controller`,
|
|
2039
|
+
service: `${variants.pascal}Service`,
|
|
2040
|
+
repository: `${variants.pascal}Repository`,
|
|
2041
|
+
hasService,
|
|
2042
|
+
hasRepo
|
|
2043
|
+
});
|
|
2044
|
+
const out = resolve10(ctx.cwd, ctx.config.paths.modules, `${variants.kebab}.module.ts`);
|
|
2045
|
+
writeFile8(out, code);
|
|
2046
|
+
logger10.success(`created ${out}`);
|
|
2047
|
+
logger10.finger(`add ${variants.pascal}Module to AppModule.imports.`);
|
|
2048
|
+
return 0;
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
var make_module_default = makeModuleCommand;
|
|
2052
|
+
|
|
2053
|
+
// packages/cli/src/commands/make-queue.ts
|
|
2054
|
+
import { resolve as resolve11 } from "path";
|
|
2055
|
+
import {
|
|
2056
|
+
flagBool as flagBool4,
|
|
2057
|
+
logger as logger11,
|
|
2058
|
+
nameVariants as nameVariants8,
|
|
2059
|
+
render as render10,
|
|
2060
|
+
writeFile as writeFile9
|
|
2061
|
+
} from "@nexusts/core/index.js";
|
|
2062
|
+
var WORKER_TEMPLATE = `
|
|
2063
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
2064
|
+
import { QueueService, OnQueueReady } from '@nexusts/queue';
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* {{ name }} worker \u2014 generated by \`nx make:queue {{ name }}\`.
|
|
2068
|
+
*
|
|
2069
|
+
* Registers a handler for the \`{{ name }}\` job name on boot.
|
|
2070
|
+
*/
|
|
2071
|
+
@Injectable()
|
|
2072
|
+
export class {{ name }}Worker {
|
|
2073
|
+
constructor(@Inject(QueueService.TOKEN) private readonly queue: QueueService) {}
|
|
2074
|
+
|
|
2075
|
+
@OnQueueReady()
|
|
2076
|
+
async register(): Promise<void> {
|
|
2077
|
+
await this.queue.process('{{ name }}', async (data, ctx) => {
|
|
2078
|
+
ctx.prefix; // \u2192 "[queue:{{ name }}]"
|
|
2079
|
+
try {
|
|
2080
|
+
await this.handle(data as {{ name }}Data);
|
|
2081
|
+
return { status: 'completed' };
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
return {
|
|
2084
|
+
status: 'failed',
|
|
2085
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
2086
|
+
willRetry: ctx.attempts < 3,
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Replace this body with your actual worker logic.
|
|
2094
|
+
*/
|
|
2095
|
+
async handle(data: {{ name }}Data): Promise<void> {
|
|
2096
|
+
// TODO: implement
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* Typed payload for the \`{{ name }}\` job.
|
|
2102
|
+
*/
|
|
2103
|
+
export interface {{ name }}Data {
|
|
2104
|
+
// TODO: define fields
|
|
2105
|
+
[key: string]: unknown;
|
|
2106
|
+
}
|
|
2107
|
+
`.trimStart();
|
|
2108
|
+
var JOB_HELPER_TEMPLATE = `
|
|
2109
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
2110
|
+
import { QueueService } from '@nexusts/queue';
|
|
2111
|
+
import type { {{ name }}Data } from './{{ kebab }}.worker.js';
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Helper for enqueuing \`{{ name }}\` jobs from controllers / services.
|
|
2115
|
+
* Generated by \`nx make:queue {{ name }}\`.
|
|
2116
|
+
*/
|
|
2117
|
+
@Injectable()
|
|
2118
|
+
export class {{ name }}Job {
|
|
2119
|
+
constructor(@Inject(QueueService.TOKEN) private readonly queue: QueueService) {}
|
|
2120
|
+
|
|
2121
|
+
/** Enqueue a single {{ name }} job. */
|
|
2122
|
+
async enqueue(data: {{ name }}Data, options?: {
|
|
2123
|
+
delaySeconds?: number;
|
|
2124
|
+
attempts?: number;
|
|
2125
|
+
}) {
|
|
2126
|
+
return this.queue.add('{{ name }}', data, options);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/** Enqueue many at once. */
|
|
2130
|
+
async enqueueBatch(items: {{ name }}Data[]) {
|
|
2131
|
+
return this.queue.addBatch(
|
|
2132
|
+
items.map((data) => ({ name: '{{ name }}', data })),
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
`.trimStart();
|
|
2137
|
+
var WIRE_HINT = `
|
|
2138
|
+
import { QueueService } from '@nexusts/queue';
|
|
2139
|
+
import { %%NAME%%Job } from './queue/jobs/%%KEBAB%%.job.js';
|
|
2140
|
+
import { %%NAME%%Worker } from './queue/workers/%%KEBAB%%.worker.js';
|
|
2141
|
+
|
|
2142
|
+
@Module({
|
|
2143
|
+
providers: [%%NAME%%Job, %%NAME%%Worker],
|
|
2144
|
+
})
|
|
2145
|
+
export class QueueBindingsModule {}
|
|
2146
|
+
`.trim();
|
|
2147
|
+
var makeQueueCommand = {
|
|
2148
|
+
name: "make:queue",
|
|
2149
|
+
aliases: ["mq", "make-queue"],
|
|
2150
|
+
summary: "Scaffold a queue worker",
|
|
2151
|
+
description: "Generates a worker class + enqueue helper under app/queue/. The backend is read from nx.config.ts (or --backend).",
|
|
2152
|
+
examples: [
|
|
2153
|
+
"nx make:queue send-welcome-email",
|
|
2154
|
+
"nx make:queue process-image --backend cloudflare",
|
|
2155
|
+
"nx make:queue notify --backend memory"
|
|
2156
|
+
],
|
|
2157
|
+
flags: [
|
|
2158
|
+
{
|
|
2159
|
+
name: "backend",
|
|
2160
|
+
description: "Override backend (bullmq|cloudflare|memory)"
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
name: "no-job",
|
|
2164
|
+
description: "Skip the enqueue helper, only generate the worker"
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
name: "no-worker",
|
|
2168
|
+
description: "Skip the worker class, only generate the enqueue helper"
|
|
2169
|
+
}
|
|
2170
|
+
],
|
|
2171
|
+
async run(ctx) {
|
|
2172
|
+
const name = ctx.positional[0];
|
|
2173
|
+
if (!name) {
|
|
2174
|
+
logger11.error("Usage: nx make:queue <Name>");
|
|
2175
|
+
return 1;
|
|
2176
|
+
}
|
|
2177
|
+
const variants = nameVariants8(name);
|
|
2178
|
+
const backend = ctx.flags["backend"] ?? ctx.config.queue?.backend ?? "memory";
|
|
2179
|
+
if (!["bullmq", "cloudflare", "memory"].includes(backend)) {
|
|
2180
|
+
logger11.error(`Unknown backend: ${backend}. Allowed: bullmq, cloudflare, memory.`);
|
|
2181
|
+
return 1;
|
|
2182
|
+
}
|
|
2183
|
+
logger11.heading(`Scaffolding ${variants.pascal} (backend=${backend})`);
|
|
2184
|
+
if (!flagBool4(ctx.flags, "no-worker", false)) {
|
|
2185
|
+
const code = render10(WORKER_TEMPLATE, {
|
|
2186
|
+
name: variants.pascal,
|
|
2187
|
+
kebab: variants.kebab
|
|
2188
|
+
});
|
|
2189
|
+
const out = resolve11(ctx.cwd, "app/queue/workers", `${variants.kebab}.worker.ts`);
|
|
2190
|
+
if (writeFile9(out, code, { skipIfExists: true })) {
|
|
2191
|
+
logger11.success(`created ${out}`);
|
|
2192
|
+
} else {
|
|
2193
|
+
logger11.warn(`skipped (exists): ${out}`);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
if (!flagBool4(ctx.flags, "no-job", false)) {
|
|
2197
|
+
const code = render10(JOB_HELPER_TEMPLATE, {
|
|
2198
|
+
name: variants.pascal,
|
|
2199
|
+
kebab: variants.kebab
|
|
2200
|
+
});
|
|
2201
|
+
const out = resolve11(ctx.cwd, "app/queue/jobs", `${variants.kebab}.job.ts`);
|
|
2202
|
+
if (writeFile9(out, code, { skipIfExists: true })) {
|
|
2203
|
+
logger11.success(`created ${out}`);
|
|
2204
|
+
} else {
|
|
2205
|
+
logger11.warn(`skipped (exists): ${out}`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
logger11.blank();
|
|
2209
|
+
logger11.heading("Next steps");
|
|
2210
|
+
logger11.info(`1. Add QueueModule.forRoot() to your AppModule.imports:`);
|
|
2211
|
+
logger11.info(` QueueModule.forRoot({ backend: '${backend}', ... })`);
|
|
2212
|
+
logger11.info(`2. Wire the worker + helper as providers:`);
|
|
2213
|
+
logger11.info(render10(WIRE_HINT, { name: variants.pascal, kebab: variants.kebab }).split(`
|
|
2214
|
+
`).map((l) => " " + l).join(`
|
|
2215
|
+
`));
|
|
2216
|
+
logger11.info(`3. Replace the TODO in ${variants.pascal}Worker.handle() with your logic.`);
|
|
2217
|
+
logger11.blank();
|
|
2218
|
+
return 0;
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
var make_queue_default = makeQueueCommand;
|
|
2222
|
+
|
|
2223
|
+
// packages/cli/src/commands/make-schedule.ts
|
|
2224
|
+
import { resolve as resolve12 } from "path";
|
|
2225
|
+
import { logger as logger12, nameVariants as nameVariants9, render as render11, writeFile as writeFile10 } from "@nexusts/core/index.js";
|
|
2226
|
+
var TASK_TEMPLATE = `
|
|
2227
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
2228
|
+
import { Cron, Interval, Timeout, ScheduleService } from '@nexusts/schedule';
|
|
2229
|
+
|
|
2230
|
+
/**
|
|
2231
|
+
* {{ name }} task \u2014 generated by \`nx make:schedule {{ name }}\`.
|
|
2232
|
+
*
|
|
2233
|
+
* Mark methods with \`@Cron\`, \`@Interval\`, or \`@Timeout\` and register
|
|
2234
|
+
* them at boot via \`scanForSchedulers(this, scheduleService)\`.
|
|
2235
|
+
*/
|
|
2236
|
+
@Injectable()
|
|
2237
|
+
export class {{ name }}Task {
|
|
2238
|
+
constructor(@Inject(ScheduleService.TOKEN) private readonly schedule: ScheduleService) {}
|
|
2239
|
+
|
|
2240
|
+
// TODO: add @Cron, @Interval, or @Timeout handlers below.
|
|
2241
|
+
|
|
2242
|
+
// @Cron('0 * * * *') // every hour
|
|
2243
|
+
// async hourly() {
|
|
2244
|
+
// this.schedule; // unused \u2014 remove if you don't need it
|
|
2245
|
+
// }
|
|
2246
|
+
|
|
2247
|
+
// @Interval(60_000) // every minute
|
|
2248
|
+
// async tick() { /* ... */ }
|
|
2249
|
+
|
|
2250
|
+
// @Timeout(5_000) // 5s after boot
|
|
2251
|
+
// async startup() { /* ... */ }
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
import { scanForSchedulers } from '@nexusts/schedule';
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* Bootstrap helper \u2014 call from main.ts to register every @Cron /
|
|
2258
|
+
* @Interval / @Timeout handler on this task class.
|
|
2259
|
+
*/
|
|
2260
|
+
export async function register{{ name }}(task: {{ name }}Task, schedule: ScheduleService) {
|
|
2261
|
+
return scanForSchedulers(task, schedule);
|
|
2262
|
+
}
|
|
2263
|
+
`.trimStart();
|
|
2264
|
+
var makeScheduleCommand = {
|
|
2265
|
+
name: "make:schedule",
|
|
2266
|
+
aliases: ["msk", "make-schedule"],
|
|
2267
|
+
summary: "Scaffold a scheduled task class",
|
|
2268
|
+
description: "Generates an @Injectable task class with example @Cron / @Interval / @Timeout handlers under app/schedule/tasks/.",
|
|
2269
|
+
examples: ["nx make:schedule HourlyCleanup", "nx make:schedule DailyDigest"],
|
|
2270
|
+
async run(ctx) {
|
|
2271
|
+
const name = ctx.positional[0];
|
|
2272
|
+
if (!name) {
|
|
2273
|
+
logger12.error("Usage: nx make:schedule <Name>");
|
|
2274
|
+
return 1;
|
|
2275
|
+
}
|
|
2276
|
+
const variants = nameVariants9(name);
|
|
2277
|
+
const code = render11(TASK_TEMPLATE, {
|
|
2278
|
+
name: variants.pascal,
|
|
2279
|
+
kebab: variants.kebab
|
|
2280
|
+
});
|
|
2281
|
+
const out = resolve12(ctx.cwd, "app/schedule/tasks", `${variants.kebab}.task.ts`);
|
|
2282
|
+
if (writeFile10(out, code, { skipIfExists: true })) {
|
|
2283
|
+
logger12.success(`created ${out}`);
|
|
2284
|
+
} else {
|
|
2285
|
+
logger12.warn(`skipped (exists): ${out}`);
|
|
2286
|
+
}
|
|
2287
|
+
logger12.blank();
|
|
2288
|
+
logger12.heading("Next steps");
|
|
2289
|
+
logger12.info("1. Add @Cron / @Interval / @Timeout handlers to the class.");
|
|
2290
|
+
logger12.info(`2. Import + register at boot:`);
|
|
2291
|
+
logger12.info(` scanForSchedulers(task, schedule)`);
|
|
2292
|
+
logger12.info(`3. Don't forget to call \`schedule.start()\` to begin the tick.`);
|
|
2293
|
+
logger12.blank();
|
|
2294
|
+
return 0;
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
var make_schedule_default = makeScheduleCommand;
|
|
2298
|
+
|
|
2299
|
+
// packages/cli/src/commands/make-service.ts
|
|
2300
|
+
import { resolve as resolve13 } from "path";
|
|
2301
|
+
import { logger as logger13, nameVariants as nameVariants10, render as render12, writeFile as writeFile11 } from "@nexusts/core/index.js";
|
|
2302
|
+
var makeServiceCommand = {
|
|
2303
|
+
name: "make:service",
|
|
2304
|
+
aliases: ["ms", "make-service"],
|
|
2305
|
+
summary: "Generate a service class",
|
|
2306
|
+
description: "Generates an @Injectable() service under app/services/. If the project's ORM is configured, the service constructor takes a repository.",
|
|
2307
|
+
examples: ["nx make:service User", "nx make:service Order --no-repo"],
|
|
2308
|
+
flags: [
|
|
2309
|
+
{
|
|
2310
|
+
name: "no-repo",
|
|
2311
|
+
description: "Skip injecting a repository (no ORM dependency)"
|
|
2312
|
+
}
|
|
2313
|
+
],
|
|
2314
|
+
async run(ctx) {
|
|
2315
|
+
const name = ctx.positional[0];
|
|
2316
|
+
if (!name) {
|
|
2317
|
+
logger13.error("Usage: nx make:service <Name>");
|
|
2318
|
+
return 1;
|
|
2319
|
+
}
|
|
2320
|
+
const variants = nameVariants10(name);
|
|
2321
|
+
const hasRepo = ctx.flags["no-repo"] !== true && ctx.config.orm !== "none";
|
|
2322
|
+
const repository = `${variants.pascal}Repository`;
|
|
2323
|
+
const repositoryCamel = variants.camel + "Repository";
|
|
2324
|
+
const code = render12(templates.service, {
|
|
2325
|
+
name: variants.pascal,
|
|
2326
|
+
camel: variants.camel,
|
|
2327
|
+
kebab: variants.kebab,
|
|
2328
|
+
hasRepo,
|
|
2329
|
+
repository,
|
|
2330
|
+
repositoryCamel
|
|
2331
|
+
});
|
|
2332
|
+
const out = resolve13(ctx.cwd, ctx.config.paths.services, `${variants.kebab}.service.ts`);
|
|
2333
|
+
writeFile11(out, code);
|
|
2334
|
+
logger13.success(`created ${out}`);
|
|
2335
|
+
return 0;
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
var make_service_default = makeServiceCommand;
|
|
2339
|
+
|
|
2340
|
+
// packages/cli/src/commands/make-session.ts
|
|
2341
|
+
import { resolve as resolve14 } from "path";
|
|
2342
|
+
import { logger as logger14, nameVariants as nameVariants11, render as render13, writeFile as writeFile12 } from "@nexusts/core/index.js";
|
|
2343
|
+
var SESSION_TEMPLATE = `
|
|
2344
|
+
import { Inject, Injectable } from '@nexusts/core';
|
|
2345
|
+
import { SessionService } from '@nexusts/session';
|
|
2346
|
+
import type { SessionRecord } from '@nexusts/session';
|
|
2347
|
+
|
|
2348
|
+
/**
|
|
2349
|
+
* {{ name }} session helper \u2014 generated by \`nx make:session {{ name }}\`.
|
|
2350
|
+
*
|
|
2351
|
+
* Wraps SessionService with typed accessors for the {{ name }} session's
|
|
2352
|
+
* data. Use from controllers / services.
|
|
2353
|
+
*/
|
|
2354
|
+
@Injectable()
|
|
2355
|
+
export class {{ name }}Session {
|
|
2356
|
+
constructor(@Inject(SessionService.TOKEN) private readonly sessions: SessionService) {}
|
|
2357
|
+
|
|
2358
|
+
/** Read the current session record (or null). */
|
|
2359
|
+
async getCurrent(sessionId: string | null | undefined) {
|
|
2360
|
+
if (!sessionId) return null;
|
|
2361
|
+
return this.sessions.read(sessionId);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/** Patch the {{ name }} session's data. */
|
|
2365
|
+
async update(sessionId: string, patch: {{ name }}DataPatch) {
|
|
2366
|
+
return this.sessions.update(sessionId, { dataPatch: patch });
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
/** Destroy the session (logout-everywhere equivalent). */
|
|
2370
|
+
async destroy(sessionId: string) {
|
|
2371
|
+
return this.sessions.destroy(sessionId, 'logout');
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/**
|
|
2376
|
+
* Typed payload for the {{ name }} session.
|
|
2377
|
+
*
|
|
2378
|
+
* TODO: define the fields below to match your feature.
|
|
2379
|
+
*/
|
|
2380
|
+
export interface {{ name }}Data {
|
|
2381
|
+
// userId?: string;
|
|
2382
|
+
// createdAt?: string;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
export type {{ name }}DataPatch = Partial<{{ name }}Data>;
|
|
2386
|
+
`.trimStart();
|
|
2387
|
+
var makeSessionCommand = {
|
|
2388
|
+
name: "make:session",
|
|
2389
|
+
aliases: ["msess", "make-session"],
|
|
2390
|
+
summary: "Scaffold a session helper class",
|
|
2391
|
+
description: "Generates an @Injectable session helper under app/session/services/.",
|
|
2392
|
+
examples: ["nx make:session Cart", "nx make:session Flash"],
|
|
2393
|
+
async run(ctx) {
|
|
2394
|
+
const name = ctx.positional[0];
|
|
2395
|
+
if (!name) {
|
|
2396
|
+
logger14.error("Usage: nx make:session <Name>");
|
|
2397
|
+
return 1;
|
|
2398
|
+
}
|
|
2399
|
+
const variants = nameVariants11(name);
|
|
2400
|
+
const code = render13(SESSION_TEMPLATE, {
|
|
2401
|
+
name: variants.pascal,
|
|
2402
|
+
kebab: variants.kebab
|
|
2403
|
+
});
|
|
2404
|
+
const out = resolve14(ctx.cwd, "app/session/services", `${variants.kebab}.session.ts`);
|
|
2405
|
+
if (writeFile12(out, code, { skipIfExists: true })) {
|
|
2406
|
+
logger14.success(`created ${out}`);
|
|
2407
|
+
} else {
|
|
2408
|
+
logger14.warn(`skipped (exists): ${out}`);
|
|
2409
|
+
}
|
|
2410
|
+
logger14.blank();
|
|
2411
|
+
logger14.heading("Next steps");
|
|
2412
|
+
logger14.info("1. Define the typed payload (replace the TODO interface).");
|
|
2413
|
+
logger14.info(`2. Inject {{name}}Session in your controllers / services.`);
|
|
2414
|
+
logger14.info("3. Bind it in the module's providers array.");
|
|
2415
|
+
logger14.blank();
|
|
2416
|
+
return 0;
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
var make_session_default = makeSessionCommand;
|
|
2420
|
+
|
|
2421
|
+
// packages/cli/src/commands/make-validator.ts
|
|
2422
|
+
import { resolve as resolve15 } from "path";
|
|
2423
|
+
import { logger as logger15, nameVariants as nameVariants12, render as render14, writeFile as writeFile13 } from "@nexusts/core/index.js";
|
|
2424
|
+
var makeValidatorCommand = {
|
|
2425
|
+
name: "make:validator",
|
|
2426
|
+
aliases: ["mv", "make-validator"],
|
|
2427
|
+
summary: "Generate a Zod validation schema",
|
|
2428
|
+
description: "Generates a Zod schema and inferred type under app/dto/.",
|
|
2429
|
+
examples: ["nx make:validator User", "nx make:validator CreateOrder"],
|
|
2430
|
+
async run(ctx) {
|
|
2431
|
+
const name = ctx.positional[0];
|
|
2432
|
+
if (!name) {
|
|
2433
|
+
logger15.error("Usage: nx make:validator <Name>");
|
|
2434
|
+
return 1;
|
|
2435
|
+
}
|
|
2436
|
+
const variants = nameVariants12(name);
|
|
2437
|
+
const code = render14(templates.validator, {
|
|
2438
|
+
name: variants.pascal
|
|
2439
|
+
});
|
|
2440
|
+
const out = resolve15(ctx.cwd, ctx.config.paths.dto, `${variants.kebab}.dto.ts`);
|
|
2441
|
+
writeFile13(out, code);
|
|
2442
|
+
logger15.success(`created ${out}`);
|
|
2443
|
+
return 0;
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
var make_validator_default = makeValidatorCommand;
|
|
2447
|
+
|
|
2448
|
+
// packages/cli/src/commands/db-migrate.ts
|
|
2449
|
+
import { spawn } from "child_process";
|
|
2450
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2451
|
+
import { resolve as resolve16 } from "path";
|
|
2452
|
+
import { logger as logger16 } from "@nexusts/core/index.js";
|
|
2453
|
+
var dbMigrateCommand = {
|
|
2454
|
+
name: "db:migrate",
|
|
2455
|
+
aliases: ["db:m", "migrate"],
|
|
2456
|
+
summary: "Apply pending database migrations",
|
|
2457
|
+
description: "Runs the Drizzle migrator against the configured migrations folder. Use --status to inspect, --generate to scaffold a new migration via drizzle-kit. See also `nx db:seed` for fixture data.",
|
|
2458
|
+
examples: [
|
|
2459
|
+
"nx db:migrate",
|
|
2460
|
+
"nx db:migrate --status",
|
|
2461
|
+
"nx db:migrate --generate 'add_email_to_users'",
|
|
2462
|
+
"nx db:migrate --folder ./drizzle"
|
|
2463
|
+
],
|
|
2464
|
+
flags: [
|
|
2465
|
+
{
|
|
2466
|
+
name: "status",
|
|
2467
|
+
description: "List applied migrations and exit (no apply)."
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
name: "generate",
|
|
2471
|
+
description: "Run `drizzle-kit generate` with the given migration name."
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
name: "folder",
|
|
2475
|
+
description: "Override migrations folder (default: from nx.config.ts)."
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
name: "dialect",
|
|
2479
|
+
description: "Drizzle dialect (postgres|mysql|sqlite|bun-sqlite|d1). Default: bun-sqlite."
|
|
2480
|
+
},
|
|
2481
|
+
{
|
|
2482
|
+
name: "config",
|
|
2483
|
+
description: "Path to drizzle.config.ts. Default: ./drizzle.config.ts."
|
|
2484
|
+
}
|
|
2485
|
+
],
|
|
2486
|
+
async run(ctx) {
|
|
2487
|
+
const folder = ctx.flags["folder"] ?? resolve16(ctx.cwd, ctx.config.paths.migrations);
|
|
2488
|
+
const dialect = ctx.flags["dialect"] ?? ctx.config.dialect ?? "bun-sqlite";
|
|
2489
|
+
const configPath = ctx.flags["config"] ?? resolve16(ctx.cwd, "drizzle.config.ts");
|
|
2490
|
+
const wantStatus = Boolean(ctx.flags["status"]);
|
|
2491
|
+
const generateName = ctx.flags["generate"];
|
|
2492
|
+
if (generateName) {
|
|
2493
|
+
return runDrizzleKit(ctx.cwd, [
|
|
2494
|
+
"generate",
|
|
2495
|
+
...existsSync2(configPath) ? [`--config=${configPath}`] : [],
|
|
2496
|
+
"--name",
|
|
2497
|
+
generateName
|
|
2498
|
+
]);
|
|
2499
|
+
}
|
|
2500
|
+
if (wantStatus) {
|
|
2501
|
+
return await runStatus(ctx.cwd, folder, dialect, ctx.config.database?.url ?? "");
|
|
2502
|
+
}
|
|
2503
|
+
return runDrizzleKit(ctx.cwd, [
|
|
2504
|
+
"migrate",
|
|
2505
|
+
...existsSync2(configPath) ? [`--config=${configPath}`] : []
|
|
2506
|
+
]);
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
function runDrizzleKit(cwd, args) {
|
|
2510
|
+
return new Promise((resolveP) => {
|
|
2511
|
+
const cmd = "bunx";
|
|
2512
|
+
logger16.info(`$ ${cmd} drizzle-kit ${args.join(" ")}`);
|
|
2513
|
+
const child = spawn(cmd, ["drizzle-kit", ...args], {
|
|
2514
|
+
cwd,
|
|
2515
|
+
stdio: "inherit",
|
|
2516
|
+
shell: process.platform === "win32"
|
|
2517
|
+
});
|
|
2518
|
+
child.on("exit", (code) => resolveP(code ?? 0));
|
|
2519
|
+
child.on("error", (err) => {
|
|
2520
|
+
logger16.error(`failed to spawn drizzle-kit: ${err.message}`);
|
|
2521
|
+
resolveP(1);
|
|
2522
|
+
});
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
async function runStatus(cwd, folder, dialect, configUrl = "") {
|
|
2526
|
+
if (!existsSync2(folder)) {
|
|
2527
|
+
logger16.warn(`migrations folder not found: ${folder}`);
|
|
2528
|
+
return 0;
|
|
2529
|
+
}
|
|
2530
|
+
const url = readEnvUrl(dialect) ?? configUrl;
|
|
2531
|
+
if (!url) {
|
|
2532
|
+
logger16.error(`could not read ${dialect} URL from environment. Set DATABASE_URL or NEXUS_DB_URL.`);
|
|
2533
|
+
return 1;
|
|
2534
|
+
}
|
|
2535
|
+
const script = `
|
|
2536
|
+
import 'reflect-metadata';
|
|
2537
|
+
import { DrizzleService } from '@nexusts/drizzle';
|
|
2538
|
+
|
|
2539
|
+
const url = ${JSON.stringify(url)};
|
|
2540
|
+
const dialect = ${JSON.stringify(dialect)};
|
|
2541
|
+
const folder = ${JSON.stringify(folder)};
|
|
2542
|
+
|
|
2543
|
+
const cfg = { dialect, connection: { url }, schema: dialect === 'postgres' ? 'public' : undefined };
|
|
2544
|
+
const svc = new DrizzleService(cfg);
|
|
2545
|
+
await svc.open();
|
|
2546
|
+
const applied = await svc.appliedMigrations();
|
|
2547
|
+
console.log(JSON.stringify({ total: applied.length, applied }, null, 2));
|
|
2548
|
+
await svc.close();
|
|
2549
|
+
`;
|
|
2550
|
+
const tmpFile = resolve16(cwd, ".nx-migrate-status.mjs");
|
|
2551
|
+
await import("fs/promises").then((m) => m.writeFile(tmpFile, script, "utf-8"));
|
|
2552
|
+
try {
|
|
2553
|
+
const code = await new Promise((resP) => {
|
|
2554
|
+
const child = spawn("bun", [tmpFile], {
|
|
2555
|
+
cwd,
|
|
2556
|
+
stdio: "inherit",
|
|
2557
|
+
shell: process.platform === "win32"
|
|
2558
|
+
});
|
|
2559
|
+
child.on("exit", (c) => resP(c ?? 0));
|
|
2560
|
+
child.on("error", () => resP(1));
|
|
2561
|
+
});
|
|
2562
|
+
return code;
|
|
2563
|
+
} finally {
|
|
2564
|
+
await import("fs/promises").then((m) => m.unlink(tmpFile).catch(() => {}));
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
function readEnvUrl(dialect) {
|
|
2568
|
+
const url = process.env["DATABASE_URL"] ?? process.env["NEXUS_DB_URL"] ?? (dialect === "postgres" ? process.env["POSTGRES_URL"] : dialect === "mysql" ? process.env["MYSQL_URL"] : dialect.includes("sqlite") ? process.env["SQLITE_FILENAME"] : null);
|
|
2569
|
+
return url ?? null;
|
|
2570
|
+
}
|
|
2571
|
+
var db_migrate_default = dbMigrateCommand;
|
|
2572
|
+
|
|
2573
|
+
// packages/cli/src/commands/db-generate.ts
|
|
2574
|
+
import { resolve as resolve17 } from "path";
|
|
2575
|
+
import { logger as logger17 } from "@nexusts/core/index.js";
|
|
2576
|
+
var dbGenerateCommand = {
|
|
2577
|
+
name: "db:generate",
|
|
2578
|
+
aliases: ["db:g", "db-generate", "generate-migration"],
|
|
2579
|
+
summary: "Generate a new migration from schema changes",
|
|
2580
|
+
description: "Generates a new migration file by running drizzle-kit generate with the project's config. " + "If no name is given, drizzle-kit auto-generates one. " + "Run after editing your schema files, then apply with `nx db:migrate`.",
|
|
2581
|
+
examples: [
|
|
2582
|
+
"nx db:generate",
|
|
2583
|
+
"nx db:generate add_users_table",
|
|
2584
|
+
"nx db:generate add_posts --dialect postgres"
|
|
2585
|
+
],
|
|
2586
|
+
flags: [
|
|
2587
|
+
{
|
|
2588
|
+
name: "dialect",
|
|
2589
|
+
description: "Database dialect (bun-sqlite|postgres|mysql|sqlite). Reads from nx.config.ts by default."
|
|
2590
|
+
},
|
|
2591
|
+
{
|
|
2592
|
+
name: "sql",
|
|
2593
|
+
description: "Generate a raw SQL file instead of using drizzle-kit"
|
|
2594
|
+
}
|
|
2595
|
+
],
|
|
2596
|
+
async run(ctx) {
|
|
2597
|
+
const name = ctx.positional[0];
|
|
2598
|
+
const dialect = ctx.flags["dialect"] ?? ctx.config.dialect ?? "bun-sqlite";
|
|
2599
|
+
const isSql = ctx.flags["sql"] === true;
|
|
2600
|
+
if (isSql) {
|
|
2601
|
+
if (!name) {
|
|
2602
|
+
logger17.error("Usage: nx db:generate <name> --sql");
|
|
2603
|
+
return 1;
|
|
2604
|
+
}
|
|
2605
|
+
logger17.info(`Generating raw SQL migration: ${name} (dialect=${dialect})`);
|
|
2606
|
+
return runSqlTemplate(ctx.cwd, name, dialect);
|
|
2607
|
+
}
|
|
2608
|
+
const configPath = resolve17(ctx.cwd, "drizzle.config.ts");
|
|
2609
|
+
const args = ["generate", "--config", configPath];
|
|
2610
|
+
if (name)
|
|
2611
|
+
args.push("--name", name);
|
|
2612
|
+
logger17.info(`Generating migration: ${name} (dialect=${dialect})`);
|
|
2613
|
+
return runDrizzleKit(ctx.cwd, args);
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
async function runSqlTemplate(cwd, name, dialect) {
|
|
2617
|
+
const { mkdirSync: mkdirSync3, writeFileSync: writeFileSync2 } = await import("fs");
|
|
2618
|
+
const { join } = await import("path");
|
|
2619
|
+
const migrationsDir = join(cwd, "app", "database", "migrations");
|
|
2620
|
+
mkdirSync3(migrationsDir, { recursive: true });
|
|
2621
|
+
const timestamp = Date.now();
|
|
2622
|
+
const filename = `${timestamp}_${name.replace(/[^a-z0-9_]+/g, "_")}.sql`;
|
|
2623
|
+
const filepath = join(migrationsDir, filename);
|
|
2624
|
+
const header = dialect === "postgres" || dialect === "mysql" ? `-- Migration: ${name}
|
|
2625
|
+
-- Dialect: ${dialect}
|
|
2626
|
+
-- Generated: ${new Date().toISOString()}
|
|
2627
|
+
|
|
2628
|
+
` : `-- Migration: ${name}
|
|
2629
|
+
-- Dialect: ${dialect} (SQLite)
|
|
2630
|
+
-- Generated: ${new Date().toISOString()}
|
|
2631
|
+
|
|
2632
|
+
`;
|
|
2633
|
+
writeFileSync2(filepath, header);
|
|
2634
|
+
logger17.success(`created ${filepath}`);
|
|
2635
|
+
logger17.info("Edit the SQL file, then run `nx db:migrate` to apply it.");
|
|
2636
|
+
return 0;
|
|
2637
|
+
}
|
|
2638
|
+
var db_generate_default = dbGenerateCommand;
|
|
2639
|
+
|
|
2640
|
+
// packages/cli/src/commands/db-seed.ts
|
|
2641
|
+
import { spawn as spawn2 } from "child_process";
|
|
2642
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2643
|
+
import { mkdir, readdir, writeFile as writeFile14, unlink } from "fs/promises";
|
|
2644
|
+
import { resolve as resolve18 } from "path";
|
|
2645
|
+
import { logger as logger18 } from "@nexusts/core/index.js";
|
|
2646
|
+
var SEED_TEMPLATE = `/**
|
|
2647
|
+
* Seed: {name}
|
|
2648
|
+
*
|
|
2649
|
+
* Run with: nx db:seed
|
|
2650
|
+
*
|
|
2651
|
+
* The default export receives a \`SeedContext\` with:
|
|
2652
|
+
* - \`ctx.db\` : the active DrizzleService
|
|
2653
|
+
* - \`ctx.logger\` : the framework logger
|
|
2654
|
+
* - \`ctx.dialect\` : the active Drizzle dialect
|
|
2655
|
+
* - \`ctx.truncate(table)\` : helper to clear a table
|
|
2656
|
+
*
|
|
2657
|
+
* Use ctx.db.insert(table).values([...]) etc. for inserts.
|
|
2658
|
+
*/
|
|
2659
|
+
|
|
2660
|
+
import type { SeedContext } from "@nexusts/cli";
|
|
2661
|
+
|
|
2662
|
+
export default async function seed(ctx: SeedContext): Promise<void> {
|
|
2663
|
+
ctx.logger.info("Running seed: {name}");
|
|
2664
|
+
// Example:
|
|
2665
|
+
// await ctx.db.insert(usersTable).values([
|
|
2666
|
+
// { email: "alice@example.com", name: "Alice" },
|
|
2667
|
+
// { email: "bob@example.com", name: "Bob" },
|
|
2668
|
+
// ]);
|
|
2669
|
+
}
|
|
2670
|
+
`;
|
|
2671
|
+
var dbSeedCommand = {
|
|
2672
|
+
name: "db:seed",
|
|
2673
|
+
aliases: ["db:s", "seed"],
|
|
2674
|
+
summary: "Run database seed scripts",
|
|
2675
|
+
description: "Loads and runs every seed file in the configured seeds folder in alphabetical order. Use --file to run a single seed, --create to scaffold a new one, --reset to truncate first (DESTRUCTIVE).",
|
|
2676
|
+
examples: [
|
|
2677
|
+
"nx db:seed",
|
|
2678
|
+
"nx db:seed --file 01_users",
|
|
2679
|
+
"nx db:seed --create users",
|
|
2680
|
+
"nx db:seed --reset",
|
|
2681
|
+
"nx db:seed --folder ./seeds"
|
|
2682
|
+
],
|
|
2683
|
+
flags: [
|
|
2684
|
+
{
|
|
2685
|
+
name: "file",
|
|
2686
|
+
description: "Run a single seed file by name (without .ts extension, fuzzy match)."
|
|
2687
|
+
},
|
|
2688
|
+
{
|
|
2689
|
+
name: "create",
|
|
2690
|
+
description: "Scaffold a new seed file with a default template. Provide a name (e.g. `users`)."
|
|
2691
|
+
},
|
|
2692
|
+
{
|
|
2693
|
+
name: "reset",
|
|
2694
|
+
description: "DESTRUCTIVE: Truncate every table in the schema before running seeds."
|
|
2695
|
+
},
|
|
2696
|
+
{
|
|
2697
|
+
name: "folder",
|
|
2698
|
+
description: "Override seeds folder. Default: ./db/seeds (or nx.config.ts paths.seeds)."
|
|
2699
|
+
},
|
|
2700
|
+
{
|
|
2701
|
+
name: "dialect",
|
|
2702
|
+
description: "Drizzle dialect (postgres|mysql|sqlite|bun-sqlite|d1). Default: from nx.config.ts or bun-sqlite."
|
|
2703
|
+
}
|
|
2704
|
+
],
|
|
2705
|
+
async run(ctx) {
|
|
2706
|
+
const folder = resolve18(ctx.cwd, ctx.flags["folder"] ?? ctx.config.paths?.seeds ?? "db/seeds");
|
|
2707
|
+
const dialect = ctx.flags["dialect"] ?? ctx.config.dialect ?? "bun-sqlite";
|
|
2708
|
+
const createName = ctx.flags["create"];
|
|
2709
|
+
const fileName = ctx.flags["file"];
|
|
2710
|
+
const reset = Boolean(ctx.flags["reset"]);
|
|
2711
|
+
if (createName) {
|
|
2712
|
+
return await createSeedFile(folder, createName);
|
|
2713
|
+
}
|
|
2714
|
+
if (!existsSync3(folder)) {
|
|
2715
|
+
logger18.info(`creating empty seeds folder at ${folder}`);
|
|
2716
|
+
await mkdir(folder, { recursive: true });
|
|
2717
|
+
await writeFile14(resolve18(folder, "_README.ts"), `// Seed files go here. Run with: nx db:seed
|
|
2718
|
+
`, "utf-8");
|
|
2719
|
+
return 0;
|
|
2720
|
+
}
|
|
2721
|
+
const files = await collectSeedFiles(folder);
|
|
2722
|
+
if (fileName) {
|
|
2723
|
+
const matched = files.filter((f) => f.toLowerCase().includes(fileName.toLowerCase()));
|
|
2724
|
+
if (matched.length === 0) {
|
|
2725
|
+
logger18.error(`no seed file matching "${fileName}" in ${folder}`);
|
|
2726
|
+
return 1;
|
|
2727
|
+
}
|
|
2728
|
+
} else if (files.length === 0) {
|
|
2729
|
+
logger18.warn(`no seed files found in ${folder}`);
|
|
2730
|
+
return 0;
|
|
2731
|
+
}
|
|
2732
|
+
const target = fileName ? files.filter((f) => f.toLowerCase().includes(fileName.toLowerCase())) : files;
|
|
2733
|
+
const url = readEnvUrl2(dialect);
|
|
2734
|
+
if (!url) {
|
|
2735
|
+
logger18.error(`could not read ${dialect} URL from environment. Set DATABASE_URL or NEXUS_DB_URL.`);
|
|
2736
|
+
return 1;
|
|
2737
|
+
}
|
|
2738
|
+
if (reset) {
|
|
2739
|
+
logger18.warn("--reset is set: truncating every table in the schema before running seeds.");
|
|
2740
|
+
}
|
|
2741
|
+
const seedImports = target.map((f, i) => `import seed_${i} from ${JSON.stringify(resolve18(folder, f))};`).join(`
|
|
2742
|
+
`);
|
|
2743
|
+
const seedCalls = target.map((_, i) => ` await seed_${i}({ db, logger, dialect, truncate: (t) => db.truncate(t) });`).join(`
|
|
2744
|
+
`);
|
|
2745
|
+
const script = `
|
|
2746
|
+
import 'reflect-metadata';
|
|
2747
|
+
import { DrizzleService } from '${relativeImport(ctx.cwd, "src/drizzle/index.js")}';
|
|
2748
|
+
import { logger as frameworkLogger } from '${relativeImport(ctx.cwd, "src/logger/index.js")}';
|
|
2749
|
+
|
|
2750
|
+
const url = ${JSON.stringify(url)};
|
|
2751
|
+
const dialect = ${JSON.stringify(dialect)};
|
|
2752
|
+
const reset = ${JSON.stringify(reset)};
|
|
2753
|
+
|
|
2754
|
+
const cfg = { dialect, connection: { url }, schema: dialect === 'postgres' ? 'public' : undefined };
|
|
2755
|
+
const db = new DrizzleService(cfg);
|
|
2756
|
+
await db.open();
|
|
2757
|
+
const logger = frameworkLogger;
|
|
2758
|
+
|
|
2759
|
+
if (reset) {
|
|
2760
|
+
const tables = await db.allTables();
|
|
2761
|
+
for (const t of tables) {
|
|
2762
|
+
await db.truncate(t);
|
|
2763
|
+
}
|
|
2764
|
+
logger.info(\`Truncated \${tables.length} table(s)\`);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
${seedImports}
|
|
2768
|
+
|
|
2769
|
+
${seedCalls}
|
|
2770
|
+
|
|
2771
|
+
await db.close();
|
|
2772
|
+
logger.info(\`Seeds complete (\${${target.length}} file(s))\`);
|
|
2773
|
+
`;
|
|
2774
|
+
const tmpFile = resolve18(ctx.cwd, ".nx-db-seed.mjs");
|
|
2775
|
+
await writeFile14(tmpFile, script, "utf-8");
|
|
2776
|
+
try {
|
|
2777
|
+
const code = await new Promise((resP) => {
|
|
2778
|
+
const child = spawn2("bun", [tmpFile], {
|
|
2779
|
+
cwd: ctx.cwd,
|
|
2780
|
+
stdio: "inherit",
|
|
2781
|
+
shell: process.platform === "win32"
|
|
2782
|
+
});
|
|
2783
|
+
child.on("exit", (c) => resP(c ?? 0));
|
|
2784
|
+
child.on("error", () => resP(1));
|
|
2785
|
+
});
|
|
2786
|
+
return code;
|
|
2787
|
+
} finally {
|
|
2788
|
+
await unlink(tmpFile).catch(() => {});
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
};
|
|
2792
|
+
async function collectSeedFiles(folder) {
|
|
2793
|
+
const all = await readdir(folder, { withFileTypes: true });
|
|
2794
|
+
const out = [];
|
|
2795
|
+
for (const e of all) {
|
|
2796
|
+
if (!e.isFile())
|
|
2797
|
+
continue;
|
|
2798
|
+
if (e.name.startsWith("_"))
|
|
2799
|
+
continue;
|
|
2800
|
+
if (!/\.(ts|js|mjs|cjs)$/.test(e.name))
|
|
2801
|
+
continue;
|
|
2802
|
+
out.push(e.name);
|
|
2803
|
+
}
|
|
2804
|
+
out.sort();
|
|
2805
|
+
return out;
|
|
2806
|
+
}
|
|
2807
|
+
async function createSeedFile(folder, name) {
|
|
2808
|
+
if (!/^[a-z0-9_-]+$/i.test(name)) {
|
|
2809
|
+
logger18.error(`invalid seed name "${name}" \u2014 use letters, numbers, dash, underscore.`);
|
|
2810
|
+
return 1;
|
|
2811
|
+
}
|
|
2812
|
+
if (!existsSync3(folder))
|
|
2813
|
+
await mkdir(folder, { recursive: true });
|
|
2814
|
+
let candidate = `${name}.ts`;
|
|
2815
|
+
let i = 1;
|
|
2816
|
+
while (existsSync3(resolve18(folder, candidate))) {
|
|
2817
|
+
candidate = `${name}_${i}.ts`;
|
|
2818
|
+
i++;
|
|
2819
|
+
}
|
|
2820
|
+
const path = resolve18(folder, candidate);
|
|
2821
|
+
const body = SEED_TEMPLATE.replace(/\{name\}/g, name);
|
|
2822
|
+
await writeFile14(path, body, "utf-8");
|
|
2823
|
+
logger18.info(`created ${path}`);
|
|
2824
|
+
return 0;
|
|
2825
|
+
}
|
|
2826
|
+
function readEnvUrl2(dialect) {
|
|
2827
|
+
const url = process.env["DATABASE_URL"] ?? process.env["NEXUS_DB_URL"] ?? (dialect === "postgres" ? process.env["POSTGRES_URL"] : dialect === "mysql" ? process.env["MYSQL_URL"] : dialect.includes("sqlite") ? process.env["SQLITE_FILENAME"] : null);
|
|
2828
|
+
return url ?? null;
|
|
2829
|
+
}
|
|
2830
|
+
function relativeImport(cwd, target) {
|
|
2831
|
+
const abs = resolve18(cwd, target);
|
|
2832
|
+
let rel = abs;
|
|
2833
|
+
if (rel.startsWith(cwd))
|
|
2834
|
+
rel = rel.slice(cwd.length);
|
|
2835
|
+
if (rel.startsWith("/"))
|
|
2836
|
+
rel = rel.slice(1);
|
|
2837
|
+
if (!rel.startsWith("."))
|
|
2838
|
+
rel = "./" + rel;
|
|
2839
|
+
return rel;
|
|
2840
|
+
}
|
|
2841
|
+
var db_seed_default = dbSeedCommand;
|
|
2842
|
+
|
|
2843
|
+
// packages/cli/src/commands/new.ts
|
|
2844
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2845
|
+
import { resolve as resolve19 } from "path";
|
|
2846
|
+
import { flagBool as flagBool5, logger as logger19, render as render15, select as select2 } from "@nexusts/core/index.js";
|
|
2847
|
+
var newCommand = {
|
|
2848
|
+
name: "new",
|
|
2849
|
+
aliases: ["n"],
|
|
2850
|
+
summary: "Create a new Nexus project",
|
|
2851
|
+
description: "Generates a new project directory with nx.config.ts, tsconfig, package.json, and a starter app/main.ts.",
|
|
2852
|
+
examples: [
|
|
2853
|
+
"nx new my-app",
|
|
2854
|
+
"nx new my-app --style nest --view rendu --orm drizzle --db bun-sqlite"
|
|
2855
|
+
],
|
|
2856
|
+
flags: [
|
|
2857
|
+
{
|
|
2858
|
+
name: "style",
|
|
2859
|
+
description: "Routing style (nest|adonis|functional|mixed)"
|
|
2860
|
+
},
|
|
2861
|
+
{ name: "view", description: "View engine (rendu|edge|eta|inertia|none)" },
|
|
2862
|
+
{ name: "orm", description: "ORM driver (drizzle|prisma|kysely|none)" },
|
|
2863
|
+
{
|
|
2864
|
+
name: "db",
|
|
2865
|
+
description: "Database driver (bun-sqlite|node-sqlite|libsql|postgres|mysql|none)"
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
name: "frontend",
|
|
2869
|
+
description: "Inertia frontend (react|vue|svelte|solid)"
|
|
2870
|
+
},
|
|
2871
|
+
{ name: "no-ssr", description: "Disable Inertia SSR" },
|
|
2872
|
+
{ name: "no-interaction", description: "Disable interactive prompts" }
|
|
2873
|
+
],
|
|
2874
|
+
async run(ctx) {
|
|
2875
|
+
const name = ctx.positional[0];
|
|
2876
|
+
if (!name) {
|
|
2877
|
+
logger19.error("Usage: nx new <project-name>");
|
|
2878
|
+
return 1;
|
|
2879
|
+
}
|
|
2880
|
+
const interactive = !flagBool5(ctx.flags, "no-interaction", false);
|
|
2881
|
+
const target = resolve19(ctx.cwd, name);
|
|
2882
|
+
if (existsSync4(target)) {
|
|
2883
|
+
logger19.error(`Directory already exists: ${target}`);
|
|
2884
|
+
return 1;
|
|
2885
|
+
}
|
|
2886
|
+
const routing = ctx.flags["style"] ?? await select2("Routing style", ["nest", "adonis", "functional"], {
|
|
2887
|
+
interactive,
|
|
2888
|
+
default: "nest"
|
|
2889
|
+
});
|
|
2890
|
+
const view = ctx.flags["view"] ?? await select2("View engine", ["rendu", "edge", "eta", "inertia", "none"], {
|
|
2891
|
+
interactive,
|
|
2892
|
+
default: "rendu"
|
|
2893
|
+
});
|
|
2894
|
+
const orm = ctx.flags["orm"] ?? await select2("ORM driver", ["drizzle", "prisma", "kysely", "none"], {
|
|
2895
|
+
interactive,
|
|
2896
|
+
default: "drizzle"
|
|
2897
|
+
});
|
|
2898
|
+
const db = ctx.flags["db"] ?? await select2("Database driver", ["bun-sqlite", "node-sqlite", "libsql", "postgres", "mysql", "none"], {
|
|
2899
|
+
interactive,
|
|
2900
|
+
default: "bun-sqlite"
|
|
2901
|
+
});
|
|
2902
|
+
const frontend = ctx.flags["frontend"] ?? await select2("Inertia frontend", ["react", "vue", "svelte", "solid"], {
|
|
2903
|
+
interactive,
|
|
2904
|
+
default: "react"
|
|
2905
|
+
});
|
|
2906
|
+
const ssr = !flagBool5(ctx.flags, "no-ssr", false);
|
|
2907
|
+
mkdirSync3(resolve19(target, "app"), { recursive: true });
|
|
2908
|
+
mkdirSync3(resolve19(target, "public"), { recursive: true });
|
|
2909
|
+
mkdirSync3(resolve19(target, "resources/views"), { recursive: true });
|
|
2910
|
+
writeFileSync2(resolve19(target, "public/.gitkeep"), "");
|
|
2911
|
+
writeFileSync2(resolve19(target, "resources/views/welcome.html"), `<h1>Welcome to ${name}</h1>
|
|
2912
|
+
<p>This is a sample Rendu template.</p>
|
|
2913
|
+
<p>Founded <?= year ?>.</p>
|
|
2914
|
+
`);
|
|
2915
|
+
writeFileSync2(resolve19(target, ".env"), generateEnvFile());
|
|
2916
|
+
writeFileSync2(resolve19(target, ".env.local"), generateEnvLocalFile());
|
|
2917
|
+
writeFileSync2(resolve19(target, ".gitignore"), generateGitIgnore());
|
|
2918
|
+
const code = render15(templates.project["nx.config.ts"], {
|
|
2919
|
+
routing,
|
|
2920
|
+
view,
|
|
2921
|
+
viewPaths: view === "none" ? "" : "resources/views",
|
|
2922
|
+
orm,
|
|
2923
|
+
dbDriver: db,
|
|
2924
|
+
dbUrl: db === "bun-sqlite" || db === "node-sqlite" ? "app.db" : "",
|
|
2925
|
+
inertiaFrontend: frontend,
|
|
2926
|
+
inertiaSSR: ssr,
|
|
2927
|
+
inertiaVersion: "1.0.0"
|
|
2928
|
+
});
|
|
2929
|
+
writeFileSync2(resolve19(target, "nx.config.ts"), code);
|
|
2930
|
+
writeFileSync2(resolve19(target, "package.json"), JSON.stringify({
|
|
2931
|
+
name,
|
|
2932
|
+
version: "0.1.0",
|
|
2933
|
+
type: "module",
|
|
2934
|
+
scripts: {
|
|
2935
|
+
dev: "bun --hot app/main.ts",
|
|
2936
|
+
build: "bun run build.ts",
|
|
2937
|
+
start: "bun app/main.ts",
|
|
2938
|
+
test: "vitest",
|
|
2939
|
+
nx: "nx"
|
|
2940
|
+
},
|
|
2941
|
+
dependencies: {
|
|
2942
|
+
"@nexusts/core": "*",
|
|
2943
|
+
"reflect-metadata": "^0.2.2",
|
|
2944
|
+
hono: "^4.6.0",
|
|
2945
|
+
zod: "^3.23.8"
|
|
2946
|
+
}
|
|
2947
|
+
}, null, 2));
|
|
2948
|
+
writeFileSync2(resolve19(target, "tsconfig.json"), `{
|
|
2949
|
+
"compilerOptions": {
|
|
2950
|
+
"target": "ES2022",
|
|
2951
|
+
"module": "ESNext",
|
|
2952
|
+
"moduleResolution": "bundler",
|
|
2953
|
+
"experimentalDecorators": true,
|
|
2954
|
+
"emitDecoratorMetadata": true,
|
|
2955
|
+
"strict": true,
|
|
2956
|
+
"esModuleInterop": true,
|
|
2957
|
+
"skipLibCheck": true,
|
|
2958
|
+
"types": ["bun-types"]
|
|
2959
|
+
},
|
|
2960
|
+
"include": ["app/**/*.ts", "nx.config.ts"]
|
|
2961
|
+
}
|
|
2962
|
+
`);
|
|
2963
|
+
writeFileSync2(resolve19(target, "app/main.ts"), `import 'reflect-metadata';
|
|
2964
|
+
import { Application } from '@nexusts/core';
|
|
2965
|
+
import { StaticModule } from '@nexusts/static';
|
|
2966
|
+
import { AppModule } from './app.module.js';
|
|
2967
|
+
|
|
2968
|
+
const app = new Application(AppModule);
|
|
2969
|
+
// Serve ./public files under /static/*
|
|
2970
|
+
app.server.app.use('/static/*', StaticModule.mount({ root: './public', prefix: '/static' }));
|
|
2971
|
+
|
|
2972
|
+
const port = Number(process.env["PORT"] ?? 3000);
|
|
2973
|
+
await app.listen(port);
|
|
2974
|
+
console.log("[nexusjs] Listening on http://localhost:" + port);
|
|
2975
|
+
`);
|
|
2976
|
+
writeFileSync2(resolve19(target, "app/app.module.ts"), `import { Module } from '@nexusts/core';
|
|
2977
|
+
import { HomeController } from './controllers/home.controller.js';
|
|
2978
|
+
|
|
2979
|
+
@Module({
|
|
2980
|
+
imports: [],
|
|
2981
|
+
controllers: [HomeController],
|
|
2982
|
+
})
|
|
2983
|
+
export class AppModule {}
|
|
2984
|
+
`);
|
|
2985
|
+
mkdirSync3(resolve19(target, "app/controllers"), { recursive: true });
|
|
2986
|
+
writeFileSync2(resolve19(target, "app/controllers/home.controller.ts"), `import { Controller, Get } from '@nexusts/core';
|
|
2987
|
+
|
|
2988
|
+
@Controller('/')
|
|
2989
|
+
export class HomeController {
|
|
2990
|
+
@Get('/')
|
|
2991
|
+
index() {
|
|
2992
|
+
return {
|
|
2993
|
+
view: 'welcome.html',
|
|
2994
|
+
data: { year: new Date().getFullYear() },
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
`);
|
|
2999
|
+
writeFileSync2(resolve19(target, "README.md"), `# ${name}
|
|
3000
|
+
|
|
3001
|
+
A new Nexus project.
|
|
3002
|
+
|
|
3003
|
+
## Run
|
|
3004
|
+
|
|
3005
|
+
\`\`\`bash
|
|
3006
|
+
bun install
|
|
3007
|
+
bun run dev
|
|
3008
|
+
\`\`\`
|
|
3009
|
+
|
|
3010
|
+
## Scaffolding
|
|
3011
|
+
|
|
3012
|
+
\`\`\`bash
|
|
3013
|
+
bunx nx make:crud Post
|
|
3014
|
+
\`\`\`
|
|
3015
|
+
`);
|
|
3016
|
+
logger19.success(`created ${target}`);
|
|
3017
|
+
logger19.blank();
|
|
3018
|
+
logger19.heading("Next steps");
|
|
3019
|
+
logger19.info(` cd ${name}`);
|
|
3020
|
+
logger19.info(` bun install`);
|
|
3021
|
+
logger19.info(` bun run dev`);
|
|
3022
|
+
logger19.blank();
|
|
3023
|
+
return 0;
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
function generateEnvFile() {
|
|
3027
|
+
return `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3028
|
+
# NexusTS \u2014 Environment Variables (committed to git)
|
|
3029
|
+
#
|
|
3030
|
+
# Shared defaults for all environments. Override locally via
|
|
3031
|
+
# .env.local (gitignored) or by environment via .env.{NODE_ENV}
|
|
3032
|
+
# (e.g. .env.production, .env.development).
|
|
3033
|
+
#
|
|
3034
|
+
# Uncomment the database config for your driver:
|
|
3035
|
+
# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3036
|
+
|
|
3037
|
+
# \u2500\u2500 App \u2500\u2500
|
|
3038
|
+
NODE_ENV=development
|
|
3039
|
+
PORT=3000
|
|
3040
|
+
|
|
3041
|
+
# \u2500\u2500 Session secret (REQUIRED) \u2500\u2500
|
|
3042
|
+
# Generate with: openssl rand -base64 32
|
|
3043
|
+
SESSION_SECRET=change-me-in-production
|
|
3044
|
+
|
|
3045
|
+
# \u2500\u2500 Database: SQLite (default, zero config) \u2500\u2500
|
|
3046
|
+
DATABASE_URL=app.db
|
|
3047
|
+
|
|
3048
|
+
# \u2500\u2500 Database: PostgreSQL \u2500\u2500
|
|
3049
|
+
# DATABASE_URL=postgres://user:password@localhost:5432/myapp
|
|
3050
|
+
|
|
3051
|
+
# \u2500\u2500 Database: MySQL \u2500\u2500
|
|
3052
|
+
# DATABASE_URL=mysql://user:password@localhost:3306/myapp
|
|
3053
|
+
`;
|
|
3054
|
+
}
|
|
3055
|
+
function generateEnvLocalFile() {
|
|
3056
|
+
return `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3057
|
+
# NexusTS \u2014 Local Overrides (DO NOT COMMIT to git)
|
|
3058
|
+
#
|
|
3059
|
+
# This file is gitignored. Use it for secrets and local
|
|
3060
|
+
# configuration that should never be checked in.
|
|
3061
|
+
# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3062
|
+
|
|
3063
|
+
# Override any value from .env here:
|
|
3064
|
+
# DATABASE_URL=postgres://user:password@localhost:5432/myapp
|
|
3065
|
+
# SESSION_SECRET=my-local-secret
|
|
3066
|
+
`;
|
|
3067
|
+
}
|
|
3068
|
+
function generateGitIgnore() {
|
|
3069
|
+
return `# NexusTS
|
|
3070
|
+
node_modules/
|
|
3071
|
+
app.db
|
|
3072
|
+
*.db
|
|
3073
|
+
.env.local
|
|
3074
|
+
dist/
|
|
3075
|
+
`;
|
|
3076
|
+
}
|
|
3077
|
+
var new_default = newCommand;
|
|
3078
|
+
|
|
3079
|
+
// packages/cli/src/commands/config.ts
|
|
3080
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3081
|
+
import { resolve as resolve20 } from "path";
|
|
3082
|
+
import { flagBool as flagBool6, logger as logger20, render as render16, select as select3 } from "@nexusts/core/index.js";
|
|
3083
|
+
var DEFAULT_VALUES = {
|
|
3084
|
+
routing: "nest",
|
|
3085
|
+
view: "rendu",
|
|
3086
|
+
viewPaths: "resources/views",
|
|
3087
|
+
orm: "drizzle",
|
|
3088
|
+
dbDriver: "bun-sqlite",
|
|
3089
|
+
dbUrl: "app.db",
|
|
3090
|
+
inertiaFrontend: "react",
|
|
3091
|
+
inertiaSSR: true,
|
|
3092
|
+
inertiaVersion: "1.0.0"
|
|
3093
|
+
};
|
|
3094
|
+
function parseExistingConfig(path) {
|
|
3095
|
+
const out = { ...DEFAULT_VALUES };
|
|
3096
|
+
if (!existsSync5(path))
|
|
3097
|
+
return out;
|
|
3098
|
+
const src = readFileSync2(path, "utf8");
|
|
3099
|
+
const grab = (re) => {
|
|
3100
|
+
const m = src.match(re);
|
|
3101
|
+
return m?.[1];
|
|
3102
|
+
};
|
|
3103
|
+
const routing = grab(/routing:\s*['"]([^'"]+)['"]/);
|
|
3104
|
+
const view = grab(/view:\s*['"]([^'"]+)['"]/);
|
|
3105
|
+
const viewPathsMatch = src.match(/viewPaths:\s*['"]([^'"]+)['"]/);
|
|
3106
|
+
if (viewPathsMatch) {
|
|
3107
|
+
out.viewPaths = viewPathsMatch[1];
|
|
3108
|
+
}
|
|
3109
|
+
const orm = grab(/orm:\s*['"]([^'"]+)['"]/);
|
|
3110
|
+
const driver = grab(/driver:\s*['"]([^'"]+)['"]/);
|
|
3111
|
+
const url = grab(/url:\s*process\.env\.DATABASE_URL\s*\?\?\s*['"]([^'"]+)['"]/);
|
|
3112
|
+
const frontend = grab(/frontend:\s*['"]([^'"]+)['"]/);
|
|
3113
|
+
const ssr = grab(/ssr:\s*(true|false)/);
|
|
3114
|
+
const version = grab(/version:\s*['"]([^'"]+)['"]/);
|
|
3115
|
+
if (routing)
|
|
3116
|
+
out.routing = routing;
|
|
3117
|
+
if (view)
|
|
3118
|
+
out.view = view;
|
|
3119
|
+
if (orm)
|
|
3120
|
+
out.orm = orm;
|
|
3121
|
+
if (driver)
|
|
3122
|
+
out.dbDriver = driver;
|
|
3123
|
+
if (url !== undefined)
|
|
3124
|
+
out.dbUrl = url;
|
|
3125
|
+
if (frontend)
|
|
3126
|
+
out.inertiaFrontend = frontend;
|
|
3127
|
+
if (ssr)
|
|
3128
|
+
out.inertiaSSR = ssr === "true";
|
|
3129
|
+
if (version)
|
|
3130
|
+
out.inertiaVersion = version;
|
|
3131
|
+
return out;
|
|
3132
|
+
}
|
|
3133
|
+
function driverToDialect(driver) {
|
|
3134
|
+
switch (driver) {
|
|
3135
|
+
case "bun-sqlite":
|
|
3136
|
+
case "node-sqlite":
|
|
3137
|
+
case "libsql":
|
|
3138
|
+
return "sqlite";
|
|
3139
|
+
case "postgres":
|
|
3140
|
+
return "postgresql";
|
|
3141
|
+
case "mysql":
|
|
3142
|
+
return "mysql";
|
|
3143
|
+
default:
|
|
3144
|
+
return "sqlite";
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
function defaultDbUrl(driver) {
|
|
3148
|
+
if (driver === "bun-sqlite" || driver === "node-sqlite" || driver === "libsql") {
|
|
3149
|
+
return "app.db";
|
|
3150
|
+
}
|
|
3151
|
+
return "";
|
|
3152
|
+
}
|
|
3153
|
+
var configCommand = {
|
|
3154
|
+
name: "config",
|
|
3155
|
+
aliases: ["cfg"],
|
|
3156
|
+
summary: "Update or create nx.config.ts (+ drizzle.config.ts if Drizzle is selected)",
|
|
3157
|
+
description: "Re-renders nx.config.ts from the current values (parsed from the existing file, or prompted) plus any flag overrides. Also creates or updates drizzle.config.ts when the ORM is `drizzle`.",
|
|
3158
|
+
examples: [
|
|
3159
|
+
"nx config",
|
|
3160
|
+
"nx config --db postgres --db-url postgres://localhost/mydb",
|
|
3161
|
+
"nx config --orm drizzle --db bun-sqlite",
|
|
3162
|
+
"nx config --view inertia --frontend vue --no-ssr",
|
|
3163
|
+
"nx config --force"
|
|
3164
|
+
],
|
|
3165
|
+
flags: [
|
|
3166
|
+
{ name: "target", description: "Target directory (default: cwd)" },
|
|
3167
|
+
{
|
|
3168
|
+
name: "style",
|
|
3169
|
+
description: "Routing style (nest|adonis|functional|mixed)"
|
|
3170
|
+
},
|
|
3171
|
+
{ name: "view", description: "View engine (rendu|edge|inertia|none)" },
|
|
3172
|
+
{
|
|
3173
|
+
name: "view-paths",
|
|
3174
|
+
description: "Comma-separated directories searched for view files (e.g. resources/views)"
|
|
3175
|
+
},
|
|
3176
|
+
{ name: "orm", description: "ORM driver (drizzle|prisma|kysely|none)" },
|
|
3177
|
+
{
|
|
3178
|
+
name: "db",
|
|
3179
|
+
description: "Database driver (bun-sqlite|node-sqlite|libsql|postgres|mysql|none)"
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
name: "db-url",
|
|
3183
|
+
description: "Default DATABASE_URL when the env var is unset"
|
|
3184
|
+
},
|
|
3185
|
+
{
|
|
3186
|
+
name: "frontend",
|
|
3187
|
+
description: "Inertia frontend (react|vue|svelte|solid)"
|
|
3188
|
+
},
|
|
3189
|
+
{ name: "ssr", description: "Enable Inertia SSR" },
|
|
3190
|
+
{ name: "no-ssr", description: "Disable Inertia SSR" },
|
|
3191
|
+
{ name: "force", description: "Overwrite even if file already exists" },
|
|
3192
|
+
{ name: "no-interaction", description: "Disable interactive prompts" }
|
|
3193
|
+
],
|
|
3194
|
+
async run(ctx) {
|
|
3195
|
+
const interactive = !flagBool6(ctx.flags, "no-interaction", false);
|
|
3196
|
+
const force = flagBool6(ctx.flags, "force", false);
|
|
3197
|
+
const target = resolve20(ctx.cwd, ctx.flags["target"] ?? ".");
|
|
3198
|
+
if (!existsSync5(target)) {
|
|
3199
|
+
logger20.error(`Target directory does not exist: ${target}`);
|
|
3200
|
+
return 1;
|
|
3201
|
+
}
|
|
3202
|
+
const nxConfigPath = resolve20(target, "nx.config.ts");
|
|
3203
|
+
const values = parseExistingConfig(nxConfigPath);
|
|
3204
|
+
const flag = (k) => ctx.flags[k];
|
|
3205
|
+
const flagBoolStrict = (k, def) => flagBool6(ctx.flags, k, def);
|
|
3206
|
+
if (flag("style"))
|
|
3207
|
+
values.routing = flag("style");
|
|
3208
|
+
if (flag("view"))
|
|
3209
|
+
values.view = flag("view");
|
|
3210
|
+
if (flag("view-paths")) {
|
|
3211
|
+
values.viewPaths = flag("view-paths");
|
|
3212
|
+
}
|
|
3213
|
+
if (flag("orm"))
|
|
3214
|
+
values.orm = flag("orm");
|
|
3215
|
+
if (flag("db"))
|
|
3216
|
+
values.dbDriver = flag("db");
|
|
3217
|
+
if (flag("db-url") !== undefined)
|
|
3218
|
+
values.dbUrl = flag("db-url");
|
|
3219
|
+
if (flag("frontend"))
|
|
3220
|
+
values.inertiaFrontend = flag("frontend");
|
|
3221
|
+
if (flagBoolStrict("ssr", false))
|
|
3222
|
+
values.inertiaSSR = true;
|
|
3223
|
+
if (flagBoolStrict("no-ssr", false))
|
|
3224
|
+
values.inertiaSSR = false;
|
|
3225
|
+
const anyFlag = Object.values(ctx.flags).some((v) => v !== undefined && v !== false);
|
|
3226
|
+
if (interactive && !anyFlag && !existsSync5(nxConfigPath)) {
|
|
3227
|
+
values.routing = await select3("Routing style", ["nest", "adonis", "functional"], {
|
|
3228
|
+
interactive,
|
|
3229
|
+
default: values.routing
|
|
3230
|
+
}) ?? values.routing;
|
|
3231
|
+
values.view = await select3("View engine", ["inertia", "rendu", "edge", "none"], {
|
|
3232
|
+
interactive,
|
|
3233
|
+
default: values.view
|
|
3234
|
+
}) ?? values.view;
|
|
3235
|
+
values.orm = await select3("ORM driver", ["drizzle", "prisma", "kysely", "none"], {
|
|
3236
|
+
interactive,
|
|
3237
|
+
default: values.orm
|
|
3238
|
+
}) ?? values.orm;
|
|
3239
|
+
values.dbDriver = await select3("Database driver", ["bun-sqlite", "node-sqlite", "libsql", "postgres", "mysql", "none"], { interactive, default: values.dbDriver }) ?? values.dbDriver;
|
|
3240
|
+
values.inertiaFrontend = await select3("Inertia frontend", ["react", "vue", "svelte", "solid"], { interactive, default: values.inertiaFrontend }) ?? values.inertiaFrontend;
|
|
3241
|
+
}
|
|
3242
|
+
if (flag("db-url") === undefined && flag("db") !== undefined) {
|
|
3243
|
+
values.dbUrl = defaultDbUrl(values.dbDriver);
|
|
3244
|
+
}
|
|
3245
|
+
const existed = existsSync5(nxConfigPath);
|
|
3246
|
+
if (existed && !force) {
|
|
3247
|
+
if (anyFlag) {
|
|
3248
|
+
writeNxConfig(target, values);
|
|
3249
|
+
logger20.info(` ~ nx.config.ts (updated)`);
|
|
3250
|
+
} else {
|
|
3251
|
+
logger20.info(` - nx.config.ts (unchanged; pass --force or a flag to update)`);
|
|
3252
|
+
}
|
|
3253
|
+
} else {
|
|
3254
|
+
writeNxConfig(target, values);
|
|
3255
|
+
logger20.info(` + nx.config.ts`);
|
|
3256
|
+
}
|
|
3257
|
+
const drizzleConfigPath = resolve20(target, "drizzle.config.ts");
|
|
3258
|
+
if (values.orm === "drizzle") {
|
|
3259
|
+
const dialect = driverToDialect(values.dbDriver);
|
|
3260
|
+
const dbUrl = values.dbUrl;
|
|
3261
|
+
const existedDrizzle = existsSync5(drizzleConfigPath);
|
|
3262
|
+
if (existedDrizzle && !force && !anyFlag) {
|
|
3263
|
+
logger20.info(` - drizzle.config.ts (unchanged; pass --force or a flag to update)`);
|
|
3264
|
+
} else {
|
|
3265
|
+
writeDrizzleConfig(target, { dialect, dbUrl });
|
|
3266
|
+
logger20.info(` ${existedDrizzle ? "~" : "+"} drizzle.config.ts`);
|
|
3267
|
+
}
|
|
3268
|
+
} else if (existsSync5(drizzleConfigPath)) {
|
|
3269
|
+
logger20.info(` - drizzle.config.ts (left as-is; ORM is '${values.orm}', not 'drizzle')`);
|
|
3270
|
+
}
|
|
3271
|
+
logger20.blank();
|
|
3272
|
+
logger20.success(`config updated in ${target}`);
|
|
3273
|
+
logger20.blank();
|
|
3274
|
+
logger20.heading("Next steps");
|
|
3275
|
+
logger20.info(` cd ${target === ctx.cwd ? "." : target}`);
|
|
3276
|
+
if (values.orm === "drizzle") {
|
|
3277
|
+
logger20.info(` bun run db:generate # generate migrations`);
|
|
3278
|
+
}
|
|
3279
|
+
logger20.blank();
|
|
3280
|
+
return 0;
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
function writeNxConfig(target, values) {
|
|
3284
|
+
const code = render16(templates.project["nx.config.ts"], values);
|
|
3285
|
+
writeFileSync3(resolve20(target, "nx.config.ts"), code);
|
|
3286
|
+
}
|
|
3287
|
+
function writeDrizzleConfig(target, values) {
|
|
3288
|
+
const code = render16(templates.project["drizzle.config.ts"], values);
|
|
3289
|
+
writeFileSync3(resolve20(target, "drizzle.config.ts"), code);
|
|
3290
|
+
}
|
|
3291
|
+
var config_default = configCommand;
|
|
3292
|
+
|
|
3293
|
+
// packages/cli/src/commands/repl.ts
|
|
3294
|
+
import {
|
|
3295
|
+
existsSync as existsSync6,
|
|
3296
|
+
mkdirSync as mkdirSync4,
|
|
3297
|
+
readFileSync as readFileSync3,
|
|
3298
|
+
writeFileSync as writeFileSync4
|
|
3299
|
+
} from "fs";
|
|
3300
|
+
import { dirname as dirname2, resolve as resolve21 } from "path";
|
|
3301
|
+
import * as readline from "readline";
|
|
3302
|
+
import * as vm from "vm";
|
|
3303
|
+
import { logger as logger21 } from "@nexusts/core/index.js";
|
|
3304
|
+
var BANNER = `
|
|
3305
|
+
\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
|
|
3306
|
+
\u2502 NexusTS REPL \u2502
|
|
3307
|
+
\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F
|
|
3308
|
+
`;
|
|
3309
|
+
var HELP = `
|
|
3310
|
+
Available commands:
|
|
3311
|
+
.help Show this help
|
|
3312
|
+
.exit Exit the REPL (alias: .quit)
|
|
3313
|
+
.services List services in the DI container
|
|
3314
|
+
.modules List all modules
|
|
3315
|
+
.routes List all registered routes
|
|
3316
|
+
.history Show command history
|
|
3317
|
+
.clear Clear the screen
|
|
3318
|
+
.reset Reset the REPL context (clear all variables)
|
|
3319
|
+
|
|
3320
|
+
Pre-loaded variables (when an app is booted):
|
|
3321
|
+
app The Application instance
|
|
3322
|
+
container The DI container
|
|
3323
|
+
db DrizzleService (if registered)
|
|
3324
|
+
logger LoggerService (if registered)
|
|
3325
|
+
cfg ConfigService (if registered)
|
|
3326
|
+
cache CacheService (if registered)
|
|
3327
|
+
events EventService (if registered)
|
|
3328
|
+
|
|
3329
|
+
Examples:
|
|
3330
|
+
> await db.select().from(users).all()
|
|
3331
|
+
> logger.info("hello from REPL")
|
|
3332
|
+
> app.container.size
|
|
3333
|
+
`;
|
|
3334
|
+
var PRELOAD = [
|
|
3335
|
+
["db", "../../drizzle/drizzle.service.js", "DrizzleService"],
|
|
3336
|
+
["logger", "../../logger/logger.service.js", "LoggerService"],
|
|
3337
|
+
["cfg", "../../config/config.service.js", "ConfigService"],
|
|
3338
|
+
["cache", "../../cache/cache.service.js", "CacheService"],
|
|
3339
|
+
["events", "../../events/event.service.js", "EventService"]
|
|
3340
|
+
];
|
|
3341
|
+
var replCommand = {
|
|
3342
|
+
name: "repl",
|
|
3343
|
+
aliases: ["console", "shell"],
|
|
3344
|
+
summary: "Start an interactive REPL with the app's services",
|
|
3345
|
+
description: "Boots the user's app module and drops into an interactive REPL. Useful for debugging, exploring data, and trying out queries. Multi-line input is supported. History is persisted to .nx-repl-history.",
|
|
3346
|
+
examples: [
|
|
3347
|
+
"nx repl",
|
|
3348
|
+
"nx repl --module app/app.module.ts",
|
|
3349
|
+
"nx repl --no-boot",
|
|
3350
|
+
"nx repl --history /tmp/nx-history"
|
|
3351
|
+
],
|
|
3352
|
+
flags: [
|
|
3353
|
+
{
|
|
3354
|
+
name: "module",
|
|
3355
|
+
description: "Path to the AppModule (default: app/app.module.ts)."
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
name: "no-boot",
|
|
3359
|
+
description: "Skip booting the app \u2014 start a vanilla REPL."
|
|
3360
|
+
},
|
|
3361
|
+
{
|
|
3362
|
+
name: "history",
|
|
3363
|
+
description: "History file path (default: .nx-repl-history)."
|
|
3364
|
+
}
|
|
3365
|
+
],
|
|
3366
|
+
async run(ctx) {
|
|
3367
|
+
const mod = ctx.flags["module"];
|
|
3368
|
+
const noBoot = Boolean(ctx.flags["no-boot"]);
|
|
3369
|
+
const histPath = resolve21(ctx.cwd, ctx.flags["history"] ?? ".nx-repl-history");
|
|
3370
|
+
const env = { console };
|
|
3371
|
+
if (!noBoot) {
|
|
3372
|
+
const modPath = resolve21(ctx.cwd, mod ?? "app/app.module.ts");
|
|
3373
|
+
if (!existsSync6(modPath)) {
|
|
3374
|
+
logger21.error(`module not found: ${modPath}`);
|
|
3375
|
+
logger21.info("pass --module <path> or --no-boot to skip booting");
|
|
3376
|
+
return 1;
|
|
3377
|
+
}
|
|
3378
|
+
try {
|
|
3379
|
+
const modSpec = await import(modPath);
|
|
3380
|
+
const AppModule = modSpec.default ?? modSpec.AppModule;
|
|
3381
|
+
if (!AppModule) {
|
|
3382
|
+
logger21.error(`module at ${modPath} does not export AppModule as default or named export`);
|
|
3383
|
+
return 1;
|
|
3384
|
+
}
|
|
3385
|
+
const { Application } = await import("@nexusts/core/src/application.js");
|
|
3386
|
+
const app = new Application(AppModule);
|
|
3387
|
+
env.app = app;
|
|
3388
|
+
env.container = app.container;
|
|
3389
|
+
for (const [name, path, className] of PRELOAD) {
|
|
3390
|
+
await preloadService(env, app, name, path, className);
|
|
3391
|
+
}
|
|
3392
|
+
logger21.info(`\u2713 Booted ${modPath}`);
|
|
3393
|
+
} catch (err) {
|
|
3394
|
+
logger21.error(`failed to boot: ${err.message}`);
|
|
3395
|
+
return 1;
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
console.log(BANNER);
|
|
3399
|
+
if (noBoot) {
|
|
3400
|
+
console.log(" (started with --no-boot; no app is loaded)");
|
|
3401
|
+
}
|
|
3402
|
+
const history = loadHistory(histPath);
|
|
3403
|
+
const rl = readline.createInterface({
|
|
3404
|
+
input: process.stdin,
|
|
3405
|
+
output: process.stdout,
|
|
3406
|
+
prompt: "\u276F ",
|
|
3407
|
+
terminal: true,
|
|
3408
|
+
history,
|
|
3409
|
+
historySize: 1000
|
|
3410
|
+
});
|
|
3411
|
+
const saveHist = () => saveHistoryFile(histPath, rl.history ?? []);
|
|
3412
|
+
rl.on("close", () => {
|
|
3413
|
+
saveHist();
|
|
3414
|
+
process.exit(0);
|
|
3415
|
+
});
|
|
3416
|
+
process.on("SIGINT", () => {
|
|
3417
|
+
saveHist();
|
|
3418
|
+
process.exit(0);
|
|
3419
|
+
});
|
|
3420
|
+
process.on("exit", saveHist);
|
|
3421
|
+
let buffer = "";
|
|
3422
|
+
const vmContext = vm.createContext(env);
|
|
3423
|
+
const evaluate = async (code) => {
|
|
3424
|
+
try {
|
|
3425
|
+
const script = new vm.Script(code, { filename: "<nx-repl>" });
|
|
3426
|
+
const result = script.runInContext(vmContext, {
|
|
3427
|
+
displayErrors: false
|
|
3428
|
+
});
|
|
3429
|
+
if (result === undefined)
|
|
3430
|
+
return;
|
|
3431
|
+
if (result && typeof result.then === "function") {
|
|
3432
|
+
const v = await result;
|
|
3433
|
+
console.log(formatResult(v));
|
|
3434
|
+
} else {
|
|
3435
|
+
console.log(formatResult(result));
|
|
3436
|
+
}
|
|
3437
|
+
} catch (err) {
|
|
3438
|
+
console.error(formatError(err));
|
|
3439
|
+
}
|
|
3440
|
+
};
|
|
3441
|
+
const handleDotCommand = async (line) => {
|
|
3442
|
+
const cmd = line.trim();
|
|
3443
|
+
switch (cmd) {
|
|
3444
|
+
case ".help":
|
|
3445
|
+
console.log(HELP);
|
|
3446
|
+
return true;
|
|
3447
|
+
case ".exit":
|
|
3448
|
+
case ".quit":
|
|
3449
|
+
saveHist();
|
|
3450
|
+
rl.close();
|
|
3451
|
+
return true;
|
|
3452
|
+
case ".clear":
|
|
3453
|
+
console.clear();
|
|
3454
|
+
console.log(BANNER);
|
|
3455
|
+
return true;
|
|
3456
|
+
case ".services": {
|
|
3457
|
+
const services = listServices(env.container);
|
|
3458
|
+
if (services.length === 0) {
|
|
3459
|
+
console.log(" (no services registered)");
|
|
3460
|
+
} else {
|
|
3461
|
+
for (const s of services)
|
|
3462
|
+
console.log(` ${s}`);
|
|
3463
|
+
}
|
|
3464
|
+
return true;
|
|
3465
|
+
}
|
|
3466
|
+
case ".modules": {
|
|
3467
|
+
const mods = env.app?.modules;
|
|
3468
|
+
if (!mods || mods.length === 0) {
|
|
3469
|
+
console.log(" (no modules)");
|
|
3470
|
+
} else {
|
|
3471
|
+
for (const m of mods) {
|
|
3472
|
+
console.log(` ${m.name ?? m.constructor?.name ?? "(anon)"}`);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
return true;
|
|
3476
|
+
}
|
|
3477
|
+
case ".routes": {
|
|
3478
|
+
const app = env.app;
|
|
3479
|
+
const routes = app?.server?.app?.routes ?? [];
|
|
3480
|
+
if (routes.length === 0) {
|
|
3481
|
+
console.log(" (no routes registered)");
|
|
3482
|
+
} else {
|
|
3483
|
+
for (const r of routes) {
|
|
3484
|
+
const m = (r.method ?? "?").padEnd(7);
|
|
3485
|
+
console.log(` ${m} ${r.path ?? "?"}`);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
return true;
|
|
3489
|
+
}
|
|
3490
|
+
case ".history": {
|
|
3491
|
+
const hist = rl.history ?? [];
|
|
3492
|
+
const histArr = hist;
|
|
3493
|
+
histArr.forEach((h, i) => {
|
|
3494
|
+
console.log(` ${(i + 1).toString().padStart(4)}: ${h}`);
|
|
3495
|
+
});
|
|
3496
|
+
return true;
|
|
3497
|
+
}
|
|
3498
|
+
case ".reset": {
|
|
3499
|
+
for (const k of Object.keys(env)) {
|
|
3500
|
+
if (k !== "console")
|
|
3501
|
+
delete env[k];
|
|
3502
|
+
}
|
|
3503
|
+
console.log(" context reset");
|
|
3504
|
+
return true;
|
|
3505
|
+
}
|
|
3506
|
+
default:
|
|
3507
|
+
console.error(` unknown command: ${cmd}`);
|
|
3508
|
+
console.log(" type .help for the list");
|
|
3509
|
+
return true;
|
|
3510
|
+
}
|
|
3511
|
+
};
|
|
3512
|
+
rl.on("line", async (line) => {
|
|
3513
|
+
const trimmed = line.trim();
|
|
3514
|
+
if (trimmed.startsWith(".")) {
|
|
3515
|
+
await handleDotCommand(trimmed);
|
|
3516
|
+
} else {
|
|
3517
|
+
buffer += line + `
|
|
3518
|
+
`;
|
|
3519
|
+
if (isIncomplete(buffer)) {
|
|
3520
|
+
rl.setPrompt("... ");
|
|
3521
|
+
rl.prompt();
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
await evaluate(buffer);
|
|
3525
|
+
buffer = "";
|
|
3526
|
+
rl.setPrompt("\u276F ");
|
|
3527
|
+
}
|
|
3528
|
+
rl.prompt();
|
|
3529
|
+
});
|
|
3530
|
+
rl.prompt();
|
|
3531
|
+
return new Promise(() => {});
|
|
3532
|
+
}
|
|
3533
|
+
};
|
|
3534
|
+
async function preloadService(env, app, name, path, className) {
|
|
3535
|
+
try {
|
|
3536
|
+
const mod = await import(path);
|
|
3537
|
+
const ServiceClass = mod[className];
|
|
3538
|
+
if (!ServiceClass)
|
|
3539
|
+
return;
|
|
3540
|
+
const Token = ServiceClass.TOKEN ?? ServiceClass[`${className.toUpperCase()}_TOKEN`];
|
|
3541
|
+
if (!Token)
|
|
3542
|
+
return;
|
|
3543
|
+
try {
|
|
3544
|
+
env[name] = app.container.resolve(Token);
|
|
3545
|
+
} catch {
|
|
3546
|
+
try {
|
|
3547
|
+
env[name] = app.container.resolve(ServiceClass);
|
|
3548
|
+
} catch {}
|
|
3549
|
+
}
|
|
3550
|
+
} catch {}
|
|
3551
|
+
}
|
|
3552
|
+
function listServices(container) {
|
|
3553
|
+
if (!container)
|
|
3554
|
+
return [];
|
|
3555
|
+
const c = container;
|
|
3556
|
+
if (typeof c.listProviders !== "function")
|
|
3557
|
+
return [];
|
|
3558
|
+
try {
|
|
3559
|
+
return c.listProviders().map((p) => p.token?.toString?.() ?? String(p.token));
|
|
3560
|
+
} catch {
|
|
3561
|
+
return [];
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
function isIncomplete(code) {
|
|
3565
|
+
const stack = [];
|
|
3566
|
+
let inString = null;
|
|
3567
|
+
let inComment = null;
|
|
3568
|
+
for (let i = 0;i < code.length; i++) {
|
|
3569
|
+
const c = code[i];
|
|
3570
|
+
const next = code[i + 1];
|
|
3571
|
+
if (inComment === "line") {
|
|
3572
|
+
if (c === `
|
|
3573
|
+
`)
|
|
3574
|
+
inComment = null;
|
|
3575
|
+
continue;
|
|
3576
|
+
}
|
|
3577
|
+
if (inComment === "block") {
|
|
3578
|
+
if (c === "*" && next === "/") {
|
|
3579
|
+
inComment = null;
|
|
3580
|
+
i++;
|
|
3581
|
+
}
|
|
3582
|
+
continue;
|
|
3583
|
+
}
|
|
3584
|
+
if (inString) {
|
|
3585
|
+
if (c === "\\") {
|
|
3586
|
+
i++;
|
|
3587
|
+
continue;
|
|
3588
|
+
}
|
|
3589
|
+
if (c === inString)
|
|
3590
|
+
inString = null;
|
|
3591
|
+
continue;
|
|
3592
|
+
}
|
|
3593
|
+
if (c === "/" && next === "/") {
|
|
3594
|
+
inComment = "line";
|
|
3595
|
+
i++;
|
|
3596
|
+
continue;
|
|
3597
|
+
}
|
|
3598
|
+
if (c === "/" && next === "*") {
|
|
3599
|
+
inComment = "block";
|
|
3600
|
+
i++;
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
3604
|
+
inString = c;
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3607
|
+
if (c === "{" || c === "[" || c === "(")
|
|
3608
|
+
stack.push(c);
|
|
3609
|
+
else if (c === "}" || c === "]" || c === ")")
|
|
3610
|
+
stack.pop();
|
|
3611
|
+
}
|
|
3612
|
+
return stack.length > 0 || inString !== null || inComment === "block";
|
|
3613
|
+
}
|
|
3614
|
+
function formatResult(r) {
|
|
3615
|
+
if (r === null)
|
|
3616
|
+
return "null";
|
|
3617
|
+
if (r === undefined)
|
|
3618
|
+
return "undefined";
|
|
3619
|
+
if (typeof r === "string")
|
|
3620
|
+
return r;
|
|
3621
|
+
if (typeof r === "number" || typeof r === "boolean" || typeof r === "bigint")
|
|
3622
|
+
return String(r);
|
|
3623
|
+
if (typeof r === "function") {
|
|
3624
|
+
const fn = r;
|
|
3625
|
+
return `[Function: ${fn.name || "anonymous"}]`;
|
|
3626
|
+
}
|
|
3627
|
+
if (typeof r === "symbol")
|
|
3628
|
+
return r.toString();
|
|
3629
|
+
try {
|
|
3630
|
+
return JSON.stringify(r, null, 2);
|
|
3631
|
+
} catch {
|
|
3632
|
+
return Object.prototype.toString.call(r);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
function formatError(e) {
|
|
3636
|
+
if (e.name === "SyntaxError")
|
|
3637
|
+
return e.message;
|
|
3638
|
+
return e.stack ? e.stack.split(`
|
|
3639
|
+
`).slice(0, 5).join(`
|
|
3640
|
+
`) : e.message;
|
|
3641
|
+
}
|
|
3642
|
+
function loadHistory(path) {
|
|
3643
|
+
if (!existsSync6(path))
|
|
3644
|
+
return [];
|
|
3645
|
+
try {
|
|
3646
|
+
return readFileSync3(path, "utf-8").split(`
|
|
3647
|
+
`).filter(Boolean);
|
|
3648
|
+
} catch {
|
|
3649
|
+
return [];
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
function saveHistoryFile(path, history) {
|
|
3653
|
+
try {
|
|
3654
|
+
const dir = dirname2(path);
|
|
3655
|
+
if (!existsSync6(dir))
|
|
3656
|
+
mkdirSync4(dir, { recursive: true });
|
|
3657
|
+
writeFileSync4(path, history.slice(-1000).join(`
|
|
3658
|
+
`));
|
|
3659
|
+
} catch {}
|
|
3660
|
+
}
|
|
3661
|
+
var repl_default = replCommand;
|
|
3662
|
+
|
|
3663
|
+
// packages/cli/src/commands/route-list.ts
|
|
3664
|
+
import { readdirSync, statSync } from "fs";
|
|
3665
|
+
import { resolve as resolve22 } from "path";
|
|
3666
|
+
import { colors as colors2, logger as logger22 } from "@nexusts/core/index.js";
|
|
3667
|
+
var routeListCommand = {
|
|
3668
|
+
name: "route:list",
|
|
3669
|
+
aliases: ["routes", "route-list"],
|
|
3670
|
+
summary: "List registered HTTP routes",
|
|
3671
|
+
description: "Reads route metadata from controllers and prints a table. Falls back to a scan message when controllers don't use the decorator style.",
|
|
3672
|
+
flags: [
|
|
3673
|
+
{ name: "format", description: "Output format: table (default) | json" }
|
|
3674
|
+
],
|
|
3675
|
+
async run(ctx) {
|
|
3676
|
+
const controllersDir = resolve22(ctx.cwd, ctx.config.paths.controllers);
|
|
3677
|
+
try {
|
|
3678
|
+
statSync(controllersDir);
|
|
3679
|
+
} catch {
|
|
3680
|
+
logger22.warn(`No controllers directory at ${controllersDir}.`);
|
|
3681
|
+
return 0;
|
|
3682
|
+
}
|
|
3683
|
+
const files = readdirSync(controllersDir).filter((f) => f.endsWith(".ts"));
|
|
3684
|
+
if (files.length === 0) {
|
|
3685
|
+
logger22.info("No controllers found.");
|
|
3686
|
+
return 0;
|
|
3687
|
+
}
|
|
3688
|
+
const routes = [];
|
|
3689
|
+
for (const file of files) {
|
|
3690
|
+
const fullPath = resolve22(controllersDir, file);
|
|
3691
|
+
try {
|
|
3692
|
+
const mod = await import(`${fullPath}?t=${Date.now()}`);
|
|
3693
|
+
for (const exportName of Object.keys(mod)) {
|
|
3694
|
+
const cls = mod[exportName];
|
|
3695
|
+
if (typeof cls !== "function")
|
|
3696
|
+
continue;
|
|
3697
|
+
const prefix = Reflect.getMetadata("nexus:controller:prefix", cls) ?? "";
|
|
3698
|
+
const routeList = Reflect.getMetadata("nexus:routes", cls) ?? [];
|
|
3699
|
+
for (const r of routeList) {
|
|
3700
|
+
routes.push({
|
|
3701
|
+
method: String(r.method).toUpperCase(),
|
|
3702
|
+
path: joinPath(prefix, r.path),
|
|
3703
|
+
handler: String(r.propertyKey),
|
|
3704
|
+
controller: cls.name || exportName
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
} catch (err) {
|
|
3709
|
+
logger22.warn(`could not parse ${file}: ${err.message ?? err}`);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
if (routes.length === 0) {
|
|
3713
|
+
logger22.info("No routes discovered via metadata. " + "This usually means the controllers use the Adonis or functional style \u2014 see nx.config.ts:routing.");
|
|
3714
|
+
return 0;
|
|
3715
|
+
}
|
|
3716
|
+
const format = ctx.flags["format"] ?? "table";
|
|
3717
|
+
if (format === "json") {
|
|
3718
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
3719
|
+
return 0;
|
|
3720
|
+
}
|
|
3721
|
+
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
3722
|
+
logger22.heading(`Routes (${routes.length})`);
|
|
3723
|
+
const methodColors = {
|
|
3724
|
+
GET: colors2.cyan,
|
|
3725
|
+
POST: colors2.green,
|
|
3726
|
+
PUT: colors2.yellow,
|
|
3727
|
+
PATCH: colors2.yellow,
|
|
3728
|
+
DELETE: colors2.red,
|
|
3729
|
+
OPTIONS: colors2.gray,
|
|
3730
|
+
HEAD: colors2.gray
|
|
3731
|
+
};
|
|
3732
|
+
const pathWidth = Math.max(...routes.map((r) => r.path.length));
|
|
3733
|
+
const methodWidth = Math.max(...routes.map((r) => r.method.length));
|
|
3734
|
+
for (const r of routes) {
|
|
3735
|
+
const colorize = methodColors[r.method] ?? colors2.reset;
|
|
3736
|
+
const m = colorize(r.method.padEnd(methodWidth));
|
|
3737
|
+
const p = colors2.bold(r.path.padEnd(pathWidth));
|
|
3738
|
+
const c = colors2.dim(`${r.controller}.${r.handler}`);
|
|
3739
|
+
console.log(` ${m} ${p} ${c}`);
|
|
3740
|
+
}
|
|
3741
|
+
logger22.blank();
|
|
3742
|
+
return 0;
|
|
3743
|
+
}
|
|
3744
|
+
};
|
|
3745
|
+
function joinPath(prefix, sub) {
|
|
3746
|
+
const a = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3747
|
+
const b = sub.startsWith("/") ? sub : `/${sub}`;
|
|
3748
|
+
return `${a}${b}` || "/";
|
|
3749
|
+
}
|
|
3750
|
+
var route_list_default = routeListCommand;
|
|
3751
|
+
|
|
3752
|
+
// packages/cli/src/commands/index.ts
|
|
3753
|
+
var commands = [
|
|
3754
|
+
new_default,
|
|
3755
|
+
init_default,
|
|
3756
|
+
config_default,
|
|
3757
|
+
make_crud_default,
|
|
3758
|
+
make_controller_default,
|
|
3759
|
+
make_service_default,
|
|
3760
|
+
make_module_default,
|
|
3761
|
+
make_model_default,
|
|
3762
|
+
make_migration_default,
|
|
3763
|
+
make_middleware_default,
|
|
3764
|
+
make_validator_default,
|
|
3765
|
+
make_auth_default,
|
|
3766
|
+
make_queue_default,
|
|
3767
|
+
make_schedule_default,
|
|
3768
|
+
make_listener_default,
|
|
3769
|
+
make_session_default,
|
|
3770
|
+
db_migrate_default,
|
|
3771
|
+
db_generate_default,
|
|
3772
|
+
db_seed_default,
|
|
3773
|
+
route_list_default,
|
|
3774
|
+
info_default,
|
|
3775
|
+
repl_default
|
|
3776
|
+
];
|
|
3777
|
+
function findCommand(name) {
|
|
3778
|
+
return commands.find((c) => c.name === name || (c.aliases ?? []).includes(name));
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// packages/cli/src/core/config.ts
|
|
3782
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
3783
|
+
import { resolve as resolve23 } from "path";
|
|
3784
|
+
var DEFAULT_CONFIG = {
|
|
3785
|
+
routing: "nest",
|
|
3786
|
+
view: "inertia",
|
|
3787
|
+
orm: "drizzle",
|
|
3788
|
+
database: {
|
|
3789
|
+
driver: "bun-sqlite",
|
|
3790
|
+
url: "app.db"
|
|
3791
|
+
},
|
|
3792
|
+
inertia: {
|
|
3793
|
+
frontend: "react",
|
|
3794
|
+
ssr: true,
|
|
3795
|
+
version: "1.0.0"
|
|
3796
|
+
},
|
|
3797
|
+
paths: {
|
|
3798
|
+
app: "app",
|
|
3799
|
+
controllers: "app/controllers",
|
|
3800
|
+
services: "app/services",
|
|
3801
|
+
modules: "app/modules",
|
|
3802
|
+
models: "app/models",
|
|
3803
|
+
migrations: "app/database/migrations",
|
|
3804
|
+
seeds: "db/seeds",
|
|
3805
|
+
middleware: "app/middleware",
|
|
3806
|
+
dto: "app/dto"
|
|
3807
|
+
},
|
|
3808
|
+
moduleStyle: "nest",
|
|
3809
|
+
auth: undefined,
|
|
3810
|
+
queue: undefined
|
|
3811
|
+
};
|
|
3812
|
+
var CONFIG_CANDIDATES = [
|
|
3813
|
+
"nx.config.ts",
|
|
3814
|
+
"nx.config.js",
|
|
3815
|
+
"nx.config.mjs",
|
|
3816
|
+
".nxrc.json"
|
|
3817
|
+
];
|
|
3818
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
3819
|
+
let config = {};
|
|
3820
|
+
let configSource = "<defaults>";
|
|
3821
|
+
for (const candidate of CONFIG_CANDIDATES) {
|
|
3822
|
+
const path = resolve23(cwd, candidate);
|
|
3823
|
+
if (!existsSync7(path))
|
|
3824
|
+
continue;
|
|
3825
|
+
try {
|
|
3826
|
+
if (candidate.endsWith(".json")) {
|
|
3827
|
+
const raw = readFileSync4(path, "utf8");
|
|
3828
|
+
config = JSON.parse(raw);
|
|
3829
|
+
} else {
|
|
3830
|
+
try {
|
|
3831
|
+
const mod = await import(path);
|
|
3832
|
+
config = mod.default ?? mod;
|
|
3833
|
+
} catch (importErr) {
|
|
3834
|
+
console.warn(`[nx] Could not dynamically import ${candidate}: ${importErr.message ?? importErr}. Falling back to defaults.`);
|
|
3835
|
+
config = {};
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
configSource = candidate;
|
|
3839
|
+
break;
|
|
3840
|
+
} catch (err) {
|
|
3841
|
+
throw new Error(`Failed to load ${candidate}: ${err.message ?? String(err)}`);
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
const merged = mergeWithEnv(DEFAULT_CONFIG, config);
|
|
3845
|
+
assertEnum("routing", merged.routing, [
|
|
3846
|
+
"nest",
|
|
3847
|
+
"adonis",
|
|
3848
|
+
"functional",
|
|
3849
|
+
"mixed"
|
|
3850
|
+
]);
|
|
3851
|
+
assertEnum("view", merged.view, ["rendu", "edge", "inertia", "none"]);
|
|
3852
|
+
assertEnum("orm", merged.orm, ["drizzle", "prisma", "kysely", "none"]);
|
|
3853
|
+
assertEnum("database.driver", merged.database.driver, [
|
|
3854
|
+
"bun-sqlite",
|
|
3855
|
+
"node-sqlite",
|
|
3856
|
+
"libsql",
|
|
3857
|
+
"postgres",
|
|
3858
|
+
"mysql",
|
|
3859
|
+
"none"
|
|
3860
|
+
]);
|
|
3861
|
+
assertEnum("inertia.frontend", merged.inertia.frontend, [
|
|
3862
|
+
"react",
|
|
3863
|
+
"vue",
|
|
3864
|
+
"svelte",
|
|
3865
|
+
"solid"
|
|
3866
|
+
]);
|
|
3867
|
+
if (process.env["NX_DEBUG"] === "1") {
|
|
3868
|
+
console.log(`[nx] config source: ${configSource}`);
|
|
3869
|
+
}
|
|
3870
|
+
return merged;
|
|
3871
|
+
}
|
|
3872
|
+
function mergeWithEnv(base, override) {
|
|
3873
|
+
const env = process.env;
|
|
3874
|
+
const merged = {
|
|
3875
|
+
...base,
|
|
3876
|
+
...override,
|
|
3877
|
+
database: { ...base.database, ...override.database ?? {} },
|
|
3878
|
+
inertia: { ...base.inertia, ...override.inertia ?? {} },
|
|
3879
|
+
paths: { ...base.paths, ...override.paths ?? {} }
|
|
3880
|
+
};
|
|
3881
|
+
if (env["NX_ROUTING"])
|
|
3882
|
+
merged.routing = env["NX_ROUTING"];
|
|
3883
|
+
if (env["NX_VIEW"])
|
|
3884
|
+
merged.view = env["NX_VIEW"];
|
|
3885
|
+
if (env["NX_ORM"])
|
|
3886
|
+
merged.orm = env["NX_ORM"];
|
|
3887
|
+
if (env["NX_DATABASE_DRIVER"])
|
|
3888
|
+
merged.database.driver = env["NX_DATABASE_DRIVER"];
|
|
3889
|
+
if (env["NX_DATABASE_URL"])
|
|
3890
|
+
merged.database.url = env["NX_DATABASE_URL"];
|
|
3891
|
+
if (env["NX_INERTIA_FRONTEND"])
|
|
3892
|
+
merged.inertia.frontend = env["NX_INERTIA_FRONTEND"];
|
|
3893
|
+
if (env["NX_INERTIA_SSR"])
|
|
3894
|
+
merged.inertia.ssr = env["NX_INERTIA_SSR"] !== "false" && env["NX_INERTIA_SSR"] !== "0";
|
|
3895
|
+
if (env["NX_INERTIA_VERSION"])
|
|
3896
|
+
merged.inertia.version = env["NX_INERTIA_VERSION"];
|
|
3897
|
+
return merged;
|
|
3898
|
+
}
|
|
3899
|
+
function assertEnum(key, value, allowed) {
|
|
3900
|
+
if (!allowed.includes(value)) {
|
|
3901
|
+
throw new Error(`Invalid value for ${key}: "${value}". Allowed: ${allowed.join(", ")}.`);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// packages/cli/src/core/args.ts
|
|
3906
|
+
var LONG_RE = /^--([^=]+)(?:=(.*))?$/;
|
|
3907
|
+
var SHORT_RE = /^-([A-Za-z])$/;
|
|
3908
|
+
function parseArgs(argv) {
|
|
3909
|
+
const positional = [];
|
|
3910
|
+
const flags = {};
|
|
3911
|
+
let endOfOptions = false;
|
|
3912
|
+
let i = 0;
|
|
3913
|
+
while (i < argv.length) {
|
|
3914
|
+
const arg = argv[i];
|
|
3915
|
+
if (arg === "--") {
|
|
3916
|
+
endOfOptions = true;
|
|
3917
|
+
i++;
|
|
3918
|
+
continue;
|
|
3919
|
+
}
|
|
3920
|
+
if (endOfOptions || !arg.startsWith("-")) {
|
|
3921
|
+
positional.push(arg);
|
|
3922
|
+
i++;
|
|
3923
|
+
continue;
|
|
3924
|
+
}
|
|
3925
|
+
const longMatch = LONG_RE.exec(arg);
|
|
3926
|
+
if (longMatch) {
|
|
3927
|
+
const [, name, inline] = longMatch;
|
|
3928
|
+
const flagName = name;
|
|
3929
|
+
if (inline !== undefined) {
|
|
3930
|
+
setFlag(flags, flagName, inline);
|
|
3931
|
+
i++;
|
|
3932
|
+
continue;
|
|
3933
|
+
}
|
|
3934
|
+
const next = argv[i + 1];
|
|
3935
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
3936
|
+
setFlag(flags, flagName, next);
|
|
3937
|
+
i += 2;
|
|
3938
|
+
} else {
|
|
3939
|
+
setFlag(flags, flagName, true);
|
|
3940
|
+
i++;
|
|
3941
|
+
}
|
|
3942
|
+
continue;
|
|
3943
|
+
}
|
|
3944
|
+
const shortMatch = SHORT_RE.exec(arg);
|
|
3945
|
+
if (shortMatch) {
|
|
3946
|
+
const flagName = shortMatch[1];
|
|
3947
|
+
const next = argv[i + 1];
|
|
3948
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
3949
|
+
setFlag(flags, flagName, next);
|
|
3950
|
+
i += 2;
|
|
3951
|
+
} else {
|
|
3952
|
+
setFlag(flags, flagName, true);
|
|
3953
|
+
i++;
|
|
3954
|
+
}
|
|
3955
|
+
continue;
|
|
3956
|
+
}
|
|
3957
|
+
positional.push(arg);
|
|
3958
|
+
i++;
|
|
3959
|
+
}
|
|
3960
|
+
const command = positional.shift();
|
|
3961
|
+
return { command, positional, flags };
|
|
3962
|
+
}
|
|
3963
|
+
function setFlag(flags, name, value) {
|
|
3964
|
+
if (name.startsWith("no-") && value === true) {
|
|
3965
|
+
const key = name.slice(3);
|
|
3966
|
+
flags[key] = false;
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
const existing = flags[name];
|
|
3970
|
+
if (existing === undefined) {
|
|
3971
|
+
flags[name] = value;
|
|
3972
|
+
} else if (Array.isArray(existing)) {
|
|
3973
|
+
existing.push(typeof value === "string" ? value : String(value));
|
|
3974
|
+
} else {
|
|
3975
|
+
flags[name] = [
|
|
3976
|
+
String(existing),
|
|
3977
|
+
typeof value === "string" ? value : String(value)
|
|
3978
|
+
];
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
function flagBool7(flags, name, fallback = false) {
|
|
3982
|
+
const v = flags[name];
|
|
3983
|
+
if (v === undefined)
|
|
3984
|
+
return fallback;
|
|
3985
|
+
if (typeof v === "boolean")
|
|
3986
|
+
return v;
|
|
3987
|
+
return v !== "false" && v !== "0" && v !== "no";
|
|
3988
|
+
}
|
|
3989
|
+
// packages/cli/src/core/logger.ts
|
|
3990
|
+
var USE_COLOR = process.env["NO_COLOR"] === undefined && process.env["FORCE_COLOR"] !== "0" && process.stdout.isTTY === true;
|
|
3991
|
+
var wrap = (open, close) => (s) => USE_COLOR ? `\x1B[${open}m${s}\x1B[${close}m` : s;
|
|
3992
|
+
var c = {
|
|
3993
|
+
reset: wrap(0, 0),
|
|
3994
|
+
bold: wrap(1, 22),
|
|
3995
|
+
dim: wrap(2, 22),
|
|
3996
|
+
red: wrap(31, 39),
|
|
3997
|
+
green: wrap(32, 39),
|
|
3998
|
+
yellow: wrap(33, 39),
|
|
3999
|
+
blue: wrap(34, 39),
|
|
4000
|
+
magenta: wrap(35, 39),
|
|
4001
|
+
cyan: wrap(36, 39),
|
|
4002
|
+
gray: wrap(90, 39)
|
|
4003
|
+
};
|
|
4004
|
+
var PREFIXES = {
|
|
4005
|
+
info: `${c.cyan("\u2139")}`,
|
|
4006
|
+
success: `${c.green("\u2714")}`,
|
|
4007
|
+
warn: `${c.yellow("\u26A0")}`,
|
|
4008
|
+
error: `${c.red("\u2716")}`,
|
|
4009
|
+
debug: `${c.gray("\xB7")}`,
|
|
4010
|
+
finger: `${c.magenta("\u279C")}`
|
|
4011
|
+
};
|
|
4012
|
+
|
|
4013
|
+
class Logger {
|
|
4014
|
+
verbose = false;
|
|
4015
|
+
setVerbose(v) {
|
|
4016
|
+
this.verbose = v;
|
|
4017
|
+
}
|
|
4018
|
+
info(message) {
|
|
4019
|
+
console.log(`${PREFIXES.info} ${message}`);
|
|
4020
|
+
}
|
|
4021
|
+
success(message) {
|
|
4022
|
+
console.log(`${PREFIXES.success} ${message}`);
|
|
4023
|
+
}
|
|
4024
|
+
warn(message) {
|
|
4025
|
+
console.warn(`${PREFIXES.warn} ${c.yellow(message)}`);
|
|
4026
|
+
}
|
|
4027
|
+
error(message) {
|
|
4028
|
+
console.error(`${PREFIXES.error} ${c.red(message)}`);
|
|
4029
|
+
}
|
|
4030
|
+
debug(message) {
|
|
4031
|
+
if (!this.verbose)
|
|
4032
|
+
return;
|
|
4033
|
+
console.log(`${PREFIXES.debug} ${c.gray(message)}`);
|
|
4034
|
+
}
|
|
4035
|
+
finger(message) {
|
|
4036
|
+
console.log(`${PREFIXES.finger} ${c.magenta(message)}`);
|
|
4037
|
+
}
|
|
4038
|
+
table(rows) {
|
|
4039
|
+
const labelWidth = Math.max(...rows.map(([l]) => l.length));
|
|
4040
|
+
for (const [label, value] of rows) {
|
|
4041
|
+
const padded = label.padEnd(labelWidth);
|
|
4042
|
+
console.log(` ${c.dim(padded)} ${value}`);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
heading(text) {
|
|
4046
|
+
const bar = "\u2500".repeat(text.length + 4);
|
|
4047
|
+
console.log(`
|
|
4048
|
+
${c.bold(c.cyan(bar))}`);
|
|
4049
|
+
console.log(`${c.bold(c.cyan(` ${text} `))}`);
|
|
4050
|
+
console.log(`${c.bold(c.cyan(bar))}
|
|
4051
|
+
`);
|
|
4052
|
+
}
|
|
4053
|
+
blank() {
|
|
4054
|
+
console.log("");
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
var logger23 = new Logger;
|
|
4058
|
+
var colors3 = c;
|
|
4059
|
+
// packages/cli/src/index.ts
|
|
4060
|
+
async function main() {
|
|
4061
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
4062
|
+
const verbose = flagBool7(parsed.flags, "verbose", false);
|
|
4063
|
+
logger23.setVerbose(verbose);
|
|
4064
|
+
if (parsed.flags["version"] === true) {
|
|
4065
|
+
console.log(PKG_VERSION);
|
|
4066
|
+
return 0;
|
|
4067
|
+
}
|
|
4068
|
+
if (parsed.flags["help"] === true || parsed.command === "help") {
|
|
4069
|
+
return renderHelp(parsed.positional[0]);
|
|
4070
|
+
}
|
|
4071
|
+
const command = parsed.command ? findCommand(parsed.command) : undefined;
|
|
4072
|
+
if (!command) {
|
|
4073
|
+
if (parsed.command) {
|
|
4074
|
+
logger23.error(`Unknown command: ${parsed.command}`);
|
|
4075
|
+
logger23.info(`Run \`nx help\` for a list of commands.`);
|
|
4076
|
+
return 1;
|
|
4077
|
+
}
|
|
4078
|
+
return renderHelp();
|
|
4079
|
+
}
|
|
4080
|
+
const cwd = process.cwd();
|
|
4081
|
+
const config2 = await loadConfig(cwd);
|
|
4082
|
+
return command.run({
|
|
4083
|
+
cwd,
|
|
4084
|
+
config: config2,
|
|
4085
|
+
positional: parsed.positional,
|
|
4086
|
+
flags: parsed.flags
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
function renderHelp(commandName) {
|
|
4090
|
+
if (commandName) {
|
|
4091
|
+
const cmd = findCommand(commandName);
|
|
4092
|
+
if (!cmd) {
|
|
4093
|
+
logger23.error(`Unknown command: ${commandName}`);
|
|
4094
|
+
return 1;
|
|
4095
|
+
}
|
|
4096
|
+
renderCommandHelp(cmd);
|
|
4097
|
+
return 0;
|
|
4098
|
+
}
|
|
4099
|
+
logger23.heading("nx \u2014 Nexus CLI");
|
|
4100
|
+
console.log(`
|
|
4101
|
+
${colors3.dim("Adonis ACE-style command runner for the NexusTS framework.")}
|
|
4102
|
+
|
|
4103
|
+
${colors3.bold("Usage")}
|
|
4104
|
+
nx <command> [args...]
|
|
4105
|
+
|
|
4106
|
+
${colors3.bold("Commands")}
|
|
4107
|
+
`);
|
|
4108
|
+
const nameWidth = Math.max(...commands.map((c2) => c2.name.length));
|
|
4109
|
+
for (const c2 of commands) {
|
|
4110
|
+
const padded = c2.name.padEnd(nameWidth);
|
|
4111
|
+
const aliasStr = c2.aliases?.length ? ` ${colors3.dim(`(${c2.aliases.join(", ")})`)}` : "";
|
|
4112
|
+
console.log(` ${colors3.cyan(padded)}${aliasStr} ${c2.summary}`);
|
|
4113
|
+
}
|
|
4114
|
+
console.log(`
|
|
4115
|
+
${colors3.bold("Global flags")}
|
|
4116
|
+
--help, -h Show help (or \`nx help <command>\`)
|
|
4117
|
+
--version, -v Print the CLI version
|
|
4118
|
+
--verbose Verbose output
|
|
4119
|
+
--no-color Disable ANSI color output
|
|
4120
|
+
|
|
4121
|
+
${colors3.bold("Examples")}
|
|
4122
|
+
${colors3.dim("nx new my-app")}
|
|
4123
|
+
${colors3.dim("nx init --style nest --view inertia --orm drizzle")}
|
|
4124
|
+
${colors3.dim("nx make:crud Post")}
|
|
4125
|
+
${colors3.dim("nx make:controller User")}
|
|
4126
|
+
${colors3.dim("nx make:migration create_users_table")}
|
|
4127
|
+
${colors3.dim("nx info")}
|
|
4128
|
+
${colors3.dim("nx route:list")}
|
|
4129
|
+
`);
|
|
4130
|
+
return 0;
|
|
4131
|
+
}
|
|
4132
|
+
function renderCommandHelp(cmd) {
|
|
4133
|
+
logger23.heading(cmd.name);
|
|
4134
|
+
if (cmd.aliases?.length) {
|
|
4135
|
+
console.log(` ${colors3.dim("aliases:")} ${cmd.aliases.join(", ")}`);
|
|
4136
|
+
}
|
|
4137
|
+
if (cmd.description)
|
|
4138
|
+
console.log(`
|
|
4139
|
+
${cmd.description}
|
|
4140
|
+
`);
|
|
4141
|
+
if (cmd.flags?.length) {
|
|
4142
|
+
console.log(colors3.bold(`
|
|
4143
|
+
Flags`));
|
|
4144
|
+
for (const f of cmd.flags) {
|
|
4145
|
+
const short = f.short ? `, -${f.short}` : "";
|
|
4146
|
+
const def = f.default !== undefined ? ` ${colors3.dim(`(default: ${String(f.default)})`)}` : "";
|
|
4147
|
+
console.log(` --${f.name}${short.padEnd(6)} ${f.description}${def}`);
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
if (cmd.examples?.length) {
|
|
4151
|
+
console.log(colors3.bold(`
|
|
4152
|
+
Examples`));
|
|
4153
|
+
for (const ex of cmd.examples) {
|
|
4154
|
+
console.log(` ${colors3.cyan(ex)}`);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
console.log();
|
|
4158
|
+
}
|
|
4159
|
+
var PKG_VERSION = "0.1.0";
|
|
4160
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
4161
|
+
logger23.error(err?.message ?? String(err));
|
|
4162
|
+
if (process.env["NX_DEBUG"] === "1" && err?.stack) {
|
|
4163
|
+
console.error(err.stack);
|
|
4164
|
+
}
|
|
4165
|
+
process.exit(1);
|
|
4166
|
+
});
|
|
4167
|
+
|
|
4168
|
+
//# debugId=D18D0C47D950221364756E2164756E21
|
|
4169
|
+
//# sourceMappingURL=index.js.map
|