mirror of https://github.com/FreeTubeApp/FreeTube
Implement hot reloading for the locale files during development (#5050)
* Stop using deprecated additionalAssets hook * Watch locale files without webpack-watch-external-files-plugin * Use Maps instead of objects * Use webpack's file timestamps instead of checking the files ourselves * Add hot reloading * Inject hot reload code snippet
This commit is contained in:
parent
2fcd5c985e
commit
61820b1bcf
|
@ -1,14 +1,18 @@
|
|||
const { existsSync, readFileSync, statSync } = require('fs')
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { readFile } = require('fs/promises')
|
||||
const { join } = require('path')
|
||||
const { brotliCompress, constants } = require('zlib')
|
||||
const { promisify } = require('util')
|
||||
const { load: loadYaml } = require('js-yaml')
|
||||
|
||||
const brotliCompressAsync = promisify(brotliCompress)
|
||||
|
||||
const PLUGIN_NAME = 'ProcessLocalesPlugin'
|
||||
|
||||
class ProcessLocalesPlugin {
|
||||
constructor(options = {}) {
|
||||
this.compress = !!options.compress
|
||||
this.isIncrementalBuild = false
|
||||
this.hotReload = !!options.hotReload
|
||||
|
||||
if (typeof options.inputDir !== 'string') {
|
||||
throw new Error('ProcessLocalesPlugin: no input directory `inputDir` specified.')
|
||||
|
@ -22,49 +26,68 @@ class ProcessLocalesPlugin {
|
|||
}
|
||||
this.outputDir = options.outputDir
|
||||
|
||||
this.locales = {}
|
||||
/** @type {Map<str, any>} */
|
||||
this.locales = new Map()
|
||||
this.localeNames = []
|
||||
this.activeLocales = []
|
||||
|
||||
this.cache = {}
|
||||
/** @type {Map<str, any>} */
|
||||
this.cache = new Map()
|
||||
|
||||
this.filePaths = []
|
||||
this.previousTimestamps = new Map()
|
||||
this.startTime = Date.now()
|
||||
|
||||
/** @type {(updatedLocales: [string, string][]) => void|null} */
|
||||
this.notifyLocaleChange = null
|
||||
|
||||
if (this.hotReload) {
|
||||
this.hotReloadScript = readFileSync(`${__dirname}/_hotReloadLocalesScript.js`, 'utf-8')
|
||||
}
|
||||
|
||||
this.loadLocales()
|
||||
}
|
||||
|
||||
/** @param {import('webpack').Compiler} compiler */
|
||||
apply(compiler) {
|
||||
compiler.hooks.thisCompilation.tap('ProcessLocalesPlugin', (compilation) => {
|
||||
const { CachedSource, RawSource } = compiler.webpack.sources;
|
||||
const { Compilation } = compiler.webpack
|
||||
|
||||
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
||||
const IS_DEV_SERVER = !!compiler.watching
|
||||
const { CachedSource, RawSource } = compiler.webpack.sources;
|
||||
|
||||
compilation.hooks.additionalAssets.tapPromise('process-locales-plugin', async (_assets) => {
|
||||
compilation.hooks.processAssets.tapPromise({
|
||||
name: PLUGIN_NAME,
|
||||
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
|
||||
}, async (_assets) => {
|
||||
|
||||
// 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
|
||||
const promises = []
|
||||
// Prevents `loadLocales` called twice on first time (e.g. release build)
|
||||
if (this.isIncrementalBuild) {
|
||||
this.loadLocales(true)
|
||||
} else {
|
||||
this.isIncrementalBuild = true
|
||||
|
||||
/** @type {[string, string][]} */
|
||||
const updatedLocales = []
|
||||
if (this.hotReload && !this.notifyLocaleChange) {
|
||||
console.warn('ProcessLocalesPlugin: Unable to live reload locales as `notifyLocaleChange` is not set.')
|
||||
}
|
||||
|
||||
Object.values(this.locales).forEach((localeEntry) => {
|
||||
const { locale, data, mtimeMs } = localeEntry
|
||||
|
||||
for (let [locale, data] of this.locales) {
|
||||
promises.push(new Promise(async (resolve) => {
|
||||
if (IS_DEV_SERVER) {
|
||||
const cacheEntry = this.cache[locale]
|
||||
if (IS_DEV_SERVER && compiler.fileTimestamps) {
|
||||
const filePath = join(this.inputDir, `${locale}.yaml`)
|
||||
|
||||
if (cacheEntry != null) {
|
||||
const { filename, source, mtimeMs: cachedMtimeMs } = cacheEntry
|
||||
const timestamp = compiler.fileTimestamps.get(filePath)?.safeTime
|
||||
|
||||
if (cachedMtimeMs === mtimeMs) {
|
||||
compilation.emitAsset(filename, source, { minimized: true })
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
if (timestamp && timestamp > (this.previousTimestamps.get(locale) ?? this.startTime)) {
|
||||
this.previousTimestamps.set(locale, timestamp)
|
||||
|
||||
const contents = await readFile(filePath, 'utf-8')
|
||||
data = loadYaml(contents)
|
||||
} else {
|
||||
const { filename, source } = this.cache.get(locale)
|
||||
compilation.emitAsset(filename, source, { minimized: true })
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,6 +96,10 @@ class ProcessLocalesPlugin {
|
|||
let filename = `${this.outputDir}/${locale}.json`
|
||||
let output = JSON.stringify(data)
|
||||
|
||||
if (this.hotReload && compiler.fileTimestamps) {
|
||||
updatedLocales.push([locale, output])
|
||||
}
|
||||
|
||||
if (this.compress) {
|
||||
filename += '.br'
|
||||
output = await this.compressLocale(output)
|
||||
|
@ -82,51 +109,61 @@ class ProcessLocalesPlugin {
|
|||
|
||||
if (IS_DEV_SERVER) {
|
||||
source = new CachedSource(source)
|
||||
this.cache[locale] = { filename, source, mtimeMs }
|
||||
this.cache.set(locale, { filename, source })
|
||||
|
||||
// we don't need the unmodified sources anymore, as we use the cache `this.cache`
|
||||
// so we can clear this to free some memory
|
||||
this.locales.set(locale, null)
|
||||
}
|
||||
|
||||
compilation.emitAsset(filename, source, { minimized: true })
|
||||
|
||||
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 localeEntry.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
if (this.hotReload && this.notifyLocaleChange && updatedLocales.length > 0) {
|
||||
this.notifyLocaleChange(updatedLocales)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation) => {
|
||||
if (!!compiler.watching) {
|
||||
// watch locale files for changes
|
||||
compilation.fileDependencies.addAll(this.filePaths)
|
||||
}
|
||||
})
|
||||
|
||||
compiler.hooks.emit.tap(PLUGIN_NAME, (compilation) => {
|
||||
if (this.hotReload) {
|
||||
// Find generated JavaScript output file (e.g. renderer.js or web.js)
|
||||
// and inject the code snippet that listens for locale updates and replaces vue-i18n's locales
|
||||
|
||||
/** @type {string} */
|
||||
const filename = [...[...compilation.chunks][0].files]
|
||||
.find(file => file.endsWith('.js'))
|
||||
|
||||
compilation.assets[filename]._source._children.push(`\n${this.hotReloadScript}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadLocales(loadModifiedFilesOnly = false) {
|
||||
if (this.activeLocales.length === 0) {
|
||||
this.activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`))
|
||||
}
|
||||
loadLocales() {
|
||||
const activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`))
|
||||
|
||||
for (const locale of activeLocales) {
|
||||
const filePath = join(this.inputDir, `${locale}.yaml`)
|
||||
|
||||
this.filePaths.push(filePath)
|
||||
|
||||
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.locales.set(locale, data)
|
||||
|
||||
const localeName = data['Locale Name'] ?? locale
|
||||
if (!loadModifiedFilesOnly) {
|
||||
this.localeNames.push(localeName)
|
||||
}
|
||||
this.localeNames.push(data['Locale Name'] ?? locale)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
const websocket = new WebSocket('ws://localhost:9080/ws')
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
|
||||
if (message.type === 'freetube-locale-update') {
|
||||
const i18n = document.getElementById('app').__vue__.$i18n
|
||||
|
||||
for (const [locale, data] of message.data) {
|
||||
// Only update locale data if it was already loaded
|
||||
if (i18n.availableLocales.includes(locale)) {
|
||||
const localeData = JSON.parse(data)
|
||||
|
||||
i18n.setLocaleMessage(locale, localeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ const kill = require('tree-kill')
|
|||
const path = require('path')
|
||||
const { spawn } = require('child_process')
|
||||
|
||||
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
||||
|
||||
let electronProcess = null
|
||||
let manualRestart = null
|
||||
|
||||
|
@ -76,6 +78,22 @@ async function restartElectron() {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('webpack').Compiler} compiler
|
||||
* @param {WebpackDevServer} devServer
|
||||
*/
|
||||
function setupNotifyLocaleUpdate(compiler, devServer) {
|
||||
const notifyLocaleChange = (updatedLocales) => {
|
||||
devServer.sendMessage(devServer.webSocketServer.clients, "freetube-locale-update", updatedLocales)
|
||||
}
|
||||
|
||||
compiler.options.plugins
|
||||
.filter(plugin => plugin instanceof ProcessLocalesPlugin)
|
||||
.forEach((/** @type {ProcessLocalesPlugin} */plugin) => {
|
||||
plugin.notifyLocaleChange = notifyLocaleChange
|
||||
})
|
||||
}
|
||||
|
||||
function startMain() {
|
||||
const compiler = webpack(mainConfig)
|
||||
const { name } = compiler
|
||||
|
@ -116,6 +134,7 @@ function startRenderer(callback) {
|
|||
ignored: [
|
||||
/(dashFiles|storyboards)\/*/,
|
||||
'/**/.DS_Store',
|
||||
'**/static/locales/*'
|
||||
]
|
||||
},
|
||||
publicPath: '/static'
|
||||
|
@ -126,6 +145,8 @@ function startRenderer(callback) {
|
|||
server.startCallback(err => {
|
||||
if (err) console.error(err)
|
||||
|
||||
setupNotifyLocaleUpdate(compiler, server)
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
@ -142,11 +163,12 @@ function startWeb () {
|
|||
const server = new WebpackDevServer({
|
||||
open: true,
|
||||
static: {
|
||||
directory: path.join(process.cwd(), 'dist/web/static'),
|
||||
directory: path.resolve(__dirname, '..', 'static'),
|
||||
watch: {
|
||||
ignored: [
|
||||
/(dashFiles|storyboards)\/*/,
|
||||
'/**/.DS_Store',
|
||||
'**/static/locales/*'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -155,6 +177,8 @@ function startWeb () {
|
|||
|
||||
server.startCallback(err => {
|
||||
if (err) console.error(err)
|
||||
|
||||
setupNotifyLocaleUpdate(compiler, server)
|
||||
})
|
||||
}
|
||||
if (!web) {
|
||||
|
|
|
@ -6,7 +6,6 @@ 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 CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
@ -15,6 +14,7 @@ const { version: swiperVersion } = JSON.parse(readFileSync(path.join(__dirname,
|
|||
|
||||
const processLocalesPlugin = new ProcessLocalesPlugin({
|
||||
compress: !isDevMode,
|
||||
hotReload: isDevMode,
|
||||
inputDir: path.join(__dirname, '../static/locales'),
|
||||
outputDir: 'static/locales',
|
||||
})
|
||||
|
@ -165,16 +165,4 @@ 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
|
||||
|
|
|
@ -178,6 +178,7 @@ const config = {
|
|||
|
||||
const processLocalesPlugin = new ProcessLocalesPlugin({
|
||||
compress: false,
|
||||
hotReload: isDevMode,
|
||||
inputDir: path.join(__dirname, '../static/locales'),
|
||||
outputDir: 'static/locales',
|
||||
})
|
||||
|
|
|
@ -129,7 +129,6 @@
|
|||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4",
|
||||
"webpack-watch-external-files-plugin": "^3.0.0",
|
||||
"yaml-eslint-parser": "^1.2.2"
|
||||
}
|
||||
}
|
||||
|
|
27
yarn.lock
27
yarn.lock
|
@ -4662,7 +4662,7 @@ 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@10.3.10, glob@^10.3.7:
|
||||
glob@^10.3.7:
|
||||
version "10.3.10"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
|
||||
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
|
||||
|
@ -6651,14 +6651,6 @@ path-type@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8"
|
||||
integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==
|
||||
|
||||
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"
|
||||
|
@ -7046,7 +7038,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.1, process@^0.11.10:
|
||||
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==
|
||||
|
@ -8604,13 +8596,6 @@ 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"
|
||||
|
@ -8923,14 +8908,6 @@ 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@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-watch-external-files-plugin/-/webpack-watch-external-files-plugin-3.0.0.tgz#65d75389b1c9b05e84a2cfad83897ac12f146c7e"
|
||||
integrity sha512-u0geLnZ/uJXh92B+40apyovnexoP+m9I6QktyGlG8rP6CXXYKe3yhG7zY9P2Wbg75sPTP1PYv2rropRlZdxg9A==
|
||||
dependencies:
|
||||
glob "10.3.10"
|
||||
path "0.12.7"
|
||||
|
||||
webpack@^5.91.0:
|
||||
version "5.91.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9"
|
||||
|
|
Loading…
Reference in New Issue