@lightningjs/blits 1.45.0 → 2.0.0-beta.1

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 (38) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/README.md +0 -2
  3. package/index.d.ts +23 -86
  4. package/package.json +5 -24
  5. package/scripts/prepublishOnly.js +1 -4
  6. package/src/component/base/methods.js +16 -76
  7. package/src/component/base/utils.js +18 -4
  8. package/src/component/setup/props.js +37 -28
  9. package/src/component/setup/state.js +4 -4
  10. package/src/component.js +25 -16
  11. package/src/components/Circle.js +49 -7
  12. package/src/components/FPScounter.js +226 -88
  13. package/src/components/RouterView.js +50 -5
  14. package/src/components/Sprite.js +61 -20
  15. package/src/engines/L3/element.js +109 -86
  16. package/src/engines/L3/fontLoader.js +26 -31
  17. package/src/engines/L3/shaderLoader.js +56 -6
  18. package/src/focus.js +9 -9
  19. package/src/launch.js +1 -2
  20. package/src/lib/codegenerator/generator.js +117 -16
  21. package/src/lib/colors/colors.js +0 -1
  22. package/src/lib/lifecycle.js +3 -3
  23. package/src/lib/precompiler/precompiler.js +14 -23
  24. package/src/lib/reactivityguard/computedprops.js +1 -6
  25. package/src/lib/shaders/shaders.js +163 -0
  26. package/src/lib/symbols.js +8 -0
  27. package/src/lib/templateparser/parser.js +6 -2
  28. package/src/lib/utils.js +36 -0
  29. package/src/router/router.js +7 -7
  30. package/vite/blitsTips.js +57 -0
  31. package/vite/msdfGenerator.js +52 -6
  32. package/vite/preCompiler.js +5 -15
  33. package/vscode/data/template-attributes.json +112 -0
  34. package/src/plugins/appstate.d.ts +0 -29
  35. package/src/plugins/index.d.ts +0 -29
  36. package/src/plugins/language.d.ts +0 -34
  37. package/src/plugins/storage/storage.d.ts +0 -29
  38. package/src/plugins/theme.d.ts +0 -35
@@ -18,10 +18,13 @@
18
18
  let counter
19
19
  let isDev
20
20
 
21
+ import { elementAttributes } from '../../engines/L3/element.js'
22
+
21
23
  export default function (templateObject = { children: [] }, devMode = false) {
22
24
  const ctx = {
23
25
  renderCode: [
24
26
  'const elms = []',
27
+ `const validAttributes = ${JSON.stringify(elementAttributes)}`,
25
28
  'const elementConfigs = []',
26
29
  'const forloops = []',
27
30
  'const props = []',
@@ -81,7 +84,7 @@ export default function (templateObject = { children: [] }, devMode = false) {
81
84
  forloops.length = 0
82
85
  props.length = 0
83
86
  skips.length = 0
84
- }}
87
+ }, skips}
85
88
  `)
86
89
 
87
90
  return {
@@ -97,7 +100,16 @@ export default function (templateObject = { children: [] }, devMode = false) {
97
100
  ),
98
101
  effects: ctx.effectsCode.map(
99
102
  (code) =>
100
- new Function('component', 'elms', 'context', 'components', 'rootComponent', 'effect', code)
103
+ new Function(
104
+ 'component',
105
+ 'elms',
106
+ 'context',
107
+ 'components',
108
+ 'rootComponent',
109
+ 'skips',
110
+ 'effect',
111
+ code
112
+ )
101
113
  ),
102
114
  context: ctx.context,
103
115
  }
@@ -220,13 +232,43 @@ const generateElementCode = function (
220
232
  }
221
233
  })
222
234
 
235
+ if (templateObject[Symbol.for('tagContent')] !== undefined) {
236
+ const val = templateObject[Symbol.for('tagContent')]
237
+
238
+ // Check if the string contains interpolation (`{{ }}`).
239
+ const regex = /\{\{\s*.+?\s*\}\}/
240
+ const containsInterpolation = regex.test(val)
241
+
242
+ if (containsInterpolation === false) {
243
+ // Inline text is treated as static content when interpolation is not defined.
244
+ renderCode.push(`elementConfigs[${counter}]['content'] = '${val}'`)
245
+ } else {
246
+ const output = parseTagContent(val, options.component)
247
+ renderCode.push(`elementConfigs[${counter}]['content'] = ${output}`)
248
+
249
+ const variableRegEx = /\{\{\s*(?=[^}]*\$).+?\s*\}\}/
250
+ // Check if the interpolation contains $ variable
251
+ const isReactive = variableRegEx.test(val)
252
+ if (isReactive === true) {
253
+ this.effectsCode.push(`
254
+ ${elm}.set('content', ${output})
255
+ `)
256
+ }
257
+ }
258
+ }
259
+
223
260
  if (options.holder === true) {
224
261
  renderCode.push(`
