Skip to content

Commit 7e6525a

Browse files
committed
feat(commands): option groups, invocation scopes and the invocation record
- `defineOptions(name, schema)` declares an option group: a schema that is also an injection token typed as its parsed values. `options` takes a schema or a list of groups and schemas, and `ctx.options` is typed as the merged values. Spellings collide only with different specs; an alias may not equal another option's spelling. - Every non-root group a command declares is provided in the invocation injector with its slice of the parsed values. `CliOptions` is the process-level group, provided at the root; the option table derives its global entries from it, and a command may list it but not redeclare its spellings. The built-ins that redeclared `--path` or `--help` list it. - `OptionContributions` adds groups to a command by any of its names, own or registered, or to the root, ahead of the parse that targets them. - `providedIn` on a provider, an implementation class (`@ProvidedIn`) or a contract puts the instance on the nearest injector of that scope in the resolving chain; the adapter opens each invocation's injector in the `invocation` scope and disposes it when the invocation ends. Resolving a scoped record from outside its scope is an error, `optional` or not. - `currentInvocationInjector()` resolves the synchronous injection context, then the invocation's asynchronous flow, then the most recently opened invocation. Hooks resolve their by-name dependencies against it, and a definition run as given defaults its scope to it. An in-process dispatch closes the invocations it opened.
1 parent b41cadd commit 7e6525a

28 files changed

Lines changed: 2794 additions & 249 deletions

‎lib/commands/create-project.ts‎

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { color } from "../color";
33
import {
44
booleanOption,
55
Command,
6+
CommandOptionsInput,
67
CommandOptionsSchema,
7-
CommandOptionValues,
8+
OptionValuesOf,
89
stringOption,
910
} from "../common/define-command";
11+
import { CliOptions } from "../common/contracts/cli-options";
1012
import { inject } from "../common/di";
1113
import { isInteractive } from "../common/helpers";
1214
import * as constants from "../constants";
@@ -27,27 +29,29 @@ const TABS_TEMPLATE_KEY = "Tabs";
2729
const TABS_TEMPLATE_DESCRIPTION =
2830
"An app with pre-built pages that uses tabs for navigation";
2931

30-
const createProjectCommandOptions = {
31-
js: booleanOption(),
32-
ng: booleanOption(),
33-
react: booleanOption(),
34-
solid: booleanOption(),
35-
svelte: booleanOption(),
36-
tsc: booleanOption(),
37-
vue: booleanOption(),
38-
vuejs: booleanOption(),
39-
vision: booleanOption(),
40-
"vision-ng": booleanOption(),
41-
"vision-react": booleanOption(),
42-
"vision-solid": booleanOption(),
43-
"vision-svelte": booleanOption(),
44-
"vision-vue": booleanOption(),
45-
template: stringOption(),
46-
appid: stringOption(),
47-
path: stringOption(),
48-
force: booleanOption(),
49-
ignoreScripts: booleanOption(),
50-
} satisfies CommandOptionsSchema;
32+
const createProjectCommandOptions = [
33+
CliOptions,
34+
{
35+
js: booleanOption(),
36+
ng: booleanOption(),
37+
react: booleanOption(),
38+
solid: booleanOption(),
39+
svelte: booleanOption(),
40+
tsc: booleanOption(),
41+
vue: booleanOption(),
42+
vuejs: booleanOption(),
43+
vision: booleanOption(),
44+
"vision-ng": booleanOption(),
45+
"vision-react": booleanOption(),
46+
"vision-solid": booleanOption(),
47+
"vision-svelte": booleanOption(),
48+
"vision-vue": booleanOption(),
49+
template: stringOption(),
50+
appid: stringOption(),
51+
force: booleanOption(),
52+
ignoreScripts: booleanOption(),
53+
} satisfies CommandOptionsSchema,
54+
] satisfies CommandOptionsInput;
5155

5256
interface ITemplateChoice {
5357
key?: string;
@@ -217,7 +221,7 @@ const flavorTemplates: { [flavorName: string]: () => ITemplateChoice[] } = {
217221

218222
/** The template a flavor flag selects, without asking anything. */
219223
function selectTemplateFromOptions(
220-
options: CommandOptionValues<typeof createProjectCommandOptions>,
224+
options: OptionValuesOf<typeof createProjectCommandOptions>,
221225
): string {
222226
if (options["vision-ng"] || (options.vision && options.ng)) {
223227
return constants.RESERVED_TEMPLATE_NAMES["vision-ng"];

‎lib/commands/install.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { IFileSystem } from "../common/declarations";
33
import {
44
booleanOption,
55
CommandContext,
6+
CommandOptionsInput,
67
CommandOptionsSchema,
78
defineCommand,
89
stringOption,
910
} from "../common/define-command";
11+
import { CliOptions } from "../common/contracts/cli-options";
1012
import { PlatformTypes } from "../constants";
1113
import {
1214
INodePackageManager,
@@ -19,12 +21,14 @@ import { IProjectDataService } from "../definitions/project";
1921
import { ProjectData } from "../contracts/project-data";
2022
import { provideProject } from "./command-base";
2123

22-
const installCommandOptions = {
23-
frameworkPath: stringOption(),
24-
disableNpmInstall: booleanOption(),
25-
ignoreScripts: booleanOption(),
26-
path: stringOption(),
27-
} satisfies CommandOptionsSchema;
24+
const installCommandOptions = [
25+
CliOptions,
26+
{
27+
frameworkPath: stringOption(),
28+
disableNpmInstall: booleanOption(),
29+
ignoreScripts: booleanOption(),
30+
} satisfies CommandOptionsSchema,
31+
] satisfies CommandOptionsInput;
2832

2933
async function installProjectDependencies(
3034
context: CommandContext<typeof installCommandOptions>,

‎lib/commands/plugin/build-plugin.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ import {
88
import { IFileSystem } from "../../common/declarations";
99
import {
1010
Command,
11+
CommandOptionsInput,
1112
CommandOptionsSchema,
1213
stringOption,
1314
} from "../../common/define-command";
15+
import { CliOptions } from "../../common/contracts/cli-options";
1416
import { inject } from "../../common/di";
1517
import { ITempService } from "../../definitions/temp-service";
1618

17-
const buildPluginCommandOptions = {
18-
path: stringOption(),
19-
gradlePath: stringOption(),
20-
gradleArgs: stringOption(),
21-
} satisfies CommandOptionsSchema;
19+
const buildPluginCommandOptions = [
20+
CliOptions,
21+
{
22+
gradlePath: stringOption(),
23+
gradleArgs: stringOption(),
24+
} satisfies CommandOptionsSchema,
25+
] satisfies CommandOptionsInput;
2226

2327
export class BuildPluginCommand extends Command({
2428
name: "plugin|build",

‎lib/commands/plugin/create-plugin.ts‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { INodePackageManager } from "../../declarations";
44
import { IErrors, IFileSystem, IChildProcess } from "../../common/declarations";
55
import {
66
Command,
7+
CommandOptionsInput,
78
CommandOptionsSchema,
89
stringOption,
910
} from "../../common/define-command";
11+
import { CliOptions } from "../../common/contracts/cli-options";
1012
import { inject } from "../../common/di";
1113
import { ITerminalSpinnerService } from "../../definitions/terminal-spinner-service";
1214

@@ -21,14 +23,16 @@ export const INCLUDE_ANGULAR_DEMO_MESSAGE =
2123
export const PATH_ALREADY_EXISTS_MESSAGE_TEMPLATE =
2224
"Path already exists and is not empty %s";
2325

24-
const createPluginCommandOptions = {
25-
path: stringOption(),
26-
template: stringOption(),
27-
username: stringOption(),
28-
pluginName: stringOption(),
29-
includeTypeScriptDemo: stringOption(),
30-
includeAngularDemo: stringOption(),
31-
} satisfies CommandOptionsSchema;
26+
const createPluginCommandOptions = [
27+
CliOptions,
28+
{
29+
template: stringOption(),
30+
username: stringOption(),
31+
pluginName: stringOption(),
32+
includeTypeScriptDemo: stringOption(),
33+
includeAngularDemo: stringOption(),
34+
} satisfies CommandOptionsSchema,
35+
] satisfies CommandOptionsInput;
3236

3337
export class CreatePluginCommand extends Command({
3438
name: "plugin|create",

‎lib/commands/test-init.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { INodePackageManager } from "../declarations";
77
import { IPluginsService } from "../definitions/plugins";
88
import {
99
Command,
10+
CommandOptionsInput,
1011
CommandOptionsSchema,
1112
booleanOption,
1213
stringOption,
1314
} from "../common/define-command";
15+
import { CliOptions } from "../common/contracts/cli-options";
1416
import { inject } from "../common/di";
1517
import {
1618
IDictionary,
@@ -26,13 +28,15 @@ const karmaConfigAdditionalFrameworks: IDictionary<string[]> = {
2628
mocha: ["chai"],
2729
};
2830

29-
const testInitCommandOptions = {
30-
framework: stringOption(),
31-
disableNpmInstall: booleanOption(),
32-
frameworkPath: stringOption(),
33-
ignoreScripts: booleanOption(),
34-
path: stringOption(),
35-
} satisfies CommandOptionsSchema;
31+
const testInitCommandOptions = [
32+
CliOptions,
33+
{
34+
framework: stringOption(),
35+
disableNpmInstall: booleanOption(),
36+
frameworkPath: stringOption(),
37+
ignoreScripts: booleanOption(),
38+
} satisfies CommandOptionsSchema,
39+
] satisfies CommandOptionsInput;
3640

3741
export class TestInitCommand extends Command({
3842
name: "test|init",

‎lib/common/bootstrap.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ injector.require("stringParameter", "./command-params");
2323
injector.require("stringParameterBuilder", "./command-params");
2424

2525
injector.require("commandsService", "./services/commands-service");
26+
injector.require("optionContributions", "./services/option-contributions");
2627

2728
injector.require("messagesService", "./services/messages-service");
2829

‎lib/common/commands/help.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import * as _ from "lodash";
22
import { CommandRegistry } from "../contracts/command-registry";
33
import { IHelpService } from "../declarations";
4-
import { booleanOption, defineCommand } from "../define-command";
4+
import { CliOptions } from "../contracts/cli-options";
5+
import { defineCommand } from "../define-command";
56
import { inject } from "../di";
67

78
export const helpCommandDefinition = defineCommand({
89
name: ["help", "/?"],
910
description: "Shows the help for a command.",
10-
options: {
11-
help: booleanOption(),
12-
},
11+
options: [CliOptions],
1312
// The command names whatever command it explains, so every argument after
1413
// the first is that command's own.
1514
arguments: "any",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { booleanOption, defineOptions, stringOption } from "../define-command";
2+
3+
/**
4+
* The process-level options: parsed once at startup, before a command is
5+
* chosen, and read by the services that run for every command — the logger,
6+
* analytics, project resolution. Provided at the root, so any service injects
7+
* it. Its spellings are protected: a command may not redeclare one.
8+
*/
9+
export const CliOptions = defineOptions("cli", {
10+
log: stringOption(),
11+
verbose: booleanOption(),
12+
version: booleanOption({ alias: "v" }),
13+
help: booleanOption({ alias: "h" }),
14+
profileDir: stringOption({ hasSensitiveValue: true }),
15+
analyticsClient: stringOption(),
16+
path: stringOption({ alias: "p", hasSensitiveValue: true }),
17+
config: stringOption({ alias: "c", hasSensitiveValue: true }),
18+
});

‎lib/common/contracts/commands-service.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import type { CommandReference } from "../define-command";
55
export interface CommandDispatchOptions {
66
/**
77
* The injector a definition run as given is compiled against, the way
8-
* Angular's `createComponent` takes one. Omitted, the call's own injection
9-
* context is used, and the root when there is none. A registered name keeps
10-
* the scope it was registered under, so passing one with a name throws.
8+
* Angular's `createComponent` takes one. Omitted, the invocation running
9+
* now is used - the call's own injection context, else the invocation its
10+
* asynchronous flow belongs to, else the most recently opened one - and
11+
* the root when there is none. A registered name keeps the scope it was
12+
* registered under, so passing one with a name throws.
1113
*/
1214
injector?: Injector;
1315
}

‎lib/common/contracts/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ export { COMMAND_PRECONDITIONS } from "./command-preconditions";
1919
export type { CommandPrecondition } from "./command-preconditions";
2020
export { CommandsService } from "./commands-service";
2121
export type { CommandDispatchOptions } from "./commands-service";
22+
export { CliOptions } from "./cli-options";
23+
export { OptionContributions } from "./option-contributions";
2224
export { ModuleRegistry } from "./module-registry";
2325
export { PublicApiBuilder } from "./public-api-builder";

0 commit comments

Comments
 (0)