Skip to content

Commit 823e9cb

Browse files
edusperoniNathanWalker
authored andcommitted
docs(hooks): lead with the reworked defineHook API and correct the contract
Documents the bag form, define-time validation, the strict name match and the one-definition-per-file rule; splits ctx into payload/wrap/abort sections; states that dispatch-fired hook points carry no payload and lists the hook points where wrap() is honored. Corrects two long-standing errors: a hook named plainly `watch` never fires (the points are `before-watch`/`after-watch`), and downgrading a rejection to a warning needs `errorAsWarning === true` together with a Boolean `stopExecution`, not `stopExecution: false` alone.
1 parent 271fd8f commit 823e9cb

1 file changed

Lines changed: 55 additions & 18 deletions

File tree

extending-cli.md

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ For the NativeScript CLI to execute your hooks, you must place them in the `hook
1111

1212
You can attach the hook before or after `prepare` operations or to `--watch` operations.
1313

14-
Note that `watch` hooks can be executed only at the time of running `--watch` operations. The `watch` hooks are the last thing executed before launching the file system watcher which tracks for changes to your code.
14+
Note that `watch` hooks can be executed only at the time of running `--watch` operations. The `before-watch` hooks are the last thing executed before launching the file system watcher which tracks for changes to your code.
1515

1616
Your hooks must conform to the following naming and placement conventions:
1717

@@ -36,27 +36,29 @@ Your hooks must conform to the following naming and placement conventions:
3636
├── hook1 (this is an executable file)
3737
└── hook2 (this is an executable file)
3838
```
39-
* If you want to attach a hook for `--watch` operations, you must place the hook in the root of the `hooks` subdirectory. The file must be named `watch`. For example:
39+
* If you want to attach a hook for `--watch` operations, you must place the hook in the root of the `hooks` subdirectory. The file must be named `before-watch` or `after-watch`. For example:
4040
4141
```
4242
my-app/
4343
├── index.js
4444
├── package.json
4545
└── hooks/
46-
└── watch.js (this is a Node.js script)
46+
└── before-watch.js (this is a Node.js script)
4747
```
48-
* If you want to attach multiple hooks for `--watch` operations, you must place them inside a `watch` subdirectory of the `hooks` subdirectory. You can specify any meaningful name for the the hooks inside the subdirectory. For example:
48+
* If you want to attach multiple hooks for `--watch` operations, you must place them inside a `before-watch` or `after-watch` subdirectory of the `hooks` subdirectory. You can specify any meaningful name for the the hooks inside the subdirectory. For example:
4949
5050
```
5151
my-app/
5252
├── index.js
5353
├── package.json
5454
└── hooks/
55-
└── watch (a directory)
55+
└── before-watch (a directory)
5656
├── hook1 (this is an executable file)
5757
└── hook2 (this is an executable file)
5858
```
5959
60+
A file named plainly `watch` is never executed: like every other hook point, the watch hooks are addressed by the `before-`/`after-` names above.
61+
6062
> **NOTE:** When multiple hooks are attached to a single event (i.e. multiple hooks are stored in dedicated subdirectories), at the specified time, the CLI executes each hook one by one. However, the order of hook execution is not strict and might change over command executions.
6163
6264
Execute Hooks as Child Process
@@ -81,24 +83,39 @@ The CLI assumes that this is a CommonJS module and calls the hook it exports —
8183
8284
## Writing a hook
8385
84-
Export a hook definition built with `defineHook`. It takes the hook point in the usual naming convention (`before-prepare`, `after-watch`) and a handler that receives a context object.
86+
Export a hook definition built with `defineHook`. It takes the hook point in the usual naming convention (`before-prepare`, `after-watch`) and a `run` handler that receives a context object.
8587
8688
```JavaScript
8789
const { defineHook, inject, DoctorService } = require("nativescript/contracts");
8890
89-
module.exports = defineHook("before-prepare", async (ctx) => {
90-
const doctorService = inject(DoctorService);
91-
await doctorService.canExecuteLocalBuild();
91+
module.exports = defineHook({
92+
name: "before-prepare",
93+
run: async (ctx) => {
94+
const doctorService = inject(DoctorService);
95+
await doctorService.canExecuteLocalBuild();
96+
},
9297
});
9398
```
9499

100+
`defineHook(name, run)` is shorthand for the same definition:
101+
102+
```JavaScript
103+
module.exports = defineHook("before-prepare", async (ctx) => { /* ... */ });
104+
```
105+
106+
`defineHook` validates its input immediately: a missing or non-string `name`, a missing or non-function `run`, and unknown fields all throw at definition time, naming the definition and both accepted forms.
107+
108+
The `name` decides when the hook fires and must match the hook point the file is placed at. A definition whose `name` disagrees with its location is **skipped with a warning** rather than run at the wrong point. Export exactly one definition (or one plain function) per file — an array export is rejected.
109+
95110
Services come from `inject()` — the same API used everywhere else (see [dependency-injection.md](dependency-injection.md)):
96111