225
262
  skips[${counter}] = []
226
- if(typeof cmps[${counter}] !== 'undefined') {
227
- for(let key in cmps[${counter}][Symbol.for('config')].props) {
228
- delete elementConfigs[${counter}][cmps[${counter}][Symbol.for('config')].props[key]]
229
- skips[${counter}].push(cmps[${counter}][Symbol.for('config')].props[key])
263
+ if(typeof cmps[${counter}] !== 'undefined' && cmps[${counter}][Symbol.for('config')].props !== undefined) {
264
+ // attributes that are a prop should be removed from element config (even if it's a know element prop)
265
+ let props = cmps[${counter}][Symbol.for('config')].props
266
+ if(Array.isArray(props) === false) props = Object.keys(cmps[${counter}][Symbol.for('config')].props)
267
+ for(let k = 0; k < props.length; k++) {
268
+ const key = props[k]
269
+ if(validAttributes.indexOf(key) !== -1) continue
270
+ delete elementConfigs[${counter}][key]
271
+ skips[${counter}].push(key)
230
272
  }
231
273
  }
232
274
  `)
@@ -409,7 +451,7 @@ const generateForLoopCode = function (templateObject, parent) {
409
451
  .replace(')', '')
410
452
  .split(/\s*,\s*/)
411
453
 
412
- const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)([\\w$]+))`, 'gi')
454
+ const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)(\\$?\\w+))`, 'gi')
413
455
 
414
456
  // local context
415
457
  const ctx = {
@@ -540,9 +582,9 @@ const generateForLoopCode = function (templateObject, parent) {
540
582
  }
541
583
 
542
584
  // separate effects that only rely on variables in the itteration
543
- const innerScopeEffects = ctx.effectsCode.filter(
544
- (effect) => [...effect.matchAll(scopeRegex)].length === 0
545
- )
585
+ const innerScopeEffects = ctx.effectsCode.filter((effect) => {
586
+ return [...effect.matchAll(scopeRegex)].length === 0
587
+ })
546
588
 
547
589
  // separate effects that (also) rely on variables in the outer scope
548
590
  const outerScopeEffects = ctx.effectsCode.filter(
@@ -760,12 +802,12 @@ const cast = (val = '', key = false, component = 'component.') => {
760
802
  castedValue = parseFloat(val)
761
803
  if (val.endsWith('%')) {
762
804
  const map = {
763
- w: 'width',
764
- width: 'width',
765
- x: 'width',
766
- h: 'height',
767
- height: 'height',
768
- y: 'height',
805
+ w: 'w',
806
+ width: 'w',
807
+ x: 'w',
808
+ h: 'h',
809
+ height: 'h',
810
+ y: 'h',
769
811
  }
770
812
  const base = map[key]
771
813
  if (base) {
@@ -813,6 +855,65 @@ const cast = (val = '', key = false, component = 'component.') => {
813
855
  return castedValue
814
856
  }
815
857
 
858
+ function escapeSingleQuotes(str) {
859
+ return str.replace(/(\\*)'/g, (match, backslashes) => {
860
+ // If the number of backslashes is odd, quote is already escaped
861
+ if (backslashes.length % 2 === 1) {
862
+ return match
863
+ }
864
+ // Otherwise, escape the quote
865
+ return backslashes + "\\'"
866
+ })
867
+ }
868
+
869
+ const parseTagContent = (val = '', component = 'component.') => {
870
+ // unescaped single quotes must be escaped while preserving escaped backslashes
871
+ let escapedVal = escapeSingleQuotes(val)
872
+
873
+ const dynamicParts = /\{\{\s*.+?\s*\}\}/g
874
+ const matches = [...escapedVal.matchAll(dynamicParts)]
875
+
876
+ if (matches.length > 0) {
877
+ const isValStartsWithBrace = escapedVal.startsWith('{{')
878
+ const isValEndsWithBrace = escapedVal.endsWith('}}')
879
+ for (let matchObj of matches) {
880
+ const { 0: match, index } = matchObj
881
+ const isMatchAtStart = index === 0
882
+ const isMatchAtLast = val[index + match.length] === undefined ? true : false
883
+
884
+ let parsedMatch = match
885
+
886
+ const replaceDollar = /\$(\$(?=\$)|\$?)/g
887
+ const dollarMatches = [...parsedMatch.matchAll(replaceDollar)]
888
+ if (dollarMatches.length > 0) {
889
+ parsedMatch = parsedMatch.replace(replaceDollar, (match, group1) => {
890
+ if (group1 === '') {
891
+ return component
892
+ } else if (group1 === '$') {
893
+ return component + '$'
894
+ }
895
+ })
896
+ }
897
+
898
+ parsedMatch = parsedMatch.replace('{{', '(').replace('}}', ')')
899
+ if (isMatchAtStart === false) {
900
+ parsedMatch = `"+${parsedMatch}`
901
+ }
902
+ if (isMatchAtLast === false) {
903
+ parsedMatch = `${parsedMatch}+"`
904
+ }
905
+ escapedVal = escapedVal.replace(match, parsedMatch)
906
+ }
907
+ if (isValStartsWithBrace === false) {
908
+ escapedVal = '"' + escapedVal
909
+ }
910
+ if (isValEndsWithBrace === false) {
911
+ escapedVal = escapedVal + '"'
912
+ }
913
+ }
914
+ return escapedVal
915
+ }
916
+
816
917
  const isReactiveKey = (str) => str.startsWith(':')
