@lightningjs/blits 1.47.0 → 1.48.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## v1.48.0
5
+
6
+ _10 feb 2026_
7
+
8
+ - Improved cleanup of for-loop effects
9
+ - Fixed router issue with params not being reset
10
+ - Added next tick debouncing for transitions on multiple props
11
+ - Bumped renderer to 2.21.0
12
+ - Added support for using function-notation in `@loaded`, `@error` and `@updated` callbacks
13
+ - Added displaying of correct component type in inspector data when using `is`-attribute
14
+ - Replaced `$`-prefix for automatic inspector data with `blits-` prefix (fixes fatal error in Safari)
15
+
16
+
3
17
  ## v1.47.0
4
18
 
5
19
  _26 jan 2026_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.47.0",
3
+ "version": "1.48.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@lightningjs/msdf-generator": "^1.2.0",
77
- "@lightningjs/renderer": "^2.20.4",
77
+ "@lightningjs/renderer": "^2.21.0",
78
78
  "magic-string": "^0.30.21"
79
79
  },
80
80
  "repository": {
@@ -69,7 +69,6 @@ try {
69
69
  // Run ESLint on the changed files
70
70
  execSync(`npx eslint ${fileList.join(' ')}`, { stdio: 'inherit' })
71
71
  console.log('Linting passed successfully!\n')
72
- process.exit(0)
73
72
  } catch (error) {
74
73
  console.log('\n==========================================================================')
75
74
  console.log('ESLint found issues in your code. Please fix them before pushing.')
@@ -79,3 +78,24 @@ try {
79
78
  console.log('==========================================================================\n')
80
79
  process.exit(1)
81
80
  }
81
+
82
+ // By default, always run tests unless RUN_TEST=skip
83
+ const runTestsFlag = process.env.RUN_TEST || ''
84
+
85
+ if (runTestsFlag.toLowerCase() === 'skip') {
86
+ console.log('Skipping tests due to RUN_TEST=skip. Proceeding with push.')
87
+ process.exit(0)
88
+ } else {
89
+ // For any other value (including empty or unknown), run tests
90
+ try {
91
+ console.log('Running tests...\n')
92
+ execSync('npm run test', { stdio: 'inherit' })
93
+ console.log('Tests passed successfully!\n')
94
+ process.exit(0)
95
+ } catch (error) {
96
+ console.log('\n==========================================================================')
97
+ console.log('Some tests failed. Please fix them before pushing.')
98
+ console.log('==========================================================================\n')
99
+ process.exit(1)
100
+ }
101
+ }
package/src/component.js CHANGED
@@ -450,6 +450,9 @@ const Component = (name = required('name'), config = required('config')) => {
450
450
  // during the code generation step
451
451
  factory[Symbol.for('config')] = config
452
452
 
453
+ // Display name for inspector
454
+ factory[Symbol.for('componentType')] = name
455
+
453
456
  // To determine whether dynamic component is actual Blits component or not
454
457
  factory[symbols.isComponent] = true
455
458
 
@@ -602,7 +602,7 @@ const Element = {
602
602
  this.props.props['data'] = {}
603
603
  }
604
604
 
605
- // Merge framework data (with $ prefix to prevent collisions)
605
+ // Merge framework data (blits-* keys data attributes)
606
606
  Object.assign(this.props['data'], data)
607
607
  Object.assign(this.props.props['data'], data)
608
608
 
@@ -656,13 +656,31 @@ const Element = {
656
656
  }
657
657
  },
658
658
  animate(prop, value, transition) {
659
+ // Clear any existing debounce timeout for this property
660
+ if (this.debounceTimeouts[prop] !== undefined) {
661
+ clearTimeout(this.debounceTimeouts[prop])
662
+ Log.debug(`Cleared debounce timeout for property "${prop}"`)
663
+ }
664
+
665
+ // Debounce the animation execution
666
+ this.debounceTimeouts[prop] = setTimeout(() => {
667
+ delete this.debounceTimeouts[prop]
668
+ this._executeAnimation(prop, value, transition)
669
+ }, 0)
670
+ },
671
+ _executeAnimation(prop, value, transition) {
659
672
  // check if a transition is already scheduled to run on the same prop
660
673
  // and cancels it if it does
674
+ const stateOfAnimation =
675
+ this.scheduledTransitions[prop] !== undefined
676
+ ? this.scheduledTransitions[prop].f.state
677
+ : undefined
678
+
661
679
  if (
662
- this.scheduledTransitions[prop] !== undefined &&
663
- this.scheduledTransitions[prop].f.state === 'scheduled'
680
+ stateOfAnimation !== undefined &&
681
+ (stateOfAnimation === 'scheduled' || stateOfAnimation === 'running')
664
682
  ) {
665
- this.scheduledTransitions[prop].f.stop()
683
+ this.scheduledTransitions[prop].f.stop(stateOfAnimation === 'running' ? false : true)
666
684
  }
667
685
 
668
686
  // if current value is the same as the value to animate to, instantly resolve
@@ -700,7 +718,7 @@ const Element = {
700
718
 
701
719
  // Update inspector metadata when transition starts
702
720
  if (inspectorEnabled === true) {
703
- this.setInspectorMetadata({ $isTransitioning: true })
721
+ this.setInspectorMetadata({ 'blits-isTransitioning': true })
704
722
  }
705
723
 
706
724
  if (transition.start !== undefined && typeof transition.start === 'function') {
@@ -740,7 +758,7 @@ const Element = {
740
758
  // Update inspector metadata when transition ends
741
759
  if (inspectorEnabled === true) {
742
760
  this.setInspectorMetadata({
743
- $isTransitioning: Object.keys(this.scheduledTransitions).length > 0,
761
+ 'blits-isTransitioning': Object.keys(this.scheduledTransitions).length > 0,
744
762
  })
745
763
  }
746
764
  })
@@ -752,6 +770,14 @@ const Element = {
752
770
  if (this.node === null) return
753
771
 
754
772
  Log.debug('Deleting Node', this.nodeId)
773
+
774
+ // Clear all pending debounce timeouts
775
+ const debounceProps = Object.keys(this.debounceTimeouts)
776
+ for (let i = 0; i < debounceProps.length; i++) {
777
+ clearTimeout(this.debounceTimeouts[debounceProps[i]])
778
+ }
779
+ this.debounceTimeouts = null
780
+
755
781
  // Clearing transition end callback functions
756
782
  const transitionProps = Object.keys(this.scheduledTransitions)
757
783
  for (let i = 0; i < transitionProps.length; i++) {
@@ -834,6 +860,7 @@ export default (config, component) => {
834
860
  return Object.assign(Object.create(Element), {
835
861
  props: Object.assign(Object.create(propsTransformer), { props: {} }),
836
862
  scheduledTransitions: {},
863
+ debounceTimeouts: {},
837
864
  config,
838
865
  component,
839
866
  })
@@ -360,17 +360,17 @@ const generateComponentCode = function (
360
360
  }
361
361
  `)
362
362
 
363
- // For forloops, this code runs per instance, setting metadata for each component instance
364
363
  if (isDev === true) {
365
- const componentType = templateObject[Symbol.for('componentType')]
364
+ const templateTagName = templateObject[Symbol.for('componentType')]
366
365
  const holderElm = options.key
367
366
  ? `elms[${holderCounter}][${options.key}]`
368
367
  : `elms[${holderCounter}]`
368
+ const componentDisplayName = `typeof componentType === 'string'
369
+ ? componentType
370
+ : (componentType?.[Symbol.for('componentType')] || '${templateTagName}')`
369
371
  renderCode.push(`
370
372
  if (${holderElm} !== undefined && typeof ${holderElm}.setInspectorMetadata === 'function') {
371
- ${holderElm}.setInspectorMetadata({
372
- $componentType: '${componentType}'
373
- })
373
+ ${holderElm}.setInspectorMetadata({ 'blits-componentType': ${componentDisplayName} })
374
374
  }
375
375
  `)
376
376
  }
@@ -503,10 +503,16 @@ const generateForLoopCode = function (templateObject, parent) {
503
503
 
504
504
  component !== null && component[Symbol.for('removeGlobalEffects')](effects[${forStartCounter}])
505
505
 
506
- for(let i = 0; i < effects[${forStartCounter}].length; i++) {
507
- const value = effects[${forStartCounter}][i]
508
- const index = component[Symbol.for('effects')].indexOf(value)
509
- if (index > -1) component[Symbol.for('effects')].splice(index, 1)
506
+ const effectsToRemove = new Set(effects[${forStartCounter}].slice(0))
507
+ if (effectsToRemove.size > 0) {
508
+ const componentEffects = component?.[Symbol.for('effects')] || []
509
+ let writeIndex = 0
510
+ for (let readIndex = 0; readIndex < componentEffects.length; readIndex++) {
511
+ if (!effectsToRemove.has(componentEffects[readIndex])) {
512
+ componentEffects[writeIndex++] = componentEffects[readIndex]
513
+ }
514
+ }
515
+ componentEffects.length = writeIndex
510
516
  }
511
517
 
512
518
  effects[${forStartCounter}].length = 0
@@ -804,8 +810,13 @@ const cast = (val = '', key = false, component = 'component.') => {
804
810
  }
805
811
  // @-listener
806
812
  else if (key.startsWith('@') && val) {
807
- const c = component.slice(0, -1)
808
- castedValue = `${c}['${val.replace('$', '')}'] && ${c}['${val.replace('$', '')}'].bind(${c})`
813
+ const trimmed = val.trim()
814
+ if (/^\$?\w+$/.test(trimmed)) {
815
+ const c = component.slice(0, -1)
816
+ castedValue = `${c}['${trimmed.replace('$', '')}'] && ${c}['${trimmed.replace('$', '')}'].bind(${c})`
817
+ } else {
818
+ castedValue = interpolate(trimmed, component)
819
+ }
809
820
  }
810
821
  // dynamic value
811
822
  else if (val.startsWith('$')) {
@@ -97,7 +97,7 @@ export default {
97
97
  this.component[symbols.holder] &&
98
98
  typeof this.component[symbols.holder].setInspectorMetadata === 'function'
99
99
  ) {
100
- this.component[symbols.holder].setInspectorMetadata({ $hasFocus: true })
100
+ this.component[symbols.holder].setInspectorMetadata({ 'blits-hasFocus': true })
101
101
  }
102
102
  }
103
103
  if (v === 'unfocus') {
@@ -107,7 +107,7 @@ export default {
107
107
  this.component[symbols.holder] &&
108
108
  typeof this.component[symbols.holder].setInspectorMetadata === 'function'
109
109
  ) {
110
- this.component[symbols.holder].setInspectorMetadata({ $hasFocus: false })
110
+ this.component[symbols.holder].setInspectorMetadata({ 'blits-hasFocus': false })
111
111
  }
112
112
  }
113
113
  }
@@ -32,9 +32,10 @@ const objectMap = new WeakMap()
32
32
  const globalEffectsMap = new Map()
33
33
 
34
34
  export const removeGlobalEffects = (effectsToRemove) => {
35
- if (globalEffectsMap.size === 0) return
35
+ if (globalEffectsMap.size === 0 || effectsToRemove.length === 0) return
36
+ const effectsToRemoveSet = new Set(effectsToRemove)
36
37
  for (const [effect, target] of globalEffectsMap) {
37
- if (effectsToRemove.indexOf(effect) === -1) continue
38
+ if (!effectsToRemoveSet.has(effect)) continue
38
39
  const effectsSet = objectMap.get(target)
39
40
  if (effectsSet === undefined) continue
40
41
  for (const set of effectsSet.values()) {
@@ -403,7 +403,7 @@ export const navigate = async function () {
403
403
  // Update router state after announcements and final route resolution,
404
404
  // right before initializing or restoring the view
405
405
  state.path = route.path
406
- state.params = route.params || {}
406
+ state.params = Object.keys(route.params).length === 0 ? null : route.params
407
407
  state.hash = route.hash
408
408
  state.data = null
409
409
  state.data = route.data || {}