create-nexwire 4.1.2 → 4.2.1

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 (54) hide show
  1. package/express/gitignore-stub +31 -24
  2. package/express/package.json +2 -2
  3. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
  4. package/express/src/database/migrations/sqlite/meta/_journal.json +12 -12
  5. package/express/src/framework/http/router.ts +16 -2
  6. package/express/src/framework/maker-cli/db/index.mjs +1 -1
  7. package/express/src/framework/maker-cli/deploy/core.mjs +0 -1
  8. package/express/src/framework/maker-cli/index.mjs +13 -3
  9. package/express/src/framework/maker-cli/module/core.mjs +121 -43
  10. package/express/src/framework/maker-cli/runtime/core.mjs +26 -7
  11. package/express/src/framework/maker-cli/runtime/index.mjs +120 -104
  12. package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
  13. package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
  14. package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
  15. package/express/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
  16. package/express/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
  17. package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
  18. package/express/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
  19. package/express/src/framework/maker-cli/utils/help.mjs +374 -50
  20. package/express/src/framework/maker-cli/utils/naming.mjs +83 -20
  21. package/express/src/framework/maker-cli/utils/project-commands.mjs +140 -0
  22. package/express/src/framework/queue/queue.ts +402 -342
  23. package/express/src/modules/auth/database/seeders/role.ts +18 -18
  24. package/express/src/modules/auth/database/seeders/user.ts +35 -35
  25. package/express/vitest.config.ts +38 -23
  26. package/hono/gitignore-stub +31 -24
  27. package/hono/package.json +2 -2
  28. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
  29. package/hono/src/database/migrations/sqlite/meta/_journal.json +12 -12
  30. package/hono/src/framework/database/paginate.ts +253 -253
  31. package/hono/src/framework/http/router.ts +35 -14
  32. package/hono/src/framework/maker-cli/db/index.mjs +1 -1
  33. package/hono/src/framework/maker-cli/deploy/core.mjs +0 -1
  34. package/hono/src/framework/maker-cli/index.mjs +13 -3
  35. package/hono/src/framework/maker-cli/module/core.mjs +116 -38
  36. package/hono/src/framework/maker-cli/runtime/core.mjs +24 -6
  37. package/hono/src/framework/maker-cli/runtime/index.mjs +120 -104
  38. package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
  39. package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
  40. package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
  41. package/hono/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
  42. package/hono/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
  43. package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
  44. package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
  45. package/hono/src/framework/maker-cli/utils/help.mjs +374 -50
  46. package/hono/src/framework/maker-cli/utils/naming.mjs +83 -20
  47. package/hono/src/framework/maker-cli/utils/project-commands.mjs +140 -0
  48. package/hono/src/framework/queue/queue.ts +402 -342
  49. package/hono/src/modules/auth/database/seeders/role.ts +18 -18
  50. package/hono/src/modules/auth/database/seeders/user.ts +35 -35
  51. package/hono/vitest.config.ts +38 -23
  52. package/package.json +1 -1
  53. package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -73
  54. package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -69
