@loomcli/core 0.5.0 → 0.7.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 (90) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +18 -2
  3. package/dist/application.js +302 -75
  4. package/dist/bindings.d.ts +15 -10
  5. package/dist/bindings.js +34 -15
  6. package/dist/capture.d.ts +65 -0
  7. package/dist/capture.js +99 -0
  8. package/dist/chain.d.ts +15 -6
  9. package/dist/chain.js +40 -20
  10. package/dist/command-rules.d.ts +55 -0
  11. package/dist/command-rules.js +142 -0
  12. package/dist/command.d.ts +65 -23
  13. package/dist/command.js +734 -236
  14. package/dist/controls.d.ts +8 -0
  15. package/dist/controls.js +23 -0
  16. package/dist/defect.d.ts +18 -0
  17. package/dist/defect.js +272 -0
  18. package/dist/developer.d.ts +24 -0
  19. package/dist/developer.js +52 -0
  20. package/dist/diagnostic-text.d.ts +81 -0
  21. package/dist/diagnostic-text.js +283 -0
  22. package/dist/diagnostic.d.ts +11 -0
  23. package/dist/diagnostic.js +70 -0
  24. package/dist/errors.d.ts +108 -24
  25. package/dist/errors.js +324 -53
  26. package/dist/exit-codes.d.ts +45 -0
  27. package/dist/exit-codes.js +46 -0
  28. package/dist/extension.d.ts +41 -8
  29. package/dist/extension.js +142 -57
  30. package/dist/facts.d.ts +71 -11
  31. package/dist/facts.js +106 -23
  32. package/dist/globals.d.ts +29 -21
  33. package/dist/globals.js +117 -46
  34. package/dist/glyphs.generated.js +1 -1
  35. package/dist/hints.d.ts +79 -0
  36. package/dist/hints.js +247 -0
  37. package/dist/host.d.ts +13 -0
  38. package/dist/host.js +43 -1
  39. package/dist/identity.d.ts +19 -0
  40. package/dist/identity.js +72 -0
  41. package/dist/index.d.ts +12 -2
  42. package/dist/index.js +6 -0
  43. package/dist/input-rules.d.ts +68 -0
  44. package/dist/input-rules.js +152 -0
  45. package/dist/inspect.d.ts +12 -12
  46. package/dist/inspect.js +92 -48
  47. package/dist/lanes.js +1 -1
  48. package/dist/locate.js +4 -4
  49. package/dist/options.d.ts +30 -2
  50. package/dist/options.js +143 -46
  51. package/dist/output.d.ts +9 -2
  52. package/dist/output.js +18 -2
  53. package/dist/plain.d.ts +56 -0
  54. package/dist/plain.js +238 -0
  55. package/dist/plugin-rules.d.ts +68 -0
  56. package/dist/plugin-rules.js +164 -0
  57. package/dist/plugin-settings.d.ts +18 -0
  58. package/dist/plugin-settings.js +38 -0
  59. package/dist/plugin.d.ts +27 -15
  60. package/dist/plugin.js +429 -144
  61. package/dist/prototypes.d.ts +7 -0
  62. package/dist/prototypes.js +29 -0
  63. package/dist/rendering.d.ts +6 -1
  64. package/dist/rendering.js +23 -5
  65. package/dist/rules.d.ts +51 -0
  66. package/dist/rules.js +115 -0
  67. package/dist/sequence.js +6 -1
  68. package/dist/sources.d.ts +6 -4
  69. package/dist/sources.js +25 -16
  70. package/dist/style-layout.js +2 -2
  71. package/dist/style-width.d.ts +13 -0
  72. package/dist/style-width.js +170 -0
  73. package/dist/style-wire.js +1 -1
  74. package/dist/style.js +1 -1
  75. package/dist/theme.d.ts +4 -0
  76. package/dist/theme.js +25 -5
  77. package/dist/thenable.d.ts +15 -0
  78. package/dist/thenable.js +29 -0
  79. package/dist/translators.d.ts +69 -0
  80. package/dist/translators.js +253 -0
  81. package/dist/types.d.ts +13 -3
  82. package/dist/unicode.generated.d.ts +27 -0
  83. package/dist/unicode.generated.js +1036 -0
  84. package/dist/validation.d.ts +69 -7
  85. package/dist/validation.js +211 -67
  86. package/dist/view.d.ts +61 -22
  87. package/dist/view.js +168 -81
  88. package/licenses/unicode-LICENSE.txt +41 -0
  89. package/licenses/uucode-LICENSE.md +35 -0
  90. package/package.json +9 -5
