Skip to content

Commit a5db3c7

Browse files
committed
feat(core): allow middleware to return new object instead of mutating original object
1 parent e5e7380 commit a5db3c7

File tree

11 files changed

+149
-9
lines changed

11 files changed

+149
-9
lines changed

examples/middleware.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,20 @@ const cli = cliForge('basic-cli')
3939
.middleware((args) => {
4040
args.name = args.name.toUpperCase();
4141
})
42+
// Middleware can add new properties to the args object
43+
.middleware((args) => {
44+
return {
45+
...args,
46+
env: process.env.NODE_ENV || 'development',
47+
};
48+
})
4249
// Multiple middleware can be registered
4350
.middleware(() => {
4451
console.log('HELLO MIDDLEWARE');
4552
}),
4653
// Handler is used to define the command's behavior
4754
handler: (args) => {
48-
console.log(`Hello, ${args.name}!`);
55+
console.log(`Hello, ${args.name}! [${args.env.toUpperCase()}]`);
4956
},
5057
});
5158

examples/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "examples",
33
"targets": {
44
"run-example": {
5-
"command": "tsx {projectRoot}/{args.example}.ts"
5+
"command": "tsx --tsconfig {projectRoot}/tsconfig.json {projectRoot}/{args.example}.ts"
66
}
77
},
88
"implicitDependencies": ["cli-forge", "parser"]

examples/zod.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// ---
2+
// id: zod-middleware
3+
// title: Zod Middleware
4+
// description: |
5+
// As another example of middleware, we can look at how to integrate [Zod](https://npmjs.com/zod). CLI Forge
6+
// provides a middleware function under `cli-forge/middleware/zod` that can be used to validate, parse, transform,
7+
// and otherwise manipulate command arguments using Zod schemas.
8+
9+
// commands:
10+
// - '{filename} hello --name sir'
11+
// ---
12+
import cliForge from 'cli-forge';
13+
import { zodMiddleware } from 'cli-forge/middleware/zod';
14+
import { z } from 'zod';
15+
16+
const cli = cliForge('basic-cli')
17+
// Requires a command to be provided
18+
.demandCommand()
19+
20+
// Registers "hello" command
21+
.command('hello', {
22+
// Builder is used to define the command's options
23+
builder: (args) =>
24+
args
25+
.option('name', {
26+
type: 'string',
27+
description: 'The name to say hello to',
28+
default: 'World',
29+
})
30+
// Middleware registered on parent commands will be invoked before the child command's middleware
31+
.middleware(
32+
zodMiddleware(
33+
z
34+
.object({
35+
name: z
36+
.string()
37+
.min(2, 'Name must be at least 2 characters long'),
38+
})
39+
.transform((data) => ({
40+
name: data.name.trim().toUpperCase(),
41+
snakeCase: data.name.trim().replace(/\s+/g, '_').toLowerCase(),
42+
}))
43+
)
44+
),
45+
46+
// Handler is used to define the command's behavior
47+
handler: (args) => {
48+
console.log(`Hello, ${args.name}!`);
49+
console.log(`sssssnake_case: ${args.snakeCase}`);
50+
},
51+
});
52+
53+
// We export the CLI for a few reasons:
54+
// - Testing
55+
// - Composition (a CLI can be a subcommand of another CLI)
56+
// - Docs generation
57+
export default cli;
58+
59+
// Calling `.forge()` executes the CLI. It's single parameter is the CLI args
60+
// and they default to `process.argv.slice(2)`.
61+
if (require.main === module) {
62+
(async () => {
63+
await cli.forge();
64+
})();
65+
}

