@lightningjs/blits 0.7.2 → 0.7.3

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,16 @@
1
1
  # Changelog
2
2
 
3
+ # v0.7.3
4
+
5
+ _28 feb 2024_
6
+
7
+ - Added functionality that forces focus hook to fire when parent receives focus from child
8
+ - Improved destroy sequence of nodes to be disposed
9
+ - Fixed issue with tracking reactive objects multiple times (potential max call stack error)
10
+ - Added on the fly MSDF font generation from a `.ttf` file placed in `publics/fonts`
11
+ - Added support for pre-compilation for files with more than 1 Blits component
12
+ - Fixed transitions on scale attribute when passed an object with `x` and `y` values
13
+
3
14
  # v0.7.2
4
15
 
5
16
  _23 feb 2024_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
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": "node -r 'global-jsdom/register' ./node_modules/.bin/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",
@@ -40,6 +40,8 @@
40
40
  "husky": "^7.0.4",
41
41
  "jsdom": "24.0.0",
42
42
  "lint-staged": "^12.3.3",
43
+ "msdf-bmfont-xml": "^2.7.0",
44
+ "ora": "^7.0.1",
43
45
  "prettier": "^2.5.1",
44
46
  "tap-diff": "^0.1.1",
45
47
  "tape": "^5.5.0"
package/src/element.js CHANGED
@@ -158,12 +158,18 @@ const Props = {
158
158
  },
159
159
  set scale(v) {
160
160
  if (typeof v === 'object' || (isObjectString(v) && (v = parseToObject(v)))) {
161
- if ('x' in v) this._props.scaleX = v.x
162
- if ('y' in v) this._props.scaleY = v.y
161
+ if ('x' in v) {
162
+ this._set.add('scaleX')
163
+ this._props.scaleX = v.x
164
+ }
165
+ if ('y' in v) {
166
+ this._set.add('scaleX')
167
+ this._props.scaleY = v.y
168
+ }
163
169
  } else {
164
170
  this._props.scale = v
171
+ this._set.add('scale')
165
172
  }
166
- this._set.add('scale')
167
173
  },
