@lightningjs/blits 2.5.0 → 2.7.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/src/launch.js CHANGED
@@ -19,6 +19,7 @@ import Settings from './settings.js'
19
19
  import { initLog, Log } from './lib/log.js'
20
20
  import engine from './engine.js'
21
21
  import blitsPackageInfo from '../package.json' with { type: 'json' }
22
+ import { configurePlatform } from './platform.js'
22
23
 
23
24
  /**
24
25
  * @typedef {Object} Font
@@ -86,10 +87,12 @@ async function rendererVersion() {
86
87
  * @property {number} [holdTimeout] - Time after which a key press is considered a hold
87
88
  * @property {HTMLCanvasElement} [canvas] - Custom canvas object used to render the App
88
89
  * @property {number} [textureProcessingTimeLimit] - Max time renderer can process textures in a frame
90
+ * @property {import('@lightningjs/renderer').RendererMainSettings['platform']} [rendererPlatform] - Custom platform used by the renderer
89
91
  * @property {Partial<import('@lightningjs/renderer').RendererMainSettings>} [advanced] - Advanced renderer settings
90
92
  * @property {boolean} [announcer] - Whether or not the announcer should be activated on initialization
91
93
  * @property {boolean} [enableMouse] - Enable mouse support (hover and click-to-focus). Defaults to false.
92
94
  * @property {number} [maxFPS] - Maximum FPS
95
+ * @property {function} [platform] - Platform factory for non-browser environments
93
96
  */
94
97
 
95
98
  /**
@@ -109,10 +112,30 @@ async function rendererVersion() {
109
112
  * @returns {void}
110
113
  */
