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,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. */
|
|
@@ -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
|
+
}
|