assign-gingerly 0.0.41 → 0.0.42

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 CHANGED
@@ -4723,6 +4723,62 @@ customElements.assignFeatures(MyElement, {
4723
4723
 
4724
4724
  **Note on `attributeChangedCallback`:** The feature only receives callbacks for attributes listed in the element's `static observedAttributes`. Features cannot add to this list after `define()` is called.
4725
4725
 
4726
+ ### Class-level setup with `static onAssigned`
4727
+
4728
+ Some features need one-time class-level setup before any instances are created — for example, installing prototype getter/setters or pre-loading modules. The `static onAssigned` method on the spawn class is called by `assignFeatures` immediately after registration:
4729
+
4730
+ ```JavaScript
4731
+ class RoundaboutFeature {
4732
+ // Called once when assignFeatures processes this feature
4733
+ static async onAssigned(ctr, featureConfig) {
4734
+ // One-time class-level setup: install prototype getter/setters, pre-load modules
4735
+ await makeRoundaboutReady(ctr, featureConfig.customData);
4736
+ }
4737
+
4738
+ constructor(host, ctx, initVals) {
4739
+ // Instance-level setup (runs on first getter access)
4740
+ const [vm, propagator] = roundaboutSync({
4741
+ vm: host,
4742
+ ...ctx.injection.customData,
4743
+ });
4744
+ this._vm = vm;
4745
+ this._propagator = propagator;
4746
+ }
4747
+ }
4748
+ ```
4749
+
4750
+ **Usage:**
4751
+
4752
+ ```JavaScript
4753
+ // await is safe — returns undefined if no async onAssigned hooks exist
4754
+ await customElements.assignFeatures(MyElement, {
4755
+ roundabout: {
4756
+ spawn: RoundaboutFeature,
4757
+ customData: raConfig
4758
+ }
4759
+ });
4760
+
4761
+ // Now define — class is fully set up, connectedCallback will be synchronous
4762
+ customElements.define('my-element', MyElement);
4763
+ ```
4764
+
4765
+ **How it works:**
4766
+
4767
+ - `assignFeatures` checks if the spawn class defines `static onAssigned` (via `Object.hasOwn`).
4768
+ - If found, calls `SpawnClass.onAssigned(ctr, featureConfig)` after installing the getter.
4769
+ - If `onAssigned` returns a Promise, `assignFeatures` returns a `Promise<void>` that resolves when all async hooks complete.
4770
+ - If no `onAssigned` hooks are async (or none exist), `assignFeatures` returns `undefined` (backward compatible — existing code that doesn't `await` still works).
4771
+ - 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.
4772
+
4773
+ **`await` is always safe:**
4774
+
4775
+ ```JavaScript
4776
+ // These are equivalent for sync features (no onAssigned or sync onAssigned):
4777
+ customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
4778
+ await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
4779
+ // Both work — await on undefined is a no-op
4780
+ ```
4781
+
4726
4782
  ### Roadmap (future phases)
4727
4783
 
4728
4784
  - **Nested features**: Support `?.path?.notation` keys directly in `assignFeatures` (without requiring `PropertyBag`).
package/assignFeatures.js CHANGED
@@ -383,6 +383,7 @@ export function assignFeatures(ctr, features, featuresRegistry) {
383
383
  if (!supportedFeatures) {
384
384
  throw new Error(`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`);
385
385
  }
386
+ const onAssignedPromises = [];
386
387
  for (const key of Object.keys(features)) {
387
388
  // 1. Confirm the key is opted-in via supportedFeatures
388
389
  if (!(key in supportedFeatures)) {
@@ -411,8 +412,18 @@ export function assignFeatures(ctr, features, featuresRegistry) {
411
412
  if (allCallbacks.length > 0) {
412
413
  installCallbackForwarding(ctr, key, allCallbacks);
413
414
  }
415
+ // 7. Call static onAssigned if the spawn class defines it
416
+ const SpawnClass = featureConfig.spawn;
417
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
418
+ Object.hasOwn(SpawnClass, 'onAssigned') &&
419
+ typeof SpawnClass.onAssigned === 'function') {
420
+ const result = SpawnClass.onAssigned(ctr, featureConfig);
421
+ if (result && typeof result.then === 'function') {
422
+ onAssignedPromises.push(result);
423
+ }
424
+ }
414
425
  }
415
- // 6. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
426
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
416
427
  const featuresConfig = ctr.featuresConfig;
417
428
  if (featuresConfig?.lifecycleKeys) {
418
429
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -420,6 +431,11 @@ export function assignFeatures(ctr, features, featuresRegistry) {
420
431
  installWhenFeatureReadyMethod(ctr, methodName);
421
432
  }
422
433
  }
434
+ // Return a Promise if any onAssigned hooks are async, otherwise undefined
435
+ if (onAssignedPromises.length > 0) {
436
+ return Promise.all(onAssignedPromises).then(() => { });
437
+ }
438
+ return undefined;
423
439
  }
424
440
  /**
425
441
  * Captures own-properties that shadow feature getters and stores them as initVals.
@@ -525,7 +541,7 @@ if (typeof CustomElementRegistry !== 'undefined') {
525
541
  });
526
542
  Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
527
543
  value: function (ctr, features) {
528
- assignFeatures(ctr, features, this.featuresRegistry);
544
+ return assignFeatures(ctr, features, this.featuresRegistry);
529
545
  },
530
546
  writable: true,
531
547
  enumerable: false,
package/assignFeatures.ts CHANGED
@@ -607,7 +607,7 @@ export function assignFeatures(
607
607
  ctr: Function,
608
608
  features: FeatureConfigsMap,
609
609
  featuresRegistry: FeaturesRegistry
610
- ): void {
610
+ ): Promise<void> | undefined {
611
611
  // Validate that the constructor has static supportedFeatures
612
612
  const supportedFeatures: SupportedFeaturesMap | undefined = (ctr as any).supportedFeatures;
613
613
 
@@ -617,6 +617,8 @@ export function assignFeatures(
617
617
  );
618
618
  }
619
619
 
620
+ const onAssignedPromises: Promise<void>[] = [];
621
+
620
622
  for (const key of Object.keys(features)) {
621
623
  // 1. Confirm the key is opted-in via supportedFeatures
622
624
  if (!(key in supportedFeatures)) {
@@ -656,9 +658,20 @@ export function assignFeatures(
656
658
  if (allCallbacks.length > 0) {
657
659
  installCallbackForwarding(ctr, key, allCallbacks);
658
660
  }
661
+
662
+ // 7. Call static onAssigned if the spawn class defines it
663
+ const SpawnClass = featureConfig.spawn;
664
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
665
+ Object.hasOwn(SpawnClass as any, 'onAssigned') &&
666
+ typeof (SpawnClass as any).onAssigned === 'function') {
667
+ const result = (SpawnClass as any).onAssigned(ctr, featureConfig);
668
+ if (result && typeof result.then === 'function') {
669
+ onAssignedPromises.push(result);
670
+ }
671
+ }
659
672
  }
660
673
 
661
- // 6. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
674
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
662
675
  const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
663
676
  if (featuresConfig?.lifecycleKeys) {
664
677
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -666,6 +679,12 @@ export function assignFeatures(
666
679
  installWhenFeatureReadyMethod(ctr, methodName);
667
680
  }
668
681
  }
682
+
683
+ // Return a Promise if any onAssigned hooks are async, otherwise undefined
684
+ if (onAssignedPromises.length > 0) {
685
+ return Promise.all(onAssignedPromises).then(() => {});
686
+ }
687
+ return undefined;
669
688
  }
670
689
 
671
690
  /**
@@ -772,7 +791,7 @@ export class PropertyBag {
772
791
  declare global {
773
792
  interface CustomElementRegistry {
774
793
  featuresRegistry: FeaturesRegistry;
775
- assignFeatures(ctr: Function, features: FeatureConfigsMap): void;
794
+ assignFeatures(ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined;
776
795
  }
777
796
  }
778
797
 
@@ -793,8 +812,8 @@ if (typeof CustomElementRegistry !== 'undefined') {
793
812
  });
794
813
 
795
814
  Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
796
- value: function (ctr: Function, features: FeatureConfigsMap): void {
797
- assignFeatures(ctr, features, this.featuresRegistry);
815
+ value: function (ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined {
816
+ return assignFeatures(ctr, features, this.featuresRegistry);
798
817
  },
799
818
  writable: true,
800
819
  enumerable: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
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": {
@@ -459,4 +459,4 @@ export declare function assignFeatures(
459
459
  ctr: Function,
460
460
  features: FeatureConfigsMap,
461
461
  featuresRegistry: FeaturesRegistry
462
- ): void;
462
+ ): Promise<void> | undefined;