@lightningjs/blits 1.21.1 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.github/workflows/run-blits-tests.yml +69 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +1 -1
  4. package/docs/README.md +1 -2
  5. package/docs/built-in/directives.md +1 -1
  6. package/docs/built-in/for-loop.md +8 -8
  7. package/docs/built-in/layout.md +23 -23
  8. package/docs/components/user_input.md +3 -3
  9. package/docs/components/utility_methods.md +11 -7
  10. package/docs/essentials/displaying_images.md +3 -3
  11. package/docs/essentials/displaying_text.md +3 -3
  12. package/docs/essentials/element_attributes.md +3 -3
  13. package/docs/essentials/template_syntax.md +1 -1
  14. package/docs/getting_started/intro.md +1 -1
  15. package/docs/plugins/global_app_state.md +2 -2
  16. package/docs/plugins/language.md +2 -2
  17. package/docs/plugins/theme.md +1 -1
  18. package/docs/router/basics.md +4 -4
  19. package/docs/router/hooks.md +1 -1
  20. package/docs/transitions_animations/transitions.md +3 -3
  21. package/index.d.ts +50 -7
  22. package/package.json +2 -2
  23. package/src/component/base/index.js +3 -1
  24. package/src/component/setup/index.js +4 -3
  25. package/src/component/setup/routes.js +6 -1
  26. package/src/component.js +24 -7
  27. package/src/component.test.js +6 -5
  28. package/src/components/Circle.js +1 -1
  29. package/src/components/FPScounter.js +1 -1
  30. package/src/components/RouterView.js +1 -1
  31. package/src/components/Sprite.js +1 -1
  32. package/src/engines/L3/launch.js +26 -22
  33. package/src/lib/codegenerator/generator.js +61 -8
  34. package/src/lib/codegenerator/generator.test.js +1089 -167
  35. package/src/lib/eventListeners.js +3 -3
  36. package/src/lib/precompiler/precompiler.js +2 -3
  37. package/src/lib/reactivity/reactive.js +1 -0
  38. package/src/lib/reactivityguard/computedprops.js +207 -0
  39. package/src/lib/symbols.js +1 -0
  40. package/src/router/router.js +28 -6
  41. package/vite/index.js +3 -1
  42. package/vite/preCompiler.js +1 -1
  43. package/vite/reactivityGuard.js +45 -0
@@ -39,11 +39,11 @@ export default {
39
39
  deregisterListener(component, event) {
40
40
  let componentsMap = eventsMap.get(event)
41
41
  if (componentsMap === undefined) {
42
- return;
42
+ return
43
43
  }
44
44
 
45
- if (componentsMap.contains(component)) {
46
- componentsMap.delete(component);
45
+ if (componentsMap.has(component)) {
46
+ componentsMap.delete(component)
47
47
  eventsMap.set(event, componentsMap)
48
48
  callbackCache.delete(event)
49
49
  }
@@ -18,7 +18,7 @@
18
18
  import parser from '../templateparser/parser.js'
19
19
  import generator from '../codegenerator/generator.js'
20
20
 
21
- export default (source, filePath) => {
21
+ export default (source, filePath, mode) => {
22
22
  if (
23
23
  source.indexOf('Blits.Component(') > -1 ||
24
24
  source.indexOf('Blits.Application(') > -1 ||
@@ -42,7 +42,6 @@ export default (source, filePath) => {
42
42
  const templateStartIndex = template.index + offset
43
43
  const templateEndIndex = templateStartIndex + template[0].length
44
44
 
45
-
46
45
  // Parse the template
47
46
  let resourceName = 'Blits.Application'
48
47
  if (source.indexOf('Blits.Component(') > -1) {
@@ -52,7 +51,7 @@ export default (source, filePath) => {
52
51
  const parsed = parser(templateContent, resourceName, null, filePath)
53
52
 
54
53
  // Generate the code
55
- const code = generator.call({ components: {} }, parsed)
54
+ const code = generator.call({ components: {} }, parsed, mode === 'development')
56
55
 
57
56
  // Insert the code in the component using the 'code' key, replacing the template key
58
57
  const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map(
@@ -115,6 +115,7 @@ const reactiveProxy = (original, _parent = null, _key, global) => {
115
115
  if (Array.isArray(value) === true) {
116
116
  value = getRaw(value).slice(0)
117
117
  } else if (
118
+ value !== null &&
118
119
  target[key] !== null &&
119
120
  target[key] !== undefined &&
120
121
  Array.isArray(target) === false &&
@@ -0,0 +1,207 @@
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
+ export default (code) => {
19
+ // Find component declarations
20
+ const componentRegex =
21
+ /Blits\.(Component|Application)\s*\(\s*(?:'[^']*'|"[^"]*")?\s*(?:,\s*)?({[\s\S]*?})\s*\)/g
22
+ const modifications = []
23
+ const commentText = ' /* auto-generated reactivity guard */ '
24
+
25
+ let match
26
+ while ((match = componentRegex.exec(code)) !== null) {
27
+ const configObject = match[2]
28
+
29
+ // Find computed section with proper brace balancing
30
+ const computedKeywordMatch = /computed\s*:\s*{/.exec(configObject)
31
+
32
+ if (computedKeywordMatch) {
33
+ // Find start position of the computed block
34
+ const computedStart = computedKeywordMatch.index + computedKeywordMatch[0].length - 1 // Position of the opening brace
35
+
36
+ // Find the matching closing brace with balance tracking
37
+ let braceLevel = 1
38
+ let pos = computedStart + 1
39
+
40
+ while (braceLevel > 0 && pos < configObject.length) {
41
+ if (configObject[pos] === '{') braceLevel++
42
+ else if (configObject[pos] === '}') braceLevel--
43
+ pos++
44
+ }
45
+
46
+ if (braceLevel === 0) {
47
+ const computedEnd = pos
48
+ const computedObj = configObject.substring(computedStart, computedEnd)
49
+
50
+ // Process each computed property
51
+ const computedProps = []
52
+ let currentPos = 1 // Start after the opening brace
53
+
54
+ // Process computed properties content
55
+ // intentionally not using regexes to extract functions and their content
56
+ // using cursor-based parsing gives better accuracy
57
+ while (currentPos < computedObj.length - 1) {
58
+ // -1 to exclude the closing brace
59
+ // Find the next function name
60
+ const functionNameMatch = /\s*(\w+)\s*\(\)/.exec(computedObj.substring(currentPos))
61
+ if (!functionNameMatch) break
62
+
63
+ const funcName = functionNameMatch[1]
64
+ const funcNamePos = currentPos + functionNameMatch.index
65
+
66
+ // Find opening brace for this function
67
+ const openingBracePos = computedObj.indexOf('{', funcNamePos)
68
+ if (openingBracePos === -1) break
69
+
70
+ // Find closing brace with proper nesting
71
+ let braceLevel = 1
72
+ let closingBracePos = openingBracePos + 1
73
+
74
+ while (braceLevel > 0 && closingBracePos < computedObj.length) {
75
+ if (computedObj[closingBracePos] === '{') braceLevel++
76
+ else if (computedObj[closingBracePos] === '}') braceLevel--
77
+ closingBracePos++
78
+ }
79
+
80
+ if (braceLevel !== 0) {
81
+ // Could not find matching brace
82
+ break
83
+ }
84
+
85
+ // Extract the entire function and its body
86
+ // This is better and more accurate than using regex to avoid nested braces
87
+ const functionStartPos = funcNamePos
88
+ const functionEndPos = closingBracePos
89
+ const fullFunction = computedObj.substring(functionStartPos, functionEndPos)
90
+ const functionBody = computedObj
91
+ .substring(openingBracePos + 1, closingBracePos - 1)
92
+ .trim()
93
+
94
+ computedProps.push({
95
+ name: funcName,
96
+ fullText: fullFunction,
97
+ body: functionBody,
98
+ startPos: functionStartPos,
99
+ endPos: functionEndPos,
100
+ })
101
+
102
+ // Move past this function
103
+ currentPos = closingBracePos
104
+ }
105
+
106
+ // Store the original computed object for later replacement
107
+ const originalComputedObj = configObject.substring(
108
+ computedStart - 'computed: '.length,
109
+ computedEnd
110
+ )
111
+
112
+ let modifiedComputedObj = originalComputedObj
113
+ let hasChanges = false
114
+
115
+ for (const prop of computedProps) {
116
+ const thisRefs = extractThisReferences(prop.body)
117
+
118
+ if (thisRefs.size > 0) {
119
+ // Skip if already modified
120
+ if (
121
+ prop.body.includes(commentText) ||
122
+ Array.from(thisRefs).some((ref) => prop.body.startsWith(ref))
123
+ ) {
124
+ continue
125
+ }
126
+
127
+ // reactivity code
128
+ const refCode = Array.from(thisRefs)
129
+ .map((ref) => `${ref};`)
130
+ .join(' ')
131
+
132
+ // replacement with the same whitespace
133
+ const openBraceIndex = prop.fullText.indexOf('{')
134
+
135
+ if (openBraceIndex !== -1) {
136
+ const modifiedPropText =
137
+ prop.fullText.substring(0, openBraceIndex + 1) +
138
+ commentText +
139
+ refCode +
140
+ ' ' +
141
+ prop.body +
142
+ '}'
143
+
144
+ // Replace this property in the computed object text
145
+ modifiedComputedObj = modifiedComputedObj.replace(prop.fullText, modifiedPropText)
146
+ hasChanges = true
147
+ }
148
+ }
149
+ }
150
+
151
+ if (hasChanges) {
152
+ // Store the modification of the entire computed object
153
+ modifications.push({
154
+ original: originalComputedObj,
155
+ replacement: modifiedComputedObj,
156
+ })
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // Apply all modifications at once, from last to first to preserve positions
163
+ if (modifications.length > 0) {
164
+ let modifiedCode = code
165
+ // Sort modifications from last to first to avoid position changes
166
+ modifications.sort((a, b) => {
167
+ const posA = modifiedCode.indexOf(a.original)
168
+ const posB = modifiedCode.indexOf(b.original)
169
+ return posB - posA
170
+ })
171
+
172
+ for (const mod of modifications) {
173
+ modifiedCode = modifiedCode.replace(mod.original, mod.replacement)
174
+ }
175
+
176
+ return { code: modifiedCode }
177
+ }
178
+
179
+ return null
180
+ }
181
+
182
+ const extractThisReferences = (funcBody) => {
183
+ const thisRefs = new Set()
184
+
185
+ // Find all this.X references, but exclude method calls
186
+ const refRegex = /this\.\w+(?!\s*\()/g // matches this.X but not this.X() or this.X.Y()
187
+ let refMatch
188
+
189
+ while ((refMatch = refRegex.exec(funcBody)) !== null) {
190
+ const fullRef = refMatch[0]
191
+
192
+ // Extract just the variable part (this.X)
193
+ // We need to avoid method calls or chained methods
194
+ const varRef = fullRef.split('.').slice(0, 2).join('.')
195
+
196
+ // Don't add if it's part of a method call
197
+ if (
198
+ !funcBody
199
+ .substring(refMatch.index)
200
+ .match(new RegExp(`^${varRef.replace(/\./g, '\\.')}\\s*\\(`))
201
+ ) {
202
+ thisRefs.add(varRef)
203
+ }
204
+ }
205
+
206
+ return thisRefs
207
+ }
@@ -20,6 +20,7 @@ export default {
20
20
  ready: Symbol('ready'),
21
21
  renderer: Symbol('renderer'),
22
22
  routes: Symbol('routes'),
23
+ routerHooks: Symbol('routerHooks'),
23
24
  settings: Symbol('settings'),
24
25
  state: Symbol('state'),
25
26
  stateKeys: Symbol('stateKeys'),
@@ -33,7 +33,9 @@ export const state = reactive({
33
33
  hash: '',
34
34
  })
35
35
 
36
- const cacheMap = new WeakMap()
36
+ // Changed from WeakMap to Map to allow for caching of views by the url hash.
37
+ // We are manually doing the cleanup of the cache when the route is not marked as keepAlive.
38
+ const cacheMap = new Map()
37
39
  const history = []
38
40
 
39
41
  let overrideOptions = {}
@@ -133,8 +135,22 @@ export const navigate = async function () {
133
135
  }
134
136
 
135
137
  currentRoute = route
136
- let beforeHookOutput
137
138
  if (route) {
139
+ let beforeEachResult
140
+ if (this.parent[symbols.routerHooks]) {
141
+ const hooks = this.parent[symbols.routerHooks]
142
+ if (hooks.beforeAll) {
143
+ beforeEachResult = await hooks.beforeAll(route, previousRoute)
144
+ if (isString(beforeEachResult)) {
145
+ to(beforeEachResult)
146
+ return
147
+ }
148
+ }
149
+ }
150
+ // If the resolved result is an object, assign it to the target route object
151
+ route = isObject(beforeEachResult) ? beforeEachResult : route
152
+
153
+ let beforeHookOutput
138
154
  if (route.hooks) {
139
155
  if (route.hooks.before) {
140
156
  beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
@@ -163,7 +179,7 @@ export const navigate = async function () {
163
179
 
164
180
  let holder
165
181
  let routeData
166
- let { view, focus } = cacheMap.get(route) || {}
182
+ let { view, focus } = cacheMap.get(route.hash) || {}
167
183
 
168
184
  if (!view) {
169
185
  // create a holder element for the new view
@@ -312,9 +328,15 @@ const removeView = async (route, view, transition) => {
312
328
  }
313
329
 
314
330
  // cache the page when it's as 'keepAlive' instead of destroying
315
- if (route.options && route.options.keepAlive === true) {
316
- cacheMap.set(route, { view: view, focus: previousFocus })
317
- } else {
331
+ if (route.options && route.options.keepAlive === true && navigatingBack === false) {
332
+ cacheMap.set(route.hash, { view: view, focus: previousFocus })
333
+ } else if (navigatingBack === true) {
334
+ // remove the previous route from the cache when navigating back
335
+ // cacheMap.delete will not throw an error if the route is not in the cache
336
+ cacheMap.delete(route.hash)
337
+ }
338
+
339
+ if (route.options && route.options.keepAlive !== true) {
318
340
  view.destroy()
319
341
  view = null
320
342
  }
package/vite/index.js CHANGED
@@ -18,9 +18,11 @@
18
18
  import preCompiler from './preCompiler.js'
19
19
  import msdfGenerator from './msdfGenerator.js'
20
20
  import blitsFileConverter from './blitsFileConverter.js'
21
+ import reactivityGuard from './reactivityGuard.js'
21
22
 
22
23
  export { default as preCompiler } from './preCompiler.js'
23
24
  export { default as msdfGenerator } from './msdfGenerator.js'
24
25
  export { default as blitsFileConverter } from './blitsFileConverter.js'
26
+ export { default as reactivityGuard } from './reactivityGuard.js'
25
27
 
26
- export default [blitsFileConverter(), preCompiler(), msdfGenerator()]
28
+ export default [blitsFileConverter(), reactivityGuard(), preCompiler(), msdfGenerator()]
@@ -32,7 +32,7 @@ export default function () {
32
32
 
33
33
  // we should only precompile .blits, .js and .ts files
34
34
  if (fileExtension === '.js' || fileExtension === '.ts' || fileExtension === '.blits') {
35
- return compiler(source, filePath)
35
+ return compiler(source, filePath, config.mode)
36
36
  }
37
37
 
38
38
  // vite expects null if there is no modification
@@ -0,0 +1,45 @@
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 { default as processComputedProps } from './../src/lib/reactivityguard/computedprops.js'
19
+
20
+ export default function reactivityGuard() {
21
+ return {
22
+ name: 'reactivityGuard',
23
+ // run before other plugings except blitsFileConverter
24
+ // the order between blitsFileConverter and this plugin is maintained by the order of the plugins in the array in index.js
25
+ enforce: 'pre',
26
+
27
+ transform(code, id) {
28
+ if (!id.endsWith('.js') && !id.endsWith('.ts') && !id.endsWith('.blits')) {
29
+ return null
30
+ }
31
+
32
+ // Skip if no Blits component/application
33
+ if (!code.includes('Blits.Component') && !code.includes('Blits.Application')) {
34
+ return null
35
+ }
36
+
37
+ try {
38
+ return processComputedProps(code)
39
+ } catch (error) {
40
+ console.error(`[blits-reactivity-guard] Error processing ${id}:`, error)
41
+ return null // Return original code on error
42
+ }
43
+ },
44
+ }
45
+ }