@lightningjs/blits 2.0.0-beta.1 → 2.0.0-beta.10

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +2 -0
  3. package/element-test-full.txt +0 -0
  4. package/index.d.ts +114 -14
  5. package/package.json +28 -8
  6. package/scripts/lint.js +17 -0
  7. package/scripts/prepublishOnly.js +15 -6
  8. package/scripts/prepush.js +25 -7
  9. package/src/announcer/announcer.js +54 -6
  10. package/src/announcer/speechSynthesis.js +156 -37
  11. package/src/application.js +98 -3
  12. package/src/component/base/methods.js +5 -2
  13. package/src/component/base/router.js +6 -0
  14. package/src/component/base/timeouts_intervals.js +56 -0
  15. package/src/component/setup/routes.js +5 -0
  16. package/src/component/setup/state.js +2 -0
  17. package/src/component.js +64 -24
  18. package/src/components/Circle.js +5 -3
  19. package/src/components/FPScounter.js +1591 -1577
  20. package/src/components/RouterView.js +141 -124
  21. package/src/components/Sprite.js +65 -8
  22. package/src/components/index.js +0 -2
  23. package/src/constants.js +17 -0
  24. package/src/engine.js +17 -0
  25. package/src/engines/L3/element.js +101 -7
  26. package/src/engines/L3/fontLoader.js +17 -0
  27. package/src/engines/L3/index.js +17 -0
  28. package/src/engines/L3/launch.js +4 -1
  29. package/src/engines/L3/shaderLoader.js +17 -0
  30. package/src/engines/index.js +17 -0
  31. package/src/{focus.js → focus/focus.js} +6 -18
  32. package/src/focus/helpers.js +31 -0
  33. package/src/focus/hover.js +94 -0
  34. package/src/helpers/fetchJson.js +4 -6
  35. package/src/launch.js +1 -0
  36. package/src/lib/codegenerator/generator.js +55 -9
  37. package/src/lib/lifecycle.js +34 -3
  38. package/src/lib/log.js +4 -4
  39. package/src/lib/precompiler/precompiler.js +23 -14
  40. package/src/lib/reactivity/effect.js +3 -2
  41. package/src/lib/reactivityguard/computedprops.js +113 -28
  42. package/src/lib/shaders/shaders.js +2 -1
  43. package/src/lib/symbols.js +19 -0
  44. package/src/lib/utils.js +17 -0
  45. package/src/plugin.js +17 -0
  46. package/src/plugins/appstate.d.ts +26 -0
  47. package/src/plugins/index.d.ts +28 -0
  48. package/src/plugins/language.d.ts +37 -0
  49. package/src/plugins/storage/localCookie.js +4 -6
  50. package/src/plugins/storage/storage.d.ts +31 -0
  51. package/src/plugins/theme.d.ts +43 -0
  52. package/src/plugins/theme.js +30 -3
  53. package/src/router/router.js +78 -14
  54. package/test-output.txt +0 -0
  55. package/vite/inject-dev-config.js +17 -0
  56. package/vite/preCompiler.js +15 -5
  57. package/vscode/data/template-attributes.json +335 -86
