assign-gingerly 0.0.53 → 0.0.55
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 +847 -5
- package/assignFrom.js +229 -9
- package/assignFrom.ts +338 -9
- package/assignGingerly.js +34 -1
- package/assignGingerly.ts +54 -1
- package/beVigilant.js +73 -0
- package/beVigilant.ts +85 -0
- package/enhanceAll.js +106 -0
- package/enhanceAll.ts +138 -0
- package/handlers/join.js +74 -0
- package/handlers/join.ts +80 -0
- package/handlers/lazyLoad.js +212 -0
- package/handlers/lazyLoad.ts +307 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +63 -0
- package/handlers/microDataJoin.js +184 -0
- package/handlers/microDataJoin.ts +270 -0
- package/inferencer/.gitmodules +3 -0
- package/inferencer/.vscode/settings.json +2 -0
- package/inferencer/InferencedPropagator.js +230 -0
- package/inferencer/InferencedPropagator.ts +269 -0
- package/inferencer/LICENSE +21 -0
- package/inferencer/README.md +524 -0
- package/inferencer/Requirements/SupportForPropagator.md +368 -0
- package/inferencer/imports.html +7 -0
- package/inferencer/inferencer.js +254 -0
- package/inferencer/inferencer.ts +292 -0
- package/inferencer/package-lock.json +129 -0
- package/inferencer/package.json +60 -0
- package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
- package/inferencer/playwright-report/index.html +90 -0
- package/inferencer/playwright.config.ts +54 -0
- package/inferencer/test-results/.last-run.json +6 -0
- package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
- package/inferencer/tests/inferencedPropagator.html +428 -0
- package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
- package/inferencer/tests/inferencer.html +355 -0
- package/inferencer/tests/inferencer.spec.ts +19 -0
- package/inferencer/tsconfig.json +19 -0
- package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
- package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
- package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
- package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
- package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
- package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
- package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
- package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
- package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
- package/inferencer/types/LICENSE +21 -0
- package/inferencer/types/NewCustomElement.md +388 -0
- package/inferencer/types/NewCustomElementFeature.md +683 -0
- package/inferencer/types/NewEnhancementInstructions.md +705 -0
- package/inferencer/types/README.md +2 -0
- package/inferencer/types/agrace/types.d.ts +11 -0
- package/inferencer/types/assign-gingerly/types.d.ts +572 -0
- package/inferencer/types/be-a-beacon/types.d.ts +17 -0
- package/inferencer/types/be-bound/types.d.ts +66 -0
- package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
- package/inferencer/types/be-calculating/types.d.ts +54 -0
- package/inferencer/types/be-clonable/types.d.ts +38 -0
- package/inferencer/types/be-committed/types.d.ts +22 -0
- package/inferencer/types/be-consoling/types.d.ts +24 -0
- package/inferencer/types/be-decked-with/types.d.ts +26 -0
- package/inferencer/types/be-delible/types.d.ts +27 -0
- package/inferencer/types/be-dispatching/types.d.ts +34 -0
- package/inferencer/types/be-evanescent/types.d.ts +20 -0
- package/inferencer/types/be-flashy/types.d.ts +21 -0
- package/inferencer/types/be-gone/types.d.ts +25 -0
- package/inferencer/types/be-observing/types.d.ts +55 -0
- package/inferencer/types/be-reflective/types.d.ts +78 -0
- package/inferencer/types/be-reformable/types.d.ts +49 -0
- package/inferencer/types/be-render-neutral/types.d.ts +32 -0
- package/inferencer/types/be-switched/types.d.ts +146 -0
- package/inferencer/types/be-typed/types.d.ts +32 -0
- package/inferencer/types/be-valued/types.d.ts +22 -0
- package/inferencer/types/data-props/types.d.ts +34 -0
- package/inferencer/types/do-inc/types.d.ts +56 -0
- package/inferencer/types/do-invoke/types.d.ts +38 -0
- package/inferencer/types/do-merge/types.d.ts +28 -0
- package/inferencer/types/do-toggle/types.d.ts +31 -0
- package/inferencer/types/face-up/types.d.ts +100 -0
- package/inferencer/types/fetch-for/types.d.ts +36 -0
- package/inferencer/types/folder-picker/types.d.ts +21 -0
- package/inferencer/types/global.d.ts +29 -0
- package/inferencer/types/id-generation/types.d.ts +26 -0
- package/inferencer/types/inferencer/types.d.ts +46 -0
- package/inferencer/types/mount-observer/types.d.ts +363 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
- package/inferencer/types/pipe-in/types.d.ts +52 -0
- package/inferencer/types/roundabout/types.d.ts +268 -0
- package/inferencer/types/soak-up/types.d.ts +40 -0
- package/inferencer/types/templ-maker/types.d.ts +43 -0
- package/inferencer/types/time-ticker/types.d.ts +62 -0
- package/inferencer/types/truth-sourcer/types.d.ts +44 -0
- package/inferencer/upSearch.js +27 -0
- package/inferencer/upSearch.ts +26 -0
- package/inferencer/withScopePerimeter.js +27 -0
- package/inferencer/withScopePerimeter.ts +33 -0
- package/inferredAssignments.js +38 -0
- package/inferredAssignments.ts +65 -0
- package/isAllowedImportPath.js +42 -0
- package/isAllowedImportPath.ts +53 -0
- package/package.json +57 -3
- package/paths.js +231 -0
- package/paths.ts +413 -0
- package/processHandlerCommands.js +188 -0
- package/processHandlerCommands.ts +217 -0
- package/resolveIdRef.js +144 -0
- package/resolveIdRef.ts +170 -0
- package/resolveValues.js +41 -2
- package/resolveValues.ts +41 -1
- package/transitionHelper.js +109 -0
- package/transitionHelper.ts +132 -0
- package/types/assign-gingerly/types.d.ts +89 -0
package/paths.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths.ts — Typed path proxy and template tag for assignFrom authoring.
|
|
3
|
+
*
|
|
4
|
+
* Provides compile-time autocomplete and type safety for `?.`-prefixed path strings.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { paths, sp } from 'assign-gingerly/paths.js';
|
|
8
|
+
*
|
|
9
|
+
* interface Person {
|
|
10
|
+
* firstName?: string;
|
|
11
|
+
* middleName?: string;
|
|
12
|
+
* lastName: string;
|
|
13
|
+
* address: { city: string; zip: string };
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* const $ = paths<Person>();
|
|
17
|
+
*
|
|
18
|
+
* // Use sp (split into parts) to create arrays for builtIns.join:
|
|
19
|
+
* const value = sp`${$.lastName}, ${$.firstName}`;
|
|
20
|
+
* // ['?.lastName', ', ', '?.firstName']
|
|
21
|
+
*
|
|
22
|
+
* // Use .path for raw string contexts (object keys, plain arrays):
|
|
23
|
+
* const key = $.textContent.path; // '?.textContent'
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Symbol used internally to detect path proxy objects.
|
|
28
|
+
* The sp tag function uses this to auto-extract path strings from proxies.
|
|
29
|
+
*/
|
|
30
|
+
const PATH_SYMBOL = Symbol('assign-gingerly-path');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Type that maps an object type to a proxy where every property access
|
|
34
|
+
* returns either a deeper proxy (for object properties) or a terminal
|
|
35
|
+
* with a `.path` string accessor — while providing full autocomplete.
|
|
36
|
+
* Enhanced: also callable (for method call syntax).
|
|
37
|
+
*/
|
|
38
|
+
export type PathProxy<T> = {
|
|
39
|
+
[K in keyof T]-?: T[K] extends ((...args: any[]) => infer R)
|
|
40
|
+
? ((...args: any[]) => PathProxy<NonNullable<R>> & { readonly path: string }) & PathProxy<NonNullable<R>> & { readonly path: string }
|
|
41
|
+
: T[K] extends (object | undefined | null)
|
|
42
|
+
? PathProxy<NonNullable<T[K]>> & { readonly path: string } & ((...args: any[]) => PathProxy<any> & { readonly path: string })
|
|
43
|
+
: { readonly path: string } & ((...args: any[]) => PathProxy<any> & { readonly path: string });
|
|
44
|
+
} & { readonly path: string } & ((...args: any[]) => PathProxy<any> & { readonly path: string });
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options for paths proxy creation.
|
|
48
|
+
*/
|
|
49
|
+
export interface PathsOptions {
|
|
50
|
+
/** Alias map (alias → full name). Proxy reverses aliases: full name → alias in output. */
|
|
51
|
+
aka?: Record<string, string>;
|
|
52
|
+
/** Method names — used for disambiguation (future use). */
|
|
53
|
+
withMethods?: string[] | Set<string>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a proxy for id-ref paths (#[varName]).
|
|
58
|
+
* After the initial #[varName], further property access chains with ?. from the resolved element.
|
|
59
|
+
* .path returns the #[varName] prefix (optionally with further ?. path).
|
|
60
|
+
*/
|
|
61
|
+
function createIdRefProxy(idRef: string, options?: PathsOptions): any {
|
|
62
|
+
function handler() {}
|
|
63
|
+
return new Proxy(handler, {
|
|
64
|
+
get(_, prop: string | symbol) {
|
|
65
|
+
if (prop === 'path' || prop === PATH_SYMBOL) {
|
|
66
|
+
return idRef;
|
|
67
|
+
}
|
|
68
|
+
if (typeof prop === 'symbol') return undefined;
|
|
69
|
+
|
|
70
|
+
// Chain further path segments after the id ref
|
|
71
|
+
const chained = `${idRef}?.${String(prop)}`;
|
|
72
|
+
return createIdRefProxy(chained, options);
|
|
73
|
+
},
|
|
74
|
+
apply(_, __, args) {
|
|
75
|
+
if (args.length > 0) {
|
|
76
|
+
const arg = args[0];
|
|
77
|
+
let argStr: string;
|
|
78
|
+
if (arg === true) argStr = 'true';
|
|
79
|
+
else if (arg === false) argStr = 'false';
|
|
80
|
+
else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
|
|
81
|
+
const fullPath = arg[PATH_SYMBOL] as string;
|
|
82
|
+
argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
|
|
83
|
+
}
|
|
84
|
+
else argStr = String(arg);
|
|
85
|
+
|
|
86
|
+
const chained = `${idRef}?.${argStr}`;
|
|
87
|
+
return createIdRefProxy(chained, options);
|
|
88
|
+
}
|
|
89
|
+
return createIdRefProxy(idRef, options);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a recursive proxy that records property access paths.
|
|
96
|
+
* Supports both property access and method call syntax (via apply trap on function target).
|
|
97
|
+
*
|
|
98
|
+
* When `aka` is provided, property names that match an alias *value* are output
|
|
99
|
+
* using the alias *key* instead (reverse alias).
|
|
100
|
+
*/
|
|
101
|
+
function createPathProxy(prefix: string, options?: PathsOptions): any {
|
|
102
|
+
const aliasMap = options?.aka;
|
|
103
|
+
|
|
104
|
+
// Use a function as the target to enable the apply trap
|
|
105
|
+
function handler() {}
|
|
106
|
+
|
|
107
|
+
return new Proxy(handler, {
|
|
108
|
+
get(_, prop: string | symbol) {
|
|
109
|
+
if (prop === 'path' || prop === PATH_SYMBOL) {
|
|
110
|
+
return prefix.length > 0 ? `?.${prefix}` : '?.';
|
|
111
|
+
}
|
|
112
|
+
// Ignore symbol access (Symbol.iterator, Symbol.toPrimitive, etc.)
|
|
113
|
+
if (typeof prop === 'symbol') return undefined;
|
|
114
|
+
|
|
115
|
+
let segment = String(prop);
|
|
116
|
+
|
|
117
|
+
// #-prefix: $['#firstName'] → '#[firstName]' (cached element ref)
|
|
118
|
+
if (segment.startsWith('#')) {
|
|
119
|
+
const varName = segment.substring(1);
|
|
120
|
+
const idRef = `#[${varName}]`;
|
|
121
|
+
// Return a proxy that starts from this id ref (can chain further with ?.)
|
|
122
|
+
return createIdRefProxy(idRef, options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Apply reverse alias: if prop matches an alias value, use the alias key
|
|
126
|
+
if (aliasMap) {
|
|
127
|
+
for (const [alias, target] of Object.entries(aliasMap)) {
|
|
128
|
+
if (target === segment) { segment = alias; break; }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newPath = prefix ? `${prefix}?.${segment}` : segment;
|
|
133
|
+
return createPathProxy(newPath, options);
|
|
134
|
+
},
|
|
135
|
+
apply(_, __, args) {
|
|
136
|
+
// Method call syntax: $.querySelector('.username') → extends path with the argument
|
|
137
|
+
if (args.length > 0) {
|
|
138
|
+
const arg = args[0];
|
|
139
|
+
let argStr: string;
|
|
140
|
+
if (arg === true) argStr = 'true';
|
|
141
|
+
else if (arg === false) argStr = 'false';
|
|
142
|
+
else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
|
|
143
|
+
// Proxy arg — extract path without '?.' prefix
|
|
144
|
+
const fullPath = arg[PATH_SYMBOL] as string;
|
|
145
|
+
argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
|
|
146
|
+
}
|
|
147
|
+
else argStr = String(arg);
|
|
148
|
+
|
|
149
|
+
const newPath = prefix ? `${prefix}?.${argStr}` : argStr;
|
|
150
|
+
return createPathProxy(newPath, options);
|
|
151
|
+
}
|
|
152
|
+
// No args — method called with no arguments, return self
|
|
153
|
+
return createPathProxy(prefix, options);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a typed path proxy for a given interface/type.
|
|
160
|
+
* Property accesses on the returned proxy produce `?.`-prefixed path strings.
|
|
161
|
+
* Method calls append their argument to the path.
|
|
162
|
+
*
|
|
163
|
+
* @param options - Optional aka aliases and withMethods for path generation
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* const $ = paths<Person>();
|
|
167
|
+
* $.lastName.path // '?.lastName'
|
|
168
|
+
* $.address.city.path // '?.address?.city'
|
|
169
|
+
*
|
|
170
|
+
* // With aka (reverse alias applied):
|
|
171
|
+
* const $ = paths<MyEl>({ aka: { q: 'querySelector' } });
|
|
172
|
+
* $.querySelector('.user').textContent.path // '?.q?..user?.textContent'
|
|
173
|
+
*
|
|
174
|
+
* // Inside sp template literals, .path is not needed:
|
|
175
|
+
* sp`${$.lastName}, ${$.firstName}` // ['?.lastName', ', ', '?.firstName']
|
|
176
|
+
*/
|
|
177
|
+
export function paths<T>(options?: PathsOptions): PathProxy<T> {
|
|
178
|
+
return createPathProxy('', options) as any;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create an assignment pair: { [lhs.path]: rhs.path }.
|
|
183
|
+
* Used to express "set this target to this source value" in a spreadable form.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* set($.clone.querySelector('.username').textContent).to($.username)
|
|
187
|
+
* // { '?.clone?.q?..username?.textContent': '?.username' }
|
|
188
|
+
*
|
|
189
|
+
* // Spread into an assign object:
|
|
190
|
+
* assign: {
|
|
191
|
+
* ...set($.textContent).to($.name),
|
|
192
|
+
* ...set($.className).to($.theme),
|
|
193
|
+
* count: 1
|
|
194
|
+
* }
|
|
195
|
+
*/
|
|
196
|
+
export function set(lhs: any): { to: (rhs: any) => Record<string, any> } {
|
|
197
|
+
const lhsStr = lhs && typeof lhs === 'object' && PATH_SYMBOL in lhs
|
|
198
|
+
? lhs[PATH_SYMBOL]
|
|
199
|
+
: String(lhs);
|
|
200
|
+
return {
|
|
201
|
+
to(rhs: any): Record<string, any> {
|
|
202
|
+
const rhsStr = rhs && typeof rhs === 'object' && PATH_SYMBOL in rhs
|
|
203
|
+
? rhs[PATH_SYMBOL]
|
|
204
|
+
: rhs;
|
|
205
|
+
return { [lhsStr]: rhsStr };
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Recursively walk a value and convert any path proxy objects to their
|
|
212
|
+
* `?.`-prefixed string representation.
|
|
213
|
+
*
|
|
214
|
+
* Use this to wrap entire config objects or arrays that contain proxy values,
|
|
215
|
+
* extracting all path strings in one pass.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* const $ = paths<MyVM>({ aka: { q: 'querySelector' } });
|
|
219
|
+
*
|
|
220
|
+
* const config = smoothOver({
|
|
221
|
+
* assign: {
|
|
222
|
+
* incrementButton: $.clone.querySelector('.increment'),
|
|
223
|
+
* decrementButton: $.clone.querySelector('.decrement'),
|
|
224
|
+
* }
|
|
225
|
+
* });
|
|
226
|
+
* // { assign: { incrementButton: '?.clone?.q?..increment', ... } }
|
|
227
|
+
*/
|
|
228
|
+
export function smoothOver(value: any): any {
|
|
229
|
+
if (value && typeof value === 'object' && PATH_SYMBOL in value) {
|
|
230
|
+
return value[PATH_SYMBOL];
|
|
231
|
+
}
|
|
232
|
+
if (Array.isArray(value)) {
|
|
233
|
+
return value.map(smoothOver);
|
|
234
|
+
}
|
|
235
|
+
if (value && typeof value === 'object') {
|
|
236
|
+
const proto = Object.getPrototypeOf(value);
|
|
237
|
+
if (proto === Object.prototype || proto === null) {
|
|
238
|
+
const result: Record<string, any> = {};
|
|
239
|
+
for (const [k, v] of Object.entries(value)) {
|
|
240
|
+
result[k] = smoothOver(v);
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Merge multiple set(...).to(...) pairs (and/or plain objects) into an `{ assign: {...} }` object.
|
|
250
|
+
* Spread the result into a merge config to avoid repeated `...` per entry.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* {
|
|
254
|
+
* ifKeyIn: ['statusClassName', 'statusMessageText'],
|
|
255
|
+
* ifAllOf: ['clone'],
|
|
256
|
+
* ...doAssign(
|
|
257
|
+
* set($.clone.querySelector('.status').className).to($.statusClassName),
|
|
258
|
+
* set($.clone.querySelector('.status-text').textContent).to($.statusMessageText),
|
|
259
|
+
* )
|
|
260
|
+
* }
|
|
261
|
+
* // Equivalent to: { ifKeyIn: [...], ifAllOf: [...], assign: { '?.clone?.q?..status?.className': '?.statusClassName', ... } }
|
|
262
|
+
*
|
|
263
|
+
* // Mix with literal values:
|
|
264
|
+
* ...doAssign(
|
|
265
|
+
* set($.clone.querySelector('.count-value').textContent).to($.count),
|
|
266
|
+
* { renderCount: 1 },
|
|
267
|
+
* )
|
|
268
|
+
*/
|
|
269
|
+
export function doAssign(...pairs: Record<string, any>[]): { assign: Record<string, any> } {
|
|
270
|
+
return { assign: Object.assign({}, ...pairs) };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Compile-time loop expansion: generates one entry per key from a factory function.
|
|
275
|
+
* Creates a typed proxy internally — the factory receives both the key and the proxy.
|
|
276
|
+
*
|
|
277
|
+
* @param keys - Array of property names to iterate (type-checked against T)
|
|
278
|
+
* @param factory - Function that produces a config entry for each key
|
|
279
|
+
* @param options - Optional PathsOptions (aka, withMethods) for the internal proxy
|
|
280
|
+
* @returns Array of factory results (one per key) — spread into merges array
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* import { forEachKeyIn, set, doAssign } from 'assign-gingerly/paths.js';
|
|
284
|
+
*
|
|
285
|
+
* interface Person extends HTMLElement { firstName: string; lastName: string; }
|
|
286
|
+
*
|
|
287
|
+
* const merges = [
|
|
288
|
+
* ...forEachKeyIn<Person>(['firstName', 'lastName'], (key, $) => ({
|
|
289
|
+
* ifKeyIn: [key],
|
|
290
|
+
* assignOptions: { withIds: { [key]: { qry: `[name="${key}"]` } } },
|
|
291
|
+
* ...doAssign(set($['#' + key]).to($[key]))
|
|
292
|
+
* })),
|
|
293
|
+
* ];
|
|
294
|
+
*/
|
|
295
|
+
export function forEachKeyIn<T>(
|
|
296
|
+
keys: (keyof T & string)[],
|
|
297
|
+
factory: (key: keyof T & string, proxy: PathProxy<T>) => any,
|
|
298
|
+
options?: PathsOptions
|
|
299
|
+
): any[] {
|
|
300
|
+
const $ = paths<T>(options);
|
|
301
|
+
return keys.map(key => factory(key, $));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Tagged template literal that splits a template into an array of parts.
|
|
306
|
+
* Interleaves static string segments with interpolated values.
|
|
307
|
+
*
|
|
308
|
+
* Path proxy objects are auto-detected and converted to their `?.`-prefixed
|
|
309
|
+
* string representation — no `.path` call needed inside sp template literals.
|
|
310
|
+
*
|
|
311
|
+
* Arrays passed as interpolations are preserved as nested arrays (for
|
|
312
|
+
* all-or-nothing optional segments in builtIns.join).
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* const $ = paths<Person>();
|
|
316
|
+
*
|
|
317
|
+
* // Basic usage:
|
|
318
|
+
* sp`${$.lastName}, ${$.firstName}`
|
|
319
|
+
* // ['?.lastName', ', ', '?.firstName']
|
|
320
|
+
*
|
|
321
|
+
* // With optional segment (nested array, all-or-nothing in join):
|
|
322
|
+
* sp`${$.lastName}${[', ', $.middleName]}, ${$.firstName}`
|
|
323
|
+
* // ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
324
|
+
*/
|
|
325
|
+
export function sp(strings: TemplateStringsArray, ...values: any[]): any[] {
|
|
326
|
+
const result: any[] = [];
|
|
327
|
+
for (let i = 0; i < strings.length; i++) {
|
|
328
|
+
if (strings[i]) result.push(strings[i]);
|
|
329
|
+
if (i < values.length) {
|
|
330
|
+
const v = values[i];
|
|
331
|
+
if (v && typeof v === 'object' && PATH_SYMBOL in v) {
|
|
332
|
+
// Auto-extract path from proxy object
|
|
333
|
+
result.push(v[PATH_SYMBOL]);
|
|
334
|
+
} else if (Array.isArray(v)) {
|
|
335
|
+
// Nested array — recursively extract paths from proxy elements
|
|
336
|
+
result.push(v.map(el =>
|
|
337
|
+
el && typeof el === 'object' && PATH_SYMBOL in el ? el[PATH_SYMBOL] : el
|
|
338
|
+
));
|
|
339
|
+
} else {
|
|
340
|
+
result.push(v);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Extract the last segment from a `?.`-prefixed path string.
|
|
349
|
+
* e.g., '?.address?.city' → 'city', '?.firstName' → 'firstName'
|
|
350
|
+
*/
|
|
351
|
+
function extractPropName(pathStr: string): string {
|
|
352
|
+
const parts = pathStr.split('?.');
|
|
353
|
+
return parts[parts.length - 1];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Tagged template literal that produces an array of {prop, val} objects + literal strings.
|
|
358
|
+
* Designed for `builtIns.microDataJoin` — provides both the property name (for itemprop)
|
|
359
|
+
* and the path string (for resolution).
|
|
360
|
+
*
|
|
361
|
+
* Path proxy objects are auto-detected and converted to `{prop, val}` objects.
|
|
362
|
+
* Plain objects passed as interpolations are preserved as-is (allows developer overrides
|
|
363
|
+
* with custom prop, val, format, etc.).
|
|
364
|
+
* Arrays are preserved as nested arrays (for optional segments).
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* const $ = paths<Person>();
|
|
368
|
+
*
|
|
369
|
+
* // Basic usage:
|
|
370
|
+
* md`${$.firstName} ${$.lastName}`
|
|
371
|
+
* // [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'lastName', val: '?.lastName' }]
|
|
372
|
+
*
|
|
373
|
+
* // With developer override (custom prop name, format):
|
|
374
|
+
* md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`
|
|
375
|
+
* // [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'birthDate', val: '?.birthDT', format: 'long' }]
|
|
376
|
+
*
|
|
377
|
+
* // With optional segment:
|
|
378
|
+
* md`${$.firstName}${[' ', $.middleName]} ${$.lastName}`
|
|
379
|
+
* // [{ prop: 'firstName', val: '?.firstName' }, [' ', { prop: 'middleName', val: '?.middleName' }], ' ', { prop: 'lastName', val: '?.lastName' }]
|
|
380
|
+
*/
|
|
381
|
+
export function md(strings: TemplateStringsArray, ...values: any[]): any[] {
|
|
382
|
+
const result: any[] = [];
|
|
383
|
+
for (let i = 0; i < strings.length; i++) {
|
|
384
|
+
if (strings[i]) result.push(strings[i]);
|
|
385
|
+
if (i < values.length) {
|
|
386
|
+
const v = values[i];
|
|
387
|
+
if (v && typeof v === 'object' && PATH_SYMBOL in v) {
|
|
388
|
+
// Proxy object → {prop, val}
|
|
389
|
+
const pathStr = v[PATH_SYMBOL] as string;
|
|
390
|
+
result.push({ prop: extractPropName(pathStr), val: pathStr });
|
|
391
|
+
} else if (Array.isArray(v)) {
|
|
392
|
+
// Nested array — recursively convert proxy elements to {prop, val}
|
|
393
|
+
result.push(v.map(el => {
|
|
394
|
+
if (el && typeof el === 'object' && PATH_SYMBOL in el) {
|
|
395
|
+
const pathStr = el[PATH_SYMBOL] as string;
|
|
396
|
+
return { prop: extractPropName(pathStr), val: pathStr };
|
|
397
|
+
}
|
|
398
|
+
return el;
|
|
399
|
+
}));
|
|
400
|
+
} else if (v && typeof v === 'object' && 'prop' in v) {
|
|
401
|
+
// Developer override object — extract val from proxy if present
|
|
402
|
+
const processed = { ...v };
|
|
403
|
+
if (processed.val && typeof processed.val === 'object' && PATH_SYMBOL in processed.val) {
|
|
404
|
+
processed.val = processed.val[PATH_SYMBOL];
|
|
405
|
+
}
|
|
406
|
+
result.push(processed);
|
|
407
|
+
} else {
|
|
408
|
+
result.push(v);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* processHandlerCommands - Handles ` =>` operator keys in assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported only when ` =>` keys are detected in the pattern.
|
|
5
|
+
*/
|
|
6
|
+
import { resolveValues } from './resolveValues.js';
|
|
7
|
+
import { evaluatePathWithMethods } from './assignGingerly.js';
|
|
8
|
+
import { isAllowedImportPath } from './isAllowedImportPath.js';
|
|
9
|
+
/**
|
|
10
|
+
* Map of built-in handler names to their module paths.
|
|
11
|
+
* These are auto-loaded on demand — no explicit import required.
|
|
12
|
+
*/
|
|
13
|
+
const BUILT_IN_MAP = {
|
|
14
|
+
'builtIns.lazyLoad': './handlers/lazyLoad.js',
|
|
15
|
+
'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
|
|
16
|
+
'builtIns.join': './handlers/join.js',
|
|
17
|
+
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Find a handler class in a dynamically imported module.
|
|
21
|
+
* Checks default export first, then searches for the first class with `assign` on prototype.
|
|
22
|
+
*/
|
|
23
|
+
function findHandlerInModule(module) {
|
|
24
|
+
// Check default export first
|
|
25
|
+
if (module.default && typeof module.default === 'function'
|
|
26
|
+
&& module.default.prototype && 'assign' in module.default.prototype) {
|
|
27
|
+
return module.default;
|
|
28
|
+
}
|
|
29
|
+
// Search other exports
|
|
30
|
+
for (const key of Object.keys(module)) {
|
|
31
|
+
const exported = module[key];
|
|
32
|
+
if (typeof exported === 'function' && exported.prototype && 'assign' in exported.prototype) {
|
|
33
|
+
return exported;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Dynamically load a built-in handler by name.
|
|
40
|
+
* Returns the handler constructor, or undefined if the name isn't a recognized built-in.
|
|
41
|
+
*/
|
|
42
|
+
async function loadBuiltIn(name) {
|
|
43
|
+
const path = BUILT_IN_MAP[name];
|
|
44
|
+
if (!path)
|
|
45
|
+
return undefined;
|
|
46
|
+
const module = await import(path);
|
|
47
|
+
return findHandlerInModule(module);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a handler from options.handlers (class constructor or import path).
|
|
51
|
+
*/
|
|
52
|
+
async function resolveFromHandlers(name, handlers, permissions) {
|
|
53
|
+
if (!handlers || !(name in handlers))
|
|
54
|
+
return undefined;
|
|
55
|
+
const entry = handlers[name];
|
|
56
|
+
// Class constructor — use directly
|
|
57
|
+
if (typeof entry === 'function') {
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
// Import path string — validate and dynamically import
|
|
61
|
+
if (typeof entry === 'string') {
|
|
62
|
+
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
63
|
+
throw new Error(`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
|
64
|
+
`Only relative, absolute, or bare specifier paths are allowed (no cross-domain URLs). ` +
|
|
65
|
+
`Pass { crossDomainImports: true } in permissions to override.`);
|
|
66
|
+
}
|
|
67
|
+
const module = await import(entry);
|
|
68
|
+
const HandlerClass = findHandlerInModule(module);
|
|
69
|
+
if (!HandlerClass) {
|
|
70
|
+
throw new Error(`assignFrom: handler "${name}" — module "${entry}" does not export a valid handler class.`);
|
|
71
|
+
}
|
|
72
|
+
return HandlerClass;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Process all handler command keys (ending with ' =>') in a pattern.
|
|
78
|
+
*
|
|
79
|
+
* @param target - The target object being assigned to
|
|
80
|
+
* @param handlerKeys - Array of keys ending with ' =>'
|
|
81
|
+
* @param pattern - The original pattern object
|
|
82
|
+
* @param options - The assignFrom options
|
|
83
|
+
* @param handlerRegistry - The registry of handler classes
|
|
84
|
+
*/
|
|
85
|
+
export async function processHandlerCommands(target, handlerKeys, pattern, options, permissions) {
|
|
86
|
+
for (const key of handlerKeys) {
|
|
87
|
+
const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
|
|
88
|
+
const rhs = pattern[key];
|
|
89
|
+
// Normalize RHS to an array of handler configs
|
|
90
|
+
const configs = Array.isArray(rhs) ? rhs : [rhs];
|
|
91
|
+
// Validate — no nested arrays
|
|
92
|
+
for (const config of configs) {
|
|
93
|
+
if (Array.isArray(config)) {
|
|
94
|
+
throw new Error(`assignFrom: handler command "${key}" does not support nested arrays`);
|
|
95
|
+
}
|
|
96
|
+
if (!config || typeof config !== 'object' || !config.do) {
|
|
97
|
+
throw new Error(`assignFrom: handler command "${key}" requires a config object with a "do" field`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Empty array — skip silently
|
|
101
|
+
if (configs.length === 0)
|
|
102
|
+
continue;
|
|
103
|
+
// Resolve the LHS path, preserving parent + key for return-value assignment.
|
|
104
|
+
// lhsParent[lhsKey] === lhsTarget (the current value at the path)
|
|
105
|
+
let lhsTarget;
|
|
106
|
+
let lhsParent = undefined;
|
|
107
|
+
let lhsKey = undefined;
|
|
108
|
+
if (lhsPath.startsWith('?.')) {
|
|
109
|
+
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
110
|
+
const withMethodsSet = options.withMethods
|
|
111
|
+
? options.withMethods instanceof Set
|
|
112
|
+
? options.withMethods
|
|
113
|
+
: new Set(options.withMethods)
|
|
114
|
+
: undefined;
|
|
115
|
+
if (withMethodsSet && pathParts.length > 0) {
|
|
116
|
+
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
117
|
+
lhsParent = result.target;
|
|
118
|
+
lhsKey = result.lastKey;
|
|
119
|
+
lhsTarget = result.target[result.lastKey];
|
|
120
|
+
// If last key is a method, call it to get the target
|
|
121
|
+
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
122
|
+
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
123
|
+
lhsParent = undefined; // Can't assign back to a method call result
|
|
124
|
+
lhsKey = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Simple path navigation — walk to parent, keep last key
|
|
129
|
+
if (pathParts.length === 0) {
|
|
130
|
+
lhsTarget = target;
|
|
131
|
+
}
|
|
132
|
+
else if (pathParts.length === 1) {
|
|
133
|
+
lhsParent = target;
|
|
134
|
+
lhsKey = pathParts[0];
|
|
135
|
+
lhsTarget = target[pathParts[0]];
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
let current = target;
|
|
139
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
140
|
+
if (current == null)
|
|
141
|
+
break;
|
|
142
|
+
current = current[pathParts[i]];
|
|
143
|
+
}
|
|
144
|
+
lhsParent = current;
|
|
145
|
+
lhsKey = pathParts[pathParts.length - 1];
|
|
146
|
+
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (lhsPath) {
|
|
151
|
+
lhsParent = target;
|
|
152
|
+
lhsKey = lhsPath;
|
|
153
|
+
lhsTarget = target[lhsPath];
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
lhsTarget = target;
|
|
157
|
+
}
|
|
158
|
+
// Execute handlers sequentially, sharing the same lhsTarget
|
|
159
|
+
for (const config of configs) {
|
|
160
|
+
// 1. Check options.handlers (local, per-call)
|
|
161
|
+
let HandlerClass = await resolveFromHandlers(config.do, options.handlers, permissions);
|
|
162
|
+
// 2. Fallback to built-in auto-load
|
|
163
|
+
if (!HandlerClass && config.do.startsWith('builtIns.')) {
|
|
164
|
+
HandlerClass = await loadBuiltIn(config.do);
|
|
165
|
+
}
|
|
166
|
+
if (!HandlerClass) {
|
|
167
|
+
throw new Error(`assignFrom: unknown handler "${config.do}". Provide it in options.handlers.`);
|
|
168
|
+
}
|
|
169
|
+
// Resolve 'resolve' map if present — uses full resolveValues (paths, protocols, literals)
|
|
170
|
+
let resolvedParams = {};
|
|
171
|
+
if (config.resolve) {
|
|
172
|
+
resolvedParams = await resolveValues(config.resolve, options.from, {
|
|
173
|
+
withMethods: options.withMethods,
|
|
174
|
+
aka: options.aka,
|
|
175
|
+
protocols: options.protocols
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Instantiate and invoke the handler
|
|
179
|
+
const handler = new HandlerClass(config);
|
|
180
|
+
const result = await handler.assign(lhsTarget, resolvedParams, options);
|
|
181
|
+
// Return-value protocol: if handler returns a non-undefined value,
|
|
182
|
+
// assign it back to the LHS path
|
|
183
|
+
if (result !== undefined && lhsParent != null && lhsKey != null) {
|
|
184
|
+
lhsParent[lhsKey] = result;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|