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

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 (45) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +2 -0
  3. package/element-test-full.txt +0 -0
  4. package/index.d.ts +106 -13
  5. package/package.json +25 -4
  6. package/scripts/prepublishOnly.js +13 -4
  7. package/scripts/prepush.js +21 -1
  8. package/src/announcer/announcer.js +50 -4
  9. package/src/announcer/speechSynthesis.js +156 -37
  10. package/src/application.js +95 -3
  11. package/src/component/base/methods.js +5 -2
  12. package/src/component/base/router.js +6 -0
  13. package/src/component/base/timeouts_intervals.js +56 -0
  14. package/src/component/setup/routes.js +5 -0
  15. package/src/component/setup/state.js +2 -0
  16. package/src/component.js +64 -24
  17. package/src/components/Circle.js +5 -3
  18. package/src/components/FPScounter.js +1591 -1577
  19. package/src/components/RouterView.js +141 -124
  20. package/src/components/Sprite.js +29 -8
  21. package/src/components/index.js +0 -2
  22. package/src/engines/L3/element.js +101 -7
  23. package/src/engines/L3/launch.js +4 -1
  24. package/src/{focus.js → focus/focus.js} +6 -18
  25. package/src/focus/helpers.js +31 -0
  26. package/src/focus/hover.js +94 -0
  27. package/src/launch.js +1 -0
  28. package/src/lib/codegenerator/generator.js +44 -8
  29. package/src/lib/lifecycle.js +34 -3
  30. package/src/lib/log.js +4 -4
  31. package/src/lib/precompiler/precompiler.js +23 -14
  32. package/src/lib/reactivity/effect.js +3 -2
  33. package/src/lib/reactivityguard/computedprops.js +86 -15
  34. package/src/lib/shaders/shaders.js +2 -1
  35. package/src/lib/symbols.js +2 -0
  36. package/src/plugins/appstate.d.ts +26 -0
  37. package/src/plugins/index.d.ts +28 -0
  38. package/src/plugins/language.d.ts +37 -0
  39. package/src/plugins/storage/storage.d.ts +31 -0
  40. package/src/plugins/theme.d.ts +43 -0
  41. package/src/plugins/theme.js +30 -3
  42. package/src/router/router.js +78 -14
  43. package/test-output.txt +0 -0
  44. package/vite/preCompiler.js +15 -5
  45. package/vscode/data/template-attributes.json +335 -86
@@ -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
+ }
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
@@ -825,8 +856,13 @@ const cast = (val = '', key = false, component = 'component.') => {
825
856
  }
826
857
  // @-listener
827
858
  else if (key.startsWith('@') && val) {
828
- const c = component.slice(0, -1)
829
- castedValue = `${c}['${val.replace('$', '')}'] && ${c}['${val.replace('$', '')}'].bind(${c})`
859
+ const trimmed = val.trim()
860
+ if (/^\$?\w+$/.test(trimmed)) {
861
+ const c = component.slice(0, -1)
862
+ castedValue = `${c}['${trimmed.replace('$', '')}'] && ${c}['${trimmed.replace('$', '')}'].bind(${c})`
863
+ } else {
864
+ castedValue = interpolate(trimmed, component)
865
+ }
830
866
  }
831
867
  // dynamic value
832
868
  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 receiving focus (can occur multiple times)
