@lightningjs/blits 1.48.0 → 2.0.0-beta.2

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/src/focus.js CHANGED
@@ -50,7 +50,7 @@ export default {
50
50
 
51
51
  // unfocus currently focused components
52
52
  if (focusedComponent !== null) {
53
- if (focusChain[focusChain.length - 1] === component.parent) {
53
+ if (focusChain[focusChain.length - 1] === component[symbols.parent]) {
54
54
  focusChain.push(component)
55
55
  } else {
56
56
  const newFocusChain = getAncestors([component])
@@ -58,7 +58,7 @@ export default {
58
58
  while (i--) {
59
59
  // don't unfocus when part of the new focus chain
60
60
  if (newFocusChain.indexOf(focusChain[i]) > -1) break
61
- focusChain[i].lifecycle.state = 'unfocus'
61
+ focusChain[i][symbols.lifecycle].state = 'unfocus'
62
62
  }
63
63
  focusChain = newFocusChain
64
64
  }
@@ -67,7 +67,7 @@ export default {
67
67
  // ensure that all components in the focus path have focus state
68
68
  let i = 0
69
69
  while (i < focusChain.length - 1) {
70
- focusChain[i].lifecycle.state = 'focus'
70
+ focusChain[i][symbols.lifecycle].state = 'focus'
71
71
  i++
72
72
  }
73
73
 
@@ -105,8 +105,8 @@ export default {
105
105
  * @returns array components
106
106
  */
107
107
  const getAncestors = (components) => {
108
- if (components[0].parent !== undefined) {
109
- components.unshift(components[0].parent)
108
+ if (components[0][symbols.parent] !== undefined) {
109
+ components.unshift(components[0][symbols.parent])
110
110
  return getAncestors(components)
111
111
  }
112
112
  return components
@@ -125,8 +125,8 @@ export const getComponentWithInputEvent = (component, key) => {
125
125
  typeof component[symbols.inputEvents].any === 'function')
126
126
  ) {
127
127
  return component
128
- } else if (component.parent !== undefined) {
129
- return getComponentWithInputEvent(component.parent, key)
128
+ } else if (component[symbols.parent] !== undefined) {
129
+ return getComponentWithInputEvent(component[symbols.parent], key)
130
130
  } else return null
131
131
  }
132
132
 
@@ -138,11 +138,11 @@ export const getComponentWithInputEvent = (component, key) => {
138
138
  const setFocus = (component, event) => {
139
139
  Log.info(
140
140
  '\nFocus chain:\n',
141
- focusChain.map((c, index) => '\t'.repeat(index) + '↳ ' + c.componentId).join('\n')
141
+ focusChain.map((c, index) => '\t'.repeat(index) + '↳ ' + c.$componentId).join('\n')
142
142
  )
143
143
 
144
144
  focusedComponent = component
145
- component.lifecycle.state = 'focus'
145
+ component[symbols.lifecycle].state = 'focus'
146
146
 
147
147
  if (event instanceof KeyboardEvent) {
148
148
  const internalEvent = new KeyboardEvent('keydown', event)
package/src/launch.js CHANGED
@@ -41,9 +41,8 @@ import blitsPackageInfo from '../package.json' with { type: 'json' }
41
41
 
42
42
  /**
43
43
  * @typedef {import('@lightningjs/renderer').RendererMain} RendererMain
44
- * @type {RendererMain}
44
+ * @type {RendererMain | {}}
45
45
  */
46
- // @ts-ignore - We ignore this because it will be replaced by the renderer once the app is launched
47
46
  export let renderer = {}
48
47
  export const stage = {}
49
48
 
@@ -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
  }
@@ -224,13 +236,43 @@ const generateElementCode = function (
224
236
  }
225
237
  })
226
238
 
239
+ if (templateObject[Symbol.for('tagContent')] !== undefined) {
240
+ const val = templateObject[Symbol.for('tagContent')]
241
+
242
+ // Check if the string contains interpolation (`{{ }}`).
243
+ const regex = /\{\{\s*.+?\s*\}\}/
244
+ const containsInterpolation = regex.test(val)
245
+
246
+ if (containsInterpolation === false) {
247
+ // Inline text is treated as static content when interpolation is not defined.
248
+ renderCode.push(`elementConfigs[${counter}]['content'] = '${val}'`)
249
+ } else {
250
+ const output = parseTagContent(val, options.component)
251
+ renderCode.push(`elementConfigs[${counter}]['content'] = ${output}`)
252
+
253
+ const variableRegEx = /\{\{\s*(?=[^}]*\$).+?\s*\}\}/
254
+ // Check if the interpolation contains $ variable
255
+ const isReactive = variableRegEx.test(val)
256
+ if (isReactive === true) {
257
+ this.effectsCode.push(`
258
+ ${elm}.set('content', ${output})
259
+ `)
260
+ }
261
+ }
262
+ }
263
+
227
264
  if (options.holder === true) {
228
265
  renderCode.push(`
229
266
  skips[${counter}] = []
230
- if(typeof cmps[${counter}] !== 'undefined') {
231
- for(let key in cmps[${counter}][Symbol.for('config')].props) {
232
- delete elementConfigs[${counter}][cmps[${counter}][Symbol.for('config')].props[key]]
233
- skips[${counter}].push(cmps[${counter}][Symbol.for('config')].props[key])
267
+ if(typeof cmps[${counter}] !== 'undefined' && cmps[${counter}][Symbol.for('config')].props !== undefined) {
268
+ // attributes that are a prop should be removed from element config (even if it's a know element prop)
269
+ let props = cmps[${counter}][Symbol.for('config')].props
270
+ if(Array.isArray(props) === false) props = Object.keys(cmps[${counter}][Symbol.for('config')].props)
271
+ for(let k = 0; k < props.length; k++) {
272
+ const key = props[k]
273
+ if(validAttributes.indexOf(key) !== -1) continue
274
+ delete elementConfigs[${counter}][key]
275
+ skips[${counter}].push(key)
234
276
  }
235
277
  }
236
278
  `)
@@ -567,9 +609,9 @@ const generateForLoopCode = function (templateObject, parent) {
567
609
  }
568
610
 
569
611
  // separate effects that only rely on variables in the itteration
570
- const innerScopeEffects = ctx.effectsCode.filter(
571
- (effect) => [...effect.matchAll(scopeRegex)].length === 0
572
- )
612
+ const innerScopeEffects = ctx.effectsCode.filter((effect) => {
613
+ return [...effect.matchAll(scopeRegex)].length === 0
614
+ })
573
615
 
574
616
  // separate effects that (also) rely on variables in the outer scope
575
617
  const outerScopeEffects = ctx.effectsCode.filter(
@@ -787,12 +829,12 @@ const cast = (val = '', key = false, component = 'component.') => {
787
829
  castedValue = parseFloat(val)
788
830
  if (val.endsWith('%')) {
789
831
  const map = {
790
- w: 'width',
791
- width: 'width',
792
- x: 'width',
793
- h: 'height',
794
- height: 'height',
795
- y: 'height',
832
+ w: 'w',
833
+ width: 'w',
834
+ x: 'w',
835
+ h: 'h',
836
+ height: 'h',
837
+ y: 'h',
796
838
  }
797
839
  const base = map[key]
798
840
  if (base) {
@@ -845,6 +887,65 @@ const cast = (val = '', key = false, component = 'component.') => {
845
887
  return castedValue
846
888
  }
847
889
 
890
+ function escapeSingleQuotes(str) {
891
+ return str.replace(/(\\*)'/g, (match, backslashes) => {
892
+ // If the number of backslashes is odd, quote is already escaped
893
+ if (backslashes.length % 2 === 1) {
894
+ return match
895
+ }
896
+ // Otherwise, escape the quote
897
+ return backslashes + "\\'"
898
+ })
899
+ }
900
+
901
+ const parseTagContent = (val = '', component = 'component.') => {
902
+ // unescaped single quotes must be escaped while preserving escaped backslashes
903
+ let escapedVal = escapeSingleQuotes(val)
904
+
905
+ const dynamicParts = /\{\{\s*.+?\s*\}\}/g
906
+ const matches = [...escapedVal.matchAll(dynamicParts)]
907
+
908
+ if (matches.length > 0) {
909
+ const isValStartsWithBrace = escapedVal.startsWith('{{')
910
+ const isValEndsWithBrace = escapedVal.endsWith('}}')
911
+ for (let matchObj of matches) {
912
+ const { 0: match, index } = matchObj
913
+ const isMatchAtStart = index === 0
914
+ const isMatchAtLast = val[index + match.length] === undefined ? true : false
915
+
916
+ let parsedMatch = match
917
+
918
+ const replaceDollar = /\$(\$(?=\$)|\$?)/g
919
+ const dollarMatches = [...parsedMatch.matchAll(replaceDollar)]
920
+ if (dollarMatches.length > 0) {
921
+ parsedMatch = parsedMatch.replace(replaceDollar, (match, group1) => {
922
+ if (group1 === '') {
923
+ return component
924
+ } else if (group1 === '$') {
925
+ return component + '$'
926
+ }
927
+ })
928
+ }
929
+
930
+ parsedMatch = parsedMatch.replace('{{', '(').replace('}}', ')')
931
+ if (isMatchAtStart === false) {
932
+ parsedMatch = `"+${parsedMatch}`
933
+ }
934
+ if (isMatchAtLast === false) {
935
+ parsedMatch = `${parsedMatch}+"`
936
+ }
937
+ escapedVal = escapedVal.replace(match, parsedMatch)
938
+ }
939
+ if (isValStartsWithBrace === false) {
940
+ escapedVal = '"' + escapedVal
941
+ }
942
+ if (isValEndsWithBrace === false) {
943
+ escapedVal = escapedVal + '"'
944
+ }
945
+ }
946
+ return escapedVal
947
+ }
948
+
848
949
  const isReactiveKey = (str) => str.startsWith(':')
849
950
 
850
951
  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
  }
@@ -75,7 +75,7 @@ export default {
75
75
  set state(v) {
76
76
  if ((states.indexOf(v) > -1 && v !== this.current) || v === 'refocus') {
77
77
  Log.debug(
78
- `Setting lifecycle state from ${this.current} to ${v} for ${this.component.componentId}`
78
+ `Setting lifecycle state from ${this.current} to ${v} for ${this.component.$componentId}`
79
79
  )
80
80
  this.previous = this.current
81
81
  this.current = v
@@ -84,14 +84,14 @@ export default {
84
84
  privateEmit(v, this.component[symbols.identifier], this.component)
85
85
  // emit 'public' hook
86
86
  emit(v, this.component[symbols.identifier], this.component)
87
- // update the built-in hasFocus state variable
87
+ // update the built-in $hasFocus state variable
88
88
  if (v === 'focus' || v === 'unfocus') {
89
89
  if (inspectorEnabled === null) {
90
90
  inspectorEnabled = Settings.get('inspector', false)
91
91
  }
92
92
  }
93
93
  if (v === 'focus') {
94
- this.component[symbols.state].hasFocus = true
94
+ this.component[symbols.state].$hasFocus = true
95
95
  if (
96
96
  inspectorEnabled === true &&
97
97
  this.component[symbols.holder] &&
@@ -101,7 +101,7 @@ export default {
101
101
  }
102
102
  }
103
103
  if (v === 'unfocus') {
104
- this.component[symbols.state].hasFocus = false
104
+ this.component[symbols.state].$hasFocus = false
105
105
  if (
106
106
  inspectorEnabled === true &&
107
107
  this.component[symbols.holder] &&
@@ -0,0 +1,164 @@
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, v)
143
+ }
144
+ if (Array.isArray(v) === true) {
145
+ return v.map((i) => this.parseProp(i))
146
+ }
147
+ return v
148
+ },
149
+ parseProps(v) {
150
+ if (typeof v === 'string') {
151
+ v = parseToObject(v)
152
+ }
153
+ const keys = Object.keys(v)
154
+ const length = keys.length
155
+ for (let i = 0; i < length; i++) {
156
+ const key = keys[i]
157
+ if (key === 'type') {
158
+ continue
159
+ }
160
+ v[key] = this.parseProp(v[key])
161
+ }
162
+ return v
163
+ },
164
+ }
@@ -21,12 +21,14 @@
21
21
  * @property {symbol} isProxy
22
22
  * @property {symbol} launched
23
23
  * @property {symbol} level
24
+ * @property {symbol} lifecycle
24
25
  * @property {symbol} methodKeys
25
26
  * @property {symbol} originalState
26
27
  * @property {symbol} propKeys
27
28
  * @property {symbol} raw
28
29
  * @property {symbol} ready
29
30
  * @property {symbol} renderer
31
+ * @property {symbol} rootParent
30
32
  * @property {symbol} routes
31
33
  * @property {symbol} routerHooks
32
34
  * @property {symbol} settings
@@ -43,6 +45,7 @@
43
45
  * @property {symbol} config
44
46
  * @property {symbol} isSlot
45
47
  * @property {symbol} props
48
+ * @property {symbol} parent
46
49
  * @property {symbol} slots
47
50
  * @property {symbol} componentType
48
51
  * @property {symbol} isComponent
@@ -75,12 +78,15 @@ export default {
75
78
  isProxy: Symbol('isProxy'),
76
79
  launched: Symbol('launched'),
77
80
  level: Symbol('level'),
81
+ lifecycle: Symbol('lifecycle'),
78
82
  methodKeys: Symbol('methodKeys'),
79
83
  originalState: Symbol('originalState'),
84
+ parent: Symbol('parent'),
80
85
  propKeys: Symbol('propKeys'),
81
86
  raw: Symbol('raw'),
82
87
  ready: Symbol('ready'),
83
88
  renderer: Symbol('renderer'),
89
+ rootParent: Symbol('rootParent'),
84
90
  routes: Symbol('routes'),
85
91
  routerHooks: Symbol('routerHooks'),
86
92
  settings: Symbol('settings'),
@@ -115,4 +121,6 @@ export default {
115
121
  effects: Symbol.for('effects'),
116
122
  // Symbol 'removeGlobalEffects' utilized within generated code
117
123
  removeGlobalEffects: Symbol.for('removeGlobalEffects'),
124
+ // Symbol 'removeGlobalEffects' utilized within generated code
125
+ tagContent: Symbol.for('tagContent'),
118
126
  }
@@ -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
+ }
@@ -249,17 +249,17 @@ export const navigate = async function () {
249
249
  Announcer.clear()
250
250
  state.navigating = true
251
251
  let reuse = false
252
- if (preventHashChangeNavigation === false && this.parent[symbols.routes]) {
252
+ if (preventHashChangeNavigation === false && this[symbols.parent][symbols.routes]) {
253
253
  let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
254
- let route = matchHash(getHash(document.location.hash), this.parent[symbols.routes])
254
+ let route = matchHash(getHash(document.location.hash), this[symbols.parent][symbols.routes])
255
255
 
256
256
  currentRoute = route
257
257
 
258
258
  if (route) {
259
259
  const currentPath = currentRoute.path
260
260
  let beforeEachResult
261
- if (this.parent[symbols.routerHooks]) {
262
- const hooks = this.parent[symbols.routerHooks]
261
+ if (this[symbols.parent][symbols.routerHooks]) {
262
+ const hooks = this[symbols.parent][symbols.routerHooks]
263
263
  if (hooks.beforeEach) {
264
264
  try {
265
265
  beforeEachResult = await hooks.beforeEach.call(this.parent, route, previousRoute)
@@ -507,8 +507,8 @@ export const navigate = async function () {
507
507
  }
508
508
  }
509
509
 
510
- if (this.parent[symbols.routerHooks]) {
511
- const hooks = this.parent[symbols.routerHooks]
510
+ if (this[symbols.parent][symbols.routerHooks]) {
511
+ const hooks = this[symbols.parent][symbols.routerHooks]
512
512
  if (hooks.afterEach) {
513
513
  try {
514
514
  await hooks.afterEach.call(
@@ -535,9 +535,9 @@ export const navigate = async function () {
535
535
  }
536
536
  } else {
537
537
  Log.error(`Route ${route.hash} not found`)
538
- const routerHooks = this.parent[symbols.routerHooks]
538
+ const routerHooks = this[symbols.parent][symbols.routerHooks]
539
539
  if (routerHooks && typeof routerHooks.error === 'function') {
540
- routerHooks.error.call(this.parent, `Route ${route.hash} not found`)
540
+ routerHooks.error.call(this[symbols.parent], `Route ${route.hash} not found`)
541
541
  }
542
542
  }
543
543
  }
@@ -659,7 +659,7 @@ export const back = function () {
659
659
  }
660
660
  // Construct new path to backtrack to
661
661
  path = path.replace(hashEnd, '')
662
- const route = matchHash(getHash(path), this.parent[symbols.routes])
662
+ const route = matchHash(getHash(path), this[symbols.parent][symbols.routes])
663
663
 
664
664
  if (route && backtrack) {
665
665
  to(route.path, route.data, route.options)
Binary file
@@ -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
+ }