@lightningjs/blits 1.20.0 → 1.21.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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.21.0
4
+
5
+ _27 feb 2025_
6
+
7
+ - Added functionality for keeping query params when navigating back in the router
8
+ - Exposed more info in reactive router state
9
+ - Added `range`-attribute to for-loop
10
+ - Fixed issue when setting `alpha` to truthy or false values
11
+ - Upgraded to renderer v2.13.0
12
+
13
+ ## v1.20.1
14
+
15
+ _14 feb 2025_
16
+
17
+ - Added error message when root element in template contains a `:for`-attribute
18
+ - Added workaround fix for shader caching issue
19
+ - Upgraded to renderer v2.12.1
20
+
3
21
  ## v1.20.0
4
22
 
5
23
  _12 feb 2025_
package/index.d.ts CHANGED
@@ -799,8 +799,59 @@ declare module '@lightningjs/blits' {
799
799
  * and cleaned up
800
800
  *
801
801
  * Defaults to `200` (mb)
802
+ * @deprecated
803
+ * Deprecated: use `gpuMemory` launch setting instead
802
804
  */
803
805
  gpuMemoryLimit?: number,
806
+ /**
807
+ * Configures the gpu memory settings used by the renderer
808
+ */
809
+ gpuMemory: {
810
+ /**
811
+ * Maximum GPU memory threshold (in `mb`) after which
812
+ * the renderer will immediately start cleaning up textures to free
813
+ * up graphical memory
814
+ *
815
+ * When setting to `0`, texture memory management is disabled
816
+ *
817
+ * @default `200`
818
+ */
819
+ max: number,
820
+ /**
821
+ * Target threshold of GPU memory usage, defined as a fraction of
822
+ * the max threshold. The renderer will attempt to keep memory
823
+ * usage below this target by cleaning up non-renderable textures
824
+ *
825
+ * @default `0.8`
826
+ */
827
+ target: 0.8,
828
+ /**
829
+ * Interval at which regular texture cleanups occur (in `ms`)
830
+ *
831
+ * @default `5000`
832
+ */
833
+ cleanupInterval: 5000,
834
+ /**
835
+ * Baseline GPU memory usage of the App (in `mb`), without rendering any
836
+ * textures. This value will be used as a basis when calculating
837
+ * the total memory usage towards the max and target memory
838
+ * usage
839
+ *
840
+ * @default `25`
841
+ */
842
+ baseline: 25,
843
+ /**
844
+ * Whether or not the max threshold should be considered
845
+ * as a strict number that can not be exceeded in any way
846
+ *
847
+ * When set to `true`, new textures won't be created when the
848
+ * max threshold has been reached, until agressive texture cleanup
849
+ * has brought the memory back down
850
+ *
851
+ * @default false
852
+ */
853
+ strict: false,
854
+ },
804
855
  /**
805
856
  * Defines which mode the renderer should operate in: `webgl` or `canvas`
806
857
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@lightningjs/msdf-generator": "^1.1.1",
55
- "@lightningjs/renderer": "^2.12.0"
55
+ "@lightningjs/renderer": "^2.13.0"
56
56
  },
57
57
  "repository": {
58
58
  "type": "git",
@@ -22,6 +22,9 @@ import { Log } from '../../lib/log.js'
22
22
  import symbols from '../../lib/symbols.js'
23
23
  import Settings from '../../settings.js'
24
24
 
25
+ // temporary counter to work around shader caching issues
26
+ let counter = 0
27
+
25
28
  const createPaddingObject = (padding, direction) => {
26
29
  if (padding === undefined) {
27
30
  return { start: 0, end: 0, oppositeStart: 0, oppositeEnd: 0 }
@@ -309,7 +312,7 @@ const propsTransformer = {
309
312
  }
310
313
  },
311
314
  set alpha(v) {
312
- if (this.raw['show'] === undefined || this.raw['show'] === true) {
315
+ if (this.raw['show'] === undefined || this.raw['show'] == true) {
313
316
  this.props['alpha'] = v
314
317
  }
315
318
  },
@@ -330,14 +333,21 @@ const propsTransformer = {
330
333
  if (this.element.node === undefined) {
331
334
  this.props['shader'] = renderer.createShader('DynamicShader', {
332
335
  effects: v.map((effect) => {
333
- return renderer.createEffect(effect.type, effect.props, effect.type)
336
+ // temporary add counter to work around shader caching issues
337
+ return renderer.createEffect(
338
+ effect.type,
339
+ effect.props,
340
+ effect.type + this.element.counter
341
+ )
334
342
  }),
335
343
  })
336
344
  } else {
337
345
  for (let i = 0; i < v.length; i++) {
338
- const target = this.element.node.shader.props[v[i].type]
346
+ // temporary add counter to work around shader caching issues
347
+ const target = this.element.node.shader.props[v[i].type + this.element.counter]
339
348
  const props = Object.keys(v[i].props)
340
349
 
350
+ if (target == undefined) continue
341
351
  for (let j = 0; j < props.length; j++) {
342
352
  target[props[j]] = v[i].props[props[j]]
343
353
  }
@@ -642,5 +652,6 @@ export default (config, component) => {
642
652
  scheduledTransitions: {},
643
653
  config,
644
654
  component,
655
+ counter: counter++,
645
656
  })
646
657
  }
@@ -42,6 +42,27 @@ const textRenderEngines = (settings) => {
42
42
  if (renderMode === 'canvas') return [CanvasTextRenderer]
43
43
  }
44
44
 
45
+ const textureMemorySettings = (settings) => {
46
+ const gpuMemory = {
47
+ ...{
48
+ max: 200,
49
+ target: 0.8,
50
+ cleanupInterval: 5000,
51
+ baseline: 25,
52
+ strict: false,
53
+ },
54
+ ...('gpuMemory' in settings === true ? settings.gpuMemory : {}),
55
+ }
56
+
57
+ return {
58
+ criticalThreshold: gpuMemory.max * 1024 * 1024, // convert from mb to bytes
59
+ targetThresholdLevel: gpuMemory.target,
60
+ cleanupInterval: gpuMemory.cleanupInterval,
61
+ baselineMemoryAllocation: gpuMemory.baseline * 1024 * 1024, // convert from mb to bytes
62
+ doNotExceedCriticalThreshold: gpuMemory.strict,
63
+ }
64
+ }
65
+
45
66
  export default (App, target, settings = {}) => {
46
67
  renderer = new RendererMain(
47
68
  {
@@ -62,13 +83,11 @@ export default (App, target, settings = {}) => {
62
83
  clearColor: (settings.canvasColor && colors.normalize(settings.canvasColor)) || 0x00000000,
63
84
  inspector: settings.inspector === true ? Inspector : undefined,
64
85
  boundsMargin: settings.viewportMargin || 0,
65
- // gpu memory limit, converted from mb to bytes - defaults to 200mb
66
- txMemByteThreshold:
67
- 'gpuMemoryLimit' in settings ? settings.gpuMemoryLimit * 1024 * 1024 : 200 * 1024 * 1024,
68
86
  renderEngine: renderEngine(settings),
69
87
  fontEngines: textRenderEngines(settings),
70
88
  canvas: settings.canvas,
71
89
  textureProcessingTimeLimit: settings.textureProcessingTimeLimit,
90
+ textureMemory: textureMemorySettings(settings),
72
91
  },
73
92
  target
74
93
  )
@@ -291,6 +291,9 @@ const generateComponentCode = function (
291
291
  const generateForLoopCode = function (templateObject, parent) {
292
292
  const forLoop = templateObject[':for']
293
293
  delete templateObject[':for']
294
+ const range = templateObject['range'] || templateObject[':range'] || '{}'
295
+ delete templateObject['range']
296
+ delete templateObject[':range']
294
297
 
295
298
  const key = templateObject['key']
296
299
  const forKey = interpolate(key, 'scope.')
@@ -340,10 +343,19 @@ const generateForLoopCode = function (templateObject, parent) {
340
343
 
341
344
  ctx.renderCode.push(`
342
345
  const created${forStartCounter} = []
346
+
347
+ let from${forStartCounter}
348
+ let to${forStartCounter}
349
+
343
350
  const forloop${forStartCounter} = (collection = [], elms, created) => {
344
351
  const rawCollection = getRaw(collection)
345
352
  const keys = new Set()
346
353
  let l = rawCollection.length
354
+
355
+ const range = ${interpolate(range)} || {}
356
+ from${forStartCounter} = range['from'] || 0
357
+ to${forStartCounter} = 'to' in range ? range['to'] : rawCollection.length
358
+
347
359
  while(l--) {
348
360
  const ${item} = rawCollection[l]
349
361
  `)
@@ -354,7 +366,9 @@ const generateForLoopCode = function (templateObject, parent) {
354
366
  `)
355
367
  }
356
368
  ctx.renderCode.push(`
357
- keys.add('' + ${interpolate(key, '') || 'l'})
369
+ if(l < to${forStartCounter} && l >= from${forStartCounter}) {
370
+ keys.add('' + ${interpolate(key, '') || 'l'})
371
+ }
358
372
  }
359
373
  `)
360
374
 
@@ -368,6 +382,7 @@ const generateForLoopCode = function (templateObject, parent) {
368
382
  const length = rawCollection.length
369
383
  const effects = []
370
384
  for(let __index = 0; __index < length; __index++) {
385
+ if(__index < from${forStartCounter} || __index >= to${forStartCounter}) continue
371
386
  const scope = Object.create(component)
372
387
  parent = ${parent}
373
388
  scope['${item}'] = rawCollection[__index]
@@ -488,6 +503,10 @@ const generateForLoopCode = function (templateObject, parent) {
488
503
  effectKey = effectKey.match(/[^.]+$/)[0]
489
504
  }
490
505
 
506
+ // get the reference to range from and to
507
+ const effectKeysRegex = /\$([^,} ]+)/g
508
+ const effectKeys = [...range.matchAll(effectKeysRegex)].map((match) => `'${match[1]}'`)
509
+
491
510
  ctx.renderCode.push(`
492
511
  let forEffects${forStartCounter}
493
512
  effect(() => {
@@ -497,7 +516,7 @@ const generateForLoopCode = function (templateObject, parent) {
497
516
  result[2],
498
517
  ':for'
499
518
  )}, elms, created${forStartCounter})
500
- }, '${effectKey}' )
519
+ }, ['${effectKey}', ${effectKeys.join(',')}] )
501
520
  `)
502
521
 
503
522
  outerScopeEffects.forEach((effect) => {
@@ -520,6 +539,7 @@ const generateForLoopCode = function (templateObject, parent) {
520
539
  effect(() => {
521
540
  void ${refs.join(', ')}
522
541
  for(let __index = 0; __index < ${interpolate(result[2])}.length; __index++) {
542
+ if(__index < from${forStartCounter} || __index >= to${forStartCounter}) continue
523
543
  const scope = {}
524
544
  scope['${index}'] = __index
525
545
  scope['${item}'] = ${interpolate(result[2])}[__index]
@@ -49,9 +49,11 @@ export const track = (target, key, global = false) => {
49
49
  if (paused) {
50
50
  return
51
51
  }
52
- if (currentKey !== null && key !== currentKey) {
53
- return
54
- }
52
+ // note: nesting the conditions like this seems to perform better ¯\_(ツ)_/¯
53
+ if (Array.isArray(currentKey) === true) {
54
+ if (currentKey.includes(key) === false) return
55
+ } else if (currentKey !== null && key !== currentKey) return
56
+
55
57
  let effectsMap = objectMap.get(target)
56
58
  if (!effectsMap) {
57
59
  effectsMap = new Map()
@@ -101,10 +101,14 @@ const reactiveProxy = (original, _parent = null, _key, global) => {
101
101
  const rawValue = getRaw(value)
102
102
 
103
103
  let result = true
104
- const isEqual =
105
- Array.isArray(rawValue) === true
106
- ? deepEqualArray(oldRawValue, rawValue)
107
- : oldRawValue === rawValue
104
+ let isEqual = false
105
+
106
+ // We need to check that the oldRawValue is also an array before we do the deep equal check
107
+ if (Array.isArray(rawValue) === true && Array.isArray(oldRawValue) === true) {
108
+ isEqual = deepEqualArray(oldRawValue, rawValue)
109
+ } else if (oldRawValue === rawValue) {
110
+ isEqual = true
111
+ }
108
112
 
109
113
  if (isEqual === false) {
110
114
  if (typeof value === 'object') {
@@ -239,6 +239,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
239
239
  #2: There must be exactly one top-level element (an element at level 0). This element may either be a self-closing
240
240
  element or an opening tag followed by a closing tag. If more than one top-level element is encountered, an error
241
241
  should be thrown.
242
+ But the top-level element can not have :for attribute.
242
243
  */
243
244
  const format = (parsedData) => {
244
245
  let stack = []
@@ -254,6 +255,10 @@ export default (template = '', componentName, parentComponent, filePath = null)
254
255
  if (rootElementDefined) {
255
256
  throw TemplateStructureError('MultipleTopLevelTags', element)
256
257
  }
258
+ // Check for :for attribute on root level element
259
+ if (element[':for'] !== undefined) {
260
+ throw TemplateStructureError('ForAttributeOnRootElement', element)
261
+ }
257
262
  rootElementDefined = true
258
263
  }
259
264
 
@@ -1388,3 +1388,24 @@ test('Parse template with color and effects attributes and parsing should conver
1388
1388
  )
1389
1389
  assert.end()
1390
1390
  })