35
+ 'unhover', // fired when losing focus (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()) {
@@ -35,10 +35,12 @@ const findMatchingBrace = (text, startPos) => {
35
35
  // Skip comments when not in strings
36
36
  else if (!inString && char === '/' && text[position + 1] === '/') {
37
37
  // Skip line comment
38
- position = text.indexOf('\n', position) || text.length - 1
38
+ const nextNewline = text.indexOf('\n', position + 2)
39
+ position = nextNewline === -1 ? text.length - 1 : nextNewline
39
40
  } else if (!inString && char === '/' && text[position + 1] === '*') {
40
41
  // Skip block comment
41
- position = text.indexOf('*/', position + 2) || text.length - 1
42
+ const end = text.indexOf('*/', position + 2)
43
+ position = end === -1 ? text.length - 1 : end + 1
42
44
  }
43
45
  // Count braces only outside strings and comments
44
46
  else if (!inString) {
@@ -60,6 +62,44 @@ const findComputedFunctionEnd = (computedObj, openingBracePos) => {
60
62
  return findMatchingBrace(computedObj, openingBracePos)
61
63
  }
62
64
 
65
+ // Skip whitespace + commas + comments (line and block) starting from `pos`, return next code position.
66
+ const skipNonCode = (text, pos) => {
67
+ let p = pos
68
+ while (p < text.length) {
69
+ const c = text[p]
70
+
71
+ // whitespace
72
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
73
+ p++
74
+ continue
75
+ }
76
+
77
+ // separators between object members
78
+ if (c === ',') {
79
+ p++
80
+ continue
81
+ }
82
+
83
+ // line comment
84
+ if (c === '/' && text[p + 1] === '/') {
85
+ const nextNewline = text.indexOf('\n', p + 2)
86
+ p = nextNewline === -1 ? text.length : nextNewline + 1
87
+ continue
88
+ }
89
+
90
+ // block comment
91
+ if (c === '/' && text[p + 1] === '*') {
92
+ const end = text.indexOf('*/', p + 2)
93
+ p = end === -1 ? text.length : end + 2
94
+ continue
95
+ }
96
+
97
+ return p
98
+ }
99
+
100
+ return p
101
+ }
102
+
63
103
  export default (code) => {
64
104
  // Find component declarations
65
105
  const componentRegex =
@@ -83,6 +123,8 @@ export default (code) => {
83
123
 
84
124
  if (computedEnd !== -1) {
85
125
  const computedObj = configObject.substring(computedStart, computedEnd)
126
+ const computedBlockStart = computedKeywordMatch.index
127
+ const computedBlockEnd = computedEnd
86
128
 
87
129
  // Process each computed property
88
130
  const computedProps = []
@@ -93,15 +135,20 @@ export default (code) => {
93
135
  // using cursor-based parsing gives better accuracy
94
136
  while (currentPos < computedObj.length - 1) {
95
137
  // -1 to exclude the closing brace
96
- // Find the next function name
97
- const functionNameMatch = /\s*(\w+)\s*\(\)/.exec(computedObj.substring(currentPos))
138
+ // Skip whitespace/comments so we don't match commented-out computed functions
139
+ currentPos = skipNonCode(computedObj, currentPos)
140
+ if (currentPos >= computedObj.length - 1) break
141
+
142
+ // Find the next function name (computed properties are `name() { ... }`)
143
+ const functionNameMatch = /^(\w+)\s*\(\)\s*/.exec(computedObj.substring(currentPos))
98
144
  if (!functionNameMatch) break
99
145
 
100
146
  const funcName = functionNameMatch[1]
101
- const funcNamePos = currentPos + functionNameMatch.index
147
+ const funcNamePos = currentPos
102
148
 
103
149
  // Find opening brace for this function
104
- const openingBracePos = computedObj.indexOf('{', funcNamePos)
150
+ const afterSignaturePos = funcNamePos + functionNameMatch[0].length
151
+ const openingBracePos = computedObj.indexOf('{', afterSignaturePos)
105
152
  if (openingBracePos === -1) break
106
153
 
107
154
  // Find closing brace for individual computed function
@@ -133,21 +180,22 @@ export default (code) => {
133
180
  }
134
181
 
135
182
  // Store the original computed object for later replacement
136
- const originalComputedObj = configObject.substring(
137
- computedStart - 'computed: '.length,
138
- computedEnd
139
- )
183
+ const originalComputedObj = configObject.substring(computedBlockStart, computedBlockEnd)
184
+ const computedObjOffsetInOriginal = computedStart - computedBlockStart
185
+ const computedObjEndInOriginal = computedObjOffsetInOriginal + computedObj.length
140
186
 
141
187
  let modifiedComputedObj = originalComputedObj
142
188
  let hasChanges = false
143
189
 
190
+ const replacements = []
191
+
144
192
  for (const prop of computedProps) {
145
193
  const thisRefs = extractThisReferences(prop.body)
146
194
 
147
195
  if (thisRefs.size > 0) {
148
196
  // Skip if already modified
149
197
  if (
150
- prop.body.includes(commentText) ||
198
+ prop.body.includes(commentText.trim()) ||
151
199
  Array.from(thisRefs).some((ref) => prop.body.startsWith(ref))
152
200
  ) {
153
201
  continue
@@ -181,13 +229,31 @@ export default (code) => {
181
229
  prop.fullText.substring(0, openBraceIndex).match(/^(\s*)/)[0] +
182
230
  '}'
183
231
 
184
- // Replace this property in the computed object text
185
- modifiedComputedObj = modifiedComputedObj.replace(prop.fullText, modifiedPropText)
186
- hasChanges = true
232
+ replacements.push({
233
+ start: prop.startPos,
234
+ end: prop.endPos,
235
+ text: modifiedPropText,
236
+ })
187
237
  }
188
238
  }
189
239
  }
190
240
 
241
+ if (replacements.length > 0) {
242
+ let modifiedComputedInner = computedObj
243
+
244
+ replacements.sort((a, b) => b.start - a.start)
245
+ for (const r of replacements) {
246
+ modifiedComputedInner =
247
+ modifiedComputedInner.slice(0, r.start) + r.text + modifiedComputedInner.slice(r.end)
248
+ }
249
+
250
+ modifiedComputedObj =
251
+ originalComputedObj.slice(0, computedObjOffsetInOriginal) +
252
+ modifiedComputedInner +
253
+ originalComputedObj.slice(computedObjEndInOriginal)
254
+ hasChanges = true
255
+ }
256
+
191
257
  if (hasChanges) {
192
258
  // Store the modification of the entire computed object
193
259
  modifications.push({
@@ -213,7 +279,12 @@ export default (code) => {
213
279
  modifiedCode = modifiedCode.replace(mod.original, mod.replacement)
214
280
  }
215
281
 
216
- return { code: modifiedCode }
282
+ // Returning code without a source map
283
+ // Vite will automatically chain this with the next plugin's source map (preCompiler)
284
+ return {
285
+ code: modifiedCode,
286
+ map: { mappings: '' },
287
+ }
217
288
  }
218
289
 
219
290
  return null
@@ -139,11 +139,12 @@ export default {
139
139
  return v
140
140
  }
141
141
  if (typeof v === 'string') {
142
- return colors.normalize(v)
142
+ return colors.normalize(v, v)
143
143
  }
144
144
  if (Array.isArray(v) === true) {
145
145
  return v.map((i) => this.parseProp(i))
146
146
  }
147
+ return v
147
148
  },
148
149
  parseProps(v) {
149
150
  if (typeof v === 'string') {
@@ -17,6 +17,7 @@
17
17
  * @property {symbol} inputEvents
18
18
  * @property {symbol} internalEvent
19
19
  * @property {symbol} intervals
20
+ * @property {symbol} debounces
20
21
  * @property {symbol} isProxy
21
22
  * @property {symbol} launched
22
23
  * @property {symbol} level
@@ -73,6 +74,7 @@ export default {
73
74
  inputEvents: Symbol('inputEvents'),
74
75
  internalEvent: Symbol('internalEvent'),
75
76
  intervals: Symbol('intervals'),
77
+ debounces: Symbol('debounces'),
76
78
  isProxy: Symbol('isProxy'),
77
79
  launched: Symbol('launched'),
78
80
  level: Symbol('level'),
@@ -0,0 +1,26 @@
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
+
18
+ export type AppStatePlugin<TState extends Record<string, unknown> = Record<string, unknown>> = TState
19
+
20
+ declare const appState: {
21
+ readonly name: 'appState'
22
+ plugin: <TState extends Record<string, unknown>>(state?: TState) => AppStatePlugin<TState>
23
+ }
24
+
25
+ export default appState
26
+
@@ -0,0 +1,28 @@
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
+
18
+ export { default as language } from './language.js'
19
+ export { default as theme } from './theme.js'
20
+ export { default as appState } from './appstate.js'
21
+ export { default as storage } from './storage/storage.js'
22
+
23
+ // Re-export plugin interfaces for convenience
24
+ export type { LanguagePlugin, LanguagePluginOptions } from './language.js'
25
+ export type { ThemePlugin, ThemePluginConfig } from './theme.js'
26
+ export type { StoragePlugin } from './storage/storage.js'
27
+ export type { AppStatePlugin } from './appstate.js'
28
+
@@ -0,0 +1,37 @@
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
+
18
+ export interface LanguagePlugin {
19
+ translate(key: string, ...replacements: any[]): string
20
+ readonly language: string
21
+ set(language: string): void
22
+ translations(translationsObject: Record<string, unknown>): void
23
+ load(file: string): Promise<void>
24
+ }
25
+
26
+ export interface LanguagePluginOptions {
27
+ file?: string
28
+ language?: string
29
+ }
30
+
31
+ declare const language: {
32
+ readonly name: 'language'
33
+ plugin: (options?: LanguagePluginOptions) => LanguagePlugin
34
+ }
35
+
36
+ export default language
37
+