assign-gingerly 0.0.84 → 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/parseWithAttrs.js CHANGED
@@ -3,19 +3,61 @@ import { resolveTemplate } from './resolve/resolveTemplate.js';
3
3
  // Module-level cache for parsed attribute values
4
4
  // Structure: Map<configKey, Map<attrValue, parsedValue>>
5
5
  const parseCache = new Map();
6
+ /**
7
+ * Detects whether a value is a class-based parser constructor.
8
+ * Class parsers must have a `parse` method on their prototype.
9
+ */
10
+ function isAttrParserConstructor(value) {
11
+ return typeof value === 'function' && !!value.prototype && 'parse' in value.prototype;
12
+ }
13
+ /**
14
+ * Instantiates a class parser and returns a wrapper function that calls its parse method.
15
+ * A new instance is created for each attribute parse (no cross-call caching).
16
+ */
17
+ function instantiateClassParser(ParserCtor, options) {
18
+ const instance = new ParserCtor(options);
19
+ return (attrValue, context) => instance.parse(attrValue, context);
20
+ }
21
+ /**
22
+ * Looks up a named parser in the scoped (if available) then global registry.
23
+ */
24
+ function lookupNamedParser(name, synthesizerElement) {
25
+ // Check scoped registry first (if synthesizerElement provided)
26
+ if (synthesizerElement) {
27
+ const scopedRegistry = getParserRegistry(synthesizerElement);
28
+ const scopedParser = scopedRegistry.get(name);
29
+ if (scopedParser) {
30
+ return scopedParser;
31
+ }
32
+ }
33
+ // Fallback to global registry
34
+ const globalParser = globalParserRegistry.get(name);
35
+ if (globalParser) {
36
+ return globalParser;
37
+ }
38
+ // Not found in either registry
39
+ throw new Error(`Parser "${name}" not found. ` +
40
+ `Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
41
+ `Ensure the parser is registered via:\n` +
42
+ `- <script type="emc-parser" src="..." parser-name="${name}">\n` +
43
+ `- registerParser(synthesizerElement, "${name}", parserFn)\n` +
44
+ `- globalParserRegistry.register("${name}", parserFn)`);
45
+ }
6
46
  /**
7
47
  * Resolves a parser specification to an actual parser function
8
48
  * Supports:
9
49
  * - Inline functions (direct use)
10
- * - Named parsers from scoped registry (if synthesizerElement provided)
11
- * - Named parsers from global registry (fallback)
50
+ * - Named parser functions or class constructors from scoped/global registry (string form)
51
+ * - Named class parsers with options from the object form { name, options }
52
+ * - Custom element static methods via tuple form [elementName, methodName]
12
53
  *
13
- * @param parserSpec - Parser function or string reference
54
+ * @param parserSpec - Parser function, string reference, tuple, or object reference
14
55
  * @param synthesizerElement - Optional synthesizer element for scoped parser lookup
56
+ * @param parserOptions - Optional constructor options when a string resolves to a class parser
15
57
  * @returns The resolved parser function
16
58
  * @throws Error if parser cannot be resolved
17
59
  */
18
- function resolveParser(parserSpec, synthesizerElement) {
60
+ function resolveParser(parserSpec, synthesizerElement, parserOptions) {
19
61
  // Undefined - no parser specified
20
62
  if (parserSpec === undefined) {
21
63
  return undefined;
@@ -24,31 +66,61 @@ function resolveParser(parserSpec, synthesizerElement) {
24
66
  if (typeof parserSpec === 'function') {
25
67
  return parserSpec;
26
68
  }
69
+ // Object form: { name, options }
70
+ if (parserSpec !== null &&
71
+ typeof parserSpec === 'object' &&
72
+ !Array.isArray(parserSpec) &&
73
+ 'name' in parserSpec) {
74
+ const { name, options } = parserSpec;
75
+ const registered = lookupNamedParser(name, synthesizerElement);
76
+ if (isAttrParserConstructor(registered)) {
77
+ return instantiateClassParser(registered, options);
78
+ }
79
+ return registered;
80
+ }
27
81
  // String reference - resolve from scoped or global registry
28
82
  if (typeof parserSpec === 'string') {
29
- // Check scoped registry first (if synthesizerElement provided)
30
- if (synthesizerElement) {
31
- const scopedRegistry = getParserRegistry(synthesizerElement);
32
- const scopedParser = scopedRegistry.get(parserSpec);
33
- if (scopedParser) {
34
- return scopedParser;
35
- }
83
+ const registered = lookupNamedParser(parserSpec, synthesizerElement);
84
+ if (isAttrParserConstructor(registered)) {
85
+ return instantiateClassParser(registered, parserOptions);
86
+ }
87
+ return registered;
88
+ }
89
+ // Tuple reference: [elementName, methodName]
90
+ if (Array.isArray(parserSpec) && parserSpec.length === 2) {
91
+ const [elementName, methodName] = parserSpec;
92
+ const Ctr = customElements.get(elementName);
93
+ if (!Ctr) {
94
+ throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`);
36
95
  }