111
114
  export default (App, target, settings) => {
115
+ let warnLegacyRendererPlatform = false
116
+
117
+ if (settings !== undefined && 'platform' in settings) {
118
+ if (typeof settings.platform === 'function') {
119
+ settings.platform = configurePlatform(settings.platform)
120
+ } else {
121
+ if ('rendererPlatform' in settings === false) {
122
+ settings.rendererPlatform = settings.platform
123
+ }
124
+ delete settings.platform
125
+ warnLegacyRendererPlatform = true
126
+ }
127
+ }
128
+
112
129
  Settings.set(settings)
113
130
 
114
131
  initLog()
115
132
 
133
+ if (warnLegacyRendererPlatform === true) {
134
+ Log.warn(
135
+ 'Passing a renderer platform via settings.platform is deprecated. Use settings.rendererPlatform instead.'
136
+ )
137
+ }
138
+
116
139
  rendererVersion().then((v) => {
117
140
  Log.info('Blits Version ', blitsPackageInfo.version)
118
141
  Log.info('Renderer Version ', v)
@@ -200,8 +200,12 @@ const processComputedBlock = (configObject) => {
200
200
  currentPos = skipNonCode(computedObj, currentPos)
201
201
  if (currentPos >= computedObj.length - 1) break
202
202
 
203
- // Find the next function name (computed properties are `name() { ... }`)
204
- const functionNameMatch = /^(\w+)\s*\(\)\s*/.exec(computedObj.substring(currentPos))
203
+ // Find the next function name (computed properties are `name() { ... }`
204
+ // or long-form `name: function() { ... }`)
205
+ const functionNameMatch = /^(\w+)\s*(?:\(\)|:\s*function\s*\([^)]*\))\s*/.exec(
206
+ computedObj.substring(currentPos)
207
+ )
208
+
205
209
  if (!functionNameMatch) {
206
210
  const nextPos = skipComputedMember(computedObj, currentPos)
207
211
  if (nextPos === -1) break
@@ -0,0 +1,114 @@
1
+ /*
2
+ * Copyright 2026 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 speechSynthesis from './announcer/speechSynthesis.js'
19
+
20
+ const globalScope = globalThis
21
+
22
+ const createKeyboardEventFactory = (KeyboardEventConstructor) => {
23
+ if (KeyboardEventConstructor === undefined) return undefined
24
+
25
+ return (type, init = {}) => {
26
+ const event = new KeyboardEventConstructor(type, init)
27
+ if ('keyCode' in init && event.keyCode !== init.keyCode) {
28
+ Object.defineProperty(event, 'keyCode', {
29
+ get: () => init.keyCode,
30
+ })
31
+ }
32
+ return event
33
+ }
34
+ }
35
+
36
+ const browserPlatform = () => {
37
+ const windowRef = globalScope.window
38
+ const documentRef = globalScope.document || (windowRef && windowRef.document)
39
+ const selfRef = globalScope.self || windowRef || globalScope
40
+ const navigatorRef =
41
+ globalScope.navigator || (windowRef && windowRef.navigator) || (selfRef && selfRef.navigator)
42
+ const KeyboardEventConstructor =
43
+ globalScope.KeyboardEvent || (windowRef && windowRef.KeyboardEvent)
44
+ let localStorage
45
+
46
+ try {
47
+ localStorage = (windowRef && windowRef.localStorage) || globalScope.localStorage
48
+ } catch {
49
+ localStorage = undefined
50
+ }
51
+
52
+ const createKeyboardEvent = createKeyboardEventFactory(KeyboardEventConstructor)
53
+
54
+ return {
55
+ input: documentRef,
56
+ viewport: windowRef,
57
+ dispatchEvent:
58
+ documentRef && documentRef.dispatchEvent
59
+ ? documentRef.dispatchEvent.bind(documentRef)
60
+ : undefined,
61
+ localStorage,
62
+ getCookie: () => (documentRef && documentRef.cookie) || '',
63
+ setCookie: (value) => {
64
+ if (documentRef !== undefined) documentRef.cookie = value
65
+ },
66
+ historyBack: () => {
67
+ if (windowRef && windowRef.history && windowRef.history.back) windowRef.history.back()
68
+ },
69
+ screenHeight: windowRef && windowRef.innerHeight,
70
+ hardwareConcurrency: navigatorRef && navigatorRef.hardwareConcurrency,
71
+ userAgent: navigatorRef && navigatorRef.userAgent,
72
+ KeyboardEvent: KeyboardEventConstructor,
73
+ isKeyboardEvent:
74
+ KeyboardEventConstructor !== undefined
75
+ ? (event) => event instanceof KeyboardEventConstructor
76
+ : undefined,
77
+ createKeyboardEvent,
78
+ announcer: speechSynthesis,
79
+ now:
80
+ globalScope.performance && globalScope.performance.now !== undefined
81
+ ? globalScope.performance.now.bind(globalScope.performance)
82
+ : Date.now,
83
+ }
84
+ }
85
+
86
+ export let platform = browserPlatform()
87
+
88
+ export const configurePlatform = (customPlatform) => {
89
+ const basePlatform = browserPlatform()
90
+ const resolvedCustomPlatform = customPlatform(basePlatform) || {}
91
+
92
+ platform = {
93
+ ...basePlatform,
94
+ ...resolvedCustomPlatform,
95
+ }
96
+
97
+ platform.KeyboardEvent = platform.KeyboardEvent || basePlatform.KeyboardEvent
98
+ platform.isKeyboardEvent =
99
+ resolvedCustomPlatform.isKeyboardEvent ||
100
+ (platform.KeyboardEvent !== undefined
101
+ ? (event) => event instanceof platform.KeyboardEvent
102
+ : basePlatform.isKeyboardEvent)
103
+ platform.createKeyboardEvent =
104
+ resolvedCustomPlatform.createKeyboardEvent ||
105
+ createKeyboardEventFactory(platform.KeyboardEvent) ||
106
+ basePlatform.createKeyboardEvent
107
+ platform.getCookie = platform.getCookie || basePlatform.getCookie
108
+ platform.setCookie = platform.setCookie || basePlatform.setCookie
109
+ platform.historyBack = platform.historyBack || basePlatform.historyBack
110
+ platform.announcer = platform.announcer || basePlatform.announcer
111
+ platform.now = platform.now || basePlatform.now
112
+
113
+ return platform
114
+ }
@@ -0,0 +1,39 @@
1
+ import type { AnnouncerDriver } from '@lightningjs/blits'
2
+
3
+ export interface WebOSRequestOptions {
4
+ method: 'speak'
5
+ parameters: {
6
+ text: string
7
+ clear: boolean
8
+ }
9
+ onFailure(error: any): void
10
+ }
11
+
12
+ export type WebOSRequest = (uri: string, options: WebOSRequestOptions) => any
13
+
14
+ export interface WebOSAnnouncerOptions {
15
+ /**
16
+ * webOS service request function. Defaults to `webOS.service.request` when available.
17
+ */
18
+ request?: WebOSRequest
19
+ /**
20
+ * webOS TTS service URI.
21
+ *
22
+ * @default 'luna://com.webos.service.tts'
23
+ */
24
+ uri?: string
25
+ /**
26
+ * Whether the platform TTS queue should be cleared before speaking.
27
+ *
28
+ * @default true
29
+ */
30
+ clear?: boolean
31
+ /**
32
+ * Optional failure callback for the webOS TTS request.
33
+ */
34
+ onFailure?: (error: any) => void
35
+ }
36
+
37
+ declare function createAnnouncer(options?: WebOSAnnouncerOptions): AnnouncerDriver
38
+
39
+ export default createAnnouncer
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Copyright 2026 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
+ const globalScope = globalThis
19
+ const defaultUri = 'luna://com.webos.service.tts'
20
+
21
+ const getDefaultRequest = () => {
22
+ const windowRef = globalScope.window
23
+ const webOS = globalScope.webOS || (windowRef && windowRef.webOS)
24
+
25
+ if (webOS && webOS.service && typeof webOS.service.request === 'function') {
26
+ return webOS.service.request.bind(webOS.service)
27
+ }
28
+ }
29
+
30
+ const createAnnouncer = (options = {}) => {
31
+ const request = options.request || getDefaultRequest()
32
+ const uri = options.uri || defaultUri
33
+ const clear = options.clear !== false
34
+
35
+ let currentRequest
36
+
37
+ return {
38
+ speak(announcement) {
39
+ if (request === undefined) {
40
+ return Promise.reject({ error: 'unavailable' })
41
+ }
42
+
43
+ if (!announcement || announcement.message === undefined || announcement.message === null) {
44
+ return Promise.reject({ error: 'Missing message' })
45
+ }
46
+
47
+ try {
48
+ currentRequest = request(uri, {
49
+ method: 'speak',
50
+ parameters: {
51
+ text: String(announcement.message),
52
+ clear,
53
+ },
54
+ onFailure(error) {
55
+ currentRequest = undefined
56
+ if (options.onFailure) {
57
+ options.onFailure(error)
58
+ }
59
+ },
60
+ })
61
+ } catch (error) {
62
+ currentRequest = undefined
63
+ return Promise.reject(error)
64
+ }
65
+
66
+ return Promise.resolve()
67
+ },
68
+ cancel() {
69
+ if (currentRequest && typeof currentRequest.cancel === 'function') {
70
+ currentRequest.cancel()
71
+ }
72
+ currentRequest = undefined
73
+ },
74
+ }
75
+ }
76
+
77
+ export default createAnnouncer
@@ -15,6 +15,8 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
+ import { platform } from '../../platform.js'
19
+
18
20
  // LocalStorage and Cookie storage library
19
21
  class localCookie {
20
22
  constructor(options) {
@@ -41,7 +43,8 @@ class localCookie {
41
43
 
42
44
  // Checks if local storage is available and works
43
45
  _checkIfLocalStorageWorks() {
44
- if (typeof localStorage !== 'undefined') {
46
+ const localStorage = platform.localStorage
47
+ if (localStorage !== undefined) {
45
48
  try {
46
49
  localStorage.setItem('feature_test', 'yes')
47
50
  if (localStorage.getItem('feature_test') === 'yes') {
@@ -66,23 +69,23 @@ class localCookie {
66
69
  * Local Storage APIs
67
70
  */
68
71
  _getItemLocalStorage(key) {
69
- return window.localStorage.getItem(key)
72
+ return platform.localStorage.getItem(key)
70
73
  }
71
74
 
72
75
  _setItemLocalStorage(key, value) {
73
- return window.localStorage.setItem(key, value)
76
+ return platform.localStorage.setItem(key, value)
74
77
  }
75
78
 
76
79
  _removeItemLocalStorage(key) {
77
- return window.localStorage.removeItem(key)
80
+ return platform.localStorage.removeItem(key)
78
81
  }
79
82
 
80
83
  _clearLocalStorage() {
81
- return window.localStorage.clear()
84
+ return platform.localStorage.clear()
82
85
  }
83
86
 
84
87
  _getLocalStorageKeys() {
85
- return Object.keys(window.localStorage)
88
+ return Object.keys(platform.localStorage)
86
89
  }
87
90
 
88
91
  /*
@@ -91,7 +94,7 @@ class localCookie {
91
94
  _getItemCookie(key) {
92
95
  /* eslint-disable */
93
96
  function escape(s) { return s.replace(/([.*+?\^${}()|\[\]\/\\])/g, '\\$1'); };
94
- var match = document.cookie.match(RegExp('(?:^|;\\s*)' + escape(key) + '=([^;]*)'));
97
+ var match = platform.getCookie().match(RegExp('(?:^|;\\s*)' + escape(key) + '=([^;]*)'));
95
98
 
96
99
  //if value in key of the entire cookie is empty, its deleted
97
100
  if (match && match[1] === '')
@@ -103,13 +106,13 @@ class localCookie {
103
106
  _setItemCookie(key, value) {
104
107
  var now = new Date()
105
108
  var expires = new Date(now.getTime() +1000*60*60*24*365*5) // 5 years!
106
- document.cookie = `${key}=${value}; expires=${expires.toUTCString()};`
109
+ platform.setCookie(`${key}=${value}; expires=${expires.toUTCString()};`)
107
110
  return //return undefined just like localStorage
108
111
  }
109
112
 
110
113
  _removeItemCookie(key) {
111
114
  // to delete an k/v set the key to empty
112
- document.cookie = `${key}=;Max-Age=-99999999;`
115
+ platform.setCookie(`${key}=;Max-Age=-99999999;`)
113
116
  return //return undefined just like localStorage
114
117
  }
115
118
 
@@ -117,14 +120,14 @@ class localCookie {
117
120
  // empty all key/values in cookies
118
121
  // What about cookies we got from the server? ¯\_(ツ)_/¯
119
122
  // They should have the HttpOnly flag, so we shouldn't be able to delete them
120
- document.cookie.split(";").forEach( (c) => {
121
- document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=;Max-Age=-99999999");
123
+ platform.getCookie().split(";").forEach( (c) => {
124
+ platform.setCookie(c.replace(/^ +/, "").replace(/=.*/, "=;expires=;Max-Age=-99999999"));
122
125
  });
123
126
 
124
127
  return //return undefined
125
128
  }
126
129
  _getCookieKeys() {
127
- return document.cookie.split(";").map((item)=>item.trim().split("=")[0])
130
+ return platform.getCookie().split(";").map((item)=>item.trim().split("=")[0])
128
131
  }
129
132
  }
130
133
 
@@ -24,6 +24,7 @@ import { stage } from '../launch.js'
24
24
  import Focus from '../focus/focus.js'
25
25
  import Announcer from '../announcer/announcer.js'
26
26
  import Settings from '../settings.js'
27
+ import { platform } from '../platform.js'
27
28
 
28
29
  /**
29
30
  * @typedef {import('../component.js').BlitsComponentFactory} BlitsComponentFactory - The component of the route
@@ -417,7 +418,8 @@ const executeBeforeHook = async function (
417
418
  this.currentRoute = previousRoute
418
419
  if (this.history.length > 0) {
419
420
  preventHashChangeNavigation = true
420
- window.history.back()
421
+ platform.historyBack()
422
+
421
423
  navigatingBack = false
422
424
  state.navigating = false
423
425
  }
@@ -434,7 +436,7 @@ const executeBeforeHook = async function (
434
436
  this.currentRoute = previousRoute
435
437
  if (this.history.length > 0) {
436
438
  preventHashChangeNavigation = true
437
- window.history.back()
439
+ platform.historyBack()
438
440
  navigatingBack = false
439
441
  state.navigating = false
440
442
  }
@@ -0,0 +1,60 @@
1
+ /*
2
+ * Copyright 2026 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 type { ComponentFactory, Settings } from '@lightningjs/blits'
19
+
20
+ export interface SnapshotNode {
21
+ type: string
22
+ attributes: Record<string, any>
23
+ children: Array<SnapshotNode | ComponentSnapshotNode>
24
+ }
25
+
26
+ export interface ComponentSnapshotNode {
27
+ type: 'Component'
28
+ name: string
29
+ hasFocus: boolean
30
+ isHovered: boolean
31
+ attributes: Record<string, any>
32
+ props: Record<string, any>
33
+ state: Record<string, any>
34
+ tree: SnapshotNode | null
35
+ }
36
+
37
+ export interface RenderComponentOptions {
38
+ props?: Record<string, any>
39
+ settings?: Settings
40
+ }
41
+
42
+ export interface RenderComponentFixture {
43
+ component: any
44
+ root: any
45
+ snapshot(): SnapshotNode | ComponentSnapshotNode
46
+ findByData(key: string, value: any): SnapshotNode | ComponentSnapshotNode | null
47
+ findAllByData(key: string, value: any): Array<SnapshotNode | ComponentSnapshotNode>
48
+ setProps(props: Record<string, any>): void
49
+ setState(state: Record<string, any>): void
50
+ focus(event?: KeyboardEvent): Promise<any>
51
+ unfocus(): any
52
+ createKeyboardEvent(key: string, init?: KeyboardEventInit): KeyboardEvent
53
+ input(key: string, event?: KeyboardEvent): boolean
54
+ destroy(): void
55
+ }
56
+
57
+ export function renderComponent(
58
+ Component: ComponentFactory,
59
+ options?: RenderComponentOptions
60
+ ): RenderComponentFixture
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright 2026 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 renderComponent from './renderComponent.js'
19
+
20
+ export { renderComponent }
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright 2026 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
+ class TestKeyboardEvent {
19
+ constructor(type, init = {}) {
20
+ this.type = type
21
+ this.key = init.key
22
+ this.code = init.code
23
+ this.keyCode = init.keyCode || 0
24
+ this.bubbles = init.bubbles === true
25
+ this.cancelable = init.cancelable === true
26
+ this.composed = init.composed === true
27
+ this.timeStamp = init.timeStamp || Date.now()
28
+ }
29
+ }
30
+
31
+ const createEventTarget = () => {
32
+ const listeners = {}
33
+
34
+ return {
35
+ addEventListener(type, handler) {
36
+ listeners[type] = listeners[type] || []
37
+ listeners[type].push(handler)
38
+ },
39
+ removeEventListener(type, handler) {
40
+ const handlers = listeners[type]
41
+ if (handlers === undefined) return
42
+ const index = handlers.indexOf(handler)
43
+ if (index > -1) handlers.splice(index, 1)
44
+ },
45
+ dispatchEvent(event) {
46
+ const handlers = listeners[event.type] || []
47
+ for (let i = 0; i < handlers.length; i++) {
48
+ handlers[i](event)
49
+ }
50
+ return true
51
+ },
52
+ }
53
+ }
54
+
55
+ const createStorage = () => {
56
+ const store = {}
57
+
58
+ return {
59
+ getItem(key) {
60
+ return store[key] !== undefined ? store[key] : null
61
+ },
62
+ setItem(key, value) {
63
+ store[key] = String(value)
64
+ },
65
+ removeItem(key) {
66
+ delete store[key]
67
+ },
68
+ clear() {
69
+ const keys = Object.keys(store)
70
+ for (let i = 0; i < keys.length; i++) {
71
+ delete store[keys[i]]
72
+ }
73
+ },
74
+ }
75
+ }
76
+
77
+ const nodePlatform = () => {
78
+ const input = createEventTarget()
79
+ const viewport = createEventTarget()
80
+ let cookie = ''
81
+
82
+ return {
83
+ input,
84
+ viewport,
85
+ dispatchEvent: input.dispatchEvent.bind(input),
86
+ localStorage: createStorage(),
87
+ getCookie: () => cookie,
88
+ setCookie: (value) => {
89
+ cookie = value
90
+ },
91
+ historyBack() {},
92
+ screenHeight: 1080,
93
+ hardwareConcurrency: 1,
94
+ userAgent: 'node',
95
+ KeyboardEvent: TestKeyboardEvent,
96
+ isKeyboardEvent: (event) => event instanceof TestKeyboardEvent,
97
+ createKeyboardEvent: (type, init = {}) => new TestKeyboardEvent(type, init),
98
+ announcer: {
99
+ speak() {
100
+ return Promise.resolve()
101
+ },
102
+ cancel() {},
103
+ },
104
+ now: Date.now,
105
+ }
106
+ }
107
+
108
+ export default nodePlatform