@loomcli/core 0.2.0 → 0.3.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 (51) hide show
  1. package/dist/application.d.ts +59 -34
  2. package/dist/application.js +162 -59
  3. package/dist/chain.d.ts +25 -6
  4. package/dist/chain.js +99 -15
  5. package/dist/command.d.ts +146 -42
  6. package/dist/command.js +620 -78
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -59
  10. package/dist/errors.js +49 -106
  11. package/dist/extension.d.ts +5 -1
  12. package/dist/extension.js +18 -1
  13. package/dist/globals.d.ts +14 -25
  14. package/dist/globals.js +10 -61
  15. package/dist/glyphs.generated.d.ts +464 -0
  16. package/dist/glyphs.generated.js +491 -0
  17. package/dist/host.js +2 -1
  18. package/dist/index.d.ts +14 -5
  19. package/dist/index.js +5 -2
  20. package/dist/inspect.d.ts +26 -3
  21. package/dist/inspect.js +19 -5
  22. package/dist/lanes.d.ts +26 -0
  23. package/dist/lanes.js +45 -0
  24. package/dist/output.d.ts +93 -15
  25. package/dist/output.js +307 -34
  26. package/dist/plugin.d.ts +32 -16
  27. package/dist/plugin.js +46 -18
  28. package/dist/rendering.d.ts +21 -0
  29. package/dist/rendering.js +72 -0
  30. package/dist/sequence.d.ts +41 -0
  31. package/dist/sequence.js +225 -0
  32. package/dist/style-ansi.d.ts +13 -0
  33. package/dist/style-ansi.js +306 -0
  34. package/dist/style-layout.d.ts +29 -0
  35. package/dist/style-layout.js +228 -0
  36. package/dist/style-resolve.d.ts +6 -0
  37. package/dist/style-resolve.js +26 -0
  38. package/dist/style-state.d.ts +14 -0
  39. package/dist/style-state.js +179 -0
  40. package/dist/style-wire.d.ts +31 -0
  41. package/dist/style-wire.js +201 -0
  42. package/dist/style.d.ts +86 -0
  43. package/dist/style.js +201 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +22 -0
  46. package/dist/types.d.ts +169 -21
  47. package/dist/validation.d.ts +8 -1
  48. package/dist/validation.js +17 -2
  49. package/dist/view.d.ts +180 -0
  50. package/dist/view.js +307 -0
  51. package/package.json +2 -1
@@ -0,0 +1,22 @@
1
+ import type { Plugin } from './plugin.js';
2
+ /** The shallow type information an Application supplies before its Command graph is composed. */
3
+ interface ApplicationEnvironment<Globals = {}, Plugins extends readonly Plugin[] = readonly []> {
4
+ readonly globals: Globals;
5
+ readonly plugins: Plugins;
6
+ }
7
+ interface RegistrationShape {
8
+ environment?: ApplicationEnvironment<unknown, readonly Plugin[]>;
9
+ }
10
+ /** The Application augments this interface once in its own TypeScript compilation context. */
11
+ interface Register extends RegistrationShape {
12
+ }
13
+ type RegisteredEnvironment = Register extends {
14
+ environment: infer Environment;
15
+ } ? Environment extends ApplicationEnvironment<unknown, readonly Plugin[]> ? Environment : never : ApplicationEnvironment;
16
+ type RegisteredGlobals = RegisteredEnvironment['globals'];
17
+ /** A private type marker avoids pretending parsed global values exist before an invocation. */
18
+ declare const applicationEnvironment: unique symbol;
19
+ type EnvironmentOf<Value extends {
20
+ readonly [applicationEnvironment]: ApplicationEnvironment<unknown, readonly Plugin[]>;
21
+ }> = Value[typeof applicationEnvironment];
22
+ export type { ApplicationEnvironment, EnvironmentOf, Register, RegisteredEnvironment, RegisteredGlobals, applicationEnvironment, };
@@ -0,0 +1 @@
1
+ export {};
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
- import type { InputIdentity, Renderer } from './types.js';
2
+ import type { InputIdentity } from './types.js';
3
3
  /**
4
4
  * The two short-group faults. A value option that is not last in its group names that option's
5
5
  * spelling; a group that mixes scopes names the whole group and the two letters that disagree.
@@ -14,28 +14,25 @@ type ShortGroupFault = {
14
14
  global: string;
15
15
  other: string;
16
16
  };
17
- /** One registration: the class it names, read as the prototype and the name that class holds. */
18
- interface Registration {
19
- name: string;
20
- prototype: unknown;
21
- render: (failure: LoomError) => unknown;
22
- }
23
- /** Phantom key. It marks a failure registration and holds no runtime value. */
24
- declare const failureRegistration: unique symbol;
25
- /** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
26
- declare class RegisteredFailure {
27
- readonly [failureRegistration]: true;
28
- constructor(registration: Registration);
29
- }
17
+ /**
18
+ * Core's own text for one failure: its message under the category prefix its class carries, with
19
+ * the trailing newline every view's text carries. The four categories are disjoint branches of the
20
+ * hierarchy, so one ordered test reads every class, and a class without a prefix of its own writes
21
+ * the sentence alone. It is the default view of every failure class and the text the plain
22
+ * fallback path writes, so it runs no application code and nothing downstream composes its newline.
23
+ */
24
+ export declare function defaultText(failure: LoomError): string;
30
25
  /** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
31
26
  export declare function commandSubject(name: string | null): string;
27
+ /** The routed path names the Command a sentence speaks of; an empty path is the root. */
28
+ export declare function routedSubject(command: readonly string[]): string;
32
29
  /** The same subject at the start of a sentence. */
