@lightningjs/blits 2.15.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 +12 -0
- package/eslint.config.cjs +2 -0
- package/index.d.ts +4 -1
- package/package.json +4 -4
- package/src/application.js +5 -8
- package/src/component/base/methods.js +1 -1
- package/src/component/setup/computed.js +5 -1
- package/src/component.js +20 -5
- package/src/components/Circle.js +2 -1
- package/src/components/FPScounter.js +2 -1
- package/src/components/RouterView.js +2 -1
- package/src/engines/L3/element.js +1 -1
- package/src/keymap.js +27 -0
- package/src/launch.js +8 -19
- 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 +6 -2
- package/vite/index.js +3 -0
- package/vite/injectVersions.js +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# 2.16.0
|
|
4
|
+
|
|
5
|
+
_4 nov 2026_
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
- Improved benchmark results using 95 student t confidendence interval
|
|
9
|
+
- Fixed using a computed prop as a reactive source of a for-loop
|
|
10
|
+
- Improved setup of automated tests
|
|
11
|
+
- Added caching mechanism to computed props
|
|
12
|
+
- Bumped L3 renderer to v3.11.0
|
|
13
|
+
|
|
14
|
+
|
|
3
15
|
# 2.15.0
|
|
4
16
|
|
|
5
17
|
_26 oct 2026_
|
package/eslint.config.cjs
CHANGED
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightningjs/blits",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Blits: The Lightning 3 App Development Framework",
|
|
5
5
|
"bin": "bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"test": "c8 npm run test:run | tap-diff",
|
|
61
|
-
"test:run": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\"",
|
|
62
|
-
"test:run:failures": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\" | tap-filter test-fail | tap-diff",
|
|
61
|
+
"test:run": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\" \"vite/**/*.test.js\"",
|
|
62
|
+
"test:run:failures": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\" \"vite/**/*.test.js\" | tap-filter test-fail | tap-diff",
|
|
63
63
|
"lint": "node scripts/lint.js",
|
|
64
64
|
"lint:fix": "node scripts/lint.js fix",
|
|
65
65
|
"prepublishOnly": "node scripts/prepublishOnly.js",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"tape": "^5.9.0"
|
|
104
104
|
},
|
|
105
105
|
"dependencies": {
|
|
106
|
-
"@lightningjs/renderer": "
|
|
106
|
+
"@lightningjs/renderer": "3.11.0",
|
|
107
107
|
"magic-string": "^1.4.1"
|
|
108
108
|
},
|
|
109
109
|
"repository": {
|
package/src/application.js
CHANGED
|
@@ -24,6 +24,9 @@ import symbols from './lib/symbols.js'
|
|
|
24
24
|
import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
|
|
25
25
|
import { renderer } from './launch.js'
|
|
26
26
|
import { platform } from './platform.js'
|
|
27
|
+
import { keyMap, setKeyMap } from './keymap.js'
|
|
28
|
+
|
|
29
|
+
export { keyMap } from './keymap.js'
|
|
27
30
|
|
|
28
31
|
// Reacts to Blits-only settings changed at runtime via `this.$settings.set()`.
|
|
29
32
|
Settings.onRuntimeSettingChange = (key, value) => {
|
|
@@ -74,13 +77,6 @@ Settings.onRuntimeSettingChange = (key, value) => {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
/**
|
|
78
|
-
* Merged keyMap (default + custom settings).
|
|
79
|
-
* Initialized once during application init.
|
|
80
|
-
* @type {Object<string, string>}
|
|
81
|
-
*/
|
|
82
|
-
export let keyMap = {}
|
|
83
|
-
|
|
84
80
|
/** @type {any} Active App component instance, set during App init */
|
|
85
81
|
let liveAppInstance = null
|
|
86
82
|
/** @type {any} Platform input target, set during App init */
|
|
@@ -100,7 +96,7 @@ let mouseMoveThrottleMs = 100
|
|
|
100
96
|
let enableMouseFlag = false
|
|
101
97
|
|
|
102
98
|
export const initKeyMap = () => {
|
|
103
|
-
|
|
99
|
+
setKeyMap({ ...DEFAULT_KEYMAP, ...Settings.get('keymap', {}) })
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
const Application = (config) => {
|
|
@@ -248,6 +244,7 @@ const Application = (config) => {
|
|
|
248
244
|
|
|
249
245
|
if (node === null) {
|
|
250
246
|
currentComponent = undefined
|
|
247
|
+
currentNode = null
|
|
251
248
|
Hover.clear()
|
|
252
249
|
return
|
|
253
250
|
}
|
|
@@ -27,7 +27,7 @@ import { trigger } from '../../lib/reactivity/effect.js'
|
|
|
27
27
|
import { Log } from '../../lib/log.js'
|
|
28
28
|
import { removeEffects } from '../../lib/reactivity/effect.js'
|
|
29
29
|
import { renderer } from '../../launch.js'
|
|
30
|
-
import { keyMap } from '../../
|
|
30
|
+
import { keyMap } from '../../keymap.js'
|
|
31
31
|
import { platform } from '../../platform.js'
|
|
32
32
|
|
|
33
33
|
const selectCache = new WeakMap()
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { Log } from '../../lib/log.js'
|
|
19
19
|
|
|
20
20
|
import symbols from '../../lib/symbols.js'
|
|
21
|
+
import { createComputed } from '../../lib/reactivity/effect.js'
|
|
21
22
|
|
|
22
23
|
export default (component, computeds) => {
|
|
23
24
|
component[symbols.computedKeys] = []
|
|
@@ -40,7 +41,10 @@ export default (component, computeds) => {
|
|
|
40
41
|
component[symbols.computedKeys].push(computed)
|
|
41
42
|
Object.defineProperty(component, computed, {
|
|
42
43
|
get() {
|
|
43
|
-
|
|
44
|
+
// Setup runs on the shared prototype; install the cache on first instance access.
|
|
45
|
+
const get = createComputed(computeds[computed], this, computed)
|
|
46
|
+
Object.defineProperty(this, computed, { get })
|
|
47
|
+
return get()
|
|
44
48
|
},
|
|
45
49
|
})
|
|
46
50
|
}
|
package/src/component.js
CHANGED
|
@@ -496,16 +496,24 @@ const Component = (name = required('name'), config = required('config')) => {
|
|
|
496
496
|
base = setupComponent(Object.create(Base), config)
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
-
//
|
|
500
|
-
if (
|
|
501
|
-
Log.debug(`Generating code for ${name} component`)
|
|
502
|
-
config.code = codegenerator.call(config, parser(config.template, name), devMode)
|
|
503
|
-
}
|
|
499
|
+
// Direct factory calls also prepare runtime code before rendering.
|
|
500
|
+
if (factory.prepare !== undefined) factory.prepare()
|
|
504
501
|
|
|
505
502
|
// create an instance of the component, using base as the prototype (which contains Base)
|
|
506
503
|
return component.call(Object.create(base), options, parentEl, parentComponent, rootComponent)
|
|
507
504
|
}
|
|
508
505
|
|
|
506
|
+
// Precompiled factories have no preparation step. Runtime factories compile once,
|
|
507
|
+
// explicitly before holder creation, then take the same path as precompiled ones.
|
|
508
|
+
if (config.code === undefined) {
|
|
509
|
+
factory.prepare = () => {
|
|
510
|
+
Log.debug(`Generating code for ${name} component`)
|
|
511
|
+
config.code = codegenerator.call(config, parser(config.template, name), devMode)
|
|
512
|
+
factory.needsInteractive = factory.needsInteractive || !!config.code.usesHoverState
|
|
513
|
+
factory.prepare = undefined
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
509
517
|
// store the config on the factory, in order to access the config
|
|
510
518
|
// during the code generation step
|
|
511
519
|
factory[Symbol.for('config')] = config
|
|
@@ -516,6 +524,13 @@ const Component = (name = required('name'), config = required('config')) => {
|
|
|
516
524
|
// To determine whether dynamic component is actual Blits component or not
|
|
517
525
|
factory[symbols.isComponent] = true
|
|
518
526
|
|
|
527
|
+
// Pointer hit testing supports both hover and click-to-focus/input.
|
|
528
|
+
factory.needsInteractive =
|
|
529
|
+
(config.hooks && !!(config.hooks.hover || config.hooks.unhover)) ||
|
|
530
|
+
(config.input &&
|
|
531
|
+
(typeof config.input.enter === 'function' || typeof config.input.any === 'function')) ||
|
|
532
|
+
!!(config.code && config.code.usesHoverState)
|
|
533
|
+
|
|
519
534
|
return factory
|
|
520
535
|
}
|
|
521
536
|
|
package/src/components/Circle.js
CHANGED
|
@@ -66,7 +66,7 @@ export default () =>
|
|
|
66
66
|
'content',
|
|
67
67
|
'placement',
|
|
68
68
|
'inspector-data',
|
|
69
|
-
'
|
|
69
|
+
'interactive',
|
|
70
70
|
]
|
|
71
71
|
const elementConfigs = []
|
|
72
72
|
const forloops = []
|
|
@@ -133,6 +133,7 @@ export default () =>
|
|
|
133
133
|
},
|
|
134
134
|
],
|
|
135
135
|
context: {},
|
|
136
|
+
usesHoverState: false,
|
|
136
137
|
},
|
|
137
138
|
props: { size: 40, bgColor: 'white' },
|
|
138
139
|
computed: {
|
|
@@ -72,7 +72,7 @@ export const FPScounter = Blits.Component('FPScounter', {
|
|
|
72
72
|
'content',
|
|
73
73
|
'placement',
|
|
74
74
|
'inspector-data',
|
|
75
|
-
'
|
|
75
|
+
'interactive',
|
|
76
76
|
]
|
|
77
77
|
const elementConfigs = []
|
|
78
78
|
const forloops = []
|
|
@@ -634,6 +634,7 @@ export const FPScounter = Blits.Component('FPScounter', {
|
|
|
634
634
|
},
|
|
635
635
|
],
|
|
636
636
|
context: {},
|
|
637
|
+
usesHoverState: false,
|
|
637
638
|
},
|
|
638
639
|
state() {
|
|
639
640
|
return {
|
|
@@ -78,7 +78,7 @@ export default () =>
|
|
|
78
78
|
'content',
|
|
79
79
|
'placement',
|
|
80
80
|
'inspector-data',
|
|
81
|
-
'
|
|
81
|
+
'interactive',
|
|
82
82
|
]
|
|
83
83
|
const elementConfigs = []
|
|
84
84
|
const forloops = []
|
|
@@ -130,6 +130,7 @@ export default () =>
|
|
|
130
130
|
},
|
|
131
131
|
effects: [],
|
|
132
132
|
context: {},
|
|
133
|
+
usesHoverState: false,
|
|
133
134
|
},
|
|
134
135
|
props: {
|
|
135
136
|
name: '',
|
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,10 +133,14 @@ 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
|
|
|
@@ -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
|
|
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
|
+
}
|