@lightningjs/blits 0.6.11 → 0.6.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ # v0.6.13
4
+
5
+ _16 feb 2024_
6
+
7
+ - Upgraded to 0.7.2 of the renderer
8
+ - Introduced new `maxheight` attribute (besided the existing `maxlines` attribute) for the `<Text />` component
9
+ - Fixed issue with order of lifecycle event emits
10
+ - Fixed logic to not unfoces a parent when it passes focus to a child
11
+ - Removed temporary fix for renderer issue [#123](https://github.com/lightning-js/renderer/issues/123)
12
+
13
+ # v0.6.12
14
+
15
+ _9 feb 2024_
16
+
17
+ - Added type hinting for `this.$clearInterval` and `this.$clearTimeout`
18
+ - Fixed issue with `.gitignore` in app create flow
19
+ - Added support for dynamic route parts in the router (i.e `/tv/:series/episodes/:episode`)
20
+
3
21
  # v0.6.11
4
22
 
5
23
  _7 feb 2024_
package/bin/helpers.js CHANGED
@@ -182,7 +182,7 @@ export const gitInit = (cwd, fixturesBase) => {
182
182
  .then(({ stdout }) => (msg = stdout))
183
183
  .then(() => {
184
184
  return fs.copyFileSync(
185
- path.join(fixturesBase, 'common/git/.gitignore'),
185
+ path.join(fixturesBase, 'common/git/gitignore'),
186
186
  path.join(cwd, '.gitignore')
187
187
  )
188
188
  })
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ node_modules
3
+ build
4
+ dist
5
+ env
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "test": "c8 npm run test:run",
14
- "test:run": "tape '{,!(node_modules)/**/}*.test.js' | tap-diff",
14
+ "test:run": "node -r 'global-jsdom/register' ./node_modules/.bin/tape '{,!(node_modules)/**/}*.test.js' | tap-diff",
15
15
  "lint": "eslint '**/*.js'",
16
16
  "lint:fix": "eslint '**/*.js' --fix",
17
17
  "prepublishOnly": "node scripts/prepublishOnly.js",
@@ -36,14 +36,16 @@
36
36
  "eslint": "^8.8.0",
37
37
  "eslint-config-prettier": "^8.3.0",
38
38
  "eslint-plugin-prettier": "^4.0.0",
39
+ "global-jsdom": "24.0.0",
39
40
  "husky": "^7.0.4",
41
+ "jsdom": "24.0.0",
40
42
  "lint-staged": "^12.3.3",
41
43
  "prettier": "^2.5.1",
42
44
  "tap-diff": "^0.1.1",
43
45
  "tape": "^5.5.0"
44
46
  },
45
47
  "dependencies": {
46
- "@lightningjs/renderer": "^0.7.1",
48
+ "@lightningjs/renderer": "^0.7.2",
47
49
  "@lightningjs/vite-plugin-import-chunk-url": "^0.3.0",
48
50
  "execa": "^8.0.1",
49
51
  "kolorist": "^1.8.0",
@@ -142,12 +142,22 @@ declare namespace Component {
142
142
  * Set a timeout that is automatically cleaned upon component destroy
143
143
  */
144
144
  $setTimeout: (callback: (args: any) => void, ms?: number | undefined) => ReturnType<typeof setTimeout>
145
+
146
+ /**
147
+ * Clear a timeout
148
+ */
149
+ $clearTimeout: (id: ReturnType<typeof setTimeout>) => void
145
150
 
146
151
  /**
147
152
  * Set an interval that is automatically cleaned upon component destroy
148
153
  */
149
154
  $setInterval: (callback: (args: any) => void, ms?: number | undefined) => ReturnType<typeof setInterval>
150
-
155
+
156
+ /**
157
+ * Clear a interval
158
+ */
159
+ $clearInterval: (id: ReturnType<typeof setInterval>) => void
160
+
151
161
  /**
152
162
  * Log to the console with prettier output and configurable debug levels in Settings
153
163
  */
package/src/component.js CHANGED
@@ -97,15 +97,13 @@ const Component = (name = required('name'), config = required('config')) => {
97
97
  },
98
98
  set state(v) {
99
99
  if (states.indexOf(v) > -1 && v !== this.current) {
100
- Log.debug(
101
- `Setting lifecycle state from ${this.previous} to ${v} for ${scope.componentId}`
102
- )
100
+ Log.debug(`Setting lifecycle state from ${this.current} to ${v} for ${scope.componentId}`)
103
101
  this.previous = this.current
102
+ this.current = v
104
103
  // emit 'private' hook
105
104
  privateEmit(v, name, scope)
106
105
  // emit 'public' hook
107
106
  emit(v, name, scope)
108
- this.current = v
109
107
  }
110
108
  },
111
109
  }
@@ -19,8 +19,6 @@ import Component from '../component.js'
19
19
  import { renderer } from '../launch.js'
20
20
  import fps_sprite from '../../public/assets/fps_sprite.base64.js'
21
21
 
22
- console.log(fps_sprite)
23
-
24
22
  export default () =>
25
23
  Component('FPScounter', {
26
24
  /* eslint-disable no-unused-vars */
@@ -19,8 +19,6 @@ import Component from '../component.js'
19
19
  import { renderer } from '../launch.js'
20
20
  import fps_sprite from '../../public/assets/fps_sprite.base64.js'
21
21
 
22
- console.log(fps_sprite)
23
-
24
22
  export default () =>
25
23
  Component('FPScounter', {
26
24
  template: `
@@ -67,6 +67,9 @@ export default () =>
67
67
  function anonymous(component, elms, context) {
68
68
  elms[0].set('maxLines', component.maxlines)
69
69
  },
70
+ function anonymous(component, elms, context) {
71
+ elms[0].set('h', component.maxheight)
72
+ },
70
73
  function anonymous(component, elms, context) {
71
74
  elms[0].set('textAlign', component.align)
72
75
  },
@@ -95,6 +98,7 @@ export default () =>
95
98
  'align',
96
99
  'wordwrap',
97
100
  'maxlines',
101
+ 'maxheight',
98
102
  'lineheight',
99
103
  'contain',
100
104
  '@loaded',
@@ -105,7 +109,7 @@ export default () =>
105
109
  _contain() {
106
110
  return (
107
111
  this.contain ||
108
- (this.wordwrap && this.maxlines ? 'both' : this.wordwrap ? 'width' : 'none')
112
+ (this.maxheight && this.wordwrap ? 'both' : this.wordwrap ? 'width' : 'none')
109
113
  )
110
114
  },
111
115
  _lineheight() {
@@ -35,6 +35,7 @@ export default () =>
35
35
  contain="$_contain"
36
36
  :wordWrap="$wordwrap"
37
37
  :maxLines="$maxlines"
38
+ :h="$maxheight"
38
39
  :textAlign="$align"
39
40
  :overflowSuffix="$textoverflow"
40
41
  @loaded="$@loaded"
@@ -59,6 +60,7 @@ export default () =>
59
60
  'align',
60
61
  'wordwrap',
61
62
  'maxlines',
63
+ 'maxheight',
62
64
  'lineheight',
63
65
  'contain',
64
66
  '@loaded',
@@ -69,7 +71,7 @@ export default () =>
69
71
  _contain() {
70
72
  return (
71
73
  this.contain ||
72
- (this.wordwrap && this.maxlines ? 'both' : this.wordwrap ? 'width' : 'none')
74
+ (this.maxheight && this.wordwrap ? 'both' : this.wordwrap ? 'width' : 'none')
73
75
  )
74
76
  },
75
77
  _lineheight() {
package/src/element.js CHANGED
@@ -51,8 +51,6 @@ const unpackTransition = (obj) => {
51
51
  return obj
52
52
  }
53
53
 
54
- let firstNode = false
55
-
56
54
  const Props = {
57
55
  set parent(v) {
58
56
  this._props.parent = v === 'root' ? renderer.root : v.node
@@ -201,7 +199,7 @@ const Props = {
201
199
  this._props.contain = v
202
200
  },
203
201
  set maxLines(v) {
204
- this.height = v * this.element.node.lineHeight
202
+ this._props.maxLines = v
205
203
  },
206
204
  set overflowSuffix(v) {
207
205
  this._props.overflowSuffix = v === false ? ' ' : v === true ? undefined : v
@@ -247,13 +245,6 @@ const Element = {
247
245
  this.props._set.has('src') || this.props._set.has('texture') ? 0xffffffff : 0
248
246
  }
249
247
 
250
- // temporary workaround for renderer issue https://github.com/lightning-js/renderer/issues/123
251
- if (!firstNode) {
252
- this.props._props.src =
253
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII='
254
- firstNode = true
255
- }
256
-
257
248
  this.node = props.__textnode
258
249
  ? renderer.createTextNode(this.props._props)
259
250
  : renderer.createNode(this.props._props)
package/src/focus.js CHANGED
@@ -35,7 +35,7 @@ export default {
35
35
  },
36
36
  set(component, event) {
37
37
  clearTimeout(setFocusTimeout)
38
- focusedComponent && focusedComponent.unfocus()
38
+ focusedComponent && focusedComponent !== component.parent && focusedComponent.unfocus()
39
39
  focusChain.reverse().forEach((cmp) => cmp.unfocus())
40
40
  if (component !== focusedComponent) {
41
41
  setFocusTimeout = setTimeout(
@@ -36,17 +36,68 @@ export const getHash = () => {
36
36
  return (document.location.hash || '/').replace(/^#/, '')
37
37
  }
38
38
 
39
+ const normalizePath = (path) => {
40
+ return (
41
+ path
42
+ // remove leading and trailing slashes
43
+ .replace(/^\/+|\/+$/g, '')
44
+ .toLowerCase()
45
+ )
46
+ }
47
+
39
48
  export const matchHash = (path, routes = []) => {
40
- const route = routes
41
- .filter((r) => {
42
- return r.path === path
43
- })
44
- .pop()
45
- if (route) {
46
- route.options = { ...route.options, ...overrideOptions }
47
- currentRoute = route
49
+ // remove trailing slashes
50
+ const originalPath = path
51
+ path = normalizePath(path)
52
+ let matchingRoute = false
53
+ let i = 0
54
+ while (!matchingRoute && i < routes.length) {
55
+ const route = routes[i]
56
+ route.path = normalizePath(route.path)
57
+ if (route.path === path) {
58
+ matchingRoute = route
59
+ } else if (route.path.indexOf(':') > -1) {
60
+ // match dynamic route parts
61
+ const dynamicRouteParts = [...route.path.matchAll(/:([^\s/]+)/gi)]
62
+
63
+ // construct a regex for the route with dynamic parts
64
+ let dynamicRoutePartsRegex = route.path
65
+ dynamicRouteParts.reverse().forEach((part) => {
66
+ dynamicRoutePartsRegex =
67
+ dynamicRoutePartsRegex.substring(0, part.index) +
68
+ '([^\\s/]+)' +
69
+ dynamicRoutePartsRegex.substring(part.index + part[0].length)
70
+ })
71
+
72
+ // test if the constructed regex matches the path
73
+ const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i'))
74
+
75
+ if (match) {
76
+ // map the route params to a params object
77
+ route.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
78
+ acc[part[1]] = match[index + 1]
79
+ return acc
80
+ }, {})
81
+ matchingRoute = route
82
+ }
83
+ } else if (route.path.endsWith('*')) {
84
+ const regex = new RegExp(route.path.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
85
+ const match = path.match(regex)
86
+
87
+ if (match) {
88
+ if (match[1]) route.params = { path: match[1] }
89
+ matchingRoute = route
90
+ }
91
+ }
92
+ i++
48
93
  }
49
- return route
94
+
95
+ if (matchingRoute) {
96
+ matchingRoute.options = { ...matchingRoute.options, ...overrideOptions }
97
+ currentRoute = matchingRoute
98
+ }
99
+
100
+ return matchingRoute
50
101
  }
51
102
 
52
103
  export const navigate = async function () {
@@ -81,16 +132,18 @@ export const navigate = async function () {
81
132
  holder.populate({})
82
133
  holder.set('w', '100%')
83
134
  holder.set('h', '100%')
84
- view = await route.component(this[symbols.props], holder, this)
135
+ // merge props with potential route params to be injected into the component instance
136
+ const props = { ...this[symbols.props], ...route.params }
137
+ view = await route.component({ props }, holder, this)
85
138
  if (view[Symbol.toStringTag] === 'Module') {
86
139
  if (view.default && typeof view.default === 'function') {
87
- view = view.default(this[symbols.props], holder, this)
140
+ view = view.default({ props }, holder, this)
88
141
  } else {
89
142
  Log.error("Dynamic import doesn't have a default export or default is not a function")
90
143
  }
91
144
  }
92
145
  if (typeof view === 'function') {
93
- view = view(this[symbols.props], holder, this)
146
+ view = view({ props }, holder, this)
94
147
  }
95
148
  } else {
96
149
  holder = view[symbols.wrapper]
@@ -207,7 +260,13 @@ export const back = () => {
207
260
  if (route) {
208
261
  // set indicator that we are navigating back (to prevent adding page to history stack)
209
262
  navigatingBack = true
210
- to(route.path)
263
+ let targetRoutePath = route.path
264
+ if (targetRoutePath.indexOf(':') > -1) {
265
+ Object.keys(route.params).forEach((item) => {
266
+ targetRoutePath = targetRoutePath.replace(`:${item}`, route.params[item])
267
+ })
268
+ }
269
+ to(targetRoutePath)
211
270
  return true
212
271
  } else {
213
272
  return false
@@ -0,0 +1,238 @@
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
+ import test from 'tape'
19
+ import { matchHash } from './router.js'
20
+
21
+ const routes = [
22
+ {
23
+ path: '/',
24
+ },
25
+ {
26
+ path: '/page1',
27
+ },
28
+ {
29
+ path: '/page1/subpage1',
30
+ },
31
+ {
32
+ path: '/tv/:show/seasons/:season',
33
+ },
34
+ {
35
+ path: '/movies/:name',
36
+ },
37
+ {
38
+ path: '/examples/*',
39
+ },
40
+ {
41
+ path: '/route/with/trailing/slash/',
42
+ },
43
+ {
44
+ path: '*',
45
+ },
46
+ ]
47
+
48
+ test('Type of matchHash', (assert) => {
49
+ const expected = 'function'
50
+ const actual = typeof matchHash
51
+
52
+ assert.equal(actual, expected, 'matchhash should be a function')
53
+ assert.end()
54
+ })
55
+
56
+ test('Match paths with static routes that have an exact match', (assert) => {
57
+ assert.equal(matchHash('/', routes), routes[0], 'Should return the correct route object')
58
+ assert.equal(matchHash('/page1', routes), routes[1], 'Should return the correct route object')
59
+ assert.equal(
60
+ matchHash('/page1/subpage1', routes),
61
+ routes[2],
62
+ 'Should return the correct route object'
63
+ )
64
+ assert.end()
65
+ })
66
+
67
+ test('Match paths with dynamic route parts', (assert) => {
68
+ assert.equal(
69
+ matchHash('/tv/simpsons/seasons/first', routes),
70
+ routes[3],
71
+ 'Should return the correct route object'
72
+ )
73
+
74
+ assert.equal(
75
+ matchHash('/movies/avengers-endgame', routes),
76
+ routes[4],
77
+ 'Should return the correct route object'
78
+ )
79
+ assert.end()
80
+ })
81
+
82
+ test('Match paths with a wildcard asterix', (assert) => {
83
+ assert.equal(
84
+ matchHash('/examples/example1', routes),
85
+ routes[5],
86
+ 'Should return the correct route object'
87
+ )
88
+
89
+ assert.equal(
90
+ matchHash('/examples/example1/subexample/and-another-page', routes),
91
+ routes[5],
92
+ 'Should return the correct route object'
93
+ )
94
+
95
+ assert.equal(matchHash('/404', routes), routes[7], 'Should return the correct route object')
96
+
97
+ assert.equal(
98
+ matchHash('/page1/subpage1/i-dont-exist', routes),
99
+ routes[7],
100
+ 'Should return the correct route object'
101
+ )
102
+
103
+ assert.end()
104
+ })
105
+
106
+ test('Add params to route for dynamic route matches', (assert) => {
107
+ const match1 = matchHash('/tv/simpsons/seasons/first', routes)
108
+
109
+ assert.deepEqual(
110
+ match1.params,
111
+ {
112
+ show: 'simpsons',
113
+ season: 'first',
114
+ },
115
+ 'Should return the correct params object'
116
+ )
117
+
118
+ const match2 = matchHash('/movies/avengers-endgame', routes)
119
+ assert.deepEqual(
120
+ match2.params,
121
+ {
122
+ name: 'avengers-endgame',
123
+ },
124
+ 'Should return the correct route object'
125
+ )
126
+
127
+ assert.end()
128
+ })
129
+
130
+ test('Add remaining path as param for wild card routes', (assert) => {
131
+ const match1 = matchHash('/examples/example1', routes)
132
+ assert.deepEqual(match1.params, { path: 'example1' }, 'Should return the correct route object')
133
+
134
+ const match2 = matchHash('/examples/example1/subexample/and-another-page', routes)
135
+ assert.deepEqual(
136
+ match2.params,
137
+ { path: 'example1/subexample/and-another-page' },
138
+ 'Should return the correct params object'
139
+ )
140
+
141
+ const match3 = matchHash('/404', routes)
142
+ assert.deepEqual(match3.params, { path: '404' }, 'Should return the correct params object')
143
+
144
+ const match4 = matchHash('/page1/subpage1/i-dont-exist', routes)
145
+ assert.deepEqual(
146
+ match4.params,
147
+ { path: 'page1/subpage1/i-dont-exist' },
148
+ 'Should return the correct params object'
149
+ )
150
+
151
+ assert.end()
152
+ })
153
+
154
+ test('Work with trailing slashes', (assert) => {
155
+ assert.equal(matchHash('/page1/', routes), routes[1], 'Should return the correct route object')
156
+ assert.equal(
157
+ matchHash('/page1//////', routes),
158
+ routes[1],
159
+ 'Should return the correct route object'
160
+ )
161
+ assert.equal(
162
+ matchHash('/page1/subpage1/', routes),
163
+ routes[2],
164
+ 'Should return the correct route object'
165
+ )
166
+
167
+ assert.equal(
168
+ matchHash('/route/with/trailing/slash', routes),
169
+ routes[6],
170
+ 'Should return the correct route object'
171
+ )
172
+
173
+ assert.end()
174
+ })
175
+
176
+ test('Work with and without leading slashes', (assert) => {
177
+ assert.equal(matchHash('page1/', routes), routes[1], 'Should return the correct route object')
178
+
179
+ assert.equal(matchHash('////page1', routes), routes[1], 'Should return the correct route object')
180
+ assert.equal(
181
+ matchHash('page1/subpage1/', routes),
182
+ routes[2],
183
+ 'Should return the correct route object'
184
+ )
185
+
186
+ assert.equal(
187
+ matchHash('route/with/trailing/slash', routes),
188
+ routes[6],
189
+ 'Should return the correct route object'
190
+ )
191
+
192
+ assert.end()
193
+ })
194
+
195
+ test('Match routes case insensitive', (assert) => {
196
+ assert.equal(matchHash('/', routes), routes[0], 'Should return the correct route object')
197
+ assert.equal(matchHash('/Page1', routes), routes[1], 'Should return the correct route object')
198
+ assert.equal(
199
+ matchHash('/pagE1/SUBpage1', routes),
200
+ routes[2],
201
+ 'Should return the correct route object'
202
+ )
203
+ assert.equal(
204
+ matchHash('/Tv/SimpsonS/Seasons/First', routes),
205
+ routes[3],
206
+ 'Should return the correct route object'
207
+ )
208
+
209
+ assert.equal(
210
+ matchHash('/Movies/Avengers-Endgame', routes),
211
+ routes[4],
212
+ 'Should return the correct route object'
213
+ )
214
+ assert.end()
215
+ })
216
+
217
+ test('Match routes case insensitive, but pass props with original casing', (assert) => {
218
+ const match1 = matchHash('/Tv/SimpsonS/Seasons/First', routes)
219
+ assert.deepEqual(
220
+ match1.params,
221
+ {
222
+ show: 'SimpsonS',
223
+ season: 'First',
224
+ },
225
+ 'Should return the correct params object'
226
+ )
227
+
228
+ const match2 = matchHash('/Movies/Avengers-Endgame', routes)
229
+ assert.deepEqual(
230
+ match2.params,
231
+ {
232
+ name: 'Avengers-Endgame',
233
+ },
234
+ 'Should return the correct params object'
235
+ )
236
+
237
+ assert.end()
238
+ })