package/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.51.0
4
+
5
+ _04 mar 2026_
6
+
7
+ - Fixed wrong message from being interrupted in announcer
8
+ - Added `@loaded` and `@error` callbacks to Sprites
9
+
10
+ ## v1.50.0
11
+
12
+ _27 feb 2026_
13
+
14
+ - Fixed issue with router navigation options not properly being overriden
15
+ - Fixed FPS counter component and made it into a separate importable component (`import { FPScounter } from @lightningjs/blits/components`)
16
+ - Added mouse (magic wand) support (including new `hover` lifecycle hook and `isHovered` state indicator)
17
+ - Updated template attributes for VScode extension
18
+
19
+
20
+ ## v1.49.0
21
+
22
+ _20 feb 2026_
23
+
24
+ - Added `data-blits-renderstate` inspector attribute
25
+ - Fixed `data-blits-hasfocus` inspector attribute to always be available
26
+ - Added boolean attribute to `idle` lifecycle hook to indicate `idle` or `active` state
27
+ - Fixed type definitions for router beforeEach hooks
28
+ - Fixed double processing of reactivity guard
29
+
30
+ ## v1.48.0
31
+
32
+ _10 feb 2026_
33
+
34
+ - Improved cleanup of for-loop effects
35
+ - Fixed router issue with params not being reset
36
+ - Added next tick debouncing for transitions on multiple props
37
+ - Bumped renderer to 2.21.0
38
+ - Added support for using function-notation in `@loaded`, `@error` and `@updated` callbacks
39
+ - Added displaying of correct component type in inspector data when using `is`-attribute
40
+ - Replaced `$`-prefix for automatic inspector data with `blits-` prefix (fixes fatal error in Safari)
41
+
42
+
43
+ ## v1.47.0
44
+
45
+ _26 jan 2026_
46
+
47
+ - Fixed issue with reactivity guard vite plugin failing for commented-out code
48
+ - Fixed issue with autosizing of holder node by adding extra `eol` check
49
+ - Added support for _variants_ to the Theme plugin
50
+
51
+ ## v1.46.0
52
+
53
+ _22 jan 2026_
54
+
55
+ - Improved Announcer on Comcast devices
56
+ - Added new `afterEach` router hook
57
+ - Added router option to dynamically disable `back`-key handling by the router
58
+ - Added `this.$debounce`-method
59
+ - Added automatic inspector data items (`$componentType`, `$hasFocus` and `$isTransitioning`)
60
+ - Added functionality to auto size the holder node of a Component when the wrapper Element has dimensions
61
+ - Bumped Lightning renderer to v2.20.4 with correct handling of failed textures
62
+
63
+ ## v1.45.2
64
+
65
+ _13 jan 2026_
66
+
67
+ - Updated renderer to v2.20.2
68
+
69
+ ## v1.45.1
70
+
71
+ _29 dec 2025_
72
+
73
+ - fixed issue `magic-string` package being a dev dependency
74
+
75
+ ## v1.45.0
76
+
77
+ _29 dec 2025_
78
+
79
+ - Added support for defining types for custom plugins on the Blits component definitions
80
+ - Added importable type definitions for built-in plugins
81
+ - Added support for JS sourcemaps
82
+ - Added reactivity for shader props (individual shaders, not effects via dynamic shader)
83
+ - Fixed issue with usage of Blits plugins not working in for loops
84
+ - Improved test coverage
85
+
3
86
  ## v1.44.0
4
87
 
5
88
  _20 Nov 2025_
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  # Blits - Lightning 3 App Development Framework
4
4
 
