@lightningjs/blits 1.42.0 → 1.43.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.43.0
4
+
5
+ _12 Nov 2025_
6
+
7
+ - Exposed different renderer events as hooks (`idle`, `fpsUpdate` and `frameTick`)
8
+ - Added `$clearListeners` utility helper
9
+ - Added `$clearListeners` and `$unlisten` to type definitions
10
+ - Fixed issue with __DEV__ flag being set in vite config
11
+ - Fixed `lint` and `lint:fix` scripts to work cross platform (added Windows support)
12
+ - Added support for utterance options (`rate`, `pitch`, `lang`, `voice` and `volume`) to Announcer plugin
13
+ - Added reactivity to `src`-attribute of the Sprite
14
+ - Updates deprecated `assert` keyword for new `with` keyword
15
+
16
+
17
+ ## v1.42.1
18
+
19
+ _28 Oct 2025_
20
+
21
+ - Updated to version 2.19.1 of the renderer
22
+ - Fixed scale animation snapping issue when using object notation with transitions (again)
23
+
3
24
  ## v1.42.0
4
25
 
5
26
  _24 Oct 2025_
package/eslint.config.cjs CHANGED
@@ -15,71 +15,75 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- const {
19
- defineConfig,
20
- globalIgnores,
21
- } = require("eslint/config");
18
+ const { defineConfig, globalIgnores } = require('eslint/config')
22
19
 
23
- const globals = require("globals");
24
- const prettier = require("eslint-plugin-prettier");
25
- const babelParser = require("@babel/eslint-parser");
26
- const js = require("@eslint/js");
20
+ const globals = require('globals')
21
+ const prettier = require('eslint-plugin-prettier')
22
+ const babelParser = require('@babel/eslint-parser')
23
+ const js = require('@eslint/js')
27
24
 
28
- const {
29
- FlatCompat,
30
- } = require("@eslint/eslintrc");
25
+ const { FlatCompat } = require('@eslint/eslintrc')
31
26
 
32
27
  const compat = new FlatCompat({
33
- baseDirectory: __dirname,
34
- recommendedConfig: js.configs.recommended,
35
- allConfig: js.configs.all
36
- });
28
+ baseDirectory: __dirname,
29
+ recommendedConfig: js.configs.recommended,
30
+ allConfig: js.configs.all,
31
+ })
37
32
 
