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.

Revisions

  1. romanlex revised this gist Feb 15, 2023. 1 changed file with 36 additions and 0 deletions.
    36 changes: 36 additions & 0 deletions history-listener.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,36 @@
    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 }
  2. romanlex revised this gist Feb 15, 2023. 1 changed file with 3 additions and 32 deletions.
    35 changes: 3 additions & 32 deletions BundleManifestPlugin.js
    Original file line number Diff line number Diff line change
    @@ -1,37 +1,8 @@
    const { getCompilerHooks } = require('webpack-manifest-plugin')
    const { sources } = require('webpack')
    const { getCompilerHooks } = require('webpack-manifest-plugin')

    const PLUGIN_NAME = 'BundleManifestPlugin'

    /**
    * Bundle asset manifest to defined var
    * For example you need check what your frontend app is outdated
    * You can make diff manifest.json with defined var at runtime for check this
    *
    * Example use:
    * ```js
    * plugin: [
    * ...
    * new WebpackManifestPlugin({
    * fileName: path.resolve(paths.railsPublic, 'manifest.json'),
    * filter: ({ name, isInitial, isChunk }) => isInitial || name.includes('.js') || isChunk,
    * }),
    * new BundleManifestPlugin({
    * to: 'DEFINE_PLUGIN_ASSET_MANIFEST',
    * })
    * ]
    * ```
    * And now in your code:
    * ```js
    * const response = await fetch('/manifest.json')
    * const currentManifest = await response.json()
    *
    * if(JSON.stringify(currentManifest) !== JSON.stringify(DEFINE_PLUGIN_ASSET_MANIFEST)) {
    * // your app is outdated
    * }
    * ```
    */

    /** @typedef {import("webpack").Compiler} Compiler */

    /**
    @@ -93,12 +64,12 @@ class BundleManifestPlugin {
    })

    beforeEmit.tap(PLUGIN_NAME, (manifest) => {
    this.manifest = JSON.stringify(manifest)
    this.manifest = manifest

    return manifest
    })
    })
    }
    }

    exports.BundleManifestPlugin = BundleManifestPlugin
    module.exports = BundleManifestPlugin
  3. romanlex revised this gist Feb 15, 2023. 2 changed files with 143 additions and 7 deletions.
    143 changes: 143 additions & 0 deletions updater.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,143 @@
    // @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 }
    }
    }
    7 changes: 0 additions & 7 deletions your-code.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +0,0 @@
    import { ASSET_MANIFEST } from 'constants/webpack'

    const response = await fetch('/manifest.json')
    const currentManifest = await response.json()
    if(JSON.stringify(ASSET_MANIFEST) !== JSON.stringify(currentManifest)) {
    // your app is outdated
    }
  4. romanlex revised this gist Jan 30, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion webpack.js
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    export const ASSET_MANIFEST = DEFINE_PLUGIN_ASSET_MANIFEST
    export const ASSET_MANIFEST = JSON.parse(DEFINE_PLUGIN_ASSET_MANIFEST)
  5. romanlex revised this gist Jan 30, 2023. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions webpack.config.js
    Original file line number Diff line number Diff line change
    @@ -9,6 +9,7 @@ modules.exports = () => {
    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',
  6. romanlex revised this gist Jan 30, 2023. 2 changed files with 1 addition and 10 deletions.
    9 changes: 0 additions & 9 deletions webpack.config.js
    Original file line number Diff line number Diff line change
    @@ -3,15 +3,6 @@ const { Compilation } = require('webpack')
    modules.exports = () => {
    return {
    // ...
    optimization: {
    // ...
    minimize: true,
    minimizer: [
    new TerserPlugin({
    exclude: /constants\/webpack/,
    }),
    ],
    },
    plugin: [
    // ...
    new WebpackManifestPlugin({
    2 changes: 1 addition & 1 deletion your-code.js
    Original file line number Diff line number Diff line change
    @@ -2,6 +2,6 @@ import { ASSET_MANIFEST } from 'constants/webpack'

    const response = await fetch('/manifest.json')
    const currentManifest = await response.json()
    if(JSON.stringify(currentManifest) !== JSON.stringify(ASSET_MANIFEST)) {
    if(JSON.stringify(ASSET_MANIFEST) !== JSON.stringify(currentManifest)) {
    // your app is outdated
    }
  7. romanlex revised this gist Jan 30, 2023. 4 changed files with 34 additions and 17 deletions.
    6 changes: 4 additions & 2 deletions BundleManifestPlugin.js
    Original file line number Diff line number Diff line change
    @@ -37,6 +37,7 @@ const PLUGIN_NAME = 'BundleManifestPlugin'
    /**
    * @typedef {Object} Options
    * @property {string} to
    * @property {number} assetHookStage
    */

    class BundleManifestPlugin {
    @@ -67,16 +68,17 @@ class BundleManifestPlugin {
    compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => {
    for (const module of modules) {
    if (
    !moduleForReplace &&
    module._source &&
    module._source._valueAsString &&
    new RegExp(options.to, 'gi').test(module._source._valueAsString)
    new RegExp(options.to, 'g').test(module._source._valueAsString)
    ) {
    moduleForReplace = module
    }
    }
    })

    compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage: Infinity }, () => {
    compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage: options.assetHookStage ?? Infinity }, () => {
    if (!this.manifest) return

    for (const chunk of compilation.chunks) {
    40 changes: 26 additions & 14 deletions webpack.config.js
    Original file line number Diff line number Diff line change
    @@ -1,16 +1,28 @@
    modules.exports = () => {
    const { Compilation } = require('webpack')

    return {
    // ...
    plugin: [
    // ...
    new WebpackManifestPlugin({
    fileName: path.resolve(paths.railsPublic, 'manifest.json'),
    filter: ({ name, isInitial, isChunk }) => isInitial || name.includes('.js') || isChunk,
    }),
    new BundleManifestPlugin({
    to: 'DEFINE_PLUGIN_ASSET_MANIFEST',
    })
    ]
    }
    modules.exports = () => {
    return {
    // ...
    optimization: {
    // ...
    minimize: true,
    minimizer: [
    new TerserPlugin({
    exclude: /constants\/webpack/,
    }),
    ],
    },
    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,
    }),
    new BundleManifestPlugin({
    to: 'DEFINE_PLUGIN_ASSET_MANIFEST',
    assetHookStage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
    })
    ]
    }
    }
    1 change: 1 addition & 0 deletions webpack.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    export const ASSET_MANIFEST = DEFINE_PLUGIN_ASSET_MANIFEST
    4 changes: 3 additions & 1 deletion your-code.js
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,7 @@
    import { ASSET_MANIFEST } from 'constants/webpack'

    const response = await fetch('/manifest.json')
    const currentManifest = await response.json()
    if(JSON.stringify(currentManifest) !== JSON.stringify(DEFINE_PLUGIN_ASSET_MANIFEST)) {
    if(JSON.stringify(currentManifest) !== JSON.stringify(ASSET_MANIFEST)) {
    // your app is outdated
    }
  8. romanlex revised this gist Jan 30, 2023. 1 changed file with 15 additions and 11 deletions.
    26 changes: 15 additions & 11 deletions BundleManifestPlugin.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,5 @@
    const { getCompilerHooks } = require('webpack-manifest-plugin')
    const { sources } = require('webpack')

    const PLUGIN_NAME = 'BundleManifestPlugin'

    @@ -75,20 +76,23 @@ class BundleManifestPlugin {
    }
    })

    beforeEmit.tap(PLUGIN_NAME, (manifest) => {
    this.manifest = JSON.stringify(manifest)
    compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage: Infinity }, () => {
    if (!this.manifest) return

    if (moduleForReplace && moduleForReplace._source._valueAsString) {
    const source = moduleForReplace._source
    if (new RegExp(options.to, 'gi').test(source._valueAsString)) {
    source._valueAsString = source._valueAsString.replace(
    new RegExp(options.to, 'gi'),
    JSON.stringify(manifest)
    )
    moduleForReplace._source = source
    compilation.rebuildModule(moduleForReplace)
    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 = JSON.stringify(manifest)

    return manifest
    })
    })
  9. romanlex revised this gist Jan 27, 2023. 1 changed file with 30 additions and 19 deletions.
    49 changes: 30 additions & 19 deletions BundleManifestPlugin.js
    Original file line number Diff line number Diff line change
    @@ -58,29 +58,40 @@ class BundleManifestPlugin {
    */
    apply(compiler) {
    const { beforeEmit } = getCompilerHooks(compiler)
    const { options } = this

    beforeEmit.tap(PLUGIN_NAME, (manifest) => {
    this.manifest = JSON.stringify(manifest)
    return manifest
    })
    let moduleForReplace

    compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
    compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => this.onOptimizeModules(modules))
    })
    }
    compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => {
    for (const module of modules) {
    if (
    module._source &&
    module._source._valueAsString &&
    new RegExp(options.to, 'gi').test(module._source._valueAsString)
    ) {
    moduleForReplace = module
    }
    }
    })

    onOptimizeModules(modules) {
    const { options } = this
    for (const module of modules) {
    if (
    options.to &&
    module._source &&
    module._source._value &&
    new RegExp(options.to, 'gi').test(module._source._value)
    ) {
    module._source._value = module._source._value.replace(new RegExp(options.to, 'gi'), this.manifest ?? '{}')
    }
    }
    beforeEmit.tap(PLUGIN_NAME, (manifest) => {
    this.manifest = JSON.stringify(manifest)

    if (moduleForReplace && moduleForReplace._source._valueAsString) {
    const source = moduleForReplace._source
    if (new RegExp(options.to, 'gi').test(source._valueAsString)) {
    source._valueAsString = source._valueAsString.replace(
    new RegExp(options.to, 'gi'),
    JSON.stringify(manifest)
    )
    moduleForReplace._source = source
    compilation.rebuildModule(moduleForReplace)
    }
    }
    return manifest
    })
    })
    }
    }

  10. romanlex renamed this gist Jan 27, 2023. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  11. romanlex revised this gist Jan 27, 2023. 2 changed files with 21 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions blabla.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    const response = await fetch('/manifest.json')
    const currentManifest = await response.json()
    if(JSON.stringify(currentManifest) !== JSON.stringify(DEFINE_PLUGIN_ASSET_MANIFEST)) {
    // your app is outdated
    }
    16 changes: 16 additions & 0 deletions webpack.config.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    modules.exports = () => {

    return {
    // ...
    plugin: [
    // ...
    new WebpackManifestPlugin({
    fileName: path.resolve(paths.railsPublic, 'manifest.json'),
    filter: ({ name, isInitial, isChunk }) => isInitial || name.includes('.js') || isChunk,
    }),
    new BundleManifestPlugin({
    to: 'DEFINE_PLUGIN_ASSET_MANIFEST',
    })
    ]
    }
    }
  12. romanlex created this gist Jan 27, 2023.
    87 changes: 87 additions & 0 deletions BundleManifestPlugin.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,87 @@
    const { getCompilerHooks } = require('webpack-manifest-plugin')

    const PLUGIN_NAME = 'BundleManifestPlugin'

    /**
    * Bundle asset manifest to defined var
    * For example you need check what your frontend app is outdated
    * You can make diff manifest.json with defined var at runtime for check this
    *
    * Example use:
    * ```js
    * plugin: [
    * ...
    * new WebpackManifestPlugin({
    * fileName: path.resolve(paths.railsPublic, 'manifest.json'),
    * filter: ({ name, isInitial, isChunk }) => isInitial || name.includes('.js') || isChunk,
    * }),
    * new BundleManifestPlugin({
    * to: 'DEFINE_PLUGIN_ASSET_MANIFEST',
    * })
    * ]
    * ```
    * And now in your code:
    * ```js
    * const response = await fetch('/manifest.json')
    * const currentManifest = await response.json()
    *
    * if(JSON.stringify(currentManifest) !== JSON.stringify(DEFINE_PLUGIN_ASSET_MANIFEST)) {
    * // your app is outdated
    * }
    * ```
    */

    /** @typedef {import("webpack").Compiler} Compiler */

    /**
    * @typedef {Object} Options
    * @property {string} to
    */

    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)

    beforeEmit.tap(PLUGIN_NAME, (manifest) => {
    this.manifest = JSON.stringify(manifest)
    return manifest
    })

    compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
    compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => this.onOptimizeModules(modules))
    })
    }

    onOptimizeModules(modules) {
    const { options } = this
    for (const module of modules) {
    if (
    options.to &&
    module._source &&
    module._source._value &&
    new RegExp(options.to, 'gi').test(module._source._value)
    ) {
    module._source._value = module._source._value.replace(new RegExp(options.to, 'gi'), this.manifest ?? '{}')
    }
    }
    }
    }

    exports.BundleManifestPlugin = BundleManifestPlugin