@loomcli/core 0.4.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/LICENSE +21 -0
- package/dist/application.d.ts +60 -31
- package/dist/application.js +356 -149
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +64 -0
- package/dist/chain.d.ts +26 -12
- package/dist/chain.js +59 -92
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +212 -93
- package/dist/command.js +1224 -454
- 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 +115 -26
- package/dist/errors.js +333 -54
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +44 -9
- package/dist/extension.js +147 -65
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +108 -25
- package/dist/globals.d.ts +63 -22
- package/dist/globals.js +164 -39
- 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 +15 -4
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +36 -5
- package/dist/inspect.js +125 -27
- package/dist/lanes.js +1 -1
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +96 -2
- package/dist/options.js +259 -71
- package/dist/output.d.ts +11 -2
- package/dist/output.js +23 -3
- 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 +121 -55
- package/dist/plugin.js +496 -126
- 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 +58 -0
- package/dist/sources.js +258 -0
- 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 +76 -21
- package/dist/validation.d.ts +65 -10
- package/dist/validation.js +314 -108
- package/dist/view.d.ts +49 -15
- package/dist/view.js +157 -79
- package/package.json +3 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every prototype in one value's chain, most derived first, or `undefined` when the chain cannot
|
|
3
|
+
* be read: a proxy trap throws, a link repeats, or the chain runs past `maxChainLinks` links. A
|
|
4
|
+
* value with a `null` prototype has an empty chain. Override resolution and translator resolution
|
|
5
|
+
* both walk the chain this returns.
|
|
6
|
+
*/
|
|
7
|
+
export declare function prototypeChain(value: object): object[] | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** The most links a prototype chain holds before core reads it as a chain that never ends. */
|
|
2
|
+
const maxChainLinks = 1024;
|
|
3
|
+
/** The walk behind `prototypeChain`, which throws where a proxy trap throws. */
|
|
4
|
+
function walkChain(value) {
|
|
5
|
+
const chain = [];
|
|
6
|
+
const seen = new Set([value]);
|
|
7
|
+
for (let link = Reflect.getPrototypeOf(value); link !== null; link = Reflect.getPrototypeOf(link)) {
|
|
8
|
+
if (seen.has(link) || chain.length >= maxChainLinks) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
seen.add(link);
|
|
12
|
+
chain.push(link);
|
|
13
|
+
}
|
|
14
|
+
return chain;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Every prototype in one value's chain, most derived first, or `undefined` when the chain cannot
|
|
18
|
+
* be read: a proxy trap throws, a link repeats, or the chain runs past `maxChainLinks` links. A
|
|
19
|
+
* value with a `null` prototype has an empty chain. Override resolution and translator resolution
|
|
20
|
+
* both walk the chain this returns.
|
|
21
|
+
*/
|
|
22
|
+
export function prototypeChain(value) {
|
|
23
|
+
try {
|
|
24
|
+
return walkChain(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/rendering.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FactSite } from './facts.js';
|
|
1
2
|
import type { Host } from './types.js';
|
|
2
3
|
type SwitchPolicy = 'auto' | 'always' | 'never';
|
|
3
4
|
interface RenderingPolicy {
|
|
@@ -14,7 +15,11 @@ interface Capabilities {
|
|
|
14
15
|
depth: 16 | 256 | 'truecolor';
|
|
15
16
|
mainGlyphs: boolean;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* One declared rendering policy, checked against its closed sets. `site` is where it was declared,
|
|
20
|
+
* the Application's `rendering` option or `run()`'s, which a fault marks.
|
|
21
|
+
*/
|
|
22
|
+
declare function renderingPolicy(value: unknown, site: FactSite): RenderingPolicy;
|
|
18
23
|
/** A run owns captured facts; stdout and stderr each resolve this one policy independently. */
|
|
19
24
|
declare function capabilities(host: Host, destination: 'stdout' | 'stderr', policy: RenderingPolicy): Capabilities;
|
|
20
25
|
export type { Capabilities, RenderingPolicy };
|
package/dist/rendering.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { DeclarationError } from './errors.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { partFinding } from './facts.js';
|
|
3
|
+
import { isPlainObject } from './plain.js';
|
|
4
|
+
import { renderingPolicyRule } from './plugin-rules.js';
|
|
5
|
+
/**
|
|
6
|
+
* One declared rendering policy, checked against its closed sets. `site` is where it was declared,
|
|
7
|
+
* the Application's `rendering` option or `run()`'s, which a fault marks.
|
|
8
|
+
*/
|
|
9
|
+
function renderingPolicy(value, site) {
|
|
4
10
|
if (value === undefined) {
|
|
5
11
|
return {};
|
|
6
12
|
}
|
|
7
13
|
if (!isPlainObject(value)) {
|
|
8
|
-
throw new DeclarationError(
|
|
14
|
+
throw new DeclarationError(renderingPolicyRule, {
|
|
15
|
+
correction: 'Supply an object, or omit rendering.',
|
|
16
|
+
findings: [partFinding(site, [])],
|
|
17
|
+
sentence: 'The rendering policy is not an object.',
|
|
18
|
+
});
|
|
9
19
|
}
|
|
10
20
|
const result = {};
|
|
11
21
|
for (const field of ['color', 'modifiers', 'hyperlinks']) {
|
|
12
22
|
const policy = value[field];
|
|
13
23
|
if (policy !== undefined && policy !== 'auto' && policy !== 'always' && policy !== 'never') {
|
|
14
|
-
throw new DeclarationError(
|
|
24
|
+
throw new DeclarationError(renderingPolicyRule, {
|
|
25
|
+
correction: `Supply one of the three, or omit ${field}.`,
|
|
26
|
+
findings: [partFinding(site, [field])],
|
|
27
|
+
sentence: `Rendering ${field} is not auto, always, or never.`,
|
|
28
|
+
});
|
|
15
29
|
}
|
|
16
30
|
if (policy !== undefined) {
|
|
17
31
|
result[field] = policy;
|
|
@@ -19,7 +33,11 @@ function renderingPolicy(value) {
|
|
|
19
33
|
}
|
|
20
34
|
const controls = value.terminalControls;
|
|
21
35
|
if (controls !== undefined && controls !== 'strip' && controls !== 'preserve') {
|
|
22
|
-
throw new DeclarationError(
|
|
36
|
+
throw new DeclarationError(renderingPolicyRule, {
|
|
37
|
+
correction: 'Supply strip or preserve, or omit terminalControls.',
|
|
38
|
+
findings: [partFinding(site, ['terminalControls'])],
|
|
39
|
+
sentence: 'Rendering terminalControls is not strip or preserve.',
|
|
40
|
+
});
|
|
23
41
|
}
|
|
24
42
|
if (controls !== undefined) {
|
|
25
43
|
result.terminalControls = controls;
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A value thrown from an action, a middleware, or a source that no translator answered. */
|
|
2
|
+
declare const foreignThrow: import("./diagnostic-text.js").DiagnosticRule;
|
|
3
|
+
/** The fix every foreign throw shares. */
|
|
4
|
+
declare const foreignThrowCorrection = "Catch the error where it is thrown and throw a failure class, such as FatalError, with a sentence the operator can act on.";
|
|
5
|
+
/** A thrown value that inherits from a failure class without having been constructed by one. */
|
|
6
|
+
declare const unconstructedFailure: import("./diagnostic-text.js").DiagnosticRule;
|
|
7
|
+
/** A translator that threw or answered with something other than a failure. */
|
|
8
|
+
declare const brokenTranslator: import("./diagnostic-text.js").DiagnosticRule;
|
|
9
|
+
/** The fix every broken translator shares. */
|
|
10
|
+
declare const brokenTranslatorCorrection = "Return a failure, or undefined to pass, and throw nothing from the translator.";
|
|
11
|
+
/** A failure view that threw or returned a value that is not a string. */
|
|
12
|
+
declare const brokenFailureView: import("./diagnostic-text.js").DiagnosticRule;
|
|
13
|
+
/** An output or lane view that threw or returned a value that is not a string. */
|
|
14
|
+
declare const brokenOutputView: import("./diagnostic-text.js").DiagnosticRule;
|
|
15
|
+
/** The fix a broken failure view and a broken output view share. */
|
|
16
|
+
declare const viewCorrection = "Return a string from the view's render function, and throw nothing from it.";
|
|
17
|
+
/** An `onFailure` hook that threw or returned a value that is not hints. */
|
|
18
|
+
declare const brokenFailureHook: import("./diagnostic-text.js").DiagnosticRule;
|
|
19
|
+
/** A plugin's middleware or source loader that rejected or exported no default function. */
|
|
20
|
+
declare const pluginLoaderFailed: import("./diagnostic-text.js").DiagnosticRule;
|
|
21
|
+
/** A middleware that called `next()` twice, or after it returned. */
|
|
22
|
+
declare const nextMisuse: import("./diagnostic-text.js").DiagnosticRule;
|
|
23
|
+
/** An action or a plugin that broke the promise a declared result makes. */
|
|
24
|
+
declare const resultContract: import("./diagnostic-text.js").DiagnosticRule;
|
|
25
|
+
/** A validator that threw, rejected, or returned a malformed result. */
|
|
26
|
+
declare const validatorFailed: import("./diagnostic-text.js").DiagnosticRule;
|
|
27
|
+
/** A middleware that assigned `view` a name the routed Command's result cannot render. */
|
|
28
|
+
declare const viewSelection: import("./diagnostic-text.js").DiagnosticRule;
|
|
29
|
+
/** The fix every invalid view selection shares. */
|
|
30
|
+
declare const viewSelectionCorrection = "Assign view one of the view names the routed Command's result declares, and only on a Command that declares a result.";
|
|
31
|
+
/** A `run()` option that holds a value of the wrong kind. */
|
|
32
|
+
declare const runOptions: import("./diagnostic-text.js").DiagnosticRule;
|
|
33
|
+
/** A configuration source whose answers break the answers rule. */
|
|
34
|
+
declare const sourceAnswers: import("./diagnostic-text.js").DiagnosticRule;
|
|
35
|
+
/** The fix every source answers fault shares. */
|
|
36
|
+
declare const sourceAnswersCorrection = "Return a record that maps each requested option's name to { value, label }, with a value of the option's raw type.";
|
|
37
|
+
/** A graph that `inspect()` did not return, or whose nodes disagree with the build it came from. */
|
|
38
|
+
declare const foreignGraph: import("./diagnostic-text.js").DiagnosticRule;
|
|
39
|
+
/** The fix every foreign graph shares. */
|
|
40
|
+
declare const foreignGraphCorrection = "Pass the graph inspect() returned, unchanged.";
|
|
41
|
+
/** A host stream that failed a write the run owed it. */
|
|
42
|
+
declare const brokenDestination: import("./diagnostic-text.js").DiagnosticRule;
|
|
43
|
+
/** A failure class whose exit code is outside 1 through 125. */
|
|
44
|
+
declare const failureExitCode: import("./diagnostic-text.js").DiagnosticRule;
|
|
45
|
+
/** A diagnostic rule identity outside the `<package>[/<subpath>...]/<kebab-case-rule>` grammar. */
|
|
46
|
+
declare const ruleIdentity: import("./diagnostic-text.js").DiagnosticRule;
|
|
47
|
+
/** A diagnostic rule whose headline or explanation holds no prose. */
|
|
48
|
+
declare const ruleProse: import("./diagnostic-text.js").DiagnosticRule;
|
|
49
|
+
/** A diagnostic rule whose docs value is not a web address. */
|
|
50
|
+
declare const ruleDocs: import("./diagnostic-text.js").DiagnosticRule;
|
|
51
|
+
export { brokenDestination, brokenFailureHook, brokenFailureView, brokenOutputView, brokenTranslator, brokenTranslatorCorrection, failureExitCode, foreignGraph, foreignGraphCorrection, foreignThrow, foreignThrowCorrection, nextMisuse, pluginLoaderFailed, resultContract, ruleDocs, ruleIdentity, ruleProse, runOptions, sourceAnswers, sourceAnswersCorrection, unconstructedFailure, validatorFailed, viewCorrection, viewSelection, viewSelectionCorrection, };
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { registerRule } from './diagnostic-text.js';
|
|
2
|
+
/*
|
|
3
|
+
* Core's own rules for the defects it raises, the run-time declaration faults it finds, and the
|
|
4
|
+
* faults of the two things an author declares to describe a failure: a failure class's exit code
|
|
5
|
+
* and a diagnostic rule. Each is declared once here and shared by every site that raises it, as a
|
|
6
|
+
* plugin's rules are.
|
|
7
|
+
*/
|
|
8
|
+
/** A value thrown from an action, a middleware, or a source that no translator answered. */
|
|
9
|
+
const foreignThrow = registerRule('@loomcli/core/foreign-throw', {
|
|
10
|
+
explanation: 'Core reports a value that an action, a middleware, or a configuration source threw, and that no translator answered, as a defect, because only the author knows what it means. A distributed build shows the operator one generic message in its place.',
|
|
11
|
+
headline: 'Unhandled exception',
|
|
12
|
+
});
|
|
13
|
+
/** The fix every foreign throw shares. */
|
|
14
|
+
const foreignThrowCorrection = 'Catch the error where it is thrown and throw a failure class, such as FatalError, with a sentence the operator can act on.';
|
|
15
|
+
/** A thrown value that inherits from a failure class without having been constructed by one. */
|
|
16
|
+
const unconstructedFailure = registerRule('@loomcli/core/unconstructed-failure', {
|
|
17
|
+
explanation: 'A failure class records its exit code when a failure is constructed, so a value that only inherits from one, such as one made with Object.create(), carries no code core can trust.',
|
|
18
|
+
headline: 'Failure never constructed',
|
|
19
|
+
});
|
|
20
|
+
/** A translator that threw or answered with something other than a failure. */
|
|
21
|
+
const brokenTranslator = registerRule('@loomcli/core/broken-translator', {
|
|
22
|
+
explanation: 'A translator turns a foreign throw into a failure synchronously. A throw, or any answer other than a failure or undefined, leaves core no failure to report, so it consults no later translator.',
|
|
23
|
+
headline: 'Broken translator',
|
|
24
|
+
});
|
|
25
|
+
/** The fix every broken translator shares. */
|
|
26
|
+
const brokenTranslatorCorrection = 'Return a failure, or undefined to pass, and throw nothing from the translator.';
|
|
27
|
+
/** A failure view that threw or returned a value that is not a string. */
|
|
28
|
+
const brokenFailureView = registerRule('@loomcli/core/broken-failure-view', {
|
|
29
|
+
explanation: "A failure view turns a failure into the text the operator reads, synchronously and without throwing. Core wrote its own text for the failure in the view's place.",
|
|
30
|
+
headline: 'Broken failure view',
|
|
31
|
+
});
|
|
32
|
+
/** An output or lane view that threw or returned a value that is not a string. */
|
|
33
|
+
const brokenOutputView = registerRule('@loomcli/core/broken-output-view', {
|
|
34
|
+
explanation: 'A view turns a value into text synchronously and without throwing. Core wrote nothing for the call whose view broke, and a run whose output is incomplete cannot succeed.',
|
|
35
|
+
headline: 'Broken output view',
|
|
36
|
+
});
|
|
37
|
+
/** The fix a broken failure view and a broken output view share. */
|
|
38
|
+
const viewCorrection = "Return a string from the view's render function, and throw nothing from it.";
|
|
39
|
+
/** An `onFailure` hook that threw or returned a value that is not hints. */
|
|
40
|
+
const brokenFailureHook = registerRule('@loomcli/core/broken-failure-hook', {
|
|
41
|
+
explanation: "An onFailure hook adds hint lines under a failure, synchronously and without throwing. Core dropped this hook's hints and rendered the failure with every other plugin's.",
|
|
42
|
+
headline: 'Broken failure hook',
|
|
43
|
+
});
|
|
44
|
+
/** A plugin's middleware or source loader that rejected or exported no default function. */
|
|
45
|
+
const pluginLoaderFailed = registerRule('@loomcli/core/plugin-loader-failed', {
|
|
46
|
+
explanation: "Core loads a plugin's middleware or source module the first time a run reaches it, and the module owes its function as its default export.",
|
|
47
|
+
headline: 'Plugin loader failed',
|
|
48
|
+
});
|
|
49
|
+
/** A middleware that called `next()` twice, or after it returned. */
|
|
50
|
+
const nextMisuse = registerRule('@loomcli/core/next-misuse', {
|
|
51
|
+
explanation: 'next() continues the middleware chain once, while the middleware that received it runs. A second call, or a call after the middleware returned, has no chain left to continue.',
|
|
52
|
+
headline: 'next() misused',
|
|
53
|
+
});
|
|
54
|
+
/** An action or a plugin that broke the promise a declared result makes. */
|
|
55
|
+
const resultContract = registerRule('@loomcli/core/result-contract', {
|
|
56
|
+
explanation: 'A Command that declares a result promises its consumer one value, or one sequence of rows, that its action emits once through out.results().',
|
|
57
|
+
headline: 'Result contract broken',
|
|
58
|
+
});
|
|
59
|
+
/** A validator that threw, rejected, or returned a malformed result. */
|
|
60
|
+
const validatorFailed = registerRule('@loomcli/core/validator-failed', {
|
|
61
|
+
explanation: 'Only an issue a validator returns states a validation verdict. A validator that throws, rejects, or returns anything but a Standard Schema result leaves core no verdict to report, so validation stops and the action does not run.',
|
|
62
|
+
headline: 'Validator failed',
|
|
63
|
+
});
|
|
64
|
+
/** A middleware that assigned `view` a name the routed Command's result cannot render. */
|
|
65
|
+
const viewSelection = registerRule('@loomcli/core/view-selection', {
|
|
66
|
+
explanation: "A middleware selects one of the views the routed Command's result declares, by name, before the action runs. A Command that declares no result has no view to select, and a name its result does not declare has no view to render.",
|
|
67
|
+
headline: 'Invalid view selection',
|
|
68
|
+
});
|
|
69
|
+
/** The fix every invalid view selection shares. */
|
|
70
|
+
const viewSelectionCorrection = "Assign view one of the view names the routed Command's result declares, and only on a Command that declares a result.";
|
|
71
|
+
/** A `run()` option that holds a value of the wrong kind. */
|
|
72
|
+
const runOptions = registerRule('@loomcli/core/run-options', {
|
|
73
|
+
explanation: 'run() reads its options before it builds the graph. A caller that embeds the application, such as a test, passes them, and a value of the wrong kind leaves the run nothing to act on.',
|
|
74
|
+
headline: 'Invalid run options',
|
|
75
|
+
});
|
|
76
|
+
/** A configuration source whose answers break the answers rule. */
|
|
77
|
+
const sourceAnswers = registerRule('@loomcli/core/source-answers', {
|
|
78
|
+
explanation: 'A configuration source answers with a record that maps each option core requested to { value, label }, where the value has the raw type the option takes and the label is one line of prose. Core fills no option from any other answer.',
|
|
79
|
+
headline: 'Invalid source answers',
|
|
80
|
+
});
|
|
81
|
+
/** The fix every source answers fault shares. */
|
|
82
|
+
const sourceAnswersCorrection = "Return a record that maps each requested option's name to { value, label }, with a value of the option's raw type.";
|
|
83
|
+
/** A graph that `inspect()` did not return, or whose nodes disagree with the build it came from. */
|
|
84
|
+
const foreignGraph = registerRule('@loomcli/core/foreign-graph', {
|
|
85
|
+
explanation: 'Core reads a Command graph through the build inspect() rendered it from. A graph inspect() did not return, or one whose nodes do not match that build, leaves core no build to read, so the run stops rather than hand a plugin the wrong Command.',
|
|
86
|
+
headline: 'Graph not from inspect()',
|
|
87
|
+
});
|
|
88
|
+
/** The fix every foreign graph shares. */
|
|
89
|
+
const foreignGraphCorrection = 'Pass the graph inspect() returned, unchanged.';
|
|
90
|
+
/** A host stream that failed a write the run owed it. */
|
|
91
|
+
const brokenDestination = registerRule('@loomcli/core/broken-destination', {
|
|
92
|
+
explanation: "Core writes the run's output and its failure reports to the host streams run() captured. A stream that fails a write loses whatever followed it, so the run cannot succeed.",
|
|
93
|
+
headline: 'Broken destination',
|
|
94
|
+
});
|
|
95
|
+
/** A failure class whose exit code is outside 1 through 125. */
|
|
96
|
+
const failureExitCode = registerRule('@loomcli/core/failure-exit-code', {
|
|
97
|
+
explanation: "A failure's exit code tells the shell how the run ended: 0 means success, and 126 and above belong to the shell and to signals, so a failure exits with a code from 1 through 125. Core never clamps or replaces the code a failure class declares, so a code no failure may exit with is rejected where the class is first constructed.",
|
|
98
|
+
headline: 'Undeclarable exit code',
|
|
99
|
+
});
|
|
100
|
+
/** A diagnostic rule identity outside the `<package>[/<subpath>...]/<kebab-case-rule>` grammar. */
|
|
101
|
+
const ruleIdentity = registerRule('@loomcli/core/rule-identity', {
|
|
102
|
+
explanation: "A rule's identity names the package that declares it and the rule inside it, so tooling keys on it and two packages never share one. It is an identity, a package name and any kebab-case subpath segments that name the part of the package that owns the rule, then a kebab-case rule name, joined by /.",
|
|
103
|
+
headline: 'Invalid rule identity',
|
|
104
|
+
});
|
|
105
|
+
/** A diagnostic rule whose headline or explanation holds no prose. */
|
|
106
|
+
const ruleProse = registerRule('@loomcli/core/rule-prose', {
|
|
107
|
+
explanation: "A rule's banner prints its headline, and its explanation teaches why the rule exists, so each holds prose.",
|
|
108
|
+
headline: 'Empty rule text',
|
|
109
|
+
});
|
|
110
|
+
/** A diagnostic rule whose docs value is not a web address. */
|
|
111
|
+
const ruleDocs = registerRule('@loomcli/core/rule-docs', {
|
|
112
|
+
explanation: "A diagnostic prints a rule's docs as a link the author follows, so it is an absolute http or https URL.",
|
|
113
|
+
headline: 'Invalid rule docs',
|
|
114
|
+
});
|
|
115
|
+
export { brokenDestination, brokenFailureHook, brokenFailureView, brokenOutputView, brokenTranslator, brokenTranslatorCorrection, failureExitCode, foreignGraph, foreignGraphCorrection, foreignThrow, foreignThrowCorrection, nextMisuse, pluginLoaderFailed, resultContract, ruleDocs, ruleIdentity, ruleProse, runOptions, sourceAnswers, sourceAnswersCorrection, unconstructedFailure, validatorFailed, viewCorrection, viewSelection, viewSelectionCorrection, };
|
package/dist/sequence.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InternalError, routedSubject } from './errors.js';
|
|
2
|
+
import { resultContract } from './rules.js';
|
|
2
3
|
/**
|
|
3
4
|
* A failure the source raised, so the writer tells it apart from the view and write faults its own
|
|
4
5
|
* pieces raise, which the output path has accounted for already.
|
|
@@ -44,7 +45,11 @@ function stepsOf(writer) {
|
|
|
44
45
|
throw new SourceFault(error);
|
|
45
46
|
}
|
|
46
47
|
if (!steps) {
|
|
47
|
-
throw new SourceFault(new InternalError(
|
|
48
|
+
throw new SourceFault(new InternalError(resultContract, {
|
|
49
|
+
cause: undefined,
|
|
50
|
+
correction: 'Pass out.results() an iterable or an async iterable of rows.',
|
|
51
|
+
sentence: `The result of ${routedSubject(writer.path)} is not iterable.`,
|
|
52
|
+
}));
|
|
48
53
|
}
|
|
49
54
|
return steps;
|
|
50
55
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { LoomError } 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
|
+
/** Offers the resolver's foreign throw to the translators, ahead of the plugin-fault wrap. */
|
|
29
|
+
offer: (thrown: unknown) => LoomError | undefined;
|
|
30
|
+
/** The channel a source writes through, whose results call names the source. */
|
|
31
|
+
out: Out;
|
|
32
|
+
plugins: readonly BuiltPlugin[];
|
|
33
|
+
/** The `OptionNode` of one option, read from the graph `inspect()` would return for the run. */
|
|
34
|
+
request: (input: OptionInput, global: boolean) => OptionNode;
|
|
35
|
+
signal: AbortSignal;
|
|
36
|
+
/** The contextual style an action receives, so a source escapes raw data before it warns. */
|
|
37
|
+
style: ContextualStyle;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* What the stage found beside the values it filled. `labels` names where each filled option's value
|
|
41
|
+
* came from, by option name, and `rejected` names the variable of each Boolean option whose value
|
|
42
|
+
* is outside the grammar. Both are internal to core's failure messages. `fault` is a configuration
|
|
43
|
+
* source's own fault, or the failure its resolver threw, which stops the stage and takes the place
|
|
44
|
+
* of every validation problem.
|
|
45
|
+
*/
|
|
46
|
+
interface SourceOutcome {
|
|
47
|
+
fault: LoomError | undefined;
|
|
48
|
+
labels: ReadonlyMap<string, string>;
|
|
49
|
+
rejected: ReadonlyMap<string, string>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The input-source stage: the environment, then the configuration source, for every option in
|
|
53
|
+
* scope that argv left unfilled. When no option is left to ask, the source never loads. A source
|
|
54
|
+
* fault stops the stage and fills nothing more.
|
|
55
|
+
*/
|
|
56
|
+
declare function fillInputs(stage: SourceStage): Promise<SourceOutcome>;
|
|
57
|
+
export type { SourceOutcome, SourceStage, StageScope };
|
|
58
|
+
export { fillInputs };
|
package/dist/sources.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { asSentence, foreignFailure, InternalError, LoomError, reasonOf } from './errors.js';
|
|
2
|
+
import { isProseLine } from './facts.js';
|
|
3
|
+
import { isSupplied } from './options.js';
|
|
4
|
+
import { isPlainObject } from './plain.js';
|
|
5
|
+
import { loadDefault, pluginSentence, pluginValues } from './plugin.js';
|
|
6
|
+
import { foreignThrow, foreignThrowCorrection, sourceAnswers, sourceAnswersCorrection, } from './rules.js';
|
|
7
|
+
/** The Boolean grammar a bound variable is read through: the whole value, case-insensitive. */
|
|
8
|
+
const truths = new Map([
|
|
9
|
+
['0', false],
|
|
10
|
+
['1', true],
|
|
11
|
+
['false', false],
|
|
12
|
+
['true', true],
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Reads one option's bound variable. An empty variable is unset and falls through, a string option
|
|
16
|
+
* receives the raw string, and a Boolean option reads the whole value through the grammar, so the
|
|
17
|
+
* value states the option's value and not a spelling.
|
|
18
|
+
*/
|
|
19
|
+
function readVariable(input, env) {
|
|
20
|
+
const variable = input.config.env;
|
|
21
|
+
const raw = variable !== undefined && Object.hasOwn(env, variable) ? env[variable] : undefined;
|
|
22
|
+
if (raw === undefined || raw === '') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
if (input.config.type === 'string') {
|
|
26
|
+
return { kind: 'fill', value: raw };
|
|
27
|
+
}
|
|
28
|
+
const value = truths.get(raw.toLowerCase());
|
|
29
|
+
return value === undefined ? { kind: 'rejected' } : { kind: 'fill', value };
|
|
30
|
+
}
|
|
31
|
+
/** Writes one filled value where the option's own shape keeps it, a list as the run's own copy. */
|
|
32
|
+
function fill(values, name, value) {
|
|
33
|
+
if (typeof value === 'boolean') {
|
|
34
|
+
values.booleans.set(name, value);
|
|
35
|
+
}
|
|
36
|
+
else if (typeof value === 'string') {
|
|
37
|
+
values.strings.set(name, value);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
values.lists.set(name, [...value]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** How a fault names the value an answer owed, by the shape the option takes. */
|
|
44
|
+
const owed = {
|
|
45
|
+
boolean: 'a Boolean',
|
|
46
|
+
list: 'an array of strings',
|
|
47
|
+
string: 'a string',
|
|
48
|
+
};
|
|
49
|
+
function rawKind(input) {
|
|
50
|
+
if (input.config.type === 'boolean') {
|
|
51
|
+
return 'boolean';
|
|
52
|
+
}
|
|
53
|
+
return input.config.multiple === true ? 'list' : 'string';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The run's own copy of an answered list, or `undefined` when it is not a list of strings. Every
|
|
57
|
+
* index is read, so a hole fails the check as any entry that is not a string does.
|
|
58
|
+
*/
|
|
59
|
+
function stringList(value) {
|
|
60
|
+
if (!Array.isArray(value)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const list = [];
|
|
64
|
+
// Iteration visits a hole as undefined, where every() skips it.
|
|
65
|
+
for (const entry of value) {
|
|
66
|
+
if (typeof entry !== 'string') {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
list.push(entry);
|
|
70
|
+
}
|
|
71
|
+
return list;
|
|
72
|
+
}
|
|
73
|
+
/** One answered value as the raw type its option takes, or `undefined` when it holds another. */
|
|
74
|
+
function rawValue(kind, value) {
|
|
75
|
+
if (kind === 'list') {
|
|
76
|
+
return stringList(value);
|
|
77
|
+
}
|
|
78
|
+
if (kind === 'boolean') {
|
|
79
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
return typeof value === 'string' ? value : undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The faults the answers rule names. Reading the answers runs the plugin's own code, such as a
|
|
85
|
+
* getter, so a throw of any other kind while reading them is the plugin's failure.
|
|
86
|
+
*/
|
|
87
|
+
const ruleFaults = new WeakSet();
|
|
88
|
+
/** A fault the answers rule names, recorded so that reading the answers rethrows it unframed. */
|
|
89
|
+
function ruleFault(message) {
|
|
90
|
+
const fault = new InternalError(sourceAnswers, {
|
|
91
|
+
cause: undefined,
|
|
92
|
+
correction: sourceAnswersCorrection,
|
|
93
|
+
sentence: message,
|
|
94
|
+
});
|
|
95
|
+
ruleFaults.add(fault);
|
|
96
|
+
return fault;
|
|
97
|
+
}
|
|
98
|
+
/** One answer read under the answers rule: an object that holds a one-line label and a value. */
|
|
99
|
+
function readAnswer(sentence, input, answer) {
|
|
100
|
+
const shape = isPlainObject(answer) ? answer : undefined;
|
|
101
|
+
const label = shape?.label;
|
|
102
|
+
if (!shape || !isProseLine(label)) {
|
|
103
|
+
throw ruleFault(`${sentence} answered option "${input.name}" with an answer that is not { value, label }.`);
|
|
104
|
+
}
|
|
105
|
+
const kind = rawKind(input);
|
|
106
|
+
const value = rawValue(kind, shape.value);
|
|
107
|
+
if (value === undefined) {
|
|
108
|
+
throw ruleFault(`${sentence} answered option "${input.name}" with a value that is not ${owed[kind]}.`);
|
|
109
|
+
}
|
|
110
|
+
return { label, value };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Every answer the resolver returned, read before any is filled, so an answer the rule rejects
|
|
114
|
+
* leaves every requested option unfilled. A key core did not request is the source's fault.
|
|
115
|
+
*/
|
|
116
|
+
function readAnswers(sentence, answers, requested) {
|
|
117
|
+
if (!isPlainObject(answers)) {
|
|
118
|
+
throw ruleFault(`${sentence} returned configuration answers that are not a record.`);
|
|
119
|
+
}
|
|
120
|
+
const byName = new Map(requested.map((target) => [target.input.name, target]));
|
|
121
|
+
return Object.entries(answers).map(([name, answer]) => {
|
|
122
|
+
const target = byName.get(name);
|
|
123
|
+
if (!target) {
|
|
124
|
+
throw ruleFault(`${sentence} answered option "${name}", which core did not request.`);
|
|
125
|
+
}
|
|
126
|
+
const { label, value } = readAnswer(sentence, target.input, answer);
|
|
127
|
+
return { label, target, value };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The default export a source loader must resolve to. A loaded module is data core never declared,
|
|
132
|
+
* so the check is the one runtime fact that decides it: the export is callable.
|
|
133
|
+
*/
|
|
134
|
+
function isResolverExport(value) {
|
|
135
|
+
return typeof value === 'function';
|
|
136
|
+
}
|
|
137
|
+
/** The fault of a source that threw, while it ran or while core read what it returned. */
|
|
138
|
+
function sourceFailure(sentence, error) {
|
|
139
|
+
return new InternalError(foreignThrow, {
|
|
140
|
+
cause: error,
|
|
141
|
+
correction: foreignThrowCorrection,
|
|
142
|
+
sentence: `${sentence} failed in its configuration source: ${asSentence(reasonOf(error))}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Asks the one configuration source about every requested option, and answers with what it said.
|
|
147
|
+
* Core loads the source here and calls it once, awaiting it; one cancelled during the call awaits
|
|
148
|
+
* it. The run checks its signal and then reaches the load with no await between, so a run
|
|
149
|
+
* cancelled before the load starts neither step, and one cancelled during the load calls nothing.
|
|
150
|
+
* Core builds the context before the call, so a fault of its own is never the plugin's.
|
|
151
|
+
*/
|
|
152
|
+
async function askSource(stage, call) {
|
|
153
|
+
const { owner, requested } = call;
|
|
154
|
+
const resolver = await loadDefault(owner.identity, owner.source.load, {
|
|
155
|
+
guard: isResolverExport,
|
|
156
|
+
noun: 'source',
|
|
157
|
+
});
|
|
158
|
+
if (stage.signal.aborted) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const sentence = pluginSentence(owner.identity);
|
|
162
|
+
const context = {
|
|
163
|
+
graph: stage.inspected(),
|
|
164
|
+
host: stage.host,
|
|
165
|
+
options: pluginValues(owner.inputs, stage.globals.values),
|
|
166
|
+
out: stage.out,
|
|
167
|
+
requests: requested.map(({ input, scope }) => stage.request(input, scope.global)),
|
|
168
|
+
style: stage.style,
|
|
169
|
+
};
|
|
170
|
+
let answers = undefined;
|
|
171
|
+
try {
|
|
172
|
+
answers = await resolver(context);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// The resolver's own failure reports with its class's code, as an action's does.
|
|
176
|
+
// That covers an InputError, the FatalError out.fatal() throws, and its out.results() fault.
|
|
177
|
+
// A foreign throw is offered to the translators.
|
|
178
|
+
// One that no translator answers is the plugin's fault.
|
|
179
|
+
if (error instanceof LoomError) {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
throw stage.offer(error) ?? sourceFailure(sentence, error);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return readAnswers(sentence, answers, requested);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error instanceof InternalError && ruleFaults.has(error)) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
throw sourceFailure(sentence, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The environment tier: each option in scope that argv left unfilled reads its bound variable. A
|
|
196
|
+
* filled option is labeled with its variable, and a Boolean one outside the grammar is recorded
|
|
197
|
+
* as rejected, which fills nothing.
|
|
198
|
+
*/
|
|
199
|
+
function fillFromEnvironment(scopes, env) {
|
|
200
|
+
const labels = new Map();
|
|
201
|
+
const rejected = new Map();
|
|
202
|
+
for (const scope of scopes) {
|
|
203
|
+
for (const input of scope.inputs) {
|
|
204
|
+
const reading = isSupplied(scope.values, input.name) ? undefined : readVariable(input, env);
|
|
205
|
+
const variable = input.config.env ?? '';
|
|
206
|
+
if (reading?.kind === 'fill') {
|
|
207
|
+
fill(scope.values, input.name, reading.value);
|
|
208
|
+
labels.set(input.name, variable);
|
|
209
|
+
}
|
|
210
|
+
else if (reading?.kind === 'rejected') {
|
|
211
|
+
rejected.set(input.name, variable);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { labels, rejected };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The options the configuration source is asked about: those still unfilled that carry its
|
|
219
|
+
* binding and hold no environment fault, the globals table first and then the routed Command's own,
|
|
220
|
+
* each in declaration order.
|
|
221
|
+
*/
|
|
222
|
+
function requestedOf(scopes, excluded, bound) {
|
|
223
|
+
return scopes.flatMap((scope) => scope.inputs
|
|
224
|
+
.filter((input) => !isSupplied(scope.values, input.name) &&
|
|
225
|
+
!excluded.has(input.name) &&
|
|
226
|
+
Object.hasOwn(bound.extensions.get(input) ?? {}, bound.binding))
|
|
227
|
+
.map((input) => ({ input, scope })));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The input-source stage: the environment, then the configuration source, for every option in
|
|
231
|
+
* scope that argv left unfilled. When no option is left to ask, the source never loads. A source
|
|
232
|
+
* fault stops the stage and fills nothing more.
|
|
233
|
+
*/
|
|
234
|
+
async function fillInputs(stage) {
|
|
235
|
+
const scopes = stage.locals ? [stage.globals, stage.locals] : [stage.globals];
|
|
236
|
+
const { labels, rejected } = fillFromEnvironment(scopes, stage.host.env);
|
|
237
|
+
const owner = stage.plugins.find((installed) => installed.source !== undefined);
|
|
238
|
+
const requested = owner
|
|
239
|
+
? requestedOf(scopes, rejected, { binding: owner.source.binding, extensions: stage.extensions })
|
|
240
|
+
: [];
|
|
241
|
+
if (!owner || requested.length === 0) {
|
|
242
|
+
return { fault: undefined, labels, rejected };
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
for (const { label, target, value } of await askSource(stage, { owner, requested })) {
|
|
246
|
+
fill(target.scope.values, target.input.name, value);
|
|
247
|
+
labels.set(target.input.name, label);
|
|
248
|
+
}
|
|
249
|
+
return { fault: undefined, labels, rejected };
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
// The resolver's own failure keeps its class, and so its code.
|
|
253
|
+
// Every other fault above is raised as an internal error, and anything else is wrapped the same way.
|
|
254
|
+
const fault = error instanceof LoomError ? error : foreignFailure(error);
|
|
255
|
+
return { fault, labels, rejected };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export { fillInputs };
|
package/dist/style-wire.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isPlainObject } from './facts.js';
|
|
2
1
|
import { glyphForms } from './glyphs.generated.js';
|
|
2
|
+
import { isPlainObject } from './plain.js';
|
|
3
3
|
import { applyOperations, emptyAttributes } from './style-state.js';
|
|
4
4
|
import { close, colorFallbacks, escape, headerEnd, isColorName, modifiers, open, tokens, } from './style.js';
|
|
5
5
|
function isByte(value) {
|
package/dist/style.js
CHANGED
package/dist/theme.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { Palette } from './style-state.js';
|
|
2
|
+
/**
|
|
3
|
+
* One plugin's theme, read as the palette core resolves semantic styles through. Each fault marks
|
|
4
|
+
* the theme, or the one entry at fault, in `plugin(identity, { theme })`.
|
|
5
|
+
*/
|
|
2
6
|
declare function buildTheme(value: unknown, identity: string): Palette;
|
|
3
7
|
export { buildTheme };
|