38
- module.exports = defineConfig([{
33
+ module.exports = defineConfig([
34
+ {
39
35
  languageOptions: {
40
- globals: {
41
- ...globals.browser,
42
- ...globals.node,
43
- globalThis: false,
44
- },
36
+ globals: {
37
+ ...globals.browser,
38
+ ...globals.node,
39
+ globalThis: false,
40
+ },
45
41
 
46
- parser: babelParser,
47
- ecmaVersion: 2018,
48
- sourceType: "module",
42
+ parser: babelParser,
43
+ ecmaVersion: 2018,
44
+ sourceType: 'module',
49
45
 
50
- parserOptions: {
51
- requireConfigFile: false,
46
+ parserOptions: {
47
+ requireConfigFile: false,
52
48
 
53
- babelOptions: {
54
- plugins: ["@babel/plugin-syntax-import-assertions"],
55
- },
49
+ babelOptions: {
50
+ plugins: ['@babel/plugin-syntax-import-assertions'],
56
51
  },
52
+ },
57
53
  },
58
54
 
59
55
  plugins: {
60
- prettier,
56
+ prettier,
61
57
  },
62
58
 
63
- extends: compat.extends("eslint:recommended", "plugin:prettier/recommended", "prettier"),
59
+ extends: compat.extends('eslint:recommended', 'plugin:prettier/recommended', 'prettier'),
64
60
 
65
61
  rules: {
66
- "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
67
- "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
68
- quotes: [2, "single", "avoid-escape"],
69
- semi: [2, "never"],
70
- "no-extra-boolean-cast": "off",
62
+ 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
63
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
64
+ quotes: [2, 'single', 'avoid-escape'],
65
+ semi: [2, 'never'],
66
+ 'no-extra-boolean-cast': 'off',
71
67
 
72
- "no-unused-vars": [1, {
73
- argsIgnorePattern: "res|next|^err",
74
- }],
68
+ 'no-unused-vars': [
69
+ 1,
70
+ {
71
+ argsIgnorePattern: 'res|next|^err',
72
+ },
73
+ ],
75
74
 
76
- "prettier/prettier": ["error", {
77
- trailingComma: "es5",
78
- singleQuote: true,
79
- tabWidth: 2,
80
- semi: false,
81
- printWidth: 100,
82
- endOfLine: "auto",
83
- }],
75
+ 'prettier/prettier': [
76
+ 'error',
77
+ {
78
+ trailingComma: 'es5',
79
+ singleQuote: true,
80
+ tabWidth: 2,
81
+ semi: false,
82
+ printWidth: 100,
83
+ endOfLine: 'auto',
84
+ },
85
+ ],
84
86
  },
85
- }, globalIgnores(["**/node_modules"])]);
87
+ },
88
+ globalIgnores(['**/node_modules']),
89
+ ])
package/index.d.ts CHANGED
@@ -23,7 +23,40 @@ import {type ShaderEffect as RendererShaderEffect, type WebGlCoreShader, type Re
23
23
  declare module '@lightningjs/blits' {
24
24
 
25
25
 
26
- export interface AnnouncerUtterance extends Promise<T> {
26
+ export interface AnnouncerUtteranceOptions {
27
+ /**
28
+ * Language code (BCP 47 format, e.g., 'en-US', 'fr-FR')
29
+ *
30
+ * @default 'en-US'
31
+ */
32
+ lang?: string,
33
+ /**
34
+ * Voice pitch (0 to 2, where 1 is normal)
35
+ *
36
+ * @default 1
37
+ */
38
+ pitch?: number,
39
+ /**
40
+ * Speech rate (0.1 to 10, where 1 is normal)
41
+ *
42
+ * @default 1
43
+ */
44
+ rate?: number,
45
+ /**
46
+ * Voice to use (obtained from `speechSynthesis.getVoices()`)
47
+ *
48
+ * @default null
49
+ */
50
+ voice?: SpeechSynthesisVoice | null,
51
+ /**
52
+ * Volume level (0 to 1, where 1 is full volume)
53
+ *
54
+ * @default 1
55
+ */
56
+ volume?: number,
57
+ }
58
+
59
+ export interface AnnouncerUtterance<T = any> extends Promise<T> {
27
60
  /**
28
61
  * Removes a specific message from the announcement queue,
29
62
  * to make sure it isn't spoke out.
@@ -46,8 +79,34 @@ declare module '@lightningjs/blits' {
46
79
  * When a message is added with politeness set to `assertive` the message
47
80
  * will be added to the beginning of the queue
48
81
  *
82
+ * @param message - The message to be spoken
83
+ * @param politeness - Politeness level ('off', 'polite', or 'assertive')
84
+ * @param options - Optional utterance options (rate, pitch, lang, voice, volume)
85
+ */
86
+ speak(message: string | number, politeness?: 'off' | 'polite' | 'assertive', options?: AnnouncerUtteranceOptions): AnnouncerUtterance;
87
+ /**
88
+ * Instruct the Announcer to speak a message with 'polite' politeness level.
89
+ * Will add the message to the end of announcement queue.
90
+ *
91
+ * @param message - The message to be spoken
92
+ * @param options - Optional utterance options (rate, pitch, lang, voice, volume)
93
+ */
94
+ polite(message: string | number, options?: AnnouncerUtteranceOptions): AnnouncerUtterance;
95
+ /**
96
+ * Instruct the Announcer to speak a message with 'assertive' politeness level.
97
+ * Will add the message to the beginning of announcement queue.
98
+ *
99
+ * @param message - The message to be spoken
100
+ * @param options - Optional utterance options (rate, pitch, lang, voice, volume)
49
101
  */
50
- speak(message: string | number, politeness?: 'off' | 'polite' | 'assertive'): AnnouncerUtterance;
102
+ assertive(message: string | number, options?: AnnouncerUtteranceOptions): AnnouncerUtterance;
103
+ /**
104
+ * Configure global default utterance options that will be applied to all
105
+ * subsequent announcements unless overridden by per-call options.
106
+ *
107
+ * @param options - Default utterance options (rate, pitch, lang, voice, volume)
108
+ */
109
+ configure(options?: AnnouncerUtteranceOptions): void;
51
110
  /**
52
111
  * Instruct the Announcer to add a pause of a certain duration (in ms). Will add this pause
53
112
  * to the end of announcement queue
@@ -135,6 +194,18 @@ declare module '@lightningjs/blits' {
135
194
  * This event can fire multiple times during the component's lifecycle
136
195
  */
137
196
  exit?: () => void;
197
+ /**
198
+ * Fires when the renderer is done rendering and enters an idle state
199
+ *
200
+ * Note: This event can fire multiple times
201
+ */
202
+ idle?: () => void;
203
+ /**
204
+ * Fires at a predefined interval and reports the current FPS value
205
+ *
206
+ * Note: This event fire multiple times
207
+ */
208
+ fpsUpdate?: () => void;
138
209
  }
139
210
 
140
211
  export interface Input {
@@ -305,6 +376,14 @@ declare module '@lightningjs/blits' {
305
376
  $listen: {
306
377
  (event: string, callback: (args: any) => void, priority?: number): void;
307
378
  }
379
+
380
+ /**
381
+ * Remove an event listener previously registered with $listen
382
+ */
383
+ $unlisten: {
384
+ (event: string): void;
385
+ }
386
+
308
387
  /**
309
388
  * Emit events that other components can listen to
310
389
  * @param name - name of the event to be emitted
@@ -316,6 +395,13 @@ declare module '@lightningjs/blits' {
316
395
  */
317
396
  $emit(name: string, data?: any, byReference?: boolean): void;
318
397
 
398
+ /**
399
+ * Remove all listeners for this component from all events
400
+ */
401
+ $clearListeners: {
402
+ (): void;
403
+ }
404
+
319
405
  /**
320
406
  * Set a timeout that is automatically cleaned upon component destroy
321
407
  */
@@ -1078,7 +1164,24 @@ declare module '@lightningjs/blits' {
1078
1164
  *
1079
1165
  * @default false
1080
1166
  */
1081
- announcer?: boolean
1167
+ announcer?: boolean,
1168
+ /**
1169
+ * Global default utterance options for the announcer.
1170
+ *
1171
+ * These options will be applied to all announcements unless overridden
1172
+ * by per-call options passed to `speak()`, `polite()`, or `assertive()`.
1173
+ *
1174
+ * @example
1175
+ * ```js
1176
+ * announcerOptions: {
1177
+ * rate: 1.0,
1178
+ * pitch: 1.0,
1179
+ * lang: 'en-US',
1180
+ * volume: 1.0
1181
+ * }
1182
+ * ```
1183
+ */
1184
+ announcerOptions?: AnnouncerUtteranceOptions,
1082
1185
  /**
1083
1186
  * Maximum FPS at which the App will be rendered
1084
1187
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -16,8 +16,8 @@
16
16
  "test": "c8 node scripts/runTests.js",
17
17
  "test:ci": "c8 npm run test:run",
18
18
  "test:run": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules|packages)/**/}*.test.js' | tap-diff",
19
- "lint": "eslint '**/*.js'",
20
- "lint:fix": "eslint '**/*.js' --fix",
19
+ "lint": "node scripts/lint.js",
20
+ "lint:fix": "node scripts/lint.js fix",
21
21
  "prepublishOnly": "node scripts/prepublishOnly.js",
22
22
  "postpublish": "node scripts/postpublish.js",
23
23
  "prepare": "husky",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@lightningjs/msdf-generator": "^1.2.0",
58
- "@lightningjs/renderer": "^2.18.2"
58
+ "@lightningjs/renderer": "^2.19.1"
59
59
  },
60
60
  "repository": {
61
61
  "type": "git",
@@ -0,0 +1,50 @@
1
+ import fg from 'fast-glob'
2
+ import { resolve } from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { ESLint } from 'eslint'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = resolve(__filename, '../..')
8
+
9
+ // Get "fix" flag from command line arguments
10
+ const isFix = process.argv[2] === 'fix'
11
+
12
+ console.log('Starting the linting process...')
13
+ console.log(`Fix mode is ${isFix ? 'enabled' : 'disabled'}`)
14
+
15
+ const lintFiles = await fg(['**/*.js', '!node_modules/**'], {
16
+ cwd: __dirname,
17
+ })
18
+
19
+ console.log(`Found ${lintFiles.length} JavaScript file(s) to lint.`)
20
+
21
+ if (lintFiles.length === 0) {
22
+ console.log('No JavaScript files found to lint.')
23
+ process.exit(0)
24
+ }
25
+
26
+ const lint = new ESLint({
27
+ fix: isFix,
28
+ cwd: __dirname,
29
+ })
30
+
31
+ const results = await lint.lintFiles(lintFiles)
32
+
33
+ // Apply fixes to files if "fix" option is enabled
34
+ if (isFix) {
35
+ await ESLint.outputFixes(results)
36
+ }
37
+
38
+ const formatter = await lint.loadFormatter('stylish')
39
+ const resultText = formatter.format(results)
40
+
41
+ if (resultText) {
42
+ console.log(resultText)
43
+ } else {
44
+ console.log('Linting complete: All files passed without issues.')
45
+ }
46
+
47
+ // Determine if there were any errors (not warnings)
48
+ const hasErrors = results.some((result) => result.errorCount > 0)
49
+
50
+ process.exit(hasErrors ? 1 : 0)
@@ -43,9 +43,13 @@ try {
43
43
  })
44
44
  } else {
45
45
  // Only check files in the commits that are being pushed right now
46
- filesToLint = execSync(`git diff --name-only ${remoteSha}..${localSha} | grep '\\.js$'`, {
47
- encoding: 'utf8',
48
- })
46
+ // Added --diff-filter=AM to exclude deleted files from linting.
47
+ filesToLint = execSync(
48
+ `git diff --diff-filter=AM --name-only ${remoteSha}..${localSha} | grep '\\.js$'`,
49
+ {
50
+ encoding: 'utf8',
51
+ }
52
+ )
49
53
  }
50
54
  } catch (error) {
51
55
  // grep will exit with code 1 if it doesn't find anything
@@ -60,7 +64,6 @@ if (!filesToLint.trim()) {
60
64
 
61
65
  const fileList = filesToLint.trim().split('\n')
62
66
  console.log(`Running ESLint on ${fileList.length} changed JavaScript files:\n`)
63
- console.log(filesToLint)
64
67
 
65
68
  try {
66
69
  // Run ESLint on the changed files
@@ -25,6 +25,9 @@ let isProcessing = false
25
25
  let currentId = null
26
26
  let debounce = null
27
27
 
28
+ // Global default utterance options
29
+ let globalDefaultOptions = {}
30
+
28
31
  const noopAnnouncement = {
29
32
  then() {},
30
33
  done() {},
@@ -45,10 +48,10 @@ const toggle = (v) => {
45
48
  active = v ? true : false
46
49
  }
47
50
 
48
- const speak = (message, politeness = 'off') => {
51
+ const speak = (message, politeness = 'off', options = {}) => {
49
52
  if (active === false) return noopAnnouncement
50
53
 
51
- return addToQueue(message, politeness)
54
+ return addToQueue(message, politeness, false, options)
52
55
  }
53
56
 
54
57
  const pause = (delay) => {
@@ -57,7 +60,7 @@ const pause = (delay) => {
57
60
  return addToQueue(undefined, undefined, delay)
58
61
  }
59
62
 
60
- const addToQueue = (message, politeness, delay = false) => {
63
+ const addToQueue = (message, politeness, delay = false, options = {}) => {
61
64
  // keep track of the id so message can be canceled
62
65
  const id = count++
63
66
 
@@ -88,8 +91,8 @@ const addToQueue = (message, politeness, delay = false) => {
88
91
  // add message of pause
89
92
  if (delay === false) {
90
93
  politeness === 'assertive'
91
- ? queue.unshift({ message, resolveFn, id })
92
- : queue.push({ message, resolveFn, id })
94
+ ? queue.unshift({ message, resolveFn, id, options })
95
+ : queue.push({ message, resolveFn, id, options })
93
96
  } else {
94
97
  queue.push({ delay, resolveFn, id })
95
98
  }
@@ -107,7 +110,7 @@ const processQueue = async () => {
107
110
  if (isProcessing === true || queue.length === 0) return
108
111
  isProcessing = true
109
112
 
110
- const { message, resolveFn, delay, id } = queue.shift()
113
+ const { message, resolveFn, delay, id, options = {} } = queue.shift()
111
114
 
112
115
  currentId = id
113
116
 
@@ -125,7 +128,12 @@ const processQueue = async () => {
125
128
  Log.debug(`Announcer - speaking: "${message}" (id: ${id})`)
126
129
 
127
130
  speechSynthesis
128
- .speak({ message, id })
131
+ .speak({
132
+ message,
133
+ id,
134
+ ...globalDefaultOptions,
135
+ ...options,
136
+ })
129
137
  .then(() => {
130
138
  Log.debug(`Announcer - finished speaking: "${message}" (id: ${id})`)
131
139
 
@@ -146,9 +154,9 @@ const processQueue = async () => {
146
154
  }
147
155
  }
148
156
 
149
- const polite = (message) => speak(message, 'polite')
157
+ const polite = (message, options = {}) => speak(message, 'polite', options)
150
158
 
151
- const assertive = (message) => speak(message, 'assertive')
159
+ const assertive = (message, options = {}) => speak(message, 'assertive', options)
152
160
 
153
161
  const stop = () => {
154
162
  speechSynthesis.cancel()
@@ -159,6 +167,10 @@ const clear = () => {
159
167
  queue.length = 0
160
168
  }
161
169
 
170
+ const configure = (options = {}) => {
171
+ globalDefaultOptions = { ...globalDefaultOptions, ...options }
172
+ }
173
+
162
174
  export default {
163
175
  speak,
164
176
  polite,
@@ -169,4 +181,5 @@ export default {
169
181
  toggle,
170
182
  clear,
171
183
  pause,
184
+ configure,
172
185
  }
@@ -58,6 +58,11 @@ const Application = (config) => {
58
58
  config.hooks[symbols.init] = function () {
59
59
  // set the initial activation state of the announcer based on launch setting
60
60
  this.$announcer.toggle(Settings.get('announcer', false))
61
+ // configure global default utterance options if provided in settings
62
+ const announcerOptions = Settings.get('announcerOptions', null)
63
+ if (announcerOptions && typeof announcerOptions === 'object') {
64
+ this.$announcer.configure(announcerOptions)
65
+ }
61
66
  const keyMap = { ...defaultKeyMap, ...Settings.get('keymap', {}) }
62
67
 
63
68
  /** @type {number} Input throttle time in milliseconds (0 = disabled) */
@@ -72,4 +72,18 @@ export default {
72
72
  enumerable: true,
73
73
  configurable: false,
74
74
  },
75
+
76
+ /**
77
+ * Removes all listeners for this component from all events.
78
+ *
79
+ * @returns {void}
80
+ */
81
+ $clearListeners: {
82
+ value: function () {
83
+ eventListeners.removeListeners(this)
84
+ },
85
+ writable: false,
86
+ enumerable: true,
87
+ configurable: false,
88
+ },
75
89
  }
package/src/component.js CHANGED
@@ -266,20 +266,33 @@ const Component = (name = required('name'), config = required('config')) => {
266
266
  this[symbols.rendererEventListeners] = []
267
267
  // register hooks if component has hooks specified
268
268
  if (config.hooks) {
269
- // frame tick event
270
- if (config.hooks.frameTick) {
271
- const cb = (r, data) => emit('frameTick', this[symbols.identifier], this, [data])
272
- this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
273
- renderer.on('frameTick', cb)
274
- }
269
+ // push to next tick to ensure
270
+ setTimeout(() => {
271
+ // frame tick event
272
+ if (config.hooks.frameTick) {
273
+ const cb = (r, data) => emit('frameTick', this[symbols.identifier], this, [data])
274
+ this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
275
+ renderer.on('frameTick', cb)
276
+ }
275
277
 
276
- if (config.hooks.idle) {
277
- const cb = () => {
278
- emit('idle', this[symbols.identifier], this)
278
+ // idle event
279
+ if (config.hooks.idle) {
280
+ const cb = () => {
281
+ emit('idle', this[symbols.identifier], this)
282
+ }
283
+ this[symbols.rendererEventListeners].push({ event: 'idle', cb })
284
+ renderer.on('idle', cb)
279
285
  }
280
- this[symbols.rendererEventListeners].push({ event: 'idle', cb })
281
- renderer.on('idle', cb)
282
- }
286
+
287
+ // fpsUpdate event
288
+ if (config.hooks.fpsUpdate) {
289
+ const cb = (r, data) => {
290
+ emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
291
+ }
292
+ this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
293
+ renderer.on('fpsUpdate', cb)
294
+ }
295
+ })
283
296
 
284
297
  // inBounds event emiting a lifecycle attach event
285
298
  if (config.hooks.attach) {
@@ -81,27 +81,65 @@ export default () =>
81
81
  function anonymous(component, elms, context, components, rootComponent, effect) {
82
82
  elms[0].set('color', component.color)
83
83
  },
84
- function anonymous(component, elms, context, components, rootComponent, effect) {
85
- elms[0].set('effects', component.effects)
86
- },
87
84
  ],
88
85
  context: {},
89
86
  },
90
87
  props: ['image', 'map', 'frame', 'color', 'effects'],
88
+
91
89
  state() {
92
90
  return {
93
91
  spriteTexture: null,
92
+ currentSrc: null,
94
93
  }
95
94
  },
95
+
96
96
  computed: {
97
97
  texture() {
98
- const options =
99
- 'frames' in this.map
100
- ? Object.assign({}, this.map.defaults || {}, this.map.frames[this.frame])
101
- : this.map[this.frame]
98
+ // If no image - nothing to render
99
+ if (this.image === undefined || this.image === null) {
100
+ return null
101
+ }
102
102
 
103
- if (this.spriteTexture !== null && options) {
104
- return this[symbols.renderer]().createTexture('SubTexture', {
103
+ // Get renderer
104
+ const renderer = this[symbols.renderer]()
105
+ if (renderer === null || renderer === undefined || renderer.createTexture === undefined) {
106
+ return null
107
+ }
108
+
109
+ // Recreate texture only when image src changes
110
+ if (
111
+ this.spriteTexture === undefined ||
112
+ this.spriteTexture === null ||
113
+ this.currentSrc !== this.image
114
+ ) {
115
+ this.spriteTexture = renderer.createTexture('ImageTexture', {
116
+ src: this.image,
117
+ })
118
+ this.currentSrc = this.image
119
+ }
120
+
121
+ // Resolve frame data from sprite map
122
+ let options = null
123
+ if (
124
+ this.map !== undefined &&
125
+ this.map !== null &&
126
+ this.frame !== undefined &&
127
+ this.frame !== null
128
+ ) {
129
+ options =
130
+ 'frames' in this.map
131
+ ? Object.assign({}, this.map.defaults || {}, this.map.frames[this.frame])
132
+ : this.map[this.frame]
133
+ }
134
+
135
+ // If no map but frame is object (manual subtexture)
136
+ if ((options === null || options === undefined) && typeof this.frame === 'object') {
137
+ options = this.frame
138
+ }
139
+
140
+ // Create SubTexture only if frame data exists
141
+ if (options !== null && options !== undefined) {
142
+ return renderer.createTexture('SubTexture', {
105
143
  texture: this.spriteTexture,
106
144
  x: options.x,
107
145
  y: options.y,
@@ -109,13 +147,9 @@ export default () =>
109
147
  height: options.h,
110
148
  })
111
149
  }
112
- },
113
- },
114
- hooks: {
115
- ready() {
116
- this.spriteTexture = this[symbols.renderer]().createTexture('ImageTexture', {
117
- src: this.image,
118
- })
150
+
151
+ // Default fallback (single image)
152
+ return this.spriteTexture
119
153
  },
120
154
  },
121
155
  })
package/src/launch.js CHANGED
@@ -18,7 +18,7 @@
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' assert { type: 'json' }
21
+ import blitsPackageInfo from '../package.json' with { type: 'json' }
22
22
 
23
23
  /**
24
24
  * @typedef {Object} Font
@@ -78,7 +78,9 @@ let navigationData = {}
78
78
  let navigatingBack = false
79
79
  let navigatingBackTo = undefined
80
80
  let previousFocus
81
-
81
+ // Skips internal router navigation when set to true only for the next "navigate"
82
+ // execution, needed for window.history management
83
+ let preventHashChangeNavigation = false
82
84
  /**
83
85
  * Get the current hash
84
86
  * @returns {Hash}
@@ -243,7 +245,7 @@ export const navigate = async function () {
243
245
  Announcer.clear()
244
246
  state.navigating = true
245
247
  let reuse = false
246
- if (this.parent[symbols.routes]) {
248
+ if (preventHashChangeNavigation === false && this.parent[symbols.routes]) {
247
249
  let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
248
250
  let route = matchHash(getHash(document.location.hash), this.parent[symbols.routes])
249
251
 
@@ -255,10 +257,22 @@ export const navigate = async function () {
255
257
  if (this.parent[symbols.routerHooks]) {
256
258
  const hooks = this.parent[symbols.routerHooks]
257
259
  if (hooks.beforeEach) {
258
- beforeEachResult = await hooks.beforeEach.call(this.parent, route, previousRoute)
259
- if (isString(beforeEachResult)) {
260
+ try {
261
+ beforeEachResult = await hooks.beforeEach.call(this.parent, route, previousRoute)
262
+ if (isString(beforeEachResult)) {
263
+ currentRoute = previousRoute
264
+ to(beforeEachResult)
265
+ return
266
+ }
267
+ } catch (error) {
268
+ Log.error('Error or Rejected Promise in "BeforeEach" Hook', error)
269
+
270
+ preventHashChangeNavigation = true
260
271
  currentRoute = previousRoute
261
- to(beforeEachResult)
272
+ window.history.back()
273
+
274
+ navigatingBack = false
275
+ state.navigating = false
262
276
  return
263
277
  }
264
278
  // If the resolved result is an object, redirect if the path in the object was changed
@@ -267,15 +281,37 @@ export const navigate = async function () {
267
281
  to(beforeEachResult.path, beforeEachResult.data, beforeEachResult.options)
268
282
  return
269
283
  }
284
+ // If the resolved result is false, cancel navigation
285
+ if (beforeEachResult === false) {
286
+ preventHashChangeNavigation = true
287
+ currentRoute = previousRoute
288
+ window.history.back()
289
+
290
+ navigatingBack = false
291
+ state.navigating = false
292
+ return
293
+ }
270
294
  }
271
295
  }
272
296
 
273
297
  let beforeHookOutput
274
298
  if (route.hooks.before) {
275
- beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
276
- if (isString(beforeHookOutput)) {
299
+ try {
300
+ beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
301
+ if (isString(beforeHookOutput)) {
302
+ currentRoute = previousRoute
303
+ to(beforeHookOutput)
304
+ return
305
+ }
306
+ } catch (error) {
307
+ Log.error('Error or Rejected Promise in "Before" Hook', error)
308
+
309
+ preventHashChangeNavigation = true
277
310
  currentRoute = previousRoute
278
- to(beforeHookOutput)
311
+ window.history.back()
312
+
313
+ navigatingBack = false
314
+ state.navigating = false
279
315
  return
280
316
  }
281
317
  // If the resolved result is an object, redirect if the path in the object was changed
@@ -284,6 +320,16 @@ export const navigate = async function () {
284
320
  to(beforeHookOutput.path, beforeHookOutput.data, beforeHookOutput.options)
285
321
  return
286
322
  }
323
+ // If the resolved result is false, cancel navigation
324
+ if (beforeHookOutput === false) {
325
+ preventHashChangeNavigation = true
326
+ currentRoute = previousRoute
327
+ window.history.back()
328
+
329
+ navigatingBack = false
330
+ state.navigating = false
331
+ return
332
+ }
287
333
  }
288
334
  // add the previous route (technically still the current route at this point)
289
335
  // into the history stack when inHistory is true and we're not navigating back
@@ -462,6 +508,7 @@ export const navigate = async function () {
462
508
  // reset navigating indicators
463
509
  navigatingBack = false
464
510
  state.navigating = false
511
+ preventHashChangeNavigation = false
465
512
  }
466
513
 
467
514
  /**
package/vite/index.js CHANGED
@@ -19,10 +19,18 @@ import preCompiler from './preCompiler.js'
19
19
  import msdfGenerator from './msdfGenerator.js'
20
20
  import blitsFileConverter from './blitsFileConverter.js'
21
21
  import reactivityGuard from './reactivityGuard.js'
22
+ import injectDevConfig from './inject-dev-config.js'
22
23
 
23
24
  export { default as preCompiler } from './preCompiler.js'
24
25
  export { default as msdfGenerator } from './msdfGenerator.js'
25
26
  export { default as blitsFileConverter } from './blitsFileConverter.js'
26
27
  export { default as reactivityGuard } from './reactivityGuard.js'
28
+ export { default as injectDevConfig } from './inject-dev-config.js'
27
29
 
28
- export default [blitsFileConverter(), reactivityGuard(), preCompiler(), msdfGenerator()]
30
+ export default [
31
+ injectDevConfig(),
32
+ blitsFileConverter(),
33
+ reactivityGuard(),
34
+ preCompiler(),
35
+ msdfGenerator(),
36
+ ]
@@ -0,0 +1,14 @@
1
+ export default function injectDevFlag() {
2
+ return {
3
+ name: 'inject-dev-config',
4
+ config(config, { mode }) {
5
+ return {
6
+ define: {
7
+ __DEV__: mode !== 'production',
8
+ // merge any existing define block
9
+ ...config.define,
10
+ },
11
+ }
12
+ },
13
+ }
14
+ }