@lightningjs/blits 1.38.3 → 1.39.1

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.39.1
4
+
5
+ _26 Sept 2025_
6
+
7
+ - Bumped Lightning renderer to 2.17.0
8
+
9
+
10
+ ## v1.39.0
11
+
12
+ _16 Sept 2025_
13
+
14
+ - Refactored router to be more performant and prevent runtime route changes to affect other route instances
15
+
3
16
  ## v1.38.3
4
17
 
5
18
  _4 Sept 2025_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.38.3",
3
+ "version": "1.39.1",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@lightningjs/msdf-generator": "^1.1.1",
58
- "@lightningjs/renderer": "^2.15.0"
58
+ "@lightningjs/renderer": "^2.17.0"
59
59
  },
60
60
  "repository": {
61
61
  "type": "git",
@@ -71,25 +71,25 @@ export const state = reactive(
71
71
  * @property {BlitsComponent} focus - The focus of the route
72
72
  */
73
73
 
74
- /** @type {Map<String, CacheMapEntry>} */
75
- const cacheMap = new Map()
76
74
  const history = []
77
75
 
78
76
  let overrideOptions = {}
79
77
  let navigationData = {}
80
78
  let navigatingBack = false
79
+ let navigatingBackTo = undefined
81
80
  let previousFocus
82
81
 
83
82
  /**
84
83
  * Get the current hash
85
84
  * @returns {Hash}
86
85
  */
87
- export const getHash = () => {
88
- const hashParts = (document.location.hash || '/').replace(/^#/, '').split('?')
86
+ export const getHash = (hash) => {
87
+ if (!hash) hash = '/'
88
+ const hashParts = hash.replace(/^#/, '').split('?')
89
89
  return {
90
90
  path: hashParts[0],
91
91
  queryParams: new URLSearchParams(hashParts[1]),
92
- hash: document.location.hash,
92
+ hash: hash,
93
93
  }
94
94
  }
95
95
 
@@ -116,33 +116,50 @@ const isObject = (v) => typeof v === 'object' && v !== null
116
116
  */
117
117
  const isString = (v) => typeof v === 'string'
118
118
 
119
+ const queryParamsToObject = (queryParams) => {
120
+ if (!queryParams) return {}
121
+ const object = {}
122
+ const queryParamsEntries = [...queryParams.entries()]
123
+ for (let i = 0; i < queryParamsEntries.length; i++) {
124
+ object[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
125
+ }
126
+
127
+ return object
128
+ }
129
+
119
130
  /**
120
131
  * Match a path to a route
121
132
  *
122
- * @param {string} path
133
+ * @param {object} hashObject
123
134
  * @param {Route[]} routes
124
135
  * @returns {Route}
125
136
  */
126
- export const matchHash = (path, routes = []) => {
137
+ export const matchHash = ({ hash, path, queryParams }, routes = []) => {
127
138
  // remove trailing slashes
128
139
  const originalPath = path.replace(/^\/+|\/+$/g, '')
129
- path = normalizePath(path)
140
+ const originalNormalizedPath = normalizePath(path)
141
+
142
+ const override = {
143
+ hash: hash,
144
+ queryParams: queryParamsToObject(queryParams),
145
+ path: path,
146
+ }
130
147
 
131
148
  /** @type {boolean|Route} */
132
149
  let matchingRoute = false
133
150
  let i = 0
134
151
  while (!matchingRoute && i < routes.length) {
135
152
  const route = routes[i]
136
- route.path = normalizePath(route.path)
137
- if (route.path === path) {
138
- route.params = {}
139
- matchingRoute = route
140
- } else if (route.path.indexOf(':') > -1) {
153
+
154
+ const normalizedPath = normalizePath(route.path)
155
+ if (normalizePath(normalizedPath) === originalNormalizedPath) {
156
+ matchingRoute = makeRouteObject(route, override)
157
+ } else if (normalizedPath.indexOf(':') > -1) {
141
158
  // match dynamic route parts
142
- const dynamicRouteParts = [...route.path.matchAll(/:([^\s/]+)/gi)]
159
+ const dynamicRouteParts = [...normalizedPath.matchAll(/:([^\s/]+)/gi)]
143
160
 
144
161
  // construct a regex for the route with dynamic parts
145
- let dynamicRoutePartsRegex = route.path
162
+ let dynamicRoutePartsRegex = normalizedPath
146
163
  dynamicRouteParts.reverse().forEach((part) => {
147
164
  dynamicRoutePartsRegex =
148
165
  dynamicRoutePartsRegex.substring(0, part.index) +
@@ -157,31 +174,26 @@ export const matchHash = (path, routes = []) => {
157
174
 
158
175
  if (match) {
159
176
  // map the route params to a params object
160
- route.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
177
+ override.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
161
178
  acc[part[1]] = match[index + 1]
162
179
  return acc
163
180
  }, {})
164
- matchingRoute = route
181
+
182
+ matchingRoute = makeRouteObject(route, override)
165
183
  }
166
- } else if (route.path.endsWith('*')) {
167
- const regex = new RegExp(route.path.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
168
- const match = path.match(regex)
184
+ } else if (normalizedPath.endsWith('*')) {
185
+ const regex = new RegExp(normalizedPath.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
186
+ const match = originalNormalizedPath.match(regex)
169
187
 
170
188
  if (match) {
171
- if (match[1]) route.params = { path: match[1] }
172
- matchingRoute = route
189
+ override.params = {}
190
+ if (match[1]) override.params.path = match[1]
191
+ matchingRoute = makeRouteObject(route, override)
173
192
  }
174
193
  }
175
194
  i++
176
195
  }
177
196
 
178
- if (matchingRoute) {
179
- matchingRoute.options = { ...defaultOptions, ...matchingRoute.options, ...overrideOptions }
180
- if (!matchingRoute.data) {
181
- matchingRoute.data = {}
182
- }
183
- }
184
-
185
197
  // @ts-ignore - Remove me when we have a better way to handle this
186
198
  return matchingRoute
187
199
  }
@@ -197,6 +209,22 @@ const defaultOptions = {
197
209
  reuseComponent: false,
198
210
  }
199
211
 
212
+ const makeRouteObject = (route, overrides) => {
213
+ const cleanRoute = {
214
+ hash: overrides.hash,
215
+ path: route.path,
216
+ component: route.component,
217
+ transition: 'transition' in route ? route.transition : fadeInFadeOutTransition,
218
+ options: { ...defaultOptions, ...route.options, ...overrideOptions },
219
+ announce: route.announce || false,
220
+ hooks: route.hooks || {},
221
+ data: { ...route.data, ...navigationData, ...overrides.queryParams },
222
+ params: overrides.params || {},
223
+ }
224
+
225
+ return cleanRoute
226
+ }
227
+
200
228
  /**
201
229
  * Navigate to a route
202
230
  *
@@ -216,28 +244,11 @@ export const navigate = async function () {
216
244
  let reuse = false
217
245
  if (this.parent[symbols.routes]) {
218
246
  let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
219
- const { hash, path, queryParams } = getHash()
220
- let route = matchHash(path, this.parent[symbols.routes])
247
+ let route = matchHash(getHash(document.location.hash), this.parent[symbols.routes])
221
248
 
222
249
  currentRoute = route
223
250
 
224
251
  if (route) {
225
- const queryParamsData = {}
226
- const queryParamsEntries = [...queryParams.entries()]
227
- for (let i = 0; i < queryParamsEntries.length; i++) {
228
- queryParamsData[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
229
- }
230
-
231
- route.data = {
232
- ...route.data,
233
- ...navigationData,
234
- ...queryParamsData,
235
- }
236
-
237
- // Adding the location hash to the route if it exists.
238
- if (hash !== null) {
239
- route.hash = hash
240
- }
241
252
  let beforeEachResult
242
253
  if (this.parent[symbols.routerHooks]) {
243
254
  const hooks = this.parent[symbols.routerHooks]
@@ -250,30 +261,33 @@ export const navigate = async function () {
250
261
  }
251
262
  }
252
263
  // If the resolved result is an object, assign it to the target route object
253
- route = isObject(beforeEachResult) ? beforeEachResult : route
264
+ if (isObject(beforeEachResult) === true) {
265
+ route = beforeEachResult
266
+ }
254
267
 
255
268
  let beforeHookOutput
256
- if (route.hooks) {
257
- if (route.hooks.before) {
258
- beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
259
- if (isString(beforeHookOutput)) {
260
- currentRoute = previousRoute
261
- to(beforeHookOutput)
262
- return
263
- }
269
+ if (route.hooks.before) {
270
+ beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
271
+ if (isString(beforeHookOutput)) {
272
+ currentRoute = previousRoute
273
+ to(beforeHookOutput)
274
+ return
264
275
  }
265
276
  }
266
- route = isObject(beforeHookOutput) ? beforeHookOutput : route
277
+ // If the resolved result is an object, assign it to the target route object
278
+ if (isObject(beforeHookOutput) === true) {
279
+ route = beforeHookOutput
280
+ }
267
281
  // add the previous route (technically still the current route at this point)
268
- // into the history stack, unless navigating back or inHistory flag of route is false
269
- if (navigatingBack === false && previousRoute && previousRoute.options.inHistory === true) {
282
+ // into the history stack when inHistory is true and we're not navigating back
283
+ if (
284
+ previousRoute !== undefined &&
285
+ previousRoute.options.inHistory === true &&
286
+ navigatingBack === false
287
+ ) {
270
288
  history.push(previousRoute)
271
289
  }
272
- // apply default transition if none specified
273
- if (!('transition' in route)) {
274
- /** @ts-ignore */
275
- route.transition = fadeInFadeOutTransition
276
- }
290
+
277
291
  // a transition can be a function returning a dynamic transition object
278
292
  // based on current and previous route
279
293
  if (typeof route.transition === 'function') {
@@ -283,8 +297,14 @@ export const navigate = async function () {
283
297
  /** @type {import('../engines/L3/element.js').BlitsElement} */
284
298
  let holder
285
299
 
286
- let { view, focus } = cacheMap.get(route.hash) || {}
287
-
300
+ let view
301
+ let focus
302
+ // when navigating back let's see if we're navigating back to a route that was kept alive
303
+ if (navigatingBack === true && navigatingBackTo !== undefined) {
304
+ view = navigatingBackTo.view
305
+ focus = navigatingBackTo.focus
306
+ navigatingBackTo = null
307
+ }
288
308
  // merge props with potential route params, navigation data and route data to be injected into the component instance
289
309
  const props = {
290
310
  ...this[symbols.props],
@@ -292,11 +312,13 @@ export const navigate = async function () {
292
312
  ...route.data,
293
313
  }
294
314
 
315
+ // see if the component of the previous route can be reused for the
316
+ // current route
295
317
  if (
296
318
  previousRoute &&
297
- route.component === previousRoute.component &&
298
319
  route.options.reuseComponent === true &&
299
- route.options.keepAlive !== true
320
+ route.options.keepAlive !== true &&
321
+ route.component === previousRoute.component
300
322
  ) {
301
323
  reuse = true
302
324
  view = this[symbols.children][this[symbols.children].length - 1]
@@ -320,6 +342,7 @@ export const navigate = async function () {
320
342
  state.path = route.path
321
343
  state.params = route.params || {}
322
344
  state.hash = route.hash
345
+ state.data = null
323
346
  state.data = route.data || {}
324
347
 
325
348
  if (!view) {
@@ -331,6 +354,7 @@ export const navigate = async function () {
331
354
 
332
355
  view = await route.component({ props }, holder, this)
333
356
 
357
+ // is the component a dynamic module?
334
358
  if (view[Symbol.toStringTag] === 'Module') {
335
359
  if (view.default && typeof view.default === 'function') {
336
360
  view = view.default({ props }, holder, this)
@@ -338,6 +362,7 @@ export const navigate = async function () {
338
362
  Log.error("Dynamic import doesn't have a default export or default is not a function")
339
363
  }
340
364
  }
365
+
341
366
  if (typeof view === 'function') {
342
367
  // had to inline this because the tscompiler does not like LHS reassignments
343
368
  // that also change the type of the variable in a variable union
@@ -366,6 +391,7 @@ export const navigate = async function () {
366
391
  }
367
392
  }
368
393
 
394
+ // store the new view as new child, only if we're not reusing the previous page component
369
395
  if (reuse === false) {
370
396
  this[symbols.children].push(view)
371
397
  }
@@ -377,7 +403,7 @@ export const navigate = async function () {
377
403
  this.activeView = children[children.length - 1]
378
404
 
379
405
  // set focus to the view that we're routing to (unless explicitly disabling passing focus)
380
- if (route.options === undefined || route.options.passFocus !== false) {
406
+ if (route.options.passFocus !== false) {
381
407
  focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
382
408
  }
383
409
 
@@ -394,8 +420,9 @@ export const navigate = async function () {
394
420
 
395
421
  let shouldAnimate = false
396
422
 
397
- // apply out out transition on previous view
398
- if (previousRoute && reuse === false) {
423
+ // apply out out transition on previous view if available, unless
424
+ // we're reusing the prvious page component
425
+ if (previousRoute !== undefined && reuse === false) {
399
426
  // only animate when there is a previous route
400
427
  shouldAnimate = true
401
428
  const oldView = this[symbols.children].splice(1, 1).pop()
@@ -417,10 +444,10 @@ export const navigate = async function () {
417
444
  }
418
445
  }
419
446
  } else {
420
- Log.error(`Route ${hash} not found`)
447
+ Log.error(`Route ${route.hash} not found`)
421
448
  const routerHooks = this.parent[symbols.routerHooks]
422
449
  if (routerHooks && typeof routerHooks.error === 'function') {
423
- routerHooks.error.call(this.parent, `Route ${hash} not found`)
450
+ routerHooks.error.call(this.parent, `Route ${route.hash} not found`)
424
451
  }
425
452
  }
426
453
  }
@@ -452,13 +479,19 @@ const removeView = async (route, view, transition, navigatingBack) => {
452
479
  }
453
480
 
454
481
  // cache the page when it's as 'keepAlive' instead of destroying
455
- if (navigatingBack === false && route.options && route.options.keepAlive === true) {
456
- cacheMap.set(route.hash, { view: view, focus: previousFocus })
457
- } else if (navigatingBack === true) {
458
- // remove the previous route from the cache when navigating back
459
- // cacheMap.delete will not throw an error if the route is not in the cache
460
- cacheMap.delete(route.hash)
482
+ if (
483
+ navigatingBack === false &&
484
+ route.options &&
485
+ route.options.keepAlive === true &&
486
+ route.options.inHistory === true
487
+ ) {
488
+ const historyItem = history[history.length - 1]
489
+ if (historyItem !== undefined) {
490
+ historyItem.view = view
491
+ historyItem.focus = previousFocus
492
+ }
461
493
  }
494
+
462
495
  /* Destroy the view in the following cases:
463
496
  * 1. Navigating forward, and the previous route is not configured with "keep alive" set to true.
464
497
  * 2. Navigating back, and the previous route is configured with "keep alive" set to true.
@@ -470,6 +503,7 @@ const removeView = async (route, view, transition, navigatingBack) => {
470
503
  }
471
504
 
472
505
  previousFocus = null
506
+ route = null
473
507
  }
474
508
 
475
509
  const setOrAnimate = (node, transition, shouldAnimate = true) => {
@@ -505,7 +539,7 @@ export const back = function () {
505
539
  if (route && currentRoute !== route) {
506
540
  // set indicator that we are navigating back (to prevent adding page to history stack)
507
541
  navigatingBack = true
508
-
542
+ navigatingBackTo = route
509
543
  to(route.hash, route.data, route.options)
510
544
  return true
511
545
  }
@@ -534,10 +568,10 @@ export const back = function () {
534
568
  }
535
569
  // Construct new path to backtrack to
536
570
  path = path.replace(hashEnd, '')
537
- const route = matchHash(path, this.parent[symbols.routes])
571
+ const route = matchHash(getHash(path), this.parent[symbols.routes])
538
572
 
539
573
  if (route && backtrack) {
540
- to(route.path)
574
+ to(route.path, route.data, route.options)
541
575
  return true
542
576
  }
543
577
  }