create-nexwire 4.1.1 → 4.2.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.
Files changed (58) hide show
  1. package/express/gitignore-stub +31 -24
  2. package/express/package.json +3 -3
  3. package/express/src/database/migrations/sqlite/0000_init.sql +60 -60
  4. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  5. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  6. package/express/src/database/schema.ts +3 -3
  7. package/express/src/framework/maker-cli/db/index.mjs +1 -1
  8. package/express/src/framework/maker-cli/deploy/core.mjs +0 -1
  9. package/express/src/framework/maker-cli/index.mjs +13 -3
  10. package/express/src/framework/maker-cli/module/core.mjs +121 -43
  11. package/express/src/framework/maker-cli/runtime/core.mjs +26 -7
  12. package/express/src/framework/maker-cli/runtime/index.mjs +120 -104
  13. package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
  14. package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
  15. package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
  16. package/express/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
  17. package/express/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
  18. package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
  19. package/express/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
  20. package/express/src/framework/maker-cli/utils/help.mjs +374 -50
  21. package/express/src/framework/maker-cli/utils/naming.mjs +83 -20
  22. package/express/src/framework/maker-cli/utils/project-commands.mjs +140 -0
  23. package/express/src/framework/queue/queue.ts +402 -342
  24. package/express/src/modules/auth/database/seeders/role.ts +18 -18
  25. package/express/src/modules/auth/database/seeders/user.ts +35 -35
  26. package/express/src/storage/database/nexwire.sqlite +0 -0
  27. package/express/vitest.config.ts +38 -23
  28. package/hono/gitignore-stub +31 -24
  29. package/hono/package.json +3 -3
  30. package/hono/src/database/migrations/sqlite/0000_init.sql +60 -60
  31. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  32. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  33. package/hono/src/database/schema.ts +3 -3
  34. package/hono/src/framework/database/paginate.ts +253 -253
  35. package/hono/src/framework/maker-cli/db/index.mjs +1 -1
  36. package/hono/src/framework/maker-cli/deploy/core.mjs +0 -1
  37. package/hono/src/framework/maker-cli/index.mjs +13 -3
  38. package/hono/src/framework/maker-cli/module/core.mjs +116 -38
  39. package/hono/src/framework/maker-cli/runtime/core.mjs +24 -6
  40. package/hono/src/framework/maker-cli/runtime/index.mjs +120 -104
  41. package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
  42. package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
  43. package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
  44. package/hono/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
  45. package/hono/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
  46. package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
  47. package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
  48. package/hono/src/framework/maker-cli/utils/help.mjs +374 -50
  49. package/hono/src/framework/maker-cli/utils/naming.mjs +83 -20
  50. package/hono/src/framework/maker-cli/utils/project-commands.mjs +140 -0
  51. package/hono/src/framework/queue/queue.ts +402 -342
  52. package/hono/src/modules/auth/database/seeders/role.ts +18 -18
  53. package/hono/src/modules/auth/database/seeders/user.ts +35 -35
  54. package/hono/src/storage/database/nexwire.sqlite +0 -0
  55. package/hono/vitest.config.ts +38 -23
  56. package/package.json +1 -1
  57. package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -73
  58. package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -69