package-lock.json

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"verdaccio": "^5.0.4",
3737
"vite": "^5.0.0",
3838
"vitest": "^1.3.1",
39-
"yaml": "^2.5.0"
39+
"yaml": "^2.5.0",
40+
"zod": "^4.1.13"
4041
},
4142
"nx": {
4243
"includedScripts": []

packages/cli-forge/package.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
},
88
"peerDependencies": {
99
"markdown-factory": "0.2.0",
10-
"tsx": "4.19.0"
10+
"tsx": "4.19.0",
11+
"zod": "4.1.13"
1112
},
1213
"peerDependenciesMeta": {
1314
"markdown-factory": {
@@ -17,6 +18,9 @@
1718
"tsx": {
1819
"optional": true,
1920
"dev": true
21+
},
22+
"zod": {
23+
"optional": true
2024
}
2125
},
2226
"type": "commonjs",
@@ -31,6 +35,21 @@
3135
"bin": {
3236
"cli-forge": "./bin/cli.js"
3337
},
38+
"exports": {
39+
".": {
40+
"require": "./src/index.js",
41+
"types": "./src/index.d.ts"
42+
},
43+
"./middleware": {
44+
"require": "./src/middleware.js",
45+
"types": "./src/middleware.d.ts"
46+
},
47+
"./middleware/*": {
48+
"require": "./src/middleware/*.js",
49+
"types": "./src/middleware/*.d.ts"
50+
},
51+
"./package.json": "./package.json"
52+
},
3453
"publishConfig": {
3554
"access": "public"
3655
}

packages/cli-forge/src/lib/internal-cli.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,14 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
289289
console.log(this.formatHelp());
290290
}
291291

292-
middleware(callback: (args: TArgs) => void): CLI<TArgs> {
292+
middleware<TArgs2>(
293+
callback: (args: TArgs) => TArgs2 | Promise<TArgs2>
294+
): CLI<TArgs2 extends void ? TArgs : TArgs & TArgs2> {
293295
this.registeredMiddleware.push(callback);
294-
return this;
296+
// If middleware returns void, TArgs doesn't change...
297+
// If it returns something, we need to merge it into TArgs...
298+
// that's not here though, its where we apply the middleware results.
299+
return this as any;
295300
}
296301

297302
/**
@@ -317,7 +322,13 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
317322
}
318323
if (cmd.configuration?.handler) {
319324
for (const middleware of middlewares) {
320-
await middleware(args);
325+
const middlewareResult = await middleware(args);
326+
if (
327+
middlewareResult !== void 0 &&
328+
typeof middlewareResult === 'object'
329+
) {
330+
args = middlewareResult as T;
331+
}
321332
}
322333
await cmd.configuration.handler(args, {
323334
command: cmd,

packages/cli-forge/src/lib/public-api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ export interface CLI<TArgs extends ParsedArgs = ParsedArgs> {
174174
}): CLI<TArgs>;
175175
group(label: string, keys: (keyof TArgs)[]): CLI<TArgs>;
176176

177-
middleware(callback: (args: TArgs) => void): CLI<TArgs>;
177+
middleware<TArgs2>(
178+
callback: MiddlewareFunction<TArgs, TArgs2>
179+
): CLI<TArgs2 extends void ? TArgs : TArgs & TArgs2>;
178180

179181
/**
180182
* Parses argv and executes the CLI
@@ -273,6 +275,10 @@ export type ErrorHandler = (
273275
}
274276
) => void;
275277

278+
export type MiddlewareFunction<TArgs extends ParsedArgs, TArgs2> = (
279+
args: TArgs
280+
) => TArgs2 | Promise<TArgs2>;
281+
276282
/**
277283
* Constructs a CLI instance. See {@link CLI} for more information.
278284
* @param name Name for the top level CLI
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './middleware/zod';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ParsedArgs } from '@cli-forge/parser';
2+
import { MiddlewareFunction } from '../lib/public-api';
3+
import type { z, ZodObject, ZodPipe } from 'zod';
4+
5+
export function zodMiddleware<
6+
TArgs extends ParsedArgs,
7+
TSchema extends ZodObject | ZodPipe<ZodObject>
8+
>(schema: TSchema): MiddlewareFunction<TArgs, TArgs & z.infer<TSchema>> {
9+
return async (args: TArgs) => {
10+
const parsed = (await schema.parseAsync(args)) as z.infer<TSchema>;
11+
if (typeof parsed !== 'object') {
12+
throw new Error('Zod schema did not return an object');
13+
}
14+
return { ...args, ...parsed };
15+
};
16+
}

0 commit comments

Comments
 (0)