assign-gingerly 0.0.59 → 0.0.60
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 +269 -12
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +190 -12
- package/assignFrom.ts +184 -11
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +11 -9
- package/assignFromAsync.ts +27 -11
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +25 -0
- package/builtInEmoji.ts +33 -0
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +46 -1
- package/handlers/manageTemplateList.js +54 -31
- package/handlers/manageTemplateList.ts +51 -28
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +34 -4
- package/inferredAssignments.ts +56 -6
- package/package.json +13 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +6 -1
- package/processHandlerCommands.ts +7 -1
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/types/assign-gingerly/types.d.ts +10 -0
- package/withIdsCorrector.js +47 -0
- package/withIdsCorrector.ts +59 -0
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* },
|
|
19
19
|
* fromEachItem: {
|
|
20
20
|
* assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
21
|
-
* withOptions: { withMethods: ['querySelector'],
|
|
21
|
+
* withOptions: { withMethods: ['querySelector'], infer: true },
|
|
22
22
|
* resolve: { key: '?.rank' }
|
|
23
23
|
* }
|
|
24
24
|
* }
|
|
@@ -75,6 +75,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const fromEachItem = this.config.fromEachItem;
|
|
78
|
+
const configs = fromEachItem?.configs; // Array form for multi-element templates
|
|
78
79
|
const assignToFragment = fromEachItem?.assignToFragment ?? {};
|
|
79
80
|
const withOptions = fromEachItem?.withOptions ?? {};
|
|
80
81
|
const perItemResolve = fromEachItem?.resolve ?? {};
|
|
@@ -85,10 +86,10 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
85
86
|
const sourceAssignToFragment = fromSource?.assignToFragment;
|
|
86
87
|
const sourceWithOptions = fromSource?.withOptions ?? {};
|
|
87
88
|
|
|
88
|
-
// Detect fast path: no assignToFragment patterns, just
|
|
89
|
-
const hasAssignPatterns = Object.keys(assignToFragment).length > 0;
|
|
90
|
-
const inferredConfig = withOptions.
|
|
91
|
-
const useFastPath = !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
89
|
+
// Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
|
|
90
|
+
const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
|
|
91
|
+
const inferredConfig = !configs && withOptions.infer;
|
|
92
|
+
const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
92
93
|
|
|
93
94
|
const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
|
|
94
95
|
|
|
@@ -149,17 +150,28 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
149
150
|
if (state.keyToNodes.has(key) && oldKeys.has(key)) {
|
|
150
151
|
// Existing item — update in place
|
|
151
152
|
const existingNodes = state.keyToNodes.get(key)!;
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
154
|
+
if (shouldYield) await new Promise(r => setTimeout(r, 0));
|
|
155
|
+
|
|
156
|
+
if (configs) {
|
|
157
|
+
// Multi-element: zip configs with element nodes
|
|
158
|
+
const elements = existingNodes.filter(n => n instanceof Element) as Element[];
|
|
159
|
+
const len = Math.min(elements.length, configs.length);
|
|
160
|
+
for (let j = 0; j < len; j++) {
|
|
161
|
+
const cfg = configs[j];
|
|
162
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
160
163
|
}
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
} else {
|
|
165
|
+
const rootEl = existingNodes.find(n => n instanceof Element) as Element | undefined;
|
|
166
|
+
if (rootEl) {
|
|
167
|
+
if (processInferred) {
|
|
168
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
169
|
+
} else {
|
|
170
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
171
|
+
}
|
|
172
|
+
if (sourceAssignToFragment && options?.from) {
|
|
173
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
174
|
+
}
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
newKeyToNodes.set(key, existingNodes);
|
|
@@ -180,21 +192,32 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
180
192
|
const clonedNodes = Array.from(content.childNodes);
|
|
181
193
|
|
|
182
194
|
// Apply per-item assignments to the cloned fragment
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
195
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
196
|
+
if (shouldYield) await new Promise(r => setTimeout(r, 0));
|
|
197
|
+
|
|
198
|
+
if (configs) {
|
|
199
|
+
// Multi-element: zip configs with element nodes
|
|
200
|
+
const elements = clonedNodes.filter(n => n instanceof Element) as Element[];
|
|
201
|
+
const len = Math.min(elements.length, configs.length);
|
|
202
|
+
for (let j = 0; j < len; j++) {
|
|
203
|
+
const cfg = configs[j];
|
|
204
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
194
205
|
}
|
|
206
|
+
} else {
|
|
207
|
+
const rootEl = clonedNodes.find(n => n instanceof Element) as Element | undefined;
|
|
208
|
+
if (rootEl) {
|
|
209
|
+
const tempContainer = document.createDocumentFragment();
|
|
210
|
+
tempContainer.appendChild(content);
|
|
195
211
|
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
if (processInferred) {
|
|
213
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
214
|
+
} else {
|
|
215
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (sourceAssignToFragment && options?.from) {
|
|
219
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
220
|
+
}
|
|
198
221
|
}
|
|
199
222
|
}
|
|
200
223
|
|
package/inferencer/inferencer.js
CHANGED
|
@@ -81,38 +81,26 @@ export class Infer {
|
|
|
81
81
|
return inferValueProperty(this.enhancedElement);
|
|
82
82
|
}
|
|
83
83
|
['|'](itempropAttr, scopeBoundary = '[itemscope]') {
|
|
84
|
-
|
|
85
|
-
return Array.from(candidates)
|
|
86
|
-
.filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
87
|
-
.map(x => new Infer(x, itempropAttr));
|
|
84
|
+
return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
|
|
88
85
|
}
|
|
89
86
|
['@'](nameAttr, scopeBoundary) {
|
|
90
|
-
|
|
91
|
-
const filtered = scopeBoundary
|
|
92
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
93
|
-
: Array.from(candidates);
|
|
94
|
-
return filtered.map(x => new Infer(x, nameAttr));
|
|
87
|
+
return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
|
|
95
88
|
}
|
|
96
89
|
['%'](partAttr, scopeBoundary) {
|
|
97
|
-
|
|
98
|
-
const filtered = scopeBoundary
|
|
99
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
100
|
-
: Array.from(candidates);
|
|
101
|
-
return filtered.map(x => new Infer(x, partAttr));
|
|
90
|
+
return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
|
|
102
91
|
}
|
|
103
92
|
['#'](id, scopeBoundary) {
|
|
104
|
-
|
|
105
|
-
const filtered = scopeBoundary
|
|
106
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
107
|
-
: Array.from(candidates);
|
|
108
|
-
return filtered.map(x => new Infer(x, id));
|
|
93
|
+
return this.#queryScoped(`#${id}`, id, scopeBoundary);
|
|
109
94
|
}
|
|
110
95
|
['.'](className, scopeBoundary) {
|
|
111
|
-
|
|
96
|
+
return this.#queryScoped(`.${className}`, className, scopeBoundary);
|
|
97
|
+
}
|
|
98
|
+
#queryScoped(selector, propName, scopeBoundary) {
|
|
99
|
+
const candidates = this.enhancedElement.querySelectorAll(selector);
|
|
112
100
|
const filtered = scopeBoundary
|
|
113
101
|
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
114
102
|
: Array.from(candidates);
|
|
115
|
-
return filtered.map(x => new Infer(x,
|
|
103
|
+
return filtered.map(x => new Infer(x, propName));
|
|
116
104
|
}
|
|
117
105
|
setDisplay(vm) {
|
|
118
106
|
const val = this.#propName ? vm[this.#propName] : inferBindingProperty(this.enhancedElement);
|
package/inferencer/inferencer.ts
CHANGED
|
@@ -103,42 +103,31 @@ export class Infer<TValue = any, TDisplay = any> {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
['|'](itempropAttr: string, scopeBoundary: string = '[itemscope]'){
|
|
106
|
-
|
|
107
|
-
return Array.from(candidates)
|
|
108
|
-
.filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
109
|
-
.map(x => new Infer(x, itempropAttr));
|
|
106
|
+
return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
|
|
110
107
|
}
|
|
111
108
|
|
|
112
109
|
['@'](nameAttr: string, scopeBoundary?: string){
|
|
113
|
-
|
|
114
|
-
const filtered = scopeBoundary
|
|
115
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
116
|
-
: Array.from(candidates);
|
|
117
|
-
return filtered.map(x => new Infer(x, nameAttr));
|
|
110
|
+
return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
['%'](partAttr: string, scopeBoundary?: string){
|
|
121
|
-
|
|
122
|
-
const filtered = scopeBoundary
|
|
123
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
124
|
-
: Array.from(candidates);
|
|
125
|
-
return filtered.map(x => new Infer(x, partAttr));
|
|
114
|
+
return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
|
|
126
115
|
}
|
|
127
116
|
|
|
128
117
|
['#'](id: string, scopeBoundary?: string){
|
|
129
|
-
|
|
130
|
-
const filtered = scopeBoundary
|
|
131
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
132
|
-
: Array.from(candidates);
|
|
133
|
-
return filtered.map(x => new Infer(x, id));
|
|
118
|
+
return this.#queryScoped(`#${id}`, id, scopeBoundary);
|
|
134
119
|
}
|
|
135
120
|
|
|
136
121
|
['.'](className: string, scopeBoundary?: string){
|
|
137
|
-
|
|
122
|
+
return this.#queryScoped(`.${className}`, className, scopeBoundary);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#queryScoped(selector: string, propName: string, scopeBoundary?: string): Infer[] {
|
|
126
|
+
const candidates = this.enhancedElement.querySelectorAll(selector);
|
|
138
127
|
const filtered = scopeBoundary
|
|
139
128
|
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
140
129
|
: Array.from(candidates);
|
|
141
|
-
return filtered.map(x => new Infer(x,
|
|
130
|
+
return filtered.map(x => new Infer(x, propName));
|
|
142
131
|
}
|
|
143
132
|
|
|
144
133
|
setDisplay(vm: any){
|
package/inferredAssignments.js
CHANGED
|
@@ -17,11 +17,14 @@ export function processInferredAssignments(target, from, config) {
|
|
|
17
17
|
return;
|
|
18
18
|
if (!from || typeof from !== 'object')
|
|
19
19
|
return;
|
|
20
|
-
const { byItemprop } = config;
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const { byItemprop, byName } = config;
|
|
21
|
+
// Resolve aliases: '|' → byItemprop, '@' → byName
|
|
22
|
+
const effectiveByItemprop = byItemprop ?? config['|'];
|
|
23
|
+
const effectiveByName = byName ?? config['@'];
|
|
24
|
+
if (effectiveByItemprop) {
|
|
25
|
+
const keys = effectiveByItemprop === true
|
|
23
26
|
? Object.keys(from)
|
|
24
|
-
:
|
|
27
|
+
: effectiveByItemprop;
|
|
25
28
|
const infer = new Infer(target);
|
|
26
29
|
for (const key of keys) {
|
|
27
30
|
if (!(key in from))
|
|
@@ -35,4 +38,31 @@ export function processInferredAssignments(target, from, config) {
|
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
}
|
|
41
|
+
if (effectiveByName) {
|
|
42
|
+
// Normalize config
|
|
43
|
+
let keys;
|
|
44
|
+
let scopeBoundary;
|
|
45
|
+
if (effectiveByName === true) {
|
|
46
|
+
keys = Object.keys(from);
|
|
47
|
+
}
|
|
48
|
+
else if (Array.isArray(effectiveByName)) {
|
|
49
|
+
keys = effectiveByName;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Object form: { props, outside }
|
|
53
|
+
keys = effectiveByName.props === true ? Object.keys(from) : effectiveByName.props;
|
|
54
|
+
scopeBoundary = effectiveByName.outside;
|
|
55
|
+
}
|
|
56
|
+
const infer = new Infer(target);
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
if (!(key in from))
|
|
59
|
+
continue;
|
|
60
|
+
const value = from[key];
|
|
61
|
+
// Use inferencer's ['@'] method — by name attribute, optional scope boundary
|
|
62
|
+
const matches = infer['@'](key, scopeBoundary);
|
|
63
|
+
for (const match of matches) {
|
|
64
|
+
match.value = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
38
68
|
}
|
package/inferredAssignments.ts
CHANGED
|
@@ -20,8 +20,23 @@ export interface InferredAssignmentsConfig {
|
|
|
20
20
|
*/
|
|
21
21
|
byItemprop?: string[] | true;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Concise alias for byItemprop */
|
|
24
|
+
'|'?: string[] | true;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Array of property keys to distribute by name attribute.
|
|
28
|
+
* For each key, finds [name="${key}"] elements and sets the value using
|
|
29
|
+
* the inferred property (value, checked, etc.).
|
|
30
|
+
*
|
|
31
|
+
* Pass `true` to infer all keys from the `from` source object.
|
|
32
|
+
*
|
|
33
|
+
* Object form enables donut-hole scoping:
|
|
34
|
+
* { props: ['firstName', 'lastName'], outside: 'fieldset' }
|
|
35
|
+
*/
|
|
36
|
+
byName?: string[] | true | { props: string[] | true; outside: string };
|
|
37
|
+
|
|
38
|
+
/** Concise alias for byName */
|
|
39
|
+
'@'?: string[] | true | { props: string[] | true; outside: string };
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
/**
|
|
@@ -39,12 +54,16 @@ export function processInferredAssignments(
|
|
|
39
54
|
if (!(target instanceof Element)) return;
|
|
40
55
|
if (!from || typeof from !== 'object') return;
|
|
41
56
|
|
|
42
|
-
const { byItemprop } = config;
|
|
57
|
+
const { byItemprop, byName } = config;
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
// Resolve aliases: '|' → byItemprop, '@' → byName
|
|
60
|
+
const effectiveByItemprop = byItemprop ?? config['|'];
|
|
61
|
+
const effectiveByName = byName ?? config['@'];
|
|
62
|
+
|
|
63
|
+
if (effectiveByItemprop) {
|
|
64
|
+
const keys = effectiveByItemprop === true
|
|
46
65
|
? Object.keys(from)
|
|
47
|
-
:
|
|
66
|
+
: effectiveByItemprop;
|
|
48
67
|
|
|
49
68
|
const infer = new Infer(target);
|
|
50
69
|
|
|
@@ -62,4 +81,35 @@ export function processInferredAssignments(
|
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
}
|
|
84
|
+
|
|
85
|
+
if (effectiveByName) {
|
|
86
|
+
// Normalize config
|
|
87
|
+
let keys: string[];
|
|
88
|
+
let scopeBoundary: string | undefined;
|
|
89
|
+
|
|
90
|
+
if (effectiveByName === true) {
|
|
91
|
+
keys = Object.keys(from);
|
|
92
|
+
} else if (Array.isArray(effectiveByName)) {
|
|
93
|
+
keys = effectiveByName;
|
|
94
|
+
} else {
|
|
95
|
+
// Object form: { props, outside }
|
|
96
|
+
keys = effectiveByName.props === true ? Object.keys(from) : effectiveByName.props;
|
|
97
|
+
scopeBoundary = effectiveByName.outside;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const infer = new Infer(target);
|
|
101
|
+
|
|
102
|
+
for (const key of keys) {
|
|
103
|
+
if (!(key in from)) continue;
|
|
104
|
+
|
|
105
|
+
const value = from[key];
|
|
106
|
+
|
|
107
|
+
// Use inferencer's ['@'] method — by name attribute, optional scope boundary
|
|
108
|
+
const matches: Infer[] = infer['@'](key, scopeBoundary);
|
|
109
|
+
|
|
110
|
+
for (const match of matches) {
|
|
111
|
+
match.value = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
65
115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.60",
|
|
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": {
|
|
@@ -150,6 +150,18 @@
|
|
|
150
150
|
"default": "./assignFrom.js",
|
|
151
151
|
"types": "./assignFrom.ts"
|
|
152
152
|
},
|
|
153
|
+
"./assignFrom-extension.js": {
|
|
154
|
+
"default": "./assignFrom-extension.js",
|
|
155
|
+
"types": "./assignFrom-extension.ts"
|
|
156
|
+
},
|
|
157
|
+
"./assignFromAsync-extension.js": {
|
|
158
|
+
"default": "./assignFromAsync-extension.js",
|
|
159
|
+
"types": "./assignFromAsync-extension.ts"
|
|
160
|
+
},
|
|
161
|
+
"./builtInEmoji.js": {
|
|
162
|
+
"default": "./builtInEmoji.js",
|
|
163
|
+
"types": "./builtInEmoji.ts"
|
|
164
|
+
},
|
|
153
165
|
"./assignFeatures.js": {
|
|
154
166
|
"default": "./assignFeatures.js",
|
|
155
167
|
"types": "./assignFeatures.ts"
|
package/playwright.config.ts
CHANGED
|
@@ -16,9 +16,9 @@ export default defineConfig({
|
|
|
16
16
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
17
17
|
forbidOnly: !!process.env.CI,
|
|
18
18
|
/* Retry on CI only */
|
|
19
|
-
retries: process.env.CI ?
|
|
19
|
+
retries: process.env.CI ? 1 : 0,
|
|
20
20
|
/* Opt out of parallel tests on CI. */
|
|
21
|
-
workers: process.env.CI ?
|
|
21
|
+
workers: process.env.CI ? 2 : undefined,
|
|
22
22
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
23
23
|
reporter: [ ['html', { open: 'never' }] ],
|
|
24
24
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
@@ -50,5 +50,6 @@ export default defineConfig({
|
|
|
50
50
|
command: 'npm run serve',
|
|
51
51
|
port: 8000,
|
|
52
52
|
reuseExistingServer: !process.env.CI,
|
|
53
|
+
timeout: 30000,
|
|
53
54
|
},
|
|
54
55
|
});
|
|
@@ -70,8 +70,13 @@ async function resolveFromHandlers(name, handlers, permissions) {
|
|
|
70
70
|
if (typeof entry === 'function') {
|
|
71
71
|
return entry;
|
|
72
72
|
}
|
|
73
|
-
//
|
|
73
|
+
// String value — could be a built-in alias or an import path
|
|
74
74
|
if (typeof entry === 'string') {
|
|
75
|
+
// Built-in alias: redirect to built-in loader
|
|
76
|
+
if (entry.startsWith('builtIns.')) {
|
|
77
|
+
return loadBuiltIn(entry);
|
|
78
|
+
}
|
|
79
|
+
// Import path string — validate and dynamically import
|
|
75
80
|
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
76
81
|
throw new Error(`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
|
77
82
|
`Only relative, absolute, or bare specifier paths are allowed (no cross-domain URLs). ` +
|
|
@@ -81,8 +81,14 @@ async function resolveFromHandlers(
|
|
|
81
81
|
return entry as AssignFromHandlerConstructor;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// String value — could be a built-in alias or an import path
|
|
85
85
|
if (typeof entry === 'string') {
|
|
86
|
+
// Built-in alias: redirect to built-in loader
|
|
87
|
+
if (entry.startsWith('builtIns.')) {
|
|
88
|
+
return loadBuiltIn(entry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Import path string — validate and dynamically import
|
|
86
92
|
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
87
93
|
throw new Error(
|
|
88
94
|
`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
package/resolveIdRef.js
CHANGED
|
@@ -18,14 +18,15 @@ const idCacheMap = new WeakMap();
|
|
|
18
18
|
const idCounterMap = new WeakMap();
|
|
19
19
|
/**
|
|
20
20
|
* Generate a unique ID within a rootNode.
|
|
21
|
-
* Format:
|
|
21
|
+
* Format: -ag:0, -ag:1, -ag:2, ...
|
|
22
|
+
* Starts with '-' and contains ':' to avoid collision with JS identifiers/globalThis properties.
|
|
22
23
|
*/
|
|
23
24
|
function generateUniqueId(rootNode) {
|
|
24
25
|
let counter = idCounterMap.get(rootNode) ?? 0;
|
|
25
26
|
let id;
|
|
26
27
|
// Ensure uniqueness (skip if ID already exists in the document)
|
|
27
28
|
do {
|
|
28
|
-
id =
|
|
29
|
+
id = `-ag:${counter}`;
|
|
29
30
|
counter++;
|
|
30
31
|
} while (rootNode.getElementById?.(id));
|
|
31
32
|
idCounterMap.set(rootNode, counter);
|
|
@@ -43,6 +44,55 @@ export function resolveIdVariable(varName, target, withIds) {
|
|
|
43
44
|
const config = withIds[varName];
|
|
44
45
|
if (config === undefined)
|
|
45
46
|
return undefined;
|
|
47
|
+
// For 'at' option path-based configs (array or { path }), resolve directly from target — no ID, no caching
|
|
48
|
+
// These are target-relative and fast (~2-4ns), for when structure is guaranteed stable
|
|
49
|
+
if (Array.isArray(config)) {
|
|
50
|
+
let current = target;
|
|
51
|
+
for (const idx of config) {
|
|
52
|
+
if (!current || !current.children)
|
|
53
|
+
break;
|
|
54
|
+
current = current.children[idx];
|
|
55
|
+
}
|
|
56
|
+
return current instanceof Element ? current : undefined;
|
|
57
|
+
}
|
|
58
|
+
if (typeof config === 'object' && 'path' in config && !('qry' in config)) {
|
|
59
|
+
// Determine if this is from 'at' (no ID assignment) or 'withIds' with path (assigns ID + caches)
|
|
60
|
+
// When called from 'at', we skip ID/caching. When from 'withIds', we assign ID and cache.
|
|
61
|
+
// Distinguish by presence in the options — caller passes the merged map.
|
|
62
|
+
// For now: { path } without 'noId' → assign ID + cache (withIds behavior)
|
|
63
|
+
const rootNode = target.getRootNode?.() ?? target;
|
|
64
|
+
let current = target;
|
|
65
|
+
for (const idx of config.path) {
|
|
66
|
+
if (!current || !current.children)
|
|
67
|
+
break;
|
|
68
|
+
current = current.children[idx];
|
|
69
|
+
}
|
|
70
|
+
let el = current instanceof Element ? current : null;
|
|
71
|
+
// Validation: check if resolved element matches expected selector
|
|
72
|
+
if (config.expect) {
|
|
73
|
+
const didNotMatch = !el || !el.matches(config.expect);
|
|
74
|
+
if (didNotMatch) {
|
|
75
|
+
if (config.fallback) {
|
|
76
|
+
el = target.querySelector?.(config.expect) ?? el;
|
|
77
|
+
}
|
|
78
|
+
// Fire-and-forget: log correction suggestion
|
|
79
|
+
const capturedConfig = config;
|
|
80
|
+
const capturedVarName = varName;
|
|
81
|
+
import('./withIdsCorrector.js').then(module => {
|
|
82
|
+
module.logConfigCorrection(target, capturedVarName, capturedConfig);
|
|
83
|
+
}).catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!el)
|
|
87
|
+
return undefined;
|
|
88
|
+
// Assign ID for stability against future DOM mutations
|
|
89
|
+
let id = el.id;
|
|
90
|
+
if (!id) {
|
|
91
|
+
id = generateUniqueId(rootNode);
|
|
92
|
+
el.id = id;
|
|
93
|
+
}
|
|
94
|
+
return el;
|
|
95
|
+
}
|
|
46
96
|
const rootNode = target.getRootNode?.() ?? target;
|
|
47
97
|
// Get or create cache for this rootNode
|
|
48
98
|
let cache = idCacheMap.get(rootNode);
|
|
@@ -50,28 +100,27 @@ export function resolveIdVariable(varName, target, withIds) {
|
|
|
50
100
|
cache = new Map();
|
|
51
101
|
idCacheMap.set(rootNode, cache);
|
|
52
102
|
}
|
|
53
|
-
// Check cache first
|
|
54
|
-
const cached = cache.get(varName);
|
|
55
|
-
if (cached) {
|
|
56
|
-
const el = cached.ref.deref();
|
|
57
|
-
if (el)
|
|
58
|
-
return el;
|
|
59
|
-
// WeakRef was collected — try getElementById fallback
|
|
60
|
-
const el2 = rootNode.getElementById?.(cached.id);
|
|
61
|
-
if (el2) {
|
|
62
|
-
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
63
|
-
return el2;
|
|
64
|
-
}
|
|
65
|
-
// Element no longer exists — fall through to re-query
|
|
66
|
-
}
|
|
67
103
|
// First time or cache miss — resolve the element
|
|
68
104
|
let el = null;
|
|
69
105
|
if (typeof config === 'string') {
|
|
70
106
|
// String form: existing ID — use getElementById directly
|
|
107
|
+
// Check cache first (getElementById lookups are cacheable — global to rootNode)
|
|
108
|
+
const cached = cache.get(varName);
|
|
109
|
+
if (cached) {
|
|
110
|
+
const cachedEl = cached.ref.deref();
|
|
111
|
+
if (cachedEl)
|
|
112
|
+
return cachedEl;
|
|
113
|
+
// WeakRef was collected — try getElementById fallback
|
|
114
|
+
const el2 = rootNode.getElementById?.(cached.id);
|
|
115
|
+
if (el2) {
|
|
116
|
+
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
117
|
+
return el2;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
71
120
|
el = rootNode.getElementById?.(config) ?? null;
|
|
72
121
|
}
|
|
73
122
|
else {
|
|
74
|
-
// Object form: { qry } — run querySelector against target
|
|
123
|
+
// Object form: { qry } — run querySelector against target (target-relative, no cache)
|
|
75
124
|
el = target.querySelector?.(config.qry) ?? null;
|
|
76
125
|
}
|
|
77
126
|
if (!el)
|
|
@@ -82,8 +131,10 @@ export function resolveIdVariable(varName, target, withIds) {
|
|
|
82
131
|
id = generateUniqueId(rootNode);
|
|
83
132
|
el.id = id;
|
|
84
133
|
}
|
|
85
|
-
// Cache
|
|
86
|
-
|
|
134
|
+
// Cache only for string-form (getElementById) — not for qry form (target-relative)
|
|
135
|
+
if (typeof config === 'string') {
|
|
136
|
+
cache.set(varName, { id, ref: new WeakRef(el) });
|
|
137
|
+
}
|
|
87
138
|
return el;
|
|
88
139
|
}
|
|
89
140
|
/**
|