assign-gingerly 0.0.85 → 0.0.87

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.
@@ -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
 
@@ -112,6 +112,36 @@ export interface ParserContext<T = any> {
112
112
  attrName: string;
113
113
  }
114
114
 
115
+ /**
116
+ * Tuple reference for custom element static method parsers
117
+ * [elementName, methodName]
118
+ */
119
+ export type ParserTuple = [CustomElementName, CustomElementConstructorStaticMethodName];
120
+
121
+ /**
122
+ * Class-based parser interface
123
+ * Classes registered as named parsers are instantiated per attribute parse
124
+ * and their parse method is called with the attribute value and context
125
+ */
126
+ export interface AttrParser<T = any> {
127
+ parse(attrValue: string | null, context?: ParserContext<T>): any;
128
+ }
129
+
130
+ /**
131
+ * Constructor signature for class-based parsers
132
+ */
133
+ export type AttrParserConstructor<T = any> = {
134
+ new (options?: any): AttrParser<T>;
135
+ };
136
+
137
+ /**
138
+ * Object form for referencing a registered named parser with constructor options
139
+ */
140
+ export interface NamedParserRef {
141
+ name: string;
142
+ options?: any;
143
+ }
144
+
115
145
  /**
116
146
  * Parser function signature
117
147
  * Can accept just the attribute value (simple form) or value + context (advanced form)
@@ -120,6 +150,15 @@ export type ParserFunction<T = any> =
120
150
  | ((attrValue: string | null) => any)
121
151
  | ((attrValue: string | null, context?: ParserContext<T>) => any);
122
152
 
153
+ /**
154
+ * Any valid parser specification for AttrConfig.parser
155
+ */
156
+ export type ParserSpec<T = any> =
157
+ | ParserFunction<T>
158
+ | string
159
+ | ParserTuple
160
+ | NamedParserRef;
161
+
123
162
  export interface AttrConfig<T = unknown, TParserConfig = unknown> {
124
163
  /**
125
164
  * Type of the property value (JSON-serializable string format)
@@ -148,7 +187,9 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
148
187
  * - Function: Inline parser function (not JSON serializable)
149
188
  * - Simple form: (attrValue: string | null) => any
150
189
  * - Advanced form: (attrValue: string | null, context: ParserContext) => any
151
- * - String: Named parser reference (JSON serializable) - looks up in scoped registry (if available) then global parser registry (e.g., 'timestamp', 'csv')
190
+ * - String: Named parser reference (JSON serializable) - looks up in scoped registry (if available) then global parser registry (e.g., 'timestamp', 'splitter')
191
+ * - Tuple: [CustomElementName, StaticMethodName] - looks up a static method on a custom element constructor
192
+ * - Object: { name: string; options?: any } - looks up a registered class parser and instantiates it with the given options
152
193
  *
153
194
  * Parser functions can optionally accept a second parameter (ParserContext) which provides:
154
195
  * - attrConfig: The full AttrConfig object for this attribute
@@ -156,10 +197,7 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
156
197
  * - element: The element being enhanced
157
198
  * - attrName: The resolved attribute name
158
199
  */
159
- parser?:
160
- | ParserFunction<T>
161
- | string
162
- ;
200
+ parser?: ParserSpec<T>;
163
201
 
164
202
  /**
165
203
  * configuration information needed by a custom parser to properly
@@ -17,16 +17,18 @@ export interface SwipeDismissProps {
17
17
  distanceThreshold: number;
18
18
  /** Velocity threshold in px/ms; a fast flick commits even under distanceThreshold. */
19
19
  velocityThreshold: number;
20
- /** CSS selector for the drag handle. Defaults to the host element. */
21
- handleSelector: string | null;
22
- /** CSS selector for the panel that visually follows the drag. Defaults to the handle. */
23
- panelSelector: string | null;
20
+
24
21
  /** Called on every pointermove with the current delta and fraction of the threshold. */
25
22
  onProgress: ((deltaPx: number, fraction: number) => void) | null;
26
23
  /** Called when the gesture crosses the commit threshold. */
27
24
  onCommit: (() => void) | null;
28
25
  /** Called when the gesture is released before the commit threshold. */
29
26
  onCancel: (() => void) | null;
27
+
28
+ /** Element used for dragging */
29
+ handle: Element;
30
+ /** Element used to open / close */
31
+ panel: Element;
30
32
  }
