assign-gingerly 0.0.71 → 0.0.73

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 (38) hide show
  1. package/DX/emojis.js +16 -0
  2. package/DX/emojis.ts +16 -0
  3. package/README.md +30 -3
  4. package/assignFrom.js +6 -6
  5. package/assignFrom.ts +11 -10
  6. package/assignFromAsync.js +3 -3
  7. package/assignFromAsync.ts +4 -4
  8. package/assignGingerly.js +136 -69
  9. package/assignGingerly.ts +170 -103
  10. package/assignPermissions/isAllowedImportPath.js +41 -0
  11. package/assignPermissions/isAllowedImportPath.ts +38 -0
  12. package/assignPermissions/restrictedProps.js +43 -0
  13. package/assignPermissions/restrictedProps.ts +53 -0
  14. package/assignTentatively.js +45 -30
  15. package/assignTentatively.ts +85 -60
  16. package/defineWithFeatures.js +38 -32
  17. package/defineWithFeatures.ts +46 -39
  18. package/eachTime.js +12 -4
  19. package/eachTime.ts +17 -7
  20. package/enhanceAll.js +2 -2
  21. package/enhanceAll.ts +3 -3
  22. package/evaluatePathWithAsyncMethods.js +29 -16
  23. package/evaluatePathWithAsyncMethods.ts +31 -16
  24. package/handlers/addEventListener.js +11 -11
  25. package/handlers/addEventListener.ts +18 -15
  26. package/handlers/lazyLoad.ts +10 -7
  27. package/handlers/lazyLoadSwitch.ts +4 -3
  28. package/handlers/manageTemplateList.js +9 -9
  29. package/handlers/manageTemplateList.ts +11 -10
  30. package/handlers/rangeSelector.ts +4 -3
  31. package/inferencer/types/assign-gingerly/types.d.ts +63 -4
  32. package/inferencer/types/nested-regex-groups/types.d.ts +12 -0
  33. package/package.json +6 -5
  34. package/processHandlerCommands.js +7 -4
  35. package/processHandlerCommands.ts +13 -10
  36. package/types/assign-gingerly/types.d.ts +63 -4
  37. package/isAllowedImportPath.js +0 -42
  38. package/isAllowedImportPath.ts +0 -53
@@ -0,0 +1,53 @@
1
+ import type { AssignPermissions, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
2
+
3
+ export type RestrictedPropSettingsMap = Map<string, RestrictedPropSetting | undefined>;
4
+
5
+ const warnedOnce = new Set<string>();
6
+
7
+ function warnRestricted(key: string): void {
8
+ if (!warnedOnce.has(key)) {
9
+ warnedOnce.add(key);
10
+ console.warn(`assignGingerly: property '${key}' is in restrictedPropSettings — assignment skipped.`);
11
+ }
12
+ }
13
+
14
+ export function buildRestrictedPropSet(permissions: AssignPermissions | undefined): RestrictedPropSettingsMap | undefined {
15
+ const settings = permissions?.restrictedPropSettings;
16
+ if (!settings || settings.length === 0) return undefined;
17
+ const restrictedPropSet: RestrictedPropSettingsMap = new Map();
18
+ for (const setting of settings) {
19
+ const prop = typeof setting === 'string' ? setting : setting.prop;
20
+ if (restrictedPropSet.has(prop)) {
21
+ throw new Error(`assignGingerly: duplicate restrictedPropSettings entry for '${prop}'.`);
22
+ }
23
+ restrictedPropSet.set(prop, typeof setting === 'string' ? undefined : setting);
24
+ }
25
+ return restrictedPropSet;
26
+ }
27
+
28
+ export function checkRestrictedProp(restrictedPropSet: RestrictedPropSettingsMap | undefined, key: string): boolean {
29
+ if (!restrictedPropSet || !restrictedPropSet.has(key)) return false;
30
+ warnRestricted(key);
31
+ return true;
32
+ }
33
+
34
+ export function redirectRestrictedProp(
35
+ restrictedPropSet: RestrictedPropSettingsMap | undefined,
36
+ target: any,
37
+ key: string,
38
+ value: any
39
+ ): boolean {
40
+ if (!restrictedPropSet || !restrictedPropSet.has(key)) return false;
41
+ const setting = restrictedPropSet.get(key);
42
+ if (!setting?.useMethod) {
43
+ warnRestricted(key);
44
+ return true;
45
+ }
46
+ const method = target?.[setting.useMethod];
47
+ if (typeof method !== 'function') {
48
+ warnRestricted(key);
49
+ return true;
50
+ }
51
+ method.call(target, value);
52
+ return true;
53
+ }
@@ -1,3 +1,4 @@
1
+ import { buildRestrictedPropSet, checkRestrictedProp } from './assignPermissions/restrictedProps.js';
1
2
  /**
2
3
  * Helper function to check if a string key represents an += command
3
4
  */
@@ -13,6 +14,23 @@ function parseIncCommand(key) {
13
14
  }
14
15
  return key.substring(0, key.length - 3); // Remove ' +=' suffix
15
16
  }
