assign-gingerly 0.0.48 → 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 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, `assignFeatures` returns a `Promise<void>` that resolves when all async hooks complete.
4797
- - If no `onAssigned` hooks are async (or none exist), `assignFeatures` returns `undefined` (backward compatible existing code that doesn't `await` still works).
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
 
@@ -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.48",
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"