create-nexwire 4.2.0 → 4.2.2
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/src/framework/http/router.ts +16 -2
- package/express/src/framework/maker-cli/stubs/seeder/name.ts.stub +1 -1
- package/express/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +1 -1
- package/express/src/framework/maker-cli/utils/project-commands.mjs +73 -12
- package/express/vitest.config.ts +7 -7
- package/hono/src/framework/http/router.ts +35 -14
- package/hono/src/framework/maker-cli/stubs/seeder/name.ts.stub +1 -1
- package/hono/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +1 -1
- package/hono/src/framework/maker-cli/utils/project-commands.mjs +73 -12
- package/hono/vitest.config.ts +7 -7
- package/package.json +2 -2
|
@@ -127,8 +127,16 @@ export function createRouter(): NexwireRouter {
|
|
|
127
127
|
const nativeRoute = router.route.bind(router);
|
|
128
128
|
states.set(router, { routes: [], children: [] });
|
|
129
129
|
|
|
130
|
+
// Group middlewares are kept here rather than registered with router.use(),
|
|
131
|
+
// because a catch-all use() also matches requests whose route lives in a
|
|
132
|
+
// sibling router or in the parent. They are merged into every .api() call
|
|
133
|
+
// instead, so they travel with the routes they were declared on.
|
|
134
|
+
const groupMiddlewares: RequestHandler[] = [];
|
|
135
|
+
|
|
130
136
|
router.group = function (...middlewares: RequestHandler[]) {
|
|
131
|
-
|
|
137
|
+
for (const middleware of middlewares) {
|
|
138
|
+
groupMiddlewares.push(middleware);
|
|
139
|
+
}
|
|
132
140
|
return this;
|
|
133
141
|
};
|
|
134
142
|
|
|
@@ -140,7 +148,13 @@ export function createRouter(): NexwireRouter {
|
|
|
140
148
|
throw new Error("api(route, middlewares, handler) requires a handler function");
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
(router as any)[route.method](
|
|
151
|
+
(router as any)[route.method](
|
|
152
|
+
expressPath(route.path),
|
|
153
|
+
validationMiddleware(route),
|
|
154
|
+
...groupMiddlewares,
|
|
155
|
+
...middlewares,
|
|
156
|
+
finalHandler
|
|
157
|
+
);
|
|
144
158
|
stateOf(router).routes.push(route);
|
|
145
159
|
return this;
|
|
146
160
|
};
|
|
@@ -13,6 +13,6 @@ export default async function {{ClassName}}Seeder() {
|
|
|
13
13
|
|
|
14
14
|
for (const row of rows) await db.insert({{tableVariable}}).values(row);
|
|
15
15
|
|
|
16
|
-
console.log(
|
|
16
|
+
console.log(`{{ClassName}}: ${rows.length} row(s) prepared`);
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -18,5 +18,5 @@ export default async function {{ClassName}}Seeder() {
|
|
|
18
18
|
|
|
19
19
|
// for (const row of rows) await db.insert({{tableVariable}}).values(row);
|
|
20
20
|
|
|
21
|
-
console.log(
|
|
21
|
+
console.log(`{{ClassName}}: ${rows.length} row(s) ready`);
|
|
22
22
|
}
|
|
@@ -13,8 +13,16 @@ import { PROJECT_COMMAND_FLAG } from "./help.mjs";
|
|
|
13
13
|
* there would be overwritten on the next framework update - and `src/modules/`
|
|
14
14
|
* is swept by the route, job and seeder globs. The project root is the one
|
|
15
15
|
* place nothing else owns.
|
|
16
|
+
*
|
|
17
|
+
* Two names are read: commands.mjs (plain JavaScript, loaded by plain node)
|
|
18
|
+
* and commands.ts (TypeScript, loaded through tsx). The .mjs is the incumbent
|
|
19
|
+
* and wins if a project keeps both.
|
|
16
20
|
*/
|
|
17
21
|
export const PROJECT_COMMANDS_PATH = "maker/commands.mjs";
|
|
22
|
+
export const PROJECT_COMMANDS_TS_PATH = "maker/commands.ts";
|
|
23
|
+
|
|
24
|
+
/** Process-global so a re-evaluated module cannot register tsx twice. */
|
|
25
|
+
const TSX_REGISTERED = "__nexwireMakerTsxRegistered";
|
|
18
26
|
|
|
19
27
|
const PROJECT_COMMANDS_TEMPLATE = `/**
|
|
20
28
|
* Your project's maker commands.
|
|
@@ -36,8 +44,11 @@ const PROJECT_COMMANDS_TEMPLATE = `/**
|
|
|
36
44
|
* which one. Rename yours; the built-in stays.
|
|
37
45
|
* - An error in this file is reported, never swallowed. If your commands stop
|
|
38
46
|
* appearing, the fault is here rather than in your command code.
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
47
|
+
* - This file runs as plain JavaScript under plain Node, so it cannot import
|
|
48
|
+
* your TypeScript source or use your tsconfig path alias. If a command
|
|
49
|
+
* needs either, name this file maker/commands.ts instead and delete this
|
|
50
|
+
* one - nexwire loads that file through tsx, with your tsconfig applied.
|
|
51
|
+
* For editor type support, create maker/tsconfig.json as shown in the docs.
|
|
41
52
|
*
|
|
42
53
|
* Replace or delete the example below once you have a command of your own.
|
|
43
54
|
*/
|
|
@@ -54,14 +65,47 @@ export default function register(program) {
|
|
|
54
65
|
}
|
|
55
66
|
`;
|
|
56
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Why: Plain node cannot import TypeScript, so maker/commands.ts needs tsx
|
|
70
|
+
* registered before it is imported. Registered at most once per process
|
|
71
|
+
* and never unregistered: two overlapping registrations corrupt the
|
|
72
|
+
* loader, and commands routinely import more TypeScript at action time,
|
|
73
|
+
* long after this function returns. The flag lives on globalThis because
|
|
74
|
+
* this module can be evaluated more than once in one process (vitest's
|
|
75
|
+
* per-file isolation) while the loader hooks are process-wide.
|
|
76
|
+
* When: Only when the project's file is TypeScript - maker/commands.mjs never
|
|
77
|
+
* pays for tsx being installed.
|
|
78
|
+
* Where: utils/project-commands.mjs.
|
|
79
|
+
*/
|
|
80
|
+
async function registerTsLoader(rel) {
|
|
81
|
+
if (globalThis[TSX_REGISTERED]) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const { register } = await import("tsx/esm/api");
|
|
85
|
+
register();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${rel} needs tsx to load TypeScript, but it could not be registered: ${error?.message || error}. tsx ships as a devDependency of every nexwire project - run npm install, or rename the file to ${PROJECT_COMMANDS_PATH}.`,
|
|
89
|
+
{ cause: error }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
globalThis[TSX_REGISTERED] = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
/**
|
|
58
97
|
* Why: Registers the commands a project defined for itself.
|
|
59
98
|
* When: Every maker invocation, right after the framework's own registrars.
|
|
60
99
|
* Where: Maker CLI entry flow, from utils/project-commands.mjs.
|
|
61
|
-
* How: Imports maker/commands.mjs
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
100
|
+
* How: Imports maker/commands.mjs - or maker/commands.ts, loaded through tsx,
|
|
101
|
+
* which resolves the project's tsconfig path aliases - resolved from
|
|
102
|
+
* process.cwd(), not from import.meta.dirname, which points inside
|
|
103
|
+
* node_modules and would find the framework's own directory instead of the
|
|
104
|
+
* project. If a project keeps both files the .mjs wins and the .ts file is
|
|
105
|
+
* skipped with a warning naming it, so neither file is ever silently dead:
|
|
106
|
+
* a project that already runs must not be re-registered by a file that
|
|
107
|
+
* appeared later, and a file that is skipped must say so. Absent is silent
|
|
108
|
+
* and free; broken is loud, because a developer's commands silently not
|
|
65
109
|
* existing is a far worse experience than an error message. A name that
|
|
66
110
|
* collides with a framework command is refused by commander itself, which
|
|
67
111
|
* is the right outcome: the built-in wins and the project cannot shadow a
|
|
@@ -70,10 +114,23 @@ export default function register(program) {
|
|
|
70
114
|
* asked for the name.
|
|
71
115
|
*/
|
|
72
116
|
export async function registerProjectCommands(program, rawArgs, cwd = process.cwd()) {
|
|
73
|
-
const
|
|
117
|
+
const mjsFile = path.resolve(cwd, PROJECT_COMMANDS_PATH);
|
|
118
|
+
const tsFile = path.resolve(cwd, PROJECT_COMMANDS_TS_PATH);
|
|
119
|
+
const hasMjs = fs.existsSync(mjsFile);
|
|
120
|
+
const hasTs = fs.existsSync(tsFile);
|
|
121
|
+
|
|
122
|
+
if (!hasMjs && !hasTs) {
|
|
123
|
+
return { loaded: false, file: mjsFile, added: [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isTs = !hasMjs && hasTs;
|
|
127
|
+
const file = isTs ? tsFile : mjsFile;
|
|
128
|
+
const rel = isTs ? PROJECT_COMMANDS_TS_PATH : PROJECT_COMMANDS_PATH;
|
|
74
129
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
130
|
+
if (hasMjs && hasTs) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`${PROJECT_COMMANDS_TS_PATH} is ignored because ${PROJECT_COMMANDS_PATH} exists. Keep one - delete the file you are not using.`
|
|
133
|
+
);
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
// Not wrapped in a bare try/catch on purpose. A file that exists but fails to
|
|
@@ -81,18 +138,22 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
|
|
|
81
138
|
// command that is actually in this loader. The catches below only attach the
|
|
82
139
|
// file name, because an error from inside the project file otherwise reads as
|
|
83
140
|
// if it came from the framework.
|
|
141
|
+
if (isTs) {
|
|
142
|
+
await registerTsLoader(rel);
|
|
143
|
+
}
|
|
144
|
+
|
|
84
145
|
let module;
|
|
85
146
|
try {
|
|
86
147
|
module = await import(pathToFileURL(file).href);
|
|
87
148
|
} catch (error) {
|
|
88
|
-
throw new Error(`Could not load ${
|
|
149
|
+
throw new Error(`Could not load ${rel}: ${error?.message || error}`, { cause: error });
|
|
89
150
|
}
|
|
90
151
|
|
|
91
152
|
const register = module.default ?? module.register;
|
|
92
153
|
|
|
93
154
|
if (typeof register !== "function") {
|
|
94
155
|
throw new TypeError(
|
|
95
|
-
`${
|
|
156
|
+
`${rel} must export a default function (program, args) => void. Found: ${typeof register}.`
|
|
96
157
|
);
|
|
97
158
|
}
|
|
98
159
|
|
|
@@ -101,7 +162,7 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
|
|
|
101
162
|
try {
|
|
102
163
|
await register(program, rawArgs);
|
|
103
164
|
} catch (error) {
|
|
104
|
-
throw new Error(`Could not register commands from ${
|
|
165
|
+
throw new Error(`Could not register commands from ${rel}: ${error?.message || error}`, {
|
|
105
166
|
cause: error
|
|
106
167
|
});
|
|
107
168
|
}
|
package/express/vitest.config.ts
CHANGED
|
@@ -25,14 +25,14 @@ export default defineConfig({
|
|
|
25
25
|
// framework's own tests under `src/framework/**/__tests__/` are not
|
|
26
26
|
// published, because they test the framework rather than the application.
|
|
27
27
|
// That measures 8.49% statements, so a floor calibrated for this repository
|
|
28
|
-
//
|
|
29
|
-
//
|
|
28
|
+
// reports a failure for a developer who has done nothing wrong and whose
|
|
29
|
+
// 56 tests all pass.
|
|
30
30
|
//
|
|
31
|
-
// The floor is a guard on this repository, so it is
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// be meaningless in someone else's
|
|
35
|
-
// untested code is added here.
|
|
31
|
+
// The floor is a guard on this repository, not on yours, so it is passed on
|
|
32
|
+
// the command line when the framework's own suite is run - it is never read
|
|
33
|
+
// from this file. That keeps this file byte-identical across both engines,
|
|
34
|
+
// keeps it free of a number that would be meaningless in someone else's
|
|
35
|
+
// project, and still fails when untested code is added here.
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
});
|
|
@@ -11,47 +11,71 @@ export type NexwireRouter = OpenAPIHono & {
|
|
|
11
11
|
* When: Building root app and module route files.
|
|
12
12
|
* Where: HTTP app bootstrap and route stubs.
|
|
13
13
|
* How: Wraps OpenAPIHono and adds group/api convenience APIs.
|
|
14
|
+
*
|
|
15
|
+
* group() records its middlewares here instead of calling this.use("*", mw).
|
|
16
|
+
* A use("*") is a route entry: mounting the router copies it and "*" rewrites
|
|
17
|
+
* to the mount prefix, so a limiter declared on one sub-router also ran for its
|
|
18
|
+
* siblings and for every route added after the mount. Recorded middlewares are
|
|
19
|
+
* merged into each .api() call instead, so they travel with the routes they
|
|
20
|
+
* were declared on and reach nothing else.
|
|
14
21
|
*/
|
|
15
22
|
export function createRouter(): NexwireRouter {
|
|
16
23
|
const router = new OpenAPIHono({ strict: false, defaultHook }) as NexwireRouter;
|
|
17
24
|
const baseOpenapi = router.openapi.bind(router);
|
|
18
25
|
|
|
26
|
+
// Store group middlewares on the instance
|
|
27
|
+
const groupMiddlewares: any[] = [];
|
|
28
|
+
|
|
19
29
|
router.group = function (...middlewares) {
|
|
30
|
+
// Accumulate group middlewares (supports multiple .group() calls)
|
|
20
31
|
for (const middleware of middlewares) {
|
|
21
|
-
|
|
32
|
+
groupMiddlewares.push(middleware);
|
|
22
33
|
}
|
|
23
|
-
|
|
24
34
|
return this;
|
|
25
35
|
};
|
|
26
36
|
|
|
27
37
|
router.api = function (route: any, handlerOrMiddlewares: any, handler?: any) {
|
|
38
|
+
// Normalize arguments:
|
|
39
|
+
// .api(route, handler) → [groupMiddlewares], handler
|
|
40
|
+
// .api(route, [mw1, mw2], handler) → [groupMiddlewares, mw1, mw2], handler
|
|
41
|
+
let routeMiddlewares: any[] = [];
|
|
42
|
+
let finalHandler: any;
|
|
43
|
+
|
|
28
44
|
if (!Array.isArray(handlerOrMiddlewares)) {
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
finalHandler = handlerOrMiddlewares;
|
|
46
|
+
} else {
|
|
47
|
+
routeMiddlewares = handlerOrMiddlewares;
|
|
48
|
+
finalHandler = handler;
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
const
|
|
51
|
+
// Combine: group middleware runs first, then route middleware
|
|
52
|
+
const allMiddlewares = [...groupMiddlewares, ...routeMiddlewares];
|
|
35
53
|
|
|
36
54
|
if (typeof finalHandler !== "function") {
|
|
37
55
|
throw new Error("api(route, middlewares, handler) requires a handler function");
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
// No middleware at all → simple pass-through
|
|
59
|
+
if (allMiddlewares.length === 0) {
|
|
60
|
+
baseOpenapi(route, finalHandler);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Wrap middleware chain into the handler — travels with the route,
|
|
65
|
+
// no matter where the router is mounted.
|
|
40
66
|
const wrappedHandler = async (c: any) => {
|
|
41
67
|
let index = 0;
|
|
42
68
|
|
|
43
69
|
const next = async (): Promise<any> => {
|
|
44
|
-
if (index <
|
|
45
|
-
const middleware =
|
|
70
|
+
if (index < allMiddlewares.length) {
|
|
71
|
+
const middleware = allMiddlewares[index++];
|
|
46
72
|
let downstream: any;
|
|
47
73
|
const result = await middleware(c, async () => {
|
|
48
74
|
downstream = await next();
|
|
49
75
|
return downstream;
|
|
50
76
|
});
|
|
51
|
-
|
|
52
77
|
return result === undefined ? downstream : result;
|
|
53
78
|
}
|
|
54
|
-
|
|
55
79
|
return await finalHandler(c);
|
|
56
80
|
};
|
|
57
81
|
|
|
@@ -66,10 +90,7 @@ export function createRouter(): NexwireRouter {
|
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
/**
|
|
69
|
-
*
|
|
70
|
-
* When: Route files need shared middleware composition.
|
|
71
|
-
* Where: Module route declarations.
|
|
72
|
-
* How: Creates a router instance and applies all provided middlewares.
|
|
93
|
+
* Shorthand for creating a router with group middleware pre-applied.
|
|
73
94
|
*/
|
|
74
95
|
export function group(...middlewares: any[]) {
|
|
75
96
|
return createRouter().group(...middlewares);
|
|
@@ -13,6 +13,6 @@ export default async function {{ClassName}}Seeder() {
|
|
|
13
13
|
|
|
14
14
|
for (const row of rows) await db.insert({{tableVariable}}).values(row);
|
|
15
15
|
|
|
16
|
-
console.log(
|
|
16
|
+
console.log(`{{ClassName}}: ${rows.length} row(s) prepared`);
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -18,5 +18,5 @@ export default async function {{ClassName}}Seeder() {
|
|
|
18
18
|
|
|
19
19
|
// for (const row of rows) await db.insert({{tableVariable}}).values(row);
|
|
20
20
|
|
|
21
|
-
console.log(
|
|
21
|
+
console.log(`{{ClassName}}: ${rows.length} row(s) ready`);
|
|
22
22
|
}
|
|
@@ -13,8 +13,16 @@ import { PROJECT_COMMAND_FLAG } from "./help.mjs";
|
|
|
13
13
|
* there would be overwritten on the next framework update - and `src/modules/`
|
|
14
14
|
* is swept by the route, job and seeder globs. The project root is the one
|
|
15
15
|
* place nothing else owns.
|
|
16
|
+
*
|
|
17
|
+
* Two names are read: commands.mjs (plain JavaScript, loaded by plain node)
|
|
18
|
+
* and commands.ts (TypeScript, loaded through tsx). The .mjs is the incumbent
|
|
19
|
+
* and wins if a project keeps both.
|
|
16
20
|
*/
|
|
17
21
|
export const PROJECT_COMMANDS_PATH = "maker/commands.mjs";
|
|
22
|
+
export const PROJECT_COMMANDS_TS_PATH = "maker/commands.ts";
|
|
23
|
+
|
|
24
|
+
/** Process-global so a re-evaluated module cannot register tsx twice. */
|
|
25
|
+
const TSX_REGISTERED = "__nexwireMakerTsxRegistered";
|
|
18
26
|
|
|
19
27
|
const PROJECT_COMMANDS_TEMPLATE = `/**
|
|
20
28
|
* Your project's maker commands.
|
|
@@ -36,8 +44,11 @@ const PROJECT_COMMANDS_TEMPLATE = `/**
|
|
|
36
44
|
* which one. Rename yours; the built-in stays.
|
|
37
45
|
* - An error in this file is reported, never swallowed. If your commands stop
|
|
38
46
|
* appearing, the fault is here rather than in your command code.
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
47
|
+
* - This file runs as plain JavaScript under plain Node, so it cannot import
|
|
48
|
+
* your TypeScript source or use your tsconfig path alias. If a command
|
|
49
|
+
* needs either, name this file maker/commands.ts instead and delete this
|
|
50
|
+
* one - nexwire loads that file through tsx, with your tsconfig applied.
|
|
51
|
+
* For editor type support, create maker/tsconfig.json as shown in the docs.
|
|
41
52
|
*
|
|
42
53
|
* Replace or delete the example below once you have a command of your own.
|
|
43
54
|
*/
|
|
@@ -54,14 +65,47 @@ export default function register(program) {
|
|
|
54
65
|
}
|
|
55
66
|
`;
|
|
56
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Why: Plain node cannot import TypeScript, so maker/commands.ts needs tsx
|
|
70
|
+
* registered before it is imported. Registered at most once per process
|
|
71
|
+
* and never unregistered: two overlapping registrations corrupt the
|
|
72
|
+
* loader, and commands routinely import more TypeScript at action time,
|
|
73
|
+
* long after this function returns. The flag lives on globalThis because
|
|
74
|
+
* this module can be evaluated more than once in one process (vitest's
|
|
75
|
+
* per-file isolation) while the loader hooks are process-wide.
|
|
76
|
+
* When: Only when the project's file is TypeScript - maker/commands.mjs never
|
|
77
|
+
* pays for tsx being installed.
|
|
78
|
+
* Where: utils/project-commands.mjs.
|
|
79
|
+
*/
|
|
80
|
+
async function registerTsLoader(rel) {
|
|
81
|
+
if (globalThis[TSX_REGISTERED]) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const { register } = await import("tsx/esm/api");
|
|
85
|
+
register();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${rel} needs tsx to load TypeScript, but it could not be registered: ${error?.message || error}. tsx ships as a devDependency of every nexwire project - run npm install, or rename the file to ${PROJECT_COMMANDS_PATH}.`,
|
|
89
|
+
{ cause: error }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
globalThis[TSX_REGISTERED] = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
/**
|
|
58
97
|
* Why: Registers the commands a project defined for itself.
|
|
59
98
|
* When: Every maker invocation, right after the framework's own registrars.
|
|
60
99
|
* Where: Maker CLI entry flow, from utils/project-commands.mjs.
|
|
61
|
-
* How: Imports maker/commands.mjs
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
100
|
+
* How: Imports maker/commands.mjs - or maker/commands.ts, loaded through tsx,
|
|
101
|
+
* which resolves the project's tsconfig path aliases - resolved from
|
|
102
|
+
* process.cwd(), not from import.meta.dirname, which points inside
|
|
103
|
+
* node_modules and would find the framework's own directory instead of the
|
|
104
|
+
* project. If a project keeps both files the .mjs wins and the .ts file is
|
|
105
|
+
* skipped with a warning naming it, so neither file is ever silently dead:
|
|
106
|
+
* a project that already runs must not be re-registered by a file that
|
|
107
|
+
* appeared later, and a file that is skipped must say so. Absent is silent
|
|
108
|
+
* and free; broken is loud, because a developer's commands silently not
|
|
65
109
|
* existing is a far worse experience than an error message. A name that
|
|
66
110
|
* collides with a framework command is refused by commander itself, which
|
|
67
111
|
* is the right outcome: the built-in wins and the project cannot shadow a
|
|
@@ -70,10 +114,23 @@ export default function register(program) {
|
|
|
70
114
|
* asked for the name.
|
|
71
115
|
*/
|
|
72
116
|
export async function registerProjectCommands(program, rawArgs, cwd = process.cwd()) {
|
|
73
|
-
const
|
|
117
|
+
const mjsFile = path.resolve(cwd, PROJECT_COMMANDS_PATH);
|
|
118
|
+
const tsFile = path.resolve(cwd, PROJECT_COMMANDS_TS_PATH);
|
|
119
|
+
const hasMjs = fs.existsSync(mjsFile);
|
|
120
|
+
const hasTs = fs.existsSync(tsFile);
|
|
121
|
+
|
|
122
|
+
if (!hasMjs && !hasTs) {
|
|
123
|
+
return { loaded: false, file: mjsFile, added: [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isTs = !hasMjs && hasTs;
|
|
127
|
+
const file = isTs ? tsFile : mjsFile;
|
|
128
|
+
const rel = isTs ? PROJECT_COMMANDS_TS_PATH : PROJECT_COMMANDS_PATH;
|
|
74
129
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
130
|
+
if (hasMjs && hasTs) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`${PROJECT_COMMANDS_TS_PATH} is ignored because ${PROJECT_COMMANDS_PATH} exists. Keep one - delete the file you are not using.`
|
|
133
|
+
);
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
// Not wrapped in a bare try/catch on purpose. A file that exists but fails to
|
|
@@ -81,18 +138,22 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
|
|
|
81
138
|
// command that is actually in this loader. The catches below only attach the
|
|
82
139
|
// file name, because an error from inside the project file otherwise reads as
|
|
83
140
|
// if it came from the framework.
|
|
141
|
+
if (isTs) {
|
|
142
|
+
await registerTsLoader(rel);
|
|
143
|
+
}
|
|
144
|
+
|
|
84
145
|
let module;
|
|
85
146
|
try {
|
|
86
147
|
module = await import(pathToFileURL(file).href);
|
|
87
148
|
} catch (error) {
|
|
88
|
-
throw new Error(`Could not load ${
|
|
149
|
+
throw new Error(`Could not load ${rel}: ${error?.message || error}`, { cause: error });
|
|
89
150
|
}
|
|
90
151
|
|
|
91
152
|
const register = module.default ?? module.register;
|
|
92
153
|
|
|
93
154
|
if (typeof register !== "function") {
|
|
94
155
|
throw new TypeError(
|
|
95
|
-
`${
|
|
156
|
+
`${rel} must export a default function (program, args) => void. Found: ${typeof register}.`
|
|
96
157
|
);
|
|
97
158
|
}
|
|
98
159
|
|
|
@@ -101,7 +162,7 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
|
|
|
101
162
|
try {
|
|
102
163
|
await register(program, rawArgs);
|
|
103
164
|
} catch (error) {
|
|
104
|
-
throw new Error(`Could not register commands from ${
|
|
165
|
+
throw new Error(`Could not register commands from ${rel}: ${error?.message || error}`, {
|
|
105
166
|
cause: error
|
|
106
167
|
});
|
|
107
168
|
}
|
package/hono/vitest.config.ts
CHANGED
|
@@ -25,14 +25,14 @@ export default defineConfig({
|
|
|
25
25
|
// framework's own tests under `src/framework/**/__tests__/` are not
|
|
26
26
|
// published, because they test the framework rather than the application.
|
|
27
27
|
// That measures 8.49% statements, so a floor calibrated for this repository
|
|
28
|
-
//
|
|
29
|
-
//
|
|
28
|
+
// reports a failure for a developer who has done nothing wrong and whose
|
|
29
|
+
// 56 tests all pass.
|
|
30
30
|
//
|
|
31
|
-
// The floor is a guard on this repository, so it is
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// be meaningless in someone else's
|
|
35
|
-
// untested code is added here.
|
|
31
|
+
// The floor is a guard on this repository, not on yours, so it is passed on
|
|
32
|
+
// the command line when the framework's own suite is run - it is never read
|
|
33
|
+
// from this file. That keeps this file byte-identical across both engines,
|
|
34
|
+
// keeps it free of a number that would be meaningless in someone else's
|
|
35
|
+
// project, and still fails when untested code is added here.
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
});
|