@lightningjs/blits 0.7.1 → 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/.eslintrc.cjs +1 -0
- package/CHANGELOG.md +18 -0
- package/package.json +4 -2
- package/src/element.js +10 -4
- package/src/fontLoader.js +9 -0
- package/src/launch.d.ts +28 -8
- package/src/launch.js +1 -0
- package/src/lib/lifecycle.js +1 -1
- package/src/lib/precompiler/precompiler.js +33 -27
- package/src/lib/reactivity/reactive.js +6 -2
- package/vite/index.js +5 -2
- package/vite/msdfGenerator.js +164 -0
package/.eslintrc.cjs
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
14
|
+
# v0.7.2
|
|
15
|
+
|
|
16
|
+
_23 feb 2024_
|
|
17
|
+
|
|
18
|
+
- Added small fix to support for (re)assigning an array used in a for-loop
|
|
19
|
+
- Added option to enable the Lightning inspector in launch setting (`inspector: true/false`)
|
|
20
|
+
|
|
3
21
|
# v0.7.1
|
|
4
22
|
|
|
5
23
|
_22 feb 2024_
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightningjs/blits",
|
|
3
|
-
"version": "0.7.
|
|
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
|
|
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)
|
|
162
|
-
|
|
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.
|
|
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
|
|
35
|
+
interface SdfFontWithFile {
|
|
36
36
|
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
@@ -172,6 +183,15 @@ export interface Settings {
|
|
|
172
183
|
*
|
|
173
184
|
*/
|
|
174
185
|
canvasColor?: string,
|
|
186
|
+
/**
|
|
187
|
+
* Enable inspector
|
|
188
|
+
*
|
|
189
|
+
* Enables the inspector tool for debugging and inspecting the application, the node tree
|
|
190
|
+
* will be replicated in the DOM and can be inspected using the browser's developer tools
|
|
191
|
+
*
|
|
192
|
+
* Defaults to `false`
|
|
193
|
+
*/
|
|
194
|
+
inspector?: boolean
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
/**
|
package/src/launch.js
CHANGED
|
@@ -52,6 +52,7 @@ export default (App, target, settings) => {
|
|
|
52
52
|
? settings.webWorkersLimit
|
|
53
53
|
: window.navigator.hardwareConcurrency || 2,
|
|
54
54
|
clearColor: (settings.canvasColor && colors.normalize(settings.canvasColor)) || 0x00000000,
|
|
55
|
+
enableInspector: settings.inspector || false,
|
|
55
56
|
},
|
|
56
57
|
target,
|
|
57
58
|
driver
|
package/src/lib/lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -59,7 +59,11 @@ const reactiveProxy = (target) => {
|
|
|
59
59
|
|
|
60
60
|
let result
|
|
61
61
|
if (typeof value === 'object' && Array.isArray(value) && proxyMap.get(target[key])) {
|
|
62
|
-
|
|
62
|
+
if (oldValue !== value) {
|
|
63
|
+
result = oldValue.splice(0, target[key].length, ...value)
|
|
64
|
+
} else {
|
|
65
|
+
result = true
|
|
66
|
+
}
|
|
63
67
|
} else {
|
|
64
68
|
result = Reflect.set(target, key, value, receiver)
|
|
65
69
|
}
|
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
|
+
}
|