@zvenigora/ng-eval-forms 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -25
- package/fesm2022/zvenigora-ng-eval-forms-signals.mjs +483 -0
- package/fesm2022/zvenigora-ng-eval-forms-signals.mjs.map +1 -0
- package/fesm2022/zvenigora-ng-eval-forms.mjs +90 -2
- package/fesm2022/zvenigora-ng-eval-forms.mjs.map +1 -1
- package/package.json +9 -1
- package/types/zvenigora-ng-eval-forms-signals.d.ts +135 -0
- package/types/zvenigora-ng-eval-forms.d.ts +61 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignalContext } from '@zvenigora/ng-eval-signals';
|
|
1
|
+
import { SignalContextWriteError, createSignalContext } from '@zvenigora/ng-eval-signals';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Coerces an expression's result to the boolean a `visible` rule needs.
|
|
@@ -46,6 +46,94 @@ const toVisible = (value) => !!value;
|
|
|
46
46
|
*/
|
|
47
47
|
const toText = (value) => value === null || value === undefined ? '' : String(value);
|
|
48
48
|
|
|
49
|
+
// A **value** import, and it has to stay one: `instanceof` needs the
|
|
50
|
+
// constructor, not the type. `field-context.ts` already imports a value from
|
|
51
|
+
// this package, so this adds no dependency edge.
|
|
52
|
+
//
|
|
53
|
+
// **The compiler enforces the form, and the plan said otherwise** - S 3.4 and
|
|
54
|
+
// S 8.1 both call a type-only import here a change that "would compile and the
|
|
55
|
+
// guard would silently never fire". Measured in step 3: `import type` is
|
|
56
|
+
// **TS1361** at `applyErrorPolicy`'s `instanceof`, so it fails
|
|
57
|
+
// `build:production` and gate 7, and under ts-jest's transpile path the elided
|
|
58
|
+
// binding is a `ReferenceError` inside the `catch` that reddens *both* policy
|
|
59
|
+
// arms. Loud at every gate, in other words - see the plan's revision 13.
|
|
60
|
+
//
|
|
61
|
+
// The failure mode that *is* silent is a different one and no import syntax
|
|
62
|
+
// prevents it: two resolved copies of `@zvenigora/ng-eval-signals` give two
|
|
63
|
+
// distinct constructors, and `instanceof` is then false for an error the other
|
|
64
|
+
// copy threw. That is what the write-error arm of `evaluate-rule.spec.ts`
|
|
65
|
+
// covers by throwing across the real package boundary rather than
|
|
66
|
+
// constructing the error itself.
|
|
67
|
+
/**
|
|
68
|
+
* Runs one rule under one {@link ExpressionErrorPolicy}, with the one error
|
|
69
|
+
* that must never be routed through it taken out first.
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* applyErrorPolicy(() => evaluateRule(compiled, context, options), onError)
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* **`SignalContextWriteError` re-throws in every mode, including a handler
|
|
76
|
+
* function** (plan S 3.4, 1.2.11). On `/reactive` that bypass came free -
|
|
77
|
+
* `createEvalSignal` re-throws it regardless of `onError` (`eval-signal.ts:353`)
|
|
78
|
+
* - and that behaviour lives *inside* `createEvalSignal`, which the `/signals`
|
|
79
|
+
* path never calls. It does not travel with the type, so it is re-implemented
|
|
80
|
+
* here or it does not exist. An assigning expression is illegal on every
|
|
81
|
+
* recompute with every dataset, so under this module's default of
|
|
82
|
+
* `'undefined'` it would otherwise render as a permanently blank field with
|
|
83
|
+
* nothing in the console.
|
|
84
|
+
*
|
|
85
|
+
* **The `instanceof` needs the constructor, so the import of that class is a
|
|
86
|
+
* *value* import** - see the note above it, and the plan's revision 13, for
|
|
87
|
+
* why that form is protected by the compiler rather than by this spec.
|
|
88
|
+
* `evaluate-rule.spec.ts`'s write-error arm asserts on the **class** rather
|
|
89
|
+
* than on a message because what it covers is the bypass's *behaviour* across
|
|
90
|
+
* a real package boundary, which no compiler check reaches.
|
|
91
|
+
*
|
|
92
|
+
* **It lives in the core while `/signals`' choke point does not, and the pair
|
|
93
|
+
* is not inconsistent** (S 3.4.1). The discriminator is what the symbol grants
|
|
94
|
+
* a caller who reaches it: `evaluateRule` published *is* a second path to the
|
|
95
|
+
* walk, which S 9.1 requires there be only one of, while this is a pure
|
|
96
|
+
* function over an error and a policy that takes no context, holds no compiled
|
|
97
|
+
* callback and cannot reach a walk. Publishing it grants a caller nothing they
|
|
98
|
+
* could not write in four lines, and buys the thing duplication would lose -
|
|
99
|
+
* one implementation of the write-error bypass for both adapters, which cannot
|
|
100
|
+
* then drift into disagreeing about the one error that must never be
|
|
101
|
+
* swallowed.
|
|
102
|
+
*
|
|
103
|
+
* **It re-throws the error it caught; `/reactive` re-wraps.** `createEvalSignal`
|
|
104
|
+
* throws a *new* `SignalContextWriteError` carrying the offending expression
|
|
105
|
+
* string, which it has and this path does not - a `LogicFn` holds a compiled
|
|
106
|
+
* callback, not the source text. So the same misuse surfaces a different
|
|
107
|
+
* object at each entry point: wrapped with the expression under `/reactive`,
|
|
108
|
+
* the original here. Stated rather than hidden; it is the one thing a consumer
|
|
109
|
+
* catching this error across both adapters would notice.
|
|
110
|
+
*
|
|
111
|
+
* @param run - The rule invocation. On `/signals` this is the choke point,
|
|
112
|
+
* never a bare walk: containment is inside `run`, so a throw is
|
|
113
|
+
* contained before this function decides what to do about it.
|
|
114
|
+
* @param policy - Defaults to `'undefined'`, per {@link ExpressionErrorPolicy}.
|
|
115
|
+
* Resolving the default *here* is what makes an absent policy
|
|
116
|
+
* mean `'undefined'` rather than inheriting `eval-signals`'
|
|
117
|
+
* opposite one.
|
|
118
|
+
*/
|
|
119
|
+
const applyErrorPolicy = (run, policy = 'undefined') => {
|
|
120
|
+
try {
|
|
121
|
+
return run();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof SignalContextWriteError) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
if (policy === 'throw') {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (policy === 'undefined') {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return policy(error);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
49
137
|
/**
|
|
50
138
|
* Composes one `EvalContext` for one field, out of two live sources.
|
|
51
139
|
*
|
|
@@ -128,5 +216,5 @@ const createFieldContext = (formSource, fieldSource, options) => {
|
|
|
128
216
|
* Generated bundle index. Do not edit.
|
|
129
217
|
*/
|
|
130
218
|
|
|
131
|
-
export { createFieldContext, toText, toVisible };
|
|
219
|
+
export { applyErrorPolicy, createFieldContext, toText, toVisible };
|
|
132
220
|
//# sourceMappingURL=zvenigora-ng-eval-forms.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zvenigora-ng-eval-forms.mjs","sources":["../../../../modules/eval-forms/src/lib/coercion.ts","../../../../modules/eval-forms/src/lib/field-context.ts","../../../../modules/eval-forms/src/zvenigora-ng-eval-forms.ts"],"sourcesContent":["/**\r\n * Coerces an expression's result to the boolean a `visible` rule needs.\r\n *\r\n * **JavaScript truthiness, and nothing cleverer** (plan S 3.6). A rule author\r\n * writing `visible: \"country === 'US'\"` gets a boolean already; the coercion\r\n * exists for the ones who write `visible: \"country\"`.\r\n *\r\n * `undefined` is therefore the load-bearing input rather than an edge case.\r\n * An *empty* field and a *missing* field are indistinguishable here - S 3.4.3:\r\n * a field key bound to `undefined` is treated as absent, and an empty\r\n * `FormControl` is the common case - so both arrive as `undefined`, and both\r\n * mean \"not visible\".\r\n *\r\n * The string `'false'` is **visible**, because it is a non-empty string. That\r\n * is truthiness rather than parsing, and it is the answer a form-builder UI\r\n * storing every value as a string will hit. Documented in the README rather\r\n * than special-cased: a coercion that parsed `'false'` would then owe an\r\n * answer for `'no'`, `'0'` and `'off'`, none of which JavaScript has one for.\r\n *\r\n * Both adapters call this, which is why it is in the core: `/reactive` and\r\n * `/signals` differ in how they schedule the recompute and not in what the\r\n * result means (S 3.4.5).\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns `true` when the value is truthy.\r\n */\r\nexport const toVisible = (value: unknown): boolean => !!value;\r\n\r\n/**\r\n * Coerces an expression's result to the string a `text` rule needs.\r\n *\r\n * `String(value)`, with `null` and `undefined` mapping to `''` (plan S 3.6).\r\n * Those two are the carve-out and the only one: rendering the literal text\r\n * `undefined` into a label is the failure this rule exists to prevent, and it\r\n * is what a bare `String(value)` would do for the most ordinary input a form\r\n * has - the empty control of S 3.4.3.\r\n *\r\n * Every *other* falsy value stringifies normally: `0` is `'0'` and `false` is\r\n * `'false'`. Mapping all falsy values to `''` would be the obvious way to\r\n * write this rule wrongly, and it would blank a field whose value is\r\n * legitimately zero.\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns The value as a string, or `''` when it is `null` or `undefined`.\r\n */\r\nexport const toText = (value: unknown): string =>\r\n value === null || value === undefined ? '' : String(value);\r\n","import { EvalContext, EvalOptions } from '@zvenigora/ng-eval-core';\r\nimport { SignalContextSource, createSignalContext } from '@zvenigora/ng-eval-signals';\r\n\r\n/**\r\n * Composes one `EvalContext` for one field, out of two live sources.\r\n *\r\n * The mechanism is **two lookups, not a joined record** (plan S 3.4.2). The\r\n * field half is a `createSignalContext` over `fieldSource`; the form half is a\r\n * second resolver pushed onto the same context's `lookups`. Both close over\r\n * their source and read it at resolve time, so a key added to either after\r\n * construction resolves - there is no third object to keep in sync, and no\r\n * write-through to N contexts.\r\n *\r\n * `EvalContext.get` walks `lookups` in order and takes the first\r\n * non-`undefined`, so the field half is consulted first.\r\n *\r\n * One context per field, never one shared across fields (plan S 3.4.1):\r\n * `get` resolves `scopes` and `original` *before* `lookups`, so anything left\r\n * on a shared context - an arrow function's leaked scope, say - would be read\r\n * ahead of every other field's source key of the same name, for the life of\r\n * the form.\r\n *\r\n * **Resolution is live; appearance is not reactive.** An expression that read\r\n * a then-missing key subscribed to nothing, so the owner of the source calls\r\n * `invalidate()` when the *key set* changes. A change to a *value* is\r\n * Angular's job and needs nothing.\r\n *\r\n * **Precedence is value-dependent, not permanent.** A field key wins while\r\n * its value is *present*: `EvalContext.get` treats `undefined` as absent at\r\n * every step, and the field resolver returns `undefined` both for \"no such\r\n * key\" and for \"key bound to `undefined`\" - so an empty `FormControl`, which\r\n * is the common case rather than an exotic one, falls through to the form\r\n * value. Documented as a limitation (plan S 3.4.3): distinguishing the two\r\n * would need a sentinel threaded through `EvalContext.get`, which is\r\n * `eval-core`'s and out of this phase's scope.\r\n *\r\n * @param formSource - The form-wide keys, shared by every field of the form.\r\n * @param fieldSource - The field-local keys. These win on a name collision,\r\n * while their value is not `undefined`.\r\n * @param options - Passed to `createSignalContext` for **both** halves;\r\n * configures this context and its resolvers, not the walk.\r\n * As upstream, `caseInsensitive` corrects identifier keys\r\n * here but not *property* names - the member visitor reads\r\n * those off the state's options, so the same options must\r\n * also reach `simpleEval` / `createState`.\r\n */\r\nexport const createFieldContext = (\r\n formSource: SignalContextSource,\r\n fieldSource: SignalContextSource,\r\n options?: EvalOptions\r\n): EvalContext => {\r\n\r\n const context = createSignalContext(fieldSource, options);\r\n\r\n // The form half **borrows upstream's resolver rather than rewriting it**\r\n // (S 3.4.2, candidate A, settled in step 2). The second context is\r\n // discarded and only its closure survives, so this is still one\r\n // `EvalContext` per field and the field half is still consulted first.\r\n //\r\n // The deciding reason is row 2 of S 3.4.2's measured table, and it is that\r\n // the hand-written read is *wrong*, not merely thinner: it returns a signal\r\n // un-called, so a form key holding `signal(undefined)` resolves to the\r\n // signal **function**, which is truthy - and `visible: \"country\"` then\r\n // renders a field precisely when its value is absent, silently, on an empty\r\n // control. That is every form's first render. Recorded here because the\r\n // alternative was rejected on correctness and stays rejected even once\r\n // S 3.5 names a mechanism that keeps a plain-value record live.\r\n //\r\n // Borrowing also brings own-property semantics - so a server-supplied field\r\n // named `constructor` cannot resolve off `Object.prototype` *through this\r\n // half*, which is a narrower claim than it looks and no test reaches it:\r\n // `get` consults `original` before `lookups`, and `getContextValue` reads a\r\n // plain object as a bare property access, so an inherited name resolves\r\n // there first and never arrives here (S 3.4.3's third layer, upstream and\r\n // out of this phase's scope). It also brings `caseInsensitive` correction\r\n // and `warnOnNestedSignals` over the form source, with no import beyond the\r\n // one already here.\r\n //\r\n // The scan is the one cost: `createSignalContext` runs\r\n // `warnOnNestedSignals` unconditionally, so a form of N fields scans the\r\n // same form source N times and, in dev mode, would warn N times under\r\n // *upstream's* symbol name for a call this library made. Construction-time\r\n // only - no per-node or per-recompute path is touched.\r\n context.lookups.push(...createSignalContext(formSource, options).lookups);\r\n\r\n return context;\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACI,MAAM,SAAS,GAAG,CAAC,KAAc,KAAc,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;AAgBG;AACI,MAAM,MAAM,GAAG,CAAC,KAAc,KACnC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK;;AC3C3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CG;AACI,MAAM,kBAAkB,GAAG,CAChC,UAA+B,EAC/B,WAAgC,EAChC,OAAqB,KACN;IAEf,MAAM,OAAO,GAAG,mBAAmB,CAAC,WAAW,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BzD,IAAA,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC;AAEzE,IAAA,OAAO,OAAO;AAChB;;ACtFA;;AAEG;;;;"}
|
|
1
|
+
{"version":3,"file":"zvenigora-ng-eval-forms.mjs","sources":["../../../../modules/eval-forms/src/lib/coercion.ts","../../../../modules/eval-forms/src/lib/error-policy.ts","../../../../modules/eval-forms/src/lib/field-context.ts","../../../../modules/eval-forms/src/zvenigora-ng-eval-forms.ts"],"sourcesContent":["/**\r\n * Coerces an expression's result to the boolean a `visible` rule needs.\r\n *\r\n * **JavaScript truthiness, and nothing cleverer** (plan S 3.6). A rule author\r\n * writing `visible: \"country === 'US'\"` gets a boolean already; the coercion\r\n * exists for the ones who write `visible: \"country\"`.\r\n *\r\n * `undefined` is therefore the load-bearing input rather than an edge case.\r\n * An *empty* field and a *missing* field are indistinguishable here - S 3.4.3:\r\n * a field key bound to `undefined` is treated as absent, and an empty\r\n * `FormControl` is the common case - so both arrive as `undefined`, and both\r\n * mean \"not visible\".\r\n *\r\n * The string `'false'` is **visible**, because it is a non-empty string. That\r\n * is truthiness rather than parsing, and it is the answer a form-builder UI\r\n * storing every value as a string will hit. Documented in the README rather\r\n * than special-cased: a coercion that parsed `'false'` would then owe an\r\n * answer for `'no'`, `'0'` and `'off'`, none of which JavaScript has one for.\r\n *\r\n * Both adapters call this, which is why it is in the core: `/reactive` and\r\n * `/signals` differ in how they schedule the recompute and not in what the\r\n * result means (S 3.4.5).\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns `true` when the value is truthy.\r\n */\r\nexport const toVisible = (value: unknown): boolean => !!value;\r\n\r\n/**\r\n * Coerces an expression's result to the string a `text` rule needs.\r\n *\r\n * `String(value)`, with `null` and `undefined` mapping to `''` (plan S 3.6).\r\n * Those two are the carve-out and the only one: rendering the literal text\r\n * `undefined` into a label is the failure this rule exists to prevent, and it\r\n * is what a bare `String(value)` would do for the most ordinary input a form\r\n * has - the empty control of S 3.4.3.\r\n *\r\n * Every *other* falsy value stringifies normally: `0` is `'0'` and `false` is\r\n * `'false'`. Mapping all falsy values to `''` would be the obvious way to\r\n * write this rule wrongly, and it would blank a field whose value is\r\n * legitimately zero.\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns The value as a string, or `''` when it is `null` or `undefined`.\r\n */\r\nexport const toText = (value: unknown): string =>\r\n value === null || value === undefined ? '' : String(value);\r\n","// A **value** import, and it has to stay one: `instanceof` needs the\r\n// constructor, not the type. `field-context.ts` already imports a value from\r\n// this package, so this adds no dependency edge.\r\n//\r\n// **The compiler enforces the form, and the plan said otherwise** - S 3.4 and\r\n// S 8.1 both call a type-only import here a change that \"would compile and the\r\n// guard would silently never fire\". Measured in step 3: `import type` is\r\n// **TS1361** at `applyErrorPolicy`'s `instanceof`, so it fails\r\n// `build:production` and gate 7, and under ts-jest's transpile path the elided\r\n// binding is a `ReferenceError` inside the `catch` that reddens *both* policy\r\n// arms. Loud at every gate, in other words - see the plan's revision 13.\r\n//\r\n// The failure mode that *is* silent is a different one and no import syntax\r\n// prevents it: two resolved copies of `@zvenigora/ng-eval-signals` give two\r\n// distinct constructors, and `instanceof` is then false for an error the other\r\n// copy threw. That is what the write-error arm of `evaluate-rule.spec.ts`\r\n// covers by throwing across the real package boundary rather than\r\n// constructing the error itself.\r\nimport { SignalContextWriteError } from '@zvenigora/ng-eval-signals';\r\n\r\n/**\r\n * What a field property does when its expression throws at runtime.\r\n *\r\n * - `'throw'` - rethrow, matching `eval-signals`' own default.\r\n * - `'undefined'` - the property resolves `undefined`, which `toVisible`\r\n * reads as not visible and `toText` reads as `''`.\r\n * - a function - called with the error; its return value becomes the\r\n * property's value.\r\n *\r\n * **The default here is `'undefined'`, the opposite of `eval-signals`'**, and\r\n * the consumer is the reason (plan S 3.4.4). An expression that fails in\r\n * `eval-signals` was written by the developer reading the stack trace. An\r\n * expression that fails here may have been typed into a form builder by an\r\n * end user, and the right response to \"the admin wrote a bad rule\" is a field\r\n * that does not render, not an application that throws on every change\r\n * detection pass. Consumers who want the strict behaviour pass `'throw'`.\r\n *\r\n * Resolving that default is a real step and not a formality: `createEvalSignal`\r\n * does `options?.onError ?? 'throw'`, so forwarding an *absent* policy verbatim\r\n * inherits `'throw'` - the opposite of this default. The `/reactive` binding\r\n * substitutes its own before the call.\r\n *\r\n * The union is structurally identical to `EvalSignalOptions['onError']`\r\n * **deliberately**: `/reactive` resolves the default here and forwards the\r\n * value to `createEvalSignal` with no mapping between the two unions, which is\r\n * where two near-identical types would drift apart.\r\n *\r\n * `SignalContextWriteError` is **not** routed through this, in either adapter,\r\n * matching `phase-3-plan.md` S 3.6.3. A write violation is static - illegal on\r\n * every recompute with every dataset - and swallowing it under a default of\r\n * `'undefined'` would hand every consumer a silent blank for a bug in the\r\n * rule's own syntax. `createEvalSignal` already bypasses `onError` for it.\r\n *\r\n * **{@link applyErrorPolicy} ships beside this type as of Phase 6** (its\r\n * S 3.4). Through Phase 4 only the type shipped, because the helper would have\r\n * had no caller: on the `/reactive` path - the only path Phase 4 shipped -\r\n * `createEvalSignal` applies the three cases internally, and shipping an\r\n * unexercised code path is what S 9.1 declined to do for the scope-leak\r\n * containment. The `/signals` adapter is that caller **from step 4**, where\r\n * the registrars stop being stubs; `/reactive` does not use the helper at all.\r\n */\r\nexport type ExpressionErrorPolicy =\r\n | 'throw'\r\n | 'undefined'\r\n | ((error: unknown) => unknown);\r\n\r\n/**\r\n * Runs one rule under one {@link ExpressionErrorPolicy}, with the one error\r\n * that must never be routed through it taken out first.\r\n *\r\n * ```ts\r\n * applyErrorPolicy(() => evaluateRule(compiled, context, options), onError)\r\n * ```\r\n *\r\n * **`SignalContextWriteError` re-throws in every mode, including a handler\r\n * function** (plan S 3.4, 1.2.11). On `/reactive` that bypass came free -\r\n * `createEvalSignal` re-throws it regardless of `onError` (`eval-signal.ts:353`)\r\n * - and that behaviour lives *inside* `createEvalSignal`, which the `/signals`\r\n * path never calls. It does not travel with the type, so it is re-implemented\r\n * here or it does not exist. An assigning expression is illegal on every\r\n * recompute with every dataset, so under this module's default of\r\n * `'undefined'` it would otherwise render as a permanently blank field with\r\n * nothing in the console.\r\n *\r\n * **The `instanceof` needs the constructor, so the import of that class is a\r\n * *value* import** - see the note above it, and the plan's revision 13, for\r\n * why that form is protected by the compiler rather than by this spec.\r\n * `evaluate-rule.spec.ts`'s write-error arm asserts on the **class** rather\r\n * than on a message because what it covers is the bypass's *behaviour* across\r\n * a real package boundary, which no compiler check reaches.\r\n *\r\n * **It lives in the core while `/signals`' choke point does not, and the pair\r\n * is not inconsistent** (S 3.4.1). The discriminator is what the symbol grants\r\n * a caller who reaches it: `evaluateRule` published *is* a second path to the\r\n * walk, which S 9.1 requires there be only one of, while this is a pure\r\n * function over an error and a policy that takes no context, holds no compiled\r\n * callback and cannot reach a walk. Publishing it grants a caller nothing they\r\n * could not write in four lines, and buys the thing duplication would lose -\r\n * one implementation of the write-error bypass for both adapters, which cannot\r\n * then drift into disagreeing about the one error that must never be\r\n * swallowed.\r\n *\r\n * **It re-throws the error it caught; `/reactive` re-wraps.** `createEvalSignal`\r\n * throws a *new* `SignalContextWriteError` carrying the offending expression\r\n * string, which it has and this path does not - a `LogicFn` holds a compiled\r\n * callback, not the source text. So the same misuse surfaces a different\r\n * object at each entry point: wrapped with the expression under `/reactive`,\r\n * the original here. Stated rather than hidden; it is the one thing a consumer\r\n * catching this error across both adapters would notice.\r\n *\r\n * @param run - The rule invocation. On `/signals` this is the choke point,\r\n * never a bare walk: containment is inside `run`, so a throw is\r\n * contained before this function decides what to do about it.\r\n * @param policy - Defaults to `'undefined'`, per {@link ExpressionErrorPolicy}.\r\n * Resolving the default *here* is what makes an absent policy\r\n * mean `'undefined'` rather than inheriting `eval-signals`'\r\n * opposite one.\r\n */\r\nexport const applyErrorPolicy = <T>(\r\n run: () => T,\r\n policy: ExpressionErrorPolicy = 'undefined'\r\n): T | undefined => {\r\n\r\n try {\r\n return run();\r\n } catch (error) {\r\n if (error instanceof SignalContextWriteError) {\r\n throw error;\r\n }\r\n\r\n if (policy === 'throw') {\r\n throw error;\r\n }\r\n\r\n if (policy === 'undefined') {\r\n return undefined;\r\n }\r\n\r\n return policy(error) as T | undefined;\r\n }\r\n};\r\n","import { EvalContext, EvalOptions } from '@zvenigora/ng-eval-core';\r\nimport { SignalContextSource, createSignalContext } from '@zvenigora/ng-eval-signals';\r\n\r\n/**\r\n * Composes one `EvalContext` for one field, out of two live sources.\r\n *\r\n * The mechanism is **two lookups, not a joined record** (plan S 3.4.2). The\r\n * field half is a `createSignalContext` over `fieldSource`; the form half is a\r\n * second resolver pushed onto the same context's `lookups`. Both close over\r\n * their source and read it at resolve time, so a key added to either after\r\n * construction resolves - there is no third object to keep in sync, and no\r\n * write-through to N contexts.\r\n *\r\n * `EvalContext.get` walks `lookups` in order and takes the first\r\n * non-`undefined`, so the field half is consulted first.\r\n *\r\n * One context per field, never one shared across fields (plan S 3.4.1):\r\n * `get` resolves `scopes` and `original` *before* `lookups`, so anything left\r\n * on a shared context - an arrow function's leaked scope, say - would be read\r\n * ahead of every other field's source key of the same name, for the life of\r\n * the form.\r\n *\r\n * **Resolution is live; appearance is not reactive.** An expression that read\r\n * a then-missing key subscribed to nothing, so the owner of the source calls\r\n * `invalidate()` when the *key set* changes. A change to a *value* is\r\n * Angular's job and needs nothing.\r\n *\r\n * **Precedence is value-dependent, not permanent.** A field key wins while\r\n * its value is *present*: `EvalContext.get` treats `undefined` as absent at\r\n * every step, and the field resolver returns `undefined` both for \"no such\r\n * key\" and for \"key bound to `undefined`\" - so an empty `FormControl`, which\r\n * is the common case rather than an exotic one, falls through to the form\r\n * value. Documented as a limitation (plan S 3.4.3): distinguishing the two\r\n * would need a sentinel threaded through `EvalContext.get`, which is\r\n * `eval-core`'s and out of this phase's scope.\r\n *\r\n * @param formSource - The form-wide keys, shared by every field of the form.\r\n * @param fieldSource - The field-local keys. These win on a name collision,\r\n * while their value is not `undefined`.\r\n * @param options - Passed to `createSignalContext` for **both** halves;\r\n * configures this context and its resolvers, not the walk.\r\n * As upstream, `caseInsensitive` corrects identifier keys\r\n * here but not *property* names - the member visitor reads\r\n * those off the state's options, so the same options must\r\n * also reach `simpleEval` / `createState`.\r\n */\r\nexport const createFieldContext = (\r\n formSource: SignalContextSource,\r\n fieldSource: SignalContextSource,\r\n options?: EvalOptions\r\n): EvalContext => {\r\n\r\n const context = createSignalContext(fieldSource, options);\r\n\r\n // The form half **borrows upstream's resolver rather than rewriting it**\r\n // (S 3.4.2, candidate A, settled in step 2). The second context is\r\n // discarded and only its closure survives, so this is still one\r\n // `EvalContext` per field and the field half is still consulted first.\r\n //\r\n // The deciding reason is row 2 of S 3.4.2's measured table, and it is that\r\n // the hand-written read is *wrong*, not merely thinner: it returns a signal\r\n // un-called, so a form key holding `signal(undefined)` resolves to the\r\n // signal **function**, which is truthy - and `visible: \"country\"` then\r\n // renders a field precisely when its value is absent, silently, on an empty\r\n // control. That is every form's first render. Recorded here because the\r\n // alternative was rejected on correctness and stays rejected even once\r\n // S 3.5 names a mechanism that keeps a plain-value record live.\r\n //\r\n // Borrowing also brings own-property semantics - so a server-supplied field\r\n // named `constructor` cannot resolve off `Object.prototype` *through this\r\n // half*, which is a narrower claim than it looks and no test reaches it:\r\n // `get` consults `original` before `lookups`, and `getContextValue` reads a\r\n // plain object as a bare property access, so an inherited name resolves\r\n // there first and never arrives here (S 3.4.3's third layer, upstream and\r\n // out of this phase's scope). It also brings `caseInsensitive` correction\r\n // and `warnOnNestedSignals` over the form source, with no import beyond the\r\n // one already here.\r\n //\r\n // The scan is the one cost: `createSignalContext` runs\r\n // `warnOnNestedSignals` unconditionally, so a form of N fields scans the\r\n // same form source N times and, in dev mode, would warn N times under\r\n // *upstream's* symbol name for a call this library made. Construction-time\r\n // only - no per-node or per-recompute path is touched.\r\n context.lookups.push(...createSignalContext(formSource, options).lookups);\r\n\r\n return context;\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACI,MAAM,SAAS,GAAG,CAAC,KAAc,KAAc,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;AAgBG;AACI,MAAM,MAAM,GAAG,CAAC,KAAc,KACnC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK;;AC9C3D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAiDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDG;AACI,MAAM,gBAAgB,GAAG,CAC9B,GAAY,EACZ,MAAA,GAAgC,WAAW,KAC1B;AAEjB,IAAA,IAAI;QACF,OAAO,GAAG,EAAE;IACd;IAAE,OAAO,KAAK,EAAE;AACd,QAAA,IAAI,KAAK,YAAY,uBAAuB,EAAE;AAC5C,YAAA,MAAM,KAAK;QACb;AAEA,QAAA,IAAI,MAAM,KAAK,OAAO,EAAE;AACtB,YAAA,MAAM,KAAK;QACb;AAEA,QAAA,IAAI,MAAM,KAAK,WAAW,EAAE;AAC1B,YAAA,OAAO,SAAS;QAClB;AAEA,QAAA,OAAO,MAAM,CAAC,KAAK,CAAkB;IACvC;AACF;;ACzIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CG;AACI,MAAM,kBAAkB,GAAG,CAChC,UAA+B,EAC/B,WAAgC,EAChC,OAAqB,KACN;IAEf,MAAM,OAAO,GAAG,mBAAmB,CAAC,WAAW,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BzD,IAAA,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC;AAEzE,IAAA,OAAO,OAAO;AAChB;;ACtFA;;AAEG;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvenigora/ng-eval-forms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Angular form field properties from ng-eval expressions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,9 +18,13 @@
|
|
|
18
18
|
"@angular/core": ">=19.0.0",
|
|
19
19
|
"@angular/forms": ">=19.0.0",
|
|
20
20
|
"rxjs": "^7.8.0",
|
|
21
|
+
"acorn-walk": "^8.3.0",
|
|
21
22
|
"@zvenigora/ng-eval-core": "^0.3.0",
|
|
22
23
|
"@zvenigora/ng-eval-signals": "^0.1.0"
|
|
23
24
|
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
24
28
|
"sideEffects": false,
|
|
25
29
|
"module": "fesm2022/zvenigora-ng-eval-forms.mjs",
|
|
26
30
|
"typings": "types/zvenigora-ng-eval-forms.d.ts",
|
|
@@ -35,6 +39,10 @@
|
|
|
35
39
|
"./reactive": {
|
|
36
40
|
"types": "./types/zvenigora-ng-eval-forms-reactive.d.ts",
|
|
37
41
|
"default": "./fesm2022/zvenigora-ng-eval-forms-reactive.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./signals": {
|
|
44
|
+
"types": "./types/zvenigora-ng-eval-forms-signals.d.ts",
|
|
45
|
+
"default": "./fesm2022/zvenigora-ng-eval-forms-signals.mjs"
|
|
38
46
|
}
|
|
39
47
|
},
|
|
40
48
|
"type": "module",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { WritableSignal } from '@angular/core';
|
|
3
|
+
import * as _angular_forms_signals from '@angular/forms/signals';
|
|
4
|
+
import { PathKind, SchemaPath, SchemaPathRules } from '@angular/forms/signals';
|
|
5
|
+
import { EvalOptions } from '@zvenigora/ng-eval-core';
|
|
6
|
+
import { ExpressionErrorPolicy } from '@zvenigora/ng-eval-forms';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What `createExpressionRules` takes, and what a single registration may
|
|
10
|
+
* override (plan S 5).
|
|
11
|
+
*/
|
|
12
|
+
interface ExpressionRuleOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Passed to the context and to the walk. `caseInsensitive` has to reach
|
|
15
|
+
* both: on the context it corrects identifier keys, and on the walk it
|
|
16
|
+
* corrects *property* names, which the member visitor reads off the
|
|
17
|
+
* state's options.
|
|
18
|
+
*
|
|
19
|
+
* **A per-registration value reaches exactly one of the three places it
|
|
20
|
+
* has to: the walk** (plan S 3.5.3). The other two read the **factory's**
|
|
21
|
+
* options, which are fixed when `createExpressionRules` is called: the memo
|
|
22
|
+
* is built once, there; the context is minted per registration by
|
|
23
|
+
* `createRuleContext()` but always from that same fixed setting, so a
|
|
24
|
+
* registration cannot move it. So overriding this per registration corrects
|
|
25
|
+
* *property* names and leaves *identifier* keys on the factory's setting,
|
|
26
|
+
* and one expression then obeys two casing rules. Set `caseInsensitive` on
|
|
27
|
+
* the **factory** unless that is the behaviour you want.
|
|
28
|
+
*
|
|
29
|
+
* Revision 19 item 2 corrects "both are made once, at
|
|
30
|
+
* `createExpressionRules` time", which was wrong about the context and is
|
|
31
|
+
* the claim the README states correctly.
|
|
32
|
+
*/
|
|
33
|
+
eval?: EvalOptions;
|
|
34
|
+
/**
|
|
35
|
+
* What a field property does when its expression throws at runtime.
|
|
36
|
+
* Defaults to `'undefined'` here, the opposite of `eval-signals`' own
|
|
37
|
+
* default - see `ExpressionErrorPolicy` for why.
|
|
38
|
+
*/
|
|
39
|
+
onError?: ExpressionErrorPolicy;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The three registrars one factory returns, one per Angular rule.
|
|
43
|
+
*
|
|
44
|
+
* **One call per Angular primitive, never an aggregate** (plan S 3.5.1). A
|
|
45
|
+
* single `evalRules(p.x, { visible, text, disabled })` would register three
|
|
46
|
+
* different Angular rules behind one name, hiding which one each property
|
|
47
|
+
* maps to - which is the `/signals` reviewer checklist's item 4 expressed as
|
|
48
|
+
* an API.
|
|
49
|
+
*
|
|
50
|
+
* Named after the **property**, not after Angular's rule: `evalVisible`
|
|
51
|
+
* registers `hidden`, inverted once inside the registrar rather than in every
|
|
52
|
+
* consumer's expression, so the same expression string means the same thing
|
|
53
|
+
* at this entry point and at `/reactive`.
|
|
54
|
+
*/
|
|
55
|
+
interface ExpressionRules {
|
|
56
|
+
/** Registers Angular's `hidden`, inverted (plan S 3.5.1). */
|
|
57
|
+
evalVisible: <TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, expression: string, options?: ExpressionRuleOptions) => void;
|
|
58
|
+
/** Registers `metadata(path, TEXT, …)` (plan S 1.2.8, S 3.5). */
|
|
59
|
+
evalText: <TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, expression: string, options?: ExpressionRuleOptions) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Registers Angular's `disabled`.
|
|
62
|
+
*
|
|
63
|
+
* The `reason` is authored as a static string and is never
|
|
64
|
+
* expression-derived (plan S 3.5.2). Angular's `when` returns
|
|
65
|
+
* `boolean | string` and a truthy string is *both* "disabled" and "the
|
|
66
|
+
* reason", so a rule yielding `'false'` would otherwise disable the field
|
|
67
|
+
* with the reason `"false"` - the `toVisible` truthiness trap in a new
|
|
68
|
+
* shape. Keeping the expression boolean and the reason static is what kills
|
|
69
|
+
* it.
|
|
70
|
+
*/
|
|
71
|
+
evalDisabled: <TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, expression: string, options?: ExpressionRuleOptions & {
|
|
72
|
+
reason?: string;
|
|
73
|
+
}) => void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Binds one model signal and returns the three expression-driven registrars.
|
|
77
|
+
*
|
|
78
|
+
* **A factory rather than free functions, because a `LogicFn` cannot recover
|
|
79
|
+
* the source** (plan S 3.2.1). `RootFieldContext` exposes the *current*
|
|
80
|
+
* field's node plus compile-time-token accessors and no root or parent
|
|
81
|
+
* handle, so a rule on `p.city` evaluating `country === "US"` has no route to
|
|
82
|
+
* `country` from inside the `LogicFn`. The source must be closed over at
|
|
83
|
+
* registration or it is unreachable.
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const rules = createExpressionRules(model);
|
|
87
|
+
* const s = schema<Model>((p) => {
|
|
88
|
+
* required(p.email); // Angular's
|
|
89
|
+
* rules.evalVisible(p.city, 'country === "US"'); // ours
|
|
90
|
+
* });
|
|
91
|
+
* const f = form(model, s);
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* **A schema *value* shared across models is the unsupported shape.** Nothing
|
|
95
|
+
* stops two `form()` calls from one schema - Angular re-invokes the schema
|
|
96
|
+
* body once per `form()`, so each form mints its own contexts - but the
|
|
97
|
+
* registrars close over the **factory's** model, and the factory is bound to
|
|
98
|
+
* one. A schema built from `createExpressionRules(modelA)` and reused for
|
|
99
|
+
* `form(modelB, s)` re-registers every rule and every one of them still reads
|
|
100
|
+
* model A: form B renders against form A's data, silently, with no error and
|
|
101
|
+
* a fully functional form (Q9).
|
|
102
|
+
*
|
|
103
|
+
* The supported reuse shape is therefore a schema **function of the rules** -
|
|
104
|
+
* `const makeSchema = (rules) => schema<Model>(p => …)`, called per form -
|
|
105
|
+
* which keeps reuse while giving each form a factory bound to its own model.
|
|
106
|
+
*
|
|
107
|
+
* What one factory retains: **one private memo**, per factory, bounded by the
|
|
108
|
+
* union of keys the rules mention; and, per rule per `form()`, one
|
|
109
|
+
* `EvalContext` and one compiled callback. Nothing registers with a
|
|
110
|
+
* `DestroyRef` and there is no `destroy()` - it all becomes garbage with the
|
|
111
|
+
* form (S 3.6).
|
|
112
|
+
*/
|
|
113
|
+
declare const createExpressionRules: <TModel extends object>(model: WritableSignal<TModel>, options?: ExpressionRuleOptions) => ExpressionRules;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The metadata key `evalText` writes through and a consumer reads back
|
|
117
|
+
* (plan S 1.2.8, S 3.5).
|
|
118
|
+
*
|
|
119
|
+
* `text` has no dedicated primitive in `@angular/forms/signals` the way
|
|
120
|
+
* `hidden` and `disabled` do, so it is `metadata(path, TEXT, logic)` against a
|
|
121
|
+
* key created once - which is Angular's own mechanism rather than a second one
|
|
122
|
+
* beside it, per the `/signals` reviewer checklist's item 4.
|
|
123
|
+
*
|
|
124
|
+
* **Created once, at module scope, deliberately.** `createMetadataKey` mints a
|
|
125
|
+
* fresh key per call, and the write and the read-back have to name the same
|
|
126
|
+
* object: a key re-created per access would let `metadata(p.x, TEXT, …)` write
|
|
127
|
+
* under one identity and `f.x().metadata(TEXT)` read under another, and the
|
|
128
|
+
* read would be `undefined` with nothing to show why. `text-key.spec.ts`
|
|
129
|
+
* asserts the two halves of that - `createMetadataKey` is per-call, and this
|
|
130
|
+
* is one of its results rather than the function itself.
|
|
131
|
+
*/
|
|
132
|
+
declare const TEXT: _angular_forms_signals.MetadataKey<_angular_core.Signal<string | undefined>, string, string | undefined>;
|
|
133
|
+
|
|
134
|
+
export { TEXT, createExpressionRules };
|
|
135
|
+
export type { ExpressionRuleOptions, ExpressionRules };
|
|
@@ -80,15 +80,68 @@ declare const toText: (value: unknown) => string;
|
|
|
80
80
|
* `'undefined'` would hand every consumer a silent blank for a bug in the
|
|
81
81
|
* rule's own syntax. `createEvalSignal` already bypasses `onError` for it.
|
|
82
82
|
*
|
|
83
|
-
* **
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
83
|
+
* **{@link applyErrorPolicy} ships beside this type as of Phase 6** (its
|
|
84
|
+
* S 3.4). Through Phase 4 only the type shipped, because the helper would have
|
|
85
|
+
* had no caller: on the `/reactive` path - the only path Phase 4 shipped -
|
|
86
|
+
* `createEvalSignal` applies the three cases internally, and shipping an
|
|
87
|
+
* unexercised code path is what S 9.1 declined to do for the scope-leak
|
|
88
|
+
* containment. The `/signals` adapter is that caller **from step 4**, where
|
|
89
|
+
* the registrars stop being stubs; `/reactive` does not use the helper at all.
|
|
90
90
|
*/
|
|
91
91
|
type ExpressionErrorPolicy = 'throw' | 'undefined' | ((error: unknown) => unknown);
|
|
92
|
+
/**
|
|
93
|
+
* Runs one rule under one {@link ExpressionErrorPolicy}, with the one error
|
|
94
|
+
* that must never be routed through it taken out first.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* applyErrorPolicy(() => evaluateRule(compiled, context, options), onError)
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* **`SignalContextWriteError` re-throws in every mode, including a handler
|
|
101
|
+
* function** (plan S 3.4, 1.2.11). On `/reactive` that bypass came free -
|
|
102
|
+
* `createEvalSignal` re-throws it regardless of `onError` (`eval-signal.ts:353`)
|
|
103
|
+
* - and that behaviour lives *inside* `createEvalSignal`, which the `/signals`
|
|
104
|
+
* path never calls. It does not travel with the type, so it is re-implemented
|
|
105
|
+
* here or it does not exist. An assigning expression is illegal on every
|
|
106
|
+
* recompute with every dataset, so under this module's default of
|
|
107
|
+
* `'undefined'` it would otherwise render as a permanently blank field with
|
|
108
|
+
* nothing in the console.
|
|
109
|
+
*
|
|
110
|
+
* **The `instanceof` needs the constructor, so the import of that class is a
|
|
111
|
+
* *value* import** - see the note above it, and the plan's revision 13, for
|
|
112
|
+
* why that form is protected by the compiler rather than by this spec.
|
|
113
|
+
* `evaluate-rule.spec.ts`'s write-error arm asserts on the **class** rather
|
|
114
|
+
* than on a message because what it covers is the bypass's *behaviour* across
|
|
115
|
+
* a real package boundary, which no compiler check reaches.
|
|
116
|
+
*
|
|
117
|
+
* **It lives in the core while `/signals`' choke point does not, and the pair
|
|
118
|
+
* is not inconsistent** (S 3.4.1). The discriminator is what the symbol grants
|
|
119
|
+
* a caller who reaches it: `evaluateRule` published *is* a second path to the
|
|
120
|
+
* walk, which S 9.1 requires there be only one of, while this is a pure
|
|
121
|
+
* function over an error and a policy that takes no context, holds no compiled
|
|
122
|
+
* callback and cannot reach a walk. Publishing it grants a caller nothing they
|
|
123
|
+
* could not write in four lines, and buys the thing duplication would lose -
|
|
124
|
+
* one implementation of the write-error bypass for both adapters, which cannot
|
|
125
|
+
* then drift into disagreeing about the one error that must never be
|
|
126
|
+
* swallowed.
|
|
127
|
+
*
|
|
128
|
+
* **It re-throws the error it caught; `/reactive` re-wraps.** `createEvalSignal`
|
|
129
|
+
* throws a *new* `SignalContextWriteError` carrying the offending expression
|
|
130
|
+
* string, which it has and this path does not - a `LogicFn` holds a compiled
|
|
131
|
+
* callback, not the source text. So the same misuse surfaces a different
|
|
132
|
+
* object at each entry point: wrapped with the expression under `/reactive`,
|
|
133
|
+
* the original here. Stated rather than hidden; it is the one thing a consumer
|
|
134
|
+
* catching this error across both adapters would notice.
|
|
135
|
+
*
|
|
136
|
+
* @param run - The rule invocation. On `/signals` this is the choke point,
|
|
137
|
+
* never a bare walk: containment is inside `run`, so a throw is
|
|
138
|
+
* contained before this function decides what to do about it.
|
|
139
|
+
* @param policy - Defaults to `'undefined'`, per {@link ExpressionErrorPolicy}.
|
|
140
|
+
* Resolving the default *here* is what makes an absent policy
|
|
141
|
+
* mean `'undefined'` rather than inheriting `eval-signals`'
|
|
142
|
+
* opposite one.
|
|
143
|
+
*/
|
|
144
|
+
declare const applyErrorPolicy: <T>(run: () => T, policy?: ExpressionErrorPolicy) => T | undefined;
|
|
92
145
|
|
|
93
146
|
/**
|
|
94
147
|
* Composes one `EvalContext` for one field, out of two live sources.
|
|
@@ -135,5 +188,5 @@ type ExpressionErrorPolicy = 'throw' | 'undefined' | ((error: unknown) => unknow
|
|
|
135
188
|
*/
|
|
136
189
|
declare const createFieldContext: (formSource: SignalContextSource, fieldSource: SignalContextSource, options?: EvalOptions) => EvalContext;
|
|
137
190
|
|
|
138
|
-
export { createFieldContext, toText, toVisible };
|
|
191
|
+
export { applyErrorPolicy, createFieldContext, toText, toVisible };
|
|
139
192
|
export type { ExpressionErrorPolicy };
|