33
30
  export declare function commandSentence(name: string | null): string;
34
31
  /**
35
32
  * Every failure `run()` reports is an instance of a public class. Each class carries the facts its
36
- * sentence interpolates, so a renderer reads them instead of parsing prose, and the exit status is
33
+ * sentence interpolates, so a view reads them instead of parsing prose, and the exit status is
37
34
  * a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
38
- * default renderers add it.
35
+ * default views add it.
39
36
  */
40
37
  export declare abstract class LoomError extends Error {
41
38
  readonly exitCode: 1 | 2;
@@ -109,58 +106,33 @@ export declare class DeclarationError extends LoomError {
109
106
  export declare class FatalError extends LoomError {
110
107
  constructor(message: string);
111
108
  }
112
- /** Exit 1: an unexpected exception, a non-error throw, or a renderer that could not answer. */
109
+ /** Exit 1: an unexpected exception, a non-error throw, or a view that could not answer. */
113
110
  export declare class InternalError extends LoomError {
114
111
  readonly cause: unknown;
115
112
  constructor(message: string, cause: unknown);
116
113
  }
114
+ /** The four ways the results lane is broken, each named where core meets it. */
115
+ export type ResultFault = 'missing' | 'repeated' | 'undeclared' | 'middleware';
116
+ /**
117
+ * Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
118
+ * `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
119
+ * and its default text carries the same prefix, while an override keyed by this class reaches it
120
+ * alone.
121
+ */
122
+ export declare class ResultError extends InternalError {
123
+ readonly path: readonly string[];
124
+ readonly kind: ResultFault;
125
+ readonly cause: undefined;
126
+ constructor(kind: ResultFault, path: readonly string[]);
127
+ }
117
128
  /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
118
129
  export declare function reasonOf(thrown: unknown): string;
119
130
  /**
120
- * Why a returned value is not the text a renderer owes. A renderer is synchronous, so a returned
121
- * promise is a non-string return like any other, and its rejection is adopted and swallowed here:
122
- * an unobserved rejection would end the process before the invocation could report anything.
131
+ * Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
132
+ * a non-string return like any other, and its rejection is adopted and swallowed here: an
133
+ * unobserved rejection would end the process before the invocation could report anything.
123
134
  */
124
135
  export declare function notTextReason(value: unknown): string;
125
136
  /** Every thrown value reaches reporting as a failure class; anything else is internal. */
126
137
  export declare function toFailure(thrown: unknown): LoomError;
127
- /** An opaque registration pairing one failure class with a renderer for its instances. */
128
- export type FailureRenderer = Pick<RegisteredFailure, typeof failureRegistration>;
129
- /**
130
- * A registration pairing one failure class with a renderer for its instances. The helper is the
131
- * typed path for a class-keyed list, because an array literal cannot carry a different type
132
- * parameter per element.
133
- */
134
- export declare function renderFailure<Failure extends LoomError>(type: abstract new (...args: never[]) => Failure, renderer: Renderer<Failure>): FailureRenderer;
135
- /** The renderers one application registered, keyed by the class each one names. */
136
- export type FailureRegistry = ReadonlyMap<unknown, Registration>;
137
- /**
138
- * One class answers to one renderer inside one contributor, so a second registration for it is a
139
- * declaration fault. The subject names the contributor: the Application, or an installed plugin.
140
- */
141
- export declare function buildFailures(failures: readonly FailureRenderer[], subject?: string): FailureRegistry;
142
- /**
143
- * One registry from every contributor's own, resolving first-in-wins: the application's
144
- * registrations, then each installed plugin's in installation order, then core's text.
145
- */
146
- export declare function mergeFailures(registries: readonly FailureRegistry[]): FailureRegistry;
147
- /**
148
- * The report of one failure: the text core writes, and whether a registered renderer produced it.
149
- * An unrendered report carries core's own text, which the plain fallback path writes beside the
150
- * diagnostic naming the renderer that could not answer.
151
- */
152
- export type FailureReport = {
153
- kind: 'rendered';
154
- text: string;
155
- } | {
156
- kind: 'unrendered';
157
- text: string;
158
- reason: string;
159
- };
160
- /**
161
- * The text core writes for one failure. Resolution walks the failure's prototype chain most
162
- * derived first through the application's registrations, then falls to core's own text, so a
163
- * registration for a base class brands every failure below it.
164
- */
165
- export declare function describeFailure(registry: FailureRegistry, failure: LoomError): FailureReport;
166
138
  export {};
package/dist/errors.js CHANGED
@@ -1,7 +1,20 @@
1
- /** The routed path names the Command a token fault belongs to; an empty path is the root. */
1
+ /** The same subject at the start of a sentence, where a token fault names its Command. */
2
2
  function routedSentence(command) {
3
- const name = command.at(-1);
4
- return name === undefined ? 'The root Command' : commandSentence(name);
3
+ const subject = routedSubject(command);
4
+ return `${subject.slice(0, 1).toUpperCase()}${subject.slice(1)}`;
5
+ }
6
+ /** The sentence one results-lane fault reports, which names the Command that holds it. */
7
+ function resultMessage(kind, command) {
8
+ if (kind === 'missing') {
9
+ return `${routedSentence(command)} declares a result and its action returned without emitting one. Call out.results() once.`;
10
+ }
11
+ if (kind === 'repeated') {
12
+ return `${routedSentence(command)} emitted its result twice. Call out.results() once.`;
13
+ }
14
+ if (kind === 'undeclared') {
15
+ return `${routedSentence(command)} declares no result. Declare one with result() or rows() before action().`;
16
+ }
17
+ return `A middleware called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
5
18
  }
6
19
  /**
7
20
  * The clause that offers the candidates, which is absent when there are none: every child of the
@@ -17,12 +30,12 @@ function shortGroupMessage(fault) {
17
30
  }
18
31
  /**
19
32
  * Core's own text for one failure: its message under the category prefix its class carries, with
20
- * the trailing newline every renderer's text carries. The four categories are disjoint branches of
21
- * the hierarchy, so one ordered test reads every class, and a class without a prefix of its own
22
- * writes the sentence alone. A default rendering is the same kind of value as a custom one, so
23
- * nothing downstream composes the newline for it.
33
+ * the trailing newline every view's text carries. The four categories are disjoint branches of the
34
+ * hierarchy, so one ordered test reads every class, and a class without a prefix of its own writes
35
+ * the sentence alone. It is the default view of every failure class and the text the plain
36
+ * fallback path writes, so it runs no application code and nothing downstream composes its newline.
24
37
  */
25
- function defaultText(failure) {
38
+ export function defaultText(failure) {
26
39
  if (failure instanceof UsageError) {
27
40
  return `Invalid input: ${failure.message}\n`;
28
41
  }
@@ -34,36 +47,15 @@ function defaultText(failure) {
34
47
  }
35
48
  return `${failure.message}\n`;
36
49
  }
37
- /** Every prototype in a failure's chain, most derived first, so one walk reads the registry. */
38
- function chainOf(failure) {
39
- const chain = [];
40
- let prototype = Object.getPrototypeOf(failure);
41
- while (prototype !== null) {
42
- chain.push(prototype);
43
- prototype = Object.getPrototypeOf(prototype);
44
- }
45
- return chain;
46
- }
47
- /** Authored registrations register here, so the public type publishes nothing to reach. */
48
- const nodes = new WeakMap();
49
- /** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
50
- class RegisteredFailure {
51
- constructor(registration) {
52
- nodes.set(this, registration);
53
- }
54
- }
55
- /** Reads the pair behind a registered value; anything else is a declaration error. */
56
- function nodeOf(value, subject) {
57
- const registration = nodes.get(value);
58
- if (!registration) {
59
- throw new DeclarationError(`${subject} holds a value that is not a failure renderer. Supply the value returned by renderFailure(type, renderer).`);
60
- }
61
- return registration;
62
- }
63
50
  /** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
64
51
  export function commandSubject(name) {
65
52
  return name === null ? 'the root Command' : `Command "${name}"`;
66
53
  }
54
+ /** The routed path names the Command a sentence speaks of; an empty path is the root. */
55
+ export function routedSubject(command) {
56
+ const name = command.at(-1);
57
+ return name === undefined ? 'the root Command' : commandSubject(name);
58
+ }
67
59
  /** The same subject at the start of a sentence. */
68
60
  export function commandSentence(name) {
69
61
  const subject = commandSubject(name);
@@ -71,9 +63,9 @@ export function commandSentence(name) {
71
63
  }
72
64
  /**
73
65
  * Every failure `run()` reports is an instance of a public class. Each class carries the facts its
74
- * sentence interpolates, so a renderer reads them instead of parsing prose, and the exit status is
66
+ * sentence interpolates, so a view reads them instead of parsing prose, and the exit status is
75
67
  * a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
76
- * default renderers add it.
68
+ * default views add it.
77
69
  */
78
70
  export class LoomError extends Error {
79
71
  exitCode;
@@ -193,7 +185,7 @@ export class FatalError extends LoomError {
193
185
  this.name = 'FatalError';
194
186
  }
195
187
  }
196
- /** Exit 1: an unexpected exception, a non-error throw, or a renderer that could not answer. */
188
+ /** Exit 1: an unexpected exception, a non-error throw, or a view that could not answer. */
197
189
  export class InternalError extends LoomError {
198
190
  cause;
199
191
  constructor(message, cause) {
@@ -202,85 +194,36 @@ export class InternalError extends LoomError {
202
194
  this.name = 'InternalError';
203
195
  }
204
196
  }
197
+ /**
198
+ * Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
199
+ * `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
200
+ * and its default text carries the same prefix, while an override keyed by this class reaches it
201
+ * alone.
202
+ */
203
+ export class ResultError extends InternalError {
204
+ path;
205
+ kind;
206
+ constructor(kind, path) {
207
+ super(resultMessage(kind, path), undefined);
208
+ this.kind = kind;
209
+ this.name = 'ResultError';
210
+ this.path = path;
211
+ }
212
+ }
205
213
  /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
206
214
  export function reasonOf(thrown) {
207
215
  return thrown instanceof Error ? thrown.message : 'An unknown error occurred.';
208
216
  }
209
217
  /**
210
- * Why a returned value is not the text a renderer owes. A renderer is synchronous, so a returned
211
- * promise is a non-string return like any other, and its rejection is adopted and swallowed here:
212
- * an unobserved rejection would end the process before the invocation could report anything.
218
+ * Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
219
+ * a non-string return like any other, and its rejection is adopted and swallowed here: an
220
+ * unobserved rejection would end the process before the invocation could report anything.
213
221
  */
214
222
  export function notTextReason(value) {
215
223
  void Promise.resolve(value).catch(() => undefined);
216
- return `The renderer returned ${typeof value} instead of a string.`;
224
+ return `The view returned ${typeof value} instead of a string.`;
217
225
  }
218
226
  /** Every thrown value reaches reporting as a failure class; anything else is internal. */
219
227
  export function toFailure(thrown) {
220
228
  return thrown instanceof LoomError ? thrown : new InternalError(reasonOf(thrown), thrown);
221
229
  }
222
- /**
223
- * A registration pairing one failure class with a renderer for its instances. The helper is the
224
- * typed path for a class-keyed list, because an array literal cannot carry a different type
225
- * parameter per element.
226
- */
227
- export function renderFailure(type, renderer) {
228
- return new RegisteredFailure({
229
- name: 'name' in type && typeof type.name === 'string' ? type.name : 'a failure class',
230
- prototype: 'prototype' in type ? type.prototype : undefined,
231
- // Resolution reaches this registration through the same class, so the test always holds.
232
- render: (failure) => (failure instanceof type ? renderer.render(failure) : undefined),
233
- });
234
- }
235
- /**
236
- * One class answers to one renderer inside one contributor, so a second registration for it is a
237
- * declaration fault. The subject names the contributor: the Application, or an installed plugin.
238
- */
239
- export function buildFailures(failures, subject = 'The Application') {
240
- const registry = new Map();
241
- for (const failure of failures) {
242
- const registration = nodeOf(failure, subject);
243
- if (registry.has(registration.prototype)) {
244
- throw new DeclarationError(`${subject} registers two failure renderers for "${registration.name}". Remove one registration.`);
245
- }
246
- registry.set(registration.prototype, registration);
247
- }
248
- return registry;
249
- }
250
- /**
251
- * One registry from every contributor's own, resolving first-in-wins: the application's
252
- * registrations, then each installed plugin's in installation order, then core's text.
253
- */
254
- export function mergeFailures(registries) {
255
- const merged = new Map();
256
- for (const registry of registries) {
257
- for (const [type, registration] of registry) {
258
- if (!merged.has(type)) {
259
- merged.set(type, registration);
260
- }
261
- }
262
- }
263
- return merged;
264
- }
265
- /**
266
- * The text core writes for one failure. Resolution walks the failure's prototype chain most
267
- * derived first through the application's registrations, then falls to core's own text, so a
268
- * registration for a base class brands every failure below it.
269
- */
270
- export function describeFailure(registry, failure) {
271
- const registration = chainOf(failure)
272
- .map((prototype) => registry.get(prototype))
273
- .find((entry) => entry !== undefined);
274
- if (!registration) {
275
- return { kind: 'rendered', text: defaultText(failure) };
276
- }
277
- try {
278
- const text = registration.render(failure);
279
- return typeof text === 'string'
280
- ? { kind: 'rendered', text }
281
- : { kind: 'unrendered', reason: notTextReason(text), text: defaultText(failure) };
282
- }
283
- catch (error) {
284
- return { kind: 'unrendered', reason: reasonOf(error), text: defaultText(failure) };
285
- }
286
- }
@@ -91,5 +91,9 @@ interface ExtensionSlot {
91
91
  * the record, so a typed read compares by reference without the graph carrying a reference.
92
92
  */
93
93
  declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, unknown>>;
94
+ /** Layers validate in authoring order; replacement updates existing keys without reinsertion. */
95
+ declare function buildCommandExtensions(slot: Omit<ExtensionSlot, 'declared' | 'target'> & {
96
+ layers: readonly unknown[];
97
+ }): Readonly<Record<string, unknown>>;
94
98
  export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSubject, ExtensionTarget, ExtensionValue, };
95
- export { buildExtensions, extension, isDescriptor, readExtension, registerDescriptor };
99
+ export { buildCommandExtensions, buildExtensions, extension, isDescriptor, readExtension, registerDescriptor, };
package/dist/extension.js CHANGED
@@ -310,4 +310,21 @@ function buildExtensions(slot) {
310
310
  owners.set(frozen, defined);
311
311
  return frozen;
312
312
  }
313
- export { buildExtensions, extension, isDescriptor, readExtension, registerDescriptor };
313
+ /** Layers validate in authoring order; replacement updates existing keys without reinsertion. */
314
+ function buildCommandExtensions(slot) {
315
+ const stored = new Map();
316
+ const defined = new Map();
317
+ for (const declared of slot.layers) {
318
+ const layer = buildExtensions({ ...slot, declared, target: 'command' });
319
+ for (const [identity, value] of Object.entries(layer)) {
320
+ stored.set(identity, value);
321
+ }
322
+ for (const [identity, descriptor] of owners.get(layer) ?? []) {
323
+ defined.set(identity, descriptor);
324
+ }
325
+ }
326
+ const frozen = Object.freeze(Object.fromEntries(stored));
327
+ owners.set(frozen, defined);
328
+ return frozen;
329
+ }
330
+ export { buildCommandExtensions, buildExtensions, extension, isDescriptor, readExtension, registerDescriptor, };
package/dist/globals.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import { DeclarationError } from './errors.js';
2
2
  import { compileOptions } from './options.js';
3
3
  import type { BuiltPlugin, PluginBuild } from './plugin.js';
4
- import type { DefaultConstraint, MultipleConstraint, NameConstraint, OptionConfig, OptionValue, ValidateOmittedConstraint } from './types.js';
4
+ import type { OptionConfig, OptionValue } from './types.js';
5
5
  import type { InputDeclaration, OptionInput, ValidatedInputs } from './validation.js';
6
- /** Phantom key. It keeps the declared option types exact and holds no runtime value. */
7
- declare const declaredTypes: unique symbol;
8
6
  /**
9
7
  * Who declared one option that shares the globals table, or the Command that declares a local
10
8
  * option colliding with it. Every collision sentence is derived from a pair of these, so the
@@ -36,33 +34,24 @@ declare function spellingCollision(spelling: string, first: OptionSite, second:
36
34
  * validated and never reaches an action.
37
35
  */
38
36
  interface BuiltGlobals {
37
+ bind: (values: ValidatedInputs) => unknown;
39
38
  inputs: readonly InputDeclaration[];
40
39
  names: ReadonlyMap<string, OptionOwner>;
41
40
  options: ReturnType<typeof compileOptions>;
42
41
  plugins: readonly BuiltPlugin[];
43
- source: unknown;
44
42
  }
45
- declare class GlobalOptionsBuilder<Options> {
46
- #private;
47
- readonly [declaredTypes]: Options;
48
- constructor(inputs: readonly OptionInput[], bind: (values: ValidatedInputs) => Options);
49
- option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): GlobalOptions<Options & Record<Name, OptionValue<Config>>>;
43
+ /** The Application's private global declarations and their schema-derived value binder. */
44
+ interface GlobalsState<Globals = unknown> {
45
+ bind: (values: ValidatedInputs) => Globals;
46
+ inputs: readonly OptionInput[];
50
47
  }
48
+ declare function emptyGlobals(): GlobalsState<{}>;
49
+ declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, input: OptionInput<Name, Config>): GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
51
50
  /**
52
- * Application-wide options. `option()` is the whole authoring surface: it returns a new value and
53
- * leaves its receiver unchanged. The value the declarations share is the application's globals.
54
- * The declarations themselves stay private, so no consumer can read or replace them.
51
+ * The one table the pre-scan reads: the application's global options in authoring order, then each
52
+ * installed plugin's options in installation order. Every collision between the two scopes, by key
53
+ * or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
55
54
  */
56
- type GlobalOptions<Options = {}> = Pick<GlobalOptionsBuilder<Options>, typeof declaredTypes | 'option'>;
57
- /**
58
- * Whether a value is an authored GlobalOptions declaration, whichever call produced it. The
59
- * registry is the test, because `option()` returns a new declaration of its own and the exported
60
- * constructor is only the first of them.
61
- */
62
- declare function isGlobalOptions(value: unknown): boolean;
63
- /** Absent globals compile to an empty table whose source is `undefined`, like the declarations. */
64
- declare function buildGlobals(globals: object | undefined, plugins: readonly BuiltPlugin[], build: PluginBuild): BuiltGlobals;
65
- declare function bindGlobals<Options>(globals: GlobalOptions<Options> | undefined, values: ValidatedInputs): Options;
66
- declare const GlobalOptions: new () => GlobalOptions;
67
- export type { BuiltGlobals, OptionOwner, OptionSite };
68
- export { bindGlobals, buildGlobals, GlobalOptions, isGlobalOptions, keyCollision, spellingCollision, };
55
+ declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[], build: PluginBuild): BuiltGlobals;
56
+ export type { BuiltGlobals, GlobalsState, OptionOwner, OptionSite };
57
+ export { buildGlobals, declareGlobalOption, emptyGlobals, keyCollision, spellingCollision };
package/dist/globals.js CHANGED
@@ -2,7 +2,6 @@ import { DeclarationError } from './errors.js';
2
2
  import { buildExtensions } from './extension.js';
3
3
  import { checkDeprecated, checkDescription, checkHidden } from './facts.js';
4
4
  import { compileOptions } from './options.js';
5
- import { captureConfig } from './validation.js';
6
5
  const globalSubject = 'the global options';
7
6
  /** A collision sentence names a plugin first, then the application's globals, then a local. */
8
7
  const ranks = {
@@ -57,51 +56,21 @@ function spellingCollision(spelling, first, second) {
57
56
  const [leading, trailing] = ordered(first, second);
58
57
  return new DeclarationError(`Option spelling "${spelling}" is used by ${usedBy(leading)} and ${usedBy(trailing)}. Change one declaration.`);
59
58
  }
60
- /** Authored values register here, so the public type publishes no state to reach or replace. */
61
- const nodes = new WeakMap();
62
- class GlobalOptionsBuilder {
63
- #bind;
64
- #inputs;
65
- constructor(inputs, bind) {
66
- this.#bind = bind;
67
- this.#inputs = inputs;
68
- nodes.set(this, { bind, inputs, source: this });
69
- }
70
- option(name, config) {
71
- const input = {
72
- config: captureConfig(config),
73
- kind: 'option',
74
- name,
75
- };
76
- const previous = this.#bind;
77
- return new GlobalOptionsBuilder([...this.#inputs, input], (values) => ({
78
- ...previous(values),
79
- ...values.option(input),
80
- }));
81
- }
59
+ function emptyGlobals() {
60
+ return { bind: () => ({}), inputs: [] };
82
61
  }
83
- /** Reads the declarations behind an authored value; anything else is a declaration error. */
84
- function nodeOf(globals) {
85
- const node = nodes.get(globals);
86
- if (!node) {
87
- throw new DeclarationError('The Application holds a value that is not a GlobalOptions declaration. Supply the value returned by new GlobalOptions().');
88
- }
89
- return node;
90
- }
91
- /**
92
- * Whether a value is an authored GlobalOptions declaration, whichever call produced it. The
93
- * registry is the test, because `option()` returns a new declaration of its own and the exported
94
- * constructor is only the first of them.
95
- */
96
- function isGlobalOptions(value) {
97
- return typeof value === 'object' && value !== null && nodes.has(value);
62
+ function declareGlobalOption(state, input) {
63
+ return {
64
+ bind: (values) => ({ ...state.bind(values), ...values.option(input) }),
65
+ inputs: [...state.inputs, input],
66
+ };
98
67
  }
99
68
  /**
100
69
  * The one table the pre-scan reads: the application's global options in authoring order, then each
101
70
  * installed plugin's options in installation order. Every collision between the two scopes, by key
102
71
  * or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
103
72
  */
104
- function compileTable(node, plugins, build) {
73
+ function buildGlobals(node, plugins, build) {
105
74
  const names = new Map();
106
75
  const application = { kind: 'application' };
107
76
  // A global option belongs to the application, not to one Command, so its facts read that way.
@@ -125,7 +94,7 @@ function compileTable(node, plugins, build) {
125
94
  options,
126
95
  });
127
96
  });
128
- return { inputs: node.inputs, names, options, plugins, source: node.source };
97
+ return { bind: node.bind, inputs: node.inputs, names, options, plugins };
129
98
  }
130
99
  /** One plugin's options joining the table the application's globals already hold. */
131
100
  function join(owner, inputs, table) {
@@ -145,24 +114,4 @@ function join(owner, inputs, table) {
145
114
  options.set(spelling, option);
146
115
  }
147
116
  }
148
- /** Absent globals compile to an empty table whose source is `undefined`, like the declarations. */
149
- function buildGlobals(globals, plugins, build) {
150
- const node = globals === undefined ? { bind: () => ({}), inputs: [], source: undefined } : nodeOf(globals);
151
- return compileTable(node, plugins, build);
152
- }
153
- function bindGlobals(globals, values) {
154
- const bound = globals === undefined ? {} : nodeOf(globals).bind(values);
155
- // Last resort: no typed path exists. The public GlobalOptions type hides its declarations.
156
- // The registry is the only bridge from a value to its binder, and a WeakMap cannot carry the
157
- // Options type of its key. It holds because the registered binder belongs to this value alone.
158
- // Its record composes exactly the declarations that its Options type records.
159
- // A declaration without globals publishes `Options = {}`, and the empty record is exactly that.
160
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion
161
- return bound;
162
- }
163
- const GlobalOptions = class extends GlobalOptionsBuilder {
164
- constructor() {
165
- super([], () => ({}));
166
- }
167
- };
168
- export { bindGlobals, buildGlobals, GlobalOptions, isGlobalOptions, keyCollision, spellingCollision, };
117
+ export { buildGlobals, declareGlobalOption, emptyGlobals, keyCollision, spellingCollision };