package/dist/options.js CHANGED
@@ -1,52 +1,129 @@
1
- import { DeclarationError, MissingValueError, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
1
+ import { declaredName } from './command-rules.js';
2
+ import { valueCode } from './diagnostic-text.js';
3
+ import { DeclarationError, MissingValueError, quoted, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
4
+ import { factFault, flagFault, siteFinding } from './facts.js';
5
+ import { booleanOptionMultiple, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, } from './input-rules.js';
2
6
  /** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
3
7
  export function emptyValues() {
4
8
  return { booleans: new Map(), lists: new Map(), spellings: new Map(), strings: new Map() };
5
9
  }
6
- function validateDeclaration({ name, config }) {
10
+ /**
11
+ * The part of one declaration that yields a spelling of the given role, which a spelling fault
12
+ * marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
13
+ * generates a negative form.
14
+ */
15
+ export function spellingMark(site, role) {
16
+ if (role === 'long') {
17
+ return site.named;
18
+ }
19
+ return `${site.at}.${role === 'short' ? 'short' : 'polarity'}`;
20
+ }
21
+ /** The declared name answers the declared-name rule an argument's name answers. */
22
+ export function checkOptionName(name, site) {
23
+ const findings = [siteFinding(site, site.named)];
7
24
  if (typeof name !== 'string') {
8
- throw new DeclarationError('Option names must be strings. Supply a string name.');
25
+ throw new DeclarationError(declaredName, {
26
+ correction: 'Supply a string name.',
27
+ findings,
28
+ sentence: `Option name ${valueCode(name)} is not a string.`,
29
+ });
9
30
  }
10
31
  if (!name || name.startsWith('-') || /[\s=]/u.test(name)) {
11
- throw new DeclarationError(`Option name "${name}" is invalid. Use a nonempty name without a leading hyphen, whitespace, or "=".`);
12
- }
13
- if (!['string', 'boolean'].includes(config.type)) {
14
- throw new DeclarationError(`Option "${name}" has an invalid type. Use "string" or "boolean".`);
32
+ throw new DeclarationError(declaredName, {
33
+ correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
34
+ findings,
35
+ sentence: `Option name ${quoted(name)} is invalid.`,
36
+ });
15
37
  }
16
- if (config.short !== undefined &&
17
- (typeof config.short !== 'string' || !/^[A-Za-z]$/u.test(config.short))) {
18
- throw new DeclarationError(`Option "${name}" requires a short alias of one ASCII letter.`);
38
+ }
39
+ /** Whether a declared short alias is one ASCII letter, the rule every short spelling answers. */
40
+ export function isShortAlias(short) {
41
+ return typeof short === 'string' && /^[A-Za-z]$/u.test(short);
42
+ }
43
+ /** The sentence and correction of the short-alias fault for the option `subject` names. */
44
+ export function shortAliasText(subject) {
45
+ return {
46
+ correction: 'Supply one ASCII letter.',
47
+ sentence: `${subject} declares a short alias that is not one ASCII letter.`,
48
+ };
49
+ }
50
+ /** The short alias, and `shortOnly`, which leaves the option that alias alone. */
51
+ function checkShortForms(config, site, subject) {
52
+ if (config.short !== undefined && !isShortAlias(config.short)) {
53
+ throw factFault(shortAlias, site, { ...shortAliasText(subject), fact: 'short' });
19
54
  }
20
55
  if (config.shortOnly !== undefined && typeof config.shortOnly !== 'boolean') {
21
- throw new DeclarationError(`Option "${name}" shortOnly must be Boolean. Use true or false.`);
56
+ throw flagFault(site, 'shortOnly');
22
57
  }
23
58
  if (config.shortOnly && config.short === undefined) {
24
- throw new DeclarationError(`Option "${name}" with shortOnly requires a short alias.`);
59
+ throw factFault(shortOnlyWithoutShort, site, {
60
+ correction: 'Add short or remove shortOnly.',
61
+ fact: 'shortOnly',
62
+ sentence: `${subject} declares shortOnly and no short alias.`,
63
+ });
25
64
  }
65
+ }
66
+ /** A Boolean option's polarity, which a string option does not declare. */
67
+ function checkPolarity(config, site, subject) {
68
+ if (config.polarity === undefined) {
69
+ return;
70
+ }
71
+ if (config.type !== 'boolean') {
72
+ throw factFault(polarityOnString, site, {
73
+ correction: 'Remove polarity or use type "boolean".',
74
+ fact: 'polarity',
75
+ sentence: `${subject} declares polarity but is not Boolean.`,
76
+ });
77
+ }
78
+ if (!['positive', 'both', 'negative'].includes(config.polarity)) {
79
+ throw factFault(optionPolarity, site, {
80
+ correction: 'Use "positive", "both", or "negative".',
81
+ fact: 'polarity',
82
+ sentence: `${subject} has an invalid polarity.`,
83
+ });
84
+ }
85
+ if (config.polarity === 'both' && config.shortOnly) {
86
+ throw factFault(shortOnlyBothPolarities, site, {
87
+ correction: 'Enable long forms or select one polarity.',
88
+ fact: 'shortOnly',
89
+ sentence: `${subject} cannot express both polarities with shortOnly.`,
90
+ });
91
+ }
92
+ }
93
+ /** Every rule one option declaration answers alone, before the table meets it. */
94
+ function validateDeclaration({ name, config }, site) {
95
+ checkOptionName(name, site);
96
+ const subject = `Option ${quoted(name)}`;
97
+ if (!['string', 'boolean'].includes(config.type)) {
98
+ throw factFault(optionType, site, {
99
+ correction: 'Use "string" or "boolean".',
100
+ fact: 'type',
101
+ sentence: `${subject} has an invalid type.`,
102
+ });
103
+ }
104
+ checkShortForms(config, site, subject);
26
105
  if (config.type === 'boolean' && config.multiple !== undefined) {
27
- throw new DeclarationError(`Option "${name}" is a boolean option and declares multiple. Remove multiple or declare a string option.`);
106
+ throw factFault(booleanOptionMultiple, site, {
107
+ correction: 'Remove multiple or declare a string option.',
108
+ fact: 'multiple',
109
+ sentence: `${subject} is a boolean option and declares multiple.`,
110
+ });
28
111
  }
29
112
  if (config.multiple !== undefined && typeof config.multiple !== 'boolean') {
30
- throw new DeclarationError(`Option "${name}" multiple must be Boolean. Use true or false.`);
31
- }
32
- if (config.polarity !== undefined) {
33
- if (config.type !== 'boolean') {
34
- throw new DeclarationError(`Option "${name}" declares polarity but is not Boolean. Remove polarity or use type "boolean".`);
35
- }
36
- if (!['positive', 'both', 'negative'].includes(config.polarity)) {
37
- throw new DeclarationError(`Option "${name}" has an invalid polarity. Use "positive", "both", or "negative".`);
38
- }
39
- if (config.polarity === 'both' && config.shortOnly) {
40
- throw new DeclarationError(`Option "${name}" cannot express both polarities with shortOnly. Enable long forms or select one polarity.`);
41
- }
113
+ throw flagFault(site, 'multiple');
42
114
  }
115
+ checkPolarity(config, site, subject);
43
116
  }
44
- function addSpelling(spellings, spelling, option) {
45
- const existing = spellings.get(spelling);
117
+ function addSpelling(claims, spelling, claim) {
118
+ const existing = claims.get(spelling);
46
119
  if (existing) {
47
- throw new DeclarationError(`Option spelling "${spelling}" is used by both "${existing.name}" and "${option.name}". Change one declaration.`);
120
+ throw new DeclarationError(spellingTaken, {
121
+ correction: 'Change one declaration.',
122
+ findings: [existing, claim].map(({ option, site }) => siteFinding(site, spellingMark(site, option.role))),
123
+ sentence: `Option spelling ${quoted(spelling)} is used by both ${quoted(existing.option.name)} and ${quoted(claim.option.name)}.`,
124
+ });
48
125
  }
49
- spellings.set(spelling, option);
126
+ claims.set(spelling, claim);
50
127
  }
51
128
  /**
52
129
  * One Boolean option's value for one invocation: the value the parser consumed, or the value its
@@ -57,37 +134,50 @@ function addSpelling(spellings, spelling, option) {
57
134
  export function booleanValue(values, name, config) {
58
135
  return values.booleans.get(name) ?? config.polarity === 'negative';
59
136
  }
60
- export function compileOptions(declarations, subject) {
61
- const spellings = new Map();
62
- const names = new Set();
63
- for (const { name, config } of declarations) {
64
- validateDeclaration({ config, name });
65
- if (names.has(name)) {
66
- throw new DeclarationError(`Option "${name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
137
+ /**
138
+ * One scope's options compiled into the spelling table the parser reads, with every rule one
139
+ * declaration answers alone and every rule two of them answer together.
140
+ */
141
+ export function compileOptions(declarations, scope) {
142
+ const claims = new Map();
143
+ const names = new Map();
144
+ for (const declaration of declarations) {
145
+ const { name, config } = declaration;
146
+ const site = scope.siteOf(declaration);
147
+ validateDeclaration({ config, name }, site);
148
+ const first = names.get(name);
149
+ if (first) {
150
+ const earlier = scope.siteOf(first);
151
+ throw new DeclarationError(optionDeclaredTwice, {
152
+ correction: 'Remove or rename the duplicate.',
153
+ findings: [
154
+ siteFinding(earlier, earlier.named, 'the first declaration'),
155
+ siteFinding(site, site.named, 'the second declaration'),
156
+ ],
157
+ sentence: `Option ${quoted(name)} is declared more than once on ${scope.subject}.`,
158
+ });
67
159
  }
68
- names.add(name);
160
+ names.set(name, declaration);
69
161
  const positive = config.type === 'string'
70
162
  ? { multiple: config.multiple === true, name, type: 'string' }
71
163
  : { name, type: 'boolean', value: config.polarity !== 'negative' };
72
164
  if (!config.shortOnly) {
73
165
  if (config.type === 'string' || config.polarity !== 'negative') {
74
- addSpelling(spellings, `--${name}`, { ...positive, role: 'long' });
166
+ addSpelling(claims, `--${name}`, { option: { ...positive, role: 'long' }, site });
75
167
  }
76
168
  if (config.type === 'boolean' &&
77
169
  (config.polarity === 'both' || config.polarity === 'negative')) {
78
- addSpelling(spellings, `--no-${name}`, {
79
- name,
80
- role: 'negative',
81
- type: 'boolean',
82
- value: false,
170
+ addSpelling(claims, `--no-${name}`, {
171
+ option: { name, role: 'negative', type: 'boolean', value: false },
172
+ site,
83
173
  });
84
174
  }
85
175
  }
86
176
  if (config.short !== undefined) {
87
- addSpelling(spellings, `-${config.short}`, { ...positive, role: 'short' });
177
+ addSpelling(claims, `-${config.short}`, { option: { ...positive, role: 'short' }, site });
88
178
  }
89
179
  }
90
- return spellings;
180
+ return new Map([...claims].map(([spelling, { option }]) => [spelling, option]));
91
181
  }
92
182
  /**
93
183
  * Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
@@ -199,6 +289,9 @@ function readOption(spellings, input, state) {
199
289
  /**
200
290
  * A hyphen token belongs to the globals when its long spelling or every short letter does. The
201
291
  * pre-scan reads the globals alone, so a letter it does not own is only "not a global option".
292
+ * The scan stops at a global value option, because the letters after it may be the value the
293
+ * operator meant to pass: the token then belongs to the globals, and parsing reports the
294
+ * value-position fault that names that option alone.
202
295
  */
203
296
  function isGlobalToken(spellings, token) {
204
297
  const long = longToken(token);
@@ -210,8 +303,12 @@ function isGlobalToken(spellings, token) {
210
303
  let other = '';
211
304
  for (let index = 0; index < group.length; index += 1) {
212
305
  const letter = group.charAt(index);
213
- if (spellings.has(`-${letter}`)) {
306
+ const option = spellings.get(`-${letter}`);
307
+ if (option) {
214
308
  global = global === '' ? letter : global;
309
+ if (option.type === 'string') {
310
+ break;
311
+ }
215
312
  }
216
313
  else {
217
314
  other = other === '' ? letter : other;
package/dist/output.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { RenderingPolicy } from './rendering.js';
4
4
  import type { Palette } from './style-state.js';
5
5
  import type { ActionChannel, Host, OpenResult, Out, ResultBinding, ViewContext } from './types.js';
6
6
  import type { ViewRegistry } from './view.js';
7
- type WriteState = {
7
+ export type WriteState = {
8
8
  kind: 'ok';
9
9
  } | {
10
10
  kind: 'failed';
@@ -19,6 +19,7 @@ export declare class Output {
19
19
  private readonly signal;
20
20
  private readonly destinations;
21
21
  private renderFault;
22
+ private readonly viewFaults;
22
23
  private readonly stops;
23
24
  private route;
24
25
  /**
@@ -56,7 +57,7 @@ export declare class Output {
56
57
  private resultFault;
57
58
  /** The registry one invocation resolves through, republished as each contributor is read. */
58
59
  useViews(registry: ViewRegistry): void;
59
- /** The routed path, published once routing resolved it, which an incomplete sequence names. */
60
+ /** The path routing walked, updated as each name routes, which an incomplete sequence names. */
60
61
  useRoute(path: readonly string[]): void;
61
62
  /** What this invocation's output raised beside its calls, in the order it was raised. */
62
63
  get stopped(): readonly unknown[];
@@ -112,6 +113,12 @@ export declare class Output {
112
113
  * awaits the call must not end the process with an unhandled rejection.
113
114
  */
114
115
  private renderFailed;
116
+ /**
117
+ * Whether one value is what a view or a message check failed with during this invocation. A
118
+ * broken view is a defect in the code that broke the view contract, so an action or a source that
119
+ * lets its rejection propagate reports it as that defect.
120
+ */
121
+ raisedByView(value: unknown): boolean;
115
122
  /** What a view failed with during this invocation, if one did. */
116
123
  get fault(): {
117
124
  cause: unknown;
package/dist/output.js CHANGED
@@ -145,10 +145,13 @@ export class Output {
145
145
  signal;
146
146
  destinations = new Map();
147
147
  renderFault = undefined;
148
+ // Every value a view failed with.
149
+ // A caller that lets one propagate never offers it to a translator.
150
+ viewFaults = new WeakSet();
148
151
  // Every fault the output path raised beside its calls, reported after the primary outcome.
149
152
  // A source a sequence stopped on and a results-lane fault are both such faults.
150
153
  stops = [];
151
- // The routed Command an incomplete sequence names, published once routing resolved it.
154
+ // The path routing walked, which an incomplete sequence names, updated as each name routes.
152
155
  route = [];
153
156
  /**
154
157
  * The channel every caller writes through. It is typed with the result left open, because the
@@ -263,7 +266,7 @@ export class Output {
263
266
  useViews(registry) {
264
267
  this.registry = registry;
265
268
  }
266
- /** The routed path, published once routing resolved it, which an incomplete sequence names. */
269
+ /** The path routing walked, updated as each name routes, which an incomplete sequence names. */
267
270
  useRoute(path) {
268
271
  this.route = path;
269
272
  }
@@ -395,10 +398,23 @@ export class Output {
395
398
  */
396
399
  renderFailed(cause) {
397
400
  this.renderFault ??= { cause };
401
+ if ((typeof cause === 'object' || typeof cause === 'function') && cause !== null) {
402
+ this.viewFaults.add(cause);
403
+ }
398
404
  const rejection = Promise.reject(cause);
399
405
  void rejection.catch(() => undefined);
400
406
  return rejection;
401
407
  }
408
+ /**
409
+ * Whether one value is what a view or a message check failed with during this invocation. A
410
+ * broken view is a defect in the code that broke the view contract, so an action or a source that
411
+ * lets its rejection propagate reports it as that defect.
412
+ */
413
+ raisedByView(value) {
414
+ return ((typeof value === 'object' || typeof value === 'function') &&
415
+ value !== null &&
416
+ this.viewFaults.has(value));
417
+ }
402
418
  /** What a view failed with during this invocation, if one did. */
403
419
  get fault() {
404
420
  return this.renderFault;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Runs one declaring call with verdicts of its own, dropped when it returns or throws. A call made
3
+ * inside another, such as a `plugin()` a getter makes, is part of the outer read and shares them.
4
+ */
5
+ declare function declaring<Result>(call: () => Result): Result;
6
+ /**
7
+ * A structural value core reads as plain data: an object literal, and never a declaration that
8
+ * carries state of its own. The options slots read it to reject a value that is not an options
9
+ * object, and `snapshot` reads it to copy a declared value faithfully. A value a declaring call
10
+ * already judged answers with that verdict.
11
+ */
12
+ declare function isPlainObject(value: unknown): value is Record<string, unknown>;
13
+ /**
14
+ * Whether a declaring call reads one part of a declaration as plain data, decided once: the first
15
+ * verdict is recorded, and `isPlainObject` answers with it for the same value from then on.
16
+ */
17
+ declare function decidePlain(value: unknown): value is Record<string, unknown>;
18
+ /**
19
+ * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
20
+ * consumer cannot reach the source through the copy. A value that holds itself is copied with the
21
+ * same cycle, because each source object is judged and copied once and every path to it reaches
22
+ * that copy. Primitives and library objects, such as a class instance or a `Date` a schema
23
+ * produced, are reported as they are, because core cannot copy them meaningfully. Build reads it
24
+ * for a converter's schema, and the chain for the request one middleware holds.
25
+ */
26
+ declare function snapshot(value: unknown): unknown;
27
+ /** The snapshot of one plain object, under the record type the caller already established. */
28
+ declare function snapshotRecord(value: Record<string, unknown>): Readonly<Record<string, unknown>>;
29
+ /**
30
+ * The snapshot `snapshot` takes, of a value whose paths may hold at most `levels` arrays and plain
31
+ * objects, the value itself included. Every path through a container the value holds twice counts
32
+ * it, and a value that holds itself has a path without end. Either kind of path past `levels`
33
+ * throws `NestedTooDeepError` before the walk goes deeper than `levels`, so neither the walk nor
34
+ * any later reader of the copy goes deeper either. A declaring call reads it for a declared default.
35
+ */
36
+ declare function boundedSnapshot(value: unknown, levels: number): unknown;
37
+ /** What `boundedSnapshot` throws for a value with a path longer than its limit. */
38
+ declare class NestedTooDeepError extends Error {
39
+ name: string;
40
+ }
41
+ /**
42
+ * A copy of every own string key of one object, enumerable or not, each described once and read
43
+ * once, in the order the object lists its keys. `entering` hears each key before it is read, so a
44
+ * read that throws can name it. A symbol key is not copied, because no declaration declares one.
45
+ */
46
+ declare function copyOwnKeys(declared: object, entering?: (key: string) => void): Record<string, unknown>;
47
+ /**
48
+ * The copy of one part of a declaration that `decidePlain` judges plain, by `copyOwnKeys`. Any other
49
+ * value is answered as it is, so the rule for its slot reports it.
50
+ */
51
+ declare function shallowRecord(value: unknown): unknown;
52
+ /** A copy of one list's entries by `fillList`. Each entry is what its factory built, so it is kept. */
53
+ declare function copyList<Entry>(list: readonly Entry[]): Entry[];
54
+ /** The copy of a value that is a list, by `copyList`. Any other value is answered as it is. */
55
+ declare function shallowList(value: unknown): unknown;
56
+ export { boundedSnapshot, copyList, copyOwnKeys, decidePlain, declaring, isPlainObject, NestedTooDeepError, shallowList, shallowRecord, snapshot, snapshotRecord, };
package/dist/plain.js ADDED
@@ -0,0 +1,238 @@
1
+ /**
2
+ * The verdict the declaring call under way reached on each part of a declaration it captured, so
3
+ * every later rule of that call reads that one verdict and never asks the part's prototype again.
4
+ * No call is under way outside `declaring`, so a run and every later call judge afresh.
5
+ */
6
+ let verdicts = undefined;
7
+ /**
8
+ * Runs one declaring call with verdicts of its own, dropped when it returns or throws. A call made
9
+ * inside another, such as a `plugin()` a getter makes, is part of the outer read and shares them.
10
+ */
11
+ function declaring(call) {
12
+ if (verdicts !== undefined) {
13
+ return call();
14
+ }
15
+ verdicts = new WeakMap();
16
+ try {
17
+ return call();
18
+ }
19
+ finally {
20
+ verdicts = undefined;
21
+ }
22
+ }
23
+ /**
24
+ * A structural value core reads as plain data: an object literal, and never a declaration that
25
+ * carries state of its own. The options slots read it to reject a value that is not an options
26
+ * object, and `snapshot` reads it to copy a declared value faithfully. A value a declaring call
27
+ * already judged answers with that verdict.
28
+ */
29
+ function isPlainObject(value) {
30
+ if (value === null || typeof value !== 'object') {
31
+ return false;
32
+ }
33
+ return verdicts?.get(value) ?? hasPlainPrototype(value);
34
+ }
35
+ /** Whether one object's prototype, read once, is `Object.prototype` or `null`. */
36
+ function hasPlainPrototype(value) {
37
+ const prototype = Object.getPrototypeOf(value);
38
+ return prototype === Object.prototype || prototype === null;
39
+ }
40
+ /**
41
+ * Whether a declaring call reads one part of a declaration as plain data, decided once: the first
42
+ * verdict is recorded, and `isPlainObject` answers with it for the same value from then on.
43
+ */
44
+ function decidePlain(value) {
45
+ if (value === null || typeof value !== 'object') {
46
+ return false;
47
+ }
48
+ const known = verdicts?.get(value);
49
+ if (known !== undefined) {
50
+ return known;
51
+ }
52
+ const verdict = hasPlainPrototype(value);
53
+ verdicts?.set(value, verdict);
54
+ return verdict;
55
+ }
56
+ /**
57
+ * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
58
+ * consumer cannot reach the source through the copy. A value that holds itself is copied with the
59
+ * same cycle, because each source object is judged and copied once and every path to it reaches
60
+ * that copy. Primitives and library objects, such as a class instance or a `Date` a schema
61
+ * produced, are reported as they are, because core cannot copy them meaningfully. Build reads it
62
+ * for a converter's schema, and the chain for the request one middleware holds.
63
+ */
64
+ function snapshot(value) {
65
+ return copied(value, copying(Number.POSITIVE_INFINITY), 1);
66
+ }
67
+ /** The snapshot of one plain object, under the record type the caller already established. */
68
+ function snapshotRecord(value) {
69
+ return copiedRecord(value, copying(Number.POSITIVE_INFINITY), 1);
70
+ }
71
+ /**
72
+ * The snapshot `snapshot` takes, of a value whose paths may hold at most `levels` arrays and plain
73
+ * objects, the value itself included. Every path through a container the value holds twice counts
74
+ * it, and a value that holds itself has a path without end. Either kind of path past `levels`
75
+ * throws `NestedTooDeepError` before the walk goes deeper than `levels`, so neither the walk nor
76
+ * any later reader of the copy goes deeper either. A declaring call reads it for a declared default.
77
+ */
78
+ function boundedSnapshot(value, levels) {
79
+ return copied(value, copying(levels), 1);
80
+ }
81
+ /** What `boundedSnapshot` throws for a value with a path longer than its limit. */
82
+ class NestedTooDeepError extends Error {
83
+ name = 'NestedTooDeepError';
84
+ }
85
+ /** A snapshot about to start, whose paths may hold at most `levels` containers. */
86
+ function copying(levels) {
87
+ return { copies: new Map(), heights: new Map(), levels };
88
+ }
89
+ /**
90
+ * One value of a snapshot in progress, reached at `level`, where the value itself is level 1. An
91
+ * object the walk reached already answers as it did then, so its prototype is read once.
92
+ */
93
+ function copied(value, walk, level) {
94
+ if (typeof value !== 'object' || value === null) {
95
+ return value;
96
+ }
97
+ if (walk.copies.has(value)) {
98
+ return walk.copies.get(value);
99
+ }
100
+ if (Array.isArray(value)) {
101
+ return copiedList(value, walk, level);
102
+ }
103
+ if (isPlainObject(value)) {
104
+ return copiedRecord(value, walk, level);
105
+ }
106
+ return kept(value, walk);
107
+ }
108
+ /** An object core cannot copy, kept as it is, which every later reach answers with unread. */
109
+ function kept(value, walk) {
110
+ walk.copies.set(value, value);
111
+ walk.heights.set(value, 0);
112
+ return value;
113
+ }
114
+ /**
115
+ * How many containers the longest path from one entry holds, once the walk has copied the entry of
116
+ * a container reached at `level`. A container the walk filled counts every container below it,
117
+ * even one it reached first by a shorter path. A container still being filled is one this entry
118
+ * leads back into, so the path through it has no end, and any other value holds none.
119
+ */
120
+ function checkedHeight(entry, walk, level) {
121
+ const height = typeof entry === 'object' && entry !== null
122
+ ? (walk.heights.get(entry) ?? Number.POSITIVE_INFINITY)
123
+ : 0;
124
+ if (level + height > walk.levels) {
125
+ throw new NestedTooDeepError();
126
+ }
127
+ return height;
128
+ }
129
+ /**
130
+ * The copier of each entry of one container reached at `level`, which counts how many containers
131
+ * the longest path from that container holds, itself included.
132
+ */
133
+ function entryCopier(walk, level) {
134
+ let height = 1;
135
+ return {
136
+ copy: (entry) => {
137
+ const copy = copied(entry, walk, level + 1);
138
+ height = Math.max(height, checkedHeight(entry, walk, level) + 1);
139
+ return copy;
140
+ },
141
+ height: () => height,
142
+ };
143
+ }
144
+ /** Throws `NestedTooDeepError` for a container the walk first reaches past its limit. */
145
+ function checkLevel(walk, level) {
146
+ if (level > walk.levels) {
147
+ throw new NestedTooDeepError();
148
+ }
149
+ }
150
+ /**
151
+ * One array's copy. It joins `copies` before its entries are read, so an entry that leads back to
152
+ * it reaches the copy, and it is frozen once it is filled. A hole stays a hole.
153
+ */
154
+ function copiedList(list, walk, level) {
155
+ checkLevel(walk, level);
156
+ const copy = [];
157
+ walk.copies.set(list, copy);
158
+ const entries = entryCopier(walk, level);
159
+ fillList(copy, list, entries.copy);
160
+ walk.heights.set(list, entries.height());
161
+ return Object.freeze(copy);
162
+ }
163
+ /**
164
+ * One plain object's copy on `Object.prototype`, filled and frozen as an array's copy is. Each key
165
+ * is defined as an own data property, so a key of `__proto__` is stored under that name.
166
+ */
167
+ function copiedRecord(record, walk, level) {
168
+ checkLevel(walk, level);
169
+ const copy = {};
170
+ walk.copies.set(record, copy);
171
+ const entries = entryCopier(walk, level);
172
+ for (const [key, entry] of Object.entries(record)) {
173
+ defineEntry(copy, key, entries.copy(entry));
174
+ }
175
+ walk.heights.set(record, entries.height());
176
+ return Object.freeze(copy);
177
+ }
178
+ /** Defines one key as an own data property, so a key of `__proto__` is stored under that name. */
179
+ function defineEntry(target, key, value) {
180
+ Object.defineProperty(target, key, {
181
+ configurable: true,
182
+ enumerable: true,
183
+ value,
184
+ writable: true,
185
+ });
186
+ }
187
+ /**
188
+ * A copy of every own string key of one object, enumerable or not, each described once and read
189
+ * once, in the order the object lists its keys. `entering` hears each key before it is read, so a
190
+ * read that throws can name it. A symbol key is not copied, because no declaration declares one.
191
+ */
192
+ function copyOwnKeys(declared, entering = () => undefined) {
193
+ const copy = {};
194
+ for (const key of Reflect.ownKeys(declared)) {
195
+ if (typeof key === 'string') {
196
+ entering(key);
197
+ if (Reflect.getOwnPropertyDescriptor(declared, key) !== undefined) {
198
+ defineEntry(copy, key, Reflect.get(declared, key));
199
+ }
200
+ }
201
+ }
202
+ return copy;
203
+ }
204
+ /**
205
+ * The copy of one part of a declaration that `decidePlain` judges plain, by `copyOwnKeys`. Any other
206
+ * value is answered as it is, so the rule for its slot reports it.
207
+ */
208
+ function shallowRecord(value) {
209
+ return decidePlain(value) ? copyOwnKeys(value) : value;
210
+ }
211
+ /**
212
+ * Fills `copy` with what `convert` makes of each entry of `list`, read by its length once and then
213
+ * index by index, so none of the list's own methods runs. A hole stays a hole.
214
+ */
215
+ function fillList(copy, list, convert) {
216
+ const { length } = list;
217
+ for (let index = 0; index < length; index += 1) {
218
+ if (Object.hasOwn(list, index)) {
219
+ defineEntry(copy, index, convert(list[index]));
220
+ }
221
+ }
222
+ copy.length = length;
223
+ }
224
+ /** A copy of one list's entries by `fillList`. Each entry is what its factory built, so it is kept. */
225
+ function copyList(list) {
226
+ const copy = [];
227
+ fillList(copy, list, (entry) => entry);
228
+ return copy;
229
+ }
230
+ /** The copy of a value that is a list, by `copyList`. Any other value is answered as it is. */
231
+ function shallowList(value) {
232
+ if (!Array.isArray(value)) {
233
+ return value;
234
+ }
235
+ const list = value;
236
+ return copyList(list);
237
+ }
238
+ export { boundedSnapshot, copyList, copyOwnKeys, decidePlain, declaring, isPlainObject, NestedTooDeepError, shallowList, shallowRecord, snapshot, snapshotRecord, };