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.
Files changed (4) hide show
  1. package/README.md +83 -1
  2. package/index.d.ts +35 -0
  3. package/index.js +224 -0
  4. package/package.json +23 -2
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # assign-gingerly
2
2
 
3
+ [![Playwright Tests](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml/badge.svg?branch=baseline)](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml)
4
+ [![NPM version](https://badge.fury.io/js/assign-gingerly.png)](http://badge.fury.io/js/assign-gingerly)
5
+ [![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/assign-gingerly?style=for-the-badge)](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.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
- "test": "playwright test"
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
  }