@lightningjs/blits 1.47.0 → 1.49.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,29 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## v1.49.0
5
+
6
+ _20 feb 2026_
7
+
8
+ - Added `data-blits-renderstate` inspector attribute
9
+ - Fixed `data-blits-hasfocus` inspector attribute to always be available
10
+ - Added boolean attribute to `idle` lifecycle hook to indicate `idle` or `active` state
11
+ - Fixed type definitions for
12
+ - Fixed double processiong of reactivity guard
13
+
14
+ ## v1.48.0
15
+
16
+ _10 feb 2026_
17
+
18
+ - Improved cleanup of for-loop effects
19
+ - Fixed router issue with params not being reset
20
+ - Added next tick debouncing for transitions on multiple props
21
+ - Bumped renderer to 2.21.0
22
+ - Added support for using function-notation in `@loaded`, `@error` and `@updated` callbacks
23
+ - Added displaying of correct component type in inspector data when using `is`-attribute
24
+ - Replaced `$`-prefix for automatic inspector data with `blits-` prefix (fixes fatal error in Safari)
25
+
26
+
3
27
  ## v1.47.0
4
28
 
5
29
  _26 jan 2026_
package/index.d.ts CHANGED
@@ -201,11 +201,14 @@ declare module '@lightningjs/blits' {
201
201
  */
202
202
  exit?: () => void;
203
203
  /**
204
- * Fires when the renderer is done rendering and enters an idle state
204
+ * Fires when the renderer is done rendering and enters an idle state (idle = true)
205
+ * and when the renderer starts rendering again and leaves the idle state (idle = false)
206
+ *
207
+ * @param idle - boolean to indicate whether the renderer is in idle state or not
205
208
  *
206
209
  * Note: This event can fire multiple times
207
210
  */
208
- idle?: () => void;
211
+ idle?: (idle: boolean) => void;
209
212
  /**
210
213
  * Fires at a predefined interval and reports the current FPS value
211
214
  *
@@ -666,9 +669,9 @@ declare module '@lightningjs/blits' {
666
669
 
667
670
  export interface RouterHooks {
668
671
  init?: () => Promise<void> | void;
669
- beforeEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
670
- afterEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
671
- error?: (err: string) => string | Route | Promise<string | Route> | void;
672
+ beforeEach?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
673
+ afterEach?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
674
+ error?: (err: string) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
672
675
  }
673
676
 
674
677
  export interface RouterConfig<P extends Props, S, M, C> {
@@ -694,13 +697,13 @@ declare module '@lightningjs/blits' {
694
697
 
695
698
  /**
696
699
  * Enable or disable RouterView history navigation on Back input
697
- *
700
+ *
698
701
  * @default true
699
- *
702
+ *
700
703
  * @remarks
701
704
  * This is an app-wide setting that affects all RouterView instances in your application.
702
705
  * The router state is global and shared across all router instances.
703
- *
706
+ *
704
707
  * @example
705
708
  * ```js
706
709
  * router: {
@@ -809,8 +812,8 @@ declare module '@lightningjs/blits' {
809
812
  }[keyof T]
810
813
 
811
814
  export interface RouteHooks {
812
- before?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
813
- after?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
815
+ before?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
816
+ after?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
814
817
  }
815
818
 
816
819
  export type Route = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.47.0",
3
+ "version": "1.49.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.23.0",
78
78
  "magic-string": "^0.30.21"
79
79
  },
80
80
  "repository": {
@@ -19,7 +19,7 @@ import fs from 'fs'
19
19
  import path from 'path'
20
20
  import { fileURLToPath } from 'url'
21
21
  import compiler from '../src/lib/precompiler/precompiler.js'
22
- import { exec } from 'child_process'
22
+ import childProcess from 'child_process'
23
23
 
24
24
  const currentDir = process.cwd()
25
25
  const componentDirs = ['src/components']
@@ -71,7 +71,7 @@ function processFile(filePath) {
71
71
  function formatFileWithESLint(filePath) {
72
72
  const command = `eslint "${filePath}" --fix`
73
73
 
74
- exec(command, (error, stdout, stderr) => {
74
+ childProcess.exec(command, (error, stdout, stderr) => {
75
75
  if (error) {
76
76
  console.error(`Execution Error: ${error.message}`)
77
77
  console.error(`Exit Code: ${error.code}`)
@@ -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
@@ -267,33 +267,60 @@ const Component = (name = required('name'), config = required('config')) => {
267
267
  this[symbols.slots] = this[symbols.children].filter((child) => child[symbols.isSlot])
268
268
 
269
269
  this[symbols.rendererEventListeners] = []
270
+
271
+ if (this[symbols.holder] !== undefined && Settings.get('inspector', false)) {
272
+ this[symbols.holder].setInspectorMetadata({ 'blits-hasFocus': false })
273
+ }
274
+
275
+ // setup inspector data for render state
276
+ if (name === 'App' && Settings.get('inspector', false)) {
277
+ this.$setTimeout(() => {
278
+ const wrapperEl = this[symbols.wrapper]
279
+ const idleCb = () => {
280
+ wrapperEl.setInspectorMetadata({ 'blits-renderState': 'idle' })
281
+ }
282
+ const activeCb = () => {
283
+ wrapperEl.setInspectorMetadata({ 'blits-renderState': 'active' })
284
+ }
285
+ renderer.on('idle', idleCb)
286
+ renderer.on('active', activeCb)
287
+ this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
288
+ this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
289
+ })
290
+ }
291
+
270
292
  // register hooks if component has hooks specified
271
293
  if (config.hooks) {
272
294
  // push to next tick to ensure
273
- setTimeout(() => {
295
+ this.$setTimeout(() => {
274
296
  // frame tick event
275
297
  if (config.hooks.frameTick) {
276
- const cb = (r, data) => emit('frameTick', this[symbols.identifier], this, [data])
277
- this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
298
+ const cb = (_r, data) => emit('frameTick', this[symbols.identifier], this, [data])
278
299
  renderer.on('frameTick', cb)
300
+ this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
279
301
  }
280
302
 
281
303
  // idle event
282
304
  if (config.hooks.idle) {
283
- const cb = () => {
284
- emit('idle', this[symbols.identifier], this)
305
+ const idleCb = () => {
306
+ emit('idle', this[symbols.identifier], this, [true])
307
+ }
308
+ const activeCb = () => {
309
+ emit('idle', this[symbols.identifier], this, [false])
285
310
  }
286
- this[symbols.rendererEventListeners].push({ event: 'idle', cb })
287
- renderer.on('idle', cb)
311
+ renderer.on('idle', idleCb)
312
+ renderer.on('active', activeCb)
313
+ this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
314
+ this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
288
315
  }
289
316
 
290
317
  // fpsUpdate event
291
318
  if (config.hooks.fpsUpdate) {
292
- const cb = (r, data) => {
319
+ const cb = (_r, data) => {
293
320
  emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
294
321
  }
295
- this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
296
322
  renderer.on('fpsUpdate', cb)
323
+ this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
297
324
  }
298
325
  })
299
326
 
@@ -450,6 +477,9 @@ const Component = (name = required('name'), config = required('config')) => {
450
477
  // during the code generation step
451
478
  factory[Symbol.for('config')] = config
452
479
 
480
+ // Display name for inspector
481
+ factory[Symbol.for('componentType')] = name
482
+
453
483
  // To determine whether dynamic component is actual Blits component or not
454
484
  factory[symbols.isComponent] = true
455
485
 
@@ -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()) {
@@ -195,7 +195,7 @@ export default (code) => {
195
195
  if (thisRefs.size > 0) {
196
196
  // Skip if already modified
197
197
  if (
198
- prop.body.includes(commentText) ||
198
+ prop.body.includes(commentText.trim()) ||
199
199
  Array.from(thisRefs).some((ref) => prop.body.startsWith(ref))
200
200
  ) {
201
201
  continue
@@ -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 || {}