create-nexwire 4.1.2 → 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.
- package/express/gitignore-stub +31 -24
- package/express/package.json +2 -2
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
- package/express/src/database/migrations/sqlite/meta/_journal.json +12 -12
- package/express/src/framework/maker-cli/db/index.mjs +1 -1
- package/express/src/framework/maker-cli/deploy/core.mjs +0 -1
- package/express/src/framework/maker-cli/index.mjs +13 -3
- package/express/src/framework/maker-cli/module/core.mjs +121 -43
- package/express/src/framework/maker-cli/runtime/core.mjs +26 -7
- package/express/src/framework/maker-cli/runtime/index.mjs +120 -104
- package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
- package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
- package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
- package/express/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
- package/express/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
- package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
- package/express/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
- package/express/src/framework/maker-cli/utils/help.mjs +374 -50
- package/express/src/framework/maker-cli/utils/naming.mjs +83 -20
- package/express/src/framework/maker-cli/utils/project-commands.mjs +140 -0
- package/express/src/framework/queue/queue.ts +402 -342
- package/express/src/modules/auth/database/seeders/role.ts +18 -18
- package/express/src/modules/auth/database/seeders/user.ts +35 -35
- package/express/vitest.config.ts +38 -23
- package/hono/gitignore-stub +31 -24
- package/hono/package.json +2 -2
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +411 -411
- package/hono/src/database/migrations/sqlite/meta/_journal.json +12 -12
- package/hono/src/framework/database/paginate.ts +253 -253
- package/hono/src/framework/maker-cli/db/index.mjs +1 -1
- package/hono/src/framework/maker-cli/deploy/core.mjs +0 -1
- package/hono/src/framework/maker-cli/index.mjs +13 -3
- package/hono/src/framework/maker-cli/module/core.mjs +116 -38
- package/hono/src/framework/maker-cli/runtime/core.mjs +24 -6
- package/hono/src/framework/maker-cli/runtime/index.mjs +120 -104
- package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +6 -6
- package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +6 -6
- package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +1 -1
- package/hono/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -19
- package/hono/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
- package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +2 -2
- package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +1 -1
- package/hono/src/framework/maker-cli/utils/help.mjs +374 -50
- package/hono/src/framework/maker-cli/utils/naming.mjs +83 -20
- package/hono/src/framework/maker-cli/utils/project-commands.mjs +140 -0
- package/hono/src/framework/queue/queue.ts +402 -342
- package/hono/src/modules/auth/database/seeders/role.ts +18 -18
- package/hono/src/modules/auth/database/seeders/user.ts +35 -35
- package/hono/vitest.config.ts +38 -23
- package/package.json +1 -1
- package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -73
- package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +0 -69
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import { db } from "@/framework/facade.js";
|
|
2
|
-
import { {{tableVariable}} } from "@/modules/{{module}}/database/models/{{name}}.js";
|
|
3
|
-
|
|
4
|
-
export const table = {{tableVariable}};
|
|
5
|
-
|
|
6
|
-
export default async function {{ClassName}}Seeder() {
|
|
7
|
-
const rows = [
|
|
8
|
-
// Add your seed data here
|
|
9
|
-
// Example:
|
|
10
|
-
// { name: "{{ClassName}} 1", description: "This is the first {{name}}" },
|
|
11
|
-
// { name: "{{ClassName}} 2", description: "This is the second {{name}}" }
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
for (const row of rows) {
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
console.log("{{ClassName}} seeder completed");
|
|
19
|
-
}
|
|
1
|
+
import { db } from "@/framework/facade.js";
|
|
2
|
+
import { {{tableVariable}} } from "@/modules/{{module}}/database/models/{{name}}.js";
|
|
3
|
+
|
|
4
|
+
export const table = {{tableVariable}};
|
|
5
|
+
|
|
6
|
+
export default async function {{ClassName}}Seeder() {
|
|
7
|
+
const rows = [
|
|
8
|
+
// Add your seed data here
|
|
9
|
+
// Example:
|
|
10
|
+
// { name: "{{ClassName}} 1", description: "This is the first {{name}}" },
|
|
11
|
+
// { name: "{{ClassName}} 2", description: "This is the second {{name}}" }
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (const row of rows) await db.insert({{tableVariable}}).values(row);
|
|
15
|
+
|
|
16
|
+
console.log("{{ClassName}}: " + rows.length + " row(s) prepared");
|
|
17
|
+
}
|
|
20
18
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone seeder for "{{name}}" — no model of that name exists in module
|
|
3
|
+
* "{{module}}", so nothing was imported for you. Uncomment the two lines below,
|
|
4
|
+
* point them at the table you want, then add your rows.
|
|
5
|
+
*
|
|
6
|
+
* Run with: bun maker module:seed {{module}}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { db } from "@/framework/facade.js";
|
|
10
|
+
// import { {{tableVariable}} } from "@/modules/{{module}}/database/models/{{name}}.js";
|
|
11
|
+
// export const table = {{tableVariable}};
|
|
12
|
+
|
|
13
|
+
export default async function {{ClassName}}Seeder() {
|
|
14
|
+
const rows: Record<string, unknown>[] = [
|
|
15
|
+
// { name: "{{ClassName}} 1" },
|
|
16
|
+
// { name: "{{ClassName}} 2" }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// for (const row of rows) await db.insert({{tableVariable}}).values(row);
|
|
20
|
+
|
|
21
|
+
console.log("{{ClassName}}: " + rows.length + " row(s) ready");
|
|
22
|
+
}
|
|
@@ -10,10 +10,10 @@ import { db } from "@/framework/facade.js";
|
|
|
10
10
|
* import { {{tableVariable}} } from "@/modules/{{module}}/database/models/{{controller}}.js";
|
|
11
11
|
* return db.select().from({{tableVariable}});
|
|
12
12
|
*/
|
|
13
|
-
export const {{
|
|
13
|
+
export const {{ServiceName}} = {
|
|
14
14
|
/**
|
|
15
15
|
* Why: CRUD service for {{controller}}.
|
|
16
|
-
* When: Consumed directly by the controller as `{{
|
|
16
|
+
* When: Consumed directly by the controller as `{{ServiceName}}.list()`, etc.
|
|
17
17
|
* Where: Wired into the module's controller handlers.
|
|
18
18
|
*/
|
|
19
19
|
list: async () => {
|
|
@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm";
|
|
|
2
2
|
import { db } from "@/framework/facade.js";
|
|
3
3
|
import { {{tableVariable}} } from "@/modules/{{module}}/database/models/{{controller}}.js";
|
|
4
4
|
|
|
5
|
-
export const {{
|
|
5
|
+
export const {{ServiceName}} = {
|
|
6
6
|
/**
|
|
7
7
|
* Why: Returns every {{controller}} row for list screens.
|
|
8
8
|
* When: Called by the controller's index() handler.
|
|
@@ -1,50 +1,374 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Why: Detects the package manager used to run the CLI, for help usage text.
|
|
3
|
-
* When: Building the program name shown in the "Usage:" line.
|
|
4
|
-
* Where: Maker CLI entry flow.
|
|
5
|
-
* How: Reads npm config user-agent and maps to the corresponding prefix string.
|
|
6
|
-
*/
|
|
7
|
-
export function makerCommandPrefix() {
|
|
8
|
-
const ua = String(process.env.npm_config_user_agent || "").toLowerCase();
|
|
9
|
-
if (ua.startsWith("pnpm/")) return "pnpm";
|
|
10
|
-
if (ua.startsWith("yarn/")) return "yarn";
|
|
11
|
-
if (ua.startsWith("bun/")) return "bun";
|
|
12
|
-
if (ua.startsWith("npm/")) return "npm run";
|
|
13
|
-
return "";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Why: Overrides Commander's help so the subcommand listing shows the actual
|
|
18
|
-
* long flags (e.g. `--app-only|--server-only|--dev`) instead of a generic
|
|
19
|
-
* `[options]` marker.
|
|
20
|
-
* When: Building the CLI program before commands are registered.
|
|
21
|
-
* Where: Maker CLI entry flow.
|
|
22
|
-
* How: Injects a custom `subcommandTerm` via `configureHelp`.
|
|
23
|
-
*/
|
|
24
|
-
export function configureDeployHelp(program) {
|
|
25
|
-
program.configureHelp({
|
|
26
|
-
subcommandTerm(cmd) {
|
|
27
|
-
const flags = cmd.options
|
|
28
|
-
.map((option) => option.flags.split(" ")[0])
|
|
29
|
-
.filter((flag) => flag.startsWith("--"))
|
|
30
|
-
.join("|");
|
|
31
|
-
const args = cmd.registeredArguments.map((arg) => humanReadableArgName(arg)).join(" ");
|
|
32
|
-
return cmd.name() + (flags ? ` [${flags}]` : "") + (args ? ` ${args}` : "");
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function humanReadableArgName(arg) {
|
|
38
|
-
const nameOutput = arg.name() + (arg.variadic === true ? "..." : "");
|
|
39
|
-
return arg.required ? `<${nameOutput}>` : `[${nameOutput}]`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Why: Shows commander-generated help for the maker CLI program.
|
|
44
|
-
* When: CLI runs without a valid command.
|
|
45
|
-
* Where: Maker CLI entry flow and error handlers.
|
|
46
|
-
* How: Delegates to commander's outputHelp for consistent automatically-synced output.
|
|
47
|
-
*/
|
|
48
|
-
export function showHelp(program) {
|
|
49
|
-
program.outputHelp();
|
|
50
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Why: Detects the package manager used to run the CLI, for help usage text.
|
|
3
|
+
* When: Building the program name shown in the "Usage:" line.
|
|
4
|
+
* Where: Maker CLI entry flow.
|
|
5
|
+
* How: Reads npm config user-agent and maps to the corresponding prefix string.
|
|
6
|
+
*/
|
|
7
|
+
export function makerCommandPrefix() {
|
|
8
|
+
const ua = String(process.env.npm_config_user_agent || "").toLowerCase();
|
|
9
|
+
if (ua.startsWith("pnpm/")) return "pnpm";
|
|
10
|
+
if (ua.startsWith("yarn/")) return "yarn";
|
|
11
|
+
if (ua.startsWith("bun/")) return "bun";
|
|
12
|
+
if (ua.startsWith("npm/")) return "npm run";
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Why: Overrides Commander's help so the subcommand listing shows the actual
|
|
18
|
+
* long flags (e.g. `--app-only|--server-only|--dev`) instead of a generic
|
|
19
|
+
* `[options]` marker.
|
|
20
|
+
* When: Building the CLI program before commands are registered.
|
|
21
|
+
* Where: Maker CLI entry flow.
|
|
22
|
+
* How: Injects a custom `subcommandTerm` via `configureHelp`.
|
|
23
|
+
*/
|
|
24
|
+
export function configureDeployHelp(program) {
|
|
25
|
+
program.configureHelp({
|
|
26
|
+
subcommandTerm(cmd) {
|
|
27
|
+
const flags = cmd.options
|
|
28
|
+
.map((option) => option.flags.split(" ")[0])
|
|
29
|
+
.filter((flag) => flag.startsWith("--"))
|
|
30
|
+
.join("|");
|
|
31
|
+
const args = cmd.registeredArguments.map((arg) => humanReadableArgName(arg)).join(" ");
|
|
32
|
+
return cmd.name() + (flags ? ` [${flags}]` : "") + (args ? ` ${args}` : "");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function humanReadableArgName(arg) {
|
|
38
|
+
const nameOutput = arg.name() + (arg.variadic === true ? "..." : "");
|
|
39
|
+
return arg.required ? `<${nameOutput}>` : `[${nameOutput}]`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Why: Shows commander-generated help for the maker CLI program.
|
|
44
|
+
* When: CLI runs without a valid command.
|
|
45
|
+
* Where: Maker CLI entry flow and error handlers.
|
|
46
|
+
* How: Delegates to commander's outputHelp for consistent automatically-synced output.
|
|
47
|
+
*/
|
|
48
|
+
export function showHelp(program) {
|
|
49
|
+
program.outputHelp();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Marks a command as coming from the project's own maker/commands.mjs, so the
|
|
54
|
+
* help screen can put it beside `maker:init` instead of in the fallback section.
|
|
55
|
+
* Set by registerProjectCommands, read by groupCommands below.
|
|
56
|
+
*/
|
|
57
|
+
export const PROJECT_COMMAND_FLAG = "makerProjectCommand";
|
|
58
|
+
|
|
59
|
+
/** Where a command nobody classified ends up. Better labelled than missing. */
|
|
60
|
+
export const FALLBACK_GROUP_TITLE = "Other commands";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Why: Groups the top-level command list so a developer can find a command by
|
|
64
|
+
* the question they are asking rather than by scrolling 57 flat lines.
|
|
65
|
+
* When: `maker --help`, and the help commander prints for an unknown command.
|
|
66
|
+
* Where: Maker CLI help rendering, applied by configureGroupedHelp.
|
|
67
|
+
* How: Explicit name lists rather than prefix patterns. A `module:make-*`
|
|
68
|
+
* pattern would also swallow `module:migrate` and `module:make-notification`
|
|
69
|
+
* into the generators section, and a command filed under the wrong heading
|
|
70
|
+
* is worse than an extra line of configuration here. The cost is that a new
|
|
71
|
+
* command must be added here too - which is the intent, because
|
|
72
|
+
* __tests__/help.test.ts fails when it is not, so nothing can be quietly
|
|
73
|
+
* dropped from the screen.
|
|
74
|
+
*
|
|
75
|
+
* Names only, no argument or option markers. Commander's default listing
|
|
76
|
+
* prints `[options] <module> <name>`, which pushed the description column
|
|
77
|
+
* to 47 characters and left almost no room to say what anything did. That
|
|
78
|
+
* detail is still one command away: `maker <command> --help`.
|
|
79
|
+
*/
|
|
80
|
+
const COMMAND_GROUPS = [
|
|
81
|
+
{
|
|
82
|
+
title: "Getting started",
|
|
83
|
+
commands: ["dev", "serve", "ui:dev", "admin:dev", "maildev:view", "redis:view", "vite:cache:clear"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: "Generators - global",
|
|
87
|
+
// Deliberately separated from the module generators. `middleware:make`
|
|
88
|
+
// writes to src/middlewares and applies to the whole app;
|
|
89
|
+
// `module:make-middleware` writes inside one module and applies only to it.
|
|
90
|
+
// In the same section the two look interchangeable, which is exactly the
|
|
91
|
+
// mistake a developer is about to make.
|
|
92
|
+
commands: ["middleware:make"]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: "Generators - modules",
|
|
96
|
+
commands: [
|
|
97
|
+
"module:make",
|
|
98
|
+
"module:make-notification",
|
|
99
|
+
"module:make-model",
|
|
100
|
+
"module:make-schema",
|
|
101
|
+
"module:make-service",
|
|
102
|
+
"module:make-controller",
|
|
103
|
+
"module:make-route",
|
|
104
|
+
"module:make-job",
|
|
105
|
+
"module:make-console",
|
|
106
|
+
"module:make-seeder",
|
|
107
|
+
"module:make-test",
|
|
108
|
+
"module:make-helper",
|
|
109
|
+
"module:make-middleware",
|
|
110
|
+
"module:make-type"
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
title: "Modules - manage",
|
|
115
|
+
commands: [
|
|
116
|
+
"module:list",
|
|
117
|
+
"module:seed",
|
|
118
|
+
"module:migrate",
|
|
119
|
+
"module:delete",
|
|
120
|
+
"module:delete-notification",
|
|
121
|
+
"module:trash:clean"
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
title: "Database",
|
|
126
|
+
commands: [
|
|
127
|
+
"db",
|
|
128
|
+
"db:schema",
|
|
129
|
+
"db:generate",
|
|
130
|
+
"db:migrate",
|
|
131
|
+
"db:migrate:run",
|
|
132
|
+
"db:migrate:reset",
|
|
133
|
+
"db:fresh",
|
|
134
|
+
"db:seed",
|
|
135
|
+
"db:module:seed",
|
|
136
|
+
"db:status",
|
|
137
|
+
"db:push",
|
|
138
|
+
"db:check",
|
|
139
|
+
"db:studio",
|
|
140
|
+
"db:reset",
|
|
141
|
+
"db:wipe"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
title: "Queue and scheduler",
|
|
146
|
+
commands: ["queue:work", "queue:clear", "schedule:work"]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
title: "Deploy",
|
|
150
|
+
commands: [
|
|
151
|
+
"deploy:init",
|
|
152
|
+
"deploy:db:import",
|
|
153
|
+
"deploy:db:import:remote",
|
|
154
|
+
"deploy:workflow",
|
|
155
|
+
"deploy:workflow:remote",
|
|
156
|
+
"deploy:workflow:promote"
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
title: "Testing",
|
|
161
|
+
commands: ["test", "test:watch", "test:coverage", "test:ui"]
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
title: "Your commands",
|
|
165
|
+
commands: ["maker:init"],
|
|
166
|
+
includeProjectCommands: true
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
/** Widest name column before descriptions start on their own space anyway. */
|
|
171
|
+
const MAX_TERM_WIDTH = 30;
|
|
172
|
+
const MIN_DESCRIPTION_WIDTH = 28;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Why: Sorts commands into their display sections.
|
|
176
|
+
* When: Rendering `maker --help`, and asserted directly by the tests.
|
|
177
|
+
* Where: Maker CLI help rendering.
|
|
178
|
+
* How: Walks COMMAND_GROUPS in declared order and claims each named command.
|
|
179
|
+
* Anything left over goes to a fallback section rather than disappearing,
|
|
180
|
+
* so a newly added command is visible even in the moment before anyone
|
|
181
|
+
* decides where it belongs.
|
|
182
|
+
*/
|
|
183
|
+
export function groupCommands(commands) {
|
|
184
|
+
const visible = commands.filter((command) => !command._hidden);
|
|
185
|
+
const claimed = new Set();
|
|
186
|
+
const groups = [];
|
|
187
|
+
|
|
188
|
+
for (const group of COMMAND_GROUPS) {
|
|
189
|
+
const members = [];
|
|
190
|
+
|
|
191
|
+
for (const name of group.commands) {
|
|
192
|
+
const command = visible.find((candidate) => candidate.name() === name && !claimed.has(name));
|
|
193
|
+
if (command) {
|
|
194
|
+
members.push(command);
|
|
195
|
+
claimed.add(name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (group.includeProjectCommands) {
|
|
200
|
+
for (const command of visible) {
|
|
201
|
+
if (command[PROJECT_COMMAND_FLAG] && !claimed.has(command.name())) {
|
|
202
|
+
members.push(command);
|
|
203
|
+
claimed.add(command.name());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (members.length > 0) {
|
|
209
|
+
groups.push({ title: group.title, commands: members });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const leftovers = visible.filter((command) => !claimed.has(command.name()));
|
|
214
|
+
if (leftovers.length > 0) {
|
|
215
|
+
groups.push({ title: FALLBACK_GROUP_TITLE, commands: leftovers });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return groups;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Why: The plain-text header, and the ASCII wordmark used when there is room.
|
|
223
|
+
* When: Rendering the top of `maker --help`.
|
|
224
|
+
* Where: Maker CLI help rendering.
|
|
225
|
+
* How: The wordmark is a fixed literal. figlet was used to design it offline and
|
|
226
|
+
* is deliberately not a dependency: the CLI runs from source inside the
|
|
227
|
+
* user's project, so a runtime figlet would mean every scaffolded project
|
|
228
|
+
* installs it purely to print five fixed lines. Nothing is computed here.
|
|
229
|
+
*
|
|
230
|
+
* Three rules keep it from getting in the developer's way, which is the
|
|
231
|
+
* only thing a banner has to be careful about:
|
|
232
|
+
*
|
|
233
|
+
* 1. A terminal narrower than the art gets plain text, because art that
|
|
234
|
+
* overflows wraps into an unreadable mess.
|
|
235
|
+
* 2. Piped output gets plain text. Nobody asked for a wordmark at the top
|
|
236
|
+
* of their log file, and `maker > out.txt` should stay readable.
|
|
237
|
+
* 3. Per-command help gets no banner at all - see configureGroupedHelp.
|
|
238
|
+
*/
|
|
239
|
+
const NEXWIRE_WORDMARK = [
|
|
240
|
+
" _",
|
|
241
|
+
" ____ ___ _ ___ __(_)_______",
|
|
242
|
+
" / __ \\/ _ \\| |/_/ | /| / / / ___/ _ \\",
|
|
243
|
+
" / / / / __/> < | |/ |/ / / / / __/",
|
|
244
|
+
"/_/ /_/\\___/_/|_| |__/|__/_/_/ \\___/"
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const PLAIN_HEADER = "nexwire maker";
|
|
248
|
+
|
|
249
|
+
/** Widest wordmark line, measured from the literal above. */
|
|
250
|
+
const WORDMARK_WIDTH = Math.max(...NEXWIRE_WORDMARK.map((line) => line.length));
|
|
251
|
+
|
|
252
|
+
/** Two columns of breathing room, so the art never touches the window edge. */
|
|
253
|
+
const MINIMUM_TERMINAL_WIDTH = WORDMARK_WIDTH + 2;
|
|
254
|
+
|
|
255
|
+
export function makerBanner(stream = process.stdout) {
|
|
256
|
+
if (!stream.isTTY) return PLAIN_HEADER;
|
|
257
|
+
if ((stream.columns || 80) < MINIMUM_TERMINAL_WIDTH) return PLAIN_HEADER;
|
|
258
|
+
return NEXWIRE_WORDMARK.join("\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Why: Replaces the top-level help screen with the grouped one.
|
|
263
|
+
* When: Once, while building the program, before anything prints help.
|
|
264
|
+
* Where: Maker CLI entry flow, alongside the other program configuration.
|
|
265
|
+
* How: Overrides commander's formatHelp and rebuilds the screen from the
|
|
266
|
+
* documented Help helpers (commandUsage, visibleOptions, subcommandTerm).
|
|
267
|
+
* Deliberately not string surgery on commander's output: that would depend
|
|
268
|
+
* on the exact shape of its "Commands:" heading and break on a version bump.
|
|
269
|
+
* Per-command help is left to commander, so `maker <command> --help` is
|
|
270
|
+
* unchanged and carries no wordmark.
|
|
271
|
+
*/
|
|
272
|
+
export function configureGroupedHelp(program, { stream = process.stdout } = {}) {
|
|
273
|
+
program.configureHelp({
|
|
274
|
+
formatHelp(cmd, helper) {
|
|
275
|
+
const width = helper.helpWidth || stream.columns || 100;
|
|
276
|
+
// commandUsage returns the bare "maker [options] [command]"; the heading
|
|
277
|
+
// word is styled separately by commander and is not part of that string.
|
|
278
|
+
const blocks = [];
|
|
279
|
+
|
|
280
|
+
// cmd.parent is null only for the top-level program, which is what makes
|
|
281
|
+
// the wordmark an overview-only flourish rather than something on screen
|
|
282
|
+
// every time someone checks a flag.
|
|
283
|
+
if (!cmd.parent) blocks.push(makerBanner(stream));
|
|
284
|
+
|
|
285
|
+
blocks.push(`Usage: ${helper.commandUsage(cmd)}`);
|
|
286
|
+
|
|
287
|
+
const description = helper.commandDescription(cmd);
|
|
288
|
+
if (description) blocks.push(description);
|
|
289
|
+
|
|
290
|
+
const options = helper.visibleOptions(cmd);
|
|
291
|
+
if (options.length > 0) {
|
|
292
|
+
blocks.push(renderTable("Options", options.map((option) => [helper.optionTerm(option), helper.optionDescription(option)]), width));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const argumentsList = helper.visibleArguments(cmd);
|
|
296
|
+
if (argumentsList.length > 0) {
|
|
297
|
+
blocks.push(
|
|
298
|
+
renderTable(
|
|
299
|
+
"Arguments",
|
|
300
|
+
argumentsList.map((argument) => [helper.argumentTerm(argument), helper.argumentDescription(argument)]),
|
|
301
|
+
width
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const group of groupCommands(cmd.commands)) {
|
|
307
|
+
blocks.push(
|
|
308
|
+
renderTable(
|
|
309
|
+
group.title,
|
|
310
|
+
group.commands.map((command) => [command.name(), helper.subcommandDescription(command)]),
|
|
311
|
+
width
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Commander adds its own `help [command]` lazily, after this formatter has
|
|
317
|
+
// already run, so the command is working but cannot be listed here. Rather
|
|
318
|
+
// than drop it silently, name both spellings in the footer - and prefer
|
|
319
|
+
// that to a heading containing one leftover entry, which reads as a
|
|
320
|
+
// mistake. __tests__/help.test.ts asserts `maker help <command>` works.
|
|
321
|
+
blocks.push("Run `maker <command> --help`, or `maker help <command>`, for the options and arguments of one command.");
|
|
322
|
+
|
|
323
|
+
return `${blocks.join("\n\n")}\n`;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Renders one `Heading:` block of `name description` rows. Exported for tests. */
|
|
329
|
+
export function renderTable(heading, rows, width) {
|
|
330
|
+
const termWidth = Math.min(MAX_TERM_WIDTH, Math.max(0, ...rows.map(([term]) => term.length)));
|
|
331
|
+
const descriptionWidth = Math.max(MIN_DESCRIPTION_WIDTH, width - termWidth - 4);
|
|
332
|
+
|
|
333
|
+
const lines = [`${heading}:`];
|
|
334
|
+
|
|
335
|
+
for (const [term, description] of rows) {
|
|
336
|
+
const gap = Math.max(2, termWidth - term.length + 2);
|
|
337
|
+
// A continuation line must start where the description started, not at the
|
|
338
|
+
// left edge. That column is 2 (indent) + termWidth (widest term) + gap.
|
|
339
|
+
const descriptionColumn = 2 + termWidth + gap;
|
|
340
|
+
const indent = " ".repeat(descriptionColumn);
|
|
341
|
+
|
|
342
|
+
if (!description) {
|
|
343
|
+
lines.push(` ${term}`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const wrapped = wrapText(description, descriptionWidth);
|
|
348
|
+
lines.push(` ${term}${" ".repeat(gap)}${wrapped[0]}`);
|
|
349
|
+
for (const line of wrapped.slice(1)) lines.push(`${indent}${line}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return lines.join("\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Greedy word wrap. Keeps a long description readable instead of one huge line. */
|
|
356
|
+
function wrapText(text, width) {
|
|
357
|
+
const words = String(text).split(/\s+/).filter(Boolean);
|
|
358
|
+
if (words.length === 0) return [""];
|
|
359
|
+
|
|
360
|
+
const lines = [];
|
|
361
|
+
let line = "";
|
|
362
|
+
|
|
363
|
+
for (const word of words) {
|
|
364
|
+
if (line === "") line = word;
|
|
365
|
+
else if (line.length + 1 + word.length <= width) line += ` ${word}`;
|
|
366
|
+
else {
|
|
367
|
+
lines.push(line);
|
|
368
|
+
line = word;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (line !== "") lines.push(line);
|
|
372
|
+
|
|
373
|
+
return lines;
|
|
374
|
+
}
|
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
38
|
-
if (
|
|
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
|
-
|
|
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. */
|