1391
+
1392
+ test('Parse template with :for attribute on root element and parsing should fail', (assert) => {
1393
+ const template = `
1394
+ <Element :for="item in $items">
1395
+ <Text content="$item.name" />
1396
+ </Element>
1397
+ `
1398
+
1399
+ try {
1400
+ parser(template)
1401
+ assert.fail('Parser should throw TemplateStructureError:ForAttributeOnRootElement')
1402
+ } catch (error) {
1403
+ assert.equal(error.name, 'TemplateStructureError', 'Parser should throw TemplateStructureError')
1404
+ assert.ok(
1405
+ error.message.startsWith('ForAttributeOnRootElement'),
1406
+ 'Parser should throw TemplateStructureError:ForAttributeOnRootElement'
1407
+ )
1408
+ }
1409
+
1410
+ assert.end()
1411
+ })
@@ -25,7 +25,13 @@ import Focus from '../focus.js'
25
25
  import Announcer from '../announcer/announcer.js'
26
26
 
27
27
  export let currentRoute
28
- export const state = reactive({ path: null, navigating: false })
28
+ export const state = reactive({
29
+ path: '',
30
+ navigating: false,
31
+ data: null,
32
+ params: null,
33
+ hash: '',
34
+ })
29
35
 
30
36
  const cacheMap = new WeakMap()