97112
* `inject()` is valid in the synchronous part of the handler — not after an `await`. Resolve what you need up front; for late lookups, grab the container first: `const injector = inject(Injector)` (`Injector` is exported from `nativescript/contracts` too), then `injector.get(...)` later.
98113
* Tokens resolve by class first and by their canonical name on a miss, so this works even if your dependency tree carries its own copy of `nativescript` — a duplicated token class still resolves to the running CLI's service.
99114
* Only a first tranche of services has typed tokens so far ([dependency-injection.md](dependency-injection.md#available-contracts) lists them); a service without a token is reachable by its registry name — `inject("logger")` — as a migration bridge.
100115
* If you build your hook in TypeScript, add `nativescript` as a `devDependency` and import the same names: `import { defineHook, inject, DoctorService } from "nativescript/contracts"`. An `.mjs` hook can `export default defineHook(...)`.
101116

117+
### `ctx.payload`
118+
102119
`ctx.payload` holds the parameters of the CLI operation being hooked; its shape depends on the hook point. It is the CLI's own object, so mutating it influences the operation:
103120

104121
```JavaScript
@@ -107,7 +124,19 @@ module.exports = defineHook("before-build-task-args", (ctx) => {
107124
});
108125
```
109126

110-
`ctx.wrap(middleware)` puts a middleware around the hooked method. The middleware receives the method's arguments and a `next` callback; call `next` to continue, or return without calling it to short-circuit the method entirely. Register it from a `before-` hook.
127+
Not every invocation carries one. The `before-<command>`/`after-<command>` hooks fired around command dispatch (`before-build`, `after-run`, …) pass no arguments at all, so `ctx.payload` is `undefined` there. Treat it as optional — in TypeScript it is typed `TPayload | undefined`:
128+
129+
```TypeScript
130+
import { defineHook } from "nativescript/contracts";
131+
132+
export default defineHook<{ args: string[] }>("before-build-task-args", (ctx) => {
133+
ctx.payload?.args.push("--offline");
134+
});
135+
```
136+
137+
### `ctx.wrap(middleware)`
138+
139+
`ctx.wrap(middleware)` puts a middleware around the hooked method. The middleware receives the method's arguments and a `next` callback; call `next` to continue, or return without calling it to short-circuit the method entirely.
111140

112141
```JavaScript
113142
module.exports = defineHook("before-prepare", (ctx) => {
@@ -118,7 +147,15 @@ module.exports = defineHook("before-prepare", (ctx) => {
118147
});
119148
```
120149

121-
`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.
150+
Only a hook point that actually folds middlewares around a method can honor `wrap()`, so it is available **only in the before-phase of the wrappable hook points** listed below. Calling it anywhere else — from any `after-` hook, or from a before-hook at a non-wrappable point — throws an error naming the hook point instead of registering a middleware that would never run.
151+
152+
The wrappable hook points are:
153+
154+
`before-buildAndroid` · `before-buildAndroidPlugin` · `before-buildIOS` · `before-checkEnvironment` · `before-checkForChanges` · `before-install` · `before-prepare` · `before-prepareNativeApp` · `before-resolveCommand` · `before-watch` · `before-watchPatterns`
155+
156+
### `ctx.abort(message)`
157+
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.
122159

123160
```JavaScript
124161
module.exports = defineHook("before-prepare", (ctx) => {
@@ -142,18 +179,18 @@ module.exports = function (hookArgs) {
142179
## The hook contract
143180

144181
The hook must return a Promise. If the hook succeeds, it must fullfil the promise, but the fullfilment value is ignored.
145-
The hook can also reject the promise with an instance of Error. The returned error can have two optional members controlling the CLI.
146-
182+
The hook can also reject the promise with an instance of Error. The returned error can carry two members that together downgrade the rejection to a warning.
183+
147184
Member | Type | Description
148185
---|---|---
149-
`stopExecution` | Boolean | Set this to `false` to let the CLI continue executing this command.
150-
`errorAsWarning` | Boolean | Set this to treat the returned error as warning. The CLI prints the error.message colored as a warning and continues executing the current command.
151-
152-
If these two members are not set, the CLI prints the returned error colored as fatal error and stops executing the current command.
186+
`errorAsWarning` | Boolean | Must be exactly `true`. The CLI prints the error.message colored as a warning and continues executing the current command.
187+
`stopExecution` | Boolean | Must be present and of type Boolean. It only enables the check — setting it alone, with either value, changes nothing.
188+
189+
**Both** members are required: the CLI continues only when `errorAsWarning === true` *and* `stopExecution` is a Boolean. Otherwise it prints the returned error colored as a fatal error and stops executing the current command.
153190

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

156-
With `defineHook` neither convention is needed: `ctx.abort` replaces throwing an error carrying `stopExecution`/`errorAsWarning`, and `ctx.wrap` replaces returning a function.
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.
157194

158195
## Legacy: parameter-name injection
159196

0 commit comments

Comments
 (0)