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

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 +169 -41
  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
@@ -1,3 +1,20 @@
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
+
1
18
  import Element from './element.js'
2
19
  import Launch from './launch.js'
3
20
 
@@ -100,6 +100,7 @@ export default (App, target, settings = {}) => {
100
100
  textureMemory: textureMemorySettings(settings),
101
101
  createImageBitmapSupport: 'auto',
102
102
  targetFPS: 'maxFPS' in settings ? settings.maxFPS : 0,
103
+ platform: 'platform' in settings ? settings.platform : null,
103
104
  },
104
105
  ...(settings.advanced || {}),
105
106
  },
@@ -118,7 +119,9 @@ export default (App, target, settings = {}) => {
118
119
 
119
120
  shaderLoader()
120
121
  fontLoader()
121
- initApp()
122
+ // Defer app initialization until after the current call stack is cleared,
123
+ // allowing the renderer to finish setting up before the app starts creating components
124
+ Promise.resolve().then(initApp)
122
125
 
123
126
  return renderer
124
127
  }
@@ -1,3 +1,20 @@
1
+ /*
2
+ * Copyright 2024 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 Settings from '../../settings.js'
2
19
  import { renderer } from './launch.js'
3
20
  import * as webglShaders from '@lightningjs/renderer/webgl/shaders'
@@ -1 +1,18 @@
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
+
1
18
  export { default as L3 } from './L3/index.js'
@@ -15,11 +15,12 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import symbols from './lib/symbols.js'
19
- import { state } from './router/router.js'
20
- import Settings from './settings.js'
21
- import { DEFAULT_HOLD_TIMEOUT_MS } from './constants.js'
22
- import { Log } from './lib/log.js'
18
+ import symbols from '../lib/symbols.js'
19
+ import { state } from '../router/router.js'
20
+ import Settings from '../settings.js'
21
+ import { DEFAULT_HOLD_TIMEOUT_MS } from '../constants.js'
22
+ import { Log } from '../lib/log.js'
23
+ import { getAncestors } from './helpers.js'
23
24
 
24
25
  let focusedComponent = null
25
26
  let focusChain = []
@@ -99,19 +100,6 @@ export default {
99
100
  },
100
101
  }
101
102
 
102
- /**
103
- * Recursive function that retrieves the ancestors of a component
104
- * @param {Array} components
105
- * @returns array components
106
- */
107
- const getAncestors = (components) => {
108
- if (components[0][symbols.parent] !== undefined) {
109
- components.unshift(components[0][symbols.parent])
110
- return getAncestors(components)
111
- }
112
- return components
113
- }
114
-
115
103
  /**
116
104
  * Get the component in the ancestors chain that has a handler for a certain key code
117
105
  * @param {Object} component
@@ -0,0 +1,31 @@
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 symbols from '../lib/symbols.js'
19
+
20
+ /**
21
+ * Recursive function that retrieves the ancestors of a component
22
+ * @param {Array} components
23
+ * @returns array components
24
+ */
25
+ export const getAncestors = (components) => {
26
+ if (components[0][symbols.parent] !== undefined) {
27
+ components.unshift(components[0][symbols.parent])
28
+ return getAncestors(components)
29
+ }
30
+ return components
31
+ }
@@ -0,0 +1,94 @@
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 { Log } from '../lib/log.js'
19
+ import { getAncestors } from './helpers.js'
20
+ import symbols from '../lib/symbols.js'
21
+
22
+ let hoveredComponent = null
23
+ let hoverChain = []
24
+
25
+ export default {
26
+ get() {
27
+ return hoveredComponent
28
+ },
29
+ set(component) {
30
+ if (component === undefined || component.eol === true) return
31
+
32
+ // early return if already hovered
33
+ if (component === hoveredComponent) return
34
+
35
+ if (hoveredComponent === null) {
36
+ hoverChain = getAncestors([component])
37
+ }
38
+
39
+ // unhover currently hovered components
40
+ if (hoveredComponent !== null) {
41
+ if (hoverChain[hoverChain.length - 1] === component.$parent) {
42
+ hoverChain.push(component)
43
+ } else {
44
+ const newhoverChain = getAncestors([component])
45
+ let i = hoverChain.length
46
+ while (i--) {
47
+ // don't unhover when part of the new hover chain
48
+ if (newhoverChain.indexOf(hoverChain[i]) > -1) break
49
+ // skip if component is destroyed
50
+ if (hoverChain[i].eol !== true) {
51
+ hoverChain[i][symbols.lifecycle].state = 'unhover'
52
+ }
53
+ }
54
+ hoverChain = newhoverChain
55
+ }
56
+ }
57
+
58
+ // ensure that all components in the hover path have hover state
59
+ let i = 0
60
+ while (i < hoverChain.length - 1) {
61
+ // skip if component is destroyed
62
+ if (hoverChain[i].eol !== true) {
63
+ hoverChain[i][symbols.lifecycle].state = 'hover'
64
+ }
65
+ i++
66
+ }
67
+
68
+ setHover(component)
69
+ },
70
+ clear() {
71
+ if (hoveredComponent === null) return
72
+ for (let i = 0; i < hoverChain.length; i++) {
73
+ if (hoverChain[i].eol !== true) {
74
+ hoverChain[i][symbols.lifecycle].state = 'unhover'
75
+ }
76
+ }
77
+ hoveredComponent = null
78
+ hoverChain = []
79
+ },
80
+ }
81
+
82
+ /**
83
+ * Set the hover to the Component
84
+ * @param {Object} component - The component to hover
85
+ */
86
+ const setHover = (component) => {
87
+ Log.info(
88
+ '\nHover chain:\n',
89
+ hoverChain.map((c, index) => '\t'.repeat(index) + '↳ ' + c.$componentId).join('\n')
90
+ )
91
+
92
+ hoveredComponent = component
93
+ component[symbols.lifecycle].state = 'hover'
94
+ }
@@ -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 2020 Metrological
6
- *
7
- * Licensed under the Apache License, Version 2.0 (the License);
2
+ * Copyright 2024 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
  export default (file) => {
package/src/launch.js CHANGED
@@ -88,6 +88,7 @@ async function rendererVersion() {
88
88
  * @property {number} [textureProcessingTimeLimit] - Max time renderer can process textures in a frame
89
89
  * @property {Partial<import('@lightningjs/renderer').RendererMainSettings>} [advanced] - Advanced renderer settings
90
90
  * @property {boolean} [announcer] - Whether or not the announcer should be activated on initialization
91
+ * @property {boolean} [enableMouse] - Enable mouse support (hover and click-to-focus). Defaults to false.
91
92
  * @property {number} [maxFPS] - Maximum FPS
92
93
  */
93
94
 
@@ -163,6 +163,10 @@ const generateElementCode = function (
163
163
 
164
164
  renderCode.push(`elementConfigs[${counter}] = {}`)
165
165
 
166
+ if (counter === 0) {
167
+ renderCode.push(`elementConfigs[${counter}]['___wrapper'] = true `)
168
+ }
169
+
166
170
  if (options.forloop) {
167
171
  renderCode.push(`if(${elm} === undefined) {`)
168
172
  }
@@ -182,6 +186,10 @@ const generateElementCode = function (
182
186
  renderCode.push(`elementConfigs[${counter}][Symbol.for('isSlot')] = true`)
183
187
  }
184
188
 
189
+ if (options.holder) {
190
+ renderCode.push(`elementConfigs[${counter}]['holder'] = true`)
191
+ }
192
+
185
193
  Object.keys(templateObject).forEach((key) => {
186
194
  if (key === 'slot') {
187
195
  renderCode.push(`
@@ -325,9 +333,11 @@ const generateComponentCode = function (
325
333
 
326
334
  const children = templateObject.children
327
335
  delete templateObject.children
336
+ // Capture holder counter before generating element code (which may process children and increment counter)
337
+ const holderCounter = counter
328
338
  generateElementCode.call(this, templateObject, parent, { ...options, ...{ holder: true } })
329
339
 
330
- parent = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
340
+ parent = options.key ? `elms[${holderCounter}][${options.key}]` : `elms[${holderCounter}]`
331
341
 
332
342
  counter++
333
343
 
@@ -396,6 +406,21 @@ const generateComponentCode = function (
396
406
  }
397
407
  `)
398
408
 
409
+ if (isDev === true) {
410
+ const templateTagName = templateObject[Symbol.for('componentType')]
411
+ const holderElm = options.key
412
+ ? `elms[${holderCounter}][${options.key}]`
413
+ : `elms[${holderCounter}]`
414
+ const componentDisplayName = `typeof componentType === 'string'
415
+ ? componentType
416
+ : (componentType?.[Symbol.for('componentType')] || '${templateTagName}')`
417
+ renderCode.push(`
418
+ if (${holderElm} !== undefined && typeof ${holderElm}.setInspectorMetadata === 'function') {
419
+ ${holderElm}.setInspectorMetadata({ 'blits-componentType': ${componentDisplayName} })
420
+ }
421
+ `)
422
+ }
423
+
399
424
  this.cleanupCode.push(`components[${counter}] = null`)
400
425
 
401
426
  if (options.forloop) {
@@ -451,7 +476,7 @@ const generateForLoopCode = function (templateObject, parent) {
451
476
  .replace(')', '')
452
477
  .split(/\s*,\s*/)
453
478
 
454
- const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)(\\$?\\w+))`, 'gi')
479
+ const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)([\\w$]+))`, 'gi')
455
480
 
456
481
  // local context
457
482
  const ctx = {
@@ -524,10 +549,16 @@ const generateForLoopCode = function (templateObject, parent) {
524
549
 
525
550
  component !== null && component[Symbol.for('removeGlobalEffects')](effects[${forStartCounter}])
526
551
 
527
- for(let i = 0; i < effects[${forStartCounter}].length; i++) {
528
- const value = effects[${forStartCounter}][i]
529
- const index = component[Symbol.for('effects')].indexOf(value)
530
- if (index > -1) component[Symbol.for('effects')].splice(index, 1)
552
+ const effectsToRemove = new Set(effects[${forStartCounter}].slice(0))
553
+ if (effectsToRemove.size > 0) {
554
+ const componentEffects = component?.[Symbol.for('effects')] || []
555
+ let writeIndex = 0
556
+ for (let readIndex = 0; readIndex < componentEffects.length; readIndex++) {
557
+ if (!effectsToRemove.has(componentEffects[readIndex])) {
558
+ componentEffects[writeIndex++] = componentEffects[readIndex]
559
+ }
560
+ }
561
+ componentEffects.length = writeIndex
531
562
  }
532
563
 
533
564
  effects[${forStartCounter}].length = 0
@@ -633,11 +664,21 @@ const generateForLoopCode = function (templateObject, parent) {
633
664
  const forEndCounter = counter
634
665
 
635
666
  for (let i = forStartCounter; i <= forEndCounter; i++) {
636
- destroyCode.push(`
667
+ if (i == forStartCounter) {
668
+ destroyCode.push(`
637
669
  elms[${i}][key] && elms[${i}][key].destroy()
638
670
  elms[${i}][key] = null
639
671
  delete elms[${i}][key]
640
672
  `)
673
+ } else {
674
+ destroyCode.push(`
675
+ if (elms[${i}][key] && elms[${i}][key].$componentId !== undefined) {
676
+ elms[${i}][key] && elms[${i}][key].destroy()
677
+ }
678
+ elms[${i}][key] = null
679
+ delete elms[${i}][key]
680
+ `)
681
+ }
641
682
  }
642
683
  destroyCode.push(`
643
684
  }
@@ -825,8 +866,13 @@ const cast = (val = '', key = false, component = 'component.') => {
825
866
  }
826
867
  // @-listener
827
868
  else if (key.startsWith('@') && val) {
828
- const c = component.slice(0, -1)
829
- castedValue = `${c}['${val.replace('$', '')}'] && ${c}['${val.replace('$', '')}'].bind(${c})`
869
+ const trimmed = val.trim()
870
+ if (/^\$?\w+$/.test(trimmed)) {
871
+ const c = component.slice(0, -1)
872
+ castedValue = `${c}['${trimmed.replace('$', '')}'] && ${c}['${trimmed.replace('$', '')}'].bind(${c})`
873
+ } else {
874
+ castedValue = interpolate(trimmed, component)
875
+ }
830
876
  }
831
877
  // dynamic value
832
878
  else if (val.startsWith('$')) {
@@ -18,6 +18,9 @@
18
18
  import { Log } from './log.js'
19
19
  import { emit, privateEmit } from './hooks.js'
20
20
  import symbols from './symbols.js'
21
+ import Settings from '../settings.js'
22
+
23
+ let inspectorEnabled = null
21
24
 
22
25
  /**
23
26
  * List of valid lifecycle states for a component.
@@ -28,6 +31,8 @@ const states = [
28
31
  'ready', // fired when component instantiated, reactivity setup done and template spawned
29
32
  'focus', // fired when receiving focus (can occur multiple times)
30
33
  'unfocus', // fired when losing focus (can occur multiple times)
34
+ 'hover', // fired when pointer enters the component (mouse support only; can occur multiple times)
35
+ 'unhover', // fired when pointer leaves the component (mouse support only; can occur multiple times)
31
36
  'destroy', // fired when component is destroyed and removed
32
37
  'attach', // fired when entering the viewport margin and attached to the render tree
33
38
  'detach', // fired when leaving the viewport margin and detached from the render tree
@@ -81,9 +86,35 @@ export default {
81
86
  privateEmit(v, this.component[symbols.identifier], this.component)
82
87
  // emit 'public' hook
83
88
  emit(v, this.component[symbols.identifier], this.component)
84
- // update the built-in hasFocus state variable
85
- if (v === 'focus') this.component[symbols.state].$hasFocus = true
86
- if (v === 'unfocus') this.component[symbols.state].$hasFocus = false
89
+ // update the built-in $hasFocus state variable
90
+ if (v === 'focus' || v === 'unfocus') {
91
+ if (inspectorEnabled === null) {
92
+ inspectorEnabled = Settings.get('inspector', false)
93
+ }
94
+ }
95
+ // update the built-in isHovered state variable
96
+ if (v === 'hover') this.component[symbols.state].$isHovered = true
97
+ if (v === 'unhover') this.component[symbols.state].$isHovered = false
98
+ if (v === 'focus') {
99
+ this.component[symbols.state].$hasFocus = true
100
+ if (
101
+ inspectorEnabled === true &&
102
+ this.component[symbols.holder] &&
103
+ typeof this.component[symbols.holder].setInspectorMetadata === 'function'
104
+ ) {
105
+ this.component[symbols.holder].setInspectorMetadata({ 'blits-hasFocus': true })
106
+ }
107
+ }
108
+ if (v === 'unfocus') {
109
+ this.component[symbols.state].$hasFocus = false
110
+ if (
111
+ inspectorEnabled === true &&
112
+ this.component[symbols.holder] &&
113
+ typeof this.component[symbols.holder].setInspectorMetadata === 'function'
114
+ ) {
115
+ this.component[symbols.holder].setInspectorMetadata({ 'blits-hasFocus': false })
116
+ }
117
+ }
87
118
  }
88
119
  },
89
120
  }
package/src/lib/log.js CHANGED
@@ -46,7 +46,7 @@ const logger = (context) => {
46
46
  return (
47
47
  ((level >= 1 || (Array.isArray(level) && level.indexOf('info') > -1)) &&
48
48
  console.info.bind(
49
- window.console,
49
+ console,
50
50
  `%c ⚡️ ${context} %c ${time()}`,
51
51
  'background-color: #0284c7; color: white; padding: 3px 6px 3px 1px; border-radius: 3px',
52
52
  'color: ##94a3b8;'
@@ -61,7 +61,7 @@ const logger = (context) => {
61
61
  return (
62
62
  ((level >= 1 || (Array.isArray(level) && level.indexOf('warn') > -1)) &&
63
63
  console.warn.bind(
64
- window.console,
64
+ console,
65
65
  `%c ⚡️ ${context} %c ${time()}`,
66
66
  'background-color: #fbbf24; color: white; padding: 3px 6px 3px 1px; border-radius: 3px',
67
67
  'color: ##94a3b8;'
@@ -76,7 +76,7 @@ const logger = (context) => {
76
76
  return (
77
77
  ((level >= 1 || (Array.isArray(level) && level.indexOf('error') > -1)) &&
78
78
  console.error.bind(
79
- window.console,
79
+ console,
80
80
  `%c ⚡️ ${context} %c ${time()}`,
81
81
  'background-color: #dc2626; color: white; padding: 3px 6px 3px 1px; border-radius: 3px',
82
82
  'color: ##94a3b8;'
@@ -91,7 +91,7 @@ const logger = (context) => {
91
91
  return (
92
92
  ((level >= 2 || (Array.isArray(level) && level.indexOf('debug') > -1)) &&
93
93
  console.debug.bind(
94
- window.console,
94
+ console,
95
95
  `%c ⚡️ ${context} %c (${new Date().toLocaleTimeString([], {
96
96
  hour: '2-digit',
97
97
  minute: '2-digit',
@@ -17,6 +17,7 @@
17
17
 
18
18
  import parser from '../templateparser/parser.js'
19
19
  import generator from '../codegenerator/generator.js'
20
+ import MagicString from 'magic-string'
20
21
 
21
22
  export default (source, filePath, mode) => {
22
23
  if (
@@ -26,12 +27,9 @@ export default (source, filePath, mode) => {
26
27
  /\{.*?template\s*:\s*(['"`])((?:\\?.)*?)\1.*?\}/s.test(source) // object with template key
27
28
  ) {
28
29
  const templates = source.matchAll(/(?<!\/\/\s*)template\s*:\s*(['"`])((?:\\?.)*?)\1/gs)
29
- let newSource = source
30
- /*
31
- if there are multiple templates in the file, we need to keep
32
- track of the offset caused by the previous replacements
33
- */
34
- let offset = 0
30
+
31
+ // Use MagicString to track changes
32
+ const s = new MagicString(source)
35
33
 
36
34
  for (const template of templates) {
37
35
  if (template[2]) {
@@ -39,7 +37,8 @@ export default (source, filePath, mode) => {
39
37
 
40
38
  // Only process if it looks like a Blits template
41
39
  if (templateContent.match(/^\s*(<!--[\s\S]*?-->|<[A-Za-z][^>]*>)/s)) {
42
- const templateStartIndex = template.index + offset
40
+ // Use original indices - MagicString handles position tracking automatically
41
+ const templateStartIndex = template.index
43
42
  const templateEndIndex = templateStartIndex + template[0].length
44
43
 
45
44
  // Parse the template
@@ -58,16 +57,26 @@ export default (source, filePath, mode) => {
58
57
  (fn) => fn.toString()
59
58
  )}], context: {}}`
60
59
 
61
- offset += replacement.length - template[0].length
62
-
63
- newSource =
64
- newSource.substring(0, templateStartIndex) +
65
- replacement +
66
- newSource.substring(templateEndIndex)
60
+ // MagicString tracks all changes automatically, no offset needed
61
+ s.overwrite(templateStartIndex, templateEndIndex, replacement)
67
62
  }
68
63
  }
69
64
  }
70
- return newSource
65
+
66
+ const newSource = s.toString()
67
+
68
+ if (newSource !== source) {
69
+ const fileName = filePath.split(/[\\/]/).pop()
70
+ return {
71
+ code: newSource,
72
+ map: s.generateMap({
73
+ hires: true,
74
+ source: fileName,
75
+ includeContent: true,
76
+ file: fileName,
77
+ }),
78
+ }
79
+ }
71
80
  }
72
81
  return source
73
82
  }
@@ -32,9 +32,10 @@ const objectMap = new WeakMap()
32
32
  const globalEffectsMap = new Map()
33
33
 
34
34
  export const removeGlobalEffects = (effectsToRemove) => {
35
- if (globalEffectsMap.size === 0) return
35
+ if (globalEffectsMap.size === 0 || effectsToRemove.length === 0) return
36
+ const effectsToRemoveSet = new Set(effectsToRemove)
36
37
  for (const [effect, target] of globalEffectsMap) {
37
- if (effectsToRemove.indexOf(effect) === -1) continue
38
+ if (!effectsToRemoveSet.has(effect)) continue
38
39
  const effectsSet = objectMap.get(target)
39
40
  if (effectsSet === undefined) continue
40
41
  for (const set of effectsSet.values()) {