Skip to content

Commit cd153f4

Browse files
committed
refactor(hooks): split ctx.abort into ctx.fail and ctx.skip
`abort(message, { asWarning: true })` did not abort anything — it warned and the command carried on, so one verb meant opposite things depending on a flag. The two outcomes now have a verb each: `ctx.fail(message)` fails the command, `ctx.skip(message)` warns and lets it continue. Both still stop the handler by throwing, so both are typed `never` and the "handler ends here" behavior is unchanged; only the command's fate differs. A missing message falls back to one naming the hook point and the method. `abort` and the `asWarning` option are removed outright rather than shimmed, as the API is unreleased and never exposed either name.
1 parent 777c3cc commit cd153f4

3 files changed

Lines changed: 78 additions & 27 deletions

File tree

extending-cli.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,28 @@ The wrappable hook points are:
153153

154154
`before-buildAndroid` · `before-buildAndroidPlugin` · `before-buildIOS` · `before-checkEnvironment` · `before-checkForChanges` · `before-install` · `before-prepare` · `before-prepareNativeApp` · `before-resolveCommand` · `before-watch` · `before-watchPatterns`
155155

156-
### `ctx.abort(message)`
156+
### `ctx.fail(message)` and `ctx.skip(message)`
157157

158-
`ctx.abort(message)` stops the hook and fails the command. Pass `{ asWarning: true }` to print the message as a warning and let the command continue instead. The message is required in practice — calling `abort()` without one falls back to a message naming the hook point.
158+
Both end the handler immediately — nothing after the call runs — and differ in what happens to the command.
159+
160+
`ctx.fail(message)` fails the command, printing `message` as the error:
161+
162+
```JavaScript
163+
module.exports = defineHook("before-prepare", (ctx) => {
164+
ctx.fail("The generated bundle is missing; run the bundler first.");
165+
});
166+
```
167+
168+
`ctx.skip(message)` prints `message` as a warning and lets the command continue:
159169

160170
```JavaScript
161171
module.exports = defineHook("before-prepare", (ctx) => {
162-
ctx.abort("Nothing to prepare.", { asWarning: true });
172+
ctx.skip("Nothing to prepare.");
163173
});
164174
```
165175

176+
The message is required in practice — calling either without one falls back to a message naming the hook point and the method.
177+
166178
### Plain function hooks
167179

168180
Exporting a plain function is still supported. It runs in an injection context too, so `inject()` works the same way; declare a `hookArgs` parameter if you need the payload.
@@ -190,7 +202,7 @@ Member | Type | Description
190202

191203
A plain-function hook can also return a function, which the CLI folds into a middleware chain around the hooked method.
192204

193-
With `defineHook` neither convention is needed, and neither applies: `ctx.abort` replaces throwing an error carrying `stopExecution`/`errorAsWarning`, and `ctx.wrap` replaces returning a function. A definition whose `run` returns a function is warned about — the returned function is not used as a middleware.
205+
With `defineHook` neither convention is needed, and neither applies: `ctx.fail`/`ctx.skip` replace throwing an error carrying `stopExecution`/`errorAsWarning`, and `ctx.wrap` replaces returning a function. A definition whose `run` returns a function is warned about — the returned function is not used as a middleware.
194206

195207
## Legacy: parameter-name injection
196208

lib/common/define-hook.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,20 @@ export interface HookContext<TPayload = any> {
4242
wrap(middleware: HookMiddleware): void;
4343

4444
/**
45-
* Stops the hook. With `asWarning`, the CLI logs the message and continues
46-
* the command; otherwise the command fails.
45+
* Ends the handler and fails the command with `message`.
46+
*
47+
* Typed `never` because it stops the handler by throwing, so nothing after
48+
* the call runs.
4749
*/
48-
abort(message: string, opts?: { asWarning?: boolean }): never;
50+
fail(message: string): never;
51+
52+
/**
53+
* Ends the handler and logs `message` as a warning; the command continues.
54+
*
55+
* Typed `never` because it stops the handler by throwing, so nothing after
56+
* the call runs — only the command outlives it.
57+
*/
58+
skip(message: string): never;
4959
}
5060

