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