@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.
Files changed (77) hide show
  1. package/dist/application.d.ts +18 -2
  2. package/dist/application.js +266 -71
  3. package/dist/bindings.d.ts +15 -10
  4. package/dist/bindings.js +34 -15
  5. package/dist/chain.d.ts +15 -6
  6. package/dist/chain.js +40 -20
  7. package/dist/command-rules.d.ts +55 -0
  8. package/dist/command-rules.js +142 -0
  9. package/dist/command.d.ts +65 -23
  10. package/dist/command.js +695 -226
  11. package/dist/controls.d.ts +8 -0
  12. package/dist/controls.js +23 -0
  13. package/dist/defect.d.ts +18 -0
  14. package/dist/defect.js +272 -0
  15. package/dist/developer.d.ts +24 -0
  16. package/dist/developer.js +52 -0
  17. package/dist/diagnostic-text.d.ts +81 -0
  18. package/dist/diagnostic-text.js +283 -0
  19. package/dist/diagnostic.d.ts +11 -0
  20. package/dist/diagnostic.js +70 -0
  21. package/dist/errors.d.ts +108 -24
  22. package/dist/errors.js +324 -53
  23. package/dist/exit-codes.d.ts +45 -0
  24. package/dist/exit-codes.js +46 -0
  25. package/dist/extension.d.ts +41 -8
  26. package/dist/extension.js +142 -57
  27. package/dist/facts.d.ts +66 -11
  28. package/dist/facts.js +98 -22
  29. package/dist/globals.d.ts +29 -21
  30. package/dist/globals.js +115 -46
  31. package/dist/hints.d.ts +79 -0
  32. package/dist/hints.js +247 -0
  33. package/dist/host.d.ts +13 -0
  34. package/dist/host.js +43 -1
  35. package/dist/identity.d.ts +19 -0
  36. package/dist/identity.js +72 -0
  37. package/dist/index.d.ts +11 -2
  38. package/dist/index.js +5 -0
  39. package/dist/input-rules.d.ts +64 -0
  40. package/dist/input-rules.js +145 -0
  41. package/dist/inspect.d.ts +12 -4
  42. package/dist/inspect.js +88 -28
  43. package/dist/lanes.js +1 -1
  44. package/dist/locate.js +4 -4
  45. package/dist/options.d.ts +23 -2
  46. package/dist/options.js +135 -44
  47. package/dist/output.d.ts +9 -2
  48. package/dist/output.js +18 -2
  49. package/dist/plain.d.ts +6 -0
  50. package/dist/plain.js +12 -0
  51. package/dist/plugin-rules.d.ts +62 -0
  52. package/dist/plugin-rules.js +155 -0
  53. package/dist/plugin.d.ts +22 -10
  54. package/dist/plugin.js +348 -116
  55. package/dist/prototypes.d.ts +7 -0
  56. package/dist/prototypes.js +29 -0
  57. package/dist/rendering.d.ts +6 -1
  58. package/dist/rendering.js +23 -5
  59. package/dist/rules.d.ts +51 -0
  60. package/dist/rules.js +115 -0
  61. package/dist/sequence.js +6 -1
  62. package/dist/sources.d.ts +6 -4
  63. package/dist/sources.js +22 -13
  64. package/dist/style-wire.js +1 -1
  65. package/dist/style.js +1 -1
  66. package/dist/theme.d.ts +4 -0
  67. package/dist/theme.js +25 -5
  68. package/dist/thenable.d.ts +15 -0
  69. package/dist/thenable.js +29 -0
  70. package/dist/translators.d.ts +69 -0
  71. package/dist/translators.js +253 -0
  72. package/dist/types.d.ts +11 -1
  73. package/dist/validation.d.ts +44 -3
  74. package/dist/validation.js +156 -57
  75. package/dist/view.d.ts +48 -14
  76. package/dist/view.js +155 -77
  77. package/package.json +1 -1
@@ -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
+ }
@@ -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
- declare function renderingPolicy(value: unknown): RenderingPolicy;
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 { isPlainObject } from './facts.js';
3
- function renderingPolicy(value) {
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('The rendering policy must be an object.');
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(`Rendering ${field} must be auto, always, or never.`);
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('Rendering terminalControls must be strip or preserve.');
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;
@@ -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(`The result of ${routedSubject(writer.path)} is not iterable.`, undefined));
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
  }
package/dist/sources.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { InputError, InternalError } from './errors.js';
1
+ import { LoomError } from './errors.js';
2
2
  import type { ExtensionRecords } from './extension.js';
3
3
  import type { CommandGraph, OptionNode } from './inspect.js';
4
4
  import type { OptionValues } from './options.js';
