@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
package/dist/theme.js
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
|
-
import { DeclarationError } from './errors.js';
|
|
2
|
-
import {
|
|
1
|
+
import { DeclarationError, quoted } from './errors.js';
|
|
2
|
+
import { partFinding, slotSite } from './facts.js';
|
|
3
|
+
import { isPlainObject } from './plain.js';
|
|
4
|
+
import { themeMapping, themeNameTaken } from './plugin-rules.js';
|
|
3
5
|
import { chains, reservedStyleNames } from './style.js';
|
|
6
|
+
/**
|
|
7
|
+
* One plugin's theme, read as the palette core resolves semantic styles through. Each fault marks
|
|
8
|
+
* the theme, or the one entry at fault, in `plugin(identity, { theme })`.
|
|
9
|
+
*/
|
|
4
10
|
function buildTheme(value, identity) {
|
|
11
|
+
const subject = `Plugin ${quoted(identity)}`;
|
|
12
|
+
const site = slotSite({ call: 'plugin', named: identity, subject }, 'theme', value);
|
|
5
13
|
if (!isPlainObject(value)) {
|
|
6
|
-
throw new DeclarationError(
|
|
14
|
+
throw new DeclarationError(themeMapping, {
|
|
15
|
+
correction: 'Supply a mapping of names to concrete style chains.',
|
|
16
|
+
findings: [partFinding(site, [])],
|
|
17
|
+
sentence: `${subject} declares a theme that is not a mapping.`,
|
|
18
|
+
});
|
|
7
19
|
}
|
|
8
20
|
const palette = new Map();
|
|
9
21
|
for (const [name, chain] of Object.entries(value)) {
|
|
10
22
|
if (reservedStyleNames.has(name)) {
|
|
11
|
-
throw new DeclarationError(
|
|
23
|
+
throw new DeclarationError(themeNameTaken, {
|
|
24
|
+
correction: 'Rename the theme entry.',
|
|
25
|
+
findings: [partFinding(site, [name])],
|
|
26
|
+
sentence: `${subject} theme name ${quoted(name)} shadows a built-in style member.`,
|
|
27
|
+
});
|
|
12
28
|
}
|
|
13
29
|
const operations = typeof chain === 'function' ? chains.get(chain) : undefined;
|
|
14
30
|
if (chain !== undefined &&
|
|
15
31
|
(operations === undefined || operations.some((entry) => entry[0] === 'token'))) {
|
|
16
|
-
throw new DeclarationError(
|
|
32
|
+
throw new DeclarationError(themeMapping, {
|
|
33
|
+
correction: 'Map the name to a concrete chain such as style.cyan.bold, without calling it or naming a semantic style.',
|
|
34
|
+
findings: [partFinding(site, [name])],
|
|
35
|
+
sentence: `${subject} theme mapping ${quoted(name)} is not an unapplied concrete style chain without semantic tokens.`,
|
|
36
|
+
});
|
|
17
37
|
}
|
|
18
38
|
palette.set(name, operations ?? []);
|
|
19
39
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether one value is a promise or another thenable. A thenable object and a promise from another
|
|
3
|
+
* realm are as unwaitable to a synchronous caller as a native one, so the test is the contract and
|
|
4
|
+
* not the class, and a function with a callable `then` counts, as Promises/A+ says. A value whose
|
|
5
|
+
* `then` cannot be read, such as a proxy whose trap throws, is not a thenable, so the test itself
|
|
6
|
+
* never throws.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Attaches a rejection handler to a thenable a synchronous function returned, and otherwise
|
|
11
|
+
* ignores it. An unobserved rejection would end the process before the run could report anything.
|
|
12
|
+
* The thenable is adopted inside a fresh promise, so a `then` that throws rejects that promise
|
|
13
|
+
* instead of throwing here.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ignoreRejection(value: PromiseLike<unknown>): void;
|
package/dist/thenable.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether one value is a promise or another thenable. A thenable object and a promise from another
|
|
3
|
+
* realm are as unwaitable to a synchronous caller as a native one, so the test is the contract and
|
|
4
|
+
* not the class, and a function with a callable `then` counts, as Promises/A+ says. A value whose
|
|
5
|
+
* `then` cannot be read, such as a proxy whose trap throws, is not a thenable, so the test itself
|
|
6
|
+
* never throws.
|
|
7
|
+
*/
|
|
8
|
+
export function isThenable(value) {
|
|
9
|
+
try {
|
|
10
|
+
return ((typeof value === 'object' || typeof value === 'function') &&
|
|
11
|
+
value !== null &&
|
|
12
|
+
'then' in value &&
|
|
13
|
+
typeof value.then === 'function');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Attaches a rejection handler to a thenable a synchronous function returned, and otherwise
|
|
21
|
+
* ignores it. An unobserved rejection would end the process before the run could report anything.
|
|
22
|
+
* The thenable is adopted inside a fresh promise, so a `then` that throws rejects that promise
|
|
23
|
+
* instead of throwing here.
|
|
24
|
+
*/
|
|
25
|
+
export function ignoreRejection(value) {
|
|
26
|
+
void new Promise((resolve) => {
|
|
27
|
+
resolve(value);
|
|
28
|
+
}).catch(() => undefined);
|
|
29
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { LoomError } from './errors.js';
|
|
2
|
+
import type { FactSite } from './facts.js';
|
|
3
|
+
/** Phantom key. It brands a translation and holds no runtime value. */
|
|
4
|
+
declare const translation: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* A class a translation is keyed on, such as `SyntaxError`. Its instance type is the type the
|
|
7
|
+
* translator receives, so the function needs no narrowing of its own.
|
|
8
|
+
*/
|
|
9
|
+
type ErrorClass<Thrown extends object> = abstract new (...args: never[]) => Thrown;
|
|
10
|
+
/**
|
|
11
|
+
* A function that turns one foreign throw into one of the author's failures, or answers
|
|
12
|
+
* `undefined` to pass the throw on to the next translator.
|
|
13
|
+
*/
|
|
14
|
+
type Translator<Thrown extends object> = (error: Thrown) => LoomError | undefined;
|
|
15
|
+
/** One translation as core reads it back: the key's prototype, its name, and the typed call. */
|
|
16
|
+
interface TranslationRecord {
|
|
17
|
+
/** The object a thrown value's prototype chain holds when the key's class constructed it. */
|
|
18
|
+
prototype: object;
|
|
19
|
+
/** The key's name, which a broken translator's diagnostic reports. */
|
|
20
|
+
name: string;
|
|
21
|
+
/**
|
|
22
|
+
* The translator, typed at `translate()` against its key. It answers `undefined` without calling
|
|
23
|
+
* the translator for a value its key does not claim as an instance.
|
|
24
|
+
*/
|
|
25
|
+
offer: (thrown: object) => unknown;
|
|
26
|
+
}
|
|
27
|
+
/** The runtime value `translate()` returns. Its record lives in the registry above. */
|
|
28
|
+
declare class TranslationDeclaration {
|
|
29
|
+
readonly [translation]: true;
|
|
30
|
+
constructor(record: TranslationRecord);
|
|
31
|
+
}
|
|
32
|
+
/** An opaque translation pairing one error class with the translator that answers it. */
|
|
33
|
+
type Translation = Pick<TranslationDeclaration, typeof translation>;
|
|
34
|
+
/**
|
|
35
|
+
* Pairs an error class with the translator that turns its instances into a failure. The
|
|
36
|
+
* translator receives the thrown instance typed from the class, and a `translators` list on the
|
|
37
|
+
* Application or a plugin registers the pair.
|
|
38
|
+
*/
|
|
39
|
+
declare function translate<Thrown extends object>(key: ErrorClass<Thrown>, translator: Translator<Thrown>): Translation;
|
|
40
|
+
/**
|
|
41
|
+
* One contributor's translations: how a diagnostic names it, and its records by key prototype,
|
|
42
|
+
* each list in the order the contributor listed it.
|
|
43
|
+
*/
|
|
44
|
+
interface TranslationContributor {
|
|
45
|
+
/** The contributor as a diagnostic's subject, such as `the Application` or `plugin "@acme/http"`. */
|
|
46
|
+
subject: string;
|
|
47
|
+
byPrototype: ReadonlyMap<object, readonly TranslationRecord[]>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every contributor in resolution order: the application's translations, then each installed
|
|
51
|
+
* plugin's in installation order.
|
|
52
|
+
*/
|
|
53
|
+
type TranslatorRegistry = readonly TranslationContributor[];
|
|
54
|
+
/**
|
|
55
|
+
* One contributor's `translators` list. `site` names the contributor at the start of a sentence,
|
|
56
|
+
* `The Application` or `Plugin "@acme/http"`, and holds the call that declared the list, which a
|
|
57
|
+
* fault marks. The slot is read defensively, because a JavaScript author reaches it with any value.
|
|
58
|
+
*/
|
|
59
|
+
declare function readTranslations(site: FactSite, declared: unknown): TranslationContributor;
|
|
60
|
+
/**
|
|
61
|
+
* The failure the translators answer for one foreign throw, or `undefined` when the throw is never
|
|
62
|
+
* offered or every translator passed. Resolution follows the override walk: each contributor in
|
|
63
|
+
* order, the thrown value's chain walked in full at each, most derived first, and within one
|
|
64
|
+
* class the list order. A broken translator ends the walk with its defect, so no later translator
|
|
65
|
+
* hides it.
|
|
66
|
+
*/
|
|
67
|
+
declare function translateThrow(registry: TranslatorRegistry, thrown: unknown): LoomError | undefined;
|
|
68
|
+
export type { ErrorClass, TranslationContributor, Translation, Translator, TranslatorRegistry };
|
|
69
|
+
export { readTranslations, translate, translateThrow };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { spelled } from './diagnostic-text.js';
|
|
2
|
+
import { asSentence, DeclarationError, InternalError, LoomError, quoted, reasonOf, } from './errors.js';
|
|
3
|
+
import { partFinding } from './facts.js';
|
|
4
|
+
import { foreignValue, notAFunction, notAList, translationKey } from './plugin-rules.js';
|
|
5
|
+
import { prototypeChain } from './prototypes.js';
|
|
6
|
+
import { brokenTranslator, brokenTranslatorCorrection } from './rules.js';
|
|
7
|
+
import { ignoreRejection, isThenable } from './thenable.js';
|
|
8
|
+
/** Authored translations register here, so the public type publishes nothing to reach. */
|
|
9
|
+
const records = new WeakMap();
|
|
10
|
+
/** The runtime value `translate()` returns. Its record lives in the registry above. */
|
|
11
|
+
class TranslationDeclaration {
|
|
12
|
+
constructor(record) {
|
|
13
|
+
records.set(this, record);
|
|
14
|
+
Object.freeze(this);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The prototype a class key carries, or `undefined` for a value that is not a class. A class is a
|
|
19
|
+
* function whose `prototype` is the object its instances' chains hold, so an arrow function, a
|
|
20
|
+
* bound function, and every non-function are no key at all. A key whose `prototype` cannot be
|
|
21
|
+
* read, such as a proxy whose trap throws, is no key either.
|
|
22
|
+
*/
|
|
23
|
+
function keyPrototype(key) {
|
|
24
|
+
let prototype = undefined;
|
|
25
|
+
try {
|
|
26
|
+
if (typeof key !== 'function' || !('prototype' in key)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
prototype = key.prototype;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return typeof prototype === 'object' && prototype !== null ? prototype : undefined;
|
|
35
|
+
}
|
|
36
|
+
/** A key's own name, or `undefined` when it has none that can be read. */
|
|
37
|
+
function nameOf(key) {
|
|
38
|
+
let name = undefined;
|
|
39
|
+
try {
|
|
40
|
+
name = 'name' in key ? key.name : undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
name = undefined;
|
|
44
|
+
}
|
|
45
|
+
return typeof name === 'string' && name !== '' ? name : undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* How a diagnostic names one key class: its name, quoted and escaped, or what it is when it has no
|
|
49
|
+
* name that can be read.
|
|
50
|
+
*/
|
|
51
|
+
function keyName(key) {
|
|
52
|
+
const name = nameOf(key);
|
|
53
|
+
return name === undefined ? 'an anonymous class' : quoted(name);
|
|
54
|
+
}
|
|
55
|
+
/** A name JavaScript source can spell as an identifier, which a finding prints a class key as. */
|
|
56
|
+
const identifier = /^[A-Za-z_$][\w$]*$/u;
|
|
57
|
+
/**
|
|
58
|
+
* The finding for one `translate()` call, marking the argument at `mark`. A class key prints as
|
|
59
|
+
* its name, as its author wrote it, where any other function prints as an ellipsis.
|
|
60
|
+
*/
|
|
61
|
+
function translateFinding(key, translator, mark) {
|
|
62
|
+
const name = typeof key === 'function' ? nameOf(key) : undefined;
|
|
63
|
+
const code = name !== undefined && identifier.test(name) ? spelled(name) : key;
|
|
64
|
+
return { arguments: [code, translator], call: 'translate', mark };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether a key claims a thrown value as its instance. A value whose chain cannot be read is not
|
|
68
|
+
* claimed, so its translator is never called and is not blamed for the failed read.
|
|
69
|
+
*/
|
|
70
|
+
function claims(thrown, key) {
|
|
71
|
+
try {
|
|
72
|
+
return thrown instanceof key;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pairs an error class with the translator that turns its instances into a failure. The
|
|
80
|
+
* translator receives the thrown instance typed from the class, and a `translators` list on the
|
|
81
|
+
* Application or a plugin registers the pair.
|
|
82
|
+
*/
|
|
83
|
+
function translate(key, translator) {
|
|
84
|
+
const prototype = keyPrototype(key);
|
|
85
|
+
// A key whose prototype's chain cannot be read, such as through a proxy whose trap throws, is no
|
|
86
|
+
// Key either, so it is reported as a non-class before any failure-class check.
|
|
87
|
+
const chain = prototype === undefined ? undefined : prototypeChain(prototype);
|
|
88
|
+
if (prototype === undefined || chain === undefined) {
|
|
89
|
+
throw new DeclarationError(translationKey, {
|
|
90
|
+
correction: 'Supply an error class, such as SyntaxError.',
|
|
91
|
+
findings: [translateFinding(key, translator, '0')],
|
|
92
|
+
sentence: 'translate() received a key that is not a class.',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Core never offers a failure to translators, so a translation keyed on a failure class never runs.
|
|
96
|
+
if (prototype === LoomError.prototype || chain.includes(LoomError.prototype)) {
|
|
97
|
+
throw new DeclarationError(translationKey, {
|
|
98
|
+
correction: 'Key the translation on the foreign class it replaces.',
|
|
99
|
+
findings: [translateFinding(key, translator, '0')],
|
|
100
|
+
sentence: 'translate() received a failure class as its key.',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (typeof translator !== 'function') {
|
|
104
|
+
throw new DeclarationError(notAFunction, {
|
|
105
|
+
correction: 'Supply a function that returns a failure or undefined.',
|
|
106
|
+
findings: [translateFinding(key, translator, '1')],
|
|
107
|
+
sentence: 'translate() received a translator that is not a function.',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return new TranslationDeclaration({
|
|
111
|
+
name: keyName(key),
|
|
112
|
+
offer: (thrown) => (claims(thrown, key) ? translator(thrown) : undefined),
|
|
113
|
+
prototype,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* One contributor's `translators` list. `site` names the contributor at the start of a sentence,
|
|
118
|
+
* `The Application` or `Plugin "@acme/http"`, and holds the call that declared the list, which a
|
|
119
|
+
* fault marks. The slot is read defensively, because a JavaScript author reaches it with any value.
|
|
120
|
+
*/
|
|
121
|
+
function readTranslations(site, declared) {
|
|
122
|
+
const sentence = site.subject;
|
|
123
|
+
const byPrototype = new Map();
|
|
124
|
+
for (const record of translationRecords(site, declared)) {
|
|
125
|
+
const listed = byPrototype.get(record.prototype) ?? [];
|
|
126
|
+
listed.push(record);
|
|
127
|
+
byPrototype.set(record.prototype, listed);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
byPrototype,
|
|
131
|
+
subject: `${sentence.slice(0, 1).toLowerCase()}${sentence.slice(1)}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** The fix a `translators` slot's list fault and entry fault share. */
|
|
135
|
+
const translationSupply = 'translate(ErrorClass, translator)';
|
|
136
|
+
/** The record behind each entry of one `translators` slot, whose every other value is its fault. */
|
|
137
|
+
function translationRecords(site, declared) {
|
|
138
|
+
if (declared !== undefined && !Array.isArray(declared)) {
|
|
139
|
+
throw new DeclarationError(notAList, {
|
|
140
|
+
correction: `Supply a list of values returned by ${translationSupply}.`,
|
|
141
|
+
findings: [partFinding(site, [])],
|
|
142
|
+
sentence: `${site.subject} declares translators that are not an array.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const list = declared ?? [];
|
|
146
|
+
// Array.from visits a hole as undefined, where map would skip it and leave the hole in place.
|
|
147
|
+
return Array.from(list, (entry, index) => {
|
|
148
|
+
const record = typeof entry === 'object' && entry !== null ? records.get(entry) : undefined;
|
|
149
|
+
if (!record) {
|
|
150
|
+
throw new DeclarationError(foreignValue, {
|
|
151
|
+
correction: `Supply the value returned by ${translationSupply}.`,
|
|
152
|
+
findings: [partFinding(site, [index])],
|
|
153
|
+
sentence: `${site.subject} holds a translator entry that is not a translation.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return record;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Every prototype in one thrown value's chain, most derived first, or `undefined` when the value is
|
|
161
|
+
* never offered: a value whose chain holds a failure class, and a value whose chain cannot be read,
|
|
162
|
+
* such as a proxy whose trap throws, or whose chain repeats a link or never ends. A value with a
|
|
163
|
+
* `null` prototype has an empty chain, which no key matches.
|
|
164
|
+
*/
|
|
165
|
+
function offeredChain(thrown) {
|
|
166
|
+
const chain = prototypeChain(thrown);
|
|
167
|
+
return chain?.includes(LoomError.prototype) ? undefined : chain;
|
|
168
|
+
}
|
|
169
|
+
/** How a broken translator's diagnostic names a value that is not a failure. */
|
|
170
|
+
function returnedKind(value) {
|
|
171
|
+
if (value === null) {
|
|
172
|
+
return 'null';
|
|
173
|
+
}
|
|
174
|
+
if (isThenable(value)) {
|
|
175
|
+
return 'a promise';
|
|
176
|
+
}
|
|
177
|
+
const kind = typeof value;
|
|
178
|
+
return /^[aeiou]/u.test(kind) ? `an ${kind}` : `a ${kind}`;
|
|
179
|
+
}
|
|
180
|
+
/** Whether a translator's answer is a failure, read without letting a hostile value throw. */
|
|
181
|
+
function isFailure(value) {
|
|
182
|
+
try {
|
|
183
|
+
return value instanceof LoomError;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** The defect of a broken translator under its rule, with the sentence and cause each shape gives. */
|
|
190
|
+
function brokenDefect(sentence, cause) {
|
|
191
|
+
return new InternalError(brokenTranslator, {
|
|
192
|
+
cause,
|
|
193
|
+
correction: brokenTranslatorCorrection,
|
|
194
|
+
sentence,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The defect of a translator that threw. Its cause is a new `AggregateError` that holds the
|
|
199
|
+
* translator's throw and then the original throw, so neither is lost and neither is mutated. The
|
|
200
|
+
* reason is read through `reasonOf`, which escapes it onto one line.
|
|
201
|
+
*/
|
|
202
|
+
function threwDefect(who, error, thrown) {
|
|
203
|
+
const reason = reasonOf(error);
|
|
204
|
+
const cause = new AggregateError([error, thrown], 'The translator threw while translating the original throw.');
|
|
205
|
+
return brokenDefect(`${who} threw: ${asSentence(reason)}`, cause);
|
|
206
|
+
}
|
|
207
|
+
/** The one translator call, whose throw or non-failure answer is the defect of that translator. */
|
|
208
|
+
function consult(contributor, record, thrown) {
|
|
209
|
+
const who = `The translator ${contributor.subject} registered for ${record.name}`;
|
|
210
|
+
let answer = undefined;
|
|
211
|
+
try {
|
|
212
|
+
answer = record.offer(thrown);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return threwDefect(who, error, thrown);
|
|
216
|
+
}
|
|
217
|
+
if (answer === undefined || isFailure(answer)) {
|
|
218
|
+
return answer;
|
|
219
|
+
}
|
|
220
|
+
// A translator is synchronous, so a returned promise is ignored once its rejection is observed.
|
|
221
|
+
if (isThenable(answer)) {
|
|
222
|
+
ignoreRejection(answer);
|
|
223
|
+
}
|
|
224
|
+
return brokenDefect(`${who} returned ${returnedKind(answer)} instead of a failure.`, thrown);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The failure the translators answer for one foreign throw, or `undefined` when the throw is never
|
|
228
|
+
* offered or every translator passed. Resolution follows the override walk: each contributor in
|
|
229
|
+
* order, the thrown value's chain walked in full at each, most derived first, and within one
|
|
230
|
+
* class the list order. A broken translator ends the walk with its defect, so no later translator
|
|
231
|
+
* hides it.
|
|
232
|
+
*/
|
|
233
|
+
function translateThrow(registry, thrown) {
|
|
234
|
+
if ((typeof thrown !== 'object' && typeof thrown !== 'function') || thrown === null) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
// With no translation registered, the chain is never read.
|
|
238
|
+
const chain = registry.some((contributor) => contributor.byPrototype.size > 0)
|
|
239
|
+
? (offeredChain(thrown) ?? [])
|
|
240
|
+
: [];
|
|
241
|
+
for (const contributor of registry) {
|
|
242
|
+
for (const prototype of chain) {
|
|
243
|
+
for (const record of contributor.byPrototype.get(prototype) ?? []) {
|
|
244
|
+
const answer = consult(contributor, record, thrown);
|
|
245
|
+
if (answer !== undefined) {
|
|
246
|
+
return answer;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
export { readTranslations, translate, translateThrow };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" preserve="true" />
|
|
2
2
|
import type { Readable, Writable } from 'node:stream';
|
|
3
3
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
|
+
import type { FailureExitCode } from './exit-codes.js';
|
|
4
5
|
import type { ExtensionValue } from './extension.js';
|
|
5
|
-
import type { ResultNode } from './inspect.js';
|
|
6
|
+
import type { CommandGraph, CommandNode, ResultNode } from './inspect.js';
|
|
6
7
|
import type { RenderingPolicy } from './rendering.js';
|
|
7
8
|
import type { ContextualStyle } from './style.js';
|
|
8
9
|
type LowercaseLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
|
|
@@ -21,12 +22,23 @@ type Presence = {
|
|
|
21
22
|
required?: false;
|
|
22
23
|
default?: unknown;
|
|
23
24
|
};
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The literal member keeps `multiple: true` exact under contextual typing, as `Presence` does.
|
|
27
|
+
* A multiple option takes its list from the configuration source, so it binds no variable.
|
|
28
|
+
*/
|
|
25
29
|
type Multiplicity = {
|
|
26
30
|
multiple: true;
|
|
27
|
-
|
|
31
|
+
env?: never;
|
|
32
|
+
} | ({
|
|
28
33
|
multiple?: false;
|
|
29
|
-
};
|
|
34
|
+
} & EnvBinding);
|
|
35
|
+
/**
|
|
36
|
+
* The environment binding: the variable the input-source stage fills the option from when argv
|
|
37
|
+
* supplies none. The declaration names it explicitly, and core derives no name.
|
|
38
|
+
*/
|
|
39
|
+
interface EnvBinding {
|
|
40
|
+
env?: string;
|
|
41
|
+
}
|
|
30
42
|
/**
|
|
31
43
|
* `validateOmitted: true` sends an omitted optional scalar to its own schema. The literal member
|
|
32
44
|
* keeps the flag exact under contextual typing, as `Multiplicity` does.
|
|
@@ -62,15 +74,22 @@ interface ArgumentExtensions {
|
|
|
62
74
|
extensions?: readonly ExtensionValue<'argument'>[];
|
|
63
75
|
}
|
|
64
76
|
/**
|
|
65
|
-
* The tokens one declaration collects before validation: one string, or
|
|
66
|
-
*
|
|
77
|
+
* The tokens one declaration collects before validation: one string, or several. A multiple option
|
|
78
|
+
* and a variadic argument collect alike, so they share this raw shape.
|
|
67
79
|
*/
|
|
68
80
|
type RawValue<Config> = Config extends {
|
|
69
81
|
multiple: true;
|
|
70
82
|
} | {
|
|
71
83
|
variadic: true;
|
|
72
84
|
} ? string[] : string;
|
|
73
|
-
|
|
85
|
+
/** A multiple option and a variadic argument pass each value through the same validator. */
|
|
86
|
+
type PerValue<Config, Value> = Config extends {
|
|
87
|
+
multiple: true;
|
|
88
|
+
} | {
|
|
89
|
+
variadic: true;
|
|
90
|
+
} ? Value[] : Value;
|
|
91
|
+
/** The validated value: the validator's output for each value, or the raw shape without one. */
|
|
92
|
+
type SchemaOutput<Config, Schema, Raw> = Schema extends StandardSchemaV1 ? PerValue<Config, StandardSchemaV1.InferOutput<Schema>> : Raw;
|
|
74
93
|
type SchemaInput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema> : string;
|
|
75
94
|
/** The named key states the rule, so a rejected declaration name reads as its own diagnostic. */
|
|
76
95
|
interface LiteralNameFault {
|
|
@@ -83,7 +102,8 @@ export type GlobalNameConstraint<Name extends string, Globals> = Name extends ke
|
|
|
83
102
|
} : unknown;
|
|
84
103
|
/** A required record key excludes open strings; distribution rejects each union member. */
|
|
85
104
|
export type NameConstraint<Name extends string, Whole extends string = Name> = {} extends Record<Name, unknown> ? LiteralNameFault : Name extends Whole ? [Whole] extends [Name] ? unknown : LiteralNameFault : LiteralNameFault;
|
|
86
|
-
|
|
105
|
+
/** The status `run()` resolves: success, a failure class's code, or a cancellation code. */
|
|
106
|
+
export type ExitCode = 0 | FailureExitCode | 130 | 143;
|
|
87
107
|
export interface InputTerminal {
|
|
88
108
|
isTTY: boolean;
|
|
89
109
|
}
|
|
@@ -104,6 +124,14 @@ export interface Host {
|
|
|
104
124
|
stdin: Readable;
|
|
105
125
|
stdout: Writable;
|
|
106
126
|
stderr: Writable;
|
|
127
|
+
/**
|
|
128
|
+
* Reads one source file for a defect's Developer Diagnostic, or answers `undefined`. Core calls
|
|
129
|
+
* it only in a development build, only while it reports a defect, and only for a path under the
|
|
130
|
+
* working directory. Process capture supplies a reader that refuses a file outside `cwd` after
|
|
131
|
+
* resolving symbolic links, and answers `undefined` for a path that is not a regular file or
|
|
132
|
+
* that exceeds 1 MiB.
|
|
133
|
+
*/
|
|
134
|
+
readSource?: (path: string, cwd: string) => string | undefined;
|
|
107
135
|
}
|
|
108
136
|
export interface RunOptions {
|
|
109
137
|
rendering?: RenderingPolicy;
|
|
@@ -324,19 +352,24 @@ export type ScalarArgument = Presence & Omission & Described & ArgumentExtension
|
|
|
324
352
|
validate?: StandardSchemaV1;
|
|
325
353
|
};
|
|
326
354
|
export type ArgumentConfig = VariadicArgument | ScalarArgument;
|
|
327
|
-
export type ValidatedValue<Config, Raw> = Config extends unknown ? 'validate' extends keyof Config ? SchemaOutput<Config['validate'], Raw> : Raw : never;
|
|
328
|
-
/** Keep each conditional declaration paired with its own
|
|
355
|
+
export type ValidatedValue<Config, Raw> = Config extends unknown ? 'validate' extends keyof Config ? SchemaOutput<Config, Config['validate'], Raw> : Raw : never;
|
|
356
|
+
/** Keep each conditional declaration paired with its own validator input type. */
|
|
329
357
|
export type DefaultConstraint<Config> = Config extends unknown ? Config & {
|
|
330
|
-
default?: 'validate' extends keyof Config ? SchemaInput<Config['validate']
|
|
358
|
+
default?: 'validate' extends keyof Config ? PerValue<Config, SchemaInput<Config['validate']>> : RawValue<Config>;
|
|
331
359
|
} : never;
|
|
332
360
|
/**
|
|
333
|
-
* A multiple option
|
|
334
|
-
* `string
|
|
361
|
+
* A multiple option or a variadic argument passes each value to its validator alone, so the
|
|
362
|
+
* declared validator must accept one `string`. The key names the fault, the way the other
|
|
363
|
+
* declaration constraints do.
|
|
335
364
|
*/
|
|
336
|
-
export type
|
|
365
|
+
export type PerValueConstraint<Config> = Config extends {
|
|
337
366
|
multiple: true;
|
|
338
|
-
}
|
|
339
|
-
|
|
367
|
+
} | {
|
|
368
|
+
variadic: true;
|
|
369
|
+
} ? 'validate' extends keyof Config ? [
|
|
370
|
+
SchemaInput<Config['validate']>
|
|
371
|
+
] extends [never] ? unknown : string extends SchemaInput<Config['validate']> ? unknown : {
|
|
372
|
+
'A validator of several values must accept one string input': Config['validate'];
|
|
340
373
|
} : unknown : unknown;
|
|
341
374
|
/**
|
|
342
375
|
* The declaration rules `validateOmitted: true` needs: a schema that accepts `undefined`, an
|
|
@@ -362,12 +395,28 @@ export type ValidateOmittedConstraint<Config> = Config extends {
|
|
|
362
395
|
} | {
|
|
363
396
|
variadic: true;
|
|
364
397
|
} ? {
|
|
365
|
-
'
|
|
398
|
+
'An input of several values receives no values as an empty array': never;
|
|
366
399
|
} : 'validate' extends keyof Config ? undefined extends SchemaInput<Config['validate']> ? unknown : {
|
|
367
|
-
'A validateOmitted
|
|
400
|
+
'A validateOmitted validator must accept an undefined input': Config['validate'];
|
|
368
401
|
} : {
|
|
369
|
-
'validateOmitted needs a
|
|
402
|
+
'validateOmitted needs a validator to receive the omission': never;
|
|
370
403
|
} : unknown;
|
|
404
|
+
/**
|
|
405
|
+
* A global option declares no presence rule, so its omission is always plain absence. A union
|
|
406
|
+
* config fails when any member declares the key, and a wide `OptionConfig` passes, because its
|
|
407
|
+
* members only allow the key.
|
|
408
|
+
*/
|
|
409
|
+
export type GlobalOmissionConstraint<Config> = [Extract<Config, {
|
|
410
|
+
required: unknown;
|
|
411
|
+
}>] extends [
|
|
412
|
+
never
|
|
413
|
+
] ? [Extract<Config, {
|
|
414
|
+
validateOmitted: unknown;
|
|
415
|
+
}>] extends [never] ? unknown : {
|
|
416
|
+
'A global option declares no validateOmitted; its omission is plain absence': never;
|
|
417
|
+
} : {
|
|
418
|
+
'A global option declares no required; the Commands that read it check for it': never;
|
|
419
|
+
};
|
|
371
420
|
export type ArgumentValue<Config extends ArgumentConfig> = Config extends {
|
|
372
421
|
variadic: true;
|
|
373
422
|
} ? ValidatedValue<Config, string[]> : ValidatedValue<Config, string> | (Config extends {
|
|
@@ -377,7 +426,7 @@ export type ArgumentValue<Config extends ArgumentConfig> = Config extends {
|
|
|
377
426
|
} | {
|
|
378
427
|
validateOmitted: true;
|
|
379
428
|
} ? never : undefined);
|
|
380
|
-
export type BooleanOption = (OptionSpelling & Described & Listed & OptionExtensions & {
|
|
429
|
+
export type BooleanOption = (OptionSpelling & Described & Listed & EnvBinding & OptionExtensions & {
|
|
381
430
|
type: 'boolean';
|
|
382
431
|
validate?: never;
|
|
383
432
|
default?: never;
|
|
@@ -385,7 +434,7 @@ export type BooleanOption = (OptionSpelling & Described & Listed & OptionExtensi
|
|
|
385
434
|
required?: never;
|
|
386
435
|
validateOmitted?: never;
|
|
387
436
|
polarity?: 'positive' | 'negative';
|
|
388
|
-
}) | (Described & Listed & OptionExtensions & {
|
|
437
|
+
}) | (Described & Listed & EnvBinding & OptionExtensions & {
|
|
389
438
|
type: 'boolean';
|
|
390
439
|
validate?: never;
|
|
391
440
|
default?: never;
|
|
@@ -422,10 +471,16 @@ export type OptionValue<Config extends OptionConfig> = Config extends StringOpti
|
|
|
422
471
|
} | {
|
|
423
472
|
validateOmitted: true;
|
|
424
473
|
} ? never : undefined) : boolean;
|
|
474
|
+
/**
|
|
475
|
+
* What an action receives. `graph` is the frozen graph `inspect()` returns for this run, and
|
|
476
|
+
* `command` is the routed node inside it: the same two values the run's middleware receive.
|
|
477
|
+
*/
|
|
425
478
|
export interface ActionContext<Args, Options = {}, Result = unknown> {
|
|
426
479
|
readonly style: ContextualStyle;
|
|
427
480
|
args: Args;
|
|
428
481
|
options: Options;
|
|
482
|
+
readonly graph: CommandGraph;
|
|
483
|
+
readonly command: CommandNode;
|
|
429
484
|
passthrough: string[];
|
|
430
485
|
out: Out<Result>;
|
|
431
486
|
host: Host;
|