assign-gingerly 0.0.47 → 0.0.49
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 +42 -3
- package/assignFeatures.js +64 -23
- package/assignFeatures.ts +79 -25
- package/defineWithFeatures.js +114 -0
- package/defineWithFeatures.ts +146 -0
- package/index.js +1 -0
- package/index.ts +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -4792,11 +4792,25 @@ customElements.define('my-element', MyElement);
|
|
|
4792
4792
|
**How it works:**
|
|
4793
4793
|
|
|
4794
4794
|
- `assignFeatures` checks if the spawn class defines `static onAssigned` (via `Object.hasOwn`).
|
|
4795
|
-
- If found, calls `SpawnClass.onAssigned(ctr, featureConfig)` after installing the getter.
|
|
4796
|
-
- If `onAssigned` returns a Promise,
|
|
4797
|
-
-
|
|
4795
|
+
- If found, calls `SpawnClass.onAssigned(ctr, featureConfig, key)` after installing the getter.
|
|
4796
|
+
- If `onAssigned` returns a Promise, it is **awaited sequentially** before processing the next feature. This guarantees that features declared earlier complete their setup before later features run.
|
|
4797
|
+
- This sequential ordering enables inter-feature communication: Feature A can post configuration (via `suggestFeatureInfo`) that Feature B reads in its own `onAssigned`.
|
|
4798
|
+
- If no features have `onAssigned`, `assignFeatures` runs synchronously and returns `undefined` (backward compatible).
|
|
4798
4799
|
- Only applies to synchronous spawners (the class must be available at registration time). Async spawners can't define `onAssigned` since the class isn't loaded yet.
|
|
4799
4800
|
|
|
4801
|
+
**Sequential ordering guarantee:**
|
|
4802
|
+
|
|
4803
|
+
```JavaScript
|
|
4804
|
+
await customElements.assignFeatures(MyElement, {
|
|
4805
|
+
featureA: { spawn: FeatureA }, // FeatureA.onAssigned runs first, completes
|
|
4806
|
+
featureB: { spawn: FeatureB } // FeatureB.onAssigned runs second, can read A's output
|
|
4807
|
+
});
|
|
4808
|
+
```
|
|
4809
|
+
|
|
4810
|
+
Features are processed in declaration order. If Feature A's `onAssigned` is async, it fully completes before Feature B's `onAssigned` starts. This makes it safe for features to communicate via `suggestFeatureInfo` / `getFeatureInfoSuggestions`.
|
|
4811
|
+
|
|
4812
|
+
For full documentation on inter-feature communication, see [docs/inter-feature-communication.md](docs/inter-feature-communication.md).
|
|
4813
|
+
|
|
4800
4814
|
**`await` is always safe:**
|
|
4801
4815
|
|
|
4802
4816
|
```JavaScript
|
|
@@ -4806,6 +4820,31 @@ await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature }
|
|
|
4806
4820
|
// Both work — await on undefined is a no-op
|
|
4807
4821
|
```
|
|
4808
4822
|
|
|
4823
|
+
### Declarative element definition with `defineWithFeatures`
|
|
4824
|
+
|
|
4825
|
+
`defineWithFeatures` enables defining custom elements from JSON-serializable configuration — no class authoring needed for derived elements:
|
|
4826
|
+
|
|
4827
|
+
```JavaScript
|
|
4828
|
+
import { defineWithFeatures } from 'assign-gingerly/defineWithFeatures.js';
|
|
4829
|
+
|
|
4830
|
+
await defineWithFeatures('time-ticker', 'el-maker', {
|
|
4831
|
+
assignFeatures: {
|
|
4832
|
+
roundabout: {
|
|
4833
|
+
customData: { template: myTemplate },
|
|
4834
|
+
withAttrs: { base: 'ra', mode: '${base}-mode' },
|
|
4835
|
+
callbackForwarding: ['connectedCallback']
|
|
4836
|
+
},
|
|
4837
|
+
truthSourcer: {
|
|
4838
|
+
callbackForwarding: ['connectedCallback', 'attributeChangedCallback']
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
});
|
|
4842
|
+
```
|
|
4843
|
+
|
|
4844
|
+
It resolves async `fallbackSpawn` implementations from the base class, creates a subclass, wires up features, and defines the element. Designed for use with [mount-observer cede scripts](https://github.com/bahrus/mount-observer#custom-element-definition-cede-scripts) but works standalone.
|
|
4845
|
+
|
|
4846
|
+
For full documentation, see [docs/defineWithFeatures.md](docs/defineWithFeatures.md).
|
|
4847
|
+
|
|
4809
4848
|
<details>
|
|
4810
4849
|
<summary>Catalog of Published Custom Element Features</summary>
|
|
4811
4850
|
|
package/assignFeatures.js
CHANGED
|
@@ -386,47 +386,92 @@ export function assignFeatures(ctr, features, featuresRegistry) {
|
|
|
386
386
|
if (!supportedFeatures) {
|
|
387
387
|
throw new Error(`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`);
|
|
388
388
|
}
|
|
389
|
-
|
|
389
|
+
let hasAsync = false;
|
|
390
|
+
async function processFeatures() {
|
|
391
|
+
for (const key of Object.keys(features)) {
|
|
392
|
+
// 1. Confirm the key is opted-in via supportedFeatures
|
|
393
|
+
if (!(key in supportedFeatures)) {
|
|
394
|
+
throw new Error(`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`);
|
|
395
|
+
}
|
|
396
|
+
// 2. Check that the prototype doesn't already have this property defined
|
|
397
|
+
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
398
|
+
if (existingDescriptor) {
|
|
399
|
+
throw new Error(`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`);
|
|
400
|
+
}
|
|
401
|
+
// 3. Check that this key hasn't already been registered for this constructor
|
|
402
|
+
if (featuresRegistry.hasKey(ctr, key)) {
|
|
403
|
+
throw new Error(`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`);
|
|
404
|
+
}
|
|
405
|
+
// 4. Register the injection
|
|
406
|
+
featuresRegistry.set(ctr, key, features[key]);
|
|
407
|
+
// 5. Install the lazy getter on the prototype
|
|
408
|
+
installFeatureGetter(ctr, key, featuresRegistry);
|
|
409
|
+
// 6. Install callback forwarding if configured (merge author + consumer)
|
|
410
|
+
const featureConfig = features[key];
|
|
411
|
+
const optIn = supportedFeatures[key];
|
|
412
|
+
const authorCallbacks = optIn.callbackForwarding || [];
|
|
413
|
+
const consumerCallbacks = featureConfig.callbackForwarding || [];
|
|
414
|
+
// Union of both (author defaults + consumer additions)
|
|
415
|
+
const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
|
|
416
|
+
if (allCallbacks.length > 0) {
|
|
417
|
+
installCallbackForwarding(ctr, key, allCallbacks);
|
|
418
|
+
}
|
|
419
|
+
// 7. Call static onAssigned if the spawn class defines it (sequentially awaited)
|
|
420
|
+
const SpawnClass = featureConfig.spawn;
|
|
421
|
+
if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
|
|
422
|
+
Object.hasOwn(SpawnClass, 'onAssigned') &&
|
|
423
|
+
typeof SpawnClass.onAssigned === 'function') {
|
|
424
|
+
const result = SpawnClass.onAssigned(ctr, featureConfig, key);
|
|
425
|
+
if (result && typeof result.then === 'function') {
|
|
426
|
+
hasAsync = true;
|
|
427
|
+
await result;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
|
|
432
|
+
const featuresConfig = ctr.featuresConfig;
|
|
433
|
+
if (featuresConfig?.lifecycleKeys) {
|
|
434
|
+
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
435
|
+
if (methodName) {
|
|
436
|
+
installWhenFeatureReadyMethod(ctr, methodName);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Check if any feature has an async onAssigned (pre-scan)
|
|
441
|
+
for (const key of Object.keys(features)) {
|
|
442
|
+
const featureConfig = features[key];
|
|
443
|
+
const SpawnClass = featureConfig.spawn;
|
|
444
|
+
if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
|
|
445
|
+
Object.hasOwn(SpawnClass, 'onAssigned') &&
|
|
446
|
+
typeof SpawnClass.onAssigned === 'function') {
|
|
447
|
+
// We can't know if it's async without calling it, so always use the async path
|
|
448
|
+
// if any onAssigned exists
|
|
449
|
+
return processFeatures();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// No onAssigned hooks — run synchronously (inline the logic to avoid the async wrapper)
|
|
390
453
|
for (const key of Object.keys(features)) {
|
|
391
|
-
// 1. Confirm the key is opted-in via supportedFeatures
|
|
392
454
|
if (!(key in supportedFeatures)) {
|
|
393
455
|
throw new Error(`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`);
|
|
394
456
|
}
|
|
395
|
-
// 2. Check that the prototype doesn't already have this property defined
|
|
396
457
|
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
397
458
|
if (existingDescriptor) {
|
|
398
459
|
throw new Error(`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`);
|
|
399
460
|
}
|
|
400
|
-
// 3. Check that this key hasn't already been registered for this constructor
|
|
401
461
|
if (featuresRegistry.hasKey(ctr, key)) {
|
|
402
462
|
throw new Error(`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`);
|
|
403
463
|
}
|
|
404
|
-
// 4. Register the injection
|
|
405
464
|
featuresRegistry.set(ctr, key, features[key]);
|
|
406
|
-
// 5. Install the lazy getter on the prototype
|
|
407
465
|
installFeatureGetter(ctr, key, featuresRegistry);
|
|
408
|
-
// 6. Install callback forwarding if configured (merge author + consumer)
|
|
409
466
|
const featureConfig = features[key];
|
|
410
467
|
const optIn = supportedFeatures[key];
|
|
411
468
|
const authorCallbacks = optIn.callbackForwarding || [];
|
|
412
469
|
const consumerCallbacks = featureConfig.callbackForwarding || [];
|
|
413
|
-
// Union of both (author defaults + consumer additions)
|
|
414
470
|
const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
|
|
415
471
|
if (allCallbacks.length > 0) {
|
|
416
472
|
installCallbackForwarding(ctr, key, allCallbacks);
|
|
417
473
|
}
|
|
418
|
-
// 7. Call static onAssigned if the spawn class defines it
|
|
419
|
-
const SpawnClass = featureConfig.spawn;
|
|
420
|
-
if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
|
|
421
|
-
Object.hasOwn(SpawnClass, 'onAssigned') &&
|
|
422
|
-
typeof SpawnClass.onAssigned === 'function') {
|
|
423
|
-
const result = SpawnClass.onAssigned(ctr, featureConfig, key);
|
|
424
|
-
if (result && typeof result.then === 'function') {
|
|
425
|
-
onAssignedPromises.push(result);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
474
|
}
|
|
429
|
-
// 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
|
|
430
475
|
const featuresConfig = ctr.featuresConfig;
|
|
431
476
|
if (featuresConfig?.lifecycleKeys) {
|
|
432
477
|
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
@@ -434,10 +479,6 @@ export function assignFeatures(ctr, features, featuresRegistry) {
|
|
|
434
479
|
installWhenFeatureReadyMethod(ctr, methodName);
|
|
435
480
|
}
|
|
436
481
|
}
|
|
437
|
-
// Return a Promise if any onAssigned hooks are async, otherwise undefined
|
|
438
|
-
if (onAssignedPromises.length > 0) {
|
|
439
|
-
return Promise.all(onAssignedPromises).then(() => { });
|
|
440
|
-
}
|
|
441
482
|
return undefined;
|
|
442
483
|
}
|
|
443
484
|
/**
|
package/assignFeatures.ts
CHANGED
|
@@ -470,17 +470,93 @@ export function assignFeatures(
|
|
|
470
470
|
);
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
|
|
473
|
+
let hasAsync = false;
|
|
474
474
|
|
|
475
|
+
async function processFeatures() {
|
|
476
|
+
for (const key of Object.keys(features)) {
|
|
477
|
+
// 1. Confirm the key is opted-in via supportedFeatures
|
|
478
|
+
if (!(key in supportedFeatures!)) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 2. Check that the prototype doesn't already have this property defined
|
|
485
|
+
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
486
|
+
if (existingDescriptor) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 3. Check that this key hasn't already been registered for this constructor
|
|
493
|
+
if (featuresRegistry.hasKey(ctr, key)) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 4. Register the injection
|
|
500
|
+
featuresRegistry.set(ctr, key, features[key]);
|
|
501
|
+
|
|
502
|
+
// 5. Install the lazy getter on the prototype
|
|
503
|
+
installFeatureGetter(ctr, key, featuresRegistry);
|
|
504
|
+
|
|
505
|
+
// 6. Install callback forwarding if configured (merge author + consumer)
|
|
506
|
+
const featureConfig = features[key];
|
|
507
|
+
const optIn = supportedFeatures![key];
|
|
508
|
+
const authorCallbacks = optIn.callbackForwarding || [];
|
|
509
|
+
const consumerCallbacks = featureConfig.callbackForwarding || [];
|
|
510
|
+
// Union of both (author defaults + consumer additions)
|
|
511
|
+
const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
|
|
512
|
+
if (allCallbacks.length > 0) {
|
|
513
|
+
installCallbackForwarding(ctr, key, allCallbacks);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 7. Call static onAssigned if the spawn class defines it (sequentially awaited)
|
|
517
|
+
const SpawnClass = featureConfig.spawn;
|
|
518
|
+
if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
|
|
519
|
+
Object.hasOwn(SpawnClass as any, 'onAssigned') &&
|
|
520
|
+
typeof (SpawnClass as any).onAssigned === 'function') {
|
|
521
|
+
const result = (SpawnClass as any).onAssigned(ctr, featureConfig, key);
|
|
522
|
+
if (result && typeof result.then === 'function') {
|
|
523
|
+
hasAsync = true;
|
|
524
|
+
await result;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
|
|
530
|
+
const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
|
|
531
|
+
if (featuresConfig?.lifecycleKeys) {
|
|
532
|
+
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
533
|
+
if (methodName) {
|
|
534
|
+
installWhenFeatureReadyMethod(ctr, methodName);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Check if any feature has an async onAssigned (pre-scan)
|
|
540
|
+
for (const key of Object.keys(features)) {
|
|
541
|
+
const featureConfig = features[key];
|
|
542
|
+
const SpawnClass = featureConfig.spawn;
|
|
543
|
+
if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
|
|
544
|
+
Object.hasOwn(SpawnClass as any, 'onAssigned') &&
|
|
545
|
+
typeof (SpawnClass as any).onAssigned === 'function') {
|
|
546
|
+
// We can't know if it's async without calling it, so always use the async path
|
|
547
|
+
// if any onAssigned exists
|
|
548
|
+
return processFeatures();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// No onAssigned hooks — run synchronously (inline the logic to avoid the async wrapper)
|
|
475
553
|
for (const key of Object.keys(features)) {
|
|
476
|
-
// 1. Confirm the key is opted-in via supportedFeatures
|
|
477
554
|
if (!(key in supportedFeatures)) {
|
|
478
555
|
throw new Error(
|
|
479
556
|
`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`
|
|
480
557
|
);
|
|
481
558
|
}
|
|
482
559
|
|
|
483
|
-
// 2. Check that the prototype doesn't already have this property defined
|
|
484
560
|
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
485
561
|
if (existingDescriptor) {
|
|
486
562
|
throw new Error(
|
|
@@ -488,43 +564,25 @@ export function assignFeatures(
|
|
|
488
564
|
);
|
|
489
565
|
}
|
|
490
566
|
|
|
491
|
-
// 3. Check that this key hasn't already been registered for this constructor
|
|
492
567
|
if (featuresRegistry.hasKey(ctr, key)) {
|
|
493
568
|
throw new Error(
|
|
494
569
|
`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`
|
|
495
570
|
);
|
|
496
571
|
}
|
|
497
572
|
|
|
498
|
-
// 4. Register the injection
|
|
499
573
|
featuresRegistry.set(ctr, key, features[key]);
|
|
500
|
-
|
|
501
|
-
// 5. Install the lazy getter on the prototype
|
|
502
574
|
installFeatureGetter(ctr, key, featuresRegistry);
|
|
503
575
|
|
|
504
|
-
// 6. Install callback forwarding if configured (merge author + consumer)
|
|
505
576
|
const featureConfig = features[key];
|
|
506
577
|
const optIn = supportedFeatures[key];
|
|
507
578
|
const authorCallbacks = optIn.callbackForwarding || [];
|
|
508
579
|
const consumerCallbacks = featureConfig.callbackForwarding || [];
|
|
509
|
-
// Union of both (author defaults + consumer additions)
|
|
510
580
|
const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
|
|
511
581
|
if (allCallbacks.length > 0) {
|
|
512
582
|
installCallbackForwarding(ctr, key, allCallbacks);
|
|
513
583
|
}
|
|
514
|
-
|
|
515
|
-
// 7. Call static onAssigned if the spawn class defines it
|
|
516
|
-
const SpawnClass = featureConfig.spawn;
|
|
517
|
-
if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
|
|
518
|
-
Object.hasOwn(SpawnClass as any, 'onAssigned') &&
|
|
519
|
-
typeof (SpawnClass as any).onAssigned === 'function') {
|
|
520
|
-
const result = (SpawnClass as any).onAssigned(ctr, featureConfig, key);
|
|
521
|
-
if (result && typeof result.then === 'function') {
|
|
522
|
-
onAssignedPromises.push(result);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
584
|
}
|
|
526
585
|
|
|
527
|
-
// 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
|
|
528
586
|
const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
|
|
529
587
|
if (featuresConfig?.lifecycleKeys) {
|
|
530
588
|
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
@@ -533,10 +591,6 @@ export function assignFeatures(
|
|
|
533
591
|
}
|
|
534
592
|
}
|
|
535
593
|
|
|
536
|
-
// Return a Promise if any onAssigned hooks are async, otherwise undefined
|
|
537
|
-
if (onAssignedPromises.length > 0) {
|
|
538
|
-
return Promise.all(onAssignedPromises).then(() => {});
|
|
539
|
-
}
|
|
540
594
|
return undefined;
|
|
541
595
|
}
|
|
542
596
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineWithFeatures - Declaratively define a custom element with features from JSON config.
|
|
3
|
+
*
|
|
4
|
+
* Resolves async fallback spawns from the base class's `static supportedFeatures`,
|
|
5
|
+
* creates a subclass, registers features with resolved spawns + JSON config,
|
|
6
|
+
* and defines the custom element.
|
|
7
|
+
*
|
|
8
|
+
* Designed to support cede scripts and other declarative custom element definition patterns.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* await defineWithFeatures('time-ticker', 'el-maker', {
|
|
12
|
+
* assignFeatures: {
|
|
13
|
+
* timeTicker: {},
|
|
14
|
+
* roundabout: {
|
|
15
|
+
* customData: {...},
|
|
16
|
+
* withAttrs: {...},
|
|
17
|
+
* callbackForwarding: ['connectedCallback']
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
import { assignFeatures } from './assignFeatures.js';
|
|
23
|
+
/**
|
|
24
|
+
* Determines if a function is an async spawner (same heuristic as assignFeatures).
|
|
25
|
+
*/
|
|
26
|
+
function isAsyncSpawn(fn) {
|
|
27
|
+
if (typeof fn !== 'function')
|
|
28
|
+
return false;
|
|
29
|
+
if (fn.constructor.name === 'AsyncFunction')
|
|
30
|
+
return true;
|
|
31
|
+
if (fn.prototype === undefined)
|
|
32
|
+
return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Cache for resolved fallback spawns.
|
|
37
|
+
* Key: BaseClass, Value: Map<featureKey, resolvedConstructor>
|
|
38
|
+
*/
|
|
39
|
+
const resolvedSpawnCache = new WeakMap();
|
|
40
|
+
/**
|
|
41
|
+
* Declaratively define a custom element with features.
|
|
42
|
+
*
|
|
43
|
+
* 1. Waits for the base class to be defined (if not already).
|
|
44
|
+
* 2. Resolves all async fallback spawns from `static supportedFeatures`.
|
|
45
|
+
* 3. Creates a subclass extending the base class.
|
|
46
|
+
* 4. Calls `assignFeatures` with resolved spawns + the JSON config.
|
|
47
|
+
* 5. Defines the new custom element in the registry.
|
|
48
|
+
*
|
|
49
|
+
* @param tagName - The custom element tag name to define (e.g., 'time-ticker')
|
|
50
|
+
* @param baseTagName - The tag name of the base class to extend (e.g., 'el-maker')
|
|
51
|
+
* @param config - JSON-serializable configuration specifying which features to activate
|
|
52
|
+
* @param registry - Optional custom element registry (defaults to global `customElements`)
|
|
53
|
+
* @returns The newly created and defined custom element class
|
|
54
|
+
*/
|
|
55
|
+
export async function defineWithFeatures(tagName, baseTagName, config, registry) {
|
|
56
|
+
const reg = registry || customElements;
|
|
57
|
+
// 1. Resolve base class — wait for it if not yet defined
|
|
58
|
+
let BaseClass = reg.get(baseTagName);
|
|
59
|
+
if (!BaseClass) {
|
|
60
|
+
await reg.whenDefined(baseTagName);
|
|
61
|
+
BaseClass = reg.get(baseTagName);
|
|
62
|
+
}
|
|
63
|
+
if (!BaseClass) {
|
|
64
|
+
throw new Error(`defineWithFeatures: base class "${baseTagName}" could not be resolved`);
|
|
65
|
+
}
|
|
66
|
+
const supportedFeatures = BaseClass.supportedFeatures;
|
|
67
|
+
if (!supportedFeatures) {
|
|
68
|
+
throw new Error(`defineWithFeatures: "${baseTagName}" does not define static supportedFeatures`);
|
|
69
|
+
}
|
|
70
|
+
// 2. Resolve all async fallback spawns (with caching)
|
|
71
|
+
let classCache = resolvedSpawnCache.get(BaseClass);
|
|
72
|
+
if (!classCache) {
|
|
73
|
+
classCache = new Map();
|
|
74
|
+
resolvedSpawnCache.set(BaseClass, classCache);
|
|
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
|
+
}));
|
|
99
|
+
// 3. Create subclass
|
|
100
|
+
const NewClass = class extends BaseClass {
|
|
101
|
+
};
|
|
102
|
+
// 4. Build FeatureConfigsMap: resolved spawns + JSON config
|
|
103
|
+
const featuresMap = {};
|
|
104
|
+
for (const [key, jsonConfig] of Object.entries(config.assignFeatures)) {
|
|
105
|
+
featuresMap[key] = {
|
|
106
|
+
spawn: resolvedSpawns.get(key),
|
|
107
|
+
...jsonConfig
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// 5. assignFeatures (sequential onAssigned) + define
|
|
111
|
+
await assignFeatures(NewClass, featuresMap, reg.featuresRegistry);
|
|
112
|
+
reg.define(tagName, NewClass);
|
|
113
|
+
return NewClass;
|
|
114
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineWithFeatures - Declaratively define a custom element with features from JSON config.
|
|
3
|
+
*
|
|
4
|
+
* Resolves async fallback spawns from the base class's `static supportedFeatures`,
|
|
5
|
+
* creates a subclass, registers features with resolved spawns + JSON config,
|
|
6
|
+
* and defines the custom element.
|
|
7
|
+
*
|
|
8
|
+
* Designed to support cede scripts and other declarative custom element definition patterns.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* await defineWithFeatures('time-ticker', 'el-maker', {
|
|
12
|
+
* assignFeatures: {
|
|
13
|
+
* timeTicker: {},
|
|
14
|
+
* roundabout: {
|
|
15
|
+
* customData: {...},
|
|
16
|
+
* withAttrs: {...},
|
|
17
|
+
* callbackForwarding: ['connectedCallback']
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { assignFeatures, FeatureConfigsMap, SupportedFeaturesMap } from './assignFeatures.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration passed to defineWithFeatures (JSON-serializable).
|
|
27
|
+
*/
|
|
28
|
+
export interface DefineWithFeaturesConfig {
|
|
29
|
+
assignFeatures: Record<string, {
|
|
30
|
+
customData?: any;
|
|
31
|
+
withAttrs?: any;
|
|
32
|
+
callbackForwarding?: string[];
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determines if a function is an async spawner (same heuristic as assignFeatures).
|
|
38
|
+
*/
|
|
39
|
+
function isAsyncSpawn(fn: any): boolean {
|
|
40
|
+
if (typeof fn !== 'function') return false;
|
|
41
|
+
if (fn.constructor.name === 'AsyncFunction') return true;
|
|
42
|
+
if (fn.prototype === undefined) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Cache for resolved fallback spawns.
|
|
48
|
+
* Key: BaseClass, Value: Map<featureKey, resolvedConstructor>
|
|
49
|
+
*/
|
|
50
|
+
const resolvedSpawnCache = new WeakMap<Function, Map<string, any>>();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Declaratively define a custom element with features.
|
|
54
|
+
*
|
|
55
|
+
* 1. Waits for the base class to be defined (if not already).
|
|
56
|
+
* 2. Resolves all async fallback spawns from `static supportedFeatures`.
|
|
57
|
+
* 3. Creates a subclass extending the base class.
|
|
58
|
+
* 4. Calls `assignFeatures` with resolved spawns + the JSON config.
|
|
59
|
+
* 5. Defines the new custom element in the registry.
|
|
60
|
+
*
|
|
61
|
+
* @param tagName - The custom element tag name to define (e.g., 'time-ticker')
|
|
62
|
+
* @param baseTagName - The tag name of the base class to extend (e.g., 'el-maker')
|
|
63
|
+
* @param config - JSON-serializable configuration specifying which features to activate
|
|
64
|
+
* @param registry - Optional custom element registry (defaults to global `customElements`)
|
|
65
|
+
* @returns The newly created and defined custom element class
|
|
66
|
+
*/
|
|
67
|
+
export async function defineWithFeatures(
|
|
68
|
+
tagName: string,
|
|
69
|
+
baseTagName: string,
|
|
70
|
+
config: DefineWithFeaturesConfig,
|
|
71
|
+
registry?: CustomElementRegistry
|
|
72
|
+
): Promise<Function> {
|
|
73
|
+
const reg = registry || customElements;
|
|
74
|
+
|
|
75
|
+
// 1. Resolve base class — wait for it if not yet defined
|
|
76
|
+
let BaseClass = (reg as any).get(baseTagName);
|
|
77
|
+
if (!BaseClass) {
|
|
78
|
+
await (reg as any).whenDefined(baseTagName);
|
|
79
|
+
BaseClass = (reg as any).get(baseTagName);
|
|
80
|
+
}
|
|
81
|
+
if (!BaseClass) {
|
|
82
|
+
throw new Error(`defineWithFeatures: base class "${baseTagName}" could not be resolved`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const supportedFeatures: SupportedFeaturesMap | undefined = BaseClass.supportedFeatures;
|
|
86
|
+
if (!supportedFeatures) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`defineWithFeatures: "${baseTagName}" does not define static supportedFeatures`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Resolve all async fallback spawns (with caching)
|
|
93
|
+
let classCache = resolvedSpawnCache.get(BaseClass);
|
|
94
|
+
if (!classCache) {
|
|
95
|
+
classCache = new Map();
|
|
96
|
+
resolvedSpawnCache.set(BaseClass, classCache);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const featureKeys = Object.keys(config.assignFeatures);
|
|
100
|
+
const resolvedSpawns = new Map<string, any>();
|
|
101
|
+
|
|
102
|
+
await Promise.all(featureKeys.map(async (key) => {
|
|
103
|
+
const optIn = supportedFeatures[key];
|
|
104
|
+
if (!optIn) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`defineWithFeatures: feature "${key}" not found in ${baseTagName}.supportedFeatures`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check cache first
|
|
111
|
+
if (classCache!.has(key)) {
|
|
112
|
+
resolvedSpawns.set(key, classCache!.get(key));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let spawn = optIn.fallbackSpawn;
|
|
117
|
+
if (spawn && isAsyncSpawn(spawn)) {
|
|
118
|
+
// Resolve the async spawner
|
|
119
|
+
spawn = await (spawn as () => Promise<any>)();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Cache the resolved spawn
|
|
123
|
+
if (spawn) {
|
|
124
|
+
classCache!.set(key, spawn);
|
|
125
|
+
}
|
|
126
|
+
resolvedSpawns.set(key, spawn);
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
// 3. Create subclass
|
|
130
|
+
const NewClass = class extends (BaseClass as any) {};
|
|
131
|
+
|
|
132
|
+
// 4. Build FeatureConfigsMap: resolved spawns + JSON config
|
|
133
|
+
const featuresMap: FeatureConfigsMap = {};
|
|
134
|
+
for (const [key, jsonConfig] of Object.entries(config.assignFeatures)) {
|
|
135
|
+
featuresMap[key] = {
|
|
136
|
+
spawn: resolvedSpawns.get(key),
|
|
137
|
+
...jsonConfig
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 5. assignFeatures (sequential onAssigned) + define
|
|
142
|
+
await assignFeatures(NewClass, featuresMap, (reg as any).featuresRegistry);
|
|
143
|
+
(reg as any).define(tagName, NewClass);
|
|
144
|
+
|
|
145
|
+
return NewClass;
|
|
146
|
+
}
|
package/index.js
CHANGED
|
@@ -11,4 +11,5 @@ export { resolveValues, resolveValue } from './resolveValues.js';
|
|
|
11
11
|
export { assignFrom } from './assignFrom.js';
|
|
12
12
|
export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions } from './assignFeatures.js';
|
|
13
13
|
export { installForwarding } from './installForwarding.js';
|
|
14
|
+
export { defineWithFeatures } from './defineWithFeatures.js';
|
|
14
15
|
import './object-extension.js';
|
package/index.ts
CHANGED
|
@@ -11,4 +11,5 @@ export {resolveValues, resolveValue} from './resolveValues.js';
|
|
|
11
11
|
export {assignFrom} from './assignFrom.js';
|
|
12
12
|
export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions} from './assignFeatures.js';
|
|
13
13
|
export {installForwarding} from './installForwarding.js';
|
|
14
|
+
export {defineWithFeatures} from './defineWithFeatures.js';
|
|
14
15
|
import './object-extension.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
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": {
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
"default": "./installForwarding.js",
|
|
65
65
|
"types": "./installForwarding.ts"
|
|
66
66
|
},
|
|
67
|
+
"./defineWithFeatures.js": {
|
|
68
|
+
"default": "./defineWithFeatures.js",
|
|
69
|
+
"types": "./defineWithFeatures.ts"
|
|
70
|
+
},
|
|
67
71
|
"./assignFrom.js": {
|
|
68
72
|
"default": "./assignFrom.js",
|
|
69
73
|
"types": "./assignFrom.ts"
|