You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: extending-cli.md
+55-18Lines changed: 55 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,7 +11,7 @@ For the NativeScript CLI to execute your hooks, you must place them in the `hook
11
11
12
12
You can attach the hook before or after `prepare` operations or to `--watch` operations.
13
13
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.
15
15
16
16
Your hooks must conform to the following naming and placement conventions:
17
17
@@ -36,27 +36,29 @@ Your hooks must conform to the following naming and placement conventions:
36
36
├── hook1 (this is an executable file)
37
37
└── hook2 (this is an executable file)
38
38
```
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:
40
40
41
41
```
42
42
my-app/
43
43
├── index.js
44
44
├── package.json
45
45
└── hooks/
46
-
└── watch.js (this is a Node.js script)
46
+
└── before-watch.js (this is a Node.js script)
47
47
```
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:
49
49
50
50
```
51
51
my-app/
52
52
├── index.js
53
53
├── package.json
54
54
└── hooks/
55
-
└── watch (a directory)
55
+
└── before-watch (a directory)
56
56
├── hook1 (this is an executable file)
57
57
└── hook2 (this is an executable file)
58
58
```
59
59
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
+
60
62
> **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.
61
63
62
64
Execute Hooks as Child Process
@@ -81,24 +83,39 @@ The CLI assumes that this is a CommonJS module and calls the hook it exports —
81
83
82
84
## Writing a hook
83
85
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.
`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
+
95
110
Services come from `inject()` — the same API used everywhere else (see [dependency-injection.md](dependency-injection.md)):
96
111
97
112
*`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.
98
113
* 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.
99
114
* 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.
100
115
* 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(...)`.
101
116
117
+
### `ctx.payload`
118
+
102
119
`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:
`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`:
`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.
`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.
`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.
@@ -142,18 +179,18 @@ module.exports = function (hookArgs) {
142
179
## The hook contract
143
180
144
181
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
+
147
184
Member | Type | Description
148
185
---|---|---
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.
153
190
154
191
A plain-function hook can also return a function, which the CLI folds into a middleware chain around the hooked method.
155
192
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.
0 commit comments