neo.mjs 10.0.0-beta.4 → 10.0.0-beta.5

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 (46) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +2 -2
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/portal/index.html +1 -1
  4. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  5. package/examples/button/effect/MainContainer.mjs +207 -0
  6. package/examples/button/effect/app.mjs +6 -0
  7. package/examples/button/effect/index.html +11 -0
  8. package/examples/button/effect/neo-config.json +6 -0
  9. package/learn/guides/datahandling/StateProviders.md +1 -0
  10. package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
  11. package/learn/tree.json +1 -0
  12. package/package.json +2 -2
  13. package/src/DefaultConfig.mjs +2 -2
  14. package/src/Neo.mjs +226 -78
  15. package/src/button/Effect.mjs +435 -0
  16. package/src/collection/Base.mjs +7 -2
  17. package/src/component/Base.mjs +67 -46
  18. package/src/container/Base.mjs +28 -24
  19. package/src/core/Base.mjs +138 -19
  20. package/src/core/Config.mjs +123 -32
  21. package/src/core/Effect.mjs +127 -0
  22. package/src/core/EffectBatchManager.mjs +68 -0
  23. package/src/core/EffectManager.mjs +38 -0
  24. package/src/grid/Container.mjs +8 -4
  25. package/src/grid/column/Component.mjs +1 -1
  26. package/src/state/Provider.mjs +343 -452
  27. package/src/state/createHierarchicalDataProxy.mjs +124 -0
  28. package/src/tab/header/EffectButton.mjs +75 -0
  29. package/src/vdom/Helper.mjs +9 -10
  30. package/src/vdom/VNode.mjs +1 -1
  31. package/src/worker/App.mjs +0 -5
  32. package/test/siesta/siesta.js +32 -0
  33. package/test/siesta/tests/CollectionBase.mjs +10 -10
  34. package/test/siesta/tests/VdomHelper.mjs +22 -59
  35. package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
  36. package/test/siesta/tests/{ReactiveConfigs.mjs → config/Basic.mjs} +58 -21
  37. package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
  38. package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
  39. package/test/siesta/tests/config/Hierarchy.mjs +94 -0
  40. package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
  41. package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
  42. package/test/siesta/tests/core/Effect.mjs +131 -0
  43. package/test/siesta/tests/core/EffectBatching.mjs +322 -0
  44. package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
  45. package/test/siesta/tests/state/Provider.mjs +537 -0
  46. package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
package/src/Neo.mjs CHANGED
@@ -1,18 +1,46 @@
1
- import DefaultConfig from './DefaultConfig.mjs';
2
- import {isDescriptor} from './core/ConfigSymbols.mjs';
1
+ import DefaultConfig from './DefaultConfig.mjs';
2
+ import {isDescriptor} from './core/ConfigSymbols.mjs';
3
3
 
4
4
  const
5
5
  camelRegex = /-./g,
6
6
  configSymbol = Symbol.for('configSymbol'),
7
7
  getSetCache = Symbol('getSetCache'),