31
33
 
32
34
  /**
@@ -35,6 +37,7 @@ export interface SwipeDismissProps {
35
37
  export interface AllProps extends SwipeDismissProps {
36
38
  /** WeakRef to the host custom element. */
37
39
  hostRef: WeakRef<Element>;
40
+
38
41
  }
39
42
 
40
43
  export type AP = AllProps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.85",
3
+ "version": "0.0.87",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -22,6 +22,7 @@
22
22
  "handlers/**",
23
23
  "inferencer/**",
24
24
  "resolve/**",
25
+ "syncOps/**",
25
26
  "utils/**",
26
27
  "README.md",
27
28
  "LICENSE",
@@ -103,9 +104,13 @@
103
104
  "default": "./handlers/lazyLoadSwitch.js",
104
105
  "types": "./handlers/lazyLoadSwitch.ts"
105
106
  },
106
- "./handlers/join.js": {
107
- "default": "./handlers/join.js",
108
- "types": "./handlers/join.ts"
107
+ "./syncOps/join.js": {
108
+ "default": "./syncOps/join.js",
109
+ "types": "./syncOps/join.ts"
110
+ },
111
+ "./syncOps/registry.js": {
112
+ "default": "./syncOps/registry.js",
113
+ "types": "./syncOps/registry.ts"
109
114
  },
110
115
  "./handlers/microDataJoin.js": {
111
116
  "default": "./handlers/microDataJoin.js",
@@ -5,16 +5,18 @@
5
5
  */
6
6
  import { resolveValues } from './resolve/resolveValues.js';
7
7
  import { getValues } from './resolve/getValues.js';
8
- import { evaluatePathWithMethods } from './assignGingerly.js';
8
+ import { resolveLhsPath } from './utils/resolveLhsPath.js';
9
9
  import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
10
10
  /**
11
11
  * Map of built-in handler names to their module paths.
12
12
  * These are auto-loaded on demand — no explicit import required.
13
+ *
14
+ * `join` moved to a synchronous ` =&` op (see syncOps/join.ts) — it never had
15
+ * side effects or anything to await, so it didn't belong behind this async pipeline.
13
16
  */
14
17
  const BUILT_IN_MAP = {
15
18
  'builtIns.lazyLoad': './handlers/lazyLoad.js',
16
19
  'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
17
- 'builtIns.join': './handlers/join.js',
18
20
  'builtIns.microDataJoin': './handlers/microDataJoin.js',
19
21
  'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
20
22
  'builtIns.rangeSelector': './handlers/rangeSelector.js',
@@ -98,59 +100,7 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
98
100
  continue;
99
101
  // Resolve the LHS path, preserving parent + key for return-value assignment.
100
102
  // lhsParent[lhsKey] === lhsTarget (the current value at the path)
101
- let lhsTarget;
102
- let lhsParent = undefined;
103
- let lhsKey = undefined;
104
- if (lhsPath.startsWith('?.')) {
105
- const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
106
- const withMethodsSet = options.withMethods
107
- ? options.withMethods instanceof Set
108
- ? options.withMethods
109
- : new Set(options.withMethods)
110
- : undefined;
111
- if (withMethodsSet && pathParts.length > 0) {
112
- const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
113
- lhsParent = result.target;
114
- lhsKey = result.lastKey;
115
- lhsTarget = result.target[result.lastKey];
116
- // If last key is a method, call it to get the target
117
- if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
118
- lhsTarget = result.target[result.lastKey].call(result.target);
119
- lhsParent = undefined; // Can't assign back to a method call result
120
- lhsKey = undefined;
121
- }
122
- }
123
- else {
124
- // Simple path navigation — walk to parent, keep last key
125
- if (pathParts.length === 0) {
126
- lhsTarget = target;
127
- }
128
- else if (pathParts.length === 1) {
129
- lhsParent = target;
130
- lhsKey = pathParts[0];
131
- lhsTarget = target[pathParts[0]];
132
- }
133
- else {
134
- let current = target;
135
- for (let i = 0; i < pathParts.length - 1; i++) {
136
- if (current == null)
137
- break;
138
- current = current[pathParts[i]];
139
- }
140
- lhsParent = current;
141
- lhsKey = pathParts[pathParts.length - 1];
142
- lhsTarget = current != null ? current[lhsKey] : undefined;
143
- }
144
- }
145
- }
146
- else if (lhsPath) {
147
- lhsParent = target;
148
- lhsKey = lhsPath;
149
- lhsTarget = target[lhsPath];
150
- }
151
- else {
152
- lhsTarget = target;
153
- }
103
+ const { lhsTarget, lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
154
104
  // Execute handlers sequentially, sharing the same lhsTarget
155
105
  for (const config of configs) {
156
106
  //return; //1.3ms
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { resolveValues } from './resolve/resolveValues.js';
8
8
  import { getValues } from './resolve/getValues.js';
9
- import { evaluatePathWithMethods } from './assignGingerly.js';
9
+ import { resolveLhsPath } from './utils/resolveLhsPath.js';
10
10
  import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
11
11
  import type { PermissionProcessor } from './types/assign-gingerly/types.js';
12
12
  import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFromAsync.js';
@@ -14,11 +14,13 @@ import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFr
14
14
  /**
15
15
  * Map of built-in handler names to their module paths.
16
16
  * These are auto-loaded on demand — no explicit import required.
17
+ *
18
+ * `join` moved to a synchronous ` =&` op (see syncOps/join.ts) — it never had
19
+ * side effects or anything to await, so it didn't belong behind this async pipeline.
17
20
  */
18
21
  const BUILT_IN_MAP: Record<string, string> = {
19
22
  'builtIns.lazyLoad': './handlers/lazyLoad.js',
20
23
  'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
21
- 'builtIns.join': './handlers/join.js',
22
24
  'builtIns.microDataJoin': './handlers/microDataJoin.js',
23
25
  'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
24
26
  'builtIns.rangeSelector': './handlers/rangeSelector.js',
@@ -121,55 +123,8 @@ export async function processHandlerCommands(
121
123
 
122
124
  // Resolve the LHS path, preserving parent + key for return-value assignment.
123
125
  // lhsParent[lhsKey] === lhsTarget (the current value at the path)
124
- let lhsTarget: any;
125
- let lhsParent: any = undefined;
126
- let lhsKey: string | undefined = undefined;
127
-
128
- if (lhsPath.startsWith('?.')) {
129
- const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
130
- const withMethodsSet = options.withMethods
131
- ? options.withMethods instanceof Set
132
- ? options.withMethods
133
- : new Set(options.withMethods)
134
- : undefined;
135
-
136
- if (withMethodsSet && pathParts.length > 0) {
137
- const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
138
- lhsParent = result.target;
139
- lhsKey = result.lastKey;
140
- lhsTarget = result.target[result.lastKey];
141
- // If last key is a method, call it to get the target
142
- if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
143
- lhsTarget = result.target[result.lastKey].call(result.target);
144
- lhsParent = undefined; // Can't assign back to a method call result
145
- lhsKey = undefined;
146
- }
147
- } else {
148
- // Simple path navigation — walk to parent, keep last key
149
- if (pathParts.length === 0) {
150
- lhsTarget = target;
151
- } else if (pathParts.length === 1) {
152
- lhsParent = target;
153
- lhsKey = pathParts[0];
154
- lhsTarget = target[pathParts[0]];
155
- } else {
156
- let current = target;
157
- for (let i = 0; i < pathParts.length - 1; i++) {
158
- if (current == null) break;
159
- current = current[pathParts[i]];
160
- }
161
- lhsParent = current;
162
- lhsKey = pathParts[pathParts.length - 1];
163
- lhsTarget = current != null ? current[lhsKey] : undefined;
164
- }
165
- }
166
- } else if (lhsPath) {
167
- lhsParent = target;
168
- lhsKey = lhsPath;
169
- lhsTarget = target[lhsPath];
170
- } else {
171
- lhsTarget = target;
172
- }
126
+ const { lhsTarget, lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
127
+
173
128
  // Execute handlers sequentially, sharing the same lhsTarget
174
129
  for (const config of configs) {
175
130
  //return; //1.3ms
@@ -1,73 +1,61 @@
1
- /**
2
- * builtIns.join handler for assignFrom.
3
- *
4
- * Joins a resolved array into a single string. Supports nested sub-arrays
5
- * with "all-or-nothing" semantics: if any element in a nested sub-array
6
- * resolves to null/undefined, the entire sub-array is dropped.
7
- *
8
- * This handler is auto-loaded by processHandlerCommands when `do: 'builtIns.join'`
9
- * is encountered no explicit import is needed.
10
- *
11
- * @example
12
- * assignFrom(oElement, {
13
- * '?.textContent =>': {
14
- * do: 'builtIns.join',
15
- * resolve: {
16
- * value: ['?.lastName', ', ', '?.firstName']
17
- * }
18
- * }
19
- * }, { from: vm });
20
- *
21
- * @example
22
- * // With optional segment (all-or-nothing):
23
- * assignFrom(oElement, {
24
- * '?.textContent =>': {
25
- * do: 'builtIns.join',
26
- * resolve: {
27
- * value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
28
- * }
29
- * }
30
- * }, { from: vm });
31
- * // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
32
- */
33
- /**
34
- * Process nested arrays with all-or-nothing null semantics.
35
- * - Top-level null/undefined values are filtered out.
36
- * - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
37
- * - Nested sub-arrays that pass are flattened into the result.
38
- */
39
- function processValue(value) {
40
- const result = [];
41
- for (const item of value) {
42
- if (Array.isArray(item)) {
43
- // All-or-nothing: if any element is null/undefined, drop the entire sub-array
44
- if (item.some(el => el == null)) {
45
- continue;
46
- }
47
- // Sub-array passes — flatten its elements (recursively process nested arrays)
48
- result.push(...processValue(item));
49
- }
50
- else if (item != null) {
51
- result.push(item);
52
- }
53
- // Top-level null/undefined are silently filtered out
54
- }
55
- return result;
56
- }
57
- /**
58
- * JoinHandler built-in handler for composing strings from resolved arrays.
59
- *
60
- * Returns the joined string via the return-value protocol, which causes
61
- * processHandlerCommands to assign it back to the LHS path.
62
- */
63
- export class JoinHandler {
64
- config;
65
- constructor(config) {
66
- this.config = config;
67
- }
68
- async assign(lhsTarget, resolvedParams) {
69
- const { value, separator = '' } = resolvedParams;
70
- const items = Array.isArray(value) ? processValue(value) : [value];
71
- return items.join(separator);
72
- }
73
- }
1
+ /**
2
+ * join synchronous compute op for the ` =&` operator.
3
+ *
4
+ * Joins a resolved array into a single string. Supports nested sub-arrays
5
+ * with "all-or-nothing" semantics: if any element in a nested sub-array
6
+ * resolves to null/undefined, the entire sub-array is dropped.
7
+ *
8
+ * This is the sync-op successor to the old `builtIns.join` handler (which lived
9
+ * behind the async ` =>` pipeline). Unlike a handler, an op is a plain function —
10
+ * no class, no dynamic import, no await anywhere in its path.
11
+ *
12
+ * @example
13
+ * assignFrom(oElement, {
14
+ * '?.textContent =&': {
15
+ * join: ['?.lastName', ', ', '?.firstName']
16
+ * }
17
+ * }, { from: vm });
18
+ *
19
+ * @example
20
+ * // With optional segment (all-or-nothing) and a separator:
21
+ * assignFrom(oElement, {
22
+ * '?.textContent =&': {
23
+ * join: ['?.lastName', ['?.middleName'], '?.firstName'],
24
+ * separator: ', '
25
+ * }
26
+ * }, { from: vm });
27
+ * // If middleName is undefined, the sub-array ['?.middleName'] is dropped entirely.
28
+ */
29
+ /**
30
+ * Process nested arrays with all-or-nothing null semantics.
31
+ * - Top-level null/undefined values are filtered out.
32
+ * - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
33
+ * - Nested sub-arrays that pass are flattened into the result.
34
+ */
35
+ function processValue(value) {
36
+ const result = [];
37
+ for (const item of value) {
38
+ if (Array.isArray(item)) {
39
+ // All-or-nothing: if any element is null/undefined, drop the entire sub-array
40
+ if (item.some(el => el == null)) {
41
+ continue;
42
+ }
43
+ // Sub-array passes flatten its elements (recursively process nested arrays)
44
+ result.push(...processValue(item));
45
+ }
46
+ else if (item != null) {
47
+ result.push(item);
48
+ }
49
+ // Top-level null/undefined are silently filtered out
50
+ }
51
+ return result;
52
+ }
53
+ /**
54
+ * `join` sync op — args is the resolved `join:` array, extra carries sibling
55
+ * config keys (currently just `separator`, default `''`).
56
+ */
57
+ export function join(args, extra = {}) {
58
+ const { separator = '' } = extra;
59
+ const items = Array.isArray(args) ? processValue(args) : [args];
60
+ return items.join(separator);
61
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * join — synchronous compute op for the ` =&` operator.
3
+ *
4
+ * Joins a resolved array into a single string. Supports nested sub-arrays
5
+ * with "all-or-nothing" semantics: if any element in a nested sub-array
6
+ * resolves to null/undefined, the entire sub-array is dropped.
7
+ *
8
+ * This is the sync-op successor to the old `builtIns.join` handler (which lived
9
+ * behind the async ` =>` pipeline). Unlike a handler, an op is a plain function —
10
+ * no class, no dynamic import, no await anywhere in its path.
11
+ *
12
+ * @example
13
+ * assignFrom(oElement, {
14
+ * '?.textContent =&': {
15
+ * join: ['?.lastName', ', ', '?.firstName']
16
+ * }
17
+ * }, { from: vm });
18
+ *
19
+ * @example
20
+ * // With optional segment (all-or-nothing) and a separator:
21
+ * assignFrom(oElement, {
22
+ * '?.textContent =&': {
23
+ * join: ['?.lastName', ['?.middleName'], '?.firstName'],
24
+ * separator: ', '
25
+ * }
26
+ * }, { from: vm });
27
+ * // If middleName is undefined, the sub-array ['?.middleName'] is dropped entirely.
28
+ */
29
+
30
+ /**
31
+ * Process nested arrays with all-or-nothing null semantics.
32
+ * - Top-level null/undefined values are filtered out.
33
+ * - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
34
+ * - Nested sub-arrays that pass are flattened into the result.
35
+ */
36
+ function processValue(value: any[]): any[] {
37
+ const result: any[] = [];
38
+ for (const item of value) {
39
+ if (Array.isArray(item)) {
40
+ // All-or-nothing: if any element is null/undefined, drop the entire sub-array
41
+ if (item.some(el => el == null)) {
42
+ continue;
43
+ }
44
+ // Sub-array passes — flatten its elements (recursively process nested arrays)
45
+ result.push(...processValue(item));
46
+ } else if (item != null) {
47
+ result.push(item);
48
+ }
49
+ // Top-level null/undefined are silently filtered out
50
+ }
51
+ return result;
52
+ }
53
+
54
+ /**
55
+ * `join` sync op — args is the resolved `join:` array, extra carries sibling
56
+ * config keys (currently just `separator`, default `''`).
57
+ */
58
+ export function join(args: any, extra: Record<string, any> = {}): string {
59
+ const { separator = '' } = extra;
60
+ const items = Array.isArray(args) ? processValue(args) : [args];
61
+ return items.join(separator);
62
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * registry.ts — Registry of ` =&` sync ops.
3
+ *
4
+ * Every op is a plain, synchronous, side-effect-free function: (args, extra) => value.
5
+ * `args` is the resolved value of the key matching the op's own name (e.g. `join:`),
6
+ * `extra` carries any sibling config keys (e.g. `separator:`). All statically
7
+ * imported — no dynamic loading, unlike the ` =>` built-in handler map — because
8
+ * the entire point of ` =&` is to never introduce an await.
9
+ *
10
+ * Add new sync ops here as they're written.
11
+ */
12
+ import { join } from './join.js';
13
+ export const SYNC_OPS = {
14
+ join,
15
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * registry.ts — Registry of ` =&` sync ops.
3
+ *
4
+ * Every op is a plain, synchronous, side-effect-free function: (args, extra) => value.
5
+ * `args` is the resolved value of the key matching the op's own name (e.g. `join:`),
6
+ * `extra` carries any sibling config keys (e.g. `separator:`). All statically
7
+ * imported — no dynamic loading, unlike the ` =>` built-in handler map — because
8
+ * the entire point of ` =&` is to never introduce an await.
9
+ *
10
+ * Add new sync ops here as they're written.
11
+ */
12
+
13
+ import { join } from './join.js';
14
+
15
+ export type SyncOp = (args: any, extra: Record<string, any>) => any;
16
+
17
+ export const SYNC_OPS: Record<string, SyncOp> = {
18
+ join,
19
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * resolveLhsPath.ts — Shared LHS path resolution for operator commands.
3
+ *
4
+ * Resolves the `?.`-prefixed path in front of an operator suffix (` =>`, ` =&`, ...)
5
+ * against a target, preserving the parent object and final key so the caller can
6
+ * assign a computed/returned value back to that path.
7
+ *
8
+ * Shared by processHandlerCommands.ts (` =>`) and the sync-op dispatcher in
9
+ * assignFrom.ts (` =&`) — the two operators that hand a value back to the LHS
10
+ * rather than assigning it directly.
11
+ */
12
+ import { evaluatePathWithMethods } from '../assignGingerly.js';
13
+ export function resolveLhsPath(target, lhsPath, options) {
14
+ let lhsTarget;
15
+ let lhsParent = undefined;
16
+ let lhsKey = undefined;
17
+ if (lhsPath.startsWith('?.')) {
18
+ const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
19
+ const withMethodsSet = options.withMethods
20
+ ? options.withMethods instanceof Set
21
+ ? options.withMethods
22
+ : new Set(options.withMethods)
23
+ : undefined;
24
+ if (withMethodsSet && pathParts.length > 0) {
25
+ const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
26
+ lhsParent = result.target;
27
+ lhsKey = result.lastKey;
28
+ lhsTarget = result.target[result.lastKey];
29
+ // If last key is a method, call it to get the target
30
+ if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
31
+ lhsTarget = result.target[result.lastKey].call(result.target);
32
+ lhsParent = undefined; // Can't assign back to a method call result
33
+ lhsKey = undefined;
34
+ }
35
+ }
36
+ else {
37
+ // Simple path navigation — walk to parent, keep last key
38
+ if (pathParts.length === 0) {
39
+ lhsTarget = target;
40
+ }
41
+ else if (pathParts.length === 1) {
42
+ lhsParent = target;
43
+ lhsKey = pathParts[0];
44
+ lhsTarget = target[pathParts[0]];
45
+ }
46
+ else {
47
+ let current = target;
48
+ for (let i = 0; i < pathParts.length - 1; i++) {
49
+ if (current == null)
50
+ break;
51
+ current = current[pathParts[i]];
52
+ }
53
+ lhsParent = current;
54
+ lhsKey = pathParts[pathParts.length - 1];
55
+ lhsTarget = current != null ? current[lhsKey] : undefined;
56
+ }
57
+ }
58
+ }
59
+ else if (lhsPath) {
60
+ lhsParent = target;
61
+ lhsKey = lhsPath;
62
+ lhsTarget = target[lhsPath];
63
+ }
64
+ else {
65
+ lhsTarget = target;
66
+ }
67
+ return { lhsTarget, lhsParent, lhsKey };
68
+ }