@loomcli/core 0.5.0 → 0.6.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/dist/application.d.ts +18 -2
- package/dist/application.js +266 -71
- package/dist/bindings.d.ts +15 -10
- package/dist/bindings.js +34 -15
- package/dist/chain.d.ts +15 -6
- package/dist/chain.js +40 -20
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +65 -23
- package/dist/command.js +695 -226
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +108 -24
- package/dist/errors.js +324 -53
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +41 -8
- package/dist/extension.js +142 -57
- package/dist/facts.d.ts +66 -11
- package/dist/facts.js +98 -22
- package/dist/globals.d.ts +29 -21
- package/dist/globals.js +115 -46
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +12 -4
- package/dist/inspect.js +88 -28
- package/dist/lanes.js +1 -1
- package/dist/locate.js +4 -4
- package/dist/options.d.ts +23 -2
- package/dist/options.js +135 -44
- package/dist/output.d.ts +9 -2
- package/dist/output.js +18 -2
- package/dist/plain.d.ts +6 -0
- package/dist/plain.js +12 -0
- package/dist/plugin-rules.d.ts +62 -0
- package/dist/plugin-rules.js +155 -0
- package/dist/plugin.d.ts +22 -10
- package/dist/plugin.js +348 -116
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +6 -4
- package/dist/sources.js +22 -13
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +11 -1
- package/dist/validation.d.ts +44 -3
- package/dist/validation.js +156 -57
- package/dist/view.d.ts +48 -14
- package/dist/view.js +155 -77
- package/package.json +1 -1
package/dist/facts.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { misplacedListingFact, notOneLine } from './command-rules.js';
|
|
1
2
|
import { DeclarationError } from './errors.js';
|
|
3
|
+
import { flagNotBoolean } from './input-rules.js';
|
|
2
4
|
/**
|
|
3
5
|
* One character outside Unicode `White_Space`, so a fact holds prose and not only spacing.
|
|
4
6
|
* The class covers the tab, the space, the line terminators, the no-break space, and every other
|
|
@@ -10,6 +12,76 @@ const prose = /\P{White_Space}/u;
|
|
|
10
12
|
* The seven are LF, VT, FF, CR, NEL, LS, and PS, each of them `White_Space` too.
|
|
11
13
|
*/
|
|
12
14
|
const lineTerminator = /[\n\v\f\r\u0085\u2028\u2029]/u;
|
|
15
|
+
/** The finding for the call one site holds, marking one part of it, with a note when given. */
|
|
16
|
+
export function siteFinding(site, mark, note) {
|
|
17
|
+
const finding = { ...site.declaration, mark };
|
|
18
|
+
return note === undefined ? finding : { ...finding, note };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Where one key of a declaration's options object sits, rebuilt with that key alone, as
|
|
22
|
+
* `plugin(identity, { middleware })` or `new Application(name, { plugins })`, at `1.<key>`. A fault
|
|
23
|
+
* about one slot shows the slot, not every other one the author declared beside it.
|
|
24
|
+
*/
|
|
25
|
+
export function slotSite(declaration, key, value) {
|
|
26
|
+
const { call, named, subject } = declaration;
|
|
27
|
+
// `Object.fromEntries` defines the key as an own property whatever its name.
|
|
28
|
+
return {
|
|
29
|
+
at: `1.${key}`,
|
|
30
|
+
declaration: { arguments: [named, Object.fromEntries([[key, value]])], call },
|
|
31
|
+
subject,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The dotted path to one part inside the value a site holds, such as `1.middleware.activate` for
|
|
36
|
+
* `activate` under a site at `1.middleware`. A site at the call's own arguments has an empty path.
|
|
37
|
+
*/
|
|
38
|
+
export function partOf(site, ...keys) {
|
|
39
|
+
return [site.at, ...keys.map(String)].filter((key) => key !== '').join('.');
|
|
40
|
+
}
|
|
41
|
+
/** The finding that marks one part inside the value a site holds, with a note when given. */
|
|
42
|
+
export function partFinding(site, keys, note) {
|
|
43
|
+
return siteFinding(site, partOf(site, ...keys), note);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One fact's fault, which marks the fact inside the call that declared it. Every rule about one key
|
|
47
|
+
* of a declaration's config object reports through it, so each marks the key the same way.
|
|
48
|
+
*/
|
|
49
|
+
export function factFault(rule, site, parts) {
|
|
50
|
+
const { correction, fact, sentence } = parts;
|
|
51
|
+
return new DeclarationError(rule, {
|
|
52
|
+
correction,
|
|
53
|
+
findings: [siteFinding(site, `${site.at}.${fact}`)],
|
|
54
|
+
sentence,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One yes-or-no declaration key that holds a value other than a Boolean, such as `required` or
|
|
59
|
+
* `hidden`. Every such key reports under one rule, in one sentence and with one correction.
|
|
60
|
+
*/
|
|
61
|
+
export function flagFault(site, flag) {
|
|
62
|
+
return factFault(flagNotBoolean, site, {
|
|
63
|
+
correction: 'Use true or false.',
|
|
64
|
+
fact: flag,
|
|
65
|
+
sentence: `${site.subject} declares ${flag} that is not a Boolean.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** The site of an input a call declared as `call(name, config)`, such as `option()`. */
|
|
69
|
+
export function callSite(subject, declaration) {
|
|
70
|
+
return { at: '1', declaration, named: '0', subject };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The site of one option a plugin declared, rebuilt as `plugin(identity, { options })` from the
|
|
74
|
+
* options the plugin holds. The option's entry in the record stands for its name.
|
|
75
|
+
*/
|
|
76
|
+
export function pluginOptionSite(plugin, name, subject) {
|
|
77
|
+
const at = `1.options.${name}`;
|
|
78
|
+
return {
|
|
79
|
+
at,
|
|
80
|
+
declaration: { arguments: [plugin.identity, { options: plugin.options }], call: 'plugin' },
|
|
81
|
+
named: at,
|
|
82
|
+
subject,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
13
85
|
/**
|
|
14
86
|
* One line of prose: a string that holds a character other than whitespace and no line terminator.
|
|
15
87
|
* Every one-line fact reads this rule, and so does the label a configuration source answers with.
|
|
@@ -22,12 +94,16 @@ export function isProseLine(value) {
|
|
|
22
94
|
* string fails the same way a blank one does, because the author reads one rule for one fact.
|
|
23
95
|
* An omitted description is absent, not a fault, so it passes through as `undefined`.
|
|
24
96
|
*/
|
|
25
|
-
export function checkDescription(
|
|
97
|
+
export function checkDescription(site, value) {
|
|
26
98
|
if (value === undefined) {
|
|
27
99
|
return undefined;
|
|
28
100
|
}
|
|
29
101
|
if (!isProseLine(value)) {
|
|
30
|
-
throw
|
|
102
|
+
throw factFault(notOneLine, site, {
|
|
103
|
+
correction: 'Supply a one-line summary.',
|
|
104
|
+
fact: 'description',
|
|
105
|
+
sentence: `${site.subject} description must hold a character other than whitespace and no line terminator.`,
|
|
106
|
+
});
|
|
31
107
|
}
|
|
32
108
|
return value;
|
|
33
109
|
}
|
|
@@ -36,12 +112,12 @@ export function checkDescription(subject, value) {
|
|
|
36
112
|
* asks one question of it, and an omitted declaration reads `false`. Routing selects and parsing
|
|
37
113
|
* binds without reading it, so a hidden member behaves as any other.
|
|
38
114
|
*/
|
|
39
|
-
export function checkHidden(
|
|
115
|
+
export function checkHidden(site, value) {
|
|
40
116
|
if (value === undefined) {
|
|
41
117
|
return false;
|
|
42
118
|
}
|
|
43
119
|
if (typeof value !== 'boolean') {
|
|
44
|
-
throw
|
|
120
|
+
throw flagFault(site, 'hidden');
|
|
45
121
|
}
|
|
46
122
|
return value;
|
|
47
123
|
}
|
|
@@ -51,12 +127,16 @@ export function checkHidden(subject, value) {
|
|
|
51
127
|
* prints. A bare `true` is rejected with every other value that is not prose: a deprecation with
|
|
52
128
|
* no migration path leaves an operator or an agent with nothing to do.
|
|
53
129
|
*/
|
|
54
|
-
export function checkDeprecated(
|
|
130
|
+
export function checkDeprecated(site, value) {
|
|
55
131
|
if (value === undefined) {
|
|
56
132
|
return undefined;
|
|
57
133
|
}
|
|
58
134
|
if (!isProseLine(value)) {
|
|
59
|
-
throw
|
|
135
|
+
throw factFault(notOneLine, site, {
|
|
136
|
+
correction: 'Supply a one-line migration path, such as "Use get instead.".',
|
|
137
|
+
fact: 'deprecated',
|
|
138
|
+
sentence: `${site.subject} deprecated message must hold a character other than whitespace and no line terminator.`,
|
|
139
|
+
});
|
|
60
140
|
}
|
|
61
141
|
return value;
|
|
62
142
|
}
|
|
@@ -66,10 +146,14 @@ export function checkDeprecated(subject, value) {
|
|
|
66
146
|
* a JavaScript author, or a TypeScript author whose argument config is inferred from a value,
|
|
67
147
|
* reaches this rule instead.
|
|
68
148
|
*/
|
|
69
|
-
export function checkNoListingFacts(
|
|
149
|
+
export function checkNoListingFacts(site, declared) {
|
|
70
150
|
for (const fact of ['hidden', 'deprecated']) {
|
|
71
151
|
if (fact in declared) {
|
|
72
|
-
throw
|
|
152
|
+
throw factFault(misplacedListingFact, site, {
|
|
153
|
+
correction: 'Remove it.',
|
|
154
|
+
fact,
|
|
155
|
+
sentence: `${site.subject} declares ${fact}, which applies to named Commands and options alone.`,
|
|
156
|
+
});
|
|
73
157
|
}
|
|
74
158
|
}
|
|
75
159
|
}
|
|
@@ -79,24 +163,16 @@ export function checkNoListingFacts(subject, declared) {
|
|
|
79
163
|
* omitted version is `0.0.0`, which means unversioned, and core keeps no record of which one the
|
|
80
164
|
* author wrote.
|
|
81
165
|
*/
|
|
82
|
-
export function checkVersion(value) {
|
|
166
|
+
export function checkVersion(site, value) {
|
|
83
167
|
if (value === undefined) {
|
|
84
168
|
return '0.0.0';
|
|
85
169
|
}
|
|
86
170
|
if (!isProseLine(value)) {
|
|
87
|
-
throw
|
|
171
|
+
throw factFault(notOneLine, site, {
|
|
172
|
+
correction: 'Supply a string such as "1.2.0".',
|
|
173
|
+
fact: 'version',
|
|
174
|
+
sentence: `${site.subject} version must be a string that holds a character other than whitespace and no line terminator.`,
|
|
175
|
+
});
|
|
88
176
|
}
|
|
89
177
|
return value;
|
|
90
178
|
}
|
|
91
|
-
/**
|
|
92
|
-
* A structural value core reads as plain data: an object literal, and never a declaration that
|
|
93
|
-
* carries state of its own. The options slots read it to reject a value that is not an options
|
|
94
|
-
* object, and inspection reads it to copy a declared value faithfully.
|
|
95
|
-
*/
|
|
96
|
-
export function isPlainObject(value) {
|
|
97
|
-
if (value === null || typeof value !== 'object') {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const prototype = Object.getPrototypeOf(value);
|
|
101
|
-
return prototype === Object.prototype || prototype === null;
|
|
102
|
-
}
|
package/dist/globals.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { BoundOption } from './bindings.js';
|
|
2
|
-
import { DeclarationError } from './errors.js';
|
|
3
2
|
import type { DescriptorRegistry } from './extension.js';
|
|
3
|
+
import type { InputSite } from './facts.js';
|
|
4
4
|
import { compileOptions } from './options.js';
|
|
5
|
+
import type { CompileScope } from './options.js';
|
|
5
6
|
import type { BuiltPlugin } from './plugin.js';
|
|
6
7
|
import type { OptionConfig, OptionValue } from './types.js';
|
|
7
8
|
import type { InputDeclaration, OptionInput, ValidatedInputs } from './validation.js';
|
|
@@ -20,26 +21,22 @@ type OptionOwner = {
|
|
|
20
21
|
identity: string;
|
|
21
22
|
order: number;
|
|
22
23
|
};
|
|
23
|
-
/** One
|
|
24
|
-
interface
|
|
25
|
-
name: string;
|
|
24
|
+
/** One option the globals table holds: the scope that declared it, and where it was declared. */
|
|
25
|
+
interface TableEntry {
|
|
26
26
|
owner: OptionOwner;
|
|
27
|
+
site: InputSite;
|
|
27
28
|
}
|
|
28
|
-
/** One key claimed twice, whichever two scopes claimed it. */
|
|
29
|
-
declare function keyCollision(name: string, first: OptionOwner, second: OptionOwner): DeclarationError;
|
|
30
|
-
/** One spelling claimed twice, whichever two scopes claimed it. */
|
|
31
|
-
declare function spellingCollision(spelling: string, first: OptionSite, second: OptionSite): DeclarationError;
|
|
32
29
|
/**
|
|
33
30
|
* The globals table the pre-scan reads, with every rule that pairs two of its options settled: one
|
|
34
|
-
* spelling map, the scope that owns each key
|
|
35
|
-
* application's global options and every installed plugin's options,
|
|
36
|
-
* table.
|
|
31
|
+
* spelling map, the scope that owns each key and where it was declared, and the variable each
|
|
32
|
+
* option binds. It holds the application's global options and every installed plugin's options,
|
|
33
|
+
* because the pre-scan reads one table.
|
|
37
34
|
*/
|
|
38
35
|
interface GlobalTable {
|
|
39
|
-
names: ReadonlyMap<string,
|
|
36
|
+
names: ReadonlyMap<string, TableEntry>;
|
|
40
37
|
options: ReturnType<typeof compileOptions>;
|
|
41
|
-
/** The variable each option in the table binds,
|
|
42
|
-
variables: ReadonlyMap<string,
|
|
38
|
+
/** The variable each option in the table binds, with the option that binds it. */
|
|
39
|
+
variables: ReadonlyMap<string, BoundOption>;
|
|
43
40
|
}
|
|
44
41
|
/**
|
|
45
42
|
* The compiled table one graph build shares. `inputs` holds the application's declarations alone,
|
|
@@ -62,12 +59,22 @@ interface GlobalsState<Globals = unknown> {
|
|
|
62
59
|
records: InputRecords;
|
|
63
60
|
}
|
|
64
61
|
declare function emptyGlobals(): GlobalsState<{}>;
|
|
62
|
+
/** Where one global option was declared: its `globalOption()` call on the Application. */
|
|
63
|
+
declare function globalSite(input: OptionInput): InputSite;
|
|
64
|
+
/**
|
|
65
|
+
* Where each of one plugin's options was declared: its entry in the `options` record of the
|
|
66
|
+
* plugin's `plugin()` call, which is rebuilt from every option the plugin holds.
|
|
67
|
+
*/
|
|
68
|
+
declare function pluginSites(identity: string, inputs: readonly OptionInput[]): (input: OptionInput) => InputSite;
|
|
65
69
|
/**
|
|
66
70
|
* One global option's own facts, binding, and extension values, checked at its `globalOption()`
|
|
67
71
|
* call against the Application's descriptors. The rules that pair it with another option belong to
|
|
68
72
|
* the table, which the caller rebuilds with it.
|
|
69
73
|
*/
|
|
70
|
-
declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>,
|
|
74
|
+
declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, declared: OptionInput<Name, Config>, descriptors: DescriptorRegistry): {
|
|
75
|
+
readonly input: OptionInput<Name, Config>;
|
|
76
|
+
readonly state: GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
|
|
77
|
+
};
|
|
71
78
|
/**
|
|
72
79
|
* The one table the pre-scan reads: the application's global options in authoring order, then each
|
|
73
80
|
* installed plugin's options in installation order. Every collision between the two scopes, by key
|
|
@@ -81,10 +88,11 @@ declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[
|
|
|
81
88
|
* application's globals and every plugin option, so a local collision reads the same sentence
|
|
82
89
|
* whichever scope on the other side claimed the name, the spelling, or the variable. One
|
|
83
90
|
* invocation's scope is this Command's own options and the table, so a variable binds one option
|
|
84
|
-
* there, while a sibling Command may bind it again.
|
|
91
|
+
* there, while a sibling Command may bind it again. `scope` names the Command and places each of
|
|
92
|
+
* its options.
|
|
85
93
|
*/
|
|
86
|
-
declare function checkLocalOptions(declarations: readonly OptionInput[], table: GlobalTable,
|
|
87
|
-
/** The options in one list that bind a variable, each named
|
|
88
|
-
declare function boundOptions(inputs: readonly OptionInput[],
|
|
89
|
-
export type { BuiltGlobals, GlobalsState, GlobalTable, InputRecords, OptionOwner,
|
|
90
|
-
export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals,
|
|
94
|
+
declare function checkLocalOptions(declarations: readonly OptionInput[], table: GlobalTable, scope: CompileScope<OptionInput>): ReturnType<typeof compileOptions>;
|
|
95
|
+
/** The options in one list that bind a variable, each named and placed by its scope. */
|
|
96
|
+
declare function boundOptions(inputs: readonly OptionInput[], describe: (input: OptionInput) => Omit<BoundOption, 'variable'>): BoundOption[];
|
|
97
|
+
export type { BuiltGlobals, GlobalsState, GlobalTable, InputRecords, OptionOwner, TableEntry };
|
|
98
|
+
export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalSite, globalTable, pluginSites, };
|
package/dist/globals.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { checkEnvBinding, claimVariables } from './bindings.js';
|
|
2
|
-
import { DeclarationError } from './errors.js';
|
|
2
|
+
import { DeclarationError, quoted } from './errors.js';
|
|
3
3
|
import { buildExtensions } from './extension.js';
|
|
4
|
-
import { checkDeprecated, checkDescription, checkHidden } from './facts.js';
|
|
5
|
-
import {
|
|
4
|
+
import { callSite, checkDeprecated, checkDescription, checkHidden, factFault, pluginOptionSite, siteFinding, } from './facts.js';
|
|
5
|
+
import { globalPresenceRule, optionDeclaredTwice, spellingTaken } from './input-rules.js';
|
|
6
|
+
import { checkOptionName, compileOptions, spellingMark } from './options.js';
|
|
7
|
+
import { captureConfig, checkInputConfig } from './validation.js';
|
|
6
8
|
const globalSubject = 'the global options';
|
|
7
9
|
/**
|
|
8
10
|
* The presence keys a global option may not declare, in the order its diagnostic names them. A
|
|
@@ -29,7 +31,7 @@ function ordered(first, second) {
|
|
|
29
31
|
/** How one owner reads in a key collision. A repeated preposition is dropped after the first. */
|
|
30
32
|
function declaredBy(owner, leading) {
|
|
31
33
|
if (owner.kind === 'plugin') {
|
|
32
|
-
return `${leading ? 'by ' : ''}plugin
|
|
34
|
+
return `${leading ? 'by ' : ''}plugin ${quoted(owner.identity)}`;
|
|
33
35
|
}
|
|
34
36
|
return owner.kind === 'application'
|
|
35
37
|
? 'as a global option'
|
|
@@ -38,11 +40,11 @@ function declaredBy(owner, leading) {
|
|
|
38
40
|
/** How one owner reads in a spelling collision, where each side names its own option. */
|
|
39
41
|
function usedBy({ name, owner }) {
|
|
40
42
|
if (owner.kind === 'plugin') {
|
|
41
|
-
return `plugin
|
|
43
|
+
return `plugin ${quoted(owner.identity)} option ${quoted(name)}`;
|
|
42
44
|
}
|
|
43
45
|
return owner.kind === 'application'
|
|
44
|
-
? `the global option
|
|
45
|
-
: `the local option
|
|
46
|
+
? `the global option ${quoted(name)}`
|
|
47
|
+
: `the local option ${quoted(name)} on ${owner.subject}`;
|
|
46
48
|
}
|
|
47
49
|
/** The correction each pair earns: a local is renamed, and two plugins are chosen between. */
|
|
48
50
|
function correction(first, second) {
|
|
@@ -53,45 +55,91 @@ function correction(first, second) {
|
|
|
53
55
|
? 'Install one of them or rename the option.'
|
|
54
56
|
: 'Rename one declaration.';
|
|
55
57
|
}
|
|
58
|
+
/** The note beside one side's finding, which says which scope declared it. */
|
|
59
|
+
const sideNotes = {
|
|
60
|
+
application: 'the global option',
|
|
61
|
+
local: 'the local option',
|
|
62
|
+
plugin: 'the plugin option',
|
|
63
|
+
};
|
|
56
64
|
/** One key claimed twice, whichever two scopes claimed it. */
|
|
57
|
-
function keyCollision(
|
|
58
|
-
const
|
|
59
|
-
|
|
65
|
+
function keyCollision(first, second) {
|
|
66
|
+
const pair = ordered(first, second);
|
|
67
|
+
const [leading, trailing] = pair;
|
|
68
|
+
return new DeclarationError(optionDeclaredTwice, {
|
|
69
|
+
correction: correction(leading.owner, trailing.owner),
|
|
70
|
+
findings: pair.map(({ owner, site }) => siteFinding(site, site.named, sideNotes[owner.kind])),
|
|
71
|
+
sentence: `Option ${quoted(leading.name)} is declared ${declaredBy(leading.owner, true)} and ${declaredBy(trailing.owner, false)}.`,
|
|
72
|
+
});
|
|
60
73
|
}
|
|
61
74
|
/** One spelling claimed twice, whichever two scopes claimed it. */
|
|
62
75
|
function spellingCollision(spelling, first, second) {
|
|
63
|
-
const
|
|
64
|
-
|
|
76
|
+
const pair = ordered(first, second);
|
|
77
|
+
const [leading, trailing] = pair;
|
|
78
|
+
return new DeclarationError(spellingTaken, {
|
|
79
|
+
correction: 'Change one declaration.',
|
|
80
|
+
findings: pair.map(({ owner, role, site }) => siteFinding(site, spellingMark(site, role), sideNotes[owner.kind])),
|
|
81
|
+
sentence: `Option spelling ${quoted(spelling)} is used by ${usedBy(leading)} and ${usedBy(trailing)}.`,
|
|
82
|
+
});
|
|
65
83
|
}
|
|
66
84
|
function emptyGlobals() {
|
|
67
85
|
return { bind: () => ({}), inputs: [], records: new Map() };
|
|
68
86
|
}
|
|
87
|
+
/** Where one global option was declared: its `globalOption()` call on the Application. */
|
|
88
|
+
function globalSite(input) {
|
|
89
|
+
// A global option belongs to the application, not to one Command, so its facts read that way.
|
|
90
|
+
return callSite(`Global option ${quoted(input.name)}`, {
|
|
91
|
+
arguments: [input.name, input.config],
|
|
92
|
+
call: 'globalOption',
|
|
93
|
+
path: [],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Where each of one plugin's options was declared: its entry in the `options` record of the
|
|
98
|
+
* plugin's `plugin()` call, which is rebuilt from every option the plugin holds.
|
|
99
|
+
*/
|
|
100
|
+
function pluginSites(identity, inputs) {
|
|
101
|
+
const options = Object.fromEntries(inputs.map(({ config, name }) => [name, config]));
|
|
102
|
+
return (input) => pluginOptionSite({ identity, options }, input.name, `Plugin ${quoted(identity)} option "${input.name}"`);
|
|
103
|
+
}
|
|
69
104
|
/**
|
|
70
105
|
* One global option's own facts, binding, and extension values, checked at its `globalOption()`
|
|
71
106
|
* call against the Application's descriptors. The rules that pair it with another option belong to
|
|
72
107
|
* the table, which the caller rebuilds with it.
|
|
73
108
|
*/
|
|
74
|
-
function declareGlobalOption(state,
|
|
75
|
-
//
|
|
76
|
-
|
|
109
|
+
function declareGlobalOption(state, declared, descriptors) {
|
|
110
|
+
// The name is judged before the config, as every other declaration judges its own name first.
|
|
111
|
+
checkOptionName(declared.name, globalSite(declared));
|
|
112
|
+
checkInputConfig(declared, { call: 'globalOption', path: [] });
|
|
113
|
+
const input = { ...declared, config: captureConfig(declared.config) };
|
|
114
|
+
const site = globalSite(input);
|
|
115
|
+
const sentence = site.subject;
|
|
77
116
|
const rejected = omissionRules.find((key) => key in input.config);
|
|
78
117
|
if (rejected !== undefined) {
|
|
79
|
-
throw
|
|
118
|
+
throw factFault(globalPresenceRule, site, {
|
|
119
|
+
correction: `Remove ${rejected}, and check for the value in each Command that needs it.`,
|
|
120
|
+
fact: rejected,
|
|
121
|
+
sentence: `${sentence} declares ${rejected}.`,
|
|
122
|
+
});
|
|
80
123
|
}
|
|
81
|
-
checkDescription(
|
|
82
|
-
checkHidden(
|
|
83
|
-
checkDeprecated(
|
|
84
|
-
checkEnvBinding(
|
|
124
|
+
checkDescription(site, input.config.description);
|
|
125
|
+
checkHidden(site, input.config.hidden);
|
|
126
|
+
checkDeprecated(site, input.config.deprecated);
|
|
127
|
+
checkEnvBinding(site, input.config);
|
|
85
128
|
const record = buildExtensions({
|
|
86
129
|
declared: input.config.extensions,
|
|
87
130
|
descriptors,
|
|
88
|
-
|
|
131
|
+
site: { ...site, at: '1.extensions' },
|
|
132
|
+
subject: { phrase: `on the global option ${quoted(input.name)}`, sentence },
|
|
89
133
|
target: 'option',
|
|
90
134
|
});
|
|
135
|
+
// The caller validates the captured input this state stores, so both read one capture.
|
|
91
136
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
137
|
+
input,
|
|
138
|
+
state: {
|
|
139
|
+
bind: (values) => ({ ...state.bind(values), ...values.option(input) }),
|
|
140
|
+
inputs: [...state.inputs, input],
|
|
141
|
+
records: new Map([...state.records, [input, record]]),
|
|
142
|
+
},
|
|
95
143
|
};
|
|
96
144
|
}
|
|
97
145
|
/**
|
|
@@ -103,9 +151,9 @@ function globalTable(inputs, plugins) {
|
|
|
103
151
|
const names = new Map();
|
|
104
152
|
const application = { kind: 'application' };
|
|
105
153
|
for (const input of inputs) {
|
|
106
|
-
names.set(input.name, application);
|
|
154
|
+
names.set(input.name, { owner: application, site: globalSite(input) });
|
|
107
155
|
}
|
|
108
|
-
const options = compileOptions(inputs, globalSubject);
|
|
156
|
+
const options = compileOptions(inputs, { siteOf: globalSite, subject: globalSubject });
|
|
109
157
|
plugins.forEach((installed, order) => {
|
|
110
158
|
join({ identity: installed.identity, kind: 'plugin', order }, installed.inputs, {
|
|
111
159
|
names,
|
|
@@ -113,8 +161,17 @@ function globalTable(inputs, plugins) {
|
|
|
113
161
|
});
|
|
114
162
|
});
|
|
115
163
|
const variables = claimVariables([
|
|
116
|
-
...boundOptions(inputs, (
|
|
117
|
-
|
|
164
|
+
...boundOptions(inputs, (input) => ({
|
|
165
|
+
phrase: `global option ${quoted(input.name)}`,
|
|
166
|
+
site: globalSite(input),
|
|
167
|
+
})),
|
|
168
|
+
...plugins.flatMap((installed) => {
|
|
169
|
+
const siteOf = pluginSites(installed.identity, installed.inputs);
|
|
170
|
+
return boundOptions(installed.inputs, (input) => ({
|
|
171
|
+
phrase: `plugin ${quoted(installed.identity)} option "${input.name}"`,
|
|
172
|
+
site: siteOf(input),
|
|
173
|
+
}));
|
|
174
|
+
}),
|
|
118
175
|
]);
|
|
119
176
|
return { names, options, variables };
|
|
120
177
|
}
|
|
@@ -127,47 +184,59 @@ function buildGlobals(node, plugins) {
|
|
|
127
184
|
* application's globals and every plugin option, so a local collision reads the same sentence
|
|
128
185
|
* whichever scope on the other side claimed the name, the spelling, or the variable. One
|
|
129
186
|
* invocation's scope is this Command's own options and the table, so a variable binds one option
|
|
130
|
-
* there, while a sibling Command may bind it again.
|
|
187
|
+
* there, while a sibling Command may bind it again. `scope` names the Command and places each of
|
|
188
|
+
* its options.
|
|
131
189
|
*/
|
|
132
|
-
function checkLocalOptions(declarations, table,
|
|
190
|
+
function checkLocalOptions(declarations, table, scope) {
|
|
191
|
+
const { siteOf, subject } = scope;
|
|
133
192
|
const local = { kind: 'local', subject };
|
|
134
|
-
const application = { kind: 'application' };
|
|
135
193
|
for (const declaration of declarations) {
|
|
136
194
|
const claimed = table.names.get(declaration.name);
|
|
137
195
|
if (claimed) {
|
|
138
|
-
throw keyCollision(declaration.name,
|
|
196
|
+
throw keyCollision({ ...claimed, name: declaration.name }, { name: declaration.name, owner: local, site: siteOf(declaration) });
|
|
139
197
|
}
|
|
140
198
|
}
|
|
141
|
-
const options = compileOptions(declarations,
|
|
199
|
+
const options = compileOptions(declarations, scope);
|
|
142
200
|
for (const [spelling, option] of options) {
|
|
143
201
|
const global = table.options.get(spelling);
|
|
144
|
-
|
|
145
|
-
|
|
202
|
+
const claimed = global && table.names.get(global.name);
|
|
203
|
+
const declaration = declarations.find((input) => input.name === option.name);
|
|
204
|
+
if (global && claimed && declaration) {
|
|
205
|
+
throw spellingCollision(spelling, { ...claimed, name: global.name, role: global.role }, { name: option.name, owner: local, role: option.role, site: siteOf(declaration) });
|
|
146
206
|
}
|
|
147
207
|
}
|
|
148
|
-
claimVariables(boundOptions(declarations, (
|
|
208
|
+
claimVariables(boundOptions(declarations, (input) => ({
|
|
209
|
+
phrase: `${subject} option "${input.name}"`,
|
|
210
|
+
site: siteOf(input),
|
|
211
|
+
})), table.variables);
|
|
149
212
|
return options;
|
|
150
213
|
}
|
|
151
|
-
/** The options in one list that bind a variable, each named
|
|
152
|
-
function boundOptions(inputs,
|
|
153
|
-
return inputs.flatMap((
|
|
214
|
+
/** The options in one list that bind a variable, each named and placed by its scope. */
|
|
215
|
+
function boundOptions(inputs, describe) {
|
|
216
|
+
return inputs.flatMap((input) => input.config.env === undefined ? [] : [{ ...describe(input), variable: input.config.env }]);
|
|
154
217
|
}
|
|
155
218
|
/** One plugin's options joining the table the application's globals already hold. */
|
|
156
219
|
function join(owner, inputs, table) {
|
|
157
220
|
const { names, options } = table;
|
|
221
|
+
const siteOf = pluginSites(owner.identity, inputs);
|
|
158
222
|
for (const input of inputs) {
|
|
159
223
|
const claimed = names.get(input.name);
|
|
160
224
|
if (claimed) {
|
|
161
|
-
throw keyCollision(input.name, owner, claimed);
|
|
225
|
+
throw keyCollision({ name: input.name, owner, site: siteOf(input) }, { ...claimed, name: input.name });
|
|
162
226
|
}
|
|
163
|
-
names.set(input.name, owner);
|
|
227
|
+
names.set(input.name, { owner, site: siteOf(input) });
|
|
164
228
|
}
|
|
165
|
-
for (const [spelling, option] of compileOptions(inputs,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
229
|
+
for (const [spelling, option] of compileOptions(inputs, {
|
|
230
|
+
siteOf,
|
|
231
|
+
subject: `plugin ${quoted(owner.identity)}`,
|
|
232
|
+
})) {
|
|
233
|
+
const existing = options.get(spelling);
|
|
234
|
+
const claimed = existing && names.get(existing.name);
|
|
235
|
+
const own = names.get(option.name);
|
|
236
|
+
if (existing && claimed && own) {
|
|
237
|
+
throw spellingCollision(spelling, { ...own, name: option.name, role: option.role }, { ...claimed, name: existing.name, role: existing.role });
|
|
169
238
|
}
|
|
170
239
|
options.set(spelling, option);
|
|
171
240
|
}
|
|
172
241
|
}
|
|
173
|
-
export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals,
|
|
242
|
+
export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalSite, globalTable, pluginSites, };
|
package/dist/hints.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import type { DeveloperScene } from './developer.js';
|
|
3
|
+
import type { LoomError } from './errors.js';
|
|
4
|
+
import type { CommandGraph, CommandNode } from './inspect.js';
|
|
5
|
+
import type { Output } from './output.js';
|
|
6
|
+
import type { BuiltPlugin } from './plugin.js';
|
|
7
|
+
import type { ContextualStyle } from './style.js';
|
|
8
|
+
import type { ViewRegistry } from './view.js';
|
|
9
|
+
/**
|
|
10
|
+
* What one `onFailure` hook reads beside the failure. `application` and `path` are the values the
|
|
11
|
+
* failure view reads, and `style` is the contextual style for stderr, so a hook escapes text the
|
|
12
|
+
* operator typed. `graph` is the frozen graph `inspect()` returns for the run, and `command` is the
|
|
13
|
+
* node at `path` inside it; reading either builds the run's graph once.
|
|
14
|
+
*/
|
|
15
|
+
interface FailureHookContext {
|
|
16
|
+
readonly application: string;
|
|
17
|
+
readonly path: readonly string[];
|
|
18
|
+
readonly style: ContextualStyle;
|
|
19
|
+
readonly graph: CommandGraph;
|
|
20
|
+
readonly command: CommandNode;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A plugin's `onFailure` hook: a synchronous function that returns one hint, a list of hints, or
|
|
24
|
+
* nothing for a failure `run()` renders after graph build. It receives the failure typed read-only,
|
|
25
|
+
* as a failure view does. Core does not freeze the failure, and it reads the exit code before any
|
|
26
|
+
* hook runs, so a working hook cannot change the code.
|
|
27
|
+
*/
|
|
28
|
+
type FailureHook = (failure: Readonly<LoomError>, context: FailureHookContext) => string | readonly string[] | undefined;
|
|
29
|
+
/** What the hooks can read once the graph has built: the installed plugins and the run's graph. */
|
|
30
|
+
interface BuiltRun {
|
|
31
|
+
plugins: readonly BuiltPlugin[];
|
|
32
|
+
inspected: () => CommandGraph;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Where one failure happened, filled where `run()` catches it. `built` is absent for a failure
|
|
36
|
+
* raised at or before graph build, which no hook runs for, because no valid graph exists.
|
|
37
|
+
*/
|
|
38
|
+
interface FailureScene {
|
|
39
|
+
application: string;
|
|
40
|
+
path: readonly string[];
|
|
41
|
+
built: BuiltRun | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What one run's build decides about its reports. A development build shows the author a
|
|
45
|
+
* Developer Diagnostic for every fault only the author can fix, each after the first report of the
|
|
46
|
+
* run opening with one blank line; a distributed build shows the generic defect message at most
|
|
47
|
+
* once per run.
|
|
48
|
+
*/
|
|
49
|
+
interface BuildReports {
|
|
50
|
+
readonly development: boolean;
|
|
51
|
+
/** Whether this run has written the generic defect message already. */
|
|
52
|
+
generic: boolean;
|
|
53
|
+
/** Whether this run has written a failure report already. */
|
|
54
|
+
reported: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Where one failure report is written, the registry its view resolves through, and the build. */
|
|
57
|
+
interface FailureSink {
|
|
58
|
+
output: Output;
|
|
59
|
+
registry: ViewRegistry;
|
|
60
|
+
stderr: Writable;
|
|
61
|
+
build: BuildReports;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reports one failure. The hooks run first, so the diagnostic or the view receives their hints. A
|
|
65
|
+
* view that breaks leaves core's default text without hints on the plain fallback path, and each
|
|
66
|
+
* broken contract's report follows that whole diagnostic. It answers whether a view or a hook
|
|
67
|
+
* broke, which forces the run's code to 1 outside a cancelled run.
|
|
68
|
+
*/
|
|
69
|
+
declare function reportFailure(sink: FailureSink, failure: LoomError, scene: FailureScene & {
|
|
70
|
+
host: DeveloperScene['host'];
|
|
71
|
+
}): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* What a run writes on the plain fallback path when a destination failed a write or reporting
|
|
74
|
+
* itself failed: the Developer Diagnostic of the broken destination in a development build, and
|
|
75
|
+
* the generic defect message, at most once per run, in a distributed one.
|
|
76
|
+
*/
|
|
77
|
+
declare function destinationReport(build: BuildReports, cause: unknown, scene: DeveloperScene): string;
|
|
78
|
+
export type { BuildReports, BuiltRun, FailureHook, FailureHookContext, FailureScene };
|
|
79
|
+
export { destinationReport, reportFailure };
|