| /** |
| * |
| * Copyright (c) 2020 Silicon Labs |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| const { spawn } = require('cross-spawn') |
| const folderHash = require('folder-hash') |
| const fs = require('fs') |
| const fsp = fs.promises |
| const path = require('path') |
| const scriptUtil = require('./script-util.js') |
| const readline = require('readline') |
| const env = require('../src-electron/util/env') |
| |
| const spaDir = path.join(__dirname, '../spa') |
| const backendDir = path.join(__dirname, '../dist') |
| const spaHashFileName = path.join(spaDir, 'hash.json') |
| const backendHashFileName = path.join(backendDir, 'hash.json') |
| process.env.PATH = process.env.PATH + ':./node_modules/.bin/' |
| |
| const hashOptions = {} |
| |
| // Utilities shared by scripts. |
| |
| /** |
| * Execute a command and resolve with the context. |
| * |
| * @param {*} ctx |
| * @param {*} cmd |
| * @param {*} args |
| */ |
| async function executeCmd(ctx, cmd, args) { |
| return new Promise((resolve, reject) => { |
| console.log( |
| env.formatEmojiMessage('🚀', `Executing: ${cmd} ${args.join(' ')}`) |
| ) |
| let c = spawn(cmd, args) |
| c.on('exit', (code) => { |
| if (code == 0) resolve(ctx) |
| else { |
| if (code) { |
| console.log( |
| env.formatEmojiMessage( |
| '👎', |
| `Program ${cmd} exited with error code: ${code}` |
| ) |
| ) |
| reject(code) |
| } else { |
| console.log( |
| env.formatEmojiMessage( |
| '👎', |
| `Program ${cmd} exited with signal code: ${c.signalCode}` |
| ) |
| ) |
| reject(c.signalCode) |
| } |
| } |
| }) |
| c.stdout.on('data', (data) => { |
| process.stdout.write(data) |
| }) |
| c.stderr.on('data', (data) => { |
| process.stderr.write('⇝ ' + data) |
| }) |
| c.on('error', (err) => { |
| reject(err) |
| }) |
| }) |
| } |
| |
| /** |
| * Executes a command with arguments and resolves with the stdout. |
| * |
| * @param {*} onError If there is an error with executable, resolve to this. |
| * @param {*} cmd Command to run. |
| * @param {*} args Arguments to pass. |
| */ |
| async function getStdout(onError, cmd, args) { |
| return new Promise((resolve, reject) => { |
| console.log( |
| env.formatEmojiMessage('🚀', `Executing: ${cmd} ${args.join(' ')}`) |
| ) |
| let c = spawn(cmd, args) |
| let str = '' |
| c.on('exit', (code) => { |
| if (code == 0) resolve(str) |
| else { |
| console.log( |
| env.formatEmojiMessage( |
| '👎', |
| `Program ${cmd} exited with error code: ${code}` |
| ) |
| ) |
| reject(code) |
| } |
| }) |
| c.stdout.on('data', (data) => { |
| str = str.concat(data) |
| }) |
| c.on('error', (err) => { |
| resolve(onError) |
| }) |
| }) |
| } |
| |
| /** |
| * Resolves into a context object. |
| * Check for context.needsRebuild |
| * |
| * @returns Promise of SPA rebuilt |
| */ |
| async function rebuildSpaIfNeeded() { |
| let srcHash = await folderHash.hashElement( |
| path.join(__dirname, '../src'), |
| hashOptions |
| ) |
| console.log(env.formatEmojiMessage('🔍', `Current src hash: ${srcHash.hash}`)) |
| let srcSharedHash = await folderHash.hashElement( |
| path.join(__dirname, '../src-shared'), |
| hashOptions |
| ) |
| console.log( |
| env.formatEmojiMessage( |
| '🔍', |
| `Current src-shared hash: ${srcSharedHash.hash}` |
| ) |
| ) |
| let ctx = { |
| hash: { |
| srcHash: srcHash.hash, |
| srcSharedHash: srcSharedHash.hash |
| } |
| } |
| return Promise.resolve(ctx) |
| .then( |
| (ctx) => |
| new Promise((resolve, reject) => { |
| fs.readFile(spaHashFileName, (err, data) => { |
| let oldHash = null |
| if (err) { |
| console.log( |
| env.formatEmojiMessage( |
| '👎', |
| `Error reading old hash file: ${spaHashFileName}` |
| ) |
| ) |
| ctx.needsRebuild = true |
| } else { |
| oldHash = JSON.parse(data) |
| console.log( |
| env.formatEmojiMessage( |
| '🔍', |
| `Previous src hash: ${oldHash.srcHash}` |
| ) |
| ) |
| console.log( |
| env.formatEmojiMessage( |
| '🔍', |
| `Previous src-shared hash: ${oldHash.srcSharedHash}` |
| ) |
| ) |
| ctx.needsRebuild = |
| oldHash.srcSharedHash != ctx.hash.srcSharedHash || |
| oldHash.srcHash != ctx.hash.srcHash |
| } |
| if (ctx.needsRebuild) { |
| console.log( |
| `🐝 Front-end code changed, so we need to rebuild SPA.` |
| ) |
| } else { |
| console.log( |
| env.formatEmojiMessage( |
| '👍', |
| "There were no changes to front-end code, so we don't have to rebuild the SPA." |
| ) |
| ) |
| } |
| resolve(ctx) |
| }) |
| }) |
| ) |
| .then((ctx) => { |
| if (ctx.needsRebuild) |
| return scriptUtil.executeCmd(ctx, 'npx', ['quasar', 'build']) |
| else return Promise.resolve(ctx) |
| }) |
| .then( |
| (ctx) => |
| new Promise((resolve, reject) => { |
| if (ctx.needsRebuild) { |
| console.log( |
| env.formatEmojiMessage('✍', 'Writing out new hash file.') |
| ) |
| fs.writeFile(spaHashFileName, JSON.stringify(ctx.hash), (err) => { |
| if (err) reject(err) |
| else resolve(ctx) |
| }) |
| } else { |
| resolve(ctx) |
| } |
| }) |
| ) |
| } |
| |
| /** |
| * Rebuilds backend if needed. |
| * @returns promise of backend rebuilt |
| */ |
| async function rebuildBackendIfNeeded() { |
| return scriptUtil |
| .executeCmd({}, 'npx', ['tsc', '--build', './tsconfig.json']) |
| .then(() => |
| scriptUtil.executeCmd({}, 'npx', [ |
| 'copyfiles', |
| './src-electron/**/*.sql', |
| './src-electron/icons/*', |
| './dist/' |
| ]) |
| ) |
| } |
| |
| /** |
| * Executes: |
| * git log -1 --format="{\"hash\": \"%H\",\"date\": \"%cI\"}" |
| * ads the timestamp and saves it into .version.json |
| */ |
| async function stampVersion() { |
| try { |
| let out = await getStdout('{"hash": null,"date": null}', 'git', [ |
| 'log', |
| '-1', |
| '--format={"hash": "%H","timestamp": %ct}' |
| ]) |
| let version = JSON.parse(out) |
| let d = new Date(version.timestamp * 1000) // git gives seconds, Date needs milliseconds |
| let result = await setPackageJsonVersion(d, 'real') |
| version.date = d |
| version.zapVersion = result.version |
| let versionFile = path.join(__dirname, '../.version.json') |
| console.log( |
| env.formatEmojiMessage( |
| '🔍', |
| `Git commit: ${version.hash} from ${version.date}` |
| ) |
| ) |
| await fsp.writeFile(versionFile, JSON.stringify(version)) |
| } catch (err) { |
| console.log(`Error retrieving version: ${err}`) |
| } |
| } |
| |
| /** |
| * Sets the version in package.json |
| * @param {*} mode 'fake', 'real' or 'print' |
| */ |
| async function setPackageJsonVersion(date, mode) { |
| if (process.env.ZAP_SKIP_REAL_VERSION != null) { |
| // If you set ZAP_SKIP_REAL_VERSION environment variable, then this whole |
| // version muddling is turned off. |
| return true |
| } |
| |
| let promise = new Promise((resolve, reject) => { |
| let packageJson = path.join(__dirname, '../package.json') |
| let output = '' |
| let cnt = 0 |
| let result = { |
| wasChanged: false, |
| version: null |
| } |
| |
| const stream = fs.createReadStream(packageJson) |
| const rl = readline.createInterface({ |
| input: stream, |
| crlfDelay: Infinity |
| }) |
| |
| rl.on('line', (line) => { |
| if (cnt < 10 && line.includes('"version":')) { |
| let output |
| if (mode == 'real') { |
| result.version = `${date.getFullYear()}.${ |
| date.getMonth() + 1 |
| }.${date.getDate()}` |
| output = ` "version": "${result.version}",` |
| } else if (mode == 'fake') { |
| result.version = '0.0.0' |
| output = ` "version": "${result.version}",` |
| } else { |
| result.version = line |
| output = line |
| } |
| |
| if (output == line) { |
| result.wasChanged = false |
| } else { |
| line = output |
| result.wasChanged = true |
| } |
| versionPrinted = line |
| } |
| output = output.concat(line + '\n') |
| cnt++ |
| }) |
| |
| rl.on('close', () => { |
| if (result.wasChanged) { |
| fs.writeFileSync(packageJson, output) |
| } |
| resolve(result) |
| }) |
| }) |
| return promise |
| } |
| |
| /** |
| * This method takes a nanosecond duration and prints out |
| * decently human readable time out of it. |
| * |
| * @param {*} nsDifference |
| * @returns duration in the form of string |
| */ |
| function duration(nsDifference) { |
| let diff = Number(nsDifference) |
| let out = '' |
| if (diff > 1000000000) { |
| out += `${Math.floor(diff / 1000000000)}s ` |
| } |
| out += `${Math.round((diff % 1000000000) / 1000000)}ms` |
| return out |
| } |
| |
| /** |
| * Printout of timings at the end of a script. |
| * This function also cleans up the package.json |
| * |
| * @param {*} startTime |
| */ |
| async function doneStamp(startTime) { |
| let nsDuration = process.hrtime.bigint() - startTime |
| console.log( |
| env.formatEmojiMessage('😎', `All done: ${duration(nsDuration)}.`) |
| ) |
| return setPackageJsonVersion(null, 'fake') |
| } |
| |
| /** |
| * Main entry of the program. |
| * |
| * @param {*} isNode |
| * @returns main js file path |
| */ |
| function mainPath(isElectron) { |
| if (isElectron) { |
| return path.join(__dirname, '../dist/src-electron/ui/main-ui.js') |
| } else { |
| return path.join(__dirname, '../dist/src-electron/main-process/main.js') |
| } |
| } |
| |
| /** |
| * Simple function that reads a JSON file representing an array, |
| * and adds an object to it. If file doesn't exist it will create it |
| * with an array containing the passed object. |
| * |
| * @param {*} file |
| * @param {*} object |
| */ |
| async function addToJsonFile(file, object) { |
| let json |
| if (fs.existsSync(file)) { |
| let data = await fsp.readFile(file) |
| json = JSON.parse(data) |
| } else { |
| json = [] |
| } |
| json.push(object) |
| await fsp.writeFile(file, JSON.stringify(json, null, 2)) |
| } |
| |
| /** |
| * Recursive entry |
| * @param {*} directory |
| * @param {*} depth |
| * @param {*} pattern |
| * @param {*} collector |
| * @returns none |
| */ |
| async function doLocate(directory, depth, pattern, collector) { |
| if (depth > 50) { |
| console.log('WARNING: Recursive depth of 50 exceeded. Aborting.') |
| return |
| } |
| |
| let content = await fsp.readdir(directory) |
| |
| for (let f of content.map((x) => path.join(directory, x))) { |
| let stat = await fsp.stat(f) |
| if (stat.isDirectory()) { |
| await doLocate(f, depth + 1, pattern, collector) |
| } else if (stat.isFile()) { |
| if (f.match(pattern)) { |
| collector.push(f) |
| } |
| } |
| } |
| } |
| /** |
| * Function that recursively finds files under a given directory |
| */ |
| async function locateRecursively(rootDir, filePattern) { |
| let files = [] |
| await doLocate(rootDir, 0, filePattern, files) |
| return files |
| } |
| |
| exports.executeCmd = executeCmd |
| exports.rebuildSpaIfNeeded = rebuildSpaIfNeeded |
| exports.rebuildBackendIfNeeded = rebuildBackendIfNeeded |
| exports.stampVersion = stampVersion |
| exports.duration = duration |
| exports.doneStamp = doneStamp |
| exports.mainPath = mainPath |
| exports.setPackageJsonVersion = setPackageJsonVersion |
| exports.addToJsonFile = addToJsonFile |
| exports.locateRecursively = locateRecursively |