168
174
  set show(v) {
169
175
  this._props.alpha = v ? 1 : 0
@@ -346,7 +352,7 @@ const Element = {
346
352
  Object.values(this.scheduledTransitions).forEach((scheduledTransition) => {
347
353
  clearTimeout(scheduledTransition.timeout)
348
354
  })
349
- this.node.parent = null
355
+ this.node.destroy()
350
356
  },
351
357
  get nodeId() {
352
358
  return this.node && this.node.id
package/src/fontLoader.js CHANGED
@@ -6,6 +6,15 @@ export default class FontLoader extends CoreExtension {
6
6
  async run(stage) {
7
7
  Settings.get('fonts', []).forEach((font) => {
8
8
  if (font.type === 'sdf' || font.type === 'msdf') {
9
+ // automatically map png key to file name
10
+ if (!font.png && font.file) {
11
+ font.png = font.file.replace(/\.[^.]+$/, `.${font.type}.png`)
12
+ }
13
+ // automatically map json to file name
14
+ if (!font.json && font.file) {
15
+ font.json = font.file.replace(/\.[^.]+$/, `.${font.type}.json`)
16
+ }
17
+
9
18
  stage.fontManager.addFontFace(
10
19
  new SdfTrFontFace(font.family, {}, font.type, stage, font.png, font.json)
11
20
  )
package/src/launch.d.ts CHANGED
@@ -32,15 +32,14 @@ interface WebFont {
32
32
  file: string
33
33
  }
34
34
 
35
- interface SdfFont {
35
+ interface SdfFontWithFile {
36
36
  /**
37
- * Name of the font family
38
- */
39
- family: string,
40
- /**
41
- * Type of font (msdf or sdf)
42
- */
43
- type: 'msdf' | 'sdf',
37
+ * Location of the font file (i.e. `/fonts/OpenSans-Medium.ttf`)
38
+ */
39
+ file: string
40
+ }
41
+
42
+ interface SdfFontWithPngJson {
44
43
  /**
45
44
  * Location of the font map (i.e. `'/fonts/Lato-Regular.msdf.json'`)
46
45
  */
@@ -51,6 +50,18 @@ interface SdfFont {
51
50
  png: string
52
51
  }
53
52
 
53
+ type SdfFont = {
54
+ /**
55
+ * Name of the font family
56
+ */
57
+ family: string,
58
+ /**
59
+ * Type of font (msdf or sdf)
60
+ */
61
+ type: 'msdf' | 'sdf',
62
+ } & (SdfFontWithFile | SdfFontWithPngJson)
63
+
64
+
54
65
  type ScreenResolutions = 'hd' | '720p' | 720 | 'fhd' | 'fullhd' | '1080p' | 1080 | '4k' | '2160p' | 2160
55
66
 
56
67
  type Font = WebFont | SdfFont
@@ -27,7 +27,7 @@ export default {
27
27
  return this.current
28
28
  },
29
29
  set state(v) {
30
- if (states.indexOf(v) > -1 && v !== this.current) {
30
+ if (states.indexOf(v) > -1 && (v !== this.current || v === 'focus')) {
31
31
  Log.debug(
32
32
  `Setting lifecycle state from ${this.current} to ${v} for ${this.component.componentId}`
33
33
  )
@@ -25,40 +25,46 @@ export default (source, filePath) => {
25
25
  source.indexOf('Blits.Application(') > -1 ||
26
26
  /=>\s*Component\(['"][A-Za-z]+['"],/s.test(source) // blits component
27
27
  ) {
28
- // get the start of the template key in de component configuration object
29
- const templateKeyRegex = /template:\s*([`"'])*/g
30
- const templateStartResult = templateKeyRegex.exec(source)
28
+ const templates = source.matchAll(/(?<!\/\/\s*)template\s*:\s*(['"`])((?:\\?.)*?)\1/gs)
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
31
35
 
32
- if (templateStartResult) {
33
- const typeOfQuotesUsed = templateStartResult[1]
34
- const templateStartIndex = templateStartResult.index
36
+ for (const template of templates) {
37
+ if (template[2]) {
38
+ const templateStartIndex = template.index + offset
39
+ const templateEndIndex = templateStartIndex + template[0].length
40
+ const templateContent = template[2]
35
41
 
36
- // get the template contents from the configuration object
37
- const templateContentsRegex = new RegExp(`${typeOfQuotesUsed}([\\s\\S]*?)${typeOfQuotesUsed}`)
38
- const templateContentResult = templateContentsRegex.exec(source.slice(templateStartIndex))
39
- const templateEndIndex =
40
- templateStartIndex + templateContentResult.index + templateContentResult[0].length
42
+ // Parse the template
43
+ let resourceName = 'Blits.Application'
44
+ if (source.indexOf('Blits.Component(') > -1) {
45
+ resourceName = source.match(/Blits\.Component\(['"](.*)['"]\s*,/)[1]
46
+ }
41
47
 
42
- // Parse the template
43
- let resourceName = 'Blits.Application'
44
- if (source.indexOf('Blits.Component(') > -1) {
45
- resourceName = source.match(/Blits\.Component\(['"](.*)['"]\s*,/)[1]
46
- }
47
- const componentPath = path.relative(process.cwd(), filePath)
48
- const parsed = parser(templateContentResult[1], resourceName, null, componentPath)
48
+ const componentPath = path.relative(process.cwd(), filePath)
49
+ const parsed = parser(templateContent, resourceName, null, componentPath)
50
+
51
+ // Generate the code
52
+ const code = generator.call({ components: {} }, parsed)
49
53
 
50
- // Generate the code
51
- const code = generator.call({ components: {} }, parsed)
54
+ // Insert the code in the component using the 'code' key, replacing the template key
55
+ const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map(
56
+ (fn) => fn.toString()
57
+ )}], context: {}}`
52
58
 
53
- // Insert the code in the component using the 'code' key, replacing the template key
54
- const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map(
55
- (fn) => fn.toString()
56
- )}], context: {}}`
57
- const newSource =
58
- source.substring(0, templateStartIndex) + replacement + source.substring(templateEndIndex)
59
+ offset += replacement.length - template[0].length
59
60
 
60
- return newSource
61
+ newSource =
62
+ newSource.substring(0, templateStartIndex) +
63
+ replacement +
64
+ newSource.substring(templateEndIndex)
65
+ }
61
66
  }
67
+ return newSource
62
68
  }
63
69
  return source
64
70
  }
@@ -46,12 +46,12 @@ const reactiveProxy = (target) => {
46
46
  if (Array.isArray(target) && arrayMethods.includes(key)) {
47
47
  return Reflect.get(target, key, receiver)
48
48
  }
49
- track(target, key)
50
49
 
51
50
  if (target[key] !== null && typeof target[key] === 'object') {
52
51
  return reactiveProxy(target[key])
53
52
  }
54
53
 
54
+ track(target, key)
55
55
  return Reflect.get(target, key, receiver)
56
56
  },
57
57
  set(target, key, value, receiver) {
package/vite/index.js CHANGED
@@ -15,10 +15,13 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import preCompiler from './preCompiler.js'
19
18
  import { importChunkUrl } from '@lightningjs/vite-plugin-import-chunk-url'
20
19
 
20
+ import preCompiler from './preCompiler.js'
21
+ import msdfGenerator from './msdfGenerator.js'
22
+
21
23
  export { importChunkUrl as importChunkUrl } from '@lightningjs/vite-plugin-import-chunk-url'
22
24
  export { default as preCompiler } from './preCompiler.js'
25
+ export { default as msdfGenerator } from './msdfGenerator.js'
23
26
 
24
- export default [importChunkUrl(), preCompiler()]
27
+ export default [importChunkUrl(), preCompiler(), msdfGenerator()]
@@ -0,0 +1,164 @@
1
+ import path from 'path'
2
+ import * as fs from 'fs'
3
+ import generateBMFont from 'msdf-bmfont-xml'
4
+
5
+ let config
6
+
7
+ export default function () {
8
+ let msdfOutputDir = ''
9
+ let buildOutputPath = ''
10
+ let publicDir = ''
11
+
12
+ return {
13
+ name: 'msdfGenerator',
14
+ configResolved(resolvedConfig) {
15
+ config = resolvedConfig
16
+ buildOutputPath = config.build.outDir
17
+ publicDir = path.join(config.root, 'public')
18
+ msdfOutputDir = path.resolve(config.root, 'node_modules', '.tmp-msdf-fonts')
19
+ },
20
+ async configureServer(server) {
21
+ server.middlewareMode = true
22
+ server.middlewares.use('/', async (req, res, next) => {
23
+ const file = req.url.substring(req.url.lastIndexOf('/') + 1)
24
+ const match = file.match(/(.+)\.msdf\.(json|png)/)
25
+
26
+ // msdf font request
27
+ if (match) {
28
+ const fontPath = path.dirname(req.url.split('?')[0]).replace(/^\//, '')
29
+ const fontDir = path.join(publicDir, fontPath) // actual font directory
30
+ const targetDir = path.join(msdfOutputDir, fontPath) // target for generated fonts
31
+
32
+ if (!fs.existsSync(path.resolve(fontDir, file))) {
33
+ const fontName = match[1]
34
+ const ext = match[2]
35
+ const fontFile = path.join(fontDir, `${fontName}.ttf`)
36
+
37
+ if (fs.existsSync(fontFile)) {
38
+ const generatedFontFile = path.join(
39
+ msdfOutputDir,
40
+ fontPath,
41
+ `${fontName}.msdf.${ext}`
42
+ )
43
+ const mimeType = ext === 'png' ? 'image/png' : 'application/json'
44
+
45
+ if (!fs.existsSync(generatedFontFile)) {
46
+ console.log(`Generating ${targetDir}/${fontName}.msdf.${ext}`)
47
+ await generateFont(fontFile, targetDir, fontName)
48
+ }
49
+
50
+ const fileContent = fs.readFileSync(generatedFontFile)
51
+
52
+ // Check if headers have already been sent
53
+ if (!res.headersSent) {
54
+ res.setHeader('Content-Type', mimeType)
55
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
56
+ res.end(fileContent)
57
+ } else {
58
+ // this should never happen except some edge cases
59
+ console.error(`ERROR: Headers already sent for ${req.url}`, res.getHeaders())
60
+ }
61
+ } else {
62
+ next() // ttf file does not exist
63
+ }
64
+ } else {
65
+ next() // file exists
66
+ }
67
+ } else {
68
+ next() // not msdf.json or msdf.png
69
+ }
70
+ })
71
+ },
72
+ // after build ends, the first hook is renderStart where we can modify the output
73
+ async renderStart() {
74
+ // Find all TTF files under publicDir
75
+ const ttfFiles = findAllTtfFiles(publicDir)
76
+
77
+ for (const ttfFile of ttfFiles) {
78
+ const relativePath = path.relative(publicDir, path.dirname(ttfFile))
79
+ const baseName = path.basename(ttfFile).replace(/\.ttf$/i, '')
80
+ const msdfJsonPath = path.join(msdfOutputDir, relativePath, `${baseName}.msdf.json`)
81
+ const msdfPngPath = path.join(msdfOutputDir, relativePath, `${baseName}.msdf.png`)
82
+
83
+ // Check if MSDF files are generated, if not, generate them
84
+ if (!fs.existsSync(msdfJsonPath) || !fs.existsSync(msdfPngPath)) {
85
+ console.log(`Generating missing MSDF files for ${ttfFile}`)
86
+ await generateFont(ttfFile, path.join(msdfOutputDir, relativePath), baseName)
87
+ }
88
+ }
89
+
90
+ if (fs.existsSync(msdfOutputDir)) {
91
+ copyDir(msdfOutputDir, buildOutputPath)
92
+ }
93
+ },
94
+ }
95
+ }
96
+
97
+ const generateFont = (fontFile, fontDir, fontName) => {
98
+ return new Promise((resolve, reject) => {
99
+ if (!fs.existsSync(fontDir)) {
100
+ fs.mkdirSync(fontDir, { recursive: true })
101
+ }
102
+ generateBMFont(
103
+ fontFile,
104
+ {
105
+ outputType: 'json',
106
+ },
107
+ (err, textures, font) => {
108
+ if (err) {
109
+ console.error(err)
110
+ reject(err)
111
+ } else {
112
+ textures.forEach((texture) => {
113
+ try {
114
+ fs.writeFileSync(path.resolve(fontDir, `${fontName}.msdf.png`), texture.texture)
115
+ } catch (e) {
116
+ console.error(e)
117
+ reject(e)
118
+ }
119
+ })
120
+ try {
121
+ fs.writeFileSync(path.resolve(fontDir, `${fontName}.msdf.json`), font.data)
122
+ resolve()
123
+ } catch (e) {
124
+ console.error(err)
125
+ reject(e)
126
+ }
127
+ }
128
+ }
129
+ )
130
+ })
131
+ }
132
+
133
+ // finds all TTF files in a directory
134
+ const findAllTtfFiles = (dir, filesList = []) => {
135
+ const files = fs.readdirSync(dir, { withFileTypes: true })
136
+
137
+ files.forEach((file) => {
138
+ if (file.isDirectory()) {
139
+ const dirPath = path.join(dir, file.name)
140
+ findAllTtfFiles(dirPath, filesList)
141
+ } else {
142
+ // Check for all case variations of TTF extension
143
+ if (file.name.match(/\.ttf$/i)) {
144
+ filesList.push(path.join(dir, file.name))
145
+ }
146
+ }
147
+ })
148
+
149
+ return filesList
150
+ }
151
+
152
+ const copyDir = (src, dest) => {
153
+ // Ensure the destination directory exists
154
+ fs.mkdirSync(dest, { recursive: true })
155
+
156
+ const entries = fs.readdirSync(src, { withFileTypes: true })
157
+
158
+ for (let entry of entries) {
159
+ const srcPath = path.join(src, entry.name)
160
+ const destPath = path.join(dest, entry.name)
161
+
162
+ entry.isDirectory() ? copyDir(srcPath, destPath) : fs.copyFileSync(srcPath, destPath) // Copy files
163
+ }
164
+ }