@@ -25,6 +25,8 @@ interface SourceStage {
25
25
  inspected: () => CommandGraph;
26
26
  /** The routed Command's own options, or `undefined` while local parsing holds a fault. */
27
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;
28
30
  /** The channel a source writes through, whose results call names the source. */
29
31
  out: Out;
30
32
  plugins: readonly BuiltPlugin[];
@@ -38,11 +40,11 @@ interface SourceStage {
38
40
  * What the stage found beside the values it filled. `labels` names where each filled option's value
39
41
  * came from, by option name, and `rejected` names the variable of each Boolean option whose value
40
42
  * is outside the grammar. Both are internal to core's failure messages. `fault` is a configuration
41
- * source's own fault, or the `InputError` its resolver threw, which stops the stage and takes the
42
- * place of every validation problem.
43
+ * source's own fault, or the failure its resolver threw, which stops the stage and takes the place
44
+ * of every validation problem.
43
45
  */
44
46
  interface SourceOutcome {
45
- fault: InternalError | InputError | undefined;
47
+ fault: LoomError | undefined;
46
48
  labels: ReadonlyMap<string, string>;
47
49
  rejected: ReadonlyMap<string, string>;
48
50
  }
package/dist/sources.js CHANGED
@@ -1,7 +1,9 @@
1
- import { asSentence, InputError, InternalError, reasonOf, ResultError } from './errors.js';
2
- import { isPlainObject, isProseLine } from './facts.js';
1
+ import { asSentence, foreignFailure, InternalError, LoomError, reasonOf } from './errors.js';
2
+ import { isProseLine } from './facts.js';
3
3
  import { isSupplied } from './options.js';
4
+ import { isPlainObject } from './plain.js';
4
5
  import { loadDefault, pluginSentence, pluginValues } from './plugin.js';
6
+ import { foreignThrow, foreignThrowCorrection, sourceAnswers, sourceAnswersCorrection, } from './rules.js';
5
7
  /** The Boolean grammar a bound variable is read through: the whole value, case-insensitive. */
6
8
  const truths = new Map([
7
9
  ['0', false],
@@ -85,7 +87,11 @@ function rawValue(kind, value) {
85
87
  const ruleFaults = new WeakSet();
86
88
  /** A fault the answers rule names, recorded so that reading the answers rethrows it unframed. */
87
89
  function ruleFault(message) {
88
- const fault = new InternalError(message, undefined);
90
+ const fault = new InternalError(sourceAnswers, {
91
+ cause: undefined,
92
+ correction: sourceAnswersCorrection,
93
+ sentence: message,
94
+ });
89
95
  ruleFaults.add(fault);
90
96
  return fault;
91
97
  }
@@ -130,7 +136,11 @@ function isResolverExport(value) {
130
136
  }
131
137
  /** The fault of a source that threw, while it ran or while core read what it returned. */
132
138
  function sourceFailure(sentence, error) {
133
- return new InternalError(`${sentence} failed in its configuration source: ${asSentence(reasonOf(error))}`, error);
139
+ return new InternalError(foreignThrow, {
140
+ cause: error,
141
+ correction: foreignThrowCorrection,
142
+ sentence: `${sentence} failed in its configuration source: ${asSentence(reasonOf(error))}`,
143
+ });
134
144
  }
135
145
  /**
136
146
  * Asks the one configuration source about every requested option, and answers with what it said.
@@ -162,13 +172,14 @@ async function askSource(stage, call) {
162
172
  answers = await resolver(context);
163
173
  }
164
174
  catch (error) {
165
- // The resolver's own InputError is a usage failure.
166
- // Its out.results() call is the results fault that names it.
167
- // Every other throw is the plugin's fault.
168
- if (error instanceof InputError || (error instanceof ResultError && error.kind === 'source')) {
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) {
169
180
  throw error;
170
181
  }
171
- throw sourceFailure(sentence, error);
182
+ throw stage.offer(error) ?? sourceFailure(sentence, error);
172
183
  }
173
184
  try {
174
185
  return readAnswers(sentence, answers, requested);
@@ -238,11 +249,9 @@ async function fillInputs(stage) {
238
249
  return { fault: undefined, labels, rejected };
239
250
  }
240
251
  catch (error) {
241
- // The resolver's InputError reports as a usage failure.
252
+ // The resolver's own failure keeps its class, and so its code.
242
253
  // Every other fault above is raised as an internal error, and anything else is wrapped the same way.
243
- const fault = error instanceof InternalError || error instanceof InputError
244
- ? error
245
- : new InternalError(reasonOf(error), error);
254
+ const fault = error instanceof LoomError ? error : foreignFailure(error);
246
255
  return { fault, labels, rejected };
247
256
  }
248
257
  }
@@ -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
@@ -1,4 +1,4 @@
1
- import { isPlainObject } from './facts.js';
1
+ import { isPlainObject } from './plain.js';
2
2
  /** Concrete terminal foregrounds; backgrounds use the same palette indices. */
3
3
  const colors = {
4
4
  black: 0,
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 };
package/dist/theme.js CHANGED
@@ -1,19 +1,39 @@
1
- import { DeclarationError } from './errors.js';
2
- import { isPlainObject } from './facts.js';
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(`Plugin "${identity}" must declare theme as a mapping of names to concrete style chains.`);
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(`Plugin "${identity}" theme name "${name}" shadows a built-in style member.`);
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(`Plugin "${identity}" theme mapping "${name}" must be an unapplied concrete style chain without semantic tokens.`);
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;
@@ -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 };