817
918
 
818
919
  const parseInlineContent = (val, component) => {
@@ -86,7 +86,6 @@ export default {
86
86
  console.warn('HSL(A) color format is not supported yet')
87
87
  return '0xffffffff'
88
88
  }
89
-
90
89
  return defaultColor
91
90
  },
92
91
  }
@@ -72,7 +72,7 @@ export default {
72
72
  set state(v) {
73
73
  if ((states.indexOf(v) > -1 && v !== this.current) || v === 'refocus') {
74
74
  Log.debug(
75
- `Setting lifecycle state from ${this.current} to ${v} for ${this.component.componentId}`
75
+ `Setting lifecycle state from ${this.current} to ${v} for ${this.component.$componentId}`
76
76
  )
77
77
  this.previous = this.current
78
78
  this.current = v
@@ -82,8 +82,8 @@ export default {
82
82
  // emit 'public' hook
83
83
  emit(v, this.component[symbols.identifier], this.component)
84
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
85
+ if (v === 'focus') this.component[symbols.state].$hasFocus = true
86
+ if (v === 'unfocus') this.component[symbols.state].$hasFocus = false
87
87
  }
88
88
  },
89
89
  }
@@ -17,7 +17,6 @@
17
17
 
18
18
  import parser from '../templateparser/parser.js'
19
19
  import generator from '../codegenerator/generator.js'
20
- import MagicString from 'magic-string'
21
20
 
22
21
  export default (source, filePath, mode) => {
23
22
  if (
@@ -27,9 +26,12 @@ export default (source, filePath, mode) => {
27
26
  /\{.*?template\s*:\s*(['"`])((?:\\?.)*?)\1.*?\}/s.test(source) // object with template key
28
27
  ) {
29
28
  const templates = source.matchAll(/(?<!\/\/\s*)template\s*:\s*(['"`])((?:\\?.)*?)\1/gs)
30
-
31
- // Use MagicString to track changes
32
- const s = new MagicString(source)
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
33
35
 
34
36
  for (const template of templates) {
35
37
  if (template[2]) {
@@ -37,8 +39,7 @@ export default (source, filePath, mode) => {
37
39
 
38
40
  // Only process if it looks like a Blits template
39
41
  if (templateContent.match(/^\s*(<!--[\s\S]*?-->|<[A-Za-z][^>]*>)/s)) {
40
- // Use original indices - MagicString handles position tracking automatically
41
- const templateStartIndex = template.index
42
+ const templateStartIndex = template.index + offset
42
43
  const templateEndIndex = templateStartIndex + template[0].length
43
44
 
44
45
  // Parse the template
@@ -57,26 +58,16 @@ export default (source, filePath, mode) => {
57
58
  (fn) => fn.toString()
58
59
  )}], context: {}}`
59
60
 
60
- // MagicString tracks all changes automatically, no offset needed
61
- s.overwrite(templateStartIndex, templateEndIndex, replacement)
62
- }
63
- }
64
- }
65
-
66
- const newSource = s.toString()
61
+ offset += replacement.length - template[0].length
67
62
 
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
- }),
63
+ newSource =
64
+ newSource.substring(0, templateStartIndex) +
65
+ replacement +
66
+ newSource.substring(templateEndIndex)
67
+ }
78
68
  }
79
69
  }
70
+ return newSource
80
71
  }
81
72
  return source
82
73
  }
@@ -213,12 +213,7 @@ export default (code) => {
213
213
  modifiedCode = modifiedCode.replace(mod.original, mod.replacement)
214
214
  }
215
215
 
216
- // Returning code without a source map
217
- // Vite will automatically chain this with the next plugin's source map (preCompiler)
218
- return {
219
- code: modifiedCode,
220
- map: { mappings: '' },
221
- }
216
+ return { code: modifiedCode }
222
217
  }
223
218
 
224
219
  return null
@@ -0,0 +1,163 @@
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
+
18
+ import { renderer } from '../../engines/L3/launch.js'
19
+ import { parseToObject, isObjectString, isArrayString } from '../utils.js'
20
+ import colors from '../colors/colors.js'
21
+
22
+ const parseBorder = (border, props = {}, prefixed = false) => {
23
+ const prefix = prefixed === true ? 'border-' : ''
24
+ if (isNaN(border) === false) {
25
+ props[prefix + 'w'] = border
26
+ return
27
+ }
28
+ if (Array.isArray(border) === true) {
29
+ props[prefix + 'w'] = border
30
+ return
31
+ }
32
+ if (isArrayString(border) === true) {
33
+ props[prefix + 'w'] = JSON.parse(border)
34
+ return
35
+ }
36
+ if (typeof border === 'object' || isObjectString(border) === true) {
37
+ border = typeof border === 'string' ? parseToObject(border) : border
38
+ for (const key in border) {
39
+ props[prefix + key] = border[key]
40
+ }
41
+ return
42
+ }
43
+ }
44
+
45
+ const parseShadow = (shadow, props = {}, prefixed = false) => {
46
+ const prefix = prefixed === true ? 'shadow-' : ''
47
+ if (Array.isArray(shadow) === true) {
48
+ props[prefix + 'projection'] = shadow
49
+ return
50
+ }
51
+ if (isArrayString(shadow) === true) {
52
+ props[prefix + 'projection'] = JSON.parse(shadow)
53
+ return
54
+ }
55
+ if (typeof shadow === 'object' || isObjectString(shadow) === true) {
56
+ shadow = typeof shadow === 'string' ? parseToObject(shadow) : shadow
57
+ for (const key in shadow) {
58
+ props[prefix + key] = shadow[key]
59
+ }
60
+ return
61
+ }
62
+ }
63
+
64
+ const parseRounded = (rounded, props = {}) => {
65
+ if (isNaN(rounded) === false) {
66
+ props.radius = rounded
67
+ return
68
+ }
69
+ if (Array.isArray(rounded) === true) {
70
+ props.radius = rounded
71
+ return
72
+ }
73
+ if (isArrayString(rounded) === true) {
74
+ props.radius = JSON.parse(rounded)
75
+ return
76
+ }
77
+ if (typeof rounded === 'object' || isObjectString(rounded) === true) {
78
+ rounded = typeof rounded === 'string' ? parseToObject(rounded) : rounded
79
+ for (const key in rounded) {
80
+ props[key] = rounded[key]
81
+ }
82
+ return
83
+ }
84
+ }
85
+
86
+ export default {
87
+ createElementProps(v) {
88
+ let { border, shadow, rounded } = v
89
+ const props = {}
90
+ const hasRounded = rounded !== undefined
91
+ const hasBorder = border !== undefined
92
+ const hasShadow = shadow !== undefined
93
+
94
+ if (border !== undefined) {
95
+ parseBorder(border, props, hasRounded || hasShadow)
96
+ }
97
+
98
+ if (shadow !== undefined) {
99
+ parseShadow(shadow, props, hasRounded || hasBorder)
100
+ }
101
+
102
+ if (hasRounded === true) {
103
+ parseRounded(rounded, props)
104
+ }
105
+ return props
106
+ },
107
+ createElementShader(v) {
108
+ let { border, shadow, rounded } = v
109
+ const props = this.createElementProps(v)
110
+ const hasShadow = shadow !== undefined
111
+ const hasBorder = border !== undefined
112
+ const hasRounded = rounded !== undefined
113
+
114
+ if (hasBorder === true && hasShadow === true) {
115
+ return renderer.createShader('roundedWithBorderAndShadow', props)
116
+ }
117
+
118
+ if (hasRounded === true) {
119
+ if (hasBorder === true) {
120
+ return renderer.createShader('roundedWithBorder', props)
121
+ }
122
+
123
+ if (hasShadow === true) {
124
+ return renderer.createShader('roundedWithShadow', props)
125
+ }
126
+
127
+ return renderer.createShader('rounded', props)
128
+ }
129
+
130
+ if (hasRounded === false && hasBorder === true) {
131
+ return renderer.createShader('border', props)
132
+ }
133
+
134
+ //only possible result left
135
+ return renderer.createShader('shadow', props)
136
+ },
137
+ parseProp(v) {
138
+ if (typeof v === 'number') {
139
+ return v
140
+ }
141
+ if (typeof v === 'string') {
142
+ return colors.normalize(v)
143
+ }
144
+ if (Array.isArray(v) === true) {
145
+ return v.map((i) => this.parseProp(i))
146
+ }
147
+ },
148
+ parseProps(v) {
149
+ if (typeof v === 'string') {
150
+ v = parseToObject(v)
151
+ }
152
+ const keys = Object.keys(v)
153
+ const length = keys.length
154
+ for (let i = 0; i < length; i++) {
155
+ const key = keys[i]
156
+ if (key === 'type') {
157
+ continue
158
+ }
159
+ v[key] = this.parseProp(v[key])
160
+ }
161
+ return v
162
+ },
163
+ }
@@ -20,12 +20,14 @@
20
20
  * @property {symbol} isProxy
21
21
  * @property {symbol} launched
22
22
  * @property {symbol} level
23
+ * @property {symbol} lifecycle
23
24
  * @property {symbol} methodKeys
24
25
  * @property {symbol} originalState
25
26
  * @property {symbol} propKeys
26
27
  * @property {symbol} raw
27
28
  * @property {symbol} ready
28
29
  * @property {symbol} renderer
30
+ * @property {symbol} rootParent
29
31
  * @property {symbol} routes
30
32
  * @property {symbol} routerHooks
31
33
  * @property {symbol} settings
@@ -42,6 +44,7 @@
42
44
  * @property {symbol} config
43
45
  * @property {symbol} isSlot
44
46
  * @property {symbol} props
47
+ * @property {symbol} parent
45
48
  * @property {symbol} slots
46
49
  * @property {symbol} componentType
47
50
  * @property {symbol} isComponent
@@ -73,12 +76,15 @@ export default {
73
76
  isProxy: Symbol('isProxy'),
74
77
  launched: Symbol('launched'),
75
78
  level: Symbol('level'),
79
+ lifecycle: Symbol('lifecycle'),
76
80
  methodKeys: Symbol('methodKeys'),
77
81
  originalState: Symbol('originalState'),
82
+ parent: Symbol('parent'),
78
83
  propKeys: Symbol('propKeys'),
79
84
  raw: Symbol('raw'),
80
85
  ready: Symbol('ready'),
81
86
  renderer: Symbol('renderer'),
87
+ rootParent: Symbol('rootParent'),
82
88
  routes: Symbol('routes'),
83
89
  routerHooks: Symbol('routerHooks'),
84
90
  settings: Symbol('settings'),
@@ -113,4 +119,6 @@ export default {
113
119
  effects: Symbol.for('effects'),
114
120
  // Symbol 'removeGlobalEffects' utilized within generated code
115
121
  removeGlobalEffects: Symbol.for('removeGlobalEffects'),
122
+ // Symbol 'removeGlobalEffects' utilized within generated code
123
+ tagContent: Symbol.for('tagContent'),
116
124
  }
@@ -143,7 +143,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
143
143
  if (currentTag[symbols.type] === 'opening') {
144
144
  const tagContent = template.slice(cursor, template.indexOf('<', cursor))
145
145
  if (tagContent) {
146
- currentTag.content = tagContent
146
+ currentTag[Symbol.for('tagContent')] = tagContent
147
147
  cursor += tagContent.length
148
148
  }
149
149
  }
@@ -189,7 +189,11 @@ export default (template = '', componentName, parentComponent, filePath = null)
189
189
  }
190
190
 
191
191
  // process color values
192
- if (['color', ':color', ':effects', 'effects'].includes(name)) {
192
+ if (
193
+ ['color', ':color', ':border', 'border', 'shadow', ':shadow', 'shader', ':shader'].includes(
194
+ name
195
+ )
196
+ ) {
193
197
  return processColors(name, value)
194
198
  }
195
199
  return { name, value }
package/src/lib/utils.js CHANGED
@@ -25,3 +25,39 @@ export const screenResolutions = {
25
25
  '2160p': 2,
26
26
  2160: 2,
27
27
  }
28
+
29
+ /**
30
+ * Checks if a value is a transition object.
31
+ * @param {any} value - The value to check.
32
+ * @returns {boolean} True if the value is a transition object, false otherwise.
33
+ */
34
+ export const isTransition = (value) => {
35
+ return value !== null && typeof value === 'object' && 'transition' in value === true
36
+ }
37
+
38
+ /**
39
+ * Checks if a string is an object string (starts and ends with curly braces).
40
+ * @param {string} str - The string to check.
41
+ * @returns {boolean} True if the string is an object string, false otherwise.
42
+ */
43
+ export const isObjectString = (str) => {
44
+ return typeof str === 'string' && str.startsWith('{') && str.endsWith('}')
45
+ }
46
+
47
+ /**
48
+ * Checks if a string is an array string (starts and ends with brackets).
49
+ * @param {string} str - The string to check.
50
+ * @returns {boolean} True if the string is an object string, false otherwise.
51
+ */
52
+ export const isArrayString = (str) => {
53
+ return typeof str === 'string' && str.startsWith('[') && str.endsWith(']')
54
+ }
55
+
56
+ /**
57
+ * Parses a string into an object, converting single quotes to double and adding quotes to keys.
58
+ * @param {string} str - The string to parse.
59
+ * @returns {object} The parsed object.
60
+ */
61
+ export const parseToObject = (str) => {
62
+ return JSON.parse(str.replace(/'/g, '"').replace(/([\w-_]+)\s*:/g, '"$1":'))
63
+ }
@@ -245,17 +245,17 @@ export const navigate = async function () {
245
245
  Announcer.clear()
246
246
  state.navigating = true
247
247
  let reuse = false
248
- if (preventHashChangeNavigation === false && this.parent[symbols.routes]) {
248
+ if (preventHashChangeNavigation === false && this[symbols.parent][symbols.routes]) {
249
249
  let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
250
- let route = matchHash(getHash(document.location.hash), this.parent[symbols.routes])
250
+ let route = matchHash(getHash(document.location.hash), this[symbols.parent][symbols.routes])
251
251
 
252
252
  currentRoute = route
253
253
 
254
254
  if (route) {
255
255
  const currentPath = currentRoute.path
256
256
  let beforeEachResult
257
- if (this.parent[symbols.routerHooks]) {
258
- const hooks = this.parent[symbols.routerHooks]
257
+ if (this[symbols.parent][symbols.routerHooks]) {
258
+ const hooks = this[symbols.parent][symbols.routerHooks]
259
259
  if (hooks.beforeEach) {
260
260
  try {
261
261
  beforeEachResult = await hooks.beforeEach.call(this.parent, route, previousRoute)
@@ -502,9 +502,9 @@ export const navigate = async function () {
502
502
  }
503
503
  } else {
504
504
  Log.error(`Route ${route.hash} not found`)
505
- const routerHooks = this.parent[symbols.routerHooks]
505
+ const routerHooks = this[symbols.parent][symbols.routerHooks]
506
506
  if (routerHooks && typeof routerHooks.error === 'function') {
507
- routerHooks.error.call(this.parent, `Route ${route.hash} not found`)
507
+ routerHooks.error.call(this[symbols.parent], `Route ${route.hash} not found`)
508
508
  }
509
509
  }
510
510
  }
@@ -626,7 +626,7 @@ export const back = function () {
626
626
  }
627
627
  // Construct new path to backtrack to
628
628
  path = path.replace(hashEnd, '')
629
- const route = matchHash(getHash(path), this.parent[symbols.routes])
629
+ const route = matchHash(getHash(path), this[symbols.parent][symbols.routes])
630
630
 
631
631
  if (route && backtrack) {
632
632
  to(route.path, route.data, route.options)
@@ -0,0 +1,57 @@
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
+ const tips = ['tip1', 'tip2', 'tip3']
19
+
20
+ let hmrCount = 0
21
+ let lastTipLines = 0
22
+ const HMR_THRESHOLD = 4
23
+
24
+ function showTip() {
25
+ const tip = tips[Math.floor(Math.random() * tips.length)]
26
+
27
+ // Clear previous tip (move up and erase lines)
28
+ if (lastTipLines > 0) {
29
+ process.stdout.write(`\x1B[${lastTipLines}A`)
30
+ for (let i = 0; i < lastTipLines; i++) {
31
+ process.stdout.write('\x1B[2K\x1B[1E') // clear line + move down
32
+ }
33
+ process.stdout.write(`\x1B[${lastTipLines}A`)
34
+ }
35
+
36
+ // Print new tip
37
+ const lines = [`💡 Blits Tip: ${tip}`, '']
38
+ lastTipLines = lines.length
39
+ process.stdout.write(lines.join('\n') + '\n')
40
+ }
41
+
42
+ export default function blitsTipsPlugin() {
43
+ return {
44
+ name: 'vite:blits-tips',
45
+ apply: 'serve',
46
+
47
+ configureServer(server) {
48
+ server.watcher.on('change', () => {
49
+ hmrCount++
50
+ if (hmrCount >= HMR_THRESHOLD) {
51
+ showTip()
52
+ hmrCount = 0
53
+ }
54
+ })
55
+ },
56
+ }
57
+ }