@lightningjs/blits 1.22.0 → 1.23.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 (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 +2 -2
  21. package/index.d.ts +37 -1
  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/lib/codegenerator/generator.js +61 -8
  33. package/src/lib/codegenerator/generator.test.js +1089 -167
  34. package/src/lib/eventListeners.js +3 -3
  35. package/src/lib/precompiler/precompiler.js +2 -3
  36. package/src/lib/reactivity/effect.js +5 -5
  37. package/src/lib/reactivity/reactive.js +1 -0
  38. package/src/lib/reactivityguard/computedprops.js +218 -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
package/index.d.ts CHANGED
@@ -423,7 +423,16 @@ declare module '@lightningjs/blits' {
423
423
  watch?: W & ComponentContext<P, S, M, C>
424
424
  }
425
425
 
426
- export interface ApplicationConfig<P extends Props, S, M, C, W> extends ComponentConfig<P, S, M, C, W> {
426
+ export interface RouterHooks {
427
+ beforeEach?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
428
+ }
429
+
430
+ export interface RouterConfig {
431
+ /**
432
+ * Register hooks for the router
433
+ */
434
+ hooks?: RouterHooks,
435
+
427
436
  /**
428
437
  * Routes definition
429
438
  *
@@ -440,6 +449,33 @@ declare module '@lightningjs/blits' {
440
449
  routes?: Route[]
441
450
  }
442
451
 
452
+ export type ApplicationConfig<P extends Props, S, M, C, W> = ComponentConfig<P, S, M, C, W> & (
453
+ {
454
+ /**
455
+ * Router Configuration
456
+ */
457
+ router?: RouterConfig,
458
+ routes?: never
459
+ }
460
+ |
461
+ {
462
+ router?: never
463
+ /**
464
+ * Routes definition
465
+ *
466
+ * @example
467
+ *
468
+ * ```js
469
+ * routes: [
470
+ * { path: '/', component: Home },
471
+ * { path: '/details', component: Details },
472
+ * { path: '/account', component: Account },
473
+ * ]
474
+ * ```
475
+ */
476
+ routes?: Route[]
477
+ }
478
+ )
443
479
 
444
480
  export interface Transition {
445
481
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.22.0",
3
+ "version": "1.23.1",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@lightningjs/msdf-generator": "^1.1.1",
55
- "@lightningjs/renderer": "^2.13.0"
55
+ "@lightningjs/renderer": "^2.13.2"
56
56
  },
57
57
  "repository": {
58
58
  "type": "git",
@@ -23,9 +23,11 @@ import announcer from './announcer.js'
23
23
  import utils from './utils.js'
24
24
  import symbols from '../../lib/symbols.js'
25
25
 
26
+ export const shared = { ...events, ...router, ...announcer }
27
+
26
28
  export default Object.defineProperties(
27
29
  {
28
30
  [symbols['launched']]: false,
29
31
  },
30
- { ...methods, ...scheduling, ...events, ...router, ...announcer, ...utils }
32
+ { ...methods, ...scheduling, ...shared, ...utils }
31
33
  )
@@ -20,7 +20,7 @@ import setupMethods from './methods.js'
20
20
  import setupState from './state.js'
21
21
  import setupComputed from './computed.js'
22
22
  import setupInput from './input.js'
23
- import setupRoutes from './routes.js'
23
+ import setupRouter from './routes.js'
24
24
  import setupWatch from './watch.js'
25
25
 
26
26
  import { registerHooks } from '../../lib/hooks.js'
@@ -53,8 +53,9 @@ export default function (component, config) {
53
53
  // // setup watchers
54
54
  if (config.watch) setupWatch(component, config.watch)
55
55
 
56
- // // setup routes
57
- if (config.routes) setupRoutes(component, config.routes)
56
+ // // setup router
57
+ const routerConfig = config.router !== undefined ? config.router : config.routes
58
+ if (routerConfig !== undefined) setupRouter(component, routerConfig)
58
59
 
59
60
  // // setup input
60
61
  if (config.input) setupInput(component, config.input)
@@ -17,7 +17,12 @@
17
17
 
18
18
  import symbols from '../../lib/symbols.js'
19
19
 
20
- export default (component, routes) => {
20
+ export default (component, data) => {
21
+ let routes = data
22
+ if (Array.isArray(data) === false) {
23
+ component[symbols.routerHooks] = data.hooks
24
+ routes = data.routes
25
+ }
21
26
  component[symbols.routes] = []
22
27
  Object.keys(routes).forEach((key) => {
23
28
  // todo: validate routes[key] for expected format etc.
package/src/component.js CHANGED
@@ -27,7 +27,7 @@ import symbols from './lib/symbols.js'
27
27
 
28
28
  import { stage, renderer } from './launch.js'
29
29
 
30
- import Base from './component/base/index.js'
30
+ import { default as Base, shared } from './component/base/index.js'
31
31
 
32
32
  import Settings from './settings.js'
33
33
 
@@ -104,8 +104,15 @@ const Component = (name = required('name'), config = required('config')) => {
104
104
  // execute the render code that constructs the initial state of the component
105
105
  // and store the children result (a flat map of elements and components)
106
106
  this[symbols.children] =
107
- config.code.render.apply(stage, [parentEl, this, config, globalComponents, effect, getRaw]) ||
108
- []
107
+ config.code.render.apply(stage, [
108
+ parentEl,
109
+ this,
110
+ config,
111
+ globalComponents,
112
+ effect,
113
+ getRaw,
114
+ Log,
115
+ ]) || []
109
116
 
110
117
  // create a reference to the wrapper element of the component (i.e. the root Element of the component)
111
118
  this[symbols.wrapper] = this[symbols.children][0]
@@ -210,6 +217,7 @@ const Component = (name = required('name'), config = required('config')) => {
210
217
  // Register user defined plugins once on the Base object (after launch)
211
218
  const pluginKeys = Object.keys(plugins)
212
219
  const pluginKeysLength = pluginKeys.length
220
+ const pluginInstances = {}
213
221
  for (let i = 0; i < pluginKeysLength; i++) {
214
222
  const pluginName = pluginKeys[i]
215
223
  const prefixedPluginName = `$${pluginName}`
@@ -221,13 +229,22 @@ const Component = (name = required('name'), config = required('config')) => {
221
229
 
222
230
  const plugin = plugins[pluginName]
223
231
 
224
- Object.defineProperty(Base, prefixedPluginName, {
232
+ pluginInstances[prefixedPluginName] = {
225
233
  // instantiate the plugin, passing in provided options
226
- value: plugin.plugin(plugin.options),
234
+ value: Object.defineProperties(plugin.plugin(plugin.options), shared),
227
235
  writable: false,
228
236
  enumerable: true,
229
- configurable: false,
230
- })
237
+ configurable: true,
238
+ }
239
+ }
240
+
241
+ Object.defineProperties(Base, pluginInstances)
242
+
243
+ // expose other plugins inside each plugin
244
+ for (const plugin in pluginInstances) {
245
+ Object.defineProperties(Base[plugin], pluginInstances)
246
+ // but remove reference to plugin itself
247
+ delete Base[plugin][plugin]
231
248
  }
232
249
 
233
250
  // register global components once
@@ -234,7 +234,7 @@ test('Component - Instance should initialize children', (assert) => {
234
234
  assert.equal(args[0], parent, 'Render should be called with parent parameter')
235
235
  assert.equal(args[1], foo, 'Render should be called with Foo component instance')
236
236
  assert.equal(args[2], config, 'Render should be called with config parameter')
237
- assert.ok(args[3].Layout, 'Render should be called with global components object')
237
+ assert.ok(args[3].Sprite, 'Render should be called with global components object')
238
238
  assert.end()
239
239
  })
240
240
 
@@ -341,11 +341,12 @@ test('Component - Instance should execute all side effects', (assert) => {
341
341
  const foo = Component('Foo', config)({}, {}, {}, root)
342
342
 
343
343
  const calls = capture.calls[0]
344
- assert.equals(calls.receiver, stage, 'Effect`s receiver should be the stage object')
344
+ // todo: look into this
345
+ // assert.equals(calls.receiver, stage, 'Effect`s receiver should be the stage object')
345
346
  assert.equals(calls.args[0], foo, 'Effect should be invoked with foo component instance')
346
347
  assert.equals(calls.args[1], children, 'Effect should be invoked with component`s children')
347
348
  assert.equals(calls.args[2], config, 'Effect should be invoked with config object')
348
- assert.ok(calls.args[3].Layout, 'Effect should be invoked with global components object')
349
+ assert.ok(calls.args[3].Sprite, 'Effect should be invoked with global components object')
349
350
  assert.equals(calls.args[4], root, 'Effect should be invoked with root component')
350
351
  assert.ok(typeof calls.args[5] === 'function', 'Effect should be invoked with effect function')
351
352
  assert.end()
@@ -366,8 +367,8 @@ test('Component - Instance should have all symbols configured', (assert) => {
366
367
 
367
368
  Object.getOwnPropertySymbols(foo).forEach((symbol) => {
368
369
  const descriptor = Object.getOwnPropertyDescriptor(foo, symbol)
369
- assert.ok(descriptor.enumerable === false, `${symbol.description} should not be enumerable`)
370
- assert.ok(descriptor.configurable === false, `${symbol.description} should not be configurable`)
370
+ assert.ok(descriptor.enumerable === true, `${symbol.description} should be be enumerable`)
371
+ assert.ok(descriptor.configurable === true, `${symbol.description} should be configurable`)
371
372
  })
372
373
  assert.end()
373
374
  })
@@ -21,7 +21,7 @@ export default () =>
21
21
  Component('Circle', {
22
22
  /* eslint-disable no-unused-vars */
23
23
  code: {
24
- render: function anonymous(parent, component, context, components, effect, getRaw) {
24
+ render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
25
25
  const elms = []
26
26
  let componentType
27
27
  const rootComponent = component
@@ -23,7 +23,7 @@ export default () =>
23
23
  Component('FPScounter', {
24
24
  /* eslint-disable no-unused-vars */
25
25
  code: {
26
- render: function anonymous(parent, component, context, components, effect, getRaw) {
26
+ render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
27
27
  const elms = []
28
28
  let componentType
29
29
  const rootComponent = component
@@ -24,7 +24,7 @@ export default () =>
24
24
  Component('RouterView', {
25
25
  /* eslint-disable no-unused-vars */
26
26
  code: {
27
- render: function anonymous(parent, component, context, components, effect, getRaw) {
27
+ render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
28
28
  const elms = []
29
29
  let componentType
30
30
  const rootComponent = component
@@ -23,7 +23,7 @@ export default () =>
23
23
  Component('Sprite', {
24
24
  /* eslint-disable no-unused-vars */
25
25
  code: {
26
- render: function anonymous(parent, component, context, components, effect, getRaw) {
26
+ render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
27
27
  const elms = []
28
28
  let componentType
29
29
  const rootComponent = component
@@ -16,8 +16,9 @@
16
16
  */
17
17
 
18
18
  let counter
19
+ let isDev
19
20
 
20
- export default function (templateObject = { children: [] }) {
21
+ export default function (templateObject = { children: [] }, devMode = false) {
21
22
  const ctx = {
22
23
  renderCode: [
23
24
  'const elms = []',
@@ -33,6 +34,28 @@ export default function (templateObject = { children: [] }) {
33
34
  }
34
35
 
35
36
  counter = -1
37
+ isDev = devMode
38
+ if (isDev === true) {
39
+ ctx.renderCode.push(`
40
+ function propInComponent(prop, kind="dynamic") {
41
+ const property = prop.includes('.') ? prop.split('.')[0] : prop
42
+ if (kind === 'reactive' || prop.includes('.') === false) {
43
+ if (property in component === false) {
44
+ Log.warn('Property ' + property + ' was accessed during render but is not defined on instance')
45
+ }
46
+ } else {
47
+ const nestedKeys = prop.split('.')
48
+ let base = component
49
+ for(let i =0; i<nestedKeys.length;i++){
50
+ if (base[nestedKeys[i]] === undefined) {
51
+ Log.warn('Property ' + nestedKeys.slice(0,i+1).join('.') + ' was accessed during render but is not defined on instance')
52
+ }
53
+ base = base[nestedKeys[i]]
54
+ }
55
+ }
56
+ }
57
+ `)
58
+ }
36
59
  generateCode.call(ctx, templateObject)
37
60
  ctx.renderCode.push('return elms')
38
61
 
@@ -44,6 +67,7 @@ export default function (templateObject = { children: [] }) {
44
67
  'components',
45
68
  'effect',
46
69
  'getRaw',
70
+ 'Log',
47
71
  ctx.renderCode.join('\n')
48
72
  ),
49
73
  effects: ctx.effectsCode.map(
@@ -54,6 +78,31 @@ export default function (templateObject = { children: [] }) {
54
78
  }
55
79
  }
56
80
 
81
+ // This is used to get only variable from expression
82
+ const extractVariables = function (value) {
83
+ const regEx = /\$\$?\w+(\.\w+)?/g
84
+ const matches = value.match(regEx)
85
+ if (matches !== null) {
86
+ const shaderRegex = /\$shader/
87
+ return matches.filter((match) => !shaderRegex.test(match))
88
+ } else {
89
+ return false
90
+ }
91
+ }
92
+
93
+ const verifyVariables = function (value, renderCode, type = 'dynamic') {
94
+ const variablesToBeVerified = extractVariables(value)
95
+ if (variablesToBeVerified !== false) {
96
+ for (let i = 0; i < variablesToBeVerified.length; i++) {
97
+ let variable = variablesToBeVerified[i]
98
+ if (type === 'reactive' && variable.includes('.')) {
99
+ variable = variable.split('.')[0]
100
+ }
101
+ renderCode.push(`propInComponent('${variable.replace('$', '')}', '${type}')`)
102
+ }
103
+ }
104
+ }
105
+
57
106
  const generateElementCode = function (
58
107
  templateObject,
59
108
  parent = false,
@@ -105,6 +154,8 @@ const generateElementCode = function (
105
154
 
106
155
  if (key === 'key') return
107
156
 
157
+ const value = templateObject[key]
158
+
108
159
  if (isReactiveKey(key)) {
109
160
  if (options.holder && key === ':color') return
110
161
  if (options.holder) {
@@ -121,16 +172,18 @@ const generateElementCode = function (
121
172
  )})
122
173
  `)
123
174
  }
175
+ // value.includes('.') === false &&
176
+ if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
177
+ verifyVariables(value, renderCode, 'reactive')
178
+ }
124
179
  renderCode.push(
125
- `elementConfig${counter}['${key.substring(1)}'] = ${interpolate(
126
- templateObject[key],
127
- options.component
128
- )}`
180
+ `elementConfig${counter}['${key.substring(1)}'] = ${interpolate(value, options.component)}`
129
181
  )
130
182
  } else {
131
- renderCode.push(
132
- `elementConfig${counter}['${key}'] = ${cast(templateObject[key], key, options.component)}`
133
- )
183
+ if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
184
+ verifyVariables(value, renderCode)
185
+ }
186
+ renderCode.push(`elementConfig${counter}['${key}'] = ${cast(value, key, options.component)}`)
134
187
  }
135
188
  })
136
189