@@ -1,32 +1,93 @@
1
+ /**
2
+ * Name handling for the generators.
3
+ *
4
+ * A developer may type a name in any common casing: `game-config`,
5
+ * `game_config`, `gameConfig` or `GameConfig`. All of them must resolve to the
6
+ * same thing, because a generator writes the name into four different places
7
+ * that each need a different shape:
8
+ *
9
+ * file name game-config.ts kebab
10
+ * SQL table game_configs snake
11
+ * identifier gameConfigs camel
12
+ * class name GameConfigSeeder pascal
13
+ *
14
+ * `normalizeName` produces the single canonical form (kebab). The other three
15
+ * are derived from it. This has to happen *before* lowercasing, because
16
+ * lowercasing `gameConfig` to `gameconfig` destroys the word boundary and no
17
+ * later step can recover it.
18
+ */
19
+
1
20
  /**
2
21
  * A name is safe when it can be used as a folder name, a URL segment, a table
3
22
  * name and a TypeScript identifier without further quoting.
4
23
  *
5
- * Kebab-case, snake_case and camelCase all pass: `pascal` already folds `-` and
6
- * `_`, and names are lowercased before this test. What fails is anything that
7
- * would produce uncompilable code - spaces and dots break the generated
8
- * `@/modules/...` import, a leading digit breaks the identifier.
24
+ * Kebab-case, snake_case, camelCase and PascalCase all pass: they are folded
25
+ * to one canonical kebab form first. What fails is anything that would produce
26
+ * uncompilable code - spaces and dots break the generated `@/modules/...`
27
+ * import, a leading digit breaks the identifier.
28
+ */
29
+ const RAW_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
30
+
31
+ /**
32
+ * Fold any accepted casing to canonical kebab-case.
33
+ *
34
+ * `gameConfig` and `GameConfig` split on the lowercase/uppercase boundary;
35
+ * `game_config` splits on the underscore; repeated separators collapse to one
36
+ * dash. An all-caps run such as `GAMECONFIG` carries no boundary information
37
+ * and is deliberately kept as the single word `gameconfig`.
38
+ */
39
+ export function normalizeName(value) {
40
+ return String(value)
41
+ .trim()
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
43
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
44
+ .replace(/[^A-Za-z0-9]+/g, "-")
45
+ .replace(/-{2,}/g, "-")
46
+ .replace(/^-+|-+$/g, "")
47
+ .toLowerCase();
48
+ }
49
+
50
+ /** Convert a kebab-case or camelCase string to snake_case (the SQL table name). */
51
+ export function snakeCase(input) {
52
+ return String(input)
53
+ .trim()
54
+ .replace(/[^A-Za-z0-9]+/g, "_")
55
+ .replace(/_{2,}/g, "_")
56
+ .replace(/^_+|_+$/g, "")
57
+ .toLowerCase();
58
+ }
59
+
60
+ /**
61
+ * Pluralise an identifier for a table/collection name.
62
+ *
63
+ * Deliberately minimal: consonant + `y` becomes `ies` (so `auditEntry` yields
64
+ * `auditEntries`, matching the export a developer would hand-write), anything
65
+ * else takes a plain `s`. A word that already ends in `s` is returned
66
+ * unchanged so the helper is idempotent. English irregulars (`person` ->
67
+ * `people`) are not modelled; correct those by hand after generating.
9
68
  */
10
- const NAME_PATTERN = /^[a-z][a-z0-9_-]*$/;
69
+ export function plural(word) {
70
+ const value = String(word);
71
+ if (!value) return value;
72
+ if (/[^aeiou]y$/i.test(value)) return `${value.slice(0, -1)}ies`;
73
+ if (/s$/i.test(value)) return value;
74
+ return `${value}s`;
75
+ }
11
76
 
12
77
  /**
13
78
  * Best-effort readable repair of a rejected name, used only in error text.
14
- * Returns "" when no safe suggestion exists (e.g. a leading digit cannot be
79
+ * Returns "" when no safe suggestion exists (e.g., a leading digit cannot be
15
80
  * reordered without guessing the developer's intent).
16
81
  */
17
82
  function suggestName(value) {
18
- const cleaned = value
19
- .trim()
20
- .toLowerCase()
21
- .replace(/[^a-z0-9_-]+/g, "-")
22
- .replace(/-{2,}/g, "-")
23
- .replace(/^[-_]+|[-_]+$/g, "");
83
+ const cleaned = normalizeName(value);
24
84
  if (!cleaned || !/^[a-z]/.test(cleaned)) return "";
25
85
  return cleaned;
26
86
  }
27
87
 