17
+ /**
18
+ * Apply the scalar and array semantics of the += command.
19
+ */
20
+ function addValue(lhs, rhs) {
21
+ if (Array.isArray(lhs)) {
22
+ return Array.isArray(rhs) ? [...lhs, ...rhs] : [...lhs, rhs];
23
+ }
24
+ if (typeof lhs === 'number' && typeof rhs === 'string') {
25
+ const parsed = Number(rhs);
26
+ return Number.isNaN(parsed) ? lhs + rhs : lhs + parsed;
27
+ }
28
+ if (typeof lhs === 'string' && typeof rhs === 'number') {
29
+ const parsed = Number(lhs);
30
+ return Number.isNaN(parsed) ? lhs + rhs : (parsed + rhs).toString();
31
+ }
32
+ return lhs + rhs;
33
+ }
16
34
  /**
17
35
  * Helper function to check if a key represents a =! command
18
36
  */
@@ -96,12 +114,13 @@ function getTopLevelKey(path) {
96
114
  /**
97
115
  * Main assignTentatively function with reversal support
98
116
  */
99
- export function assignTentatively(target, source, options) {
117
+ export function assignTentatively(target, source, options, permissions) {
100
118
  if (!target || typeof target !== 'object') {
101
119
  return target;
102
120
  }
103
121
  const reversal = options?.reversal || {};
104
122
  const trackedCreatedPaths = new Set();
123
+ const restrictedPropSet = buildRestrictedPropSet(permissions);
105
124
  // Process all keys from source
106
125
  for (const key of Object.keys(source)) {
107
126
  const value = source[key];
@@ -112,11 +131,14 @@ export function assignTentatively(target, source, options) {
112
131
  if (isNestedPath(path)) {
113
132
  const pathParts = parsePath(path);
114
133
  const topLevelKey = pathParts[0];
134
+ const lastKey = pathParts[pathParts.length - 1];
135
+ if (checkRestrictedProp(restrictedPropSet, lastKey)) {
136
+ continue;
137
+ }
115
138
  // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
116
139
  if (!(topLevelKey in target)) {
117
140
  trackedCreatedPaths.add(topLevelKey);
118
141
  }
119
- const lastKey = pathParts[pathParts.length - 1];
120
142
  const parent = ensureNestedPath(target, pathParts);
121
143
  // If property already exists, store original value for reversal
122
144
  if (lastKey in parent) {
@@ -124,18 +146,7 @@ export function assignTentatively(target, source, options) {
124
146
  if (!(fullPath in reversal)) {
125
147
  reversal[fullPath] = parent[lastKey];
126
148
  }
127
- if (Array.isArray(parent[lastKey])) {
128
- parent[lastKey] = Array.isArray(value)
129
- ? [...parent[lastKey], ...value]
130
- : [...parent[lastKey], value];
131
- }
132
- else if (typeof parent[lastKey] === 'number' && typeof value === 'string') {
133
- const parsed = Number(value);
134
- parent[lastKey] = isNaN(parsed) ? parent[lastKey] + value : parent[lastKey] + parsed;
135
- }
136
- else {
137
- parent[lastKey] += value;
138
- }
149
+ parent[lastKey] = addValue(parent[lastKey], value);
139
150
  }
140
151
  else {
141
152
  // Property doesn't exist, create it with the value
@@ -144,22 +155,14 @@ export function assignTentatively(target, source, options) {
144
155
  }
145
156
  else {
146
157
  // Plain key - direct operation on target
158
+ if (checkRestrictedProp(restrictedPropSet, path)) {
159
+ continue;
160
+ }
147
161
  if (path in target) {
148
162
  if (!(path in reversal)) {
149
163
  reversal[path] = target[path];
150
164
  }
151
- if (Array.isArray(target[path])) {
152
- target[path] = Array.isArray(value)
153
- ? [...target[path], ...value]
154
- : [...target[path], value];
155
- }
156
- else if (typeof target[path] === 'number' && typeof value === 'string') {
157
- const parsed = Number(value);
158
- target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
159
- }
160
- else {
161
- target[path] += value;
162
- }
165
+ target[path] = addValue(target[path], value);
163
166
  }
164
167
  else {
165
168
  target[path] = value;
@@ -179,16 +182,22 @@ export function assignTentatively(target, source, options) {
179
182
  if (isNestedPath(lhsPath)) {
180
183
  lhsPathParts = parsePath(lhsPath);
181
184
  const topLevelKey = lhsPathParts[0];
185
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
186
+ if (checkRestrictedProp(restrictedPropSet, lhsLastKey)) {
187
+ continue;
188
+ }
182
189
  // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
183
190
  if (!(topLevelKey in target)) {
184
191
  trackedCreatedPaths.add(topLevelKey);
185
192
  }
186
- lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
187
193
  lhsParent = ensureNestedPath(target, lhsPathParts);
188
194
  }
189
195
  else {
190
196
  lhsPathParts = [lhsPath];
191
197
  lhsLastKey = lhsPath;
198
+ if (checkRestrictedProp(restrictedPropSet, lhsLastKey)) {
199
+ continue;
200
+ }
192
201
  lhsParent = target;
193
202
  }
194
203
  // Determine what to negate
@@ -291,11 +300,14 @@ export function assignTentatively(target, source, options) {
291
300
  if (isNestedPath(key)) {
292
301
  const pathParts = parsePath(key);
293
302
  const topLevelKey = pathParts[0];
303
+ const lastKey = pathParts[pathParts.length - 1];
304
+ if (checkRestrictedProp(restrictedPropSet, lastKey)) {
305
+ continue;
306
+ }
294
307
  // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
295
308
  if (!(topLevelKey in target)) {
296
309
  trackedCreatedPaths.add(topLevelKey);
297
310
  }
298
- const lastKey = pathParts[pathParts.length - 1];
299
311
  const parent = ensureNestedPath(target, pathParts);
300
312
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
301
313
  // Recursively apply assignTentatively for nested objects
@@ -311,7 +323,7 @@ export function assignTentatively(target, source, options) {
311
323
  }
312
324
  // For nested objects, recursively apply with nested reversal tracking
313
325
  const nestedReversal = {};
314
- assignTentatively(parent[lastKey], value, { reversal: nestedReversal });
326
+ assignTentatively(parent[lastKey], value, { reversal: nestedReversal }, permissions);
315
327
  // Merge nested reversals
316
328
  for (const revKey of Object.keys(nestedReversal)) {
317
329
  if (!(revKey in reversal)) {
@@ -332,6 +344,9 @@ export function assignTentatively(target, source, options) {
332
344
  }
333
345
  else {
334
346
  // Non-nested key
347
+ if (checkRestrictedProp(restrictedPropSet, key)) {
348
+ continue;
349
+ }
335
350
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
336
351
  // Recursively apply assignTentatively for nested objects
337
352
  if (!(key in target) || typeof target[key] !== 'object') {
@@ -344,7 +359,7 @@ export function assignTentatively(target, source, options) {
344
359
  target[key] = {};
345
360
  }
346
361
  const nestedReversal = {};
347
- assignTentatively(target[key], value, { reversal: nestedReversal });
362
+ assignTentatively(target[key], value, { reversal: nestedReversal }, permissions);
348
363
  // Merge nested reversals
349
364
  for (const revKey of Object.keys(nestedReversal)) {
350
365
  if (!(revKey in reversal)) {
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * assignTentatively — reversible assignment with change tracking.
3
3
  */
4
- import type { IAssignTentativelyOptions } from './types/assign-gingerly/types.js';
4
+ import type { IAssignTentativelyOptions } from './types/assign-gingerly/types.js';
5
+ import type { AssignPermissions } from './types/assign-gingerly/types.js';
6
+ import { buildRestrictedPropSet, checkRestrictedProp } from './assignPermissions/restrictedProps.js';
5
7
  export type { IAssignTentativelyOptions };
6
8
 
7
9
  /**
@@ -14,12 +16,30 @@ function isIncCommand(key: string): boolean {
14
16
  /**
15
17
  * Helper function to parse an += command and extract the path
16
18
  */
17
- function parseIncCommand(key: string): string | null {
18
- if (!isIncCommand(key)) {
19
- return null;
20
- }
21
- return key.substring(0, key.length - 3); // Remove ' +=' suffix
22
- }
19
+ function parseIncCommand(key: string): string | null {
20
+ if (!isIncCommand(key)) {
21
+ return null;
22
+ }
23
+ return key.substring(0, key.length - 3); // Remove ' +=' suffix
24
+ }
25
+
26
+ /**
27
+ * Apply the scalar and array semantics of the += command.
28
+ */
29
+ function addValue(lhs: any, rhs: any): any {
30
+ if (Array.isArray(lhs)) {
31
+ return Array.isArray(rhs) ? [...lhs, ...rhs] : [...lhs, rhs];
32
+ }
33
+ if (typeof lhs === 'number' && typeof rhs === 'string') {
34
+ const parsed = Number(rhs);
35
+ return Number.isNaN(parsed) ? lhs + rhs : lhs + parsed;
36
+ }
37
+ if (typeof lhs === 'string' && typeof rhs === 'number') {
38
+ const parsed = Number(lhs);
39
+ return Number.isNaN(parsed) ? lhs + rhs : (parsed + rhs).toString();
40
+ }
41
+ return lhs + rhs;
42
+ }
23
43
 
24
44
  /**
25
45
  * Helper function to check if a key represents a =! command
@@ -114,16 +134,18 @@ function getTopLevelKey(path: string): string | null {
114
134
  * Main assignTentatively function with reversal support
115
135
  */
116
136
  export function assignTentatively(
117
- target: any,
118
- source: Record<string | symbol, any>,
119
- options?: IAssignTentativelyOptions
120
- ): any {
137
+ target: any,
138
+ source: Record<string | symbol, any>,
139
+ options?: IAssignTentativelyOptions,
140
+ permissions?: AssignPermissions
141
+ ): any {
121
142
  if (!target || typeof target !== 'object') {
122
143
  return target;
123
144
  }
124
145
 
125
- const reversal = options?.reversal || {};
126
- const trackedCreatedPaths = new Set<string>();
146
+ const reversal = options?.reversal || {};
147
+ const trackedCreatedPaths = new Set<string>();
148
+ const restrictedPropSet = buildRestrictedPropSet(permissions);
127
149
 
128
150
  // Process all keys from source
129
151
  for (const key of Object.keys(source)) {
@@ -133,17 +155,21 @@ export function assignTentatively(
133
155
  if (isIncCommand(key)) {
134
156
  const path = parseIncCommand(key);
135
157
  if (path) {
136
- if (isNestedPath(path)) {
137
- const pathParts = parsePath(path);
138
- const topLevelKey = pathParts[0];
158
+ if (isNestedPath(path)) {
159
+ const pathParts = parsePath(path);
160
+ const topLevelKey = pathParts[0];
161
+ const lastKey = pathParts[pathParts.length - 1];
162
+
163
+ if (checkRestrictedProp(restrictedPropSet, lastKey)) {
164
+ continue;
165
+ }
139
166
 
140
167
  // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
141
168
  if (!(topLevelKey in target)) {
142
169
  trackedCreatedPaths.add(topLevelKey);
143
170
  }
144
171
 
145
- const lastKey = pathParts[pathParts.length - 1];
146
- const parent = ensureNestedPath(target, pathParts);
172
+ const parent = ensureNestedPath(target, pathParts);
147
173
 
148
174
  // If property already exists, store original value for reversal
149
175
  if (lastKey in parent) {
@@ -151,36 +177,21 @@ export function assignTentatively(
151
177
  if (!(fullPath in reversal)) {
152
178
  reversal[fullPath] = parent[lastKey];
153
179
  }
154
- if (Array.isArray(parent[lastKey])) {
155
- parent[lastKey] = Array.isArray(value)
156
- ? [...parent[lastKey], ...value]
157
- : [...parent[lastKey], value];
158
- } else if (typeof parent[lastKey] === 'number' && typeof value === 'string') {
159
- const parsed = Number(value);
160
- parent[lastKey] = isNaN(parsed) ? parent[lastKey] + value : parent[lastKey] + parsed;
161
- } else {
162
- parent[lastKey] += value;
163
- }
180
+ parent[lastKey] = addValue(parent[lastKey], value);
164
181
  } else {
165
182
  // Property doesn't exist, create it with the value
166
183
  parent[lastKey] = value;
167
184
  }
168
- } else {
169
- // Plain key - direct operation on target
170
- if (path in target) {
185
+ } else {
186
+ // Plain key - direct operation on target
187
+ if (checkRestrictedProp(restrictedPropSet, path)) {
188
+ continue;
189
+ }
190
+ if (path in target) {
171
191
  if (!(path in reversal)) {
172
192
  reversal[path] = target[path];
173
193
  }
174
- if (Array.isArray(target[path])) {
175
- target[path] = Array.isArray(value)
176
- ? [...target[path], ...value]
177
- : [...target[path], value];
178
- } else if (typeof target[path] === 'number' && typeof value === 'string') {
179
- const parsed = Number(value);
180
- target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
181
- } else {
182
- target[path] += value;
183
- }
194
+ target[path] = addValue(target[path], value);
184
195
  } else {
185
196
  target[path] = value;
186
197
  }
@@ -199,21 +210,28 @@ export function assignTentatively(
199
210
  let lhsLastKey: string;
200
211
  let lhsPathParts: string[];
201
212
 
202
- if (isNestedPath(lhsPath)) {
203
- lhsPathParts = parsePath(lhsPath);
204
- const topLevelKey = lhsPathParts[0];
213
+ if (isNestedPath(lhsPath)) {
214
+ lhsPathParts = parsePath(lhsPath);
215
+ const topLevelKey = lhsPathParts[0];
216
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
217
+
218
+ if (checkRestrictedProp(restrictedPropSet, lhsLastKey)) {
219
+ continue;
220
+ }
205
221
 
206
222
  // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
207
223
  if (!(topLevelKey in target)) {
208
224
  trackedCreatedPaths.add(topLevelKey);
209
225
  }
210
226
 
211
- lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
212
- lhsParent = ensureNestedPath(target, lhsPathParts);
213
- } else {
214
- lhsPathParts = [lhsPath];
215
- lhsLastKey = lhsPath;
216
- lhsParent = target;
227
+ lhsParent = ensureNestedPath(target, lhsPathParts);
228
+ } else {
229
+ lhsPathParts = [lhsPath];
230
+ lhsLastKey = lhsPath;
231
+ if (checkRestrictedProp(restrictedPropSet, lhsLastKey)) {
232
+ continue;
233
+ }
234
+ lhsParent = target;
217
235
  }
218
236
 
219
237
  // Determine what to negate
@@ -314,17 +332,21 @@ export function assignTentatively(
314
332
  continue;
315
333
  }
316
334
 
317
- if (isNestedPath(key)) {
318
- const pathParts = parsePath(key);
319
- const topLevelKey = pathParts[0];
335
+ if (isNestedPath(key)) {
336
+ const pathParts = parsePath(key);
337
+ const topLevelKey = pathParts[0];
338
+ const lastKey = pathParts[pathParts.length - 1];
339
+
340
+ if (checkRestrictedProp(restrictedPropSet, lastKey)) {
341
+ continue;
342
+ }
320
343
 
321
344
  // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
322
345
  if (!(topLevelKey in target)) {
323
346
  trackedCreatedPaths.add(topLevelKey);
324
347
  }
325
348
 
326
- const lastKey = pathParts[pathParts.length - 1];
327
- const parent = ensureNestedPath(target, pathParts);
349
+ const parent = ensureNestedPath(target, pathParts);
328
350
 
329
351
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
330
352
  // Recursively apply assignTentatively for nested objects
@@ -340,7 +362,7 @@ export function assignTentatively(
340
362
  }
341
363
  // For nested objects, recursively apply with nested reversal tracking
342
364
  const nestedReversal: Record<string | symbol, any> = {};
343
- assignTentatively(parent[lastKey], value, { reversal: nestedReversal });
365
+ assignTentatively(parent[lastKey], value, { reversal: nestedReversal }, permissions);
344
366
  // Merge nested reversals
345
367
  for (const revKey of Object.keys(nestedReversal)) {
346
368
  if (!(revKey in reversal)) {
@@ -357,9 +379,12 @@ export function assignTentatively(
357
379
  }
358
380
  parent[lastKey] = value;
359
381
  }
360
- } else {
361
- // Non-nested key
362
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
382
+ } else {
383
+ // Non-nested key
384
+ if (checkRestrictedProp(restrictedPropSet, key)) {
385
+ continue;
386
+ }
387
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
363
388
  // Recursively apply assignTentatively for nested objects
364
389
  if (!(key in target) || typeof target[key] !== 'object') {
365
390
  // Store original value for reversal if it existed
@@ -371,7 +396,7 @@ export function assignTentatively(
371
396
  target[key] = {};
372
397
  }
373
398
  const nestedReversal: Record<string | symbol, any> = {};
374
- assignTentatively(target[key], value, { reversal: nestedReversal });
399
+ assignTentatively(target[key], value, { reversal: nestedReversal }, permissions);
375
400
  // Merge nested reversals
376
401
  for (const revKey of Object.keys(nestedReversal)) {
377
402
  if (!(revKey in reversal)) {
@@ -73,29 +73,33 @@ export async function defineWithFeatures(tagName, baseTagName, config, registry,
73
73
  classCache = new Map();
74
74
  resolvedSpawnCache.set(BaseClass, classCache);
75
75
  }
76
- const featureKeys = Object.keys(config.assignFeatures);
77
- const resolvedSpawns = new Map();
78
- await Promise.all(featureKeys.map(async (key) => {
79
- const optIn = supportedFeatures[key];
80
- if (!optIn) {
81
- throw new Error(`defineWithFeatures: feature "${key}" not found in ${baseTagName}.supportedFeatures`);
82
- }
83
- // Check cache first
84
- if (classCache.has(key)) {
85
- resolvedSpawns.set(key, classCache.get(key));
86
- return;
87
- }
88
- let spawn = optIn.fallbackSpawn;
89
- if (spawn && isAsyncSpawn(spawn)) {
90
- // Resolve the async spawner
91
- spawn = await spawn();
92
- }
93
- // Cache the resolved spawn
94
- if (spawn) {
95
- classCache.set(key, spawn);
96
- }
97
- resolvedSpawns.set(key, spawn);
98
- }));
76
+ const { assignFeatures: af } = config;
77
+ let resolvedSpawns;
78
+ if (af) {
79
+ const featureKeys = Object.keys(af);
80
+ resolvedSpawns = new Map();
81
+ await Promise.all(featureKeys.map(async (key) => {
82
+ const optIn = supportedFeatures[key];
83
+ if (!optIn) {
84
+ throw new Error(`defineWithFeatures: feature "${key}" not found in ${baseTagName}.supportedFeatures`);
85
+ }
86
+ // Check cache first
87
+ if (classCache.has(key)) {
88
+ resolvedSpawns.set(key, classCache.get(key));
89
+ return;
90
+ }
91
+ let spawn = optIn.fallbackSpawn;
92
+ if (spawn && isAsyncSpawn(spawn)) {
93
+ // Resolve the async spawner
94
+ spawn = await spawn();
95
+ }
96
+ // Cache the resolved spawn
97
+ if (spawn) {
98
+ classCache.set(key, spawn);
99
+ }
100
+ resolvedSpawns.set(key, spawn);
101
+ }));
102
+ }
99
103
  // 3. Create subclass
100
104
  const NewClass = class extends BaseClass {
101
105
  };
@@ -103,16 +107,18 @@ export async function defineWithFeatures(tagName, baseTagName, config, registry,
103
107
  if (options?.onSubclassCreated) {
104
108
  options.onSubclassCreated(NewClass);
105
109
  }
106
- // 4. Build FeatureConfigsMap: resolved spawns + JSON config
107
- const featuresMap = {};
108
- for (const [key, jsonConfig] of Object.entries(config.assignFeatures)) {
109
- featuresMap[key] = {
110
- spawn: resolvedSpawns.get(key),
111
- ...jsonConfig
112
- };
110
+ if (af) {
111
+ // 4. Build FeatureConfigsMap: resolved spawns + JSON config
112
+ const featuresMap = {};
113
+ for (const [key, jsonConfig] of Object.entries(af)) {
114
+ featuresMap[key] = {
115
+ spawn: resolvedSpawns.get(key),
116
+ ...jsonConfig
117
+ };
118
+ }
119
+ // 5. assignFeatures (sequential onAssigned) + define
120
+ await assignFeatures(NewClass, featuresMap, reg.featuresRegistry);
113
121
  }
114
- // 5. assignFeatures (sequential onAssigned) + define
115
- await assignFeatures(NewClass, featuresMap, reg.featuresRegistry);
116
122
  reg.define(tagName, NewClass);
117
123
  return NewClass;
118
124
  }
@@ -104,56 +104,63 @@ export async function defineWithFeatures(
104
104
  classCache = new Map();
105
105
  resolvedSpawnCache.set(BaseClass, classCache);
106
106
  }
107
+ const {assignFeatures: af} = config;
108
+ let resolvedSpawns: Map<string, any> | undefined;
109
+ if (af) {
110
+ const featureKeys = Object.keys(af);
111
+ resolvedSpawns = new Map<string, any>();
112
+
113
+ await Promise.all(featureKeys.map(async (key) => {
114
+ const optIn = supportedFeatures[key];
115
+ if (!optIn) {
116
+ throw new Error(
117
+ `defineWithFeatures: feature "${key}" not found in ${baseTagName}.supportedFeatures`
118
+ );
119
+ }
120
+
121
+ // Check cache first
122
+ if (classCache!.has(key)) {
123
+ resolvedSpawns!.set(key, classCache!.get(key));
124
+ return;
125
+ }
126
+
127
+ let spawn = optIn.fallbackSpawn;
128
+ if (spawn && isAsyncSpawn(spawn)) {
129
+ // Resolve the async spawner
130
+ spawn = await (spawn as () => Promise<any>)();
131
+ }
132
+
133
+ // Cache the resolved spawn
134
+ if (spawn) {
135
+ classCache!.set(key, spawn);
136
+ }
137
+ resolvedSpawns!.set(key, spawn);
138
+ }));
139
+ }
107
140
 
108
- const featureKeys = Object.keys(config.assignFeatures);
109
- const resolvedSpawns = new Map<string, any>();
110
-
111
- await Promise.all(featureKeys.map(async (key) => {
112
- const optIn = supportedFeatures[key];
113
- if (!optIn) {
114
- throw new Error(
115
- `defineWithFeatures: feature "${key}" not found in ${baseTagName}.supportedFeatures`
116
- );
117
- }
118
-
119
- // Check cache first
120
- if (classCache!.has(key)) {
121
- resolvedSpawns.set(key, classCache!.get(key));
122
- return;
123
- }
124
-
125
- let spawn = optIn.fallbackSpawn;
126
- if (spawn && isAsyncSpawn(spawn)) {
127
- // Resolve the async spawner
128
- spawn = await (spawn as () => Promise<any>)();
129
- }
130
-
131
- // Cache the resolved spawn
132
- if (spawn) {
133
- classCache!.set(key, spawn);
134
- }
135
- resolvedSpawns.set(key, spawn);
136
- }));
137
141
 
138
142
  // 3. Create subclass
139
- const NewClass = class extends (BaseClass as any) {};
143
+ const NewClass = class extends (BaseClass as any) { };
140
144
 
141
145
  // 3b. Call onSubclassCreated callback (before define, before features if needed)
142
146
  if (options?.onSubclassCreated) {
143
147
  options.onSubclassCreated(NewClass);
144
148
  }
145
149
 
146
- // 4. Build FeatureConfigsMap: resolved spawns + JSON config
147
- const featuresMap: FeatureConfigsMap = {};
148
- for (const [key, jsonConfig] of Object.entries(config.assignFeatures)) {
149
- featuresMap[key] = {
150
- spawn: resolvedSpawns.get(key),
151
- ...jsonConfig
152
- };
150
+ if(af){
151
+ // 4. Build FeatureConfigsMap: resolved spawns + JSON config
152
+ const featuresMap: FeatureConfigsMap = {};
153
+ for (const [key, jsonConfig] of Object.entries(af)) {
154
+ featuresMap[key] = {
155
+ spawn: resolvedSpawns!.get(key),
156
+ ...jsonConfig
157
+ };
158
+ }
159
+
160
+ // 5. assignFeatures (sequential onAssigned) + define
161
+ await assignFeatures(NewClass, featuresMap, (reg as any).featuresRegistry);
153
162
  }
154
163
 
155
- // 5. assignFeatures (sequential onAssigned) + define
156
- await assignFeatures(NewClass, featuresMap, (reg as any).featuresRegistry);
157
164
  (reg as any).define(tagName, NewClass);
158
165
 
159
166
  return NewClass;