assign-gingerly 0.0.0 → 0.0.2
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 +83 -1
- package/index.d.ts +35 -0
- package/index.js +224 -0
- package/package.json +23 -2
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# assign-gingerly
|
|
2
2
|
|
|
3
|
+
[](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml)
|
|
4
|
+
[](http://badge.fury.io/js/assign-gingerly)
|
|
5
|
+
[](https://bundlephobia.com/result?p=assign-gingerly)
|
|
6
|
+
<img src="http://img.badgesize.io/https://cdn.jsdelivr.net/npm/assign-gingerly?compression=gzip">
|
|
7
|
+
|
|
3
8
|
This package provides a utility function for carefully merging one object into another.
|
|
4
9
|
|
|
5
10
|
It builds on Object.assign. It adds support for:
|
|
@@ -13,6 +18,7 @@ It builds on Object.assign. It adds support for:
|
|
|
13
18
|
const sourceObj = {hello: 'world'};
|
|
14
19
|
assignGingerly(sourceObj, {hello: 'Venus', foo: 'bar'});
|
|
15
20
|
// Because none of the keys of the second parameter start with "?.",
|
|
21
|
+
// nor includes any symbols keys,
|
|
16
22
|
// assign gingerly produces identical results as Object.assign:
|
|
17
23
|
console.log(sourceObj);
|
|
18
24
|
//{hello: 'Venus', foo: 'bar'}
|
|
@@ -55,8 +61,84 @@ console.log(obj);
|
|
|
55
61
|
|
|
56
62
|
When the right hand side of an expression is an object, assignGingerly is recursively applied (passing the third argument in if applicable, which will be discussed below)
|
|
57
63
|
|
|
58
|
-
## Dependency injection based on a registry object
|
|
64
|
+
## Dependency injection based on a registry object and a Symbolic reference
|
|
59
65
|
|
|
60
66
|
```Typescript
|
|
67
|
+
interface IBaseRegistryItem<T = any> {
|
|
68
|
+
spawn: {new(): T} | Promise<{new(): T}>
|
|
69
|
+
map: {[key: string | symbol]: keyof T}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const isHappy = Symbol.for('TFWsx0YH5E6eSfhE7zfLxA');
|
|
73
|
+
class MyEnhancement extends ElementEnhancement(EventTarget){
|
|
74
|
+
get isHappy(){}
|
|
75
|
+
set isHappy(nv){}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const isMellow = Symbol.for('BqnnTPWRHkWdVGWcGQoAiw');
|
|
79
|
+
class YourEnhancement extends ElementEnhancement(EventTarget){
|
|
80
|
+
get isMellow(){}
|
|
81
|
+
set isMellow(nv){}
|
|
82
|
+
get madAboutFourteen(){}
|
|
83
|
+
set madAboutFourteen(nv){}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class BaseRegistry{
|
|
87
|
+
push(IBaseRegistryItem | IBaseRegistryItem[]){
|
|
88
|
+
...
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//Here's where the dependency injection mapping takes place
|
|
93
|
+
const baseRegistry = new BaseRegistry;
|
|
94
|
+
baseRegistry.push([
|
|
95
|
+
{
|
|
96
|
+
map: {
|
|
97
|
+
[isHappy]: 'isHappy'
|
|
98
|
+
},
|
|
99
|
+
spawn: MyEnhancement
|
|
100
|
+
},{
|
|
101
|
+
|
|
102
|
+
map: {
|
|
103
|
+
[isMellow]: 'isMellow'
|
|
104
|
+
},
|
|
105
|
+
spawn: async () => {
|
|
106
|
+
return YourEnhancement;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
//end of dependency injection
|
|
111
|
+
|
|
112
|
+
const asyncResult = await assignGingerly({}, {
|
|
113
|
+
[isHappy]: true,
|
|
114
|
+
[isMellow]: true,
|
|
115
|
+
'?.style.height': '40px',
|
|
116
|
+
'?.enhancements?.mellowYellow?.madAboutFourteen': true
|
|
117
|
+
}, {
|
|
118
|
+
registry: BaseRegistry
|
|
119
|
+
});
|
|
120
|
+
asyncResult.set[isMellow] = false;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The assignGingerly searches the registry for any items that has a mapping with a matching symbol of isHappy and isMellow, and if found, sees if it already has an instance of the spawn class associated with the first passed in parameter. If no such instance is found, it instantiates one, associates the instance with the first parameter, then sets the property value.
|
|
124
|
+
|
|
125
|
+
It also adds a lazy property to the first passed in parameter, "set", which returns a proxy, and that proxy watches for symbol references passed in a value, and sets the value from that spawned instance. Again, if the spawned instance is not found, it respawns it.
|
|
126
|
+
|
|
127
|
+
The suggestion to use Symbol.for with a guid, as opposed to just Symbol(), is based on some negative experiences I've had with multiple versions of the same library being referenced, but is not required. Regular symbols could also be used when that risk can be avoided.
|
|
128
|
+
|
|
129
|
+
Note that the example above is the first time we mention async. This is only necessary if you wish to work directly with the merged object. This allows for lazy loading of the spawning class, which can be useful for large applications that don't need to download all the classes at once. If you are just "depositing" values into the object, no need to await for anything. Also, the assignGingerly should first do all the class instantiations that are already loaded (where the class constructor is specified in spawn), and then does all the lazy loaded ones.
|
|
61
130
|
|
|
131
|
+
## Support for JSON assignment with Symbol.for symbols
|
|
132
|
+
|
|
133
|
+
```JavaScript
|
|
134
|
+
const asyncResult = await assignGingerly({}, {
|
|
135
|
+
"[Symbol.for('TFWsx0YH5E6eSfhE7zfLxA')]": true,
|
|
136
|
+
"[Symbol.for('BqnnTPWRHkWdVGWcGQoAiw')]": true,
|
|
137
|
+
'?.style.height': '40px',
|
|
138
|
+
'?.enhancements?.mellowYellow?.madAboutFourteen': true
|
|
139
|
+
}, {
|
|
140
|
+
registry: BaseRegistry
|
|
141
|
+
});
|
|
62
142
|
```
|
|
143
|
+
|
|
144
|
+
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for registry items that define dependency injection mappings
|
|
3
|
+
*/
|
|
4
|
+
export interface IBaseRegistryItem<T = any> {
|
|
5
|
+
spawn: { new (): T } | Promise<{ new (): T }>;
|
|
6
|
+
map: { [key: string | symbol]: keyof T };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interface for the options passed to assignGingerly
|
|
11
|
+
*/
|
|
12
|
+
export interface IAssignGingerlyOptions {
|
|
13
|
+
registry?: typeof BaseRegistry | BaseRegistry;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base registry class for managing dependency injection
|
|
18
|
+
*/
|
|
19
|
+
export declare class BaseRegistry {
|
|
20
|
+
private items;
|
|
21
|
+
push(items: IBaseRegistryItem | IBaseRegistryItem[]): void;
|
|
22
|
+
getItems(): IBaseRegistryItem[];
|
|
23
|
+
findBySymbol(symbol: symbol | string): IBaseRegistryItem | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Main assignGingerly function
|
|
28
|
+
*/
|
|
29
|
+
export declare function assignGingerly(
|
|
30
|
+
target: any,
|
|
31
|
+
source: Record<string | symbol, any>,
|
|
32
|
+
options?: IAssignGingerlyOptions
|
|
33
|
+
): Promise<any>;
|
|
34
|
+
|
|
35
|
+
export default assignGingerly;
|
package/index.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map to store spawned instances associated with objects
|
|
3
|
+
*/
|
|
4
|
+
const instanceMap = new WeakMap();
|
|
5
|
+
/**
|
|
6
|
+
* Base registry class for managing dependency injection
|
|
7
|
+
*/
|
|
8
|
+
export class BaseRegistry {
|
|
9
|
+
items = [];
|
|
10
|
+
push(items) {
|
|
11
|
+
if (Array.isArray(items)) {
|
|
12
|
+
this.items.push(...items);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.items.push(items);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
getItems() {
|
|
19
|
+
return this.items;
|
|
20
|
+
}
|
|
21
|
+
findBySymbol(symbol) {
|
|
22
|
+
return this.items.find(item => {
|
|
23
|
+
const map = item.map;
|
|
24
|
+
return Object.keys(map).some(key => {
|
|
25
|
+
if (typeof key === 'symbol' || (typeof map[key] === 'symbol')) {
|
|
26
|
+
return key === symbol || map[key] === symbol;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}) || Object.getOwnPropertySymbols(map).some(sym => sym === symbol);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Helper function to check if a string key represents a Symbol.for expression
|
|
35
|
+
*/
|
|
36
|
+
function isSymbolForKey(key) {
|
|
37
|
+
return key.startsWith('[Symbol.for(') && key.endsWith(')]');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Helper function to extract the symbol key from a Symbol.for string
|
|
41
|
+
*/
|
|
42
|
+
function parseSymbolForKey(key) {
|
|
43
|
+
const match = key.match(/^\[Symbol\.for\(['"](.+)['"]\)\]$/);
|
|
44
|
+
if (match && match[1]) {
|
|
45
|
+
return Symbol.for(match[1]);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Helper function to parse a path string with ?. notation
|
|
51
|
+
*/
|
|
52
|
+
function parsePath(path) {
|
|
53
|
+
return path
|
|
54
|
+
.split('.')
|
|
55
|
+
.map(part => part.replace(/\?/g, ''))
|
|
56
|
+
.filter(part => part.length > 0);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Helper function to check if a path starts with ?. notation
|
|
60
|
+
*/
|
|
61
|
+
function isNestedPath(path) {
|
|
62
|
+
return path.startsWith('?.');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Helper function to get or create a nested object
|
|
66
|
+
*/
|
|
67
|
+
function ensureNestedPath(obj, pathParts) {
|
|
68
|
+
let current = obj;
|
|
69
|
+
for (const part of pathParts.slice(0, -1)) {
|
|
70
|
+
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
71
|
+
current[part] = {};
|
|
72
|
+
}
|
|
73
|
+
current = current[part];
|
|
74
|
+
}
|
|
75
|
+
return current;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Main assignGingerly function
|
|
79
|
+
*/
|
|
80
|
+
export async function assignGingerly(target, source, options) {
|
|
81
|
+
if (!target || typeof target !== 'object') {
|
|
82
|
+
return target;
|
|
83
|
+
}
|
|
84
|
+
const registry = options?.registry instanceof BaseRegistry
|
|
85
|
+
? options.registry
|
|
86
|
+
: options?.registry
|
|
87
|
+
? new options.registry()
|
|
88
|
+
: undefined;
|
|
89
|
+
// Track promises for async spawning
|
|
90
|
+
const asyncSpawns = [];
|
|
91
|
+
// Convert Symbol.for string keys to actual symbols
|
|
92
|
+
const processedSource = {};
|
|
93
|
+
for (const key of Object.keys(source)) {
|
|
94
|
+
if (isSymbolForKey(key)) {
|
|
95
|
+
const symbol = parseSymbolForKey(key);
|
|
96
|
+
if (symbol) {
|
|
97
|
+
processedSource[symbol] = source[key];
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Invalid Symbol.for format - treat as regular string key
|
|
101
|
+
processedSource[key] = source[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
processedSource[key] = source[key];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Copy over actual symbol keys
|
|
109
|
+
for (const sym of Object.getOwnPropertySymbols(source)) {
|
|
110
|
+
processedSource[sym] = source[sym];
|
|
111
|
+
}
|
|
112
|
+
// First pass: handle all non-symbol keys and sync operations
|
|
113
|
+
for (const key of Object.keys(processedSource)) {
|
|
114
|
+
const value = processedSource[key];
|
|
115
|
+
if (isNestedPath(key)) {
|
|
116
|
+
const pathParts = parsePath(key);
|
|
117
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
118
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
119
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
120
|
+
// Recursively apply assignGingerly for nested objects
|
|
121
|
+
if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
|
|
122
|
+
parent[lastKey] = {};
|
|
123
|
+
}
|
|
124
|
+
await assignGingerly(parent[lastKey], value, options);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
parent[lastKey] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
132
|
+
// Recursively apply assignGingerly for nested objects
|
|
133
|
+
if (!(key in target) || typeof target[key] !== 'object') {
|
|
134
|
+
target[key] = {};
|
|
135
|
+
}
|
|
136
|
+
await assignGingerly(target[key], value, options);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
target[key] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Second pass: handle symbol keys for dependency injection
|
|
144
|
+
const symbols = Object.getOwnPropertySymbols(processedSource);
|
|
145
|
+
for (const sym of symbols) {
|
|
146
|
+
const value = processedSource[sym];
|
|
147
|
+
if (registry) {
|
|
148
|
+
const registryItem = registry.findBySymbol(sym);
|
|
149
|
+
if (registryItem) {
|
|
150
|
+
// Get or initialize the instances map for this target
|
|
151
|
+
if (!instanceMap.has(target)) {
|
|
152
|
+
instanceMap.set(target, new Map());
|
|
153
|
+
}
|
|
154
|
+
const instances = instanceMap.get(target);
|
|
155
|
+
// Check if instance already exists
|
|
156
|
+
let instance = instances.get(sym);
|
|
157
|
+
if (!instance) {
|
|
158
|
+
// Check if spawn is a constructor or a promise
|
|
159
|
+
const SpawnClass = await Promise.resolve(registryItem.spawn);
|
|
160
|
+
instance = new SpawnClass();
|
|
161
|
+
instances.set(sym, instance);
|
|
162
|
+
}
|
|
163
|
+
// Find the mapped property name
|
|
164
|
+
const mappedKey = registryItem.map[sym];
|
|
165
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
166
|
+
instance[mappedKey] = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Add lazy 'set' property that returns a proxy
|
|
172
|
+
if (registry && !('set' in target)) {
|
|
173
|
+
Object.defineProperty(target, 'set', {
|
|
174
|
+
get() {
|
|
175
|
+
return new Proxy({}, {
|
|
176
|
+
set: (_, prop, value) => {
|
|
177
|
+
if (typeof prop === 'symbol') {
|
|
178
|
+
const registryItem = registry.findBySymbol(prop);
|
|
179
|
+
if (registryItem) {
|
|
180
|
+
if (!instanceMap.has(target)) {
|
|
181
|
+
instanceMap.set(target, new Map());
|
|
182
|
+
}
|
|
183
|
+
const instances = instanceMap.get(target);
|
|
184
|
+
let instance = instances.get(prop);
|
|
185
|
+
if (!instance) {
|
|
186
|
+
const SpawnClass = registryItem.spawn;
|
|
187
|
+
if (SpawnClass instanceof Promise) {
|
|
188
|
+
// Handle async case - would need to be awaited externally
|
|
189
|
+
SpawnClass.then((SC) => {
|
|
190
|
+
instance = new SC();
|
|
191
|
+
instances.set(prop, instance);
|
|
192
|
+
const mappedKey = registryItem.map[prop];
|
|
193
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
194
|
+
instance[mappedKey] = value;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
instance = new SpawnClass();
|
|
200
|
+
instances.set(prop, instance);
|
|
201
|
+
const mappedKey = registryItem.map[prop];
|
|
202
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
203
|
+
instance[mappedKey] = value;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const mappedKey = registryItem.map[prop];
|
|
209
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
210
|
+
instance[mappedKey] = value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
configurable: true,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return target;
|
|
223
|
+
}
|
|
224
|
+
export default assignGingerly;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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": {
|
|
@@ -13,8 +13,29 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
|
|
15
15
|
"type": "module",
|
|
16
|
+
"types": "index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"index.d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./index.js",
|
|
26
|
+
"types": "./index.d.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
16
29
|
"main": "index.js",
|
|
17
30
|
"scripts": {
|
|
18
|
-
"
|
|
31
|
+
"serve": "node ./node_modules/spa-ssi/serve.js",
|
|
32
|
+
"test": "playwright test",
|
|
33
|
+
"update": "ncu -u && npm install"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@playwright/test": "^1.58.0",
|
|
37
|
+
"spa-ssi": "0.0.26",
|
|
38
|
+
"@types/node": "^25.0.10",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
19
40
|
}
|
|
20
41
|
}
|