assign-gingerly 0.0.85 → 0.0.86
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/DX/emojis.js +5 -3
- package/DX/emojis.ts +5 -3
- package/DX/paths.js +2 -2
- package/DX/paths.ts +23 -23
- package/README.md +34 -39
- package/assignFrom.js +78 -2
- package/assignFrom.ts +80 -2
- package/inferencer/types/NewCustomElementFeature.md +3 -1
- package/inferencer/types/NewHTMLFirstCustomElement.md +2 -0
- package/inferencer/types/assign-gingerly/types.d.ts +43 -5
- package/package.json +9 -4
- package/processHandlerCommands.js +5 -55
- package/processHandlerCommands.ts +6 -51
- package/{handlers → syncOps}/join.js +61 -73
- package/syncOps/join.ts +62 -0
- package/syncOps/registry.js +15 -0
- package/syncOps/registry.ts +19 -0
- package/utils/resolveLhsPath.js +68 -0
- package/utils/resolveLhsPath.ts +81 -0
- package/handlers/join.ts +0 -79
package/DX/emojis.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
8
8
|
*
|
|
9
9
|
* assignFrom(target, {
|
|
10
|
-
* '?.el =>': { do: '
|
|
10
|
+
* '?.el =>': { do: '🏷️', get: { template: [...] } }
|
|
11
11
|
* }, { from: vm, handlers: builtInEmoji });
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
* |-------|---------|
|
|
18
18
|
* | 📦 | builtIns.lazyLoad |
|
|
19
19
|
* | 🎚️ | builtIns.lazyLoadSwitch |
|
|
20
|
-
* | 🔗 | builtIns.join |
|
|
21
20
|
* | 🏷️ | builtIns.microDataJoin |
|
|
22
21
|
* | 📋 | builtIns.manageTemplateList |
|
|
22
|
+
*
|
|
23
|
+
* `join` is no longer a `do:` handler — it moved to the synchronous ` =&` operator
|
|
24
|
+
* (see syncOps/join.ts) and isn't looked up through options.handlers, so it has no
|
|
25
|
+
* emoji alias here.
|
|
23
26
|
*/
|
|
24
27
|
export const builtInEmoji = {
|
|
25
28
|
'📦': 'builtIns.lazyLoad',
|
|
26
29
|
'🎚️': 'builtIns.lazyLoadSwitch',
|
|
27
|
-
'🔗': 'builtIns.join',
|
|
28
30
|
'🏷️': 'builtIns.microDataJoin',
|
|
29
31
|
'📋': 'builtIns.manageTemplateList',
|
|
30
32
|
'📊': 'builtIns.rangeSelector',
|
package/DX/emojis.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
8
8
|
*
|
|
9
9
|
* assignFrom(target, {
|
|
10
|
-
* '?.el =>': { do: '
|
|
10
|
+
* '?.el =>': { do: '🏷️', get: { template: [...] } }
|
|
11
11
|
* }, { from: vm, handlers: builtInEmoji });
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -18,14 +18,16 @@
|
|
|
18
18
|
* |-------|---------|
|
|
19
19
|
* | 📦 | builtIns.lazyLoad |
|
|
20
20
|
* | 🎚️ | builtIns.lazyLoadSwitch |
|
|
21
|
-
* | 🔗 | builtIns.join |
|
|
22
21
|
* | 🏷️ | builtIns.microDataJoin |
|
|
23
22
|
* | 📋 | builtIns.manageTemplateList |
|
|
23
|
+
*
|
|
24
|
+
* `join` is no longer a `do:` handler — it moved to the synchronous ` =&` operator
|
|
25
|
+
* (see syncOps/join.ts) and isn't looked up through options.handlers, so it has no
|
|
26
|
+
* emoji alias here.
|
|
24
27
|
*/
|
|
25
28
|
export const builtInEmoji= {
|
|
26
29
|
'📦': 'builtIns.lazyLoad',
|
|
27
30
|
'🎚️': 'builtIns.lazyLoadSwitch',
|
|
28
|
-
'🔗': 'builtIns.join',
|
|
29
31
|
'🏷️': 'builtIns.microDataJoin',
|
|
30
32
|
'📋': 'builtIns.manageTemplateList',
|
|
31
33
|
'📊': 'builtIns.rangeSelector',
|
package/DX/paths.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* const $ = paths<Person>();
|
|
17
17
|
*
|
|
18
|
-
* // Use sp (split into parts) to create arrays for
|
|
18
|
+
* // Use sp (split into parts) to create arrays for the ' =&' join op:
|
|
19
19
|
* const value = sp`${$.lastName}, ${$.firstName}`;
|
|
20
20
|
* // ['?.lastName', ', ', '?.firstName']
|
|
21
21
|
*
|
|
@@ -321,7 +321,7 @@ export function forEachKeyIn(keys, factory, options) {
|
|
|
321
321
|
* string representation — no `.Path` call needed inside sp template literals.
|
|
322
322
|
*
|
|
323
323
|
* Arrays passed as interpolations are preserved as nested arrays (for
|
|
324
|
-
* all-or-nothing optional segments in
|
|
324
|
+
* all-or-nothing optional segments in the ' =&' join op).
|
|
325
325
|
*
|
|
326
326
|
* @example
|
|
327
327
|
* const $ = paths<Person>();
|
package/DX/paths.ts
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
*
|
|
16
16
|
* const $ = paths<Person>();
|
|
17
17
|
*
|
|
18
|
-
* // Use sp (split into parts) to create arrays for
|
|
18
|
+
* // Use sp (split into parts) to create arrays for the ' =&' join op:
|
|
19
19
|
* const value = sp`${$.lastName}, ${$.firstName}`;
|
|
20
20
|
* // ['?.lastName', ', ', '?.firstName']
|
|
21
21
|
*
|
|
22
|
-
* // Use .Path for raw string contexts (object keys, plain arrays):
|
|
23
|
-
* const key = $.textContent.Path; // '?.textContent'
|
|
22
|
+
* // Use .Path for raw string contexts (object keys, plain arrays):
|
|
23
|
+
* const key = $.textContent.Path; // '?.textContent'
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -102,25 +102,25 @@ export interface PathsOptions {
|
|
|
102
102
|
/**
|
|
103
103
|
* Create a proxy for id-ref paths (#[varName]).
|
|
104
104
|
* After the initial #[varName], further property access chains with ?. from the resolved element.
|
|
105
|
-
* .Path returns the #[varName] prefix (optionally with further ?. path).
|
|
105
|
+
* .Path returns the #[varName] prefix (optionally with further ?. path).
|
|
106
106
|
*/
|
|
107
107
|
function createIdRefProxy(idRef: string, options?: PathsOptions): any {
|
|
108
108
|
function handler() {}
|
|
109
109
|
Object.defineProperty(handler, PATH_SYMBOL, { value: idRef });
|
|
110
110
|
return new Proxy(handler, {
|
|
111
111
|
get(_, prop: string | symbol) {
|
|
112
|
-
if (prop === 'Path' || prop === PATH_SYMBOL) {
|
|
113
|
-
return idRef;
|
|
114
|
-
}
|
|
112
|
+
if (prop === 'Path' || prop === PATH_SYMBOL) {
|
|
113
|
+
return idRef;
|
|
114
|
+
}
|
|
115
115
|
if (typeof prop === 'symbol') return undefined;
|
|
116
116
|
|
|
117
117
|
const reservedToken = getReservedToken(prop);
|
|
118
118
|
if (reservedToken === 'Each') {
|
|
119
119
|
return createIdRefProxy(appendPathSegment(idRef, '@each'), options);
|
|
120
120
|
}
|
|
121
|
-
if (reservedToken && reservedToken !== 'Path') {
|
|
122
|
-
return createIdRefProxy(appendCommandSuffix(idRef, reservedToken), options);
|
|
123
|
-
}
|
|
121
|
+
if (reservedToken && reservedToken !== 'Path') {
|
|
122
|
+
return createIdRefProxy(appendCommandSuffix(idRef, reservedToken), options);
|
|
123
|
+
}
|
|
124
124
|
|
|
125
125
|
// Chain further path segments after the id ref
|
|
126
126
|
const chained = appendPathSegment(idRef, String(prop));
|
|
@@ -162,16 +162,16 @@ function createPathProxy(prefix: string, options?: PathsOptions): any {
|
|
|
162
162
|
|
|
163
163
|
return new Proxy(handler, {
|
|
164
164
|
get(_, prop: string | symbol) {
|
|
165
|
-
if (prop === 'Path' || prop === PATH_SYMBOL) {
|
|
166
|
-
return serializePath(prefix);
|
|
167
|
-
}
|
|
165
|
+
if (prop === 'Path' || prop === PATH_SYMBOL) {
|
|
166
|
+
return serializePath(prefix);
|
|
167
|
+
}
|
|
168
168
|
// Ignore symbol access (Symbol.iterator, Symbol.toPrimitive, etc.)
|
|
169
169
|
if (typeof prop === 'symbol') return undefined;
|
|
170
170
|
|
|
171
171
|
const reservedToken = getReservedToken(String(prop));
|
|
172
|
-
if (reservedToken === 'Path') {
|
|
173
|
-
return serializePath(prefix);
|
|
174
|
-
}
|
|
172
|
+
if (reservedToken === 'Path') {
|
|
173
|
+
return serializePath(prefix);
|
|
174
|
+
}
|
|
175
175
|
if (reservedToken === 'Each') {
|
|
176
176
|
return createPathProxy(appendPathSegment(prefix, '@each'), options);
|
|
177
177
|
}
|
|
@@ -231,14 +231,14 @@ function createPathProxy(prefix: string, options?: PathsOptions): any {
|
|
|
231
231
|
*
|
|
232
232
|
* @example
|
|
233
233
|
* const $ = paths<Person>();
|
|
234
|
-
* $.lastName.Path // '?.lastName'
|
|
235
|
-
* $.address.city.Path // '?.address?.city'
|
|
234
|
+
* $.lastName.Path // '?.lastName'
|
|
235
|
+
* $.address.city.Path // '?.address?.city'
|
|
236
236
|
*
|
|
237
237
|
* // With aka (reverse alias applied):
|
|
238
238
|
* const $ = paths<MyEl>({ aka: { q: 'querySelector' } });
|
|
239
|
-
* $.querySelector('.user').textContent.Path // '?.q?..user?.textContent'
|
|
239
|
+
* $.querySelector('.user').textContent.Path // '?.q?..user?.textContent'
|
|
240
240
|
*
|
|
241
|
-
* // Inside sp template literals, .Path is not needed:
|
|
241
|
+
* // Inside sp template literals, .Path is not needed:
|
|
242
242
|
* sp`${$.lastName}, ${$.firstName}` // ['?.lastName', ', ', '?.firstName']
|
|
243
243
|
*/
|
|
244
244
|
export function paths<T>(options?: PathsOptions): PathProxy<T> {
|
|
@@ -246,7 +246,7 @@ export function paths<T>(options?: PathsOptions): PathProxy<T> {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
* Create an assignment pair: { [lhs.Path]: rhs.Path }.
|
|
249
|
+
* Create an assignment pair: { [lhs.Path]: rhs.Path }.
|
|
250
250
|
* Used to express "set this target to this source value" in a spreadable form.
|
|
251
251
|
*
|
|
252
252
|
* @example
|
|
@@ -373,10 +373,10 @@ export function forEachKeyIn<T>(
|
|
|
373
373
|
* Interleaves static string segments with interpolated values.
|
|
374
374
|
*
|
|
375
375
|
* Path proxy objects are auto-detected and converted to their `?.`-prefixed
|
|
376
|
-
* string representation — no `.Path` call needed inside sp template literals.
|
|
376
|
+
* string representation — no `.Path` call needed inside sp template literals.
|
|
377
377
|
*
|
|
378
378
|
* Arrays passed as interpolations are preserved as nested arrays (for
|
|
379
|
-
* all-or-nothing optional segments in
|
|
379
|
+
* all-or-nothing optional segments in the ' =&' join op).
|
|
380
380
|
*
|
|
381
381
|
* @example
|
|
382
382
|
* const $ = paths<Person>();
|
package/README.md
CHANGED
|
@@ -851,7 +851,8 @@ While we are in the business of passing values of object A into object B, we mig
|
|
|
851
851
|
| ` -=` | Delete | Remove properties from an object | `'?.data -=': 'key'` |
|
|
852
852
|
| ` Y=` | Merge | Recursively `assignGingerly` into a sub-object | `'style Y=': { width: '100px' }` |
|
|
853
853
|
| ` ?=` | Ternary | Conditional assignment (assignFrom only) — [details](docs/ternary-assignment.md) | `'?.text ?=': ['?.cond', 'yes', 'no']` |
|
|
854
|
-
| ` =>` | Handler | Invoke a handler plugin (assignFrom only) | `'?.el =>': { do: 'builtIns.
|
|
854
|
+
| ` =>` | Handler | Invoke a handler plugin (assignFrom only) | `'?.el =>': { do: 'builtIns.lazyLoad', ... }` |
|
|
855
|
+
| ` =&` | Sync op | Compute a value synchronously and assign it back (assignFrom only) — no dynamic import, no await, ever | `'?.text =&': { join: ['?.first', ' ', '?.last'] }` |
|
|
855
856
|
|
|
856
857
|
All operators use a space before the suffix to distinguish them from property names. They compose with `?.` nested paths and `withMethods`.
|
|
857
858
|
|
|
@@ -2691,16 +2692,18 @@ await assignFromAsync(myElement, {
|
|
|
2691
2692
|
|
|
2692
2693
|
Import paths must be local (relative, absolute, or bare specifiers — no cross-domain URLs). The module's default export is checked first; otherwise the first exported class with an `assign` method is used.
|
|
2693
2694
|
|
|
2694
|
-
Built-in handlers (`builtIns.lazyLoad`, `builtIns.
|
|
2695
|
+
Built-in handlers (`builtIns.lazyLoad`, `builtIns.microDataJoin`, etc.) auto-load without needing to be listed in `handlers`.
|
|
2696
|
+
|
|
2697
|
+
> `join` isn't in this list — it's a synchronous [` =&` op](#sync-op-join) now, not an async `do:` handler, so it's never dynamically imported and never awaited.
|
|
2695
2698
|
|
|
2696
2699
|
**Handler aliases:** The `handlers` option also accepts built-in names as values, allowing you to define short aliases (including emoji) for concise configs:
|
|
2697
2700
|
|
|
2698
2701
|
```JavaScript
|
|
2699
2702
|
import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
2700
|
-
// { '📦': 'builtIns.lazyLoad', '🎚️': 'builtIns.lazyLoadSwitch', '
|
|
2703
|
+
// { '📦': 'builtIns.lazyLoad', '🎚️': 'builtIns.lazyLoadSwitch', '🏷️': 'builtIns.microDataJoin', '📋': 'builtIns.manageTemplateList' }
|
|
2701
2704
|
|
|
2702
2705
|
assignFrom(target, {
|
|
2703
|
-
'?.
|
|
2706
|
+
'?.el =>': { do: '🏷️', get: { template: [...] } }
|
|
2704
2707
|
}, { from: vm, handlers: builtInEmoji });
|
|
2705
2708
|
|
|
2706
2709
|
// Or define your own:
|
|
@@ -2960,9 +2963,15 @@ await assignFromAsync(document.body, {
|
|
|
2960
2963
|
- Mixed `do` values — fully supported, each handler is looked up independently.
|
|
2961
2964
|
- Error handling — fail-fast. If a handler throws, remaining handlers are skipped.
|
|
2962
2965
|
|
|
2963
|
-
|
|
2966
|
+
**Return-value protocol:**
|
|
2967
|
+
|
|
2968
|
+
When a handler's `assign()` method returns a non-`undefined` value, `processHandlerCommands` assigns it back to the LHS path. Handlers like `builtIns.lazyLoad` return `undefined` (void) and operate by side effects instead.
|
|
2969
|
+
|
|
2970
|
+
### Sync op: join (` =&`)
|
|
2964
2971
|
|
|
2965
|
-
|
|
2972
|
+
` =&` is a separate operator from ` =>`, for computed values that have nothing to await — no dynamic import, no handler class, no microtask hop, ever. The RHS names exactly one op from a small built-in registry (`syncOps/registry.ts`); sibling keys are that op's own config. `join` is the first (and so far only) op in the registry.
|
|
2973
|
+
|
|
2974
|
+
`join` resolves an array of `?.` paths and literals, then joins it into a single string. It supports nested sub-arrays with "all-or-nothing" semantics for optional segments.
|
|
2966
2975
|
|
|
2967
2976
|
```JavaScript
|
|
2968
2977
|
const vm = {
|
|
@@ -2971,11 +2980,8 @@ const vm = {
|
|
|
2971
2980
|
};
|
|
2972
2981
|
|
|
2973
2982
|
assignFrom(oElement, {
|
|
2974
|
-
'?.textContent
|
|
2975
|
-
|
|
2976
|
-
get: {
|
|
2977
|
-
value: ['?.lastName', ', ', '?.firstName']
|
|
2978
|
-
}
|
|
2983
|
+
'?.textContent =&': {
|
|
2984
|
+
join: ['?.lastName', ', ', '?.firstName']
|
|
2979
2985
|
}
|
|
2980
2986
|
}, { from: vm });
|
|
2981
2987
|
|
|
@@ -2984,11 +2990,11 @@ assignFrom(oElement, {
|
|
|
2984
2990
|
|
|
2985
2991
|
**How it works:**
|
|
2986
2992
|
|
|
2987
|
-
1. The `
|
|
2988
|
-
2. Top-level `null`/`undefined` values are filtered out.
|
|
2993
|
+
1. The whole config object (`{ join: [...], separator: '...' }`) is resolved in one pass by `getValues` — `?.` path strings anywhere inside it, including nested inside the `join` array, are replaced with actual values from `options.from`.
|
|
2994
|
+
2. Top-level `null`/`undefined` values in the resolved array are filtered out.
|
|
2989
2995
|
3. Nested sub-arrays use **all-or-nothing** semantics: if any element in a sub-array resolves to `null`/`undefined`, the entire sub-array is dropped.
|
|
2990
|
-
4. Remaining elements are joined with
|
|
2991
|
-
5. The joined string is
|
|
2996
|
+
4. Remaining elements are joined with `separator` (default: `''`, empty string).
|
|
2997
|
+
5. The joined string is assigned straight to the LHS path — every sync op always produces a value to assign; there's no opt-in return-value protocol to it like ` =>` has.
|
|
2992
2998
|
|
|
2993
2999
|
**Optional segments with nested arrays:**
|
|
2994
3000
|
|
|
@@ -3000,11 +3006,8 @@ const vm = {
|
|
|
3000
3006
|
};
|
|
3001
3007
|
|
|
3002
3008
|
assignFrom(oElement, {
|
|
3003
|
-
'?.textContent
|
|
3004
|
-
|
|
3005
|
-
get: {
|
|
3006
|
-
value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
3007
|
-
}
|
|
3009
|
+
'?.textContent =&': {
|
|
3010
|
+
join: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
3008
3011
|
}
|
|
3009
3012
|
}, { from: vm });
|
|
3010
3013
|
|
|
@@ -3019,21 +3022,16 @@ assignFrom(oElement, {
|
|
|
3019
3022
|
|
|
3020
3023
|
```JavaScript
|
|
3021
3024
|
assignFrom(oElement, {
|
|
3022
|
-
'?.textContent
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
value: ['?.firstName', '?.lastName'],
|
|
3026
|
-
separator: ' | '
|
|
3027
|
-
}
|
|
3025
|
+
'?.textContent =&': {
|
|
3026
|
+
join: ['?.firstName', '?.lastName'],
|
|
3027
|
+
separator: ' | '
|
|
3028
3028
|
}
|
|
3029
3029
|
}, { from: vm });
|
|
3030
3030
|
|
|
3031
3031
|
// oElement.textContent = 'Helaena | Targaryen'
|
|
3032
3032
|
```
|
|
3033
3033
|
|
|
3034
|
-
**
|
|
3035
|
-
|
|
3036
|
-
When a handler's `assign()` method returns a non-`undefined` value, `processHandlerCommands` assigns it back to the LHS path. This is how `builtIns.join` sets `textContent` — the handler computes the string and returns it. Existing handlers like `builtIns.lazyLoad` return `undefined` (void) and operate by side effects, so they're unaffected.
|
|
3034
|
+
**On `options.protocols`:** `getValues` runs synchronously and calls protocol handlers without awaiting them. `options.protocols` is typed to allow an async handler (`(key: string) => any | Promise<any>`), because ` =>`'s `resolve:` path can use one — but ` =&` has no await to catch a Promise coming back from one. If a resolved value under ` =&` turns out to be a thenable, assignFrom throws immediately rather than silently assigning `"[object Promise]"`. Use ` =>` with `resolve:` for handler configs that need an async protocol.
|
|
3037
3035
|
|
|
3038
3036
|
### Built-in handler: `builtIns.microDataJoin`
|
|
3039
3037
|
|
|
@@ -3095,7 +3093,7 @@ Produces:
|
|
|
3095
3093
|
|
|
3096
3094
|
**Optional segments (nested arrays):**
|
|
3097
3095
|
|
|
3098
|
-
Same all-or-nothing semantics as `
|
|
3096
|
+
Same all-or-nothing semantics as the [`join` sync op](#sync-op-join) — if any `val` in a nested sub-array is null/undefined, the entire sub-array is dropped:
|
|
3099
3097
|
|
|
3100
3098
|
```JavaScript
|
|
3101
3099
|
get: {
|
|
@@ -3239,7 +3237,7 @@ assignFrom(element, {
|
|
|
3239
3237
|
|
|
3240
3238
|
## Typed Path Authoring with `paths`, `sp`, and `md`
|
|
3241
3239
|
|
|
3242
|
-
For JSON generated config files generated from TypeScript/`.mts`/`mjs` files during a build or server-side rendering, the `paths` utility provides compile-time autocomplete and type safety for `?.`-prefixed path strings. The `sp` tagged template literal ("split into parts") produces arrays suitable for `
|
|
3240
|
+
For JSON generated config files generated from TypeScript/`.mts`/`mjs` files during a build or server-side rendering, the `paths` utility provides compile-time autocomplete and type safety for `?.`-prefixed path strings. The `sp` tagged template literal ("split into parts") produces arrays suitable for the [`join` sync op](#sync-op-join). The `md` tagged template literal produces `{prop, val}` objects suitable for `builtIns.microDataJoin`.
|
|
3243
3241
|
|
|
3244
3242
|
```TypeScript
|
|
3245
3243
|
import { paths, sp } from 'assign-gingerly/DX/paths.js';
|
|
@@ -3256,11 +3254,8 @@ const $ = paths<Person>();
|
|
|
3256
3254
|
// sp produces: ['?.lastName', ', ', '?.firstName']
|
|
3257
3255
|
// with full autocomplete on $.lastName, $.firstName, etc.
|
|
3258
3256
|
export default {
|
|
3259
|
-
'?.textContent
|
|
3260
|
-
|
|
3261
|
-
get: {
|
|
3262
|
-
value: sp`${$.lastName}, ${$.firstName}`
|
|
3263
|
-
}
|
|
3257
|
+
'?.textContent =&': {
|
|
3258
|
+
join: sp`${$.lastName}, ${$.firstName}`
|
|
3264
3259
|
}
|
|
3265
3260
|
};
|
|
3266
3261
|
```
|
|
@@ -3268,9 +3263,9 @@ export default {
|
|
|
3268
3263
|
**How `paths` works:**
|
|
3269
3264
|
|
|
3270
3265
|
- `paths<T>()` creates a deeply-proxied object typed as `T`
|
|
3271
|
-
- Every property access returns a deeper proxy (e.g., `$.address.city`)
|
|
3266
|
+
- Every property access returns a deeper proxy (e.g., `$.address.city`)
|
|
3272
3267
|
- Capitalized reserved tokens like `.Each` and `.EqNot` normalize into command syntax
|
|
3273
|
-
- `.Path` extracts the `?.`-prefixed string: `$.address.city.Path` → `'?.address?.city'`
|
|
3268
|
+
- `.Path` extracts the `?.`-prefixed string: `$.address.city.Path` → `'?.address?.city'`
|
|
3274
3269
|
- Inside `sp` template literals, `.Path` is not needed — proxy objects are auto-detected
|
|
3275
3270
|
|
|
3276
3271
|
**How `sp` works:**
|
|
@@ -3339,7 +3334,7 @@ md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`
|
|
|
3339
3334
|
|
|
3340
3335
|
| Tag | Output for proxy interpolation | Use with |
|
|
3341
3336
|
|-----|-------------------------------|----------|
|
|
3342
|
-
| `sp` | `'?.firstName'` (path string) | `
|
|
3337
|
+
| `sp` | `'?.firstName'` (path string) | the `join` sync op |
|
|
3343
3338
|
| `md` | `{ prop: 'firstName', val: '?.firstName' }` | `builtIns.microDataJoin` |
|
|
3344
3339
|
|
|
3345
3340
|
Both auto-detect path proxies (no `.Path` needed inside template literals) and preserve nested arrays for optional segments.
|
package/assignFrom.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* For async protocol handlers or awaitable handler execution, use assignFromAsync.
|
|
8
8
|
*
|
|
9
9
|
* Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
|
|
10
|
+
* Sync-op commands (` =&`) are always fully synchronous — see SYNC_OPS.
|
|
10
11
|
*/
|
|
11
12
|
import { getValues, getValue } from './resolve/getValues.js';
|
|
12
13
|
import assignGingerly from './assignGingerly.js';
|
|
13
14
|
import { resolveIdVariable, parseIdRef } from './resolve/resolveIdRef.js';
|
|
14
15
|
import { processInferredAssignments } from './inferredAssignments.js';
|
|
16
|
+
import { resolveLhsPath } from './utils/resolveLhsPath.js';
|
|
17
|
+
import { SYNC_OPS } from './syncOps/registry.js';
|
|
15
18
|
/**
|
|
16
19
|
* Supported substitution variables and their option keys.
|
|
17
20
|
*/
|
|
@@ -40,6 +43,48 @@ export function parseTernaryCommand(key) {
|
|
|
40
43
|
return null;
|
|
41
44
|
return key.substring(0, key.length - 3); // Remove ' ?=' suffix
|
|
42
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if a key ends with the sync-op operator ' =&'.
|
|
48
|
+
*/
|
|
49
|
+
export function isSyncOpCommand(key) {
|
|
50
|
+
return key.endsWith(' =&');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse a =& sync-op command and extract the LHS path.
|
|
54
|
+
*/
|
|
55
|
+
export function parseSyncOpCommand(key) {
|
|
56
|
+
if (!isSyncOpCommand(key))
|
|
57
|
+
return null;
|
|
58
|
+
return key.substring(0, key.length - 3); // Remove ' =&' suffix
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Throws if a resolved sync-op value (or anything nested inside it) is a thenable.
|
|
62
|
+
*
|
|
63
|
+
* getValues is synchronous by contract, but `options.protocols` handlers are
|
|
64
|
+
* typed to allow returning a Promise. A sync op has no await to catch that —
|
|
65
|
+
* left unchecked it would silently stringify as "[object Promise]" — so this
|
|
66
|
+
* turns it into a clear error instead.
|
|
67
|
+
*/
|
|
68
|
+
function assertNoThenable(value, opName, key) {
|
|
69
|
+
if (value == null)
|
|
70
|
+
return;
|
|
71
|
+
if (typeof value.then === 'function') {
|
|
72
|
+
throw new Error(`assignFrom: sync op '${opName}' (key "${key}") received an async value — ` +
|
|
73
|
+
`a protocol in options.protocols returned a Promise, which ' =&' cannot await. ` +
|
|
74
|
+
`Use ' =>' with 'resolve:' instead for async values.`);
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
for (const item of value)
|
|
78
|
+
assertNoThenable(item, opName, key);
|
|
79
|
+
}
|
|
80
|
+
else if (typeof value === 'object') {
|
|
81
|
+
const proto = Object.getPrototypeOf(value);
|
|
82
|
+
if (proto === Object.prototype || proto === null) {
|
|
83
|
+
for (const item of Object.values(value))
|
|
84
|
+
assertNoThenable(item, opName, key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
43
88
|
/**
|
|
44
89
|
* Resolve a single value — if it's a `?.` path string, resolve against source.
|
|
45
90
|
* If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
|
|
@@ -343,6 +388,7 @@ export function categorizeKeys(expandedPattern) {
|
|
|
343
388
|
const idRefNormalKeys = [];
|
|
344
389
|
const idRefHandlerKeys = [];
|
|
345
390
|
const ternaryKeys = [];
|
|
391
|
+
const syncOpKeys = [];
|
|
346
392
|
for (const key of Object.keys(expandedPattern)) {
|
|
347
393
|
if (isHandlerCommand(key)) {
|
|
348
394
|
if (key.startsWith('#[')) {
|
|
@@ -355,6 +401,9 @@ export function categorizeKeys(expandedPattern) {
|
|
|
355
401
|
else if (isTernaryCommand(key)) {
|
|
356
402
|
ternaryKeys.push(key);
|
|
357
403
|
}
|
|
404
|
+
else if (isSyncOpCommand(key)) {
|
|
405
|
+
syncOpKeys.push(key);
|
|
406
|
+
}
|
|
358
407
|
else if (key.startsWith('#[')) {
|
|
359
408
|
idRefNormalKeys.push(key);
|
|
360
409
|
}
|
|
@@ -362,7 +411,7 @@ export function categorizeKeys(expandedPattern) {
|
|
|
362
411
|
normalPattern[key] = expandedPattern[key];
|
|
363
412
|
}
|
|
364
413
|
}
|
|
365
|
-
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
|
|
414
|
+
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys };
|
|
366
415
|
}
|
|
367
416
|
/**
|
|
368
417
|
* Merge pin and at into a single lookup map for resolveIdVariable.
|
|
@@ -420,7 +469,7 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
|
|
|
420
469
|
// Expand looped substitution variables
|
|
421
470
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
422
471
|
// Categorize keys
|
|
423
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
|
|
472
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys } = categorizeKeys(expandedPattern);
|
|
424
473
|
const resolveOptions = { ...options, root: target, permissionProcessor };
|
|
425
474
|
// Process ?= ternary keys (sync)
|
|
426
475
|
if (ternaryKeys.length > 0) {
|
|
@@ -441,6 +490,33 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
|
|
|
441
490
|
assignGingerly(target, ternaryResolved, options, permissionProcessor);
|
|
442
491
|
}
|
|
443
492
|
}
|
|
493
|
+
// Process =& sync-op keys (sync — always, no dynamic import, no await, ever)
|
|
494
|
+
if (syncOpKeys.length > 0) {
|
|
495
|
+
for (const key of syncOpKeys) {
|
|
496
|
+
const lhsPath = parseSyncOpCommand(key);
|
|
497
|
+
if (lhsPath === null)
|
|
498
|
+
continue;
|
|
499
|
+
const config = expandedPattern[key];
|
|
500
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
501
|
+
throw new Error(`assignFrom: sync-op command "${key}" requires a config object naming exactly one op`);
|
|
502
|
+
}
|
|
503
|
+
const opName = Object.keys(config).find(k => k in SYNC_OPS);
|
|
504
|
+
if (!opName) {
|
|
505
|
+
throw new Error(`assignFrom: sync-op command "${key}" does not name a known op (${Object.keys(SYNC_OPS).join(', ')})`);
|
|
506
|
+
}
|
|
507
|
+
const resolvedConfig = getValues(config, options.from, resolveOptions);
|
|
508
|
+
assertNoThenable(resolvedConfig, opName, key);
|
|
509
|
+
const { [opName]: args, ...extra } = resolvedConfig;
|
|
510
|
+
const result = SYNC_OPS[opName](args, extra);
|
|
511
|
+
if (result === undefined)
|
|
512
|
+
continue;
|
|
513
|
+
const { lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
|
|
514
|
+
if (lhsParent != null && lhsKey != null
|
|
515
|
+
&& !permissionProcessor?.redirectRestrictedProp(lhsParent, lhsKey, result)) {
|
|
516
|
+
lhsParent[lhsKey] = result;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
444
520
|
// Process normal keys via getValues (sync) + assignGingerly
|
|
445
521
|
if (Object.keys(normalPattern).length > 0) {
|
|
446
522
|
// Resolve #[x] references on RHS values before getValues
|
package/assignFrom.ts
CHANGED
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
* For async protocol handlers or awaitable handler execution, use assignFromAsync.
|
|
8
8
|
*
|
|
9
9
|
* Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
|
|
10
|
+
* Sync-op commands (` =&`) are always fully synchronous — see SYNC_OPS.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { getValues, getValue } from './resolve/getValues.js';
|
|
13
14
|
import assignGingerly from './assignGingerly.js';
|
|
14
15
|
import { resolveIdVariable, parseIdRef } from './resolve/resolveIdRef.js';
|
|
15
16
|
import { processInferredAssignments } from './inferredAssignments.js';
|
|
17
|
+
import { resolveLhsPath } from './utils/resolveLhsPath.js';
|
|
18
|
+
import { SYNC_OPS } from './syncOps/registry.js';
|
|
16
19
|
import type { PermissionProcessor, AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor } from './types/assign-gingerly/types.js';
|
|
17
20
|
|
|
18
21
|
// Re-export types for consumers
|
|
@@ -49,6 +52,48 @@ export function parseTernaryCommand(key: string): string | null {
|
|
|
49
52
|
return key.substring(0, key.length - 3); // Remove ' ?=' suffix
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Check if a key ends with the sync-op operator ' =&'.
|
|
57
|
+
*/
|
|
58
|
+
export function isSyncOpCommand(key: string): boolean {
|
|
59
|
+
return key.endsWith(' =&');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a =& sync-op command and extract the LHS path.
|
|
64
|
+
*/
|
|
65
|
+
export function parseSyncOpCommand(key: string): string | null {
|
|
66
|
+
if (!isSyncOpCommand(key)) return null;
|
|
67
|
+
return key.substring(0, key.length - 3); // Remove ' =&' suffix
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Throws if a resolved sync-op value (or anything nested inside it) is a thenable.
|
|
72
|
+
*
|
|
73
|
+
* getValues is synchronous by contract, but `options.protocols` handlers are
|
|
74
|
+
* typed to allow returning a Promise. A sync op has no await to catch that —
|
|
75
|
+
* left unchecked it would silently stringify as "[object Promise]" — so this
|
|
76
|
+
* turns it into a clear error instead.
|
|
77
|
+
*/
|
|
78
|
+
function assertNoThenable(value: any, opName: string, key: string): void {
|
|
79
|
+
if (value == null) return;
|
|
80
|
+
if (typeof value.then === 'function') {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`assignFrom: sync op '${opName}' (key "${key}") received an async value — ` +
|
|
83
|
+
`a protocol in options.protocols returned a Promise, which ' =&' cannot await. ` +
|
|
84
|
+
`Use ' =>' with 'resolve:' instead for async values.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (const item of value) assertNoThenable(item, opName, key);
|
|
89
|
+
} else if (typeof value === 'object') {
|
|
90
|
+
const proto = Object.getPrototypeOf(value);
|
|
91
|
+
if (proto === Object.prototype || proto === null) {
|
|
92
|
+
for (const item of Object.values(value)) assertNoThenable(item, opName, key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
52
97
|
/**
|
|
53
98
|
* Resolve a single value — if it's a `?.` path string, resolve against source.
|
|
54
99
|
* If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
|
|
@@ -354,6 +399,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
354
399
|
const idRefNormalKeys: string[] = [];
|
|
355
400
|
const idRefHandlerKeys: string[] = [];
|
|
356
401
|
const ternaryKeys: string[] = [];
|
|
402
|
+
const syncOpKeys: string[] = [];
|
|
357
403
|
|
|
358
404
|
for (const key of Object.keys(expandedPattern)) {
|
|
359
405
|
if (isHandlerCommand(key)) {
|
|
@@ -364,6 +410,8 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
364
410
|
}
|
|
365
411
|
} else if (isTernaryCommand(key)) {
|
|
366
412
|
ternaryKeys.push(key);
|
|
413
|
+
} else if (isSyncOpCommand(key)) {
|
|
414
|
+
syncOpKeys.push(key);
|
|
367
415
|
} else if (key.startsWith('#[')) {
|
|
368
416
|
idRefNormalKeys.push(key);
|
|
369
417
|
} else {
|
|
@@ -371,7 +419,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
371
419
|
}
|
|
372
420
|
}
|
|
373
421
|
|
|
374
|
-
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
|
|
422
|
+
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys };
|
|
375
423
|
}
|
|
376
424
|
|
|
377
425
|
/**
|
|
@@ -447,7 +495,7 @@ export function assignFrom(
|
|
|
447
495
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
448
496
|
|
|
449
497
|
// Categorize keys
|
|
450
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
|
|
498
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys } = categorizeKeys(expandedPattern);
|
|
451
499
|
|
|
452
500
|
const resolveOptions = { ...options, root: target, permissionProcessor } as any;
|
|
453
501
|
|
|
@@ -469,6 +517,36 @@ export function assignFrom(
|
|
|
469
517
|
}
|
|
470
518
|
}
|
|
471
519
|
|
|
520
|
+
// Process =& sync-op keys (sync — always, no dynamic import, no await, ever)
|
|
521
|
+
if (syncOpKeys.length > 0) {
|
|
522
|
+
for (const key of syncOpKeys) {
|
|
523
|
+
const lhsPath = parseSyncOpCommand(key);
|
|
524
|
+
if (lhsPath === null) continue;
|
|
525
|
+
const config = expandedPattern[key];
|
|
526
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
527
|
+
throw new Error(`assignFrom: sync-op command "${key}" requires a config object naming exactly one op`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const opName = Object.keys(config).find(k => k in SYNC_OPS);
|
|
531
|
+
if (!opName) {
|
|
532
|
+
throw new Error(`assignFrom: sync-op command "${key}" does not name a known op (${Object.keys(SYNC_OPS).join(', ')})`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const resolvedConfig = getValues(config, options.from, resolveOptions);
|
|
536
|
+
assertNoThenable(resolvedConfig, opName, key);
|
|
537
|
+
|
|
538
|
+
const { [opName]: args, ...extra } = resolvedConfig;
|
|
539
|
+
const result = SYNC_OPS[opName](args, extra);
|
|
540
|
+
if (result === undefined) continue;
|
|
541
|
+
|
|
542
|
+
const { lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
|
|
543
|
+
if (lhsParent != null && lhsKey != null
|
|
544
|
+
&& !permissionProcessor?.redirectRestrictedProp(lhsParent, lhsKey, result)) {
|
|
545
|
+
lhsParent[lhsKey] = result;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
472
550
|
// Process normal keys via getValues (sync) + assignGingerly
|
|
473
551
|
if (Object.keys(normalPattern).length > 0) {
|
|
474
552
|
// Resolve #[x] references on RHS values before getValues
|
|
@@ -224,7 +224,7 @@ Create `.vscode/settings.json`:
|
|
|
224
224
|
}
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
-
## Step 8: Set Up .kiro Directory
|
|
227
|
+
## Step 8: Set Up .kiro Directory Only if kiro is implementing.
|
|
228
228
|
|
|
229
229
|
Create `.kiro/steering/project-context.md` to reference the shared types documentation:
|
|
230
230
|
|
|
@@ -350,6 +350,8 @@ customElements.assignFeatures(MyElement, {
|
|
|
350
350
|
|
|
351
351
|
The parsed attributes (`{ myProp: 'hello', count: 42 }`) are passed as `initVals` to the constructor.
|
|
352
352
|
|
|
353
|
+
See [withAttrs](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/withAttrs.md) for an in-depth discussion of all the various configuration options.
|
|
354
|
+
|
|
353
355
|
### Async Spawn (Lazy Loading)
|
|
354
356
|
|
|
355
357
|
Feature implementations can be loaded asynchronously:
|
|
@@ -451,6 +451,8 @@ const raConfig = {
|
|
|
451
451
|
};
|
|
452
452
|
```
|
|
453
453
|
|
|
454
|
+
[Please fully digest all the attribute parsing tha assign-gingerly provides before configuring the attributes.](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/withAttrs.md)
|
|
455
|
+
|
|
454
456
|
|
|
455
457
|
## Step 8
|
|
456
458
|
|