Skip to content

Instantly share code, notes, and snippets.

@romanlex
Last active February 15, 2023 15:29
Show Gist options
  • Save romanlex/204e5e2d387c83a577f12a42f0bbc938 to your computer and use it in GitHub Desktop.
Save romanlex/204e5e2d387c83a577f12a42f0bbc938 to your computer and use it in GitHub Desktop.
Bundle manifest.json to defined var with ManifestPlugin for update frontend app
const { sources } = require('webpack')
const { getCompilerHooks } = require('webpack-manifest-plugin')
const PLUGIN_NAME = 'BundleManifestPlugin'
/** @typedef {import("webpack").Compiler} Compiler */
/**
* @typedef {Object} Options
* @property {string} to
* @property {number} assetHookStage
*/
class BundleManifestPlugin {
/** @type {Options} */
options
manifest
/**
*
* @param {Options} options
*/
constructor(options) {
this.options = options
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const { beforeEmit } = getCompilerHooks(compiler)
const { options } = this
let moduleForReplace
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => {
for (const module of modules) {
if (
!moduleForReplace &&
module._source &&
module._source._valueAsString &&
new RegExp(options.to, 'g').test(module._source._valueAsString)
) {
moduleForReplace = module
}
}
})
compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage: options.assetHookStage ?? Infinity }, () => {
if (!this.manifest) return
for (const chunk of compilation.chunks) {
if (!chunk.containsModule(moduleForReplace)) continue
for (const file of chunk.files) {
compilation.updateAsset(file, (oldSource) => {
const source = oldSource.source()
return new sources.RawSource(source.replace(options.to, JSON.stringify(this.manifest)))
})
}
}
})
beforeEmit.tap(PLUGIN_NAME, (manifest) => {
this.manifest = manifest
return manifest
})
})
}
}
module.exports = BundleManifestPlugin
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
const updater = new Updater()
function moveWithReload() {
const reqUrl = new Url(location.pathname, true)
window.location.href = `${reqUrl.toString()}${location.search || ''}`
}
async function checkVersionApp(location) {
// show preloader
const reload = await updater.mustBeWithReload()
if (reload) {
moveWithReload(location)
return
}
history.push(`${location.pathname}${location.search ?? ''}`, { __verified__: true })
// hide preloader
}
history.block((location) => {
HistoryLogger.debug('Trying change location to:', location)
if (Boolean(location?.state?.__verified__)) {
HistoryLogger.debug('Target location is verified by previouse check. Unblock history change')
return
}
checkVersionApp(location)
return false
})
export { history }
// @flow
import { ASSET_MANIFEST } from 'core/constants/webpack'
import type { AssetsManifest } from 'core/types'
import { Logger } from 'utils/logger'
import { Storage } from 'utils/storage'
import type { UpdaterServiceInterface } from './index.js.flow'
const UpdaterLogger = Logger.get('Updater')
type Config = {|
manifestPath: string,
|}
/**
* Сервис для обновления фронта на клиентах
*/
export class Updater implements UpdaterServiceInterface {
static CHECK_TIME_LIMIT = 5 * 60 * 1000
static MANIFEST_LAST_CHECK_STORAGE_KEY = 'updater::manifestLastCheckAt'
static MANIFEST_DIGEST_STORAGE_KEY = 'updater::manifestDigest'
manifest: AssetsManifest
manifestDigest: number = 0
_config: Config
constructor(config: Config = { manifestPath: '/manifest.json' }) {
this._config = config
this.manifest = Updater.sortUnorderedKeys(ASSET_MANIFEST)
UpdaterLogger.debug('built-in manifest', this.manifest)
this.manifestDigest = this.hashCodeForString(JSON.stringify(this.manifest))
UpdaterLogger.debug('built-in manifest digest', this.manifestDigest)
this.updateStorage(new Date().getTime(), this.manifestDigest)
}
static sortUnorderedKeys<T: { [key: string]: string }>(unordered: T): T {
return Object.keys(unordered)
.sort()
.reduce((obj, key) => {
obj[key] = unordered[key]
return obj
}, (({}: any): T))
}
/**
* Проверяет необходим или нет релоад страницы
*/
mustBeWithReload = async (): Promise<boolean> => {
try {
const _lastCheckAtStorage = parseInt(Storage.getItem(Updater.MANIFEST_LAST_CHECK_STORAGE_KEY) ?? 0, 10)
const manifestDigestStorage = parseInt(Storage.getItem(Updater.MANIFEST_DIGEST_STORAGE_KEY), 10)
const currentDate = new Date()
const lastCheckAt = new Date(_lastCheckAtStorage)
// если дайджест манифестов разный - это 100% протухшая апка
if (!this.equalHashCodes(this.manifestDigest, manifestDigestStorage)) {
return true
}
// если прошло меньше 5 минут - продолжаем переход дальше
if (currentDate - lastCheckAt < Updater.CHECK_TIME_LIMIT) {
return false
}
const newManifest = await this.getAppManifest()
const appIsOutdated = this.equalHashCodes(this.manifestDigest, newManifest.manifestDigest) !== true
if (appIsOutdated) {
UpdaterLogger.debug('App is outdated')
UpdaterLogger.debug('Reload window on history change')
return true
} else {
this.updateStorage(new Date().getTime(), this.manifestDigest)
return false
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
return false
}
updateStorage = (timestamp: number, digest: number) => {
Storage.setItem(Updater.MANIFEST_LAST_CHECK_STORAGE_KEY, timestamp.toString())
Storage.setItem(Updater.MANIFEST_DIGEST_STORAGE_KEY, digest.toString())
}
equalHashCodes = (a: number, b: number) => {
return Math.abs(a) === Math.abs(b)
}
/**
* Генерим хэшкод по строке
* @param {string} строка от которой нужно сосчитать хэшкод
*/
hashCodeForString = (str: string) => {
var hash = 0
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i)
hash = (hash << 5) - hash + code
hash = hash & hash // Convert to 32bit integer
}
return hash
}
getCurrentManifest = async (): Promise<{ manifest: AssetsManifest, manifestDigest: number }> => {
if (this.manifest) return { manifest: this.manifest, manifestDigest: this.manifestDigest }
const manifest = await this.getAppManifest()
return manifest
}
/**
* Загружаем манифест
*/
getAppManifest = async (): Promise<{ manifest: AssetsManifest, manifestDigest: number }> => {
const { manifestPath } = this._config
const manifestResponse = await fetch(manifestPath, { cache: 'no-store' })
const manifestStr = await manifestResponse.text()
let manifest: AssetsManifest
let manifestDigest: number
try {
const parsed = JSON.parse(manifestStr)
manifest = Updater.sortUnorderedKeys(parsed)
manifestDigest = this.hashCodeForString(JSON.stringify(manifest))
} catch (error) {
UpdaterLogger.error('Couldn\t parse text as json')
manifest = {}
manifestDigest = 0
}
UpdaterLogger.debug('Manifest loaded', manifest, manifestDigest)
return { manifest, manifestDigest }
}
}
const { Compilation } = require('webpack')
modules.exports = () => {
return {
// ...
plugin: [
// ...
new WebpackManifestPlugin({
fileName: path.resolve(paths.railsPublic, 'manifest.json'),
filter: ({ name, isInitial, isChunk }) => isInitial || name.includes('.js') || isChunk,
assetHookStage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
writeToFileEmit: true,
}),
new BundleManifestPlugin({
to: 'DEFINE_PLUGIN_ASSET_MANIFEST',
assetHookStage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
})
]
}
}
export const ASSET_MANIFEST = JSON.parse(DEFINE_PLUGIN_ASSET_MANIFEST)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment