From 342444f433caa2c65ff4acc3df9d4e39f62b4654 Mon Sep 17 00:00:00 2001 From: PikachuEXE Date: Thu, 26 Oct 2023 05:36:35 +0800 Subject: [PATCH] Ensure updated locales are auto-reloaded in electron renderer dev mode (#4066) * * Ensure updated locales are auto-reloaded in electron renderer dev mode * * Add watch external file plugin in dev mode only * * Make changes from code review --- _scripts/ProcessLocalesPlugin.js | 110 ++++++++++++++++++---------- _scripts/webpack.renderer.config.js | 16 +++- package.json | 1 + yarn.lock | 36 ++++++++- 4 files changed, 123 insertions(+), 40 deletions(-) diff --git a/_scripts/ProcessLocalesPlugin.js b/_scripts/ProcessLocalesPlugin.js index 0d2dd6818..4b525fdc8 100644 --- a/_scripts/ProcessLocalesPlugin.js +++ b/_scripts/ProcessLocalesPlugin.js @@ -1,4 +1,4 @@ -const { existsSync, readFileSync } = require('fs') +const { existsSync, readFileSync, statSync } = require('fs') const { brotliCompress, constants } = require('zlib') const { promisify } = require('util') const { load: loadYaml } = require('js-yaml') @@ -8,6 +8,7 @@ const brotliCompressAsync = promisify(brotliCompress) class ProcessLocalesPlugin { constructor(options = {}) { this.compress = !!options.compress + this.isIncrementalBuild = false if (typeof options.inputDir !== 'string') { throw new Error('ProcessLocalesPlugin: no input directory `inputDir` specified.') @@ -21,10 +22,11 @@ class ProcessLocalesPlugin { } this.outputDir = options.outputDir - this.locales = [] + this.locales = {} this.localeNames = [] + this.activeLocales = [] - this.cache = [] + this.cache = {} this.loadLocales() } @@ -37,66 +39,98 @@ class ProcessLocalesPlugin { compilation.hooks.additionalAssets.tapPromise('process-locales-plugin', async (_assets) => { - // While running in the webpack dev server, this hook gets called for every incrememental build. + // While running in the webpack dev server, this hook gets called for every incremental build. // For incremental builds we can return the already processed versions, which saves time // and makes webpack treat them as cached - if (IS_DEV_SERVER && this.cache.length > 0) { - for (const { filename, source } of this.cache) { - compilation.emitAsset(filename, source, { minimized: true }) - } + const promises = [] + // Prevents `loadLocales` called twice on first time (e.g. release build) + if (this.isIncrementalBuild) { + this.loadLocales(true) } else { - const promises = [] + this.isIncrementalBuild = true + } - for (const { locale, data } of this.locales) { - promises.push(new Promise(async (resolve) => { - if (Object.prototype.hasOwnProperty.call(data, 'Locale Name')) { - delete data['Locale Name'] + Object.values(this.locales).forEach((localeEntry) => { + const { locale, data, mtimeMs } = localeEntry + + promises.push(new Promise(async (resolve) => { + if (IS_DEV_SERVER) { + const cacheEntry = this.cache[locale] + + if (cacheEntry != null) { + const { filename, source, mtimeMs: cachedMtimeMs } = cacheEntry + + if (cachedMtimeMs === mtimeMs) { + compilation.emitAsset(filename, source, { minimized: true }) + resolve() + return + } } + } - this.removeEmptyValues(data) + if (Object.prototype.hasOwnProperty.call(data, 'Locale Name')) { + delete data['Locale Name'] + } - let filename = `${this.outputDir}/${locale}.json` - let output = JSON.stringify(data) + this.removeEmptyValues(data) - if (this.compress) { - filename += '.br' - output = await this.compressLocale(output) - } + let filename = `${this.outputDir}/${locale}.json` + let output = JSON.stringify(data) - let source = new RawSource(output) + if (this.compress) { + filename += '.br' + output = await this.compressLocale(output) + } - if (IS_DEV_SERVER) { - source = new CachedSource(source) - this.cache.push({ filename, source }) - } + let source = new RawSource(output) - compilation.emitAsset(filename, source, { minimized: true }) + if (IS_DEV_SERVER) { + source = new CachedSource(source) + this.cache[locale] = { filename, source, mtimeMs } + } - resolve() - })) - } + compilation.emitAsset(filename, source, { minimized: true }) - await Promise.all(promises) + resolve() + })) if (IS_DEV_SERVER) { // we don't need the unmodified sources anymore, as we use the cache `this.cache` // so we can clear this to free some memory - delete this.locales + delete localeEntry.data } - } + }) + + await Promise.all(promises) }) }) } - loadLocales() { - const activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`)) + loadLocales(loadModifiedFilesOnly = false) { + if (this.activeLocales.length === 0) { + this.activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`)) + } - for (const locale of activeLocales) { - const contents = readFileSync(`${this.inputDir}/${locale}.yaml`, 'utf-8') + for (const locale of this.activeLocales) { + const filePath = `${this.inputDir}/${locale}.yaml` + // Cannot use `mtime` since values never equal + const mtimeMsFromStats = statSync(filePath).mtimeMs + if (loadModifiedFilesOnly) { + // Skip reading files where mtime (modified time) same as last read + // (stored in mtime) + const existingMtime = this.locales[locale]?.mtimeMs + if (existingMtime != null && existingMtime === mtimeMsFromStats) { + continue + } + } + const contents = readFileSync(filePath, 'utf-8') const data = loadYaml(contents) + this.locales[locale] = { locale, data, mtimeMs: mtimeMsFromStats } - this.localeNames.push(data['Locale Name'] ?? locale) - this.locales.push({ locale, data }) + const localeName = data['Locale Name'] ?? locale + if (!loadModifiedFilesOnly) { + this.localeNames.push(localeName) + } } } diff --git a/_scripts/webpack.renderer.config.js b/_scripts/webpack.renderer.config.js index eaf72b50d..fc6495f3a 100644 --- a/_scripts/webpack.renderer.config.js +++ b/_scripts/webpack.renderer.config.js @@ -1,10 +1,12 @@ const path = require('path') +const { readFileSync } = require('fs') const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const VueLoaderPlugin = require('vue-loader/lib/plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const ProcessLocalesPlugin = require('./ProcessLocalesPlugin') +const WatchExternalFilesPlugin = require('webpack-watch-external-files-plugin') const isDevMode = process.env.NODE_ENV === 'development' @@ -128,7 +130,7 @@ const config = { new MiniCssExtractPlugin({ filename: isDevMode ? '[name].css' : '[name].[contenthash].css', chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css', - }) + }), ], resolve: { alias: { @@ -146,4 +148,16 @@ const config = { target: 'electron-renderer', } +if (isDevMode) { + const activeLocales = JSON.parse(readFileSync(path.join(__dirname, '../static/locales/activeLocales.json'))) + + config.plugins.push( + new WatchExternalFilesPlugin({ + files: [ + `./static/locales/{${activeLocales.join(',')}}.yaml`, + ], + }), + ) +} + module.exports = config diff --git a/package.json b/package.json index 66b0a7256..6996f9224 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "webpack": "^5.89.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", + "webpack-watch-external-files-plugin": "^2.0.0", "yaml-eslint-parser": "^1.2.2" } } diff --git a/yarn.lock b/yarn.lock index 8ea1a5143..ba539292d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4491,6 +4491,17 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + glob@^10.3.7: version "10.3.10" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" @@ -6476,6 +6487,14 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path@0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== + dependencies: + process "^0.11.1" + util "^0.10.3" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -6858,7 +6877,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: +process@^0.11.1, process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== @@ -8431,6 +8450,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + util@^0.12.4: version "0.12.4" resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" @@ -8750,6 +8776,14 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== +webpack-watch-external-files-plugin@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-watch-external-files-plugin/-/webpack-watch-external-files-plugin-2.0.0.tgz#4c4e08c59b092c1d705e94d7a380eaa827a76d9c" + integrity sha512-dPUCEcgBjJHWyD4b0HcHD6h3mkPxVKmsfUSks5CvCbrF7HHLvIl/Wq4KSkUMMiNymloZKKkBjYpQxhubAEFM+Q== + dependencies: + glob "8.1.0" + path "0.12.7" + webpack@^5.89.0: version "5.89.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc"