@loomcli/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/application.d.ts +44 -31
- package/dist/application.js +122 -110
- package/dist/bindings.d.ts +26 -0
- package/dist/bindings.js +45 -0
- package/dist/chain.d.ts +11 -6
- package/dist/chain.js +28 -81
- package/dist/command.d.ts +161 -84
- package/dist/command.js +650 -349
- package/dist/errors.d.ts +7 -2
- package/dist/errors.js +9 -1
- package/dist/extension.d.ts +3 -1
- package/dist/extension.js +8 -11
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +10 -3
- package/dist/globals.d.ts +45 -12
- package/dist/globals.js +74 -18
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/inspect.d.ts +25 -2
- package/dist/inspect.js +43 -5
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +73 -0
- package/dist/options.js +124 -27
- package/dist/output.d.ts +2 -0
- package/dist/output.js +5 -1
- package/dist/plugin.d.ts +107 -53
- package/dist/plugin.js +204 -66
- package/dist/sources.d.ts +56 -0
- package/dist/sources.js +249 -0
- package/dist/types.d.ts +65 -20
- package/dist/validation.d.ts +22 -8
- package/dist/validation.js +184 -77
- package/dist/view.d.ts +1 -1
- package/dist/view.js +2 -2
- package/package.json +3 -2
package/dist/plugin.d.ts
CHANGED
|
@@ -1,46 +1,34 @@
|
|
|
1
1
|
import type { MiddlewareContext } from './chain.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AttachedChild, Command } from './command.js';
|
|
3
|
+
import type { AnyExtension, DescriptorRegistry } from './extension.js';
|
|
4
|
+
import type { InputRecords } from './globals.js';
|
|
5
|
+
import type { CommandGraph, OptionNode } from './inspect.js';
|
|
6
|
+
import type { OptionValues } from './options.js';
|
|
3
7
|
import type { ProcessSignal } from './signals.js';
|
|
4
8
|
import type { Palette } from './style-state.js';
|
|
5
|
-
import type { ThemeConstraint, ThemeMapping } from './style.js';
|
|
6
|
-
import type { CommandAttachHook, OptionValue, PluginOptionConfig } from './types.js';
|
|
9
|
+
import type { ContextualStyle, ThemeConstraint, ThemeMapping } from './style.js';
|
|
10
|
+
import type { CommandAttachHook, Host, OptionValue, Out, PluginOptionConfig } from './types.js';
|
|
7
11
|
import type { OptionInput } from './validation.js';
|
|
8
12
|
import type { ViewContribution } from './view.js';
|
|
9
13
|
/**
|
|
10
14
|
* The declaration record a plugin contributes its options under: the parsing part of an option
|
|
11
15
|
* config, keyed by option name. A plugin option carries no schema and no presence rule, so the
|
|
12
|
-
* config type publishes neither, and
|
|
16
|
+
* config type publishes neither, and `plugin()` repeats the rule for a JavaScript author.
|
|
13
17
|
*/
|
|
14
18
|
type PluginOptions = Readonly<Record<string, PluginOptionConfig>>;
|
|
15
19
|
/** The values one plugin's own options take, read through the same rules an action's options are. */
|
|
16
20
|
type PluginOptionValues<Options extends PluginOptions> = {
|
|
17
21
|
readonly [Name in keyof Options]: OptionValue<Options[Name]>;
|
|
18
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* The spelling that supplied each of one plugin's own options given as a token, such as `-h`,
|
|
25
|
+
* `--help`, or `--no-total`. An option filled by an input source, defaulted, or not supplied has
|
|
26
|
+
* no entry.
|
|
27
|
+
*/
|
|
28
|
+
type PluginOptionSpellings<Options extends PluginOptions> = Readonly<Partial<Record<keyof Options & string, string>>>;
|
|
19
29
|
/** Phantom key. It carries a plugin's declared options in a read position and holds no value. */
|
|
20
30
|
declare const pluginOptions: unique symbol;
|
|
21
31
|
declare const pluginTheme: unique symbol;
|
|
22
|
-
/**
|
|
23
|
-
* One plugin's declarations as the registry holds them, with the generic parts erased. Build reads
|
|
24
|
-
* every one of them defensively, because a JavaScript author reaches the same slots, so the erased
|
|
25
|
-
* shape is what the rules below read and no declaration is claimed to be well formed here.
|
|
26
|
-
*/
|
|
27
|
-
interface DeclaredPlugin {
|
|
28
|
-
theme?: unknown;
|
|
29
|
-
options?: PluginOptions;
|
|
30
|
-
middleware?: {
|
|
31
|
-
activate?: unknown;
|
|
32
|
-
load?: unknown;
|
|
33
|
-
};
|
|
34
|
-
onCommandAttach?: unknown;
|
|
35
|
-
extensions?: readonly AnyExtension[];
|
|
36
|
-
views?: unknown;
|
|
37
|
-
signals?: unknown;
|
|
38
|
-
}
|
|
39
|
-
/** The declarations behind one plugin value, read by this package alone. */
|
|
40
|
-
interface PluginNode {
|
|
41
|
-
definition: DeclaredPlugin;
|
|
42
|
-
identity: unknown;
|
|
43
|
-
}
|
|
44
32
|
/**
|
|
45
33
|
* The runtime value `plugin()` returns. `Options` appears in a read position alone, which makes it
|
|
46
34
|
* covariant: a `plugins` list holds plugins with different options the way `views` holds
|
|
@@ -49,7 +37,7 @@ interface PluginNode {
|
|
|
49
37
|
declare class PluginDeclaration<Options extends PluginOptions, Theme extends ThemeMapping> {
|
|
50
38
|
readonly [pluginTheme]: Theme;
|
|
51
39
|
readonly [pluginOptions]: () => Options;
|
|
52
|
-
constructor(node:
|
|
40
|
+
constructor(node: BuiltPlugin);
|
|
53
41
|
}
|
|
54
42
|
/**
|
|
55
43
|
* One plugin, as the opaque value `plugin()` returns. The declarations behind it stay private to
|
|
@@ -61,8 +49,38 @@ type OptionsOf<Contributor> = Contributor extends Plugin<infer Options> ? Option
|
|
|
61
49
|
/** A middleware reads its own plugin's options and either takes over or continues the chain. */
|
|
62
50
|
type Middleware<Contributor extends Plugin | ((...args: never[]) => Plugin)> = (context: MiddlewareContext<OptionsOf<Contributor>>) => Promise<void> | void;
|
|
63
51
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
52
|
+
* What a configuration source receives: the host, its own plugin's option values, resolved from
|
|
53
|
+
* argv, the environment, and their defaults, the `OptionNode` of every option core asks about, and
|
|
54
|
+
* the ordinary channels a middleware and an action already read. Each request is a node inside
|
|
55
|
+
* `graph`, the graph `inspect()` returns for the run. `out` is the channel a middleware receives,
|
|
56
|
+
* and `style` the contextual style an action receives, so a source warns and escapes as they do.
|
|
57
|
+
*/
|
|
58
|
+
interface SourceContext<Options extends PluginOptions = PluginOptions> {
|
|
59
|
+
readonly host: Host;
|
|
60
|
+
readonly options: PluginOptionValues<Options>;
|
|
61
|
+
readonly requests: readonly OptionNode[];
|
|
62
|
+
readonly graph: CommandGraph;
|
|
63
|
+
readonly out: Out;
|
|
64
|
+
readonly style: ContextualStyle;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One answer: a value of the option's raw type, a string, a Boolean, or a list of strings for a
|
|
68
|
+
* multiple option, and the one-line label core prints in a diagnostic about the value.
|
|
69
|
+
*/
|
|
70
|
+
interface SourceAnswer {
|
|
71
|
+
readonly value: string | boolean | readonly string[];
|
|
72
|
+
readonly label: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A configuration source answers the requested options by declared name. A requested option with
|
|
76
|
+
* no key in the record has no answer and falls through to its default.
|
|
77
|
+
*/
|
|
78
|
+
type SourceResolver<Contributor extends Plugin | ((...args: never[]) => Plugin)> = (context: SourceContext<OptionsOf<Contributor>>) => Promise<Readonly<Record<string, SourceAnswer>>>;
|
|
79
|
+
/**
|
|
80
|
+
* Everything a plugin declares. `plugin()` checks every rule the definition carries on its own, and
|
|
81
|
+
* creating and installing the value runs none of its code: a hook runs at graph build, the
|
|
82
|
+
* middleware runs inside an invocation, and the configuration source runs in the input-source
|
|
83
|
+
* stage when an unfilled option carries its binding.
|
|
66
84
|
*/
|
|
67
85
|
interface PluginDefinition<Options extends PluginOptions = PluginOptions, Theme extends ThemeMapping = ThemeMapping> {
|
|
68
86
|
theme?: Theme & ThemeConstraint<Theme>;
|
|
@@ -77,32 +95,61 @@ interface PluginDefinition<Options extends PluginOptions = PluginOptions, Theme
|
|
|
77
95
|
extensions?: readonly AnyExtension[];
|
|
78
96
|
views?: readonly ViewContribution[];
|
|
79
97
|
signals?: readonly ('SIGINT' | 'SIGTERM')[];
|
|
98
|
+
source?: {
|
|
99
|
+
binding: AnyExtension & {
|
|
100
|
+
readonly target: 'option';
|
|
101
|
+
};
|
|
102
|
+
load: () => Promise<{
|
|
103
|
+
default: SourceResolver<Plugin<Options>>;
|
|
104
|
+
}>;
|
|
105
|
+
};
|
|
106
|
+
commands?: readonly Command<unknown, unknown>[];
|
|
80
107
|
}
|
|
81
108
|
/**
|
|
82
109
|
* One plugin: an identity and the contributions it carries. Creating and installing the value runs
|
|
83
110
|
* none of its code: a hook runs at graph build, and the middleware runs inside an invocation, so an
|
|
84
|
-
* installed plugin an invocation never reaches costs that invocation its hooks alone.
|
|
111
|
+
* installed plugin an invocation never reaches costs that invocation its hooks alone. Every rule
|
|
112
|
+
* that one definition carries on its own throws here, before the value exists.
|
|
85
113
|
*/
|
|
86
114
|
declare function plugin<Options extends PluginOptions = {}, const Theme extends ThemeMapping = {}>(identity: string, definition: PluginDefinition<Options, Theme>): Plugin<NoInfer<Options>, NoInfer<Theme>>;
|
|
87
|
-
/** One installed plugin, with the declarations build reads out of it in installation order. */
|
|
88
|
-
interface InstalledPlugin {
|
|
89
|
-
declaration: DeclaredPlugin;
|
|
90
|
-
identity: string;
|
|
91
|
-
}
|
|
92
115
|
/** How every plugin diagnostic names one plugin at the start of a sentence. */
|
|
93
116
|
declare function pluginSentence(identity: string): string;
|
|
117
|
+
/** What the installed list resolves to: the plugins in order, and every descriptor they define. */
|
|
118
|
+
interface InstalledPlugins {
|
|
119
|
+
descriptors: DescriptorRegistry;
|
|
120
|
+
plugins: readonly BuiltPlugin[];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The installed list in composition order, with every rule that reads two plugins together: an
|
|
124
|
+
* identity installed twice, a second claim on the theme slot, the signals slot, or the
|
|
125
|
+
* configuration source, and two distinct descriptors under one identity. The slot is read
|
|
126
|
+
* defensively, because a JavaScript author reaches it with any value. Each plugin's own rules
|
|
127
|
+
* already ran at its `plugin()` call.
|
|
128
|
+
*/
|
|
129
|
+
declare function installPlugins(plugins: unknown): InstalledPlugins;
|
|
94
130
|
/**
|
|
95
|
-
* The
|
|
96
|
-
*
|
|
97
|
-
*
|
|
131
|
+
* The default export one plugin loader resolves to, checked by the guard its caller supplies. A
|
|
132
|
+
* loader that throws where it is called and one that rejects later are one failure, and a module
|
|
133
|
+
* without the export names the kind of function it owed, such as `middleware` or `source`.
|
|
98
134
|
*/
|
|
99
|
-
declare function
|
|
135
|
+
declare function loadDefault<Export>(identity: string, load: () => unknown, owed: {
|
|
136
|
+
guard: (value: unknown) => value is Export;
|
|
137
|
+
noun: string;
|
|
138
|
+
}): Promise<Export>;
|
|
100
139
|
/** One plugin's declared middleware: what wakes it, and the loader that fetches its module. */
|
|
101
140
|
interface BuiltMiddleware {
|
|
102
141
|
activate: 'always' | readonly string[];
|
|
103
142
|
load: () => unknown;
|
|
104
143
|
}
|
|
105
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* One plugin's configuration source: the identity of the binding that marks an option as
|
|
146
|
+
* configuration-bound, and the loader that fetches the resolver's module.
|
|
147
|
+
*/
|
|
148
|
+
interface BuiltSource {
|
|
149
|
+
binding: string;
|
|
150
|
+
load: () => unknown;
|
|
151
|
+
}
|
|
152
|
+
/** One plugin's declarations, read once at its `plugin()` call. */
|
|
106
153
|
interface BuiltPlugin {
|
|
107
154
|
theme: Palette | undefined;
|
|
108
155
|
/** The hook core calls once per Command at graph build, or nothing where none is declared. */
|
|
@@ -113,20 +160,27 @@ interface BuiltPlugin {
|
|
|
113
160
|
inputs: readonly OptionInput[];
|
|
114
161
|
middleware: BuiltMiddleware | undefined;
|
|
115
162
|
signals: readonly ProcessSignal[];
|
|
163
|
+
source: BuiltSource | undefined;
|
|
164
|
+
/** The Commands the plugin attaches to the root, in list order. */
|
|
165
|
+
commands: readonly AttachedChild[];
|
|
166
|
+
/** Every descriptor the plugin defines or its options' values name, by identity. */
|
|
167
|
+
descriptors: ReadonlyMap<string, AnyExtension>;
|
|
168
|
+
/** The extension record each of the plugin's own options carries. */
|
|
169
|
+
records: InputRecords;
|
|
116
170
|
}
|
|
117
|
-
/** The shared registers one build fills while it reads each plugin's contributions. */
|
|
118
|
-
interface PluginBuild {
|
|
119
|
-
descriptors: DescriptorRegistry;
|
|
120
|
-
extensions: ExtensionRecords;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Every installed plugin's declarations, in installation order. A plugin's own extensions register
|
|
124
|
-
* before any declaration carries a value, so a duplicated package copy is reported from the list
|
|
125
|
-
* that installed it.
|
|
126
|
-
*/
|
|
127
|
-
declare function buildPlugins(installed: readonly InstalledPlugin[], build: PluginBuild): readonly BuiltPlugin[];
|
|
128
171
|
/** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
|
|
129
172
|
declare function ownedSignals(plugins: readonly BuiltPlugin[]): readonly ProcessSignal[];
|
|
173
|
+
/** The value shape a plugin option takes, which is what `OptionValue` gives its declaration. */
|
|
174
|
+
type PluginValues = Record<string, string | string[] | boolean | undefined>;
|
|
175
|
+
/**
|
|
176
|
+
* One plugin's own option values for one run: what argv or an input source supplied, or the
|
|
177
|
+
* declared default, filled without validation. A collected value and an array default are copied,
|
|
178
|
+
* so a plugin that writes to what it received changes neither the declaration nor the next run.
|
|
179
|
+
* Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
180
|
+
*/
|
|
181
|
+
declare function pluginValues(inputs: readonly OptionInput[], values: OptionValues): PluginValues;
|
|
182
|
+
/** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
|
|
183
|
+
declare function pluginSpellings(inputs: readonly OptionInput[], values: OptionValues): Readonly<Record<string, string>>;
|
|
130
184
|
type ThemeOf<Contributor> = [Contributor] extends [never] ? {} : Contributor extends Plugin<PluginOptions, infer Theme> ? Theme : {};
|
|
131
|
-
export type { ThemeOf, BuiltPlugin,
|
|
132
|
-
export {
|
|
185
|
+
export type { ThemeOf, BuiltPlugin, BuiltSource, PluginValues, SourceAnswer, SourceContext, SourceResolver, Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionSpellings, PluginOptionValues, };
|
|
186
|
+
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginValues, };
|
package/dist/plugin.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { checkEnvBinding, claimVariables } from './bindings.js';
|
|
2
|
+
import { attach, commandNode } from './command.js';
|
|
3
|
+
import { DeclarationError, InternalError, reasonOf } from './errors.js';
|
|
4
|
+
import { appliesTo, buildExtensions, isDescriptor, registerDescriptor } from './extension.js';
|
|
3
5
|
import { checkDeprecated, checkDescription, checkHidden, isPlainObject } from './facts.js';
|
|
6
|
+
import { boundOptions } from './globals.js';
|
|
7
|
+
import { coreViews } from './lanes.js';
|
|
8
|
+
import { booleanValue, compileOptions } from './options.js';
|
|
4
9
|
import { isProcessSignal } from './signals.js';
|
|
5
10
|
import { buildTheme } from './theme.js';
|
|
6
11
|
import { captureConfig, checkDeclarations } from './validation.js';
|
|
12
|
+
import { buildViews, viewIdentities } from './view.js';
|
|
7
13
|
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
8
14
|
const nodes = new WeakMap();
|
|
9
15
|
/**
|
|
@@ -20,7 +26,8 @@ class PluginDeclaration {
|
|
|
20
26
|
/**
|
|
21
27
|
* One plugin: an identity and the contributions it carries. Creating and installing the value runs
|
|
22
28
|
* none of its code: a hook runs at graph build, and the middleware runs inside an invocation, so an
|
|
23
|
-
* installed plugin an invocation never reaches costs that invocation its hooks alone.
|
|
29
|
+
* installed plugin an invocation never reaches costs that invocation its hooks alone. Every rule
|
|
30
|
+
* that one definition carries on its own throws here, before the value exists.
|
|
24
31
|
*/
|
|
25
32
|
function plugin(identity, definition) {
|
|
26
33
|
const captured = {
|
|
@@ -29,10 +36,7 @@ function plugin(identity, definition) {
|
|
|
29
36
|
? {}
|
|
30
37
|
: { theme: isPlainObject(definition.theme) ? { ...definition.theme } : definition.theme }),
|
|
31
38
|
};
|
|
32
|
-
return new PluginDeclaration(
|
|
33
|
-
definition: isPlainObject(definition) ? captured : definition,
|
|
34
|
-
identity,
|
|
35
|
-
});
|
|
39
|
+
return new PluginDeclaration(readPlugin(identity, isPlainObject(definition) ? captured : definition));
|
|
36
40
|
}
|
|
37
41
|
/** How every plugin diagnostic names one plugin at the start of a sentence. */
|
|
38
42
|
function pluginSentence(identity) {
|
|
@@ -46,46 +50,70 @@ function nodeOf(value) {
|
|
|
46
50
|
}
|
|
47
51
|
return node;
|
|
48
52
|
}
|
|
49
|
-
/** The identity one
|
|
50
|
-
function readIdentity(
|
|
51
|
-
const { identity } = node;
|
|
53
|
+
/** The identity one plugin declares, which is a nonempty string. */
|
|
54
|
+
function readIdentity(identity) {
|
|
52
55
|
if (typeof identity !== 'string') {
|
|
53
56
|
throw new DeclarationError('A plugin declares an identity that is not a string. Supply a nonempty string, such as the package name.');
|
|
54
57
|
}
|
|
55
58
|
if (identity === '') {
|
|
56
59
|
throw new DeclarationError('A plugin declares an empty identity. Supply a nonempty string, such as the package name.');
|
|
57
60
|
}
|
|
58
|
-
if (installed.has(identity)) {
|
|
59
|
-
throw new DeclarationError(`The Application installs plugin "${identity}" twice. Install each plugin once.`);
|
|
60
|
-
}
|
|
61
61
|
return identity;
|
|
62
62
|
}
|
|
63
63
|
/** The declarations one plugin value carries, which a JavaScript author reaches as any value. */
|
|
64
|
-
function definitionOf(identity,
|
|
65
|
-
const { definition } = node;
|
|
64
|
+
function definitionOf(identity, definition) {
|
|
66
65
|
if (!isPlainObject(definition)) {
|
|
67
66
|
throw new DeclarationError(`${pluginSentence(identity)} declares a definition that is not an object. Supply { options, middleware, extensions, views }.`);
|
|
68
67
|
}
|
|
69
68
|
return definition;
|
|
70
69
|
}
|
|
71
70
|
/**
|
|
72
|
-
* The installed list in composition order, with
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* The installed list in composition order, with every rule that reads two plugins together: an
|
|
72
|
+
* identity installed twice, a second claim on the theme slot, the signals slot, or the
|
|
73
|
+
* configuration source, and two distinct descriptors under one identity. The slot is read
|
|
74
|
+
* defensively, because a JavaScript author reaches it with any value. Each plugin's own rules
|
|
75
|
+
* already ran at its `plugin()` call.
|
|
75
76
|
*/
|
|
76
77
|
function installPlugins(plugins) {
|
|
77
78
|
if (!Array.isArray(plugins)) {
|
|
78
79
|
throw new DeclarationError('The Application plugins must be an array. Supply a list of plugin values.');
|
|
79
80
|
}
|
|
80
|
-
const
|
|
81
|
+
const list = plugins;
|
|
82
|
+
const installed = list.map((value) => nodeOf(value));
|
|
81
83
|
const identities = new Set();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const descriptors = new Map();
|
|
85
|
+
// Each slot has one owner, so the first plugin to claim it names the second claimant's diagnostic.
|
|
86
|
+
const owners = {};
|
|
87
|
+
for (const entry of installed) {
|
|
88
|
+
const { identity } = entry;
|
|
89
|
+
if (identities.has(identity)) {
|
|
90
|
+
throw new DeclarationError(`The Application installs plugin "${identity}" twice. Install each plugin once.`);
|
|
91
|
+
}
|
|
85
92
|
identities.add(identity);
|
|
86
|
-
|
|
93
|
+
if (entry.theme !== undefined) {
|
|
94
|
+
if (owners.theme !== undefined) {
|
|
95
|
+
throw new DeclarationError(`${pluginSentence(identity)} claims the theme slot, which plugin "${owners.theme}" already holds. Install one owner.`);
|
|
96
|
+
}
|
|
97
|
+
owners.theme = identity;
|
|
98
|
+
}
|
|
99
|
+
for (const descriptor of entry.descriptors.values()) {
|
|
100
|
+
registerDescriptor(descriptors, descriptor);
|
|
101
|
+
}
|
|
102
|
+
// An empty claim leaves the signals slot free.
|
|
103
|
+
if (entry.signals.length > 0) {
|
|
104
|
+
if (owners.signals !== undefined) {
|
|
105
|
+
throw new DeclarationError(`${pluginSentence(identity)} claims the signals slot, which plugin "${owners.signals}" already holds. Install one owner.`);
|
|
106
|
+
}
|
|
107
|
+
owners.signals = identity;
|
|
108
|
+
}
|
|
109
|
+
if (entry.source) {
|
|
110
|
+
if (owners.source !== undefined) {
|
|
111
|
+
throw new DeclarationError(`${pluginSentence(identity)} declares a configuration source, which plugin "${owners.source}" already declares. Install one source.`);
|
|
112
|
+
}
|
|
113
|
+
owners.source = identity;
|
|
114
|
+
}
|
|
87
115
|
}
|
|
88
|
-
return installed;
|
|
116
|
+
return { descriptors, plugins: installed };
|
|
89
117
|
}
|
|
90
118
|
/** The keys a plugin option may not declare, in the order its diagnostic names them. */
|
|
91
119
|
const forbidden = ['validate', 'validateOmitted', 'required'];
|
|
@@ -96,7 +124,7 @@ function checkPluginOption(sentence, config) {
|
|
|
96
124
|
}
|
|
97
125
|
const rejected = forbidden.find((key) => key in config);
|
|
98
126
|
if (rejected !== undefined) {
|
|
99
|
-
throw new DeclarationError(`${sentence} declares ${rejected}. Remove it; a plugin option carries no
|
|
127
|
+
throw new DeclarationError(`${sentence} declares ${rejected}. Remove it; a plugin option carries no validator or presence rule, and the middleware interprets the value.`);
|
|
100
128
|
}
|
|
101
129
|
checkDescription(sentence, config.description);
|
|
102
130
|
checkHidden(sentence, config.hidden);
|
|
@@ -114,10 +142,11 @@ function readOptions(identity, declared, build) {
|
|
|
114
142
|
for (const [name, config] of Object.entries(declared ?? {})) {
|
|
115
143
|
const sentence = `${pluginSentence(identity)} option "${name}"`;
|
|
116
144
|
checkPluginOption(sentence, config);
|
|
145
|
+
checkEnvBinding(sentence, config);
|
|
117
146
|
const input = { config: captureConfig(config), kind: 'option', name };
|
|
118
147
|
// The shared rules name the plugin and the option, so a fault reads with its contributor.
|
|
119
148
|
checkDeclarations([input], sentence);
|
|
120
|
-
build.
|
|
149
|
+
build.records.set(input, buildExtensions({
|
|
121
150
|
declared: config.extensions,
|
|
122
151
|
descriptors: build.descriptors,
|
|
123
152
|
subject: {
|
|
@@ -128,6 +157,9 @@ function readOptions(identity, declared, build) {
|
|
|
128
157
|
}));
|
|
129
158
|
inputs.push(input);
|
|
130
159
|
}
|
|
160
|
+
// Two options of one plugin meet in the one table the pre-scan reads, so they share its rules.
|
|
161
|
+
compileOptions(inputs, `plugin "${identity}"`);
|
|
162
|
+
claimVariables(boundOptions(inputs, (name) => `plugin "${identity}" option "${name}"`));
|
|
131
163
|
return inputs;
|
|
132
164
|
}
|
|
133
165
|
/** One activation name, which must be one of the plugin's own declared options. */
|
|
@@ -158,6 +190,27 @@ function readActivation(identity, declared, names) {
|
|
|
158
190
|
function isLoader(value) {
|
|
159
191
|
return typeof value === 'function';
|
|
160
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The default export one plugin loader resolves to, checked by the guard its caller supplies. A
|
|
195
|
+
* loader that throws where it is called and one that rejects later are one failure, and a module
|
|
196
|
+
* without the export names the kind of function it owed, such as `middleware` or `source`.
|
|
197
|
+
*/
|
|
198
|
+
async function loadDefault(identity, load, owed) {
|
|
199
|
+
let module = undefined;
|
|
200
|
+
try {
|
|
201
|
+
module = await load();
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
throw new InternalError(`Loading plugin "${identity}" failed: ${reasonOf(error)}`, error);
|
|
205
|
+
}
|
|
206
|
+
const exported = module !== null && typeof module === 'object' && 'default' in module
|
|
207
|
+
? module.default
|
|
208
|
+
: undefined;
|
|
209
|
+
if (!owed.guard(exported)) {
|
|
210
|
+
throw new InternalError(`Loading plugin "${identity}" failed: the module exports no default ${owed.noun} function.`, undefined);
|
|
211
|
+
}
|
|
212
|
+
return exported;
|
|
213
|
+
}
|
|
161
214
|
/** One plugin's middleware, or `undefined` for a plugin that declares none. */
|
|
162
215
|
function readMiddleware(identity, declared, names) {
|
|
163
216
|
if (declared === undefined) {
|
|
@@ -173,6 +226,30 @@ function readMiddleware(identity, declared, names) {
|
|
|
173
226
|
}
|
|
174
227
|
return { activate, load };
|
|
175
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* The Commands one plugin attaches to the root, each checked by the attach the root applies, as a
|
|
231
|
+
* finished Command, against the plugin's own earlier Commands, and against the nesting cap. The
|
|
232
|
+
* Application attaches them again when it is constructed, against every other root child.
|
|
233
|
+
*/
|
|
234
|
+
function readCommands(identity, declared) {
|
|
235
|
+
if (declared === undefined) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
if (!Array.isArray(declared)) {
|
|
239
|
+
throw new DeclarationError(`${pluginSentence(identity)} declares commands that are not an array. Supply a list of Command values.`);
|
|
240
|
+
}
|
|
241
|
+
const list = declared;
|
|
242
|
+
const commands = [];
|
|
243
|
+
// A for...of walk reads a hole as undefined, which the entry rule rejects, where map would skip it.
|
|
244
|
+
for (const value of list) {
|
|
245
|
+
const node = commandNode(value);
|
|
246
|
+
if (!node) {
|
|
247
|
+
throw new DeclarationError(`${pluginSentence(identity)} holds a value that is not a Command. Supply the value returned by new Command(name).`);
|
|
248
|
+
}
|
|
249
|
+
commands.push(attach({ argument: undefined, children: commands, hasAction: false, name: null }, node));
|
|
250
|
+
}
|
|
251
|
+
return commands;
|
|
252
|
+
}
|
|
176
253
|
/**
|
|
177
254
|
* One plugin's claim on the signals slot, drawn from the closed set core installs listeners for.
|
|
178
255
|
* An empty list claims nothing, so it leaves the slot free for another plugin.
|
|
@@ -216,6 +293,38 @@ function readHook(identity, declared) {
|
|
|
216
293
|
}
|
|
217
294
|
return declared;
|
|
218
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* The configuration source one plugin declares, or `undefined` for a plugin that declares none.
|
|
298
|
+
* The binding is one of the plugin's own option-target extensions, so core knows which options to
|
|
299
|
+
* ask about without knowing what the binding means. The plugin's own options resolve before the
|
|
300
|
+
* source loads, so none of them may carry its binding.
|
|
301
|
+
*/
|
|
302
|
+
function readSource(identity, declaration, own) {
|
|
303
|
+
const declared = declaration.source;
|
|
304
|
+
if (declared === undefined) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
const sentence = pluginSentence(identity);
|
|
308
|
+
if (!isPlainObject(declared)) {
|
|
309
|
+
throw new DeclarationError(`${sentence} declares a source that is not an object. Supply { binding, load }.`);
|
|
310
|
+
}
|
|
311
|
+
const { binding, load } = declared;
|
|
312
|
+
const listed = (declaration.extensions ?? []).some((descriptor) => descriptor === binding);
|
|
313
|
+
if (!listed || !isDescriptor(binding)) {
|
|
314
|
+
throw new DeclarationError(`${sentence} declares a source binding that is not one of its extensions. Supply a descriptor the plugin lists under extensions.`);
|
|
315
|
+
}
|
|
316
|
+
if (binding.target !== 'option') {
|
|
317
|
+
throw new DeclarationError(`${sentence} declares source binding "${binding.identity}", which applies to ${appliesTo(binding.target)}. Supply an extension that applies to options.`);
|
|
318
|
+
}
|
|
319
|
+
if (!isLoader(load)) {
|
|
320
|
+
throw new DeclarationError(`${sentence} declares a source with no load function. Supply load: () => import('./source.js').`);
|
|
321
|
+
}
|
|
322
|
+
const carrier = own.inputs.find((input) => Object.hasOwn(own.build.records.get(input) ?? {}, binding.identity));
|
|
323
|
+
if (carrier) {
|
|
324
|
+
throw new DeclarationError(`${sentence} option "${carrier.name}" carries its own source binding. Remove the value; the source's own options resolve before it loads.`);
|
|
325
|
+
}
|
|
326
|
+
return { binding: binding.identity, load };
|
|
327
|
+
}
|
|
219
328
|
/** A plugin's own list names the extensions it defines, before any declaration carries one. */
|
|
220
329
|
function defineExtensions(identity, declaration, build) {
|
|
221
330
|
const { extensions } = declaration;
|
|
@@ -230,49 +339,78 @@ function defineExtensions(identity, declaration, build) {
|
|
|
230
339
|
}
|
|
231
340
|
}
|
|
232
341
|
/**
|
|
233
|
-
* Every
|
|
234
|
-
* before any declaration carries a value, so a duplicated package
|
|
235
|
-
* that
|
|
342
|
+
* Every rule one definition carries on its own, in the order the definition's slots are read. The
|
|
343
|
+
* plugin's own extensions register before any declaration carries a value, so a duplicated package
|
|
344
|
+
* copy is reported from the list that defines it. The views list is read against core's view
|
|
345
|
+
* identities, and the Application reads it again against every other contributor's.
|
|
236
346
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
identity,
|
|
265
|
-
inputs,
|
|
266
|
-
middleware: readMiddleware(identity, declaration.middleware, names),
|
|
267
|
-
onCommandAttach: readHook(identity, declaration.onCommandAttach),
|
|
268
|
-
signals,
|
|
269
|
-
theme,
|
|
270
|
-
views: declaration.views,
|
|
271
|
-
};
|
|
272
|
-
});
|
|
347
|
+
function readPlugin(identity, definition) {
|
|
348
|
+
const named = readIdentity(identity);
|
|
349
|
+
const declaration = definitionOf(named, definition);
|
|
350
|
+
const theme = declaration.theme === undefined ? undefined : buildTheme(declaration.theme, named);
|
|
351
|
+
const build = { descriptors: new Map(), records: new Map() };
|
|
352
|
+
defineExtensions(named, declaration, build);
|
|
353
|
+
const inputs = readOptions(named, declaration.options, build);
|
|
354
|
+
const names = new Set(inputs.map((input) => input.name));
|
|
355
|
+
const signals = readSignals(named, declaration.signals);
|
|
356
|
+
const source = readSource(named, declaration, { build, inputs });
|
|
357
|
+
const commands = readCommands(named, declaration.commands);
|
|
358
|
+
const middleware = readMiddleware(named, declaration.middleware, names);
|
|
359
|
+
const onCommandAttach = readHook(named, declaration.onCommandAttach);
|
|
360
|
+
buildViews({ declares: true, sentence: pluginSentence(named) }, declaration.views, viewIdentities(coreViews));
|
|
361
|
+
return {
|
|
362
|
+
commands,
|
|
363
|
+
descriptors: build.descriptors,
|
|
364
|
+
identity: named,
|
|
365
|
+
inputs,
|
|
366
|
+
middleware,
|
|
367
|
+
onCommandAttach,
|
|
368
|
+
records: build.records,
|
|
369
|
+
signals,
|
|
370
|
+
source,
|
|
371
|
+
theme,
|
|
372
|
+
views: declaration.views,
|
|
373
|
+
};
|
|
273
374
|
}
|
|
274
375
|
/** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
|
|
275
376
|
function ownedSignals(plugins) {
|
|
276
377
|
return plugins.find((entry) => entry.signals.length > 0)?.signals ?? [];
|
|
277
378
|
}
|
|
278
|
-
|
|
379
|
+
/** A collected value, or the declared array default, as this run's own copy. */
|
|
380
|
+
function collectedValue(collected, declared) {
|
|
381
|
+
if (collected) {
|
|
382
|
+
return [...collected];
|
|
383
|
+
}
|
|
384
|
+
return Array.isArray(declared) ? [...declared] : [];
|
|
385
|
+
}
|
|
386
|
+
/** One plugin option's value for one run: what a tier supplied, or the declared default. */
|
|
387
|
+
function pluginValue({ config, name }, values) {
|
|
388
|
+
const declared = config.default;
|
|
389
|
+
if (config.type === 'boolean') {
|
|
390
|
+
return booleanValue(values, name, config);
|
|
391
|
+
}
|
|
392
|
+
if (config.multiple === true) {
|
|
393
|
+
return collectedValue(values.lists.get(name), declared);
|
|
394
|
+
}
|
|
395
|
+
// Build already proved that a string option without a validator declares a string default.
|
|
396
|
+
return values.strings.get(name) ?? (typeof declared === 'string' ? declared : undefined);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* One plugin's own option values for one run: what argv or an input source supplied, or the
|
|
400
|
+
* declared default, filled without validation. A collected value and an array default are copied,
|
|
401
|
+
* so a plugin that writes to what it received changes neither the declaration nor the next run.
|
|
402
|
+
* Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
403
|
+
*/
|
|
404
|
+
function pluginValues(inputs, values) {
|
|
405
|
+
return Object.fromEntries(inputs.map((input) => [input.name, pluginValue(input, values)]));
|
|
406
|
+
}
|
|
407
|
+
/** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
|
|
408
|
+
function pluginSpellings(inputs, values) {
|
|
409
|
+
// Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
410
|
+
const spelled = inputs.flatMap(({ name }) => {
|
|
411
|
+
const spelling = values.spellings.get(name);
|
|
412
|
+
return spelling === undefined ? [] : [[name, spelling]];
|
|
413
|
+
});
|
|
414
|
+
return Object.freeze(Object.fromEntries(spelled));
|
|
415
|
+
}
|
|
416
|
+
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginValues, };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { InputError, InternalError } from './errors.js';
|
|
2
|
+
import type { ExtensionRecords } from './extension.js';
|
|
3
|
+
import type { CommandGraph, OptionNode } from './inspect.js';
|
|
4
|
+
import type { OptionValues } from './options.js';
|
|
5
|
+
import type { BuiltPlugin } from './plugin.js';
|
|
6
|
+
import type { ContextualStyle } from './style.js';
|
|
7
|
+
import type { Host, Out } from './types.js';
|
|
8
|
+
import type { OptionInput } from './validation.js';
|
|
9
|
+
/**
|
|
10
|
+
* One scope the stage fills: its options in declaration order, and the values argv supplied them.
|
|
11
|
+
* The stage writes each fill into `values`, so the caller hands it the run's own copy.
|
|
12
|
+
*/
|
|
13
|
+
interface StageScope {
|
|
14
|
+
global: boolean;
|
|
15
|
+
inputs: readonly OptionInput[];
|
|
16
|
+
values: OptionValues;
|
|
17
|
+
}
|
|
18
|
+
/** Everything one input-source stage reads. */
|
|
19
|
+
interface SourceStage {
|
|
20
|
+
extensions: ExtensionRecords;
|
|
21
|
+
/** The globals table: the application's global options, then each plugin's in install order. */
|
|
22
|
+
globals: StageScope;
|
|
23
|
+
host: Host;
|
|
24
|
+
/** The graph `inspect()` would return for the run, built on its first read. */
|
|
25
|
+
inspected: () => CommandGraph;
|
|
26
|
+
/** The routed Command's own options, or `undefined` while local parsing holds a fault. */
|
|
27
|
+
locals: StageScope | undefined;
|
|
28
|
+
/** The channel a source writes through, whose results call names the source. */
|
|
29
|
+
out: Out;
|
|
30
|
+
plugins: readonly BuiltPlugin[];
|
|
31
|
+
/** The `OptionNode` of one option, read from the graph `inspect()` would return for the run. */
|
|
32
|
+
request: (input: OptionInput, global: boolean) => OptionNode;
|
|
33
|
+
signal: AbortSignal;
|
|
34
|
+
/** The contextual style an action receives, so a source escapes raw data before it warns. */
|
|
35
|
+
style: ContextualStyle;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What the stage found beside the values it filled. `labels` names where each filled option's value
|
|
39
|
+
* came from, by option name, and `rejected` names the variable of each Boolean option whose value
|
|
40
|
+
* is outside the grammar. Both are internal to core's failure messages. `fault` is a configuration
|
|
41
|
+
* source's own fault, or the `InputError` its resolver threw, which stops the stage and takes the
|
|
42
|
+
* place of every validation problem.
|
|
43
|
+
*/
|
|
44
|
+
interface SourceOutcome {
|
|
45
|
+
fault: InternalError | InputError | undefined;
|
|
46
|
+
labels: ReadonlyMap<string, string>;
|
|
47
|
+
rejected: ReadonlyMap<string, string>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The input-source stage: the environment, then the configuration source, for every option in
|
|
51
|
+
* scope that argv left unfilled. When no option is left to ask, the source never loads. A source
|
|
52
|
+
* fault stops the stage and fills nothing more.
|
|
53
|
+
*/
|
|
54
|
+
declare function fillInputs(stage: SourceStage): Promise<SourceOutcome>;
|
|
55
|
+
export type { SourceOutcome, SourceStage, StageScope };
|
|
56
|
+
export { fillInputs };
|