5161
export type HookHandler<TPayload = any> = (
@@ -209,24 +219,31 @@ export function createHookInvocation<TPayload = any>(
209219

210220
middlewares.push(middleware);
211221
},
212-
abort(message: string, opts?: { asWarning?: boolean }): never {
213-
const text =
214-
typeof message === "string" && message.trim().length
215-
? message
216-
: `The "${hookName}" hook aborted without a message.`;
217-
const error: any = new Error(text);
218-
if (opts && opts.asWarning) {
219-
// The pair the hooks service checks for to downgrade a rejection.
220-
error.stopExecution = false;
221-
error.errorAsWarning = true;
222-
}
222+
fail(message: string): never {
223+
throw new Error(hookMessage(message, hookName, "fail"));
224+
},
225+
skip(message: string): never {
226+
const error: any = new Error(hookMessage(message, hookName, "skip"));
227+
// The pair the hooks service checks for to downgrade a rejection.
228+
error.stopExecution = false;
229+
error.errorAsWarning = true;
223230
throw error;
224231
},
225232
};
226233

227234
return { context, middlewares };
228235
}
229236

237+
function hookMessage(
238+
message: string,
239+
hookName: string,
240+
method: string,
241+
): string {
242+
return typeof message === "string" && message.trim().length
243+
? message
244+
: `The "${hookName}" hook called ctx.${method}() without a message.`;
245+
}
246+
230247
function derivePayload(hookArguments: any): any {
231248
if (!hookArguments || typeof hookArguments !== "object") {
232249
return undefined;

test/define-hook.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -224,35 +224,39 @@ describe("defineHook", () => {
224224
assert.isUndefined(capture.originalRan);
225225
});
226226

227-
it("logs a warning and continues when the handler aborts with asWarning", async () => {
227+
it("warns and continues the command when the handler skips, stopping the handler", async () => {
228228
writeHook(
229229
projectDir,
230230
"before-case7",
231231
`const { defineHook } = require(${JSON.stringify(apiPath)});
232232
module.exports = defineHook("before-case7", async (ctx) => {
233-
ctx.abort("soft-abort", { asWarning: true });
233+
ctx.skip("soft-skip");
234+
global.__hookCapture.afterSkip = true;
234235
});`,
235236
);
236237

237238
await hooksService().executeBeforeHooks("case7");
238239

239-
assert.include(logger().warnOutput, "soft-abort");
240+
assert.include(logger().warnOutput, "soft-skip");
241+
assert.isUndefined(capture.afterSkip);
240242
});
241243

242-
it("fails the command when the handler aborts without asWarning", async () => {
244+
it("fails the command when the handler fails, stopping the handler", async () => {
243245
writeHook(
244246
projectDir,
245247
"before-case8",
246248
`const { defineHook } = require(${JSON.stringify(apiPath)});
247249
module.exports = defineHook("before-case8", async (ctx) => {
248-
ctx.abort("hard-abort");
250+
ctx.fail("hard-fail");
251+
global.__hookCapture.afterFail = true;
249252
});`,
250253
);
251254

252255
await assert.isRejected(
253256
hooksService().executeBeforeHooks("case8"),
254-
/hard-abort/,
257+
/hard-fail/,
255258
);
259+
assert.isUndefined(capture.afterFail);
256260
});
257261

258262
it("keeps a legacy param-name hook on the old path, and never reports a definition hook", async () => {
@@ -377,19 +381,37 @@ describe("defineHook", () => {
377381
);
378382
});
379383

380-
it("defaults the abort() message instead of failing with Error(undefined)", async () => {
384+
it("defaults the fail() message instead of failing with Error(undefined)", async () => {
381385
writeHook(
382386
projectDir,
383387
"before-case15",
384388
`const { defineHook } = require(${JSON.stringify(apiPath)});
385389
module.exports = defineHook("before-case15", (ctx) => {
386-
ctx.abort();
390+
ctx.fail();
387391
});`,
388392
);
389393

390394
await assert.isRejected(
391395
hooksService().executeBeforeHooks("case15"),
392-
/The "before-case15" hook aborted without a message\./,
396+
/The "before-case15" hook called ctx\.fail\(\) without a message\./,
397+
);
398+
});
399+
400+
it("defaults the skip() message", async () => {
401+
writeHook(
402+
projectDir,
403+
"before-case18",
404+
`const { defineHook } = require(${JSON.stringify(apiPath)});
405+
module.exports = defineHook("before-case18", (ctx) => {
406+
ctx.skip();
407+
});`,
408+
);
409+
410+
await hooksService().executeBeforeHooks("case18");
411+
412+
assert.include(
413+
logger().warnOutput,
414+
'The "before-case18" hook called ctx.skip() without a message.',
393415
);
394416
});
395417

0 commit comments

Comments
 (0)