28
88
  /**
29
- * Validate that a name value exists and is safe to use.
89
+ * Validate a name value exists and is safe to use, and return its canonical
90
+ * form so every generator writes the same name to disk.
30
91
  * Throws with the exact command to run instead when the value is unusable.
31
92
  */
32
93
  export function assertName(value, label) {
@@ -34,14 +95,16 @@ export function assertName(value, label) {
34
95
  throw new Error(`${label} is required.`);
35
96
  }
36
97
 
37
- const name = value.trim().toLowerCase();
38
- if (NAME_PATTERN.test(name)) return name;
98
+ const trimmed = value.trim();
99
+ if (!RAW_PATTERN.test(trimmed)) {
100
+ const suggestion = suggestName(value);
101
+ const reason = /^[0-9]/.test(trimmed)
102
+ ? "must start with a lowercase letter"
103
+ : "may only contain lowercase letters, numbers, dashes and underscores";
104
+ throw new Error(`Invalid ${label.toLowerCase()}: "${value}" - a name ${reason}.${suggestion ? ` Use "${suggestion}" instead.` : ""}`);
105
+ }
39
106
 
40
- const suggestion = suggestName(value);
41
- const reason = /^[0-9]/.test(name)
42
- ? "must start with a lowercase letter"
43
- : "may only contain lowercase letters, numbers, dashes and underscores";
44
- throw new Error(`Invalid ${label.toLowerCase()}: "${value}" - a name ${reason}.${suggestion ? ` Use "${suggestion}" instead.` : ""}`);
107
+ return normalizeName(trimmed);
45
108
  }
46
109
 
