@lightningjs/blits 2.14.0 → 2.16.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 +27 -0
- package/eslint.config.cjs +2 -0
- package/index.d.ts +1510 -1481
- package/package.json +6 -6
- package/src/application.js +98 -26
- package/src/component/base/index.js +2 -0
- package/src/component/base/methods.js +1 -1
- package/src/component/base/router.js +6 -2
- package/src/component/base/settings.js +68 -0
- package/src/component/setup/computed.js +5 -1
- package/src/component.js +23 -7
- package/src/components/Circle.js +2 -1
- package/src/components/FPScounter.js +32 -21
- package/src/components/RouterView.js +2 -1
- package/src/engines/L3/element.js +1 -1
- package/src/engines/L3/index.js +39 -1
- package/src/engines/L3/launch.js +1 -15
- package/src/focus/focus.js +4 -0
- package/src/keymap.js +27 -0
- package/src/launch.js +18 -20
- package/src/lib/codegenerator/generator.js +25 -2
- package/src/lib/hooks.js +4 -0
- package/src/lib/precompiler/precompiler.js +26 -1
- package/src/lib/reactivity/effect.js +140 -14
- package/src/lib/reactivity/reactive.js +11 -4
- package/src/router/router.js +20 -3
- package/src/settings.js +5 -0
- package/vite/index.js +3 -0
- package/vite/injectVersions.js +58 -0
package/src/engines/L3/launch.js
CHANGED
|
@@ -20,7 +20,6 @@ import { WebGlCoreRenderer, SdfTextRenderer } from '@lightningjs/renderer/webgl'
|
|
|
20
20
|
import { CanvasCoreRenderer, CanvasTextRenderer } from '@lightningjs/renderer/canvas'
|
|
21
21
|
import { Inspector } from '@lightningjs/renderer/inspector'
|
|
22
22
|
|
|
23
|
-
import { Log } from '../../lib/log.js'
|
|
24
23
|
import { SCREEN_RESOLUTIONS, RENDER_QUALITIES } from '../../constants.js'
|
|
25
24
|
import colors from '../../lib/colors/colors.js'
|
|
26
25
|
import fontLoader from './fontLoader.js'
|
|
@@ -85,7 +84,7 @@ const textureMemorySettings = (settings) => {
|
|
|
85
84
|
* @param {Partial<import('../../launch.js').BlitsSettings>} [settings] - The settings for the renderer
|
|
86
85
|
*
|
|
87
86
|
*/
|
|
88
|
-
export default (
|
|
87
|
+
export default (target, settings = {}) => {
|
|
89
88
|
const screenHeight = settings.platform ? settings.platform.screenHeight : platform.screenHeight
|
|
90
89
|
const hardwareConcurrency = settings.platform
|
|
91
90
|
? settings.platform.hardwareConcurrency
|
|
@@ -125,21 +124,8 @@ export default (App, target, settings = {}) => {
|
|
|
125
124
|
target
|
|
126
125
|
)
|
|
127
126
|
|
|
128
|
-
const initApp = () => {
|
|
129
|
-
let app = App()
|
|
130
|
-
app.quit = () => {
|
|
131
|
-
Log.info('Closing App')
|
|
132
|
-
app.destroy()
|
|
133
|
-
app = null
|
|
134
|
-
renderer = null
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
127
|
shaderLoader()
|
|
139
128
|
fontLoader()
|
|
140
|
-
// Defer app initialization until after the current call stack is cleared,
|
|
141
|
-
// allowing the renderer to finish setting up before the app starts creating components
|
|
142
|
-
Promise.resolve().then(initApp)
|
|
143
129
|
|
|
144
130
|
return renderer
|
|
145
131
|
}
|
package/src/focus/focus.js
CHANGED
|
@@ -68,6 +68,10 @@ export default {
|
|
|
68
68
|
while (i--) {
|
|
69
69
|
// don't unfocus when part of the new focus chain
|
|
70
70
|
if (newFocusChain.indexOf(focusChain[i]) > -1) break
|
|
71
|
+
// only unfocus components that actually have focus
|
|
72
|
+
// ($hasFocus, not the lifecycle state which is shared with hover/enter/exit)
|
|
73
|
+
const componentState = focusChain[i][symbols.state]
|
|
74
|
+
if (componentState !== undefined && componentState.$hasFocus === false) continue
|
|
71
75
|
focusChain[i][symbols.lifecycle].state = 'unfocus'
|
|
72
76
|
}
|
|
73
77
|
focusChain = newFocusChain
|
package/src/keymap.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 Comcast Cable Communications Management, LLC
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*
|
|
15
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Merged keyMap (default + custom settings).
|
|
20
|
+
* Updated during application initialization and runtime settings changes.
|
|
21
|
+
* @type {Object<string, string>}
|
|
22
|
+
*/
|
|
23
|
+
export let keyMap = {}
|
|
24
|
+
|
|
25
|
+
export const setKeyMap = (value) => {
|
|
26
|
+
keyMap = value
|
|
27
|
+
}
|
package/src/launch.js
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import Settings from './settings.js'
|
|
19
19
|
import { initLog, Log } from './lib/log.js'
|
|
20
20
|
import engine from './engine.js'
|
|
21
|
-
import blitsPackageInfo from '../package.json' with { type: 'json' }
|
|
22
21
|
import { configurePlatform } from './platform.js'
|
|
23
22
|
import profiler from './lib/profiler.js'
|
|
24
23
|
|
|
@@ -52,20 +51,6 @@ const isDev = typeof __DEV__ !== 'undefined' && __DEV__
|
|
|
52
51
|
export let renderer = {}
|
|
53
52
|
export const stage = {}
|
|
54
53
|
|
|
55
|
-
async function rendererVersion() {
|
|
56
|
-
let rendererPackageInfo
|
|
57
|
-
try {
|
|
58
|
-
// Dynamically import the renderer package.json
|
|
59
|
-
rendererPackageInfo = await import('../../renderer/package.json')
|
|
60
|
-
if (rendererPackageInfo !== undefined) {
|
|
61
|
-
return rendererPackageInfo.version
|
|
62
|
-
}
|
|
63
|
-
} catch (e) {
|
|
64
|
-
// Fallback to renderer version in dependencies
|
|
65
|
-
return blitsPackageInfo.dependencies['@lightningjs/renderer']
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
54
|
/**
|
|
70
55
|
* @typedef {Object} BlitsSettings
|
|
71
56
|
* @property {number} [w] - Width of the Application
|
|
@@ -148,15 +133,28 @@ export default (App, target, settings) => {
|
|
|
148
133
|
)
|
|
149
134
|
}
|
|
150
135
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
Log.info(
|
|
137
|
+
'Blits version:',
|
|
138
|
+
typeof __BLITS_VERSION__ === 'undefined' ? 'unknown' : __BLITS_VERSION__
|
|
139
|
+
)
|
|
140
|
+
Log.info(
|
|
141
|
+
'Renderer version:',
|
|
142
|
+
typeof __RENDERER_VERSION__ === 'undefined' ? 'unknown' : __RENDERER_VERSION__
|
|
143
|
+
)
|
|
155
144
|
|
|
156
145
|
stage.element = engine.Element
|
|
157
146
|
|
|
158
|
-
renderer = engine.Launch(
|
|
147
|
+
renderer = engine.Launch(target, settings)
|
|
159
148
|
if (isDev === true) {
|
|
160
149
|
profiler.setRenderer(renderer)
|
|
161
150
|
}
|
|
151
|
+
|
|
152
|
+
// Instantiate the app and wire up the quit function
|
|
153
|
+
let app = App()
|
|
154
|
+
app.quit = () => {
|
|
155
|
+
Log.info('Closing App')
|
|
156
|
+
app.destroy()
|
|
157
|
+
app = null
|
|
158
|
+
renderer = null
|
|
159
|
+
}
|
|
162
160
|
}
|
|
@@ -87,6 +87,8 @@ export default function (templateObject = { children: [] }, devMode = false) {
|
|
|
87
87
|
}, skips}
|
|
88
88
|
`)
|
|
89
89
|
|
|
90
|
+
const renderCodeString = ctx.renderCode.join('\n')
|
|
91
|
+
|
|
90
92
|
return {
|
|
91
93
|
render: new Function(
|
|
92
94
|
'parent',
|
|
@@ -96,7 +98,7 @@ export default function (templateObject = { children: [] }, devMode = false) {
|
|
|
96
98
|
'effect',
|
|
97
99
|
'getRaw',
|
|
98
100
|
'Log',
|
|
99
|
-
|
|
101
|
+
renderCodeString
|
|
100
102
|
),
|
|
101
103
|
effects: ctx.effectsCode.map(
|
|
102
104
|
(code) =>
|
|
@@ -112,6 +114,7 @@ export default function (templateObject = { children: [] }, devMode = false) {
|
|
|
112
114
|
)
|
|
113
115
|
),
|
|
114
116
|
context: ctx.context,
|
|
117
|
+
usesHoverState: renderCodeString.includes('.$isHovered'),
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -212,6 +215,7 @@ const generateElementCode = function (
|
|
|
212
215
|
|
|
213
216
|
if (options.holder) {
|
|
214
217
|
renderCode.push(`elementConfigs[${counter}]['holder'] = true`)
|
|
218
|
+
renderCode.push(`elementConfigs[${counter}]['interactive'] = cmps[${counter}].needsInteractive`)
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
Object.keys(templateObject).forEach((key) => {
|
|
@@ -342,12 +346,31 @@ const generateComponentCode = function (
|
|
|
342
346
|
) {
|
|
343
347
|
const renderCode = options.forceEffect ? this.effectsCode : this.renderCode
|
|
344
348
|
|
|
345
|
-
|
|
349
|
+
if ('is' in templateObject || ':is' in templateObject) {
|
|
350
|
+
const selectedComponent =
|
|
351
|
+
':is' in templateObject
|
|
352
|
+
? interpolate(templateObject[':is'], options.component)
|
|
353
|
+
: cast(templateObject.is, 'is', options.component)
|
|
354
|
+
// Resolve the selected factory before populating its holder and filtering props.
|
|
355
|
+
renderCode.push(`
|
|
356
|
+
componentType = ${selectedComponent} || '${templateObject[Symbol.for('componentType')]}'
|
|
357
|
+
cmps[${counter}] = typeof componentType === 'function'
|
|
358
|
+
? componentType
|
|
359
|
+
: (context.components && context.components[componentType]) || components[componentType]
|
|
360
|
+
if (!cmps[${counter}]) {
|
|
361
|
+
throw new Error('Component "${templateObject[Symbol.for('componentType')]}" not found')
|
|
362
|
+
}
|
|
363
|
+
`)
|
|
364
|
+
} else {
|
|
365
|
+
renderCode.push(`
|
|
346
366
|
cmps[${counter}] =
|
|
347
367
|
(context.components && context.components['${
|
|
348
368
|
templateObject[Symbol.for('componentType')]
|
|
349
369
|
}']) || components['${templateObject[Symbol.for('componentType')]}']
|
|
350
370
|
`)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
renderCode.push(`if (cmps[${counter}].prepare !== undefined) cmps[${counter}].prepare()`)
|
|
351
374
|
|
|
352
375
|
this.cleanupCode.push(`
|
|
353
376
|
cmps[${counter}] = null
|
package/src/lib/hooks.js
CHANGED
|
@@ -57,3 +57,7 @@ export const registerHooks = (hooks = {}, identifier) => {
|
|
|
57
57
|
if (typeof hooks[hook] === 'function') cbs[identifier][hook] = hooks[hook]
|
|
58
58
|
})
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
export const hasHook = (hook, identifier) => {
|
|
62
|
+
return cbs[identifier] !== undefined && hook in cbs[identifier]
|
|
63
|
+
}
|
|
@@ -31,6 +31,11 @@ export default (source, filePath, mode) => {
|
|
|
31
31
|
// Use MagicString to track changes
|
|
32
32
|
const s = new MagicString(source)
|
|
33
33
|
|
|
34
|
+
// Find component definition start positions for scoping JS-layer checks
|
|
35
|
+
const componentStarts = [...source.matchAll(/Blits\.(Component|Application)\s*\(/g)]
|
|
36
|
+
.map((m) => m.index)
|
|
37
|
+
.sort((a, b) => a - b)
|
|
38
|
+
|
|
34
39
|
for (const template of templates) {
|
|
35
40
|
if (template[2]) {
|
|
36
41
|
const templateContent = template[2]
|
|
@@ -52,10 +57,30 @@ export default (source, filePath, mode) => {
|
|
|
52
57
|
// Generate the code
|
|
53
58
|
const code = generator.call({ components: {} }, parsed, mode === 'development')
|
|
54
59
|
|
|
60
|
+
// Determine the component scope containing this template
|
|
61
|
+
let scopeStart = 0
|
|
62
|
+
let scopeEnd = source.length
|
|
63
|
+
for (let i = 0; i < componentStarts.length; i++) {
|
|
64
|
+
if (componentStarts[i] <= templateStartIndex) {
|
|
65
|
+
scopeStart = componentStarts[i]
|
|
66
|
+
scopeEnd = i + 1 < componentStarts.length ? componentStarts[i + 1] : source.length
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const componentScope = source.slice(scopeStart, scopeEnd)
|
|
70
|
+
|
|
71
|
+
// Derive usesHoverState from both template ($$isHovered) and JS layer (this.$isHovered)
|
|
72
|
+
// Match strings first so URL/comment delimiters inside literals stay intact.
|
|
73
|
+
// Keep template literals too: their interpolations may read hover state.
|
|
74
|
+
const strippedScope = componentScope.replace(
|
|
75
|
+
/("(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`)|\/\/[^\r\n]*|\/\*[\s\S]*?\*\//g,
|
|
76
|
+
(match, literal) => literal || ' '
|
|
77
|
+
)
|
|
78
|
+
const usesHoverState = code.usesHoverState || strippedScope.includes('this.$isHovered')
|
|
79
|
+
|
|
55
80
|
// Insert the code in the component using the 'code' key, replacing the template key
|
|
56
81
|
const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map(
|
|
57
82
|
(fn) => fn.toString()
|
|
58
|
-
)}], context: {}}`
|
|
83
|
+
)}], context: {}, usesHoverState: ${usesHoverState}}`
|
|
59
84
|
|
|
60
85
|
// MagicString tracks all changes automatically, no offset needed
|
|
61
86
|
s.overwrite(templateStartIndex, templateEndIndex, replacement)
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import symbols from '../symbols.js'
|
|
19
|
+
|
|
18
20
|
let currentEffect = null
|
|
19
21
|
let currentKey = null
|
|
22
|
+
// Write position for dependency-array reuse, restored after nested computed getters.
|
|
23
|
+
let computedDependencyIndex = 0
|
|
20
24
|
|
|
21
25
|
// counter that tracks all pauseTracking invocations
|
|
22
26
|
// to ensure nested array operations don't prematurely
|
|
@@ -33,20 +37,98 @@ export const resumeTracking = () => {
|
|
|
33
37
|
|
|
34
38
|
const objectMap = new WeakMap()
|
|
35
39
|
const effectDependenciesMap = new WeakMap()
|
|
40
|
+
const computedEffects = new WeakMap()
|
|
41
|
+
const effectQueue = new Set()
|
|
42
|
+
let pendingEffects = null
|
|
43
|
+
|
|
44
|
+
const computedRunner = (effectFn, key) => {
|
|
45
|
+
if (effectFn.computed === true) return effectFn
|
|
46
|
+
let runner = computedEffects.get(effectFn)
|
|
47
|
+
if (runner === undefined) {
|
|
48
|
+
runner = (force) => effect(effectFn, key, force)
|
|
49
|
+
computedEffects.set(effectFn, runner)
|
|
50
|
+
}
|
|
51
|
+
return runner
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const createComputed = (getter, component, key) => {
|
|
55
|
+
let value
|
|
56
|
+
let status = 'dirty'
|
|
57
|
+
|
|
58
|
+
const invalidate = (force) => {
|
|
59
|
+
if (status === 'dirty' || status === 'disposed') return
|
|
60
|
+
status = 'dirty'
|
|
61
|
+
trigger(component, key, force)
|
|
62
|
+
}
|
|
63
|
+
invalidate.computed = true
|
|
64
|
+
invalidate.dispose = () => {
|
|
65
|
+
value = undefined
|
|
66
|
+
status = 'disposed'
|
|
67
|
+
}
|
|
68
|
+
if (component[symbols.effects] !== undefined) component[symbols.effects].push(invalidate)
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
if (status === 'disposed') return undefined
|
|
72
|
+
|
|
73
|
+
if (currentEffect !== null) track(component, key, true)
|
|
74
|
+
if (status === 'clean') return value
|
|
75
|
+
|
|
76
|
+
const previousEffect = currentEffect
|
|
77
|
+
const previousKey = currentKey
|
|
78
|
+
const previousPauseCounter = pauseCounter
|
|
79
|
+
const previousDependencyIndex = computedDependencyIndex
|
|
80
|
+
computedDependencyIndex = 0
|
|
81
|
+
try {
|
|
82
|
+
// Refresh dependencies in isolation, including reads made outside effects.
|
|
83
|
+
clearDependencies(invalidate)
|
|
84
|
+
currentEffect = invalidate
|
|
85
|
+
currentKey = null
|
|
86
|
+
pauseCounter = 0
|
|
87
|
+
value = getter.call(component)
|
|
88
|
+
status = 'clean'
|
|
89
|
+
return value
|
|
90
|
+
} catch (error) {
|
|
91
|
+
status = 'failed'
|
|
92
|
+
throw error
|
|
93
|
+
} finally {
|
|
94
|
+
const dependencies = effectDependenciesMap.get(invalidate)
|
|
95
|
+
if (dependencies !== undefined) dependencies.length = computedDependencyIndex
|
|
96
|
+
computedDependencyIndex = previousDependencyIndex
|
|
97
|
+
currentEffect = previousEffect
|
|
98
|
+
currentKey = previousKey
|
|
99
|
+
pauseCounter = previousPauseCounter
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const clearDependencies = (effect, releaseStorage) => {
|
|
105
|
+
const dependencies = effectDependenciesMap.get(effect)
|
|
106
|
+
if (dependencies === undefined) return
|
|
107
|
+
for (let i = 0; i < dependencies.length; i++) {
|
|
108
|
+
const effects = dependencies[i]
|
|
109
|
+
effects.delete(effect)
|
|
110
|
+
if (effect.computed === true) effects.computedCount--
|
|
111
|
+
}
|
|
112
|
+
if (releaseStorage === true) dependencies.length = 0
|
|
113
|
+
}
|
|
36
114
|
|
|
37
115
|
export const removeEffects = (effectsToRemove) => {
|
|
38
116
|
for (let i = 0; i < effectsToRemove.length; i++) {
|
|
39
117
|
const effect = effectsToRemove[i]
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
const computedEffect = computedEffects.get(effect)
|
|
119
|
+
if (computedEffect !== undefined) {
|
|
120
|
+
// Computed dependencies subscribe the runner itself, so unsubscribe it separately.
|
|
121
|
+
clearDependencies(computedEffect, true)
|
|
122
|
+
effectDependenciesMap.delete(computedEffect)
|
|
123
|
+
computedEffects.delete(effect)
|
|
44
124
|
}
|
|
125
|
+
clearDependencies(effect, true)
|
|
45
126
|
effectDependenciesMap.delete(effect)
|
|
127
|
+
if (effect.computed === true) effect.dispose()
|
|
46
128
|
}
|
|
47
129
|
}
|
|
48
130
|
|
|
49
|
-
export const track = (target, key) => {
|
|
131
|
+
export const track = (target, key, computed) => {
|
|
50
132
|
if (currentEffect !== null) {
|
|
51
133
|
if (pauseCounter > 0) {
|
|
52
134
|
return
|
|
@@ -56,6 +138,7 @@ export const track = (target, key) => {
|
|
|
56
138
|
if (currentKey.includes(key) === false) return
|
|
57
139
|
} else if (currentKey !== null && key !== currentKey) return
|
|
58
140
|
|
|
141
|
+
const subscriber = computed === true ? computedRunner(currentEffect, currentKey) : currentEffect
|
|
59
142
|
let effectsMap = objectMap.get(target)
|
|
60
143
|
if (effectsMap === undefined) {
|
|
61
144
|
effectsMap = new Map()
|
|
@@ -66,14 +149,19 @@ export const track = (target, key) => {
|
|
|
66
149
|
effects = new Set()
|
|
67
150
|
effectsMap.set(key, effects)
|
|
68
151
|
}
|
|
69
|
-
if (effects.has(
|
|
70
|
-
let dependencies = effectDependenciesMap.get(
|
|
152
|
+
if (effects.has(subscriber) === false) {
|
|
153
|
+
let dependencies = effectDependenciesMap.get(subscriber)
|
|
71
154
|
if (dependencies === undefined) {
|
|
72
155
|
dependencies = []
|
|
73
|
-
effectDependenciesMap.set(
|
|
156
|
+
effectDependenciesMap.set(subscriber, dependencies)
|
|
157
|
+
}
|
|
158
|
+
effects.add(subscriber)
|
|
159
|
+
if (subscriber.computed === true) {
|
|
160
|
+
dependencies[computedDependencyIndex++] = effects
|
|
161
|
+
effects.computedCount = effects.computedCount === undefined ? 1 : effects.computedCount + 1
|
|
162
|
+
} else {
|
|
163
|
+
dependencies.push(effects)
|
|
74
164
|
}
|
|
75
|
-
dependencies.push(effects)
|
|
76
|
-
effects.add(currentEffect)
|
|
77
165
|
}
|
|
78
166
|
}
|
|
79
167
|
}
|
|
@@ -85,14 +173,52 @@ export const trigger = (target, key, force = false) => {
|
|
|
85
173
|
return
|
|
86
174
|
}
|
|
87
175
|
const effects = effectsMap.get(key)
|
|
176
|
+
// Ordinary state updates keep their direct path.
|
|
177
|
+
if (
|
|
178
|
+
effects !== undefined &&
|
|
179
|
+
pendingEffects === null &&
|
|
180
|
+
(effects.computedCount === undefined || effects.computedCount === 0)
|
|
181
|
+
) {
|
|
182
|
+
for (const effect of effects) {
|
|
183
|
+
if (effect.computed !== true) effect(force)
|
|
184
|
+
}
|
|
185
|
+
return
|
|
186
|
+
}
|
|
88
187
|
if (effects !== undefined) {
|
|
89
|
-
|
|
90
|
-
|
|
188
|
+
const parentQueue = pendingEffects
|
|
189
|
+
// Reuse the root queue; only nested updates with pending consumers need another set.
|
|
190
|
+
let queue = parentQueue
|
|
191
|
+
if (queue === null) {
|
|
192
|
+
queue = effectQueue.size === 0 ? effectQueue : new Set()
|
|
193
|
+
// All consumers share the source mutation's force flag.
|
|
194
|
+
queue.force = force
|
|
195
|
+
}
|
|
196
|
+
pendingEffects = queue
|
|
197
|
+
try {
|
|
198
|
+
for (const effect of effects) {
|
|
199
|
+
if (effect.computed === true) {
|
|
200
|
+
effect(force)
|
|
201
|
+
} else {
|
|
202
|
+
const runner = computedEffects.get(effect)
|
|
203
|
+
queue.add(runner === undefined ? effect : runner)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Invalidate the whole computed chain before any consumer can read its cache.
|
|
207
|
+
if (parentQueue === null) {
|
|
208
|
+
pendingEffects = null
|
|
209
|
+
for (const effect of queue) {
|
|
210
|
+
queue.delete(effect)
|
|
211
|
+
if (effectDependenciesMap.has(effect) === true) effect(queue.force)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
pendingEffects = parentQueue
|
|
216
|
+
if (parentQueue === null) queue.clear()
|
|
91
217
|
}
|
|
92
218
|
}
|
|
93
219
|
}
|
|
94
220
|
|
|
95
|
-
export const effect = (effectFn, key = null) => {
|
|
221
|
+
export const effect = (effectFn, key = null, force) => {
|
|
96
222
|
const previousEffect = currentEffect
|
|
97
223
|
const previousKey = currentKey
|
|
98
224
|
|
|
@@ -100,7 +226,7 @@ export const effect = (effectFn, key = null) => {
|
|
|
100
226
|
currentKey = key
|
|
101
227
|
|
|
102
228
|
try {
|
|
103
|
-
effectFn()
|
|
229
|
+
effectFn(force)
|
|
104
230
|
} finally {
|
|
105
231
|
currentEffect = previousEffect
|
|
106
232
|
currentKey = previousKey
|
|
@@ -100,9 +100,7 @@ const reactiveProxy = (original, _parent = null, _key) => {
|
|
|
100
100
|
|
|
101
101
|
// handling objects (but not null values, which have object type in JS)
|
|
102
102
|
if (typeof target[key] === 'object' && target[key] !== null) {
|
|
103
|
-
|
|
104
|
-
track(target, key)
|
|
105
|
-
}
|
|
103
|
+
track(target, key)
|
|
106
104
|
// create a new reactive proxy
|
|
107
105
|
return reactiveProxy(getRaw(target[key]), target, key)
|
|
108
106
|
}
|
|
@@ -140,6 +138,8 @@ const reactiveProxy = (original, _parent = null, _key) => {
|
|
|
140
138
|
Object.getPrototypeOf(value) === Object.prototype
|
|
141
139
|
) {
|
|
142
140
|
value = Object.assign(receiver[key], value)
|
|
141
|
+
// The object keeps its identity; its nested setters already notified consumers.
|
|
142
|
+
isEqual = true
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
result = Reflect.set(target, key, value, receiver)
|
|
@@ -167,7 +167,7 @@ const reactiveDefineProperty = (target) => {
|
|
|
167
167
|
|
|
168
168
|
if (target[key] !== null && typeof target[key] === 'object') {
|
|
169
169
|
if (Object.getPrototypeOf(target[key]) === Object.prototype) {
|
|
170
|
-
|
|
170
|
+
reactiveDefineProperty(target[key])
|
|
171
171
|
} else if (Array.isArray(target[key]) === true) {
|
|
172
172
|
for (let i = 0; i < arrayPatchMethods.length; i++) {
|
|
173
173
|
target[key][arrayPatchMethods[i]] = function (v) {
|
|
@@ -189,6 +189,13 @@ const reactiveDefineProperty = (target) => {
|
|
|
189
189
|
// todo: support assigning array (as we do with proxies)
|
|
190
190
|
let oldValue = internalValue
|
|
191
191
|
if (oldValue !== newValue) {
|
|
192
|
+
if (
|
|
193
|
+
newValue !== null &&
|
|
194
|
+
typeof newValue === 'object' &&
|
|
195
|
+
Object.getPrototypeOf(newValue) === Object.prototype
|
|
196
|
+
) {
|
|
197
|
+
reactiveDefineProperty(newValue)
|
|
198
|
+
}
|
|
192
199
|
internalValue = newValue
|
|
193
200
|
trigger(target, key)
|
|
194
201
|
}
|
package/src/router/router.js
CHANGED
|
@@ -44,7 +44,7 @@ import { platform } from '../platform.js'
|
|
|
44
44
|
* @property {Object} hooks - The hooks of the route
|
|
45
45
|
* @property {Object} transition - The transition of the route
|
|
46
46
|
* @property {Object} announce - The announce of the route
|
|
47
|
-
* @property {
|
|
47
|
+
* @property {BlitsComponentFactory} component - The component factory of the route (or a lazy import function)
|
|
48
48
|
*
|
|
49
49
|
* @typedef {Object} Hash
|
|
50
50
|
* @property {string} path - The path of the hash
|
|
@@ -288,9 +288,13 @@ const performNavigation = async function (viewState) {
|
|
|
288
288
|
|
|
289
289
|
// routing to a new page (instead of routing back to a keepAlive page)
|
|
290
290
|
if (view === undefined) {
|
|
291
|
+
// Prepare eager runtime factories before creating their holder. Lazy loaders
|
|
292
|
+
// retain the conservative interactive fallback until their factory is known.
|
|
293
|
+
if (route.component.prepare !== undefined) route.component.prepare()
|
|
294
|
+
const needsInteractive = route.component.needsInteractive ?? true
|
|
291
295
|
// create a holder element for the new view
|
|
292
296
|
holder = stage.element({ parent: this[symbols.children][0] })
|
|
293
|
-
holder.populate({})
|
|
297
|
+
holder.populate({ holder: true, interactive: needsInteractive })
|
|
294
298
|
holder.set('w', '100%')
|
|
295
299
|
holder.set('h', '100%')
|
|
296
300
|
|
|
@@ -590,9 +594,22 @@ export const getSingleRegisteredRouterView = () => {
|
|
|
590
594
|
return singleRouterView
|
|
591
595
|
}
|
|
592
596
|
|
|
593
|
-
export const back = function () {
|
|
597
|
+
export const back = function (steps = 1) {
|
|
594
598
|
if (this.history === undefined) return
|
|
599
|
+
if (steps < 1) steps = 1
|
|
600
|
+
if (steps > this.history.length) steps = this.history.length
|
|
595
601
|
const viewState = getViewState(this.name)
|
|
602
|
+
|
|
603
|
+
// discard the intermediate history entries when navigating back more than 1 step
|
|
604
|
+
for (let i = 1; i < steps; i++) {
|
|
605
|
+
const poppedRoute = this.history.pop()
|
|
606
|
+
// if skipped history item is configured as keepAlive true then invoke destroy on view
|
|
607
|
+
if (poppedRoute.view !== undefined && poppedRoute.view !== null) {
|
|
608
|
+
poppedRoute.view.destroy()
|
|
609
|
+
poppedRoute.focus = null
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
596
613
|
const route = this.history.pop()
|
|
597
614
|
if (route && this.currentRoute !== route) {
|
|
598
615
|
// set indicator that we are navigating back (to prevent adding page to history stack)
|
package/src/settings.js
CHANGED
|
@@ -19,6 +19,8 @@ import symbols from './lib/symbols.js'
|
|
|
19
19
|
|
|
20
20
|
const settings = {
|
|
21
21
|
[symbols.settings]: {},
|
|
22
|
+
// Reacts to Blits-only settings changed at runtime via `this.$settings.set()`.
|
|
23
|
+
onRuntimeSettingChange: null,
|
|
22
24
|
get(key, defaultValue = null) {
|
|
23
25
|
if (key in this[symbols.settings]) {
|
|
24
26
|
return this[symbols.settings][key]
|
|
@@ -33,6 +35,9 @@ const settings = {
|
|
|
33
35
|
})
|
|
34
36
|
} else {
|
|
35
37
|
this[symbols.settings][key] = value
|
|
38
|
+
if (typeof this.onRuntimeSettingChange === 'function') {
|
|
39
|
+
this.onRuntimeSettingChange(key, value)
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
},
|
|
38
43
|
}
|
package/vite/index.js
CHANGED
|
@@ -20,15 +20,18 @@ import msdfGenerator from './msdfGenerator.js'
|
|
|
20
20
|
import blitsFileConverter from './blitsFileConverter.js'
|
|
21
21
|
import reactivityGuard from './reactivityGuard.js'
|
|
22
22
|
import injectDevConfig from './inject-dev-config.js'
|
|
23
|
+
import injectVersions from './injectVersions.js'
|
|
23
24
|
|
|
24
25
|
export { default as preCompiler } from './preCompiler.js'
|
|
25
26
|
export { default as msdfGenerator } from './msdfGenerator.js'
|
|
26
27
|
export { default as blitsFileConverter } from './blitsFileConverter.js'
|
|
27
28
|
export { default as reactivityGuard } from './reactivityGuard.js'
|
|
28
29
|
export { default as injectDevConfig } from './inject-dev-config.js'
|
|
30
|
+
export { default as injectVersions } from './injectVersions.js'
|
|
29
31
|
|
|
30
32
|
export default [
|
|
31
33
|
injectDevConfig(),
|
|
34
|
+
injectVersions(),
|
|
32
35
|
blitsFileConverter(),
|
|
33
36
|
reactivityGuard(),
|
|
34
37
|
preCompiler(),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Comcast Cable Communications Management, LLC
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*
|
|
15
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url)
|
|
23
|
+
|
|
24
|
+
function getVersions() {
|
|
25
|
+
const blits = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
26
|
+
// Resolve the public entry: the renderer does not export its package.json.
|
|
27
|
+
// Walk upwards to support hoisted, nested, and symlinked installations.
|
|
28
|
+
let directory = dirname(require.resolve('@lightningjs/renderer'))
|
|
29
|
+
while (true) {
|
|
30
|
+
const filename = join(directory, 'package.json')
|
|
31
|
+
if (existsSync(filename)) {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(filename, 'utf8'))
|
|
33
|
+
if (pkg.name === '@lightningjs/renderer') {
|
|
34
|
+
return { blits: blits.version, renderer: pkg.version }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const parent = dirname(directory)
|
|
38
|
+
if (parent === directory) throw new Error('Unable to locate @lightningjs/renderer/package.json')
|
|
39
|
+
directory = parent
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function injectVersions() {
|
|
44
|
+
return {
|
|
45
|
+
name: 'inject-versions',
|
|
46
|
+
config(config) {
|
|
47
|
+
const versions = getVersions()
|
|
48
|
+
return {
|
|
49
|
+
define: {
|
|
50
|
+
__BLITS_VERSION__: JSON.stringify(versions.blits || 'unknown'),
|
|
51
|
+
__RENDERER_VERSION__: JSON.stringify(versions.renderer || 'unknown'),
|
|
52
|
+
// merge any existing define block
|
|
53
|
+
...config.define,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|