31
37
  const history = []
@@ -38,8 +44,9 @@ let previousFocus
38
44
  export const getHash = () => {
39
45
  const hashParts = (document.location.hash || '/').replace(/^#/, '').split('?')
40
46
  return {
41
- hash: hashParts[0],
47
+ path: hashParts[0],
42
48
  queryParams: new URLSearchParams(hashParts[1]),
49
+ hash: document.location.hash,
43
50
  }
44
51
  }
45
52
 
@@ -108,7 +115,6 @@ export const matchHash = (path, routes = []) => {
108
115
  if (!matchingRoute.data) {
109
116
  matchingRoute.data = {}
110
117
  }
111
- currentRoute = matchingRoute
112
118
  }
113
119
 
114
120
  return matchingRoute
@@ -117,9 +123,16 @@ export const matchHash = (path, routes = []) => {
117
123
  export const navigate = async function () {
118
124
  state.navigating = true
119
125
  if (this.parent[symbols.routes]) {
120
- const previousRoute = currentRoute
121
- const { hash, queryParams } = getHash()
122
- let route = matchHash(hash, this.parent[symbols.routes])
126
+ let previousRoute = currentRoute ? Object.assign({}, currentRoute) : undefined
127
+ const { hash, path, queryParams } = getHash()
128
+ let route = matchHash(path, this.parent[symbols.routes])
129
+
130
+ // Adding the location hash to the route if it exists.
131
+ if (hash !== null) {
132
+ route.hash = hash
133
+ }
134
+
135
+ currentRoute = route
123
136
  let beforeHookOutput
124
137
  if (route) {
125
138
  if (route.hooks) {
@@ -149,6 +162,7 @@ export const navigate = async function () {
149
162
  }
150
163
 
151
164
  let holder
165
+ let routeData
152
166
  let { view, focus } = cacheMap.get(route) || {}
153
167
 
154
168
  if (!view) {
@@ -164,13 +178,17 @@ export const navigate = async function () {
164
178
  queryParamsData[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
165
179
  }
166
180
 
181
+ routeData = {
182
+ ...navigationData,
183
+ ...route.data,
184
+ ...queryParamsData,
185
+ }
186
+
167
187
  // merge props with potential route params, navigation data and route data to be injected into the component instance
168
188
  const props = {
169
189
  ...this[symbols.props],
170
190
  ...route.params,
171
- ...navigationData,
172
- ...route.data,
173
- ...queryParamsData,
191
+ ...routeData,
174
192
  }
175
193
 
176
194
  view = await route.component({ props }, holder, this)
@@ -236,9 +254,14 @@ export const navigate = async function () {
236
254
  if (oldView) {
237
255
  removeView(previousRoute, oldView, route.transition.out)
238
256
  }
257
+
258
+ previousRoute = undefined
239
259
  }
240
260
 
241
261
  state.path = route.path
262
+ state.params = route.params
263
+ state.hash = hash
264
+ state.data = routeData
242
265
 
243
266
  // apply in transition
244
267
  if (route.transition.in) {
@@ -320,7 +343,8 @@ const setOrAnimate = (node, transition, shouldAnimate = true) => {
320
343
  export const to = (location, data = {}, options = {}) => {
321
344
  navigationData = data
322
345
  overrideOptions = options
323
- window.location.hash = `#${location}`
346
+
347
+ window.location.hash = location
324
348
  }
325
349
 
326
350
  export const back = function () {
@@ -328,13 +352,8 @@ export const back = function () {
328
352
  if (route && currentRoute !== route) {
329
353
  // set indicator that we are navigating back (to prevent adding page to history stack)
330
354
  navigatingBack = true
331
- let targetRoutePath = route.path
332
- if (targetRoutePath.indexOf(':') > -1) {
333
- Object.keys(route.params).forEach((item) => {
334
- targetRoutePath = targetRoutePath.replace(`:${item}`, route.params[item])
335
- })
336
- }
337
- to(targetRoutePath)
355
+
356
+ to(route.hash, route.data, route.options)
338
357
  return true
339
358
  }
340
359
 
@@ -348,6 +367,7 @@ export const back = function () {
348
367
 
349
368
  const hashEnd = /(\/:?[\w%\s-]+)$/
350
369
  let path = currentRoute.path
370
+
351
371
  let level = path.split('/').length
352
372
 
353
373
  // On root return