5
+ ![npm](https://img.shields.io/npm/v/@lightningjs/blits?label=Version&color=blue) ![License](https://img.shields.io/github/license/lightning-js/blits?label=License&color=blue) ![Tests](https://github.com/lightning-js/blits/workflows/Tests/badge.svg) ![Coverage](https://img.shields.io/codecov/c/github/lightning-js/blits/master?label=Coverage) ![npm](https://img.shields.io/npm/dm/@lightningjs/blits?label=Downloads&color=blue)
6
+
5
7
  With Blits, the App Development Framework for Lightning 3, it becomes a breeze to build great Lightning 3 applications.
6
8
 
7
9
  Blits is built on top of the Lightning 3 Rendererer and aims to provide a **great developer experience**, making it fun and easy to build your apps!
Binary file
package/index.d.ts CHANGED
@@ -23,7 +23,9 @@ import { CanvasShaderType } from '@lightningjs/renderer/canvas';
23
23
  import { WebGlShaderType } from '@lightningjs/renderer/webgl';
24
24
 
25
25
  declare module '@lightningjs/blits' {
26
-
26
+ type RendererShaderEffect = import('@lightningjs/renderer').ShaderEffect
27
+ type WebGlCoreShader = import('@lightningjs/renderer').WebGlCoreShader
28
+ type RendererMainSettings = import('@lightningjs/renderer').RendererMainSettings
27
29
 
28
30
  export interface AnnouncerUtteranceOptions {
29
31
  /**
@@ -56,6 +58,12 @@ declare module '@lightningjs/blits' {
56
58
  * @default 1
57
59
  */
58
60
  volume?: number,
61
+ /**
62
+ * Whether to enable utterance keep-alive (prevents pausing on some platforms)
63
+ *
64
+ * @default undefined
65
+ */
66
+ enableUtteranceKeepAlive?: boolean
59
67
  }
60
68
 
61
69
  export interface AnnouncerUtterance<T = any> extends Promise<T> {
@@ -166,6 +174,8 @@ declare module '@lightningjs/blits' {
166
174
  * Fires when the Component is being destroyed and removed.
167
175
  */
168
176
  destroy?: () => void;
177
+ hover?: () => void;
178
+ unhover?: () => void;
169
179
  /**
170
180
  * Fires upon each frame start (allowing you to tap directly into the renderloop)
171
181
  *
@@ -197,11 +207,14 @@ declare module '@lightningjs/blits' {
197
207
  */
198
208
  exit?: () => void;
199
209
  /**
200
- * Fires when the renderer is done rendering and enters an idle state
210
+ * Fires when the renderer is done rendering and enters an idle state (idle = true)
211
+ * and when the renderer starts rendering again and leaves the idle state (idle = false)
212
+ *
213
+ * @param idle - boolean to indicate whether the renderer is in idle state or not
201
214
  *
202
215
  * Note: This event can fire multiple times
203
216
  */
204
- idle?: () => void;
217
+ idle?: (idle: boolean) => void;
205
218
  /**
206
219
  * Fires at a predefined interval and reports the current FPS value
207
220
  *
@@ -211,7 +224,7 @@ declare module '@lightningjs/blits' {
211
224
  }
212
225
 
213
226
  export interface Input {
214
- [key: string]: (event: KeyboardEvent) => void | undefined | unknown,
227
+ [key: string]: ((event: KeyboardEvent) => unknown) | undefined,
215
228
  /**
216
229
  * Catch all input function
217
230
  *
@@ -323,6 +336,11 @@ declare module '@lightningjs/blits' {
323
336
  */
324
337
  back(): boolean;
325
338
 
339
+ /**
340
+ * Enable or disable RouterView history navigation on Back input
341
+ */
342
+ backNavigation: boolean;
343
+
326
344
  /**
327
345
  * Get the current route read-only
328
346
  */
@@ -330,17 +348,17 @@ declare module '@lightningjs/blits' {
330
348
 
331
349
  /**
332
350
  * Get the list of all routes
333
- */
351
+ */
334
352
  readonly routes: Route[];
335
353
 
336
354
  /**
337
355
  * Get navigating state
338
- */
356
+ */
339
357
  readonly navigating: boolean;
340
358
 
341
359
  /**
342
360
  * Reactive router state
343
- */
361
+ */
344
362
  state: {
345
363
  /**
346
364
  * Path of the current route
@@ -364,7 +382,17 @@ declare module '@lightningjs/blits' {
364
382
  }
365
383
  }
366
384
 
367
- export type ComponentBase = {
385
+ // Extension point for app- and plugin-specific fields on the component `this`.
386
+ // Add your own properties (e.g., `$telemetry`, `componentName`) via TypeScript
387
+ // module augmentation in your app, without changing core types.
388
+ // Note: `ComponentBase` extends this interface, so augmented fields appear in all
389
+ // hooks, methods, input, computed, and watch.
390
+ export interface CustomComponentProperties {
391
+ // Empty by design: extend in your app via TypeScript module augmentation.
392
+ }
393
+
394
+
395
+ export interface ComponentBase extends CustomComponentProperties {
368
396
  /**
369
397
  * Indicates whether the component currently has focus
370
398
  *
@@ -372,6 +400,20 @@ declare module '@lightningjs/blits' {
372
400
  */
373
401
  readonly $hasFocus: boolean,
374
402
 
403
+ /**
404
+ * Parent component instance.
405
+ * Note: will be undefined for the root Application component or after destroy.
406
+ *
407
+ */
408
+ readonly $parent: ComponentBase,
409
+
410
+ /**
411
+ * Indicates whether the component currently is hovered
412
+ *
413
+ * @returns Boolean
414
+ */
415
+ $isHovered: boolean,
416
+
375
417
  /**
376
418
  * Listen to events emitted by other components
377
419
  */
@@ -414,6 +456,26 @@ declare module '@lightningjs/blits' {
414
456
  */
415
457
  $clearTimeout: (id: ReturnType<typeof setTimeout>) => void
416
458
 
459
+ /**
460
+ * Debounce a function execution, preventing memory leaks and function re-allocation
461
+ * @param name - Unique identifier for this debounce instance (unique per component instance)
462
+ * @param callback - Function to debounce
463
+ * @param ms - Delay in milliseconds
464
+ * @param args - Arguments to pass to the callback
465
+ */
466
+ $debounce: (name: string, callback: (...args: any[]) => void, ms?: number, ...args: any[]) => ReturnType<typeof setTimeout>
467
+
468
+ /**
469
+ * Clear a specific debounce by name
470
+ * @param name - The name of the debounce to clear
471
+ */
472
+ $clearDebounce: (name: string) => void
473
+
474
+ /**
475
+ * Clear all debounces registered on the component (automatically called on component destroy)
476
+ */
477
+ $clearDebounces: () => void
478
+
417
479
  /**
418
480
  * Set an interval that is automatically cleaned upon component destroy
419
481
  */
@@ -585,7 +647,7 @@ declare module '@lightningjs/blits' {
585
647
  * }
586
648
  * ```
587
649
  */
588
- state?: (this: InferProps<P>) => S;
650
+ state?: (this: Readonly<InferProps<P>> & ComponentBase) => S;
589
651
  /**
590
652
  * Methods for abstracting more complex business logic into separate function
591
653
  */
@@ -609,9 +671,10 @@ declare module '@lightningjs/blits' {
609
671
  }
610
672
 
611
673
  export interface RouterHooks {
612
- init?: () => Promise<> | void;
613
- beforeEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
614
- error?: (err: string) => string | Route | Promise<string | Route> | void;
674
+ init?: () => Promise<void> | void;
675
+ beforeEach?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
676
+ afterEach?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
677
+ error?: (err: string) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
615
678
  }
616
679
 
617
680
  export interface RouterConfig<P extends Props, S, M, C> {
@@ -634,6 +697,25 @@ declare module '@lightningjs/blits' {
634
697
  * ```
635
698
  */
636
699
  routes?: Route[]
700
+
701
+ /**
702
+ * Enable or disable RouterView history navigation on Back input
703
+ *
704
+ * @default true
705
+ *
706
+ * @remarks
707
+ * This is an app-wide setting that affects all RouterView instances in your application.
708
+ * The router state is global and shared across all router instances.
709
+ *
710
+ * @example
711
+ * ```js
712
+ * router: {
713
+ * backNavigation: false, // Disable automatic back navigation
714
+ * routes: [...]
715
+ * }
716
+ * ```
717
+ */
718
+ backNavigation?: boolean
637
719
  }
638
720
 
639
721
  export type ApplicationConfig<P extends Props, S, M, C, W> = ComponentConfig<P, S, M, C, W> & (
@@ -733,7 +815,8 @@ declare module '@lightningjs/blits' {
733
815
  }[keyof T]
734
816
 
735
817
  export interface RouteHooks {
736
- before?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
818
+ before?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
819
+ after?: (to: Route, from: Route) => string | Route | void | boolean | Promise<string | Route | void | boolean>;
737
820
  }
738
821
 
739
822
  export type Route = {
@@ -1157,6 +1240,16 @@ declare module '@lightningjs/blits' {
1157
1240
  * ```
1158
1241
  */
1159
1242
  announcerOptions?: AnnouncerUtteranceOptions,
1243
+ /**
1244
+ * Enable mouse support (hover and click-to-focus).
1245
+ *
1246
+ * When set to `true`, pointer movement over the canvas updates hover state on components
1247
+ * and click dispatches focus and Enter key input to the component under the cursor.
1248
+ * When set to `false`, no mouse or pointer listeners are registered.
1249
+ *
1250
+ * @default false
1251
+ */
1252
+ enableMouse?: boolean,
1160
1253
  /**
1161
1254
  * Maximum FPS at which the App will be rendered
1162
1255
  *
@@ -1165,7 +1258,14 @@ declare module '@lightningjs/blits' {
1165
1258
  *
1166
1259
  * Defaults to `0` which means no maximum
1167
1260
  */
1168
- maxFPS?: number
1261
+ maxFPS?: number,
1262
+ /**
1263
+ * Custom platform layer to be used by the App (experimental)
1264
+ *
1265
+ * Defaults to `null` which will be resolved internally to WebPlatform
1266
+ */
1267
+ platform?: any
1268
+
1169
1269
  }
1170
1270
 
1171
1271
  interface State {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.10",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -8,15 +8,34 @@
8
8
  "./vite": "./vite/index.js",
9
9
  "./transitions": "./src/router/transitions/index.js",
10
10
  "./precompiler": "./src/lib/precompiler/precompiler.js",
11
- "./plugins": "./src/plugins/index.js",
11
+ "./plugins": {
12
+ "types": "./src/plugins/index.d.ts",
13
+ "default": "./src/plugins/index.js"
14
+ },
15
+ "./plugins/language": {
16
+ "types": "./src/plugins/language.d.ts",
17
+ "default": "./src/plugins/language.js"
18
+ },
19
+ "./plugins/theme": {
20
+ "types": "./src/plugins/theme.d.ts",
21
+ "default": "./src/plugins/theme.js"
22
+ },
23
+ "./plugins/appstate": {
24
+ "types": "./src/plugins/appstate.d.ts",
25
+ "default": "./src/plugins/appstate.js"
26
+ },
27
+ "./plugins/storage": {
28
+ "types": "./src/plugins/storage/storage.d.ts",
29
+ "default": "./src/plugins/storage/storage.js"
30
+ },
12
31
  "./symbols": "./src/lib/symbols.js",
13
- "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js"
32
+ "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js",
33
+ "./components": "./src/components/FPScounter.js"
14
34
  },
15
35
  "scripts": {
16
- "test": "c8 node scripts/runTests.js",
17
- "test:ci": "c8 npm run test:run",
18
- "test:run": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules|packages)/**/}*.test.js' | tap-diff",
19
- "test:run:failures": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules|packages)/**/}*.test.js' | tap-filter test-fail | tap-diff",
36
+ "test": "c8 npm run test:run | tap-diff",
37
+ "test:run": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\"",
38
+ "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",
20
39
  "lint": "node scripts/lint.js",
21
40
  "lint:fix": "node scripts/lint.js fix",
22
41
  "prepublishOnly": "node scripts/prepublishOnly.js",
@@ -56,7 +75,8 @@
56
75
  "tape": "^5.5.0"
57
76
  },
58
77
  "dependencies": {
59
- "@lightningjs/renderer": "3.0.0-beta18"
78
+ "@lightningjs/renderer": "3.0.0-beta23",
79
+ "magic-string": "^0.30.21"
60
80
  },
61
81
  "repository": {
62
82
  "type": "git",
package/scripts/lint.js CHANGED
@@ -1,3 +1,20 @@
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
+
1
18
  import fg from 'fast-glob'
2
19
  import { resolve } from 'path'
3
20
  import { fileURLToPath } from 'url'
@@ -17,8 +17,9 @@
17
17
 
18
18
  import fs from 'fs'
19
19
  import path from 'path'
20
+ import { fileURLToPath } from 'url'
20
21
  import compiler from '../src/lib/precompiler/precompiler.js'
21
- import { exec } from 'child_process'
22
+ import childProcess from 'child_process'
22
23
 
23
24
  const currentDir = process.cwd()
24
25
  const componentDirs = ['src/components']
@@ -55,11 +56,14 @@ function processFile(filePath) {
55
56
  fs.copyFileSync(filePath, backupFilePath)
56
57
 
57
58
  const source = fs.readFileSync(filePath, 'utf-8')
58
- const newSource = compiler(source, filePath)
59
+ const result = compiler(source, filePath)
60
+
61
+ // Handle both string and object return types
62
+ const newSource = typeof result === 'object' && result.code ? result.code : result
59
63
  fs.writeFileSync(filePath, newSource)
60
64
 
61
- // only format the file if it was changed
62
- if (source !== newSource) {
65
+ // only format the file if it was changed (compiler returns undefined for non-Blits files)
66
+ if (newSource !== undefined && source !== newSource) {
63
67
  formatFileWithESLint(filePath)
64
68
  }
65
69
  }
@@ -67,7 +71,7 @@ function processFile(filePath) {
67
71
  function formatFileWithESLint(filePath) {
68
72
  const command = `eslint "${filePath}" --fix`
69
73
 
70
- exec(command, (error, stdout, stderr) => {
74
+ childProcess.exec(command, (error, stdout, stderr) => {
71
75
  if (error) {
72
76
  console.error(`Execution Error: ${error.message}`)
73
77
  console.error(`Exit Code: ${error.code}`)
@@ -81,4 +85,9 @@ function formatFileWithESLint(filePath) {
81
85
  })
82
86
  }
83
87
 
84
- precompileComponents()
88
+ // Only run if this file is executed directly (not imported)
89
+ if (fileURLToPath(import.meta.url) === process.argv[1]) {
90
+ precompileComponents()
91
+ }
92
+
93
+ export { precompileComponents, processDirectory, processFile, formatFileWithESLint }
@@ -1,10 +1,6 @@
1
1
  /*
2
- * If not stated otherwise in this file or this component's LICENSE file the
3
- * following copyright and licenses apply:
4
- *
5
- * Copyright 2023 Comcast
6
- *
7
- * Licensed under the Apache License, Version 2.0 (the License);
2
+ * Copyright 2025 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
4
  * you may not use this file except in compliance with the License.
9
5
  * You may obtain a copy of the License at
10
6
  *
@@ -15,6 +11,8 @@
15
11
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
12
  * See the License for the specific language governing permissions and
17
13
  * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
18
16
  */
19
17
 
20
18
  import { execSync } from 'child_process'
@@ -69,7 +67,6 @@ try {
69
67
  // Run ESLint on the changed files
70
68
  execSync(`npx eslint ${fileList.join(' ')}`, { stdio: 'inherit' })
71
69
  console.log('Linting passed successfully!\n')
72
- process.exit(0)
73
70
  } catch (error) {
74
71
  console.log('\n==========================================================================')
75
72
  console.log('ESLint found issues in your code. Please fix them before pushing.')
@@ -79,3 +76,24 @@ try {
79
76
  console.log('==========================================================================\n')
80
77
  process.exit(1)
81
78
  }
79
+
80
+ // By default, always run tests unless RUN_TEST=skip
81
+ const runTestsFlag = process.env.RUN_TEST || ''
82
+
83
+ if (runTestsFlag.toLowerCase() === 'skip') {
84
+ console.log('Skipping tests due to RUN_TEST=skip. Proceeding with push.')
85
+ process.exit(0)
86
+ } else {
87
+ // For any other value (including empty or unknown), run tests
88
+ try {
89
+ console.log('Running tests...\n')
90
+ execSync('npm run test', { stdio: 'inherit' })
91
+ console.log('Tests passed successfully!\n')
92
+ process.exit(0)
93
+ } catch (error) {
94
+ console.log('\n==========================================================================')
95
+ console.log('Some tests failed. Please fix them before pushing.')
96
+ console.log('==========================================================================\n')
97
+ process.exit(1)
98
+ }
99
+ }
@@ -26,7 +26,9 @@ let currentId = null
26
26
  let debounce = null
27
27
 
28
28
  // Global default utterance options
29
- let globalDefaultOptions = {}
29
+ let globalDefaultOptions = {
30
+ enableUtteranceKeepAlive: !/android/i.test((window.navigator || {}).userAgent || ''),
31
+ }
30
32
 
31
33
  const noopAnnouncement = {
32
34
  then() {},
@@ -50,7 +52,6 @@ const toggle = (v) => {
50
52
 
51
53
  const speak = (message, politeness = 'off', options = {}) => {
52
54
  if (active === false) return noopAnnouncement
53
-
54
55
  return addToQueue(message, politeness, false, options)
55
56
  }
56
57
 
@@ -106,6 +107,8 @@ const addToQueue = (message, politeness, delay = false, options = {}) => {
106
107
  return done
107
108
  }
108
109
 
110
+ let currentResolveFn = null
111
+
109
112
  const processQueue = async () => {
110
113
  if (isProcessing === true || queue.length === 0) return
111
114
  isProcessing = true
@@ -113,11 +116,13 @@ const processQueue = async () => {
113
116
  const { message, resolveFn, delay, id, options = {} } = queue.shift()
114
117
 
115
118
  currentId = id
119
+ currentResolveFn = resolveFn
116
120
 
117
121
  if (delay) {
118
122
  setTimeout(() => {
119
123
  isProcessing = false
120
124
  currentId = null
125
+ currentResolveFn = null
121
126
  resolveFn('finished')
122
127
  processQueue()
123
128
  }, delay)
@@ -138,19 +143,23 @@ const processQueue = async () => {
138
143
  Log.debug(`Announcer - finished speaking: "${message}" (id: ${id})`)
139
144
 
140
145
  currentId = null
146
+ currentResolveFn = null
141
147
  isProcessing = false
142
148
  resolveFn('finished')
143
149
  processQueue()
144
150
  })
145
151
  .catch((e) => {
146
- currentId = null
147
- isProcessing = false
152
+ if (id === currentId) {
153
+ currentId = null
154
+ currentResolveFn = null
155
+ isProcessing = false
156
+ }
148
157
  Log.debug(`Announcer - error ("${e.error}") while speaking: "${message}" (id: ${id})`)
149
158
  resolveFn(e.error)
150
159
  processQueue()
151
160
  })
152
161
  debounce = null
153
- }, 200)
162
+ }, 300)
154
163
  }
155
164
  }
156
165
 
@@ -158,13 +167,52 @@ const polite = (message, options = {}) => speak(message, 'polite', options)
158
167
 
159
168
  const assertive = (message, options = {}) => speak(message, 'assertive', options)
160
169
 
170
+ // Clear debounce timer
171
+ const clearDebounceTimer = () => {
172
+ if (debounce !== null) {
173
+ clearTimeout(debounce)
174
+ debounce = null
175
+ }
176
+ }
177
+
161
178
  const stop = () => {
179
+ // Clear debounce timer if speech hasn't started yet
180
+ clearDebounceTimer()
181
+
182
+ // Always cancel speech synthesis to ensure clean state
162
183
  speechSynthesis.cancel()
184
+
185
+ // Store resolve function before resetting state
186
+ const resolveFn = currentResolveFn
187
+
188
+ // Reset state
189
+ currentId = null
190
+ currentResolveFn = null
191
+ isProcessing = false
192
+
193
+ // Resolve promise if there was an active utterance
194
+ if (resolveFn !== null) {
195
+ resolveFn('interrupted')
196
+ }
163
197
  }
164
198
 
165
199
  const clear = () => {
200
+ // Clear debounce timer
201
+ clearDebounceTimer()
202
+
203
+ // Resolve all pending items in queue
204
+ while (queue.length > 0) {
205
+ const item = queue.shift()
206
+ if (item.resolveFn) {
207
+ Log.debug(`Announcer - clearing queued item: "${item.message}" (id: ${item.id})`)
208
+ item.resolveFn('cleared')
209
+ }
210
+ }
211
+
212
+ // Reset state
213
+ currentId = null
214
+ currentResolveFn = null
166
215
  isProcessing = false
167
- queue.length = 0
168
216
  }
169
217
 
170
218
  const configure = (options = {}) => {