@@ -102,8 +102,9 @@ Options
102102-------
103103
104104` options ` is a schema keyed by the long option name — ` output ` is passed as
105- ` --output ` . Declare each entry with one of the five helpers, which fix the
106- value type:
105+ ` --output ` . It may also be a list of option groups and such schemas; see
106+ [ Option groups] ( #option-groups ) . Declare each entry with one of the five
107+ helpers, which fix the value type:
107108
108109| Helper | Declared with ` default ` | Declared without |
109110| --------------- | ----------------------- | ----------------------- |
@@ -153,9 +154,12 @@ options: {
153154The schema types ` ctx.options ` and nothing else: ` ctx.options ` carries exactly
154155the declared keys, and a typo is a compile error. There is deliberately no
155156"give me everything" escape hatch — a command declares every option it reads,
156- CLI-wide ones (` --release ` , ` --path ` , ` --bundle ` , …) included. Declaring one
157+ CLI-wide ones (` --release ` , ` --watch ` , ` --bundle ` , …) included. Declaring one
157158that the CLI already knows is supported and carries its value through to
158- ` ctx.options ` exactly as a command-specific one does.
159+ ` ctx.options ` exactly as a command-specific one does. The process-level
160+ options (` --path ` , ` --log ` , ` --verbose ` and the rest of ` CliOptions ` ) are the
161+ exception: a command lists their group instead of redeclaring them. See
162+ [ Process-level options] ( #process-level-options-clioptions ) .
159163
160164### Sharing a schema between commands
161165
@@ -170,9 +174,84 @@ const buildOptions = {
170174} satisfies CommandOptionsSchema ;
171175```
172176
177+ ### Option groups
178+
179+ ` defineOptions(name, schema) ` declares an option group: a named schema that is
180+ also an injection token. A command lists groups under ` options ` , next to
181+ inline schemas:
182+
183+ ``` ts
184+ import {
185+ booleanOption ,
186+ defineCommand ,
187+ defineOptions ,
188+ stringOption ,
189+ } from " nativescript/contracts" ;
190+
191+ export const WidgetOptions = defineOptions (" widget" , {
192+ theme: stringOption (),
193+ compact: booleanOption ({ default: false }),
194+ });
195+
196+ export default defineCommand ({
197+ name: " widget|add" ,
198+ options: [WidgetOptions , { output: stringOption ({ alias: " o" }) }],
199+ run(ctx ) {
200+ // ctx.options -> { theme: string | undefined; compact: boolean;
201+ // output: string | undefined }
202+ },
203+ });
204+ ```
205+
206+ ` ctx.options ` is typed as the merged values of every part. The class form
207+ takes the same list, and types ` this.options ` the same way.
208+
209+ Each invocation provides the values of every group it parsed in its own
210+ injector, under the group. So a per-command provider, or a service scoped to
211+ the invocation, injects the group and gets the same typed values:
212+
213+ ``` ts
214+ const widget = inject (WidgetOptions ); // { theme: string | undefined; compact: boolean }
215+ ```
216+
217+ Nothing outside an invocation can resolve a group; the root injector does not
218+ provide it. A service that needs one is scoped to the invocation; see
219+ [ Services scoped to the invocation] ( #services-scoped-to-the-invocation-providedin ) .
220+
221+ ` defineOptions ` validates the schema where it is written, as ` defineCommand `
222+ does, and reports a problem as ` Invalid option group '<name>': <problem>. `
223+ followed by the accepted form.
224+
225+ The group's registry name is ` options:<name> ` . Group names share one
226+ namespace with contract and token names, so minting a second group with a
227+ name already taken throws an error that starts with `Token name
228+ 'options: widget ' is already used by an injection token.` Pick a name that is
229+ unique to your package.
230+
231+ ### Collisions between parts
232+
233+ A spelling is an option's long name or one of its aliases. One spelling may
234+ appear in several parts of ` options ` only when every part declares it with an
235+ identical spec: the same type, ` default ` , ` alias ` and ` hasSensitiveValue ` .
236+ ` description ` is not compared. An alias may not equal another option's name
237+ or alias.
238+
239+ ` defineCommand ` and ` Command() ` check this when they are called, and
240+ ` defineOptions ` checks it within its own schema. The message names both
241+ declarations:
242+
243+ ```
244+ Invalid command definition for 'widget|add': option '--compact' is declared
245+ by option group 'widget' and by the command's own options with different specs.
246+ ```
247+
248+ followed by the accepted form. A group contributed from outside the command
249+ collides under the same rules; see
250+ [ Option groups contributed from outside the command] ( #option-groups-contributed-from-outside-the-command ) .
251+
173252### Redeclaring a CLI-wide option, and shadowing one
174253
175- ` --verbose ` , ` --path ` , ` --log ` , ` --release ` , ` --env ` and friends are declared by
254+ ` --release ` , ` --env ` , ` --watch ` , ` --device ` and friends are declared by
176255the CLI itself. A command's declaration is merged over the CLI-wide dictionary
177256for the duration of that command, and that merge is the sanctioned way to give
178257a global option a per-command default — ` watch ` , ` hmr ` and ` skipNative ` all
@@ -190,14 +269,96 @@ still warns about at registration is a redeclaration that changes what the
190269spelling _ means_ :
191270
192271- a declared option whose name matches a CLI-wide one but whose type differs —
193- ` verbose : stringOption()` against the CLI's boolean ` --verbose ` ;
272+ ` release : stringOption()` against the CLI's boolean ` --release ` ;
194273- an alias that belongs to a _ different_ CLI-wide option — `output:
195- stringOption({ alias: "p " })` steals ` --path `'s shorthand. Restating an
196- option's own shorthand (` path: stringOption ({ alias: "p " })` ) is fine.
274+ stringOption({ alias: "f " })` steals ` --force `'s shorthand. Restating an
275+ option's own shorthand (` force: booleanOption ({ alias: "f " })` ) is fine.
197276
198277A redeclaration that leaves ` alias ` , ` default ` or ` hasSensitiveValue ` unset
199- keeps what the CLI-wide declaration carries for them, so ` path: stringOption() `
200- still answers to ` -p ` and stays out of the logs; set one only to change it.
278+ keeps what the CLI-wide declaration carries for them, so `release:
279+ booleanOption()` still answers to ` -r` , and ` device: stringOption()` stays out
280+ of the logs; set one only to change it.
281+
282+ None of this applies to the process-level options. Redeclaring one of those is
283+ an error, not a warning.
284+
285+ ### Process-level options: ` CliOptions `
286+
287+ ` CliOptions ` is the group of options the CLI parses once at startup, before a
288+ command is chosen: ` --log ` , ` --verbose ` , ` --version ` (` -v ` ), ` --help ` (` -h ` ),
289+ ` --profileDir ` , ` --analyticsClient ` , ` --path ` (` -p ` ) and ` --config ` (` -c ` ).
290+ The services that run for every command read them: the logger, analytics,
291+ project resolution. The group is provided at the root, so any service injects
292+ it, inside an invocation or not:
293+
294+ ``` ts
295+ import { CliOptions , inject } from " nativescript/contracts" ;
296+
297+ const { path, verbose } = inject (CliOptions );
298+ ```
299+
300+ A command may not redeclare any of these spellings, as a name or as an alias.
301+ The CLI refuses the command when it compiles it, on the first resolution of a
302+ registered command or when ` runCommand ` is given the definition:
303+
304+ ```
305+ Command 'widget|add': '--path' is a process-level option; list CliOptions under 'options', or inject it, instead of redeclaring it
306+ ```
307+
308+ To have the values on ` ctx.options ` , list the group itself. That is not a
309+ redeclaration: it reads the same declaration. ` install ` does this:
310+
311+ ``` ts
312+ const installCommandOptions = [
313+ CliOptions ,
314+ {
315+ frameworkPath: stringOption (),
316+ disableNpmInstall: booleanOption (),
317+ ignoreScripts: booleanOption (),
318+ } satisfies CommandOptionsSchema ,
319+ ] satisfies CommandOptionsInput ;
320+ ```
321+
322+ ` ctx.options.path ` is then ` string | undefined ` . A listed ` CliOptions ` is not
323+ provided again per invocation; injecting it still resolves the root's values.
324+
325+ ### Option groups contributed from outside the command
326+
327+ The ` OptionContributions ` contract adds a group to a command the caller does
328+ not own, or to the process-level options:
329+
330+ ``` ts
331+ import { inject , OptionContributions } from " nativescript/contracts" ;
332+
333+ const contributions = inject (OptionContributions );
334+ contributions .contributeToCommand (" run|ios" , WidgetOptions );
335+ contributions .contributeToRoot (TelemetryOptions );
336+ ```
337+
338+ ` forCommand(name) ` and ` forRoot() ` return what has been contributed so far.
339+
340+ A group contributed to a command is parsed with that command's options and
341+ provided per invocation, like the command's own groups. It is not on
342+ ` ctx.options ` , since the command's type does not know it; a handler or service
343+ reads it by injecting the group. It collides under the rules above, with the
344+ command's own parts and with other contributions, and it may not redeclare a
345+ process-level spelling. A collision is reported when the command's options are
346+ parsed, as ` Command '<name>': <problem> ` . The name is one the definition
347+ declares or one it was registered under, such as an extension's manifest key,
348+ and only commands defined with ` defineCommand ` or ` Command() ` read
349+ contributions.
350+
351+ A group contributed to the root joins the process-level table. It is parsed
352+ with ` CliOptions ` on the next parse after it is registered, its values are
353+ provided at the root, and its spellings are process-level: no command may
354+ redeclare them. ` contributeToRoot ` throws ` Option group '<name>': <problem> `
355+ when the group collides with ` CliOptions ` or with an earlier root group.
356+
357+ A contribution is read when the parse it targets happens, so it must be
358+ registered before that parse. One registered later does not change a parse
359+ that already happened. There is no manifest-level way to declare a
360+ contribution yet, so only code that has already run can contribute; see
361+ [ extensions.md] ( extensions.md#options-and-option-groups ) .
201362
202363### How validation behaves
203364
@@ -358,8 +519,9 @@ The run context
358519 declare, ` {} ` when there are none. It is spelled ` params ` because
359520 ` params ` is a reserved binding name in strict mode, so a destructuring
360521 ` const { args, arguments } = ctx ` would not even parse.
361- - ` ctx.options ` — the current value of each declared option, read at the moment
362- the command executes.
522+ - ` ctx.options ` — the value of each option the command declares, its groups and
523+ inline schemas merged, read when the invocation opens. A group contributed
524+ from outside the command is not on it; inject the group instead.
363525- ` ctx.injector ` — this invocation's injector, a child of the one the command
364526 was registered against; see
365527 [ Injection, and the first ` await ` ] ( #injection-and-the-first-await ) .
@@ -444,8 +606,8 @@ async run(ctx) {
444606` ctx.inject(...) ` : it is a visibly different mechanism because it obeys
445607different rules, and mistaking one for the other is exactly the bug this shape
446608prevents. It is the ** invocation's own injector** : a child of the one the
447- command was registered against, holding the context under ` COMMAND_CONTEXT `
448- and any per-command providers — see
609+ command was registered against, holding the context under ` COMMAND_CONTEXT ` ,
610+ the values of the option groups it parsed, and any per-command providers — see
449611[ Registering a definition] ( #registering-a-definition ) . ` inject() ` before the
450612first ` await ` and ` ctx.injector.get() ` after it are therefore the same lookup
451613against the same injector. The same guidance, and the reasoning behind it, is
@@ -570,6 +732,57 @@ Sharing is either of two things, and neither of them is a bag:
570732 which asks that command itself; see [ Asking another
571733 command] ( #asking-another-command ) .
572734
735+ ### Services scoped to the invocation: ` providedIn `
736+
737+ A service that reads the invocation, through its option groups or
738+ ` COMMAND_CONTEXT ` , cannot live at the root: the root provides neither. Scope
739+ it to the invocation instead, in one of three places:
740+
741+ ``` ts
742+ import {
743+ COMMAND_CONTEXT ,
744+ Contract ,
745+ inject ,
746+ ProvidedIn ,
747+ } from " nativescript/contracts" ;
748+
749+ // on the implementation class
750+ @ProvidedIn (" invocation" )
751+ export class WidgetRenderer {
752+ private widget = inject (WidgetOptions );
753+ private context = inject (COMMAND_CONTEXT );
754+ }
755+
756+ // on a contract token, for every implementation of it
757+ @Contract ({ name: " widgetRenderer" , providedIn: " invocation" })
758+ export abstract class WidgetRendererContract {}
759+
760+ // on one provider
761+ { provide : WidgetRenderer , useClass : WidgetRenderer , providedIn : " invocation" }
762+ ```
763+
764+ The provider's ` providedIn ` wins over the class's marker, and the class's over
765+ the token's. The registration may stay where it is, the root included; the
766+ instance is built and cached on the nearest invocation injector above the
767+ lookup, and its own dependencies resolve there. That is why it can inject
768+ option groups and ` COMMAND_CONTEXT ` . Each invocation gets its own instance,
769+ an in-process dispatch included.
770+
771+ Resolving such a service from outside an invocation is an error, even with
772+ ` optional: true ` . That covers the root, and a root singleton that injects it
773+ in its constructor or a field, because a root singleton resolves against the
774+ root:
775+
776+ ```
777+ <token> is provided in the 'invocation' scope; it cannot be resolved from outside one
778+ ```
779+
780+ A scoped instance is recorded on the invocation injector that holds it and is
781+ disposed with that injector when the invocation ends: after ` postRun ` when
782+ the command has one, else after ` run ` , or when ` canExecute ` refuses. A scoped
783+ service with a ` dispose() ` method gets per-invocation cleanup for free; the
784+ root singletons the invocation reached are the root's and stay.
785+
573786### ` setup ` , when a command has one
574787
575788` setup(ctx) ` runs once per invocation, after the preconditions and before the
@@ -1003,9 +1216,11 @@ name still identifies it for hooks and reporting.
10031216
10041217A definition run as given is compiled against an injector chosen at the call,
10051218the way Angular's ` createComponent ` takes one: the ` injector ` in the options
1006- bag when one is passed, otherwise the injection context the call is made from,
1007- otherwise the CLI's root. So a definition dispatched from inside an
1008- extension's command lands under the extension's scope, where ` registerCommand `
1219+ bag when one is passed, otherwise the invocation running now, which starts with
1220+ the injection context the call is made from (see [ The invocation running
1221+ now] ( #the-invocation-running-now ) ), otherwise the CLI's root. So a definition
1222+ dispatched from inside an extension's command lands under the extension's
1223+ scope, where ` registerCommand `
10091224would have placed it, and a caller holding a scope of its own names it:
10101225
10111226``` ts
@@ -1052,6 +1267,32 @@ argument list to a child that declares fewer is a rejection, not a wider check.
10521267` canExecuteCommand ` follows ` runCommand ` in everything else: the same option
10531268priming and restoration, the same routing of a parent name to its subcommand.
10541269
1270+ ### The invocation running now
1271+
1272+ Code that resolves by name outside an injection context, such as a hook or a
1273+ plugin's callback, reaches the running invocation through
1274+ ` currentInvocationInjector() ` , exported from ` "nativescript/contracts" ` . It
1275+ tries, in order:
1276+
1277+ 1 . the synchronous injection context, when there is one;
1278+ 2 . the invocation whose asynchronous flow the caller is in;
1279+ 3 . the most recently opened invocation still open, for a callback that lost
1280+ its asynchronous context, such as an emitter another invocation registered
1281+ or a library timer;
1282+ 4 . ` null ` .
1283+
1284+ The CLI uses it in two places. A hook runs against it, so its by-name
1285+ dependencies can come from the invocation's providers and scoped services; it
1286+ falls back to the root when there is no invocation. ` runCommand(definition) `
1287+ with no ` injector ` option compiles the definition against it, with the same
1288+ fallback.
1289+
1290+ The first invocation of the process, the command line's own, stays open for
1291+ the life of the process, so a long-lived command's callbacks keep resolving
1292+ through it after its ` run ` has returned. Every later invocation closes when it
1293+ finishes, and an in-process dispatch closes any invocation it opened,
1294+ including one opened only to answer ` canExecuteCommand ` .
1295+
10551296### Key shortcuts
10561297
10571298The interactive keys ` ns start ` and ` ns run ` offer are the CLI's own caller. A
0 commit comments