37
- // Fallback to global registry
38
- const globalParser = globalParserRegistry.get(parserSpec);
39
- if (globalParser) {
40
- return globalParser;
96
+ const method = Ctr[methodName];
97
+ if (typeof method !== 'function') {
98
+ throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`);
41
99
  }
42
- // Not found in either registry
43
- throw new Error(`Parser "${parserSpec}" not found. ` +
44
- `Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
45
- `Ensure the parser is registered via:\n` +
46
- `- <script type="emc-parser" src="..." parser-name="${parserSpec}">\n` +
47
- `- registerParser(synthesizerElement, "${parserSpec}", parserFn)\n` +
48
- `- globalParserRegistry.register("${parserSpec}", parserFn)`);
100
+ return method.bind(Ctr);
49
101
  }
50
102
  return undefined;
51
103
  }
104
+ /**
105
+ * Serializes parser options for use in cache keys.
106
+ * RegExp values are normalized to their string representation.
107
+ */
108
+ function serializeParserOptions(options) {
109
+ if (options === undefined || options === null) {
110
+ return '';
111
+ }
112
+ try {
113
+ return JSON.stringify(options, (_key, value) => {
114
+ if (value instanceof RegExp) {
115
+ return value.toString();
116
+ }
117
+ return value;
118
+ });
119
+ }
120
+ catch (_e) {
121
+ return String(options);
122
+ }
123
+ }
52
124
  /**
53
125
  * Creates a cache key from an AttrConfig
54
126
  * Includes instanceOf and parser identifier to ensure correct cache hits
@@ -63,7 +135,19 @@ function getCacheKey(config) {
63
135
  parserStr = 'builtin';
64
136
  }
65
137
  else if (typeof config.parser === 'string') {
66
- parserStr = `named:${config.parser}`;
138
+ const optionsStr = serializeParserOptions(config.parserOptions);
139
+ parserStr = `named:${config.parser}|options:${optionsStr}`;
140
+ }
141
+ else if (config.parser !== null &&
142
+ typeof config.parser === 'object' &&
143
+ !Array.isArray(config.parser) &&
144
+ 'name' in config.parser) {
145
+ const ref = config.parser;
146
+ const optionsStr = serializeParserOptions(ref.options);
147
+ parserStr = `named:${ref.name}|options:${optionsStr}`;
148
+ }
149
+ else if (Array.isArray(config.parser)) {
150
+ parserStr = `tuple:${config.parser[0]},${config.parser[1]}`;
67
151
  }
68
152
  else {
69
153
  parserStr = 'custom';
@@ -328,7 +412,7 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed, spawnCont
328
412
  }
329
413
  // For Boolean without valIfNull, fall through to parser
330
414
  else {
331
- const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
415
+ const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
332
416
  const parsedValue = callParser(parser, attrValue, parserContext);
333
417
  const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
334
418
  result[mapsTo] = parsedValue;
@@ -336,7 +420,7 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed, spawnCont
336
420
  continue;
337
421
  }
338
422
  // Attribute exists - parse normally
339
- const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
423
+ const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
340
424
  // Use cache if parseCache is specified
341
425
  const parsedValue = config.parseCache
342
426
  ? parseWithCache(attrValue, config, parser, parserContext)
package/parseWithAttrs.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AttrPatterns, AttrConfig, ParserFunction, ParserContext, SpawnContext } from './types/assign-gingerly/types';
1
+ import { AttrPatterns, AttrConfig, ParserFunction, ParserContext, SpawnContext, ParserSpec, AttrParserConstructor, NamedParserRef } from './types/assign-gingerly/types';
2
2
  import { globalParserRegistry, getParserRegistry } from './parserRegistry.js';
3
3
  import { resolveTemplate } from './resolve/resolveTemplate.js';
4
4
 
@@ -6,21 +6,79 @@ import { resolveTemplate } from './resolve/resolveTemplate.js';
6
6
  // Structure: Map<configKey, Map<attrValue, parsedValue>>
7
7
  const parseCache = new Map<string, Map<string, any>>();
8
8
 
9
+ /**
10
+ * Detects whether a value is a class-based parser constructor.
11
+ * Class parsers must have a `parse` method on their prototype.
12
+ */
13
+ function isAttrParserConstructor(
14
+ value: ParserFunction | AttrParserConstructor | undefined
15
+ ): value is AttrParserConstructor {
16
+ return typeof value === 'function' && !!value.prototype && 'parse' in value.prototype;
17
+ }
18
+
19
+ /**
20
+ * Instantiates a class parser and returns a wrapper function that calls its parse method.
21
+ * A new instance is created for each attribute parse (no cross-call caching).
22
+ */
23
+ function instantiateClassParser(
24
+ ParserCtor: AttrParserConstructor,
25
+ options?: any
26
+ ): ParserFunction {
27
+ const instance = new ParserCtor(options);
28
+ return (attrValue, context) => instance.parse(attrValue, context);
29
+ }
30
+
31
+ /**
32
+ * Looks up a named parser in the scoped (if available) then global registry.
33
+ */
34
+ function lookupNamedParser(
35
+ name: string,
36
+ synthesizerElement?: Element
37
+ ): ParserFunction | AttrParserConstructor {
38
+ // Check scoped registry first (if synthesizerElement provided)
39
+ if (synthesizerElement) {
40
+ const scopedRegistry = getParserRegistry(synthesizerElement);
41
+ const scopedParser = scopedRegistry.get(name);
42
+ if (scopedParser) {
43
+ return scopedParser;
44
+ }
45
+ }
46
+
47
+ // Fallback to global registry
48
+ const globalParser = globalParserRegistry.get(name);
49
+ if (globalParser) {
50
+ return globalParser;
51
+ }
52
+
53
+ // Not found in either registry
54
+ throw new Error(
55
+ `Parser "${name}" not found. ` +
56
+ `Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
57
+ `Ensure the parser is registered via:\n` +
58
+ `- <script type="emc-parser" src="..." parser-name="${name}">\n` +
59
+ `- registerParser(synthesizerElement, "${name}", parserFn)\n` +
60
+ `- globalParserRegistry.register("${name}", parserFn)`
61
+ );
62
+ }
63
+
9
64
  /**
10
65
  * Resolves a parser specification to an actual parser function
11
66
  * Supports:
12
67
  * - Inline functions (direct use)
13
- * - Named parsers from scoped registry (if synthesizerElement provided)
14
- * - Named parsers from global registry (fallback)
68
+ * - Named parser functions or class constructors from scoped/global registry (string form)
69
+ * - Named class parsers with options from the object form { name, options }
70
+ * - Custom element static methods via tuple form [elementName, methodName]
15
71
  *
16
- * @param parserSpec - Parser function or string reference
72
+ * @param parserSpec - Parser function, string reference, tuple, or object reference
17
73
  * @param synthesizerElement - Optional synthesizer element for scoped parser lookup
74
+ * @param parserOptions - Optional constructor options when a string resolves to a class parser
18
75
  * @returns The resolved parser function
19
76
  * @throws Error if parser cannot be resolved
20
77
  */
21
78
  function resolveParser(
22
- parserSpec: ParserFunction | string | undefined,
23
- synthesizerElement?: Element
79
+ parserSpec: ParserSpec | undefined,
80
+ synthesizerElement?: Element,
81
+ parserOptions?: any
24
82
  ): ParserFunction | undefined {
25
83
  // Undefined - no parser specified
26
84
  if (parserSpec === undefined) {
@@ -32,37 +90,71 @@ function resolveParser(
32
90
  return parserSpec;
33
91
  }
34
92
 
93
+ // Object form: { name, options }
94
+ if (
95
+ parserSpec !== null &&
96
+ typeof parserSpec === 'object' &&
97
+ !Array.isArray(parserSpec) &&
98
+ 'name' in parserSpec
99
+ ) {
100
+ const { name, options } = parserSpec as NamedParserRef;
101
+ const registered = lookupNamedParser(name, synthesizerElement);
102
+ if (isAttrParserConstructor(registered)) {
103
+ return instantiateClassParser(registered, options);
104
+ }
105
+ return registered;
106
+ }
107
+
35
108
  // String reference - resolve from scoped or global registry
36
109
  if (typeof parserSpec === 'string') {
37
- // Check scoped registry first (if synthesizerElement provided)
38
- if (synthesizerElement) {
39
- const scopedRegistry = getParserRegistry(synthesizerElement);
40
- const scopedParser = scopedRegistry.get(parserSpec);
41
- if (scopedParser) {
42
- return scopedParser;
43
- }
110
+ const registered = lookupNamedParser(parserSpec, synthesizerElement);
111
+ if (isAttrParserConstructor(registered)) {
112
+ return instantiateClassParser(registered, parserOptions);
44
113
  }
45
-
46
- // Fallback to global registry
47
- const globalParser = globalParserRegistry.get(parserSpec);
48
- if (globalParser) {
49
- return globalParser;
114
+ return registered;
115
+ }
116
+
117
+ // Tuple reference: [elementName, methodName]
118
+ if (Array.isArray(parserSpec) && parserSpec.length === 2) {
119
+ const [elementName, methodName] = parserSpec;
120
+ const Ctr = customElements.get(elementName);
121
+ if (!Ctr) {
122
+ throw new Error(
123
+ `Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`
124
+ );
50
125
  }
51
-
52
- // Not found in either registry
53
- throw new Error(
54
- `Parser "${parserSpec}" not found. ` +
55
- `Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
56
- `Ensure the parser is registered via:\n` +
57
- `- <script type="emc-parser" src="..." parser-name="${parserSpec}">\n` +
58
- `- registerParser(synthesizerElement, "${parserSpec}", parserFn)\n` +
59
- `- globalParserRegistry.register("${parserSpec}", parserFn)`
60
- );
126
+ const method = (Ctr as any)[methodName];
127
+ if (typeof method !== 'function') {
128
+ throw new Error(
129
+ `Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`
130
+ );
131
+ }
132
+ return method.bind(Ctr);
61
133
  }