47
110
  /** Convert a kebab-case or snake_case string to PascalCase. */
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import fsPromises from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { writeFileAlways, writeFileIfMissing } from "./file-ops.mjs";
6
+ import { PROJECT_COMMAND_FLAG } from "./help.mjs";
7
+
8
+ /**
9
+ * Where a project keeps its own maker commands.
10
+ *
11
+ * Deliberately outside `src/`. Anything under `src/framework/` is copied into
12
+ * the published package by `scripts/sync-template.mjs`, so a developer edit
13
+ * there would be overwritten on the next framework update - and `src/modules/`
14
+ * is swept by the route, job and seeder globs. The project root is the one
15
+ * place nothing else owns.
16
+ */
17
+ export const PROJECT_COMMANDS_PATH = "maker/commands.mjs";
18
+
19
+ const PROJECT_COMMANDS_TEMPLATE = `/**
20
+ * Your project's maker commands.
21
+ *
22
+ * This file is yours. Nothing in nexwire writes to it, and no framework update
23
+ * will touch it. Whatever you export here is registered on the maker CLI
24
+ * alongside the built-in commands:
25
+ *
26
+ * maker example:hello run your command
27
+ * maker --help every command, yours included
28
+ *
29
+ * \`program\` is the same commander object the built-in commands are built with,
30
+ * so \`.command()\`, \`.description()\`, \`.option()\` and \`.action()\` behave exactly
31
+ * as they do in nexwire's own commands.
32
+ *
33
+ * Three things worth knowing:
34
+ *
35
+ * - A name that collides with a framework command is ignored, and you are told
36
+ * which one. Rename yours; the built-in stays.
37
+ * - An error in this file is reported, never swallowed. If your commands stop
38
+ * appearing, the fault is here rather than in your command code.
39
+ * - Import your own code with a normal relative path, e.g.
40
+ * import { sendInvoice } from "../src/invoices/send.js";
41
+ *
42
+ * Replace or delete the example below once you have a command of your own.
43
+ */
44
+
45
+ export default function register(program) {
46
+ program
47
+ .command("example:hello <name>")
48
+ .description("Example command - replace or delete this")
49
+ .option("--loud", "Shout the greeting")
50
+ .action((name, options) => {
51
+ const greeting = \`Hello, \${name}!\`;
52
+ console.log(options.loud ? greeting.toUpperCase() : greeting);
53
+ });
54
+ }
55
+ `;
56
+
57
+ /**
58
+ * Why: Registers the commands a project defined for itself.
59
+ * When: Every maker invocation, right after the framework's own registrars.
60
+ * Where: Maker CLI entry flow, from utils/project-commands.mjs.
61
+ * How: Imports maker/commands.mjs resolved from process.cwd() - not from
62
+ * import.meta.dirname, which points inside node_modules and would find the
63
+ * framework's own directory instead of the project. Absent is silent and
64
+ * free; broken is loud, because a developer's commands silently not
65
+ * existing is a far worse experience than an error message. A name that
66
+ * collides with a framework command is refused by commander itself, which
67
+ * is the right outcome: the built-in wins and the project cannot shadow a
68
+ * command it did not write. That refusal is re-thrown here with the file
69
+ * name attached, because commander's own message does not say which file
70
+ * asked for the name.
71
+ */
72
+ export async function registerProjectCommands(program, rawArgs, cwd = process.cwd()) {
73
+ const file = path.resolve(cwd, PROJECT_COMMANDS_PATH);
74
+
75
+ if (!fs.existsSync(file)) {
76
+ return { loaded: false, file, added: [] };
77
+ }
78
+
79
+ // Not wrapped in a bare try/catch on purpose. A file that exists but fails to
80
+ // load has to say so, or the developer goes looking for a bug in their own
81
+ // command that is actually in this loader. The catches below only attach the
82
+ // file name, because an error from inside the project file otherwise reads as
83
+ // if it came from the framework.
84
+ let module;
85
+ try {
86
+ module = await import(pathToFileURL(file).href);
87
+ } catch (error) {
88
+ throw new Error(`Could not load ${PROJECT_COMMANDS_PATH}: ${error?.message || error}`, { cause: error });
89
+ }
90
+
91
+ const register = module.default ?? module.register;
92
+
93
+ if (typeof register !== "function") {
94
+ throw new TypeError(
95
+ `${PROJECT_COMMANDS_PATH} must export a default function (program, args) => void. Found: ${typeof register}.`
96
+ );
97
+ }
98
+
99
+ const before = new Set(program.commands);
100
+
101
+ try {
102
+ await register(program, rawArgs);
103
+ } catch (error) {
104
+ throw new Error(`Could not register commands from ${PROJECT_COMMANDS_PATH}: ${error?.message || error}`, {
105
+ cause: error
106
+ });
107
+ }
108
+
109
+ const added = [];
110
+
111
+ for (const command of program.commands) {
112
+ if (before.has(command)) continue;
113
+ // Tagged so `maker --help` files these under "Your commands" beside
114
+ // `maker:init`, rather than in the section for unclassified commands.
115
+ command[PROJECT_COMMAND_FLAG] = true;
116
+ added.push(command.name());
117
+ }
118
+
119
+ return { loaded: true, file, added };
120
+ }
121
+
122
+ /**
123
+ * Why: Creates maker/commands.mjs so a project can add its own commands.
124
+ * When: A developer runs `maker maker:init`.
125
+ * Where: utils/project-commands.mjs.
126
+ * How: Writes the template with the same no-clobber default as every other
127
+ * generator, so an existing file is left alone unless --force is given.
128
+ */
129
+ export async function initProjectCommands(cwd = process.cwd(), { force = false } = {}) {
130
+ const file = path.resolve(cwd, PROJECT_COMMANDS_PATH);
131
+
132
+ if (force) {
133
+ await writeFileAlways(file, PROJECT_COMMANDS_TEMPLATE);
134
+ return { file, created: false, overwritten: true };
135
+ }
136
+
137
+ await fsPromises.mkdir(path.dirname(file), { recursive: true });
138
+ const created = await writeFileIfMissing(file, PROJECT_COMMANDS_TEMPLATE);
139
+ return { file, created, overwritten: false };
140
+ }