@@ -1,24 +1,31 @@
1
- .env
2
- node_modules/
3
- dist/
4
-
5
- public/*
6
- !public/.gitkeep
7
-
8
- deploy/.env
9
- deploy/server/.env
10
-
11
- *.log
12
- npm-debug.log*
13
- yarn-debug.log*
14
- yarn-error.log*
15
- pnpm-debug.log*
16
-
17
- .DS_Store
18
- Thumbs.db
19
-
20
- # Lock files — regenerated per developer's package manager
21
- bun.lock
22
- package-lock.json
23
- pnpm-lock.yaml
24
- yarn.lock
1
+ .env
2
+ node_modules/
3
+ dist/
4
+
5
+ # Coverage reports - regenerated by `npm run test:coverage`, ~130 HTML files
6
+ coverage/
7
+
8
+ public/*
9
+ !public/.gitkeep
10
+
11
+ deploy/.env
12
+ deploy/server/.env
13
+
14
+ *.log
15
+ npm-debug.log*
16
+ yarn-debug.log*
17
+ yarn-error.log*
18
+ pnpm-debug.log*
19
+
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # Lock files — regenerated per developer's package manager
24
+ # bun.lockb is the binary format used by bun before 1.2; bun.lock is the
25
+ # current text format. Both are ignored, so upgrading bun cannot turn a
26
+ # developer's local install into an untracked file.
27
+ bun.lock
28
+ bun.lockb
29
+ package-lock.json
30
+ pnpm-lock.yaml
31
+ yarn.lock
@@ -25,7 +25,7 @@
25
25
  "test": "vitest",
26
26
  "test:run": "vitest run",
27
27
  "test:ui": "vitest --ui",
28
- "test:coverage": "vitest --coverage"
28
+ "test:coverage": "vitest run --coverage"
29
29
  },
30
30
  "dependencies": {
31
31
  "@asteasolutions/zod-to-openapi": "^8.1.0",
@@ -73,7 +73,7 @@
73
73
  "pino": "^10.3.1",
74
74
  "postgres": "^3.4.9",
75
75
  "redis-commander": "^0.9.0",
76
- "sass-embedded": "^1.100.0",
76
+ "sass": "^1.100.0",
77
77
  "socket.io": "^4.8.3",
78
78
  "socket.io-client": "^4.8.3",
79
79
  "vite": "^8.2.1",
@@ -105,4 +105,4 @@
105
105
  "pnpm": ">=11",
106
106
  "yarn": ">=4"
107
107
  }
108
- }
108
+ }
@@ -1,61 +1,61 @@
1
- CREATE TABLE `notifications` (
2
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
- `user_id` integer NOT NULL,
4
- `type` text NOT NULL,
5
- `title` text NOT NULL,
6
- `body` text,
7
- `data` text,
8
- `link` text,
9
- `read_at` integer,
10
- `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
11
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
12
- );
13
- --> statement-breakpoint
14
- CREATE TABLE `roles` (
15
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
16
- `name` text NOT NULL,
17
- `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
18
- `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
19
- );
20
- --> statement-breakpoint
21
- CREATE UNIQUE INDEX `roles_name_unique` ON `roles` (`name`);--> statement-breakpoint
22
- CREATE TABLE `email_verification_tokens` (
23
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
24
- `email` text NOT NULL,
25
- `token` text NOT NULL,
26
- `expires_at` integer NOT NULL,
27
- `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
28
- );
29
- --> statement-breakpoint
30
- CREATE TABLE `password_reset_tokens` (
31
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
32
- `email` text NOT NULL,
33
- `token` text NOT NULL,
34
- `expires_at` integer NOT NULL,
35
- `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
36
- );
37
- --> statement-breakpoint
38
- CREATE TABLE `refresh_tokens` (
39
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
40
- `user_id` integer,
41
- `jti` text NOT NULL,
42
- `revoked` integer DEFAULT false,
43
- `expires_at` integer NOT NULL,
44
- `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
45
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE set null
46
- );
47
- --> statement-breakpoint
48
- CREATE UNIQUE INDEX `refresh_tokens_jti_unique` ON `refresh_tokens` (`jti`);--> statement-breakpoint
49
- CREATE TABLE `users` (
50
- `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
51
- `name` text NOT NULL,
52
- `email` text NOT NULL,
53
- `password` text NOT NULL,
54
- `role_id` integer,
55
- `email_verified_at` integer,
56
- `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
57
- `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
58
- FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE cascade ON DELETE set null
59
- );
60
- --> statement-breakpoint
1
+ CREATE TABLE `notifications` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `user_id` integer NOT NULL,
4
+ `type` text NOT NULL,
5
+ `title` text NOT NULL,
6
+ `body` text,
7
+ `data` text,
8
+ `link` text,
9
+ `read_at` integer,
10
+ `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
11
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
12
+ );
13
+ --> statement-breakpoint
14
+ CREATE TABLE `roles` (
15
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
16
+ `name` text NOT NULL,
17
+ `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
18
+ `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
19
+ );
20
+ --> statement-breakpoint
21
+ CREATE UNIQUE INDEX `roles_name_unique` ON `roles` (`name`);--> statement-breakpoint
22
+ CREATE TABLE `email_verification_tokens` (
23
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
24
+ `email` text NOT NULL,
25
+ `token` text NOT NULL,
26
+ `expires_at` integer NOT NULL,
27
+ `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
28
+ );
29
+ --> statement-breakpoint
30
+ CREATE TABLE `password_reset_tokens` (
31
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
32
+ `email` text NOT NULL,
33
+ `token` text NOT NULL,
34
+ `expires_at` integer NOT NULL,
35
+ `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
36
+ );
37
+ --> statement-breakpoint
38
+ CREATE TABLE `refresh_tokens` (
39
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
40
+ `user_id` integer,
41
+ `jti` text NOT NULL,
42
+ `revoked` integer DEFAULT false,
43
+ `expires_at` integer NOT NULL,
44
+ `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
45
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE set null
46
+ );
47
+ --> statement-breakpoint
48
+ CREATE UNIQUE INDEX `refresh_tokens_jti_unique` ON `refresh_tokens` (`jti`);--> statement-breakpoint
49
+ CREATE TABLE `users` (
50
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
51
+ `name` text NOT NULL,
52
+ `email` text NOT NULL,
53
+ `password` text NOT NULL,
54
+ `role_id` integer,
55
+ `email_verified_at` integer,
56
+ `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
57
+ `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
58
+ FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE cascade ON DELETE set null
59
+ );
60
+ --> statement-breakpoint
61
61
  CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "6",
3
3
  "dialect": "sqlite",
4
- "id": "54057efa-2d33-41f3-aa7a-1f1a3e89a382",
4
+ "id": "3b93a28d-5009-4c71-8925-e0e6b5caef6e",
5
5
  "prevId": "00000000-0000-0000-0000-000000000000",
6
6
  "tables": {
7
7
  "notifications": {
@@ -5,7 +5,7 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "6",
8
- "when": 1790961550566,
8
+ "when": 1791141133457,
9
9
  "tag": "0000_init",
10
10
  "breakpoints": true
11
11
  }
@@ -1,3 +1,3 @@
1
- export * from "../modules/auth/database/models/notification.js";
2
- export * from "../modules/auth/database/models/role.js";
3
- export * from "../modules/auth/database/models/user.js";
1
+ export * from "../modules/auth/database/models/notification.js";
2
+ export * from "../modules/auth/database/models/role.js";
3
+ export * from "../modules/auth/database/models/user.js";
@@ -42,7 +42,7 @@ function assertName(value, label) {
42
42
 
43
43
  function unsupported() {
44
44
  console.error(
45
- 'Drizzle Kit does not support Laravel-style rollback/refresh migrations. Use "npm run maker db:fresh -- --seed" (or "bun maker db:fresh --seed") for a SQLite development reset, or manage rollback SQL manually for production databases.'
45
+ 'Drizzle Kit does not support rollback/refresh migrations. Use "npm run maker db:fresh -- --seed" (or "bun maker db:fresh --seed") for a SQLite development reset, or manage rollback SQL manually for production databases.'
46
46
  );
47
47
  process.exit(1);
48
48
  }
@@ -69,7 +69,6 @@ const STUBS = {
69
69
  },
70
70
  controller: {
71
71
  openapi: "controller/openapi.ts.stub",
72
- openapiWithModel: "controller/openapi.with-model.ts.stub",
73
72
  plain: "controller/plain.ts.stub",
74
73
  schema: {
75
74
  openapi: "controller/schema.ts.stub",
@@ -6,7 +6,8 @@ import { registerDbCommands } from "./db/index.mjs";
6
6
  import { registerMiddlewareCommands } from "./middleware/index.mjs";
7
7
  import { registerModuleCommands } from "./module/index.mjs";
8
8
  import { registerRuntimeCommands } from "./runtime/index.mjs";
9
- import { makerCommandPrefix, showHelp } from "./utils/help.mjs";
9
+ import { configureGroupedHelp, makerCommandPrefix, showHelp } from "./utils/help.mjs";
10
+ import { registerProjectCommands } from "./utils/project-commands.mjs";
10
11
 
11
12
  const args = process.argv.slice(2);
12
13
  const [command] = args;
@@ -33,8 +34,11 @@ try {
33
34
  .name(namePrefix ? `${namePrefix} maker` : "maker")
34
35
  .allowUnknownOption(true)
35
36
  .allowExcessArguments(true)
36
- .helpOption("-h, --help")
37
- .addHelpText("beforeAll", "nexwire maker\n");
37
+ .helpOption("-h, --help");
38
+
39
+ // Grouped command list. Must be set before anything prints help. Per-command
40
+ // help is untouched, so `maker <command> --help` still uses commander's layout.
41
+ configureGroupedHelp(program);
38
42
 
39
43
  registerMiddlewareCommands(program, args);
40
44
  registerModuleCommands(program, args);
@@ -42,6 +46,12 @@ try {
42
46
  registerDbCommands(program, args);
43
47
  registerRuntimeCommands(program, args);
44
48
 
49
+ // The project's own commands come last, so a built-in always wins a name
50
+ // clash. Absent file is silent and free; a file that is present but broken
51
+ // throws, and the catch below reports it and exits rather than leaving the
52
+ // developer with commands that silently do not exist.
53
+ await registerProjectCommands(program, args);
54
+
45
55
  if (!command) {
46
56
  showHelp(program);
47
57
  } else {
@@ -5,7 +5,7 @@ import { drizzleGenerateArgs, ensureDatabaseDirectory, ensureMigrationMeta, sync
5
5
  import { detectDialect, openApiEnabled } from "../utils/env-db.mjs";
6
6
  import { writeFiles } from "../utils/file-ops.mjs";
7
7
  import { hasFlag } from "../utils/flags.mjs";
8
- import { assertName, camelCase, pascal } from "../utils/naming.mjs";
8
+ import { assertName, camelCase, normalizeName, pascal, plural, snakeCase } from "../utils/naming.mjs";
9
9
  import { packageScript, runNodeScript } from "../utils/process.mjs";
10
10
 
11
11
  const stubsRoot = path.resolve(import.meta.dirname, "../stubs");
@@ -18,7 +18,6 @@ const STUBS = {
18
18
  },
19
19
  controller: {
20
20
  openapi: "controller/openapi.ts.stub",
21
- openapiWithModel: "controller/openapi.with-model.ts.stub",
22
21
  plain: "controller/plain.ts.stub",
23
22
  standalone: "controller/standalone.ts.stub"
24
23
  },
@@ -36,7 +35,8 @@ const STUBS = {
36
35
  }
37
36
  },
38
37
  seeder: {
39
- named: "seeder/name.ts.stub"
38
+ named: "seeder/name.ts.stub",
39
+ standalone: "seeder/standalone.ts.stub"
40
40
  },
41
41
  service: {
42
42
  named: "service/name.ts.stub",
@@ -185,6 +185,35 @@ function resolveModuleRef(rawModule, ...flagGroups) {
185
185
  return { moduleName, leaf: leafName(moduleName) };
186
186
  }
187
187
 
188
+ /**
189
+ * Find an existing module directory whose folder name spells the requested
190
+ * module differently, only in the separators used (`my_module` vs `my-module`).
191
+ *
192
+ * Names are canonicalised to kebab-case, so a module scaffolded before this
193
+ * happened can live in a folder the canonical name no longer resolves to.
194
+ * Without this lookup, normalising the name would lock a developer out of their
195
+ * own module. Matching is segment by segment so a panel path such as
196
+ * `admin/reporting` keeps working, and each candidate is a real directory
197
+ * inside src/modules, so the result cannot escape it.
198
+ */
199
+ async function resolveExistingModuleDir(moduleName) {
200
+ let current = path.resolve(process.cwd(), "src/modules");
201
+ for (const segment of moduleName.split("/").filter(Boolean)) {
202
+ let entries;
203
+ try {
204
+ entries = await fs.readdir(current, { withFileTypes: true });
205
+ } catch {
206
+ return "";
207
+ }
208
+ const directories = entries.filter((entry) => entry.isDirectory());
209
+ const exact = directories.find((entry) => entry.name === segment);
210
+ const loose = exact ?? directories.find((entry) => normalizeName(entry.name) === normalizeName(segment));
211
+ if (!loose) return "";
212
+ current = path.join(current, loose.name);
213
+ }
214
+ return current;
215
+ }
216
+
188
217
  /** Ensure a module directory exists, throwing a helpful error if not. */
189
218
  async function assertModuleExists(moduleName) {
190
219
  const root = moduleRoot(moduleName);
@@ -192,6 +221,8 @@ async function assertModuleExists(moduleName) {
192
221
  try {
193
222
  stats = await fs.stat(root);
194
223
  } catch {
224
+ const legacy = await resolveExistingModuleDir(moduleName);
225
+ if (legacy) return legacy;
195
226
  throw new Error(`Module does not exist: ${moduleName}. Create it first with: bun maker module:make ${moduleName}`);
196
227
  }
197
228
  if (!stats.isDirectory()) {
@@ -200,16 +231,30 @@ async function assertModuleExists(moduleName) {
200
231
  return root;
201
232
  }
202
233
 
203
- /** Build the placeholder substitution map shared by module file generators. */
234
+ /**
235
+ * Build the placeholder substitution map shared by module file generators.
236
+ *
237
+ * The same developer-supplied name reaches four different places in a stub, and
238
+ * each needs a different shape: import paths use the kebab file name, the
239
+ * service identifier is camelCase, the table identifier is camelCase plural and
240
+ * the SQL table name is snake_case plural. Deriving all of them from one
241
+ * canonical name is what keeps a multi-word name from landing in an identifier
242
+ * position as `game-configService`, which parses as subtraction.
243
+ */
204
244
  function moduleFileVars(moduleName, controller) {
205
- return {
206
- module: moduleName,
207
- controller,
208
- ClassName: pascal(controller),
209
- name: leafName(moduleName),
210
- tableVariable: `${controller}s`
211
- };
212
- }
245
+ const base = normalizeName(controller);
246
+ return {
247
+ module: moduleName,
248
+ // File name on disk and therefore every import path that points at it.
249
+ controller: base,
250
+ // Identifier the stubs export and call.
251
+ ServiceName: `${camelCase(base)}Service`,
252
+ ClassName: pascal(base),
253
+ name: leafName(moduleName),
254
+ tableName: plural(snakeCase(base)),
255
+ tableVariable: plural(camelCase(base))
256
+ };
257
+ }
213
258
 
214
259
  /**
215
260
  * Generate a controller file for a module.
@@ -244,8 +289,7 @@ async function serviceFile(moduleName, controllerName, withModel) {
244
289
  async function moduleFiles(moduleName, openApi) {
245
290
  const name = leafName(moduleName);
246
291
  const vars = moduleFileVars(moduleName, name);
247
- // The with-model controller variant only exists for the openapi style.
248
- const controllerStub = openApi ? STUBS.controller.openapiWithModel : STUBS.controller.plain;
292
+ const controllerStub = openApi ? STUBS.controller.openapi : STUBS.controller.plain;
249
293
  return {
250
294
  "facade.ts": await stub(STUBS.facade.named, vars),
251
295
  [`controllers/${name}.ts`]: await stub(controllerStub, vars),
@@ -286,25 +330,28 @@ return await stub(routeStub, {
286
330
 
287
331
  /** Generate a named model file for a module. */
288
332
  async function namedModelTemplate(moduleName, name, dialect) {
333
+ const base = normalizeName(name);
289
334
  return await stub(STUBS.model.named[dialect], {
290
335
  module: moduleName,
291
- name,
292
- controller: name,
293
- ClassName: pascal(name),
294
- tableName: `${name}s`,
295
- tableVariable: `${name}s`
336
+ name: base,
337
+ controller: base,
338
+ ClassName: pascal(base),
339
+ tableName: plural(snakeCase(base)),
340
+ tableVariable: plural(camelCase(base))
296
341
  });
297
342
  }
298
343
 
299
344
  /** Generate a seeder file for a module model. */
300
345
  async function namedSeederTemplate(moduleName, modelName, className) {
346
+ const base = normalizeName(modelName);
301
347
  return await stub(STUBS.seeder.named, {
302
348
  module: moduleName,
303
- name: modelName,
349
+ // Import path: the model file is named after the model, in kebab-case.
350
+ name: base,
304
351
  controller: className,
305
352
  ClassName: pascal(className),
306
- tableName: `${modelName}s`,
307
- tableVariable: `${modelName}s`
353
+ tableName: plural(snakeCase(base)),
354
+ tableVariable: plural(camelCase(base))
308
355
  });
309
356
  }
310
357
 
@@ -717,7 +764,16 @@ export async function makeModel(rawModule, rawNameOrFlag, extraFlags = []) {
717
764
  await writeFileSafe(modelPath, await namedModelTemplate(moduleName, name, dialect), flags, "Model file");
718
765
  }
719
766
 
720
- /** Generate a seeder file for an existing module model. */
767
+ /**
768
+ * Generate a seeder file.
769
+ *
770
+ * A seeder does not have to own the table it writes to, and does not have to
771
+ * write to only one table, so a missing model is not fatal. When the model is
772
+ * there we import it and pre-wire the insert; when it is not we emit the
773
+ * standalone shape and let the developer choose what to write. That is the
774
+ * same trade the controller makes when there is no service, and the service
775
+ * makes when there is no model. Seeder was the only generator that refused.
776
+ */
721
777
  export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
722
778
  const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
723
779
  const flags = [];
@@ -728,36 +784,58 @@ export async function makeSeeder(rawModule, rawNameOrFlag, extraFlags = []) {
728
784
  }
729
785
  flags.push(...extraFlags);
730
786
  const root = await assertModuleExists(moduleName);
731
- let modelName = name;
732
- let modelPath = path.join(root, "database", "models", `${modelName}.ts`);
733
-
734
- try {
735
- await fs.access(modelPath);
736
- } catch {
737
- // Fall back to the module's own model. This must use the leaf, not
738
- // moduleName: for a panel module moduleName is "admin/testme", so using it
739
- // would probe database/models/admin/testme.ts and never match.
740
- modelName = leaf;
741
- modelPath = path.join(root, "database", "models", `${modelName}.ts`);
742
787
 
743
- try {
744
- await fs.access(modelPath);
788
+ // The fallback probes the leaf, not moduleName: for a panel module moduleName
789
+ // is "admin/testme", so using it would look for database/models/admin/testme.ts
790
+ // and never match.
791
+ const modelName = await existingModelName(root, name, leaf);
792
+ let seeder;
793
+ if (modelName) {
794
+ seeder = await namedSeederTemplate(moduleName, modelName, name);
795
+ if (modelName !== name) {
745
796
  console.log(`Model '${name}' not found, using module model '${modelName}' for seeder '${name}'.`);
746
- } catch {
747
- const panel = panelOf(moduleName);
748
- const suggest = panel ? `${leaf} ${name} --path=${panel}` : `${leaf} ${name}`;
749
- throw new Error(
750
- `Model not found for seeder: src/modules/${moduleName}/database/models/${name}.ts. Create it first with: bun maker module:make-model ${suggest}`
751
- );
752
797
  }
798
+ } else {
799
+ seeder = await stub(STUBS.seeder.standalone, {
800
+ module: moduleName,
801
+ name,
802
+ ClassName: pascal(name),
803
+ // The stub shows this as the conventional table name, so derive it the
804
+ // same way the bound stub does - it is what a model of this name exports.
805
+ tableVariable: plural(camelCase(name))
806
+ });
807
+ const available = await listModels(root);
808
+ console.log(`No model '${name}' in module ${moduleName}, so this is a standalone seeder: it imports no model and writes nothing until you fill it in.`);
809
+ console.log(
810
+ available.length
811
+ ? ` Models in this module: ${available.join(", ")}. Run module:make-seeder ${moduleName} <one of these> for a seeder with the model already imported.`
812
+ : ` This module has no models yet. Create one with: bun maker module:make-model ${moduleName} ${name}`
813
+ );
753
814
  }
754
815
 
755
- const seeder = await namedSeederTemplate(moduleName, modelName, name);
756
816
  await writeFileSafe(path.join(root, `database/seeders/${name}.ts`), seeder, flags, "Seeder file");
757
817
 
758
818
  console.log(`Seeder ready: ${moduleName}/${name}`);
759
819
  }
760
820
 
821
+ /** The model a seeder binds to: the requested one, else the module's own. Null when neither exists. */
822
+ async function existingModelName(root, name, leaf) {
823
+ for (const candidate of [name, leaf]) {
824
+ if (await pathExists(path.join(root, "database", "models", `${candidate}.ts`))) return candidate;
825
+ }
826
+ return null;
827
+ }
828
+
829
+ /** Model file names in a module, without the extension. Empty when there are none. */
830
+ async function listModels(root) {
831
+ try {
832
+ const entries = await fs.readdir(path.join(root, "database", "models"));
833
+ return entries.filter((f) => f.endsWith(".ts")).map((f) => f.slice(0, -3));
834
+ } catch {
835
+ return [];
836
+ }
837
+ }
838
+
761
839
  /** Generate a job file for an existing module. */
762
840
  export async function makeJob(rawModule, rawNameOrFlag, extraFlags = []) {
763
841
  const { moduleName, leaf } = resolveModuleRef(rawModule, rawNameOrFlag, extraFlags);
@@ -120,7 +120,7 @@ function serveRedisUnavailablePage(port, endpoint) {
120
120
  </html>
121
121
  `;
122
122
 
123
- return new Promise((_resolve, reject) => {
123
+ return new Promise((resolve, reject) => {
124
124
  const server = http.createServer((_req, res) => {
125
125
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
126
126
  res.end(body);
@@ -498,24 +498,42 @@ export async function runVite(configPath) {
498
498
  await runCommand(localBin("vite"), ["--config", configPath]);
499
499
  }
500
500
 
501
- /** Run Vitest tests (backend). */
501
+ /**
502
+ * Strip the argument separator npm inserts.
503
+ *
504
+ * `npm run maker test -- posts` reaches us as `test posts`, but
505
+ * `npm run maker test -- -- posts` reaches us as `test -- posts`, because npm
506
+ * eats one separator and forwards the other. Vitest reads a bare `--` as a
507
+ * positional filter that matches no test file, which silently runs the whole
508
+ * suite instead of the one you asked for - so the command appears to work while
509
+ * doing something else. Dropping a leading `--` makes both spellings behave the
510
+ * same, and makes the passthrough behave identically across package managers.
511
+ */
512
+ function vitestArgs(rawArgs) {
513
+ return rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
514
+ }
515
+
516
+ /** Run Vitest tests once (backend). */
502
517
  export async function runTest(rawArgs = []) {
503
- await runCommand(localBin("vitest"), rawArgs);
518
+ // "run" is required. Bare `vitest` is watch mode, which never exits - so
519
+ // without this, `maker test` hung in any interactive terminal while CI and
520
+ // piped runs exited normally, and the two looked identical from a log.
521
+ await runCommand(localBin("vitest"), ["run", ...vitestArgs(rawArgs)]);
504
522
  }
505
523
 
506
524
  /** Run Vitest in watch mode (backend). */
507
525
  export async function runTestWatch(rawArgs = []) {
508
- await runCommand(localBin("vitest"), rawArgs);
526
+ await runCommand(localBin("vitest"), vitestArgs(rawArgs));
509
527
  }
510
528
 
511
- /** Run Vitest with coverage (backend). */
529
+ /** Run Vitest once with coverage (backend). */
512
530
  export async function runTestCoverage(rawArgs = []) {
513
- await runCommand(localBin("vitest"), ["--coverage", ...rawArgs]);
531
+ await runCommand(localBin("vitest"), ["run", "--coverage", ...vitestArgs(rawArgs)]);
514
532
  }
515
533
 
516
534
  /** Run Vitest in UI mode (backend). */
517
535
  export async function runTestUI(rawArgs = []) {
518
- await runCommand(localBin("vitest"), ["--ui", ...rawArgs]);
536
+ await runCommand(localBin("vitest"), ["--ui", ...vitestArgs(rawArgs)]);
519
537
  }
520
538
 
521
539
  /** Clear Vite cache directories. */
@@ -540,3 +558,4 @@ export async function clearViteCache() {
540
558
 
541
559
  console.log("Vite cache cleared.");
542
560
  }
561
+