diff --git a/.eslintrc.json b/.eslintrc.json index ecd35be6..8bea43fa 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,9 @@ "browser": true, "jquery": true }, + "globals": { + "globalThis": true + }, "rules": { "node/no-unsupported-features/es-syntax": 0, "no-underscore-dangle": 0, diff --git a/package.json b/package.json index 6e4b9627..182989b5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "preanalyze": "npm run build/dev", "analyze": "source-map-explorer ./packages/*/es/index.js", "prebootstrap": "rm -rf ./packages/**/.*.sw*", - "bootstrap": "lerna list | sed 's/@ulms\\/ui-//g' | xargs -I{} scripts/populate-rollup.sh {} && lerna bootstrap", + "bootstrap": "lerna list | sed 's/@ulms\\///g' | sed 's/ui-//g' | xargs -I{} scripts/populate-rollup.sh {} && lerna bootstrap", "prebuild": "npm run prebootstrap", "build": "lerna exec 'NODE_ENV=production npm run build'", "build-storybook": "build-storybook", diff --git a/packages/cache-buster/.npmignore b/packages/cache-buster/.npmignore new file mode 100644 index 00000000..726c3cab --- /dev/null +++ b/packages/cache-buster/.npmignore @@ -0,0 +1 @@ +.*.sw* diff --git a/packages/cache-buster/es/.gitkeep b/packages/cache-buster/es/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/cache-buster/es/.gitkeep @@ -0,0 +1 @@ + diff --git a/packages/cache-buster/index.js b/packages/cache-buster/index.js new file mode 100644 index 00000000..7a1036b4 --- /dev/null +++ b/packages/cache-buster/index.js @@ -0,0 +1 @@ +export { CacheBuster } from './src/cache-buster' diff --git a/packages/cache-buster/package-lock.json b/packages/cache-buster/package-lock.json new file mode 100644 index 00000000..66f32ba3 --- /dev/null +++ b/packages/cache-buster/package-lock.json @@ -0,0 +1,51 @@ +{ + "name": "@ulms/cache-buster", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } +} diff --git a/packages/cache-buster/package.json b/packages/cache-buster/package.json new file mode 100644 index 00000000..6d8a29e7 --- /dev/null +++ b/packages/cache-buster/package.json @@ -0,0 +1,33 @@ +{ + "name": "@ulms/cache-buster", + "version": "1.0.0", + "description": "", + "keywords": [ + "lerna" + ], + "homepage": "https://github.com/netology-group/ulms-media-ui#readme", + "bugs": { + "url": "https://github.com/netology-group/ulms-media-ui/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/netology-group/ulms-media-ui.git" + }, + "license": "MIT", + "author": "", + "main": "index.js", + "module": "es/index.js", + "files": [ + "es", + "src" + ], + "scripts": { + "build": "rm -rf ./es/* && BABEL_ENV=es rollup --config ./rollup.config.js index.js" + }, + "dependencies": { + "react": "^16.12.0" + }, + "peerDependencies": { + "react": "^15.6 || ^16.x || ^17.x" + } +} diff --git a/packages/cache-buster/src/cache-buster.jsx b/packages/cache-buster/src/cache-buster.jsx new file mode 100644 index 00000000..62eedbba --- /dev/null +++ b/packages/cache-buster/src/cache-buster.jsx @@ -0,0 +1,133 @@ +import React from 'react' + +import { FetchVersionResolver, VersionChecker } from './utils' + +const refresh = () => globalThis.location.reload() + +class CacheBuster extends React.Component { + state = { + error: null, + loading: true, + versionMismatch: true, + } + + constructor (props) { + super(props) + + this.intervalId = null + this._versionChecker = null + } + + componentDidMount () { + const { + interval, url, version: localVersion, + } = this.props + + const resolver = new FetchVersionResolver(url) + + this._versionChecker = new VersionChecker(resolver, localVersion) + + if (interval) { + this.intervalId = setInterval(() => { + this._versionChecker.check() + .then(this.checkMajorVersion) + .catch((error) => { + // eslint-disable-next-line no-console + console.log('[CacheBuster] interval check error:', error) + }) + }, interval * 1e3) + } + + this._versionChecker.check() + .then(this.checkVersion) + .catch((error) => { + this.setState({ + error, + loading: false, + versionMismatch: true, + }) + }) + } + + componentWillUnmount () { + clearInterval(this.intervalId) + + this.intervalId = null + + if (this._versionChecker) { + this._versionChecker.destroy() + + this._versionChecker = null + } + } + + checkMajorVersion = (result) => { + const { + error, major, version, + } = result + + if (error) { + // eslint-disable-next-line no-console + console.log('[CacheBuster] interval check error:', error) + } else if (major) { + // eslint-disable-next-line no-console + console.log(`[CacheBuster] app version ${version} - refresh needed (forced)`) + + refresh() + } + } + + checkVersion = (result) => { + const { + error, major, minor, patch, version, + } = result + + if (error) { + // eslint-disable-next-line no-console + console.log('[CacheBuster] initial check error:', error) + + this.setState({ + error, + loading: false, + versionMismatch: true, + }) + + return + } + + const versionMismatch = major || minor || patch + + // eslint-disable-next-line no-console + console.log(`[CacheBuster] app version ${version} - ${versionMismatch ? 'refresh needed' : 'OK'}`) + + this.setState({ + error: null, + loading: false, + versionMismatch, + }) + } + + render () { + const { children } = this.props + const { + error, + loading, + versionMismatch, + } = this.state + + return children({ + error, + loading, + refresh, + versionMismatch, + }) + } +} + +CacheBuster.defaultProps = { + interval: 30, + url: '/meta.json', + version: '', +} + +export { CacheBuster } diff --git a/packages/cache-buster/src/utils.js b/packages/cache-buster/src/utils.js new file mode 100644 index 00000000..eeb915e0 --- /dev/null +++ b/packages/cache-buster/src/utils.js @@ -0,0 +1,66 @@ +/* eslint-disable max-classes-per-file */ +function toSemver (versionStr) { + const [major, minor, patch] = versionStr.split('.') + + return { + major, + minor, + patch, + } +} + +export class FetchVersionResolver { + constructor (url, key = 'version') { + this._url = url + this._key = key + } + + resolve () { + return fetch(this._url, { cache: 'no-cache' }) + .then(response => response.json()) + .then(response => response[this._key]) + } +} + +export class VersionChecker { + constructor (resolver, version) { + this._resolver = resolver + this._version = version + + this._checkPromise = null + } + + static compare (local, remote) { + const localVersion = toSemver(local) + const remoteVersion = toSemver(remote) + + return { + major: localVersion.major !== remoteVersion.major, + minor: localVersion.minor !== remoteVersion.minor, + patch: localVersion.patch !== remoteVersion.patch, + } + } + + check () { + if (!this._checkPromise) { + this._checkPromise = this._resolver.resolve() + .then((version) => { + this._checkPromise = null + + return { version, ...VersionChecker.compare(this._version, version) } + }) + .catch((error) => { + this._checkPromise = null + + return { error } + }) + } + + return this._checkPromise + } + + destroy () { + this._resolver = null + this._checkPromise = null + } +}