Last active
February 15, 2023 15:29
-
-
Save romanlex/204e5e2d387c83a577f12a42f0bbc938 to your computer and use it in GitHub Desktop.
Bundle manifest.json to defined var with ManifestPlugin for update frontend app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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 } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}) | |
] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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