62
134
 
63
135
  return undefined;
64
136
  }
65
137
 
138
+ /**
139
+ * Serializes parser options for use in cache keys.
140
+ * RegExp values are normalized to their string representation.
141
+ */
142
+ function serializeParserOptions(options: any): string {
143
+ if (options === undefined || options === null) {
144
+ return '';
145
+ }
146
+ try {
147
+ return JSON.stringify(options, (_key, value) => {
148
+ if (value instanceof RegExp) {
149
+ return value.toString();
150
+ }
151
+ return value;
152
+ });
153
+ } catch (_e) {
154
+ return String(options);
155
+ }
156
+ }
157
+
66
158
  /**
67
159
  * Creates a cache key from an AttrConfig
68
160
  * Includes instanceOf and parser identifier to ensure correct cache hits
@@ -77,7 +169,19 @@ function getCacheKey(config: AttrConfig<any>): string {
77
169
  if (config.parser === undefined) {
78
170
  parserStr = 'builtin';
79
171
  } else if (typeof config.parser === 'string') {
80
- parserStr = `named:${config.parser}`;
172
+ const optionsStr = serializeParserOptions(config.parserOptions);
173
+ parserStr = `named:${config.parser}|options:${optionsStr}`;
174
+ } else if (
175
+ config.parser !== null &&
176
+ typeof config.parser === 'object' &&
177
+ !Array.isArray(config.parser) &&
178
+ 'name' in config.parser
179
+ ) {
180
+ const ref = config.parser as NamedParserRef;
181
+ const optionsStr = serializeParserOptions(ref.options);
182
+ parserStr = `named:${ref.name}|options:${optionsStr}`;
183
+ } else if (Array.isArray(config.parser)) {
184
+ parserStr = `tuple:${config.parser[0]},${config.parser[1]}`;
81
185
  } else {
82
186
  parserStr = 'custom';
83
187
  }
@@ -390,7 +494,7 @@ export function parseWithAttrs<T = any>(
390
494
  }
391
495
  // For Boolean without valIfNull, fall through to parser
392
496
  else {
393
- const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
497
+ const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
394
498
  const parsedValue = callParser(parser, attrValue, parserContext);
395
499
  const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
396
500
  result[mapsTo as string] = parsedValue;
@@ -399,7 +503,7 @@ export function parseWithAttrs<T = any>(
399
503
  }
400
504
 
401
505
  // Attribute exists - parse normally
402
- const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
506
+ const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
403
507
 
404
508
  // Use cache if parseCache is specified
405
509
  const parsedValue = config.parseCache
package/parserRegistry.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { SplitParser } from './SplitParser.js';
2
+ import { ScopedParserRegistry } from './ScopedParserRegistry.js';
1
3
  /**
2
4
  * Registry for named parsers that can be referenced by string name
3
5
  * Enables JSON serialization of configs with custom parsers
@@ -7,7 +9,7 @@ export class ParserRegistry {
7
9
  /**
8
10
  * Register a parser with a given name
9
11
  * @param name - The name to register the parser under
10
- * @param parser - The parser function
12
+ * @param parser - The parser function or class constructor
11
13
  */
12
14
  register(name, parser) {
13
15
  if (this.parsers.has(name)) {
@@ -18,7 +20,7 @@ export class ParserRegistry {
18
20
  /**
19
21
  * Get a parser by name
20
22
  * @param name - The name of the parser
21
- * @returns The parser function or undefined if not found
23
+ * @returns The parser function, class constructor, or undefined if not found
22
24
  */
23
25
  get(name) {
24
26
  return this.parsers.get(name);
@@ -55,7 +57,6 @@ export const globalParserRegistry = new ParserRegistry();
55
57
  // Register common built-in parsers
56
58
  globalParserRegistry.register('timestamp', (v) => v ? new Date(v).getTime() : null);
57
59
  globalParserRegistry.register('date', (v) => v ? new Date(v) : null);
58
- globalParserRegistry.register('csv', (v) => v ? v.split(',').map((s) => s.trim()) : []);
59
60
  globalParserRegistry.register('int', (v) => v ? parseInt(v, 10) : null);
60
61
  globalParserRegistry.register('float', (v) => v ? parseFloat(v) : null);
61
62
  globalParserRegistry.register('boolean', (v) => v !== null);
@@ -69,7 +70,7 @@ globalParserRegistry.register('json', (v) => {
69
70
  throw new Error(`Failed to parse JSON: "${v}". Error: ${e}`);
70
71
  }
71
72
  });
72
- import { ScopedParserRegistry } from './ScopedParserRegistry.js';
73
+ globalParserRegistry.register('splitter', SplitParser);
73
74
  /**
74
75
  * Symbol for storing scoped parser registry on synthesizer elements
75
76
  * Using Symbol.for ensures the same symbol is used across different versions of the package
package/parserRegistry.ts CHANGED
@@ -1,18 +1,20 @@
1
- import { ParserFunction } from './types/assign-gingerly/types';
1
+ import { ParserFunction, AttrParserConstructor } from './types/assign-gingerly/types';
2
+ import { SplitParser } from './SplitParser.js';
3
+ import { ScopedParserRegistry } from './ScopedParserRegistry.js';
2
4
 
3
5
  /**
4
6
  * Registry for named parsers that can be referenced by string name
5
7
  * Enables JSON serialization of configs with custom parsers
6
8
  */
7
9
  export class ParserRegistry {
8
- private parsers = new Map<string, ParserFunction>();
10
+ private parsers = new Map<string, ParserFunction | AttrParserConstructor>();
9
11
 
10
12
  /**
11
13
  * Register a parser with a given name
12
14
  * @param name - The name to register the parser under
13
- * @param parser - The parser function
15
+ * @param parser - The parser function or class constructor
14
16
  */
15
- register(name: string, parser: ParserFunction): void {
17
+ register(name: string, parser: ParserFunction | AttrParserConstructor): void {
16
18
  if (this.parsers.has(name)) {
17
19
  console.warn(`Parser "${name}" already registered, overwriting`);
18
20
  }
@@ -22,9 +24,9 @@ export class ParserRegistry {
22
24
  /**
23
25
  * Get a parser by name
24
26
  * @param name - The name of the parser
25
- * @returns The parser function or undefined if not found
27
+ * @returns The parser function, class constructor, or undefined if not found
26
28
  */
27
- get(name: string): ParserFunction | undefined {
29
+ get(name: string): ParserFunction | AttrParserConstructor | undefined {
28
30
  return this.parsers.get(name);
29
31
  }
30
32
 
@@ -70,10 +72,6 @@ globalParserRegistry.register('date', (v: string | null) =>
70
72
  v ? new Date(v) : null
71
73
  );
72
74
 
73
- globalParserRegistry.register('csv', (v: string | null) =>
74
- v ? v.split(',').map((s: string) => s.trim()) : []
75
- );
76
-
77
75
  globalParserRegistry.register('int', (v: string | null) =>
78
76
  v ? parseInt(v, 10) : null
79
77
  );
@@ -95,7 +93,7 @@ globalParserRegistry.register('json', (v: string | null) => {
95
93
  }
96
94
  });
97
95
 
98
- import { ScopedParserRegistry } from './ScopedParserRegistry.js';
96
+ globalParserRegistry.register('splitter', SplitParser);
99
97
 
100
98
  /**
101
99
  * Symbol for storing scoped parser registry on synthesizer elements
@@ -129,7 +127,7 @@ export function getParserRegistry(synthesizerElement: Element): ScopedParserRegi
129
127
  export function registerParser(
130
128
  synthesizerElement: Element,
131
129
  name: string,
132
- parser: ParserFunction
130
+ parser: ParserFunction | AttrParserConstructor
133
131
  ): void {
134
132
  const registry = getParserRegistry(synthesizerElement);
135
133
  registry.register(name, parser);
@@ -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