8
+ cloneMap = {
9
+ Array(obj, deep, ignoreNeoInstances) {
10
+ return !deep ? [...obj] : [...obj.map(val => Neo.clone(val, deep, ignoreNeoInstances))]
11
+ },
12
+ Date(obj) {
13
+ return new Date(obj.valueOf())
14
+ },
15
+ Map(obj) {
16
+ return new Map(obj) // shallow copy
17
+ },
18
+ NeoInstance(obj, ignoreNeoInstances) {
19
+ return ignoreNeoInstances ? obj : Neo.cloneNeoInstance(obj)
20
+ },
21
+ Set(obj) {
22
+ return new Set(obj)
23
+ },
24
+ Object(obj, deep, ignoreNeoInstances) {
25
+ const out = {};
26
+
27
+ // Use Reflect.ownKeys() to include symbol properties (e.g., for config descriptors)
28
+ Reflect.ownKeys(obj).forEach(key => {
29
+ const value = obj[key];
30
+ out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
31
+ });
32
+
33
+ return out
34
+ }
35
+ },
8
36
  typeDetector = {
9
37
  function: item => {
10
- if (item.prototype?.constructor.isClass) {
38
+ if (item.prototype?.constructor?.isClass) {
11
39
  return 'NeoClass'
12
40
  }
13
41
  },
14
42
  object: item => {
15
- if (item.constructor.isClass && item instanceof Neo.core.Base) {
43
+ if (item.constructor?.isClass && item instanceof Neo.core.Base) {
16
44
  return 'NeoInstance'
17
45
  }
18
46
  }
@@ -23,7 +51,6 @@ const
23
51
  * @module Neo
24
52
  * @singleton
25
53
  * @borrows Neo.core.Util.bindMethods as bindMethods
26
- * @borrows Neo.core.Util.capitalize as capitalize
27
54
  * @borrows Neo.core.Util.createStyleObject as createStyleObject
28
55
  * @borrows Neo.core.Util.createStyles as createStyles
29
56
  * @borrows Neo.core.Util.decamel as decamel
@@ -178,25 +205,7 @@ Neo = globalThis.Neo = Object.assign({
178
205
  * @returns {Object|Array|*} the cloned input
179
206
  */
180
207
  clone(obj, deep=false, ignoreNeoInstances=false) {
181
- let out;
182
-
183
- return {
184
- Array : () => !deep ? [...obj] : [...obj.map(val => Neo.clone(val, deep, ignoreNeoInstances))],
185
- Date : () => new Date(obj.valueOf()),
186
- Map : () => new Map(obj), // shallow copy
187
- NeoInstance: () => ignoreNeoInstances ? obj : this.cloneNeoInstance(obj),
188
- Set : () => new Set(obj),
189
-
190
- Object: () => {
191
- out = {};
192
-
193
- Object.entries(obj).forEach(([key, value]) => {
194
- out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
195
- });
196
-
197
- return out
198
- }
199
- }[Neo.typeOf(obj)]?.() || obj
208
+ return cloneMap[Neo.typeOf(obj)]?.(obj, deep, ignoreNeoInstances) || obj
200
209
  },
201
210
 
202
211
  /**
@@ -264,7 +273,7 @@ Neo = globalThis.Neo = Object.assign({
264
273
  return null
265
274
  }
266
275
 
267
- className = config.className || config.module.prototype.className;
276
+ className = config.className || config.module.prototype.className
268
277
  }
269
278
 
270
279
  if (!exists(className)) {
@@ -325,6 +334,10 @@ Neo = globalThis.Neo = Object.assign({
325
334
  return Neo.merge(Neo.merge(target, defaults), source)
326
335
  }
327
336
 
337
+ if (!target) {
338
+ return source
339
+ }
340
+
328
341
  for (const key in source) {
329
342
  const value = source[key];
330
343
 
@@ -338,6 +351,35 @@ Neo = globalThis.Neo = Object.assign({
338
351
  return target
339
352
  },
340
353
 
354
+ /**
355
+ * Merges a new value into an existing config value based on a specified strategy.
356
+ * This method is used during instance creation to apply merge strategies defined in config descriptors.
357
+ * @param {any} defaultValue - The default value of the config (from static config).
358
+ * @param {any} instanceValue - The value provided during instance creation.
359
+ * @param {string|Function} strategy - The merge strategy: 'shallow', 'deep', 'replace', or a custom function.
360
+ * @returns {any} The merged value.
361
+ */
362
+ mergeConfig(defaultValue, instanceValue, strategy) {
363
+ const
364
+ defaultValueType = Neo.typeOf(defaultValue),
365
+ instanceValueType = Neo.typeOf(instanceValue);
366
+
367
+ if (strategy === 'shallow') {
368
+ if (defaultValueType === 'Object' && instanceValueType === 'Object') {
369
+ return {...defaultValue, ...instanceValue}
370
+ }
371
+ } else if (strategy === 'deep') {
372
+ if (defaultValueType === 'Object' && instanceValueType === 'Object') {
373
+ return Neo.merge(Neo.clone(defaultValue, true), instanceValue)
374
+ }
375
+ } else if (typeof strategy === 'function') {
376
+ return strategy(defaultValue, instanceValue)
377
+ }
378
+
379
+ // Default to 'replace' or if strategy is not recognized
380
+ return instanceValue
381
+ },
382
+
341
383
  /**
342
384
  * Maps a className string into a given or global namespace
343
385
  * @example
@@ -461,13 +503,14 @@ Neo = globalThis.Neo = Object.assign({
461
503
  * @returns {T}
462
504
  */
463
505
  setupClass(cls) {
464
- let baseCfg = null,
465
- ntypeChain = [],
466
- {ntypeMap} = Neo,
467
- proto = cls.prototype || cls,
468
- ns = Neo.ns(proto.constructor.config.className, false),
469
- protos = [],
470
- cfg, config, ctor, hierarchyInfo, ntype;
506
+ let baseConfig = null,
507
+ baseConfigDescriptors = null,
508
+ ntypeChain = [],
509
+ {ntypeMap} = Neo,
510
+ proto = cls.prototype || cls,
511
+ ns = Neo.ns(proto.constructor.config.className, false),
512
+ protos = [],
513
+ cfg, config, configDescriptors, ctor, hierarchyInfo, ntype;
471
514
 
472
515
  /*
473
516
  * If the namespace already exists, directly return it.
@@ -481,12 +524,16 @@ Neo = globalThis.Neo = Object.assign({
481
524
  return ns
482
525
  }
483
526
 
527
+ // Traverse the prototype chain to collect inherited configs and descriptors
484
528
  while (proto.__proto__) {
485
529
  ctor = proto.constructor;
486
530
 
531
+ // If a class in the prototype chain has already had its config applied,
532
+ // we can use its pre-processed config and descriptors as a base.
487
533
  if (Object.hasOwn(ctor, 'classConfigApplied')) {
488
- baseCfg = Neo.clone(ctor.config, true);
489
- ntypeChain = [...ctor.ntypeChain];
534
+ baseConfig = Neo.clone(ctor.config, true);
535
+ baseConfigDescriptors = Neo.clone(ctor.configDescriptors, true);
536
+ ntypeChain = [...ctor.ntypeChain];
490
537
  break
491
538
  }
492
539
 
@@ -494,29 +541,43 @@ Neo = globalThis.Neo = Object.assign({
494
541
  proto = proto.__proto__
495
542
  }
496
543
 
497
- config = baseCfg || {};
544
+ // Initialize accumulated config and descriptors
545
+ config = baseConfig || {};
546
+ configDescriptors = baseConfigDescriptors || {};
498
547
 
548
+ // Process each class in the prototype chain (from top to bottom)
499
549
  protos.forEach(element => {
500
550
  let mixins;
501
551
 
502
552
  ctor = element.constructor;
503
-
504
- cfg = ctor.config || {};
553
+ cfg = ctor.config || {};
505
554
 
506
555
  if (Neo.overwrites) {
507
556
  ctor.applyOverwrites?.(cfg)
508
557
  }
509
558
 
559
+ // Process each config property defined in the current class's static config
510
560
  Object.entries(cfg).forEach(([key, value]) => {
511
- if (key.slice(-1) === '_') {
512
- delete cfg[key];
513
- key = key.slice(0, -1);
514
- cfg[key] = value;
515
- autoGenerateGetSet(element, key)
561
+ const
562
+ isReactive = key.slice(-1) === '_',
563
+ baseKey = isReactive ? key.slice(0, -1) : key;
564
+
565
+ // 1. Handle descriptors: If the value is a descriptor object, store it.
566
+ // The 'value' property of the descriptor is then used as the actual config value.
567
+ if (Neo.isObject(value) && value[isDescriptor] === true) {
568
+ ctor.configDescriptors ??= {};
569
+ ctor.configDescriptors[baseKey] = Neo.clone(value, true); // Deep clone to prevent mutation
570
+ value = value.value // Use the descriptor's value as the config value
516
571
  }
517
572
 
518
- // Only apply properties which have no setters inside the prototype chain.
519
- // Those will get applied on create (Neo.core.Base -> initConfig)
573
+ // 2. Handle reactive vs. non-reactive configs: Generate getters/setters for reactive configs.
574
+ if (isReactive) {
575
+ delete cfg[key]; // Remove original key with underscore
576
+ cfg[baseKey] = value; // Use the potentially modified value
577
+ autoGenerateGetSet(element, baseKey)
578
+ }
579
+ // This part handles non-reactive configs (including those that were descriptors)
580
+ // If no property setter exists, define it directly on the prototype.
520
581
  else if (!Neo.hasPropertySetter(element, key)) {
521
582
  Object.defineProperty(element, key, {
522
583
  enumerable: true,
@@ -526,6 +587,17 @@ Neo = globalThis.Neo = Object.assign({
526
587
  }
527
588
  });
528
589
 
590
+ // Merge configDescriptors: Apply "first-defined wins" strategy.
591
+ // If a descriptor for a key already exists (from a parent class), it is not overwritten.
592
+ if (ctor.configDescriptors) {
593
+ for (const key in ctor.configDescriptors) {
594
+ if (!Object.hasOwn(configDescriptors, key)) {
595
+ configDescriptors[key] = Neo.clone(ctor.configDescriptors[key], true) // Deep clone for immutability
596
+ }
597
+ }
598
+ }
599
+
600
+ // Process ntype and ntypeChain
529
601
  if (Object.hasOwn(cfg, 'ntype')) {
530
602
  ntype = cfg.ntype;
531
603
 
@@ -540,6 +612,7 @@ Neo = globalThis.Neo = Object.assign({
540
612
  ntypeMap[ntype] = cfg.className
541
613
  }
542
614
 
615
+ // Process mixins
543
616
  mixins = Object.hasOwn(config, 'mixins') && config.mixins || [];
544
617
 
545
618
  if (ctor.observable) {
@@ -551,7 +624,7 @@ Neo = globalThis.Neo = Object.assign({
551
624
  }
552
625
 
553
626
  if (mixins.length > 0) {
554
- applyMixins(ctor, mixins);
627
+ applyMixins(ctor, mixins, cfg);
555
628
 
556
629
  if (Neo.ns('Neo.core.Observable', false, ctor.prototype.mixins)) {
557
630
  ctor.observable = true
@@ -561,29 +634,45 @@ Neo = globalThis.Neo = Object.assign({
561
634
  delete cfg.mixins;
562
635
  delete config.mixins;
563
636
 
564
- Object.assign(config, cfg);
637
+ // Hierarchical merging of static config values based on descriptors.
638
+ // This ensures that values are merged (e.g., shallow/deep) instead of simply overwritten.
639
+ Object.entries(cfg).forEach(([key, value]) => {
640
+ const descriptor = configDescriptors[key];
565
641
 
642
+ if (descriptor?.merge) {
643
+ config[key] = Neo.mergeConfig(config[key], value, descriptor.merge)
644
+ } else {
645
+ config[key] = value
646
+ }
647
+ });
648
+
649
+ // Assign final processed config and descriptors to the class constructor
566
650
  Object.assign(ctor, {
567
651
  classConfigApplied: true,
568
- config : Neo.clone(config, true),
652
+ config : Neo.clone(config, true), // Deep clone final config for immutability
653
+ configDescriptors : Neo.clone(configDescriptors, true), // Deep clone final descriptors for immutability
569
654
  isClass : true,
570
655
  ntypeChain
571
656
  });
572
657
 
658
+ // Apply to global namespace if not a singleton
573
659
  !config.singleton && this.applyToGlobalNs(cls)
574
660
  });
575
661
 
576
662
  proto = cls.prototype || cls;
577
663
 
664
+ // Add is<Ntype> flags to the prototype
578
665
  ntypeChain.forEach(ntype => {
579
666
  proto[`is${Neo.capitalize(Neo.camel(ntype))}`] = true
580
667
  });
581
668
 
669
+ // If it's a singleton, create and apply the instance to the global namespace
582
670
  if (proto.singleton) {
583
671
  cls = Neo.create(cls);
584
672
  Neo.applyToGlobalNs(cls)
585
673
  }
586
674
 
675
+ // Add class hierarchy information to the manager or a temporary map
587
676
  hierarchyInfo = {
588
677
  className : proto.className,
589
678
  module : cls,
@@ -610,7 +699,7 @@ Neo = globalThis.Neo = Object.assign({
610
699
  return null
611
700
  }
612
701
 
613
- return typeDetector[typeof item]?.(item) || item.constructor.name
702
+ return typeDetector[typeof item]?.(item) || item.constructor?.name
614
703
  }
615
704
  }, Neo);
616
705
 
@@ -624,6 +713,7 @@ const ignoreMixin = [
624
713
  'classConfigApplied',
625
714
  'className',
626
715
  'constructor',
716
+ 'id',
627
717
  'isClass',
628
718
  'mixin',
629
719
  'ntype',
@@ -636,9 +726,10 @@ const ignoreMixin = [
636
726
  /**
637
727
  * @param {Neo.core.Base} cls
638
728
  * @param {Array} mixins
729
+ * @param {Object} classConfig
639
730
  * @private
640
731
  */
641
- function applyMixins(cls, mixins) {
732
+ function applyMixins(cls, mixins, classConfig) {
642
733
  if (!Array.isArray(mixins)) {
643
734
  mixins = [mixins];
644
735
  }
@@ -660,12 +751,12 @@ function applyMixins(cls, mixins) {
660
751
  }
661
752
 
662
753
  mixinCls = Neo.ns(mixin);
663
- mixinProto = mixinCls.prototype;
754
+ mixinProto = mixinCls.prototype
664
755
  }
665
756
 
666
757
  mixinProto.className.split('.').reduce(mixReduce(mixinCls), mixinClasses);
667
758
 
668
- Object.getOwnPropertyNames(mixinProto).forEach(mixinProperty(cls.prototype, mixinProto))
759
+ Object.entries(Object.getOwnPropertyDescriptors(mixinProto)).forEach(mixinProperty(cls.prototype, mixinProto, classConfig))
669
760
  }
670
761
 
671
762
  cls.prototype.mixins = mixinClasses // todo: we should do a deep merge
@@ -695,19 +786,37 @@ function autoGenerateGetSet(proto, key) {
695
786
  }
696
787
 
697
788
  if (!Neo[getSetCache][key]) {
698
- const publicDescriptor = {
789
+ // Public Descriptor
790
+ Neo[getSetCache][key] = {
699
791
  get() {
700
792
  let me = this,
793
+ config = me.getConfig(key),
701
794
  hasNewKey = Object.hasOwn(me[configSymbol], key),
702
795
  newKey = me[configSymbol][key],
703
796
  value = hasNewKey ? newKey : me[_key];
704
797
 
705
- if (Array.isArray(value)) {
706
- if (key !== 'items') {
707
- value = [...value]
798
+ if (value instanceof Date) {
799
+ value = new Date(value.valueOf());
800
+ }
801
+ // new, explicit opt-in path
802
+ else if (config.cloneOnGet) {
803
+ const {cloneOnGet} = config;
804
+
805
+ if (cloneOnGet === 'deep') {
806
+ value = Neo.clone(value, true, true);
807
+ } else if (cloneOnGet === 'shallow') {
808
+ const type = Neo.typeOf(value);
809
+
810
+ if (type === 'Array') {
811
+ value = [...value];
812
+ } else if (type === 'Object') {
813
+ value = {...value};
814
+ }
708
815
  }
709
- } else if (value instanceof Date) {
710
- value = new Date(value.valueOf())
816
+ }
817
+ // legacy behavior
818
+ else if (Array.isArray(value)) {
819
+ value = [...value];
711
820
  }
712
821
 
713
822
  if (hasNewKey) {
@@ -728,45 +837,71 @@ function autoGenerateGetSet(proto, key) {
728
837
  const config = this.getConfig(key);
729
838
  if (!config) return;
730
839
 
731
- let me = this,
732
- oldValue = config.get(); // Get the old value from the Config instance
840
+ let me = this,
841
+ oldValue = config.get(), // Get the old value from the Config instance
842
+ {EffectBatchManager} = Neo.core,
843
+ isNewBatch = !EffectBatchManager?.isBatchActive();
844
+
845
+ // If a config change is not triggered via `core.Base#set()`, honor changes inside hooks.
846
+ isNewBatch && EffectBatchManager?.startBatch();
733
847
 
734
- // every set call has to delete the matching symbol
848
+ // 1. Prevent infinite loops:
849
+ // Immediately remove the pending value from the configSymbol to prevent a getter from
850
+ // recursively re-triggering this setter.
735
851
  delete me[configSymbol][key];
736
852
 
737
- if (key !== 'items' && key !== 'vnode') {
738
- value = Neo.clone(value, true, true)
853
+ switch (config.clone) {
854
+ case 'deep':
855
+ value = Neo.clone(value, true, true);
856
+ break;
857
+ case 'shallow':
858
+ value = Neo.clone(value, false, true);
859
+ break;
739
860
  }
740
861
 
862
+ // 2. Create a temporary state for beforeSet hooks:
863
+ // Set the new value directly on the private backing property. This allows any beforeSet
864
+ // hook to access the new value of this and other configs within the same `set()` call.
865
+ me[_key] = value;
866
+
741
867
  if (typeof me[beforeSet] === 'function') {
742
868
  value = me[beforeSet](value, oldValue);
743
869
 
744
870
  // If they don't return a value, that means no change
745
871
  if (value === undefined) {
872
+ // Restore the original value if the update is canceled.
873
+ me[_key] = oldValue;
874
+ isNewBatch && EffectBatchManager?.endBatch();
746
875
  return
747
876
  }
748
877
  }
749
878
 
750
- // Set the new value into the Config instance
751
- // The config.set() method will return true if the value actually changed.
879
+ // 3. Restore state for change detection:
880
+ // Revert the private backing property to its original value. This is crucial for the
881
+ // `config.set()` method to correctly detect if the value has actually changed.
882
+ me[_key] = oldValue;
883
+
884
+ // 4. Finalize the change:
885
+ // The config.set() method performs the final check and, if the value changed,
886
+ // triggers afterSet hooks and notifies subscribers.
752
887
  if (config.set(value)) {
753
888
  me[afterSet]?.(value, oldValue);
754
889
  me.afterSetConfig?.(key, value, oldValue)
755
890
  }
891
+
892
+ isNewBatch && EffectBatchManager?.endBatch()
756
893
  }
757
894
  };
758
895
 
759
- const privateDescriptor = {
896
+ // Private Descriptor
897
+ Neo[getSetCache][_key] = {
760
898
  get() {
761
- return this.getConfig(key)?.get();
899
+ return this.getConfig(key)?.get()
762
900
  },
763
901
  set(value) {
764
- this.getConfig(key)?.setRaw(value);
902
+ this.getConfig(key)?.setRaw(value)
765
903
  }
766
- };
767
-
768
- Neo[getSetCache][key] = publicDescriptor;
769
- Neo[getSetCache][_key] = privateDescriptor;
904
+ }
770
905
  }
771
906
 
772
907
  Object.defineProperty(proto, key, Neo[getSetCache][key]);
@@ -791,9 +926,7 @@ function createArrayNs(create, current, prev) {
791
926
  arrRoot = prev[arrDetails[0]]
792
927
  }
793
928
 
794
- if (!arrRoot) {
795
- return
796
- }
929
+ if (!arrRoot) return;
797
930
 
798
931
  for (; i < len; i++) {
799
932
  arrItem = parseInt(arrDetails[i]);
@@ -827,12 +960,27 @@ function exists(className) {
827
960
  /**
828
961
  * @param {Neo.core.Base} proto
829
962
  * @param {Neo.core.Base} mixinProto
963
+ * @param {Object} classConfig
830
964
  * @returns {Function}
831
965
  * @private
832
966
  */
833
- function mixinProperty(proto, mixinProto) {
834
- return function(key) {
835
- if (~ignoreMixin.indexOf(key)) {
967
+ function mixinProperty(proto, mixinProto, classConfig) {
968
+ return function([key, descriptor]) {
969
+ if (ignoreMixin.includes(key)) return;
970
+
971
+ // Mixins must not override existing class properties with a setter
972
+ if (Neo.hasPropertySetter(proto, key)) return;
973
+
974
+ // Reactive neo configs, or public class fields defined via get() AND set()
975
+ if (descriptor.get && descriptor.set) {
976
+ autoGenerateGetSet(proto, key);
977
+
978
+ const mixinClassConfig = mixinProto.constructor.config;
979
+
980
+ if (Object.hasOwn(mixinClassConfig, key)) {
981
+ classConfig[key] = mixinClassConfig[key];
982
+ }
983
+
836
984
  return
837
985
  }
838
986
 
@@ -879,7 +1027,7 @@ function parseArrayFromString(str) {
879
1027
  )
880
1028
  }
881
1029
 
882
- Neo.config = Neo.config || {};
1030
+ Neo.config ??= {};
883
1031
 
884
1032
  Neo.assignDefaults(Neo.config, DefaultConfig);
885
1033