├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── TODO.md ├── example ├── react-vite │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── favicon.svg │ │ ├── index.css │ │ ├── logo.svg │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── svelte-vite │ ├── .gitignore │ ├── .vscode │ │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── App.svelte │ │ ├── assets │ │ │ └── svelte.png │ │ ├── lib │ │ │ └── Counter.svelte │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── vue-cli │ ├── .gitignore │ ├── .npmrc │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── main.ts │ │ ├── shim.d.ts │ │ ├── shims-tsx.d.ts │ │ └── shims-vue.d.ts │ ├── tsconfig.json │ └── vue.config.js ├── vue-vite3 │ ├── .Dockerignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .vscode │ │ └── extensions.json │ ├── Dockerfile │ ├── README.md │ ├── env.d.ts │ ├── index.html │ ├── nginx.conf │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ ├── base.css │ │ │ └── logo.svg │ │ ├── components │ │ │ ├── HelloWorld.vue │ │ │ ├── TheWelcome.vue │ │ │ ├── WelcomeItem.vue │ │ │ ├── __tests__ │ │ │ │ └── HelloWorld.spec.ts │ │ │ └── icons │ │ │ │ ├── IconCommunity.vue │ │ │ │ ├── IconDocumentation.vue │ │ │ │ ├── IconEcosystem.vue │ │ │ │ ├── IconSupport.vue │ │ │ │ └── IconTooling.vue │ │ ├── main.ts │ │ ├── router │ │ │ └── index.ts │ │ ├── shim.d.ts │ │ └── views │ │ │ ├── AboutView.vue │ │ │ └── HomeView.vue │ ├── tests │ │ └── e2e │ │ │ └── plugin.spec.ts │ ├── tsconfig.app.json │ ├── tsconfig.config.json │ ├── tsconfig.json │ ├── tsconfig.vitest.json │ └── vite.config.ts └── vue-vite5 │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── env.d.ts │ ├── index.html │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ ├── assets │ │ ├── base.css │ │ ├── logo.svg │ │ └── main.css │ ├── components │ │ ├── HelloWorld.vue │ │ ├── TheWelcome.vue │ │ ├── WelcomeItem.vue │ │ └── icons │ │ │ ├── IconCommunity.vue │ │ │ ├── IconDocumentation.vue │ │ │ ├── IconEcosystem.vue │ │ │ ├── IconSupport.vue │ │ │ └── IconTooling.vue │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── images ├── inject_content.webp ├── react_example.webp ├── react_umi_example.webp ├── svelte_example.webp └── vue_example.webp ├── package.json ├── packages ├── core │ ├── LICENSE │ ├── README.md │ ├── README.zh-CN.md │ ├── package.json │ ├── public │ │ └── webUpdateNoticeInjectStyle.css │ ├── src │ │ ├── buildScript.ts │ │ ├── constant.ts │ │ ├── index.ts │ │ ├── injectScript.ts │ │ ├── locale.ts │ │ ├── pluginBuildScript.ts │ │ ├── shim.d.ts │ │ └── type.ts │ ├── tsup.config.injectFile.ts │ └── tsup.config.ts ├── umi-plugin │ ├── LICENSE │ ├── README.md │ ├── README.zh-CN.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsup.config.ts ├── vite-plugin │ ├── LICENSE │ ├── README.md │ ├── README.zh-CN.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsup.config.ts └── webpack-plugin │ ├── LICENSE │ ├── README.md │ ├── README.zh-CN.md │ ├── package.json │ ├── src │ └── index.ts │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── copyFile.ts ├── tsconfig.json ├── turbo.json └── vitest.config.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20.x 21 | 22 | - name: Setup 23 | run: npm i -g @antfu/ni 24 | 25 | - name: Install 26 | run: nci 27 | 28 | - name: Lint 29 | run: nr lint 30 | 31 | typecheck: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set node 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 16.x 39 | 40 | - name: Setup 41 | run: npm i -g @antfu/ni 42 | 43 | - name: Install 44 | run: nci 45 | 46 | - name: Typecheck 47 | run: nr typecheck 48 | 49 | test: 50 | runs-on: ${{ matrix.os }} 51 | 52 | strategy: 53 | matrix: 54 | node: [14.x, 16.x] 55 | os: [ubuntu-latest, windows-latest, macos-latest] 56 | fail-fast: false 57 | 58 | steps: 59 | - uses: actions/checkout@v3 60 | - name: Set node ${{ matrix.node }} 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: ${{ matrix.node }} 64 | 65 | - name: Setup 66 | run: npm i -g @antfu/ni 67 | 68 | - name: Install 69 | run: nci 70 | 71 | - name: Build 72 | run: nr build 73 | 74 | - name: Test 75 | run: nr test 76 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release and publish-npm 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | # 手动触发 8 | # 允许手动从 Actions tab 运行这个 workflows 9 | workflow_dispatch: 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.x 22 | registry-url: 'https://registry.npmjs.org' 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | 28 | publish-npm: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | with: 33 | fetch-depth: 0 34 | 35 | - uses: actions/setup-node@v3 36 | with: 37 | node-version: 16.x 38 | registry-url: 'https://registry.npmjs.org' 39 | 40 | - uses: pnpm/action-setup@v2.0.1 41 | name: Install pnpm 42 | id: pnpm-install 43 | with: 44 | version: 8.7.6 45 | run_install: false 46 | 47 | - name: Get pnpm store directory 48 | id: pnpm-cache 49 | run: | 50 | echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" 51 | 52 | - uses: actions/cache@v3 53 | name: Setup pnpm cache 54 | with: 55 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 56 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 57 | restore-keys: | 58 | ${{ runner.os }}-pnpm-store- 59 | 60 | - name: Install dependencies 61 | run: pnpm --filter=!./example/** install 62 | 63 | - name: PNPM build 64 | run: pnpm build 65 | 66 | - name: Publish to NPM 67 | run: pnpm --filter=./packages/** publish --access public --no-git-checks 68 | env: 69 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | example/react-umi 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vitest.exclude": [ 3 | "**/node_modules/**", 4 | "**/dist/**", 5 | "**/.{idea,git,cache,output,temp}/**", 6 | "**/example/**" 7 | ], 8 | "typescript.tsdk": "node_modules/typescript/lib" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English](./README.md) | 简体中文 4 | 5 | # plugin-web-update-notification 6 |

7 | 8 | Gzip Size 9 | 10 | 11 | NPM Version 12 | 13 | NPM Downloads 14 | 15 | License 16 | 17 | 18 | discussions-image 19 | 20 |
21 |

22 | 23 | 检测网页更新并通知用户刷新,支持 vite、umijs 和 webpack 插件。 24 | 25 | > 以 git commit hash (也支持 svn revision number、package.json version、build timestamp、custom) 为版本号,打包时将版本号写入 json 文件。客户端轮询服务器上的版本号(浏览器窗口的 visibilitychange、focus 事件辅助),和本地作比较,如果不相同则通知用户刷新页面。 26 | 27 |

28 | 29 | 30 | 31 | 32 |

33 | **什么时候会检测更新(fetch version.json)** ? 34 | 35 | 1. 首次加载页面。 36 | 2. 轮询 (default: 10 * 60 * 1000 ms)。 37 | 3. script 脚本资源加载失败 (404 ?)。 38 | 4. 标签页 refocus or revisible。 39 | 40 | ## Why 41 | 42 | 部分用户(老板)没有关闭网页的习惯,在网页有新版本更新或问题修复时,用户继续使用旧的版本,影响用户体验和后端数据准确性。也有可能会出现报错(文件404)、白屏的情况。 43 | 44 | ## 安装 45 | 46 | ```bash 47 | # vite 48 | pnpm add @plugin-web-update-notification/vite -D 49 | 50 | # umijs 51 | pnpm add @plugin-web-update-notification/umijs -D 52 | 53 | # webpack plugin 54 | pnpm add @plugin-web-update-notification/webpack -D 55 | ``` 56 | 57 | ## 快速上手 58 | 59 | [vite](#vite) | [umi](#umijs) | [webpack](#webpack) 60 | 61 | ### 关键:禁用 `index.html` 缓存!!! 62 | 63 | 如果 `index.html` 存在缓存,可能刷新后,更新提示还会存在,所以需要禁用 `index.html` 的缓存。这也是 `SPA` 应用部署的一个最佳实践吧。 64 | 65 | 通过 `nginx` ,禁用缓存: 66 | 67 | ```nginx 68 | # nginx.conf 69 | location / { 70 | index index.html index.htm; 71 | 72 | if ( $uri = '/index.html' ) { # disabled index.html cache 73 | add_header Cache-Control "no-cache, no-store, must-revalidate"; 74 | } 75 | 76 | try_files $uri $uri/ /index.html; 77 | } 78 | ``` 79 | 80 | 直接通过 `html meta` 标签禁用缓存: 81 | 82 | ```html 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ``` 94 | 95 | ### Vite 96 | 97 | **基础使用** 98 | 99 | ```ts 100 | // vite.config.ts 101 | import { defineConfig } from 'vite' 102 | import vue from '@vitejs/plugin-vue' 103 | import { webUpdateNotice } from '@plugin-web-update-notification/vite' 104 | 105 | // https://vitejs.dev/config/ 106 | export default defineConfig({ 107 | plugins: [ 108 | vue(), 109 | webUpdateNotice({ 110 | logVersion: true, 111 | }), 112 | ] 113 | }) 114 | ``` 115 | 116 | **自定义通知栏文本** 117 | 118 | ```ts 119 | // vite.config.ts 120 | export default defineConfig({ 121 | plugins: [ 122 | vue(), 123 | webUpdateNotice({ 124 | notificationProps: { 125 | title: '标题', 126 | description: 'System update, please refresh the page', 127 | buttonText: '刷新', 128 | dismissButtonText: '忽略' 129 | }, 130 | }), 131 | ] 132 | }) 133 | ``` 134 | 135 | **国际化** 136 | 137 | ```ts 138 | // vite.config.ts 139 | export default defineConfig({ 140 | plugins: [ 141 | vue(), 142 | webUpdateNotice({ 143 | // plugin preset: zh_CN | zh_TW | en_US 144 | locale: "en_US", 145 | localeData: { 146 | en_US: { 147 | title: "📢 system update", 148 | description: "System update, please refresh the page", 149 | buttonText: "refresh", 150 | dismissButtonText: "dismiss", 151 | }, 152 | zh_CN: { 153 | ... 154 | }, 155 | ... 156 | }, 157 | }), 158 | ], 159 | }); 160 | 161 | 162 | // other file to set locale 163 | window.pluginWebUpdateNotice_.setLocale('zh_CN') 164 | ``` 165 | 166 | **取消默认的通知栏,监听更新事件自定义行为** 167 | ```ts 168 | // vite.config.ts 169 | export default defineConfig({ 170 | plugins: [ 171 | vue(), 172 | webUpdateNotice({ 173 | hiddenDefaultNotification: true 174 | }), 175 | ] 176 | }) 177 | 178 | // 在其他文件中监听自定义更新事件 179 | document.body.addEventListener('plugin_web_update_notice', (e) => { 180 | const { version, options } = e.detail 181 | // write some code, show your custom notification and etc. 182 | alert('System update!') 183 | }) 184 | ``` 185 | 186 | ### Umijs 187 | 188 | 不支持 `umi2`, `umi2` 可以尝试下通过 `chainWebpack` 配置 `webpack` 插件。 189 | 190 | ```ts 191 | // .umirc.ts 192 | import { defineConfig } from 'umi' 193 | import type { Options as WebUpdateNotificationOptions } from '@plugin-web-update-notification/umijs' 194 | 195 | export default { 196 | plugins: ['@plugin-web-update-notification/umijs'], 197 | webUpdateNotification: { 198 | logVersion: true, 199 | checkInterval: 0.5 * 60 * 1000, 200 | notificationProps: { 201 | title: 'system update', 202 | description: 'System update, please refresh the page', 203 | buttonText: 'refresh', 204 | dismissButtonText: 'dismiss', 205 | }, 206 | } as WebUpdateNotificationOptions 207 | } 208 | ``` 209 | 210 | ### webpack 211 | 212 | ```js 213 | // vue.config.js(vue-cli project) 214 | const { WebUpdateNotificationPlugin } = require('@plugin-web-update-notification/webpack') 215 | const { defineConfig } = require('@vue/cli-service') 216 | 217 | module.exports = defineConfig({ 218 | // ...other config 219 | configureWebpack: { 220 | plugins: [ 221 | new WebUpdateNotificationPlugin({ 222 | logVersion: true, 223 | }), 224 | ], 225 | }, 226 | }) 227 | ``` 228 | 229 | ## webUpdateNotice Options 230 | 231 | ```ts 232 | function webUpdateNotice(options?: Options): Plugin 233 | 234 | export interface Options { 235 | /** 236 | * support 'git_commit_hash' | 'svn_revision_number' | 'pkg_version' | 'build_timestamp' | 'custom' 237 | * * if repository type is 'Git', default is 'git_commit_hash' 238 | * * if repository type is 'SVN', default is 'svn_revision_number' 239 | * * if repository type is 'unknown', default is 'build_timestamp' 240 | * */ 241 | versionType?: VersionType 242 | /** 243 | * custom version, if versionType is 'custom', this option is required 244 | */ 245 | customVersion?: string 246 | /** polling interval(ms) 247 | * if set to 0, it will not polling 248 | * @default 10 * 60 * 1000 249 | */ 250 | checkInterval?: number 251 | /** 252 | * check update when window focus 253 | * @default true 254 | */ 255 | checkOnWindowFocus?: boolean 256 | /** 257 | * check update immediately after page loaded 258 | * @default true 259 | */ 260 | checkImmediately?: boolean 261 | /** 262 | * check update when load js file error 263 | * @default true 264 | */ 265 | checkOnLoadFileError?: boolean 266 | /** 267 | * whether to output version in console 268 | * 269 | * you can also pass a function to handle the version 270 | * ```ts 271 | * logVersion: (version) => { 272 | * console.log(`version: %c${version}`, 'color: #1890ff') // this is the default behavior 273 | * } 274 | * ``` 275 | * @default true 276 | */ 277 | logVersion?: boolean | ((version: string) => void) 278 | /** 279 | * whether to silence the notification. 280 | * such as when local version is v1.0, you can set this option to true and build a new version v1.0.1, then the notification will not show 281 | */ 282 | silence?: boolean 283 | /** 284 | * @deprecated 285 | */ 286 | customNotificationHTML?: string 287 | /** notificationProps have higher priority than locale */ 288 | notificationProps?: NotificationProps 289 | notificationConfig?: NotificationConfig 290 | /** 291 | * preset: zh_CN | zh_TW | en_US 292 | * @default 'zh_CN' 293 | * */ 294 | locale?: string 295 | /** 296 | * custom locale data 297 | * @link default data: https://github.com/GreatAuk/plugin-web-update-notification/blob/main/packages/core/src/locale.ts 298 | */ 299 | localeData?: LocaleData 300 | /** 301 | * Whether to hide the default notification, if you set it to true, you need to custom behavior by yourself 302 | * ```ts 303 | document.body.addEventListener('plugin_web_update_notice', (e) => { 304 | const { version, options } = e.detail 305 | // write some code, show your custom notification and etc. 306 | alert('System update!') 307 | }) 308 | * ``` 309 | * @default false 310 | */ 311 | hiddenDefaultNotification?: boolean 312 | /** 313 | * Whether to hide the dismiss button 314 | * @default false 315 | */ 316 | hiddenDismissButton?: boolean 317 | /** 318 | * After version 1.2.0, you not need to set this option, it will be automatically detected from the base of vite config、publicPath of webpack config or publicPath of umi config 319 | * 320 | * Base public path for inject file, Valid values include: 321 | * * Absolute URL pathname, e.g. /foo/ 322 | * * Full URL, e.g. https://foo.com/ 323 | * * Empty string(default) or ./ 324 | * 325 | * !!! Don't forget / at the end of the path 326 | */ 327 | injectFileBase?: string 328 | } 329 | 330 | export type VersionType = 'git_commit_hash' | 'pkg_version' | 'build_timestamp' | 'custom' 331 | 332 | export interface NotificationConfig { 333 | /** 334 | * refresh button color 335 | * @default '#1677ff' 336 | */ 337 | primaryColor?: string 338 | /** 339 | * dismiss button color 340 | * @default 'rgba(0,0,0,.25)' 341 | */ 342 | secondaryColor?: string 343 | /** @default 'bottomRight' */ 344 | placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' 345 | } 346 | 347 | export interface NotificationProps { 348 | title?: string 349 | description?: string 350 | /** refresh button text */ 351 | buttonText?: string 352 | /** dismiss button text */ 353 | dismissButtonText?: string 354 | } 355 | 356 | export type LocaleData = Record 357 | ``` 358 | 359 | ## 曝露的方法 360 | 361 | | name | params | describe | 362 | | ----------------------------------------------- | ----------------------------------- | ------------------------------------------------------------ | 363 | | window.pluginWebUpdateNotice_.setLocale | locale(preset: zh_CN、zh_TW、en_US) | set locale | 364 | | window.pluginWebUpdateNotice_.closeNotification | | close notification | 365 | | window.pluginWebUpdateNotice_.dismissUpdate | | dismiss current update and close notification,same behavior as dismiss button | 366 | | window.pluginWebUpdateNotice_.checkUpdate | | manual check update, a function wrap by debounce(5000ms) | 367 | ```ts 368 | interface Window { 369 | pluginWebUpdateNotice_: { 370 | /** 371 | * set language. 372 | * preset: zh_CN、zh_TW、en_US 373 | */ 374 | setLocale: (locale: string) => void 375 | /** 376 | * manual check update, a function wrap by debounce(5000ms) 377 | */ 378 | checkUpdate: () => void 379 | /** dismiss current update and close notification, same behavior as dismiss the button */ 380 | dismissUpdate: () => void 381 | /** close notification */ 382 | closeNotification: () => void 383 | /** 384 | * refresh button click event, if you set it, it will cover the default event (location.reload()) 385 | */ 386 | onClickRefresh?: (version: string) => void 387 | /** 388 | * dismiss button click event, if you set it, it will cover the default event (dismissUpdate()) 389 | */ 390 | onClickDismiss?: (version: string) => void 391 | } 392 | } 393 | ``` 394 | 395 | ## 变动了哪些内容 396 | 397 | ![inject_content](https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/main/images/inject_content.webp) 398 | 399 | ## Q&A 400 | 401 | 1. `TypeScript` 的智能提示, 如果你想使用 `window.pluginWebUpdateNotice_.` 或监听自定义更新事件。 402 | 403 | ```ts 404 | // src/shim.d.ts 405 | 406 | // if you use vite plugin 407 | /// 408 | 409 | // if you use umi plugin 410 | /// 411 | 412 | // if you use webpack plugin 413 | /// 414 | ``` 415 | 416 | 2. 请求 `version.json` 文件提示 `404 error`。 417 | 418 | 上传打包内容到 cdn 服务器: 419 | 420 | ```ts 421 | // vite.config.ts 422 | 423 | const prod = process.env.NODE_ENV === 'production' 424 | 425 | const cdnServerUrl = 'https://foo.com/' 426 | 427 | export default defineConfig({ 428 | base: prod ? cdnServerUrl : '/', 429 | plugins: [ 430 | vue(), 431 | webUpdateNotice({ 432 | injectFileBase: cdnServerUrl 433 | }) 434 | ] 435 | }) 436 | ``` 437 | 438 | 在非根目录下部署的项目: 439 | 440 | ```ts 441 | // vite.config.ts 442 | 443 | const prod = process.env.NODE_ENV === 'production' 444 | 445 | const base = '/folder/' // https://example.com/folder/ 446 | 447 | export default defineConfig({ 448 | base, 449 | plugins: [ 450 | vue(), 451 | webUpdateNotice({ 452 | injectFileBase: base 453 | }) 454 | ] 455 | }) 456 | ``` 457 | 458 | > After version 1.2.0, you not need to set this option, it will be automatically detected from the base of vite config、publicPath of webpack config or publicPath of umi config 459 | 460 | 3. 自定义 `notification` 的刷新和忽略按钮事件。 461 | 462 | ```ts 463 | // refresh button click event, if you set it, it will cover the default event (location.reload()) 464 | window.pluginWebUpdateNotice_.onClickRefresh = (version) => { alert(`click refresh btn: ${version}`) } 465 | 466 | // dismiss button click event, if you set it, it will cover the default event (dismissUpdate()) 467 | window.pluginWebUpdateNotice_.onClickDismiss = (version) => { alert(`click dismiss btn: ${version}`) } 468 | ``` 469 | 470 | 4. 自定义 notification 样式。 471 | 472 | 你可以通过更高的权重覆盖默认样式。([default css file](https://github.com/GreatAuk/plugin-web-update-notification/blob/main/packages/core/public/webUpdateNoticeInjectStyle.css)) 473 | 474 | ```html 475 | 476 | 477 |
478 |
479 |
480 |
481 | 📢 system update 482 |
483 |
484 | System update, please refresh the page 485 |
486 |
487 | dismiss 488 | 489 | refresh 490 | 491 |
492 |
493 |
494 |
495 | ``` 496 | 497 | 5. 手动检测更新 498 | 499 | ```ts 500 | // vue-router check update before each route change 501 | router.beforeEach((to, from, next) => { 502 | window.pluginWebUpdateNotice_.checkUpdate() 503 | next() 504 | }) 505 | ``` 506 | 507 | 6. 部分版本不通知。如客户版本是 `v1.0`, 你需要更新 `v1.0.1`, 但不想显示更新提示。 508 | 509 | ```ts 510 | webUpdateNotice({ 511 | ... 512 | silence: true 513 | }) 514 | ``` 515 | 516 | 517 | ## 文章 518 | * https://juejin.cn/post/7209234917288886331 519 | 520 | 521 | ## License 522 | 523 | [MIT](./LICENSE) 524 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | [ x ] 选择性是否通知。 2 | [ x ] 支持禁用默认检测行为,曝露检测方法,让用户可以手动检测。 3 | [ ] 如果不使用默认的 notification, 就不把 notification 相关的 js、css 打包。 4 | [ x ] support svn Revision Number。 5 | [ ] 微前端场景使用。 -------------------------------------------------------------------------------- /example/react-vite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/react-vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/react-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-vite-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "pnpm build && vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.0.0", 12 | "react-dom": "^18.0.0" 13 | }, 14 | "devDependencies": { 15 | "@plugin-web-update-notification/vite": "workspace:*", 16 | "@types/react": "^18.0.0", 17 | "@types/react-dom": "^18.0.0", 18 | "@vitejs/plugin-react": "^1.3.0", 19 | "typescript": "^4.6.3", 20 | "vite": "^2.9.9" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/react-vite/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | button { 41 | font-size: calc(10px + 2vmin); 42 | } 43 | -------------------------------------------------------------------------------- /example/react-vite/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import logo from './logo.svg' 3 | import './App.css' 4 | 5 | function App() { 6 | const [count, setCount] = useState(0) 7 | 8 | return ( 9 |
10 |
11 | logo 12 |

Hello Vite + React!

13 |

14 | 17 |

18 |

19 | Edit App.tsx and save to test HMR updates. 20 |

21 |

22 | 28 | Learn React 29 | 30 | {' | '} 31 | 37 | Vite Docs 38 | 39 |

40 |
41 |
42 | ) 43 | } 44 | 45 | export default App 46 | -------------------------------------------------------------------------------- /example/react-vite/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/react-vite/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /example/react-vite/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/react-vite/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /example/react-vite/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/react-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /example/react-vite/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example/react-vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { webUpdateNotice } from '@plugin-web-update-notification/vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | webUpdateNotice({ 10 | logVersion: true, 11 | checkInterval: 0.5 * 60 * 1000, 12 | }), 13 | ], 14 | }) 15 | -------------------------------------------------------------------------------- /example/svelte-vite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/svelte-vite/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /example/svelte-vite/README.md: -------------------------------------------------------------------------------- 1 | # Svelte + TS + Vite 2 | 3 | This template should help get you started developing with Svelte and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 8 | 9 | ## Need an official Svelte framework? 10 | 11 | Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. 12 | 13 | ## Technical considerations 14 | 15 | **Why use this over SvelteKit?** 16 | 17 | - It brings its own routing solution which might not be preferable for some users. 18 | - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. 19 | `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. 20 | 21 | This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. 22 | 23 | Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. 24 | 25 | **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** 26 | 27 | Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. 28 | 29 | **Why include `.vscode/extensions.json`?** 30 | 31 | Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. 32 | 33 | **Why enable `allowJs` in the TS template?** 34 | 35 | While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. 36 | 37 | **Why is HMR not preserving my local component state?** 38 | 39 | HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). 40 | 41 | If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. 42 | 43 | ```ts 44 | // store.ts 45 | // An extremely simple external store 46 | import { writable } from 'svelte/store' 47 | export default writable(0) 48 | ``` 49 | -------------------------------------------------------------------------------- /example/svelte-vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte + TS + Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/svelte-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-vite-example", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "pnpm build && vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json" 11 | }, 12 | "devDependencies": { 13 | "@plugin-web-update-notification/vite": "workspace:*", 14 | "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", 15 | "@tsconfig/svelte": "^2.0.1", 16 | "svelte": "^3.44.0", 17 | "svelte-check": "^2.2.7", 18 | "svelte-preprocess": "^4.9.8", 19 | "tslib": "^2.3.1", 20 | "typescript": "^4.5.4", 21 | "vite": "^2.9.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/svelte-vite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/example/svelte-vite/public/favicon.ico -------------------------------------------------------------------------------- /example/svelte-vite/src/App.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | Svelte Logo 8 |

Hello Typescript!

9 | 10 | 11 | 12 |

13 | Visit svelte.dev to learn how to build Svelte 14 | apps. 15 |

16 | 17 |

18 | Check out SvelteKit for 19 | the officially supported framework, also powered by Vite! 20 |

21 |
22 | 23 | 66 | -------------------------------------------------------------------------------- /example/svelte-vite/src/assets/svelte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/example/svelte-vite/src/assets/svelte.png -------------------------------------------------------------------------------- /example/svelte-vite/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /example/svelte-vite/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | 3 | const app = new App({ 4 | target: document.getElementById('app') 5 | }) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /example/svelte-vite/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /example/svelte-vite/svelte.config.js: -------------------------------------------------------------------------------- 1 | import sveltePreprocess from 'svelte-preprocess' 2 | 3 | export default { 4 | // Consult https://github.com/sveltejs/svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: sveltePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /example/svelte-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "resolveJsonModule": true, 8 | "baseUrl": ".", 9 | /** 10 | * Typecheck JS in `.svelte` and `.js` files by default. 11 | * Disable checkJs if you'd like to use dynamic types in JS. 12 | * Note that setting allowJs false does not prevent the use 13 | * of JS in `.svelte` files. 14 | */ 15 | "allowJs": true, 16 | "checkJs": true, 17 | "isolatedModules": true 18 | }, 19 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /example/svelte-vite/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example/svelte-vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | import { webUpdateNotice } from '@plugin-web-update-notification/vite' 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte(), webUpdateNotice({ 7 | locale: 'zh_TW', 8 | })], 9 | }) 10 | -------------------------------------------------------------------------------- /example/vue-cli/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /example/vue-cli/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /example/vue-cli/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example/vue-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "preview": "pnpm build && vite preview --port=4176" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.8.3", 12 | "vue": "^2.6.14" 13 | }, 14 | "devDependencies": { 15 | "@plugin-web-update-notification/webpack": "workspace:*", 16 | "@vue/cli-plugin-babel": "~5.0.0", 17 | "@vue/cli-plugin-typescript": "~5.0.0", 18 | "@vue/cli-service": "~5.0.0", 19 | "typescript": "~4.5.5", 20 | "vue-template-compiler": "^2.6.14" 21 | }, 22 | "browserslist": [ 23 | "> 1%", 24 | "last 2 versions", 25 | "not dead" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /example/vue-cli/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/example/vue-cli/public/favicon.ico -------------------------------------------------------------------------------- /example/vue-cli/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/vue-cli/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /example/vue-cli/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/example/vue-cli/src/assets/logo.png -------------------------------------------------------------------------------- /example/vue-cli/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 43 | 44 | 45 | 61 | -------------------------------------------------------------------------------- /example/vue-cli/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /example/vue-cli/src/shim.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /example/vue-cli/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | interface Element extends VNode {} 6 | interface ElementClass extends Vue {} 7 | interface IntrinsicElements { 8 | [elem: string]: any 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/vue-cli/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /example/vue-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "useDefineForClassFields": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /example/vue-cli/vue.config.js: -------------------------------------------------------------------------------- 1 | const { WebUpdateNotificationPlugin } = require('@plugin-web-update-notification/webpack') 2 | const { defineConfig } = require('@vue/cli-service') 3 | 4 | module.exports = defineConfig({ 5 | transpileDependencies: true, 6 | configureWebpack: { 7 | plugins: [ 8 | new WebUpdateNotificationPlugin({ 9 | logVersion: true, 10 | locale: 'en_US', 11 | }), 12 | ], 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /example/vue-vite3/.Dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /example/vue-vite3/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-typescript/recommended", 10 | "@vue/eslint-config-prettier", 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /example/vue-vite3/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /example/vue-vite3/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /example/vue-vite3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | RUN mkdir /app 3 | COPY ./dist /app 4 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /example/vue-vite3/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VS Code's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VS Code window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 43 | 44 | ```sh 45 | npm run test:unit 46 | ``` 47 | 48 | ### Run End-to-End Tests with PlayWright 49 | 50 | ```sh 51 | npm run build 52 | npm run test:e2e # or `npm run test:e2e:ci` for headless testing 53 | ``` 54 | 55 | ### Lint with [ESLint](https://eslint.org/) 56 | 57 | ```sh 58 | npm run lint 59 | ``` 60 | -------------------------------------------------------------------------------- /example/vue-vite3/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/vue-vite3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/vue-vite3/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | access_log /var/log/nginx/access.log main; 15 | sendfile on; 16 | keepalive_timeout 65; 17 | server { 18 | listen 80; 19 | server_name localhost; 20 | location / { 21 | root /app; 22 | index index.html; 23 | try_files $uri $uri/ /index.html; 24 | } 25 | error_page 500 502 503 504 /50x.html; 26 | location = /50x.html { 27 | root /usr/share/nginx/html; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /example/vue-vite3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-vite-example", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "run-p type-check build-only", 7 | "preview": "pnpm build && vite preview --port 4173", 8 | "test:unit": "vitest --environment jsdom", 9 | "test:e2e": "start-server-and-test preview http://localhost:4173/ 'PW_EXPERIMENTAL_TS_ESM=1 npx playwright test'", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 13 | }, 14 | "dependencies": { 15 | "vue": "^3.2.36", 16 | "vue-router": "^4.0.15" 17 | }, 18 | "devDependencies": { 19 | "@playwright/test": "^1.28.1", 20 | "@plugin-web-update-notification/core": "workspace:*", 21 | "@plugin-web-update-notification/vite": "workspace:*", 22 | "@rushstack/eslint-patch": "^1.1.0", 23 | "@types/jsdom": "^16.2.14", 24 | "@types/node": "^16.11.36", 25 | "@vitejs/plugin-vue": "^2.3.3", 26 | "@vue/eslint-config-prettier": "^7.0.0", 27 | "@vue/eslint-config-typescript": "^11.0.0", 28 | "@vue/test-utils": "^2.0.0", 29 | "@vue/tsconfig": "^0.1.3", 30 | "eslint": "^8.5.0", 31 | "eslint-plugin-vue": "^9.0.0", 32 | "jsdom": "^19.0.0", 33 | "npm-run-all": "^4.1.5", 34 | "prettier": "^2.5.1", 35 | "start-server-and-test": "^1.14.0", 36 | "typescript": "~4.7.2", 37 | "vite": "3.2.8", 38 | "vitest": "^0.13.0", 39 | "vue-tsc": "^0.35.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/vue-vite3/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | import { devices } from "@playwright/test"; 3 | 4 | const config: PlaywrightTestConfig = { 5 | testDir: "tests/e2e", 6 | forbidOnly: !!process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | use: { 9 | trace: "on-first-retry", 10 | baseURL: "http://localhost:4173", 11 | }, 12 | projects: [ 13 | { 14 | name: "chromium", 15 | use: { ...devices["Desktop Chrome"] }, 16 | }, 17 | // { 18 | // name: 'firefox', 19 | // use: { ...devices['Desktop Firefox'] }, 20 | // }, 21 | // { 22 | // name: 'webkit', 23 | // use: { ...devices['Desktop Safari'] }, 24 | // }, 25 | ], 26 | }; 27 | export default config; 28 | -------------------------------------------------------------------------------- /example/vue-vite3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/example/vue-vite3/public/favicon.ico -------------------------------------------------------------------------------- /example/vue-vite3/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | 38 | 135 | -------------------------------------------------------------------------------- /example/vue-vite3/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /example/vue-vite3/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 85 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/__tests__/HelloWorld.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | 3 | import { mount } from '@vue/test-utils' 4 | import HelloWorld from '../HelloWorld.vue' 5 | 6 | describe('HelloWorld', () => { 7 | it('renders properly', () => { 8 | const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) 9 | expect(wrapper.text()).toContain('Hello Vitest') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite3/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /example/vue-vite3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | 5 | const app = createApp(App); 6 | 7 | app.use(router); 8 | 9 | app.mount("#app"); 10 | 11 | document.body.addEventListener("plugin_web_update_notice", (e) => { 12 | const { version, options } = e.detail; 13 | console.log("[12]-main.ts", version, options); 14 | alert("system update"); 15 | }); 16 | -------------------------------------------------------------------------------- /example/vue-vite3/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomeView from '../views/HomeView.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: HomeView 11 | }, 12 | { 13 | path: '/about', 14 | name: 'about', 15 | // route level code-splitting 16 | // this generates a separate chunk (About.[hash].js) for this route 17 | // which is lazy-loaded when the route is visited. 18 | component: () => import('../views/AboutView.vue') 19 | } 20 | ] 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /example/vue-vite3/src/shim.d.ts: -------------------------------------------------------------------------------- 1 | // if you use vite 2 | /// 3 | -------------------------------------------------------------------------------- /example/vue-vite3/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /example/vue-vite3/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /example/vue-vite3/tests/e2e/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { 3 | DIRECTORY_NAME, 4 | NOTIFICATION_ANCHOR_CLASS_NAME, 5 | INJECT_STYLE_FILE_NAME, 6 | INJECT_SCRIPT_FILE_NAME, 7 | JSON_FILE_NAME, 8 | NOTIFICATION_DISMISS_BTN_CLASS_NAME, 9 | } from "@plugin-web-update-notification/core"; 10 | 11 | test.describe("test @plugin-web-update-notification/vite", () => { 12 | test.beforeEach(async ({ page }) => { 13 | await page.goto("/"); 14 | }); 15 | test("page access", async ({ page }) => { 16 | await expect(page).toHaveURL("http://localhost:4173/"); 17 | }); 18 | test("script and css file inject success", async ({ page }) => { 19 | const scriptTag = page.locator( 20 | `script[src="/${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.js"]` 21 | ); 22 | expect(await scriptTag.count()).toEqual(1); 23 | 24 | const cssTag = page.locator( 25 | `link[href="/${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.css"]` 26 | ); 27 | expect(await cssTag.count()).toEqual(1); 28 | }); 29 | 30 | test("notification anchor element should exist", async ({ page }) => { 31 | const anchor = page.locator(`.${NOTIFICATION_ANCHOR_CLASS_NAME}`); 32 | expect(await anchor.count()).toEqual(1); 33 | }); 34 | 35 | test(`should has a ${JSON_FILE_NAME}.json file`, async ({ request }) => { 36 | const jsonFileRes = await request.get( 37 | `${DIRECTORY_NAME}/${JSON_FILE_NAME}.json` 38 | ); 39 | expect(jsonFileRes.ok()).toBeTruthy(); 40 | 41 | const res = await jsonFileRes.json(); 42 | expect(res).toHaveProperty("version"); 43 | expect(typeof res?.version).toBe("string"); 44 | }); 45 | 46 | test("don't show notification when hash is the same", async ({ page }) => { 47 | const notificationContent = page.locator( 48 | `[data-cy="notification-content"]` 49 | ); 50 | expect(await notificationContent.count()).toEqual(0); 51 | }); 52 | 53 | test("should show a notification after system update", async ({ page }) => { 54 | // change the hash to force show the notification 55 | await page.route(`**/${JSON_FILE_NAME}.json?*`, async (route) => { 56 | // Fetch original response. 57 | const response = await page.request.fetch(route.request()); 58 | // Add a prefix to the title. 59 | const body = await response.json(); 60 | body.version = "1234567"; 61 | route.fulfill({ 62 | response, 63 | body: JSON.stringify(body), 64 | status: 200, 65 | }); 66 | }); 67 | await page.reload(); 68 | const notificationContent = page.locator( 69 | `[data-cy="notification-content"]` 70 | ); 71 | expect(await notificationContent.count()).toEqual(1); 72 | expect(await notificationContent.innerHTML()).toContain("system update"); 73 | expect(await notificationContent.innerHTML()).toContain("refresh"); 74 | }); 75 | 76 | test("dismiss feature", async ({ page }) => { 77 | const fakeVersion = "123456"; 78 | // change the hash to force show the notification 79 | await page.route(`**/${JSON_FILE_NAME}.json?*`, async (route) => { 80 | // Fetch original response. 81 | const response = await page.request.fetch(route.request()); 82 | // Add a prefix to the title. 83 | const body = await response.json(); 84 | body.version = fakeVersion; 85 | route.fulfill({ 86 | response, 87 | body: JSON.stringify(body), 88 | status: 200, 89 | }); 90 | }); 91 | await page.reload(); 92 | const notificationContent = page.locator( 93 | `[data-cy="notification-content"]` 94 | ); 95 | // has dismiss button 96 | expect(await notificationContent.innerHTML()).toContain("dismiss"); 97 | await page.locator(`.${NOTIFICATION_DISMISS_BTN_CLASS_NAME}`)?.click(); 98 | 99 | // localStorage should be been set 100 | // const storageValue = await page.evaluate(() => { 101 | // window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${fakeVersion}`); 102 | // return "true"; 103 | // }); 104 | // expect(storageValue).toBe("true"); 105 | 106 | // notification has been removed 107 | expect( 108 | await page 109 | .locator(`.${NOTIFICATION_ANCHOR_CLASS_NAME} .plugin-web-update-notice`) 110 | .count() 111 | ).toBe(0); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /example/vue-vite3/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/vue-vite3/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/vue-vite3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.config.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /example/vue-vite3/tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/vue-vite3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | import { webUpdateNotice } from "@plugin-web-update-notification/vite"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | webUpdateNotice({ 12 | logVersion: true, 13 | checkInterval: 0.5 * 60 * 1000, 14 | notificationProps: { 15 | title: "📢 system update", 16 | description: "System update, please refresh the page", 17 | buttonText: "refresh", 18 | dismissButtonText: "dismiss", 19 | }, 20 | notificationConfig: { 21 | primaryColor: "red", 22 | secondaryColor: "blue", 23 | placement: "topRight", 24 | }, 25 | }), 26 | ], 27 | resolve: { 28 | alias: { 29 | "@": fileURLToPath(new URL("./src", import.meta.url)), 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /example/vue-vite5/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /example/vue-vite5/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /example/vue-vite5/README.md: -------------------------------------------------------------------------------- 1 | # vue-vite5 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | -------------------------------------------------------------------------------- /example/vue-vite5/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/vue-vite5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/vue-vite5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-vite5", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build --force" 12 | }, 13 | "dependencies": { 14 | "vue": "^3.3.11" 15 | }, 16 | "devDependencies": { 17 | "@plugin-web-update-notification/vite": "workspace:^", 18 | "@tsconfig/node18": "^18.2.2", 19 | "@types/node": "^18.19.3", 20 | "@vitejs/plugin-vue": "^4.5.2", 21 | "@vue/tsconfig": "^0.5.0", 22 | "npm-run-all2": "^6.1.1", 23 | "typescript": "~5.3.0", 24 | "vite": "^5.0.10", 25 | "vue-tsc": "^1.8.25" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/vue-vite5/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/example/vue-vite5/public/favicon.ico -------------------------------------------------------------------------------- /example/vue-vite5/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | 20 | 48 | -------------------------------------------------------------------------------- /example/vue-vite5/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: 66 | color 0.5s, 67 | background-color 0.5s; 68 | line-height: 1.6; 69 | font-family: 70 | Inter, 71 | -apple-system, 72 | BlinkMacSystemFont, 73 | 'Segoe UI', 74 | Roboto, 75 | Oxygen, 76 | Ubuntu, 77 | Cantarell, 78 | 'Fira Sans', 79 | 'Droid Sans', 80 | 'Helvetica Neue', 81 | sans-serif; 82 | font-size: 15px; 83 | text-rendering: optimizeLegibility; 84 | -webkit-font-smoothing: antialiased; 85 | -moz-osx-font-smoothing: grayscale; 86 | } 87 | -------------------------------------------------------------------------------- /example/vue-vite5/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/vue-vite5/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | font-weight: normal; 8 | } 9 | 10 | a, 11 | .green { 12 | text-decoration: none; 13 | color: hsla(160, 100%, 37%, 1); 14 | transition: 0.4s; 15 | padding: 3px; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 42 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 89 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 88 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/vue-vite5/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /example/vue-vite5/src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import App from './App.vue' 5 | 6 | createApp(App).mount('#app') 7 | -------------------------------------------------------------------------------- /example/vue-vite5/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "noEmit": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/vue-vite5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /example/vue-vite5/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "types": ["node"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/vue-vite5/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { URL, fileURLToPath } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import { webUpdateNotice } from '@plugin-web-update-notification/vite' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | webUpdateNotice({ 12 | logVersion: true, 13 | checkInterval: 0.5 * 60 * 1000, 14 | notificationProps: { 15 | title: '📢 system update', 16 | description: 'System update, please refresh the page', 17 | buttonText: 'refresh', 18 | dismissButtonText: 'dismiss', 19 | }, 20 | notificationConfig: { 21 | primaryColor: 'red', 22 | secondaryColor: 'blue', 23 | placement: 'topRight', 24 | }, 25 | }), 26 | ], 27 | resolve: { 28 | alias: { 29 | '@': fileURLToPath(new URL('./src', import.meta.url)), 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /images/inject_content.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/images/inject_content.webp -------------------------------------------------------------------------------- /images/react_example.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/images/react_example.webp -------------------------------------------------------------------------------- /images/react_umi_example.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/images/react_umi_example.webp -------------------------------------------------------------------------------- /images/svelte_example.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/images/svelte_example.webp -------------------------------------------------------------------------------- /images/vue_example.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/34658e50141e93896fcc566e89d13f1863a1824a/images/vue_example.webp -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-web-update-notification", 3 | "type": "module", 4 | "version": "2.0.0", 5 | "packageManager": "pnpm@7.2.1", 6 | "description": "Detect web page updates and notify", 7 | "author": "Utopia", 8 | "license": "MIT", 9 | "homepage": "https://github.com/GreatAuk/plugin-web-update-notification", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/GreatAuk/plugin-web-update-notification" 13 | }, 14 | "bugs": "https://github.com/GreatAuk/plugin-web-update-notification/issues", 15 | "keywords": [ 16 | "plugin-web-update-notification" 17 | ], 18 | "sideEffects": false, 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "require": "./dist/index.js", 23 | "import": "./dist/index.mjs" 24 | } 25 | }, 26 | "main": "./dist/index.js", 27 | "module": "./dist/index.mjs", 28 | "types": "./dist/index.d.ts", 29 | "typesVersions": { 30 | "*": { 31 | "*": [ 32 | "./dist/*", 33 | "./dist/index.d.ts" 34 | ] 35 | } 36 | }, 37 | "files": [ 38 | "dist" 39 | ], 40 | "scripts": { 41 | "build": "turbo run build --filter='./packages/*'", 42 | "dev:vite": "pnpm --filter=@plugin-web-update-notification/vite dev", 43 | "dev:umi": "pnpm --filter=@plugin-web-update-notification/core dev", 44 | "dev:webpack": "pnpm --filter=@plugin-web-update-notification/webpack dev", 45 | "example:vue-vite": "pnpm --filter=vue-vite-example preview", 46 | "example:react-vite": "pnpm --filter=react-vite-example preview", 47 | "example:svelte-vite": "pnpm --filter svelte-vite-example preview", 48 | "example:react-umi": "pnpm --filter react-umi-example preview", 49 | "example:vue-webpack": "pnpm --filter vue-cli preview", 50 | "lint": "eslint .", 51 | "synchronous-doc": "tsx scripts/copyFile.ts", 52 | "prepublishOnly": "nr build", 53 | "release": "bumpp package.json packages/**/package.json", 54 | "publish": "pnpm --filter=./packages/** publish --access public --no-git-checks --registry=https://registry.npmjs.org/", 55 | "test:e2e": "pnpm --filter vue-vite-example test:e2e", 56 | "test:unit": "vitest", 57 | "typecheck": "tsc --noEmit" 58 | }, 59 | "devDependencies": { 60 | "@antfu/eslint-config": "^0.31.0", 61 | "@antfu/ni": "^0.18.8", 62 | "@antfu/utils": "^0.6.3", 63 | "@types/md5": "^2.3.2", 64 | "@types/node": "^18.11.9", 65 | "bumpp": "^9.0.0", 66 | "eslint": "^8.28.0", 67 | "md5": "^2.3.0", 68 | "pnpm": "^7.16.1", 69 | "rimraf": "^3.0.2", 70 | "tsup": "^6.6.3", 71 | "tsx": "^3.12.5", 72 | "turbo": "^1.10.12", 73 | "typescript": "^4.9.5", 74 | "vite": "^5.0.2", 75 | "vitest": "^0.29.2" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/core/README.zh-CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English](./README.md) | 简体中文 4 | 5 | # plugin-web-update-notification 6 |

7 | 8 | Gzip Size 9 | 10 | 11 | NPM Version 12 | 13 | NPM Downloads 14 | 15 | License 16 | 17 | 18 | discussions-image 19 | 20 |
21 |

22 | 23 | 检测网页更新并通知用户刷新,支持 vite、umijs 和 webpack 插件。 24 | 25 | > 以 git commit hash (也支持 svn revision number、package.json version、build timestamp、custom) 为版本号,打包时将版本号写入 json 文件。客户端轮询服务器上的版本号(浏览器窗口的 visibilitychange、focus 事件辅助),和本地作比较,如果不相同则通知用户刷新页面。 26 | 27 |

28 | 29 | 30 | 31 | 32 |

33 | **什么时候会检测更新(fetch version.json)** ? 34 | 35 | 1. 首次加载页面。 36 | 2. 轮询 (default: 10 * 60 * 1000 ms)。 37 | 3. script 脚本资源加载失败 (404 ?)。 38 | 4. 标签页 refocus or revisible。 39 | 40 | ## Why 41 | 42 | 部分用户(老板)没有关闭网页的习惯,在网页有新版本更新或问题修复时,用户继续使用旧的版本,影响用户体验和后端数据准确性。也有可能会出现报错(文件404)、白屏的情况。 43 | 44 | ## 安装 45 | 46 | ```bash 47 | # vite 48 | pnpm add @plugin-web-update-notification/vite -D 49 | 50 | # umijs 51 | pnpm add @plugin-web-update-notification/umijs -D 52 | 53 | # webpack plugin 54 | pnpm add @plugin-web-update-notification/webpack -D 55 | ``` 56 | 57 | ## 快速上手 58 | 59 | [vite](#vite) | [umi](#umijs) | [webpack](#webpack) 60 | 61 | ### 关键:禁用 `index.html` 缓存!!! 62 | 63 | 如果 `index.html` 存在缓存,可能刷新后,更新提示还会存在,所以需要禁用 `index.html` 的缓存。这也是 `SPA` 应用部署的一个最佳实践吧。 64 | 65 | 通过 `nginx` ,禁用缓存: 66 | 67 | ```nginx 68 | # nginx.conf 69 | location / { 70 | index index.html index.htm; 71 | 72 | if ( $uri = '/index.html' ) { # disabled index.html cache 73 | add_header Cache-Control "no-cache, no-store, must-revalidate"; 74 | } 75 | 76 | try_files $uri $uri/ /index.html; 77 | } 78 | ``` 79 | 80 | 直接通过 `html meta` 标签禁用缓存: 81 | 82 | ```html 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ``` 94 | 95 | ### Vite 96 | 97 | **基础使用** 98 | 99 | ```ts 100 | // vite.config.ts 101 | import { defineConfig } from 'vite' 102 | import vue from '@vitejs/plugin-vue' 103 | import { webUpdateNotice } from '@plugin-web-update-notification/vite' 104 | 105 | // https://vitejs.dev/config/ 106 | export default defineConfig({ 107 | plugins: [ 108 | vue(), 109 | webUpdateNotice({ 110 | logVersion: true, 111 | }), 112 | ] 113 | }) 114 | ``` 115 | 116 | **自定义通知栏文本** 117 | 118 | ```ts 119 | // vite.config.ts 120 | export default defineConfig({ 121 | plugins: [ 122 | vue(), 123 | webUpdateNotice({ 124 | notificationProps: { 125 | title: '标题', 126 | description: 'System update, please refresh the page', 127 | buttonText: '刷新', 128 | dismissButtonText: '忽略' 129 | }, 130 | }), 131 | ] 132 | }) 133 | ``` 134 | 135 | **国际化** 136 | 137 | ```ts 138 | // vite.config.ts 139 | export default defineConfig({ 140 | plugins: [ 141 | vue(), 142 | webUpdateNotice({ 143 | // plugin preset: zh_CN | zh_TW | en_US 144 | locale: "en_US", 145 | localeData: { 146 | en_US: { 147 | title: "📢 system update", 148 | description: "System update, please refresh the page", 149 | buttonText: "refresh", 150 | dismissButtonText: "dismiss", 151 | }, 152 | zh_CN: { 153 | ... 154 | }, 155 | ... 156 | }, 157 | }), 158 | ], 159 | }); 160 | 161 | 162 | // other file to set locale 163 | window.pluginWebUpdateNotice_.setLocale('zh_CN') 164 | ``` 165 | 166 | **取消默认的通知栏,监听更新事件自定义行为** 167 | ```ts 168 | // vite.config.ts 169 | export default defineConfig({ 170 | plugins: [ 171 | vue(), 172 | webUpdateNotice({ 173 | hiddenDefaultNotification: true 174 | }), 175 | ] 176 | }) 177 | 178 | // 在其他文件中监听自定义更新事件 179 | document.body.addEventListener('plugin_web_update_notice', (e) => { 180 | const { version, options } = e.detail 181 | // write some code, show your custom notification and etc. 182 | alert('System update!') 183 | }) 184 | ``` 185 | 186 | ### Umijs 187 | 188 | 不支持 `umi2`, `umi2` 可以尝试下通过 `chainWebpack` 配置 `webpack` 插件。 189 | 190 | ```ts 191 | // .umirc.ts 192 | import { defineConfig } from 'umi' 193 | import type { Options as WebUpdateNotificationOptions } from '@plugin-web-update-notification/umijs' 194 | 195 | export default { 196 | plugins: ['@plugin-web-update-notification/umijs'], 197 | webUpdateNotification: { 198 | logVersion: true, 199 | checkInterval: 0.5 * 60 * 1000, 200 | notificationProps: { 201 | title: 'system update', 202 | description: 'System update, please refresh the page', 203 | buttonText: 'refresh', 204 | dismissButtonText: 'dismiss', 205 | }, 206 | } as WebUpdateNotificationOptions 207 | } 208 | ``` 209 | 210 | ### webpack 211 | 212 | ```js 213 | // vue.config.js(vue-cli project) 214 | const { WebUpdateNotificationPlugin } = require('@plugin-web-update-notification/webpack') 215 | const { defineConfig } = require('@vue/cli-service') 216 | 217 | module.exports = defineConfig({ 218 | // ...other config 219 | configureWebpack: { 220 | plugins: [ 221 | new WebUpdateNotificationPlugin({ 222 | logVersion: true, 223 | }), 224 | ], 225 | }, 226 | }) 227 | ``` 228 | 229 | ## webUpdateNotice Options 230 | 231 | ```ts 232 | function webUpdateNotice(options?: Options): Plugin 233 | 234 | export interface Options { 235 | /** 236 | * support 'git_commit_hash' | 'svn_revision_number' | 'pkg_version' | 'build_timestamp' | 'custom' 237 | * * if repository type is 'Git', default is 'git_commit_hash' 238 | * * if repository type is 'SVN', default is 'svn_revision_number' 239 | * * if repository type is 'unknown', default is 'build_timestamp' 240 | * */ 241 | versionType?: VersionType 242 | /** 243 | * custom version, if versionType is 'custom', this option is required 244 | */ 245 | customVersion?: string 246 | /** polling interval(ms) 247 | * if set to 0, it will not polling 248 | * @default 10 * 60 * 1000 249 | */ 250 | checkInterval?: number 251 | /** 252 | * check update when window focus 253 | * @default true 254 | */ 255 | checkOnWindowFocus?: boolean 256 | /** 257 | * check update immediately after page loaded 258 | * @default true 259 | */ 260 | checkImmediately?: boolean 261 | /** 262 | * check update when load js file error 263 | * @default true 264 | */ 265 | checkOnLoadFileError?: boolean 266 | /** 267 | * whether to output version in console 268 | * 269 | * you can also pass a function to handle the version 270 | * ```ts 271 | * logVersion: (version) => { 272 | * console.log(`version: %c${version}`, 'color: #1890ff') // this is the default behavior 273 | * } 274 | * ``` 275 | * @default true 276 | */ 277 | logVersion?: boolean | ((version: string) => void) 278 | /** 279 | * whether to silence the notification. 280 | * such as when local version is v1.0, you can set this option to true and build a new version v1.0.1, then the notification will not show 281 | */ 282 | silence?: boolean 283 | /** 284 | * @deprecated 285 | */ 286 | customNotificationHTML?: string 287 | /** notificationProps have higher priority than locale */ 288 | notificationProps?: NotificationProps 289 | notificationConfig?: NotificationConfig 290 | /** 291 | * preset: zh_CN | zh_TW | en_US 292 | * @default 'zh_CN' 293 | * */ 294 | locale?: string 295 | /** 296 | * custom locale data 297 | * @link default data: https://github.com/GreatAuk/plugin-web-update-notification/blob/main/packages/core/src/locale.ts 298 | */ 299 | localeData?: LocaleData 300 | /** 301 | * Whether to hide the default notification, if you set it to true, you need to custom behavior by yourself 302 | * ```ts 303 | document.body.addEventListener('plugin_web_update_notice', (e) => { 304 | const { version, options } = e.detail 305 | // write some code, show your custom notification and etc. 306 | alert('System update!') 307 | }) 308 | * ``` 309 | * @default false 310 | */ 311 | hiddenDefaultNotification?: boolean 312 | /** 313 | * Whether to hide the dismiss button 314 | * @default false 315 | */ 316 | hiddenDismissButton?: boolean 317 | /** 318 | * After version 1.2.0, you not need to set this option, it will be automatically detected from the base of vite config、publicPath of webpack config or publicPath of umi config 319 | * 320 | * Base public path for inject file, Valid values include: 321 | * * Absolute URL pathname, e.g. /foo/ 322 | * * Full URL, e.g. https://foo.com/ 323 | * * Empty string(default) or ./ 324 | * 325 | * !!! Don't forget / at the end of the path 326 | */ 327 | injectFileBase?: string 328 | } 329 | 330 | export type VersionType = 'git_commit_hash' | 'pkg_version' | 'build_timestamp' | 'custom' 331 | 332 | export interface NotificationConfig { 333 | /** 334 | * refresh button color 335 | * @default '#1677ff' 336 | */ 337 | primaryColor?: string 338 | /** 339 | * dismiss button color 340 | * @default 'rgba(0,0,0,.25)' 341 | */ 342 | secondaryColor?: string 343 | /** @default 'bottomRight' */ 344 | placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' 345 | } 346 | 347 | export interface NotificationProps { 348 | title?: string 349 | description?: string 350 | /** refresh button text */ 351 | buttonText?: string 352 | /** dismiss button text */ 353 | dismissButtonText?: string 354 | } 355 | 356 | export type LocaleData = Record 357 | ``` 358 | 359 | ## 曝露的方法 360 | 361 | | name | params | describe | 362 | | ----------------------------------------------- | ----------------------------------- | ------------------------------------------------------------ | 363 | | window.pluginWebUpdateNotice_.setLocale | locale(preset: zh_CN、zh_TW、en_US) | set locale | 364 | | window.pluginWebUpdateNotice_.closeNotification | | close notification | 365 | | window.pluginWebUpdateNotice_.dismissUpdate | | dismiss current update and close notification,same behavior as dismiss button | 366 | | window.pluginWebUpdateNotice_.checkUpdate | | manual check update, a function wrap by debounce(5000ms) | 367 | ```ts 368 | interface Window { 369 | pluginWebUpdateNotice_: { 370 | /** 371 | * set language. 372 | * preset: zh_CN、zh_TW、en_US 373 | */ 374 | setLocale: (locale: string) => void 375 | /** 376 | * manual check update, a function wrap by debounce(5000ms) 377 | */ 378 | checkUpdate: () => void 379 | /** dismiss current update and close notification, same behavior as dismiss the button */ 380 | dismissUpdate: () => void 381 | /** close notification */ 382 | closeNotification: () => void 383 | /** 384 | * refresh button click event, if you set it, it will cover the default event (location.reload()) 385 | */ 386 | onClickRefresh?: (version: string) => void 387 | /** 388 | * dismiss button click event, if you set it, it will cover the default event (dismissUpdate()) 389 | */ 390 | onClickDismiss?: (version: string) => void 391 | } 392 | } 393 | ``` 394 | 395 | ## 变动了哪些内容 396 | 397 | ![inject_content](https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/main/images/inject_content.webp) 398 | 399 | ## Q&A 400 | 401 | 1. `TypeScript` 的智能提示, 如果你想使用 `window.pluginWebUpdateNotice_.` 或监听自定义更新事件。 402 | 403 | ```ts 404 | // src/shim.d.ts 405 | 406 | // if you use vite plugin 407 | /// 408 | 409 | // if you use umi plugin 410 | /// 411 | 412 | // if you use webpack plugin 413 | /// 414 | ``` 415 | 416 | 2. 请求 `version.json` 文件提示 `404 error`。 417 | 418 | 上传打包内容到 cdn 服务器: 419 | 420 | ```ts 421 | // vite.config.ts 422 | 423 | const prod = process.env.NODE_ENV === 'production' 424 | 425 | const cdnServerUrl = 'https://foo.com/' 426 | 427 | export default defineConfig({ 428 | base: prod ? cdnServerUrl : '/', 429 | plugins: [ 430 | vue(), 431 | webUpdateNotice({ 432 | injectFileBase: cdnServerUrl 433 | }) 434 | ] 435 | }) 436 | ``` 437 | 438 | 在非根目录下部署的项目: 439 | 440 | ```ts 441 | // vite.config.ts 442 | 443 | const prod = process.env.NODE_ENV === 'production' 444 | 445 | const base = '/folder/' // https://example.com/folder/ 446 | 447 | export default defineConfig({ 448 | base, 449 | plugins: [ 450 | vue(), 451 | webUpdateNotice({ 452 | injectFileBase: base 453 | }) 454 | ] 455 | }) 456 | ``` 457 | 458 | > After version 1.2.0, you not need to set this option, it will be automatically detected from the base of vite config、publicPath of webpack config or publicPath of umi config 459 | 460 | 3. 自定义 `notification` 的刷新和忽略按钮事件。 461 | 462 | ```ts 463 | // refresh button click event, if you set it, it will cover the default event (location.reload()) 464 | window.pluginWebUpdateNotice_.onClickRefresh = (version) => { alert(`click refresh btn: ${version}`) } 465 | 466 | // dismiss button click event, if you set it, it will cover the default event (dismissUpdate()) 467 | window.pluginWebUpdateNotice_.onClickDismiss = (version) => { alert(`click dismiss btn: ${version}`) } 468 | ``` 469 | 470 | 4. 自定义 notification 样式。 471 | 472 | 你可以通过更高的权重覆盖默认样式。([default css file](https://github.com/GreatAuk/plugin-web-update-notification/blob/main/packages/core/public/webUpdateNoticeInjectStyle.css)) 473 | 474 | ```html 475 | 476 | 477 |
478 |
479 |
480 |
481 | 📢 system update 482 |
483 |
484 | System update, please refresh the page 485 |
486 |
487 | dismiss 488 | 489 | refresh 490 | 491 |
492 |
493 |
494 |
495 | ``` 496 | 497 | 5. 手动检测更新 498 | 499 | ```ts 500 | // vue-router check update before each route change 501 | router.beforeEach((to, from, next) => { 502 | window.pluginWebUpdateNotice_.checkUpdate() 503 | next() 504 | }) 505 | ``` 506 | 507 | 6. 部分版本不通知。如客户版本是 `v1.0`, 你需要更新 `v1.0.1`, 但不想显示更新提示。 508 | 509 | ```ts 510 | webUpdateNotice({ 511 | ... 512 | silence: true 513 | }) 514 | ``` 515 | 516 | 517 | ## 文章 518 | * https://juejin.cn/post/7209234917288886331 519 | 520 | 521 | ## License 522 | 523 | [MIT](./LICENSE) 524 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plugin-web-update-notification/core", 3 | "type": "module", 4 | "version": "2.0.0", 5 | "description": "Detect web page updates and notify", 6 | "author": "Utopia", 7 | "license": "MIT", 8 | "homepage": "https://github.com/GreatAuk/plugin-web-update-notification", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/GreatAuk/plugin-web-update-notification", 12 | "directory": "packages/core" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/GreatAuk/plugin-web-update-notification/issues" 16 | }, 17 | "keywords": [ 18 | "web-update-notification" 19 | ], 20 | "sideEffects": false, 21 | "exports": { 22 | ".": { 23 | "types": "./dist/index.d.ts", 24 | "require": "./dist/index.cjs", 25 | "import": "./dist/index.js" 26 | } 27 | }, 28 | "main": "dist/index.cjs", 29 | "module": "dist/index.js", 30 | "types": "dist/index.d.ts", 31 | "files": [ 32 | "dist" 33 | ], 34 | "scripts": { 35 | "start": "tsx src/index.ts", 36 | "build": "tsup && tsup --config tsup.config.injectFile.ts", 37 | "dev": "tsup --watch" 38 | }, 39 | "devDependencies": { 40 | "find-up": "^6.3.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/public/webUpdateNoticeInjectStyle.css: -------------------------------------------------------------------------------- 1 | .plugin-web-update-notice { 2 | position: fixed; 3 | user-select: none; 4 | z-index: 99999; 5 | } 6 | 7 | .plugin-web-update-notice-content { 8 | background-color: #fff; 9 | border-radius: 4px; 10 | color: #000000d9; 11 | box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d; 12 | padding: 8px 16px; 13 | line-height: 1.5715; 14 | width: 280px; 15 | } 16 | 17 | .plugin-web-update-notice-content-title { 18 | font-weight: 500; 19 | margin-bottom: 4px; 20 | font-size: 14px; 21 | line-height: 24px; 22 | } 23 | 24 | .plugin-web-update-notice-content-desc { 25 | font-size: 13px; 26 | line-height: 20px; 27 | } 28 | .plugin-web-update-notice-tools { 29 | margin-top: 4px; 30 | text-align: right; 31 | } 32 | .plugin-web-update-notice-btn { 33 | padding: 3px 8px; 34 | line-height: 1; 35 | border-radius: 4px; 36 | transition: background-color .2s linear; 37 | cursor: pointer; 38 | font-size: 14px; 39 | } 40 | .plugin-web-update-notice-btn:hover { 41 | background-color: rgba(64, 87, 109, .1); 42 | } 43 | .plugin-web-update-notice-refresh-btn { 44 | color: #1677ff; 45 | } 46 | .plugin-web-update-notice-dismiss-btn { 47 | color: rgba(0,0,0,.25); 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/buildScript.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { readFileSync, writeFileSync } from 'node:fs' 3 | 4 | import { INJECT_SCRIPT_FILE_NAME } from './constant' 5 | 6 | /** 7 | * remove injectScript file sourcemap comment 8 | */ 9 | function removeSourcemapComment() { 10 | const path = resolve(process.cwd(), `./dist/${INJECT_SCRIPT_FILE_NAME}.js`) 11 | const injectScript = readFileSync(path, 'utf-8', 12 | ).replace(/\n\/\/# sourceMappingURL=.*$/, '') 13 | // write file 14 | writeFileSync(path, injectScript, 'utf-8') 15 | } 16 | 17 | removeSourcemapComment() 18 | -------------------------------------------------------------------------------- /packages/core/src/constant.ts: -------------------------------------------------------------------------------- 1 | export const DIRECTORY_NAME = 'pluginWebUpdateNotice' 2 | export const JSON_FILE_NAME = 'web_version_by_plugin' 3 | export const INJECT_STYLE_FILE_NAME = 'webUpdateNoticeInjectStyle' 4 | /** .global is iife suffix */ 5 | export const INJECT_SCRIPT_FILE_NAME = 'webUpdateNoticeInjectScript.global' 6 | export const CUSTOM_UPDATE_EVENT_NAME = 'plugin_web_update_notice' 7 | export const NOTIFICATION_ANCHOR_CLASS_NAME = 'plugin-web-update-notice-anchor' 8 | 9 | /** refresh button class name */ 10 | export const NOTIFICATION_REFRESH_BTN_CLASS_NAME = 'plugin-web-update-notice-refresh-btn' 11 | /** dismiss button class name */ 12 | export const NOTIFICATION_DISMISS_BTN_CLASS_NAME = 'plugin-web-update-notice-dismiss-btn' 13 | export const LOCAL_STORAGE_PREFIX = 'web_update_check_dismiss_version_' 14 | 15 | export const NOTIFICATION_POSITION_MAP = { 16 | topLeft: 'top: 24px;left: 24px', 17 | topRight: 'top: 24px;right: 24px', 18 | bottomLeft: 'bottom: 24px;left: 24px', 19 | bottomRight: 'bottom: 24px;right: 24px', 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { execSync } from 'node:child_process' 4 | import { findUpSync } from 'find-up' 5 | import md5 from 'md5' 6 | 7 | import './shim.d.ts' 8 | 9 | import { name as pkgName_ } from '../package.json' 10 | import type { Options, VersionJSON, VersionType } from './type' 11 | export * from './constant' 12 | export type { Options } from './type' 13 | export const pkgName = pkgName_ 14 | 15 | /** 16 | * It returns the directory name of the current file. 17 | * @returns __dirname 18 | */ 19 | export function get__Dirname() { 20 | try { 21 | if (import.meta && import.meta.url) 22 | return dirname(fileURLToPath(import.meta.url)) 23 | 24 | return __dirname 25 | } 26 | catch (err) { 27 | return __dirname 28 | } 29 | } 30 | 31 | /** 32 | * The function returns the first 8 characters of the MD5 hash of a given string. 33 | * @param {string} fileString - a string that represents the content of a file. 34 | * @returns the first 8 characters of the MD5 hash of the file 35 | */ 36 | export function getFileHash(fileString: string) { 37 | return md5(fileString).slice(0, 8) 38 | } 39 | 40 | /** 41 | * It checks if the current directory is a Git or SVN repository, and returns the type of repository 42 | * @returns 'Git' | 'SVN' | 'unknown' 43 | */ 44 | function checkRepoType() { 45 | const gitRepo = findUpSync('.git', { type: 'directory' }) 46 | if (gitRepo) 47 | return 'Git' 48 | const svnRepo = findUpSync('.svn', { type: 'directory' }) 49 | if (svnRepo) 50 | return 'SVN' 51 | 52 | return 'unknown' 53 | } 54 | 55 | /** 56 | * It returns the version of the host project's package.json file 57 | * @returns The version of the package.json file in the root of the project. 58 | */ 59 | export function getHostProjectPkgVersion() { 60 | try { 61 | return process.env.npm_package_version as string 62 | } 63 | catch (err) { 64 | console.warn(` 65 | ====================================================== 66 | [plugin-web-update-notice] cannot get the version of the host project's package.json file! 67 | ======================================================`) 68 | throw err 69 | } 70 | } 71 | 72 | /** 73 | * If the current directory is a git repository, return the current commit hash 74 | * @returns The git commit hash of the current branch. 75 | */ 76 | export function getGitCommitHash() { 77 | try { 78 | return execSync('git rev-parse --short HEAD').toString().replace('\n', '').trim() 79 | } 80 | catch (err) { 81 | console.warn(` 82 | ====================================================== 83 | [plugin-web-update-notice] Not a git repository! 84 | ======================================================`) 85 | throw err 86 | } 87 | } 88 | 89 | /** 90 | * get SVN revision number 91 | * @returns The SVN revision number. 92 | */ 93 | export function getSVNRevisionNumber() { 94 | try { 95 | return execSync('svnversion').toString().replace('\n', '').trim() 96 | } 97 | catch (err) { 98 | console.warn(` 99 | ====================================================== 100 | [plugin-web-update-notice] Not a SVN repository! 101 | ======================================================`) 102 | throw err 103 | } 104 | } 105 | 106 | /** 107 | * It returns the current timestamp 108 | * @returns The current time in milliseconds. 109 | */ 110 | export function getTimestamp() { 111 | return `${Date.now()}` 112 | } 113 | 114 | export function getCustomVersion(version?: string) { 115 | if (!version) { 116 | console.warn(` 117 | ====================================================== 118 | [plugin-web-update-notice] The versionType is 'custom', but the customVersion is not specified! 119 | ======================================================`) 120 | throw new Error('The versionType is \'custom\', but the customVersion is not specified!') 121 | } 122 | return version 123 | } 124 | 125 | /** 126 | * It returns the version of the current project. 127 | * @param {VersionType} [versionType=git_commit_hash] - The version type 128 | * @param {string} [customVersion] - The custom version 129 | * @returns The version by the plugin. 130 | */ 131 | export function getVersion(): string 132 | export function getVersion(versionType: 'custom', customVersion: string): string 133 | export function getVersion(versionType: Exclude): string 134 | export function getVersion(versionType?: VersionType, customVersion?: string) { 135 | const getVersionStrategies: Record string> = { 136 | pkg_version: getHostProjectPkgVersion, 137 | git_commit_hash: getGitCommitHash, 138 | build_timestamp: getTimestamp, 139 | custom: () => getCustomVersion(customVersion), 140 | svn_revision_number: getSVNRevisionNumber, 141 | } 142 | 143 | const defaultStrategyMap = { 144 | Git: 'git_commit_hash', 145 | SVN: 'svn_revision_number', 146 | unknown: '', 147 | } 148 | 149 | const versionType_ = (versionType || defaultStrategyMap[checkRepoType()]) as VersionType 150 | 151 | try { 152 | const strategy = getVersionStrategies[versionType_] 153 | if (!strategy) { 154 | console.warn(` 155 | ====================================================== 156 | [plugin-web-update-notice] The version type '${versionType}' is not supported!, we will use the packaging timestamp instead. 157 | ======================================================`) 158 | return getTimestamp() 159 | } 160 | 161 | return strategy() 162 | } 163 | catch (err) { 164 | console.warn(` 165 | ====================================================== 166 | [plugin-web-update-notice] get version throw a error, we will use the packaging timestamp instead. 167 | ======================================================`) 168 | console.error(err) 169 | return getTimestamp() 170 | } 171 | } 172 | 173 | /** 174 | * generate json file content for version 175 | * @param {string} version - git commit hash or packaging time 176 | * @returns A string 177 | */ 178 | export function generateJSONFileContent(version: string, silence = false) { 179 | const content: VersionJSON = { 180 | version, 181 | } 182 | if (silence) 183 | content.silence = true 184 | 185 | return JSON.stringify(content, null, 2) 186 | } 187 | 188 | export function generateJsFileContent(fileSource: string, version: string, options: Options) { 189 | const { logVersion = true } = options 190 | let content = `${fileSource} 191 | window.__checkUpdateSetup__(${JSON.stringify(options)});` 192 | if (logVersion) { 193 | const fn = typeof logVersion === 'function' ? logVersion : logVersionDefault 194 | content += ` 195 | ;const logFn = ${fn.toString()} 196 | ;logFn('${version}', ${Date.now()}) 197 | ` 198 | } 199 | return content 200 | } 201 | 202 | export function logVersionDefault(version: string, releaseTime: number) { 203 | // eslint-disable-next-line no-console 204 | console.log(`version: %c${version}`, 'color: #1677ff') 205 | 206 | // eslint-disable-next-line no-console 207 | console.log(`release time: %c${new Date(releaseTime).toLocaleString()}`, 'color: #1677ff') 208 | } 209 | -------------------------------------------------------------------------------- /packages/core/src/injectScript.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleData, Options, VersionJSON } from './type' 2 | import { 3 | CUSTOM_UPDATE_EVENT_NAME, 4 | DIRECTORY_NAME, 5 | JSON_FILE_NAME, 6 | LOCAL_STORAGE_PREFIX, 7 | NOTIFICATION_ANCHOR_CLASS_NAME, 8 | NOTIFICATION_DISMISS_BTN_CLASS_NAME, 9 | NOTIFICATION_POSITION_MAP, 10 | NOTIFICATION_REFRESH_BTN_CLASS_NAME, 11 | } from './constant' 12 | import presetLocaleData from './locale' 13 | 14 | let hasShowSystemUpdateNotice = false 15 | /** latest version from server */ 16 | let latestVersion = '' 17 | let currentLocale = '' 18 | let intervalTimer: NodeJS.Timer | undefined 19 | 20 | /** 21 | * limit function 22 | * @param {Function} fn - The function to be called. 23 | * @param {number} delay - The amount of time to wait before calling the function. 24 | * @returns A function that called limit 25 | */ 26 | function limit(fn: Function, delay: number) { 27 | let pending = false 28 | return function (this: any, ...args: any[]) { 29 | if (pending) 30 | return 31 | pending = true 32 | fn.apply(this, args) 33 | setTimeout(() => { 34 | pending = false 35 | }, delay) 36 | } 37 | } 38 | 39 | /** 40 | * It reloads the current page without using the browser cache 41 | */ 42 | // function reloadPageWithoutCache() { 43 | // const url = new URL(window.location.href) 44 | // url.searchParams.set('__time__', Date.now().toString()) 45 | // window.location.replace(url.href) 46 | // } 47 | 48 | /** 49 | * querySelector takes a string and returns an element. 50 | * @param {string} selector - string 51 | * @returns The first element that matches the selector. 52 | */ 53 | function querySelector(selector: string) { 54 | return document.querySelector(selector) 55 | } 56 | 57 | window.pluginWebUpdateNotice_ = { 58 | checkUpdate: () => {}, 59 | dismissUpdate, 60 | closeNotification, 61 | setLocale: (locale: string) => { 62 | window.pluginWebUpdateNotice_.locale = locale 63 | currentLocale = locale 64 | }, 65 | } 66 | 67 | /** 68 | * It checks whether the system has been updated and if so, it shows a notification. 69 | * @param {Options} options - Options 70 | */ 71 | function __checkUpdateSetup__(options: Options) { 72 | const { 73 | injectFileBase = '', 74 | checkInterval = 10 * 60 * 1000, 75 | hiddenDefaultNotification, 76 | checkOnWindowFocus = true, 77 | checkImmediately = true, 78 | checkOnLoadFileError = true, 79 | } = options 80 | const checkSystemUpdate = () => { 81 | window 82 | .fetch(`${injectFileBase}${DIRECTORY_NAME}/${JSON_FILE_NAME}.json?t=${Date.now()}`) 83 | .then((response) => { 84 | if (!response.ok) 85 | throw new Error(`Failed to fetch ${JSON_FILE_NAME}.json`) 86 | 87 | return response.json() 88 | }) 89 | .then(({ version: versionFromServer, silence }: VersionJSON) => { 90 | if (silence) 91 | return 92 | latestVersion = versionFromServer 93 | if (window.pluginWebUpdateNotice_version !== versionFromServer) { 94 | // dispatch custom event 95 | document.body.dispatchEvent(new CustomEvent(CUSTOM_UPDATE_EVENT_NAME, { 96 | detail: { 97 | options, 98 | version: versionFromServer, 99 | }, 100 | bubbles: true, 101 | })) 102 | 103 | const dismiss = localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${versionFromServer}`) === 'true' 104 | if (!hasShowSystemUpdateNotice && !hiddenDefaultNotification && !dismiss) 105 | showNotification(options) 106 | } 107 | }) 108 | .catch((err) => { 109 | console.error('[pluginWebUpdateNotice] Failed to check system update', err) 110 | }) 111 | } 112 | 113 | if (checkImmediately) { 114 | // check system update after page loaded 115 | setTimeout(checkSystemUpdate) 116 | } 117 | 118 | /** 119 | * polling check system update 120 | */ 121 | const pollingCheck = () => { 122 | if (checkInterval > 0) 123 | intervalTimer = setInterval(checkSystemUpdate, checkInterval) 124 | } 125 | pollingCheck() 126 | 127 | const limitCheckSystemUpdate = limit(checkSystemUpdate, 5000) 128 | 129 | window.pluginWebUpdateNotice_.checkUpdate = limitCheckSystemUpdate 130 | 131 | // when page visibility change, check system update 132 | window.addEventListener('visibilitychange', () => { 133 | if (document.visibilityState === 'visible') { 134 | pollingCheck() 135 | if (checkOnWindowFocus) 136 | limitCheckSystemUpdate() 137 | } 138 | if (document.visibilityState === 'hidden') 139 | intervalTimer && clearInterval(intervalTimer) 140 | }) 141 | 142 | // when page focus, check system update 143 | window.addEventListener('focus', () => { 144 | if (checkOnWindowFocus) 145 | limitCheckSystemUpdate() 146 | }) 147 | 148 | if (checkOnLoadFileError) { 149 | // listener script resource loading error 150 | window.addEventListener( 151 | 'error', 152 | (err) => { 153 | const errTagName = (err?.target as any)?.tagName 154 | if (errTagName === 'SCRIPT') 155 | checkSystemUpdate() 156 | }, 157 | true, 158 | ) 159 | } 160 | } 161 | 162 | window.__checkUpdateSetup__ = __checkUpdateSetup__ 163 | 164 | /** 165 | * close notification, remove the notification from the DOM 166 | */ 167 | function closeNotification() { 168 | hasShowSystemUpdateNotice = false 169 | querySelector(`.${NOTIFICATION_ANCHOR_CLASS_NAME} .plugin-web-update-notice`)?.remove() 170 | } 171 | 172 | /** 173 | * dismiss current update and hide notification 174 | */ 175 | function dismissUpdate() { 176 | try { 177 | closeNotification() 178 | localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${latestVersion}`, 'true') 179 | } 180 | catch (err) { 181 | console.error(err) 182 | } 183 | } 184 | 185 | /** 186 | * Bind the refresh button click event to refresh the page, and bind the dismiss button click event to 187 | * hide the notification and dismiss the system update. 188 | */ 189 | function bindBtnEvent() { 190 | // bind refresh button click event, click to refresh page 191 | const refreshBtn = querySelector(`.${NOTIFICATION_REFRESH_BTN_CLASS_NAME}`) 192 | refreshBtn?.addEventListener('click', () => { 193 | const { onClickRefresh } = window.pluginWebUpdateNotice_ 194 | if (onClickRefresh) { 195 | onClickRefresh(latestVersion) 196 | return 197 | } 198 | window.location.reload() 199 | }) 200 | 201 | // bind dismiss button click event, click to hide notification 202 | const dismissBtn = querySelector(`.${NOTIFICATION_DISMISS_BTN_CLASS_NAME}`) 203 | dismissBtn?.addEventListener('click', () => { 204 | const { onClickDismiss } = window.pluginWebUpdateNotice_ 205 | if (onClickDismiss) { 206 | onClickDismiss(latestVersion) 207 | return 208 | } 209 | dismissUpdate() 210 | }) 211 | } 212 | 213 | /** 214 | * It returns the value of the key in the localeData object, or the value of the key in the 215 | * presetLocaleData object, or the value of the key in the presetLocaleData.zh_CN object 216 | * @param {string} locale - The locale to be used, such as zh_CN, en_US, etc. 217 | * @param key - The key of the text to be obtained in the locale data. 218 | * @param {LocaleData} localeData - The locale data object that you passed in. 219 | * @returns The value of the key in the localeData object. 220 | */ 221 | function getLocaleText(locale: string, key: keyof LocaleData[string], localeData: LocaleData) { 222 | return localeData[locale]?.[key] ?? presetLocaleData[locale]?.[key] ?? presetLocaleData.zh_CN[key] 223 | } 224 | 225 | /** 226 | * show update notification 227 | */ 228 | function showNotification(options: Options) { 229 | try { 230 | hasShowSystemUpdateNotice = true 231 | 232 | const { notificationProps, notificationConfig, customNotificationHTML, hiddenDismissButton, locale = 'zh_CN', localeData: localeData_ } = options 233 | const localeData = Object.assign({}, presetLocaleData, localeData_) 234 | if (!currentLocale) { 235 | currentLocale = locale 236 | window.pluginWebUpdateNotice_.locale = locale 237 | } 238 | 239 | const notificationWrap = document.createElement('div') 240 | let notificationInnerHTML = '' 241 | 242 | if (customNotificationHTML) { 243 | notificationInnerHTML = customNotificationHTML 244 | } 245 | else { 246 | const { placement = 'bottomRight', primaryColor, secondaryColor } = notificationConfig || {} 247 | const title = notificationProps?.title ?? getLocaleText(currentLocale, 'title', localeData) 248 | const description = notificationProps?.description ?? getLocaleText(currentLocale, 'description', localeData) 249 | const buttonText = notificationProps?.buttonText ?? getLocaleText(currentLocale, 'buttonText', localeData) 250 | const dismissButtonText = notificationProps?.dismissButtonText ?? getLocaleText(currentLocale, 'dismissButtonText', localeData) 251 | const dismissButtonHtml = hiddenDismissButton ? '' : `${dismissButtonText}` 252 | 253 | notificationWrap.classList.add('plugin-web-update-notice') 254 | notificationWrap.style.cssText = `${NOTIFICATION_POSITION_MAP[placement]}` 255 | notificationInnerHTML = ` 256 |
257 |
258 | ${title} 259 |
260 |
261 | ${description} 262 |
263 |
264 | ${dismissButtonHtml} 265 | 266 | ${buttonText} 267 | 268 |
269 |
` 270 | } 271 | 272 | notificationWrap.innerHTML = notificationInnerHTML 273 | document 274 | .querySelector(`.${NOTIFICATION_ANCHOR_CLASS_NAME}`)! 275 | .appendChild(notificationWrap) 276 | 277 | bindBtnEvent() 278 | } 279 | catch (err) { 280 | console.error('[pluginWebUpdateNotice] Failed to show notification', err) 281 | } 282 | } 283 | 284 | // meaningless export, in order to let tsup bundle these functions 285 | export { 286 | __checkUpdateSetup__, 287 | } 288 | -------------------------------------------------------------------------------- /packages/core/src/locale.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleData } from './type' 2 | 3 | const localeData: LocaleData = { 4 | zh_CN: { 5 | title: '发现新版本', 6 | description: '网页更新啦!请刷新页面后使用。', 7 | buttonText: '刷新', 8 | dismissButtonText: '忽略', 9 | }, 10 | zh_TW: { 11 | title: '發現新版本', 12 | description: '網頁更新啦!請刷新頁面後使用。', 13 | buttonText: '刷新', 14 | dismissButtonText: '忽略', 15 | }, 16 | en_US: { 17 | title: 'Discover new version', 18 | description: 'A new version is available! Please refresh the page.', 19 | buttonText: 'Refresh', 20 | dismissButtonText: 'Dismiss', 21 | }, 22 | } 23 | 24 | export default localeData 25 | -------------------------------------------------------------------------------- /packages/core/src/pluginBuildScript.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { copyFileSync } from 'fs' 3 | import { name as pkgName } from '../package.json' 4 | import { INJECT_SCRIPT_FILE_NAME, INJECT_STYLE_FILE_NAME } from './constant' 5 | 6 | const scriptFilePath = resolve('node_modules', pkgName, 'dist', `${INJECT_SCRIPT_FILE_NAME}.js`) 7 | const styleFilePath = resolve('node_modules', pkgName, 'dist', `${INJECT_STYLE_FILE_NAME}.css`) 8 | 9 | // copy file from @plugin-web-update-notification/core/dist/??.js */ to dist/ 10 | copyFileSync(scriptFilePath, `dist/${INJECT_SCRIPT_FILE_NAME}.js`) 11 | 12 | // copy file from @plugin-web-update-notification/core/dist/??.css */ to dist/ 13 | copyFileSync(styleFilePath, `dist/${INJECT_STYLE_FILE_NAME}.css`) 14 | -------------------------------------------------------------------------------- /packages/core/src/shim.d.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from './type' 2 | 3 | declare global { 4 | interface Window { 5 | /** version number */ 6 | pluginWebUpdateNotice_version: string 7 | /** 8 | * don't call this function in manual。 9 | */ 10 | __checkUpdateSetup__: (options: Options) => void 11 | pluginWebUpdateNotice_: { 12 | locale?: string; 13 | /** 14 | * set language. 15 | * preset: zh_CN、zh_TW、en_US 16 | */ 17 | setLocale: (locale: string) => void 18 | /** 19 | * manual check update, a function wrap by debounce(5000ms) 20 | */ 21 | checkUpdate: () => void 22 | /** dismiss current update and close notification, same behavior as dismiss the button */ 23 | dismissUpdate: () => void 24 | /** close notification */ 25 | closeNotification: () => void 26 | /** 27 | * refresh button click event, if you set it, it will cover the default event (location.reload()) 28 | */ 29 | onClickRefresh?: (version: string) => void 30 | /** 31 | * dismiss button click event, if you set it, it will cover the default event (dismissUpdate()) 32 | */ 33 | onClickDismiss?: (version: string) => void 34 | } 35 | } 36 | interface GlobalEventHandlersEventMap { 37 | plugin_web_update_notice: CustomEvent<{ version: string; options: Options }>; 38 | } 39 | } -------------------------------------------------------------------------------- /packages/core/src/type.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | /** 3 | * support 'git_commit_hash' | 'svn_revision_number' | 'pkg_version' | 'build_timestamp' | 'custom' 4 | * * if repository type is 'Git', default is 'git_commit_hash' 5 | * * if repository type is 'SVN', default is 'svn_revision_number' 6 | * * if repository type is 'unknown', default is 'build_timestamp' 7 | * */ 8 | versionType?: VersionType 9 | /** 10 | * custom version, if versionType is 'custom', this option is required 11 | */ 12 | customVersion?: string 13 | /** polling interval(ms). 14 | * if set to 0, it will not polling 15 | * @default 10 * 60 * 1000 16 | */ 17 | checkInterval?: number 18 | /** 19 | * check update when window focus 20 | * @default true 21 | */ 22 | checkOnWindowFocus?: boolean 23 | /** 24 | * check update immediately after page loaded 25 | * @default true 26 | */ 27 | checkImmediately?: boolean 28 | /** 29 | * check update when load js file error 30 | * @default true 31 | */ 32 | checkOnLoadFileError?: boolean 33 | /** 34 | * whether to output version in console 35 | * 36 | * you can also pass a function to handle the version 37 | * ```ts 38 | * logVersion: (version) => { 39 | * console.log(`version: %c${version}`, 'color: #1890ff') // this is the default behavior 40 | * } 41 | * ``` 42 | * @default true 43 | */ 44 | logVersion?: boolean | ((version: string) => void) 45 | /** 46 | * whether to silence the notification. 47 | * such as when local version is v1.0, you can set this option to true and build a new version v1.0.1, then the notification will not show 48 | */ 49 | silence?: boolean 50 | /** 51 | * @deprecated 52 | */ 53 | customNotificationHTML?: string 54 | /** notificationProps have higher priority than locale */ 55 | notificationProps?: NotificationProps 56 | notificationConfig?: NotificationConfig 57 | /** 58 | * preset: zh_CN | zh_TW | en_US 59 | * @default 'zh_CN' 60 | * */ 61 | locale?: string 62 | /** 63 | * custom locale data 64 | * @link default data: https://github.com/GreatAuk/plugin-web-update-notification/blob/main/packages/core/src/locale.ts 65 | */ 66 | localeData?: LocaleData 67 | /** 68 | * Whether to hide the default notification, if you set it to true, you need to custom behavior by yourself 69 | * ```ts 70 | document.body.addEventListener('plugin_web_update_notice', (e) => { 71 | const { version, options } = e.detail 72 | // write some code, show your custom notification and etc. 73 | alert('System update!') 74 | }) 75 | * ``` 76 | * @default false 77 | */ 78 | hiddenDefaultNotification?: boolean 79 | /** 80 | * Whether to hide the dismiss button 81 | * @default false 82 | */ 83 | hiddenDismissButton?: boolean 84 | /** 85 | * After version 1.2.0, you not need to set this option, it will be automatically detected from the base of vite config、publicPath of webpack config or publicPath of umi config 86 | * 87 | * Base public path for inject file, Valid values include: 88 | * * Absolute URL pathname, e.g. /foo/ 89 | * * Full URL, e.g. https://foo.com/ 90 | * * Empty string(default) or ./ 91 | * 92 | * !!! Don't forget / at the end of the path 93 | */ 94 | injectFileBase?: string 95 | } 96 | 97 | export type VersionType = 'git_commit_hash' | 'svn_revision_number' | 'pkg_version' | 'build_timestamp' | 'custom' 98 | 99 | export interface NotificationConfig { 100 | /** 101 | * refresh button color 102 | * @default '#1677ff' 103 | */ 104 | primaryColor?: string 105 | /** 106 | * dismiss button color 107 | * @default 'rgba(0,0,0,.25)' 108 | */ 109 | secondaryColor?: string 110 | /** @default 'bottomRight' */ 111 | placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' 112 | } 113 | 114 | export interface NotificationProps { 115 | title?: string 116 | description?: string 117 | /** refresh button text */ 118 | buttonText?: string 119 | /** dismiss button text */ 120 | dismissButtonText?: string 121 | } 122 | 123 | export type LocaleData = Record 124 | 125 | export interface VersionJSON { 126 | version: string 127 | silence?: boolean 128 | } 129 | -------------------------------------------------------------------------------- /packages/core/tsup.config.injectFile.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { INJECT_SCRIPT_FILE_NAME } from './src/constant' 3 | 4 | export default defineConfig((options) => { // The options here is derived from CLI flags. 5 | return { 6 | entry: { 7 | [INJECT_SCRIPT_FILE_NAME.replace('.global', '')]: 'src/injectScript.ts', 8 | }, 9 | target: 'es6', 10 | splitting: false, 11 | sourcemap: false, 12 | format: ['esm', 'iife'], 13 | minify: !options.watch, 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig((options) => { // The options here is derived from CLI flags. 4 | return { 5 | entry: { 6 | index: 'src/index.ts', 7 | pluginBuildScript: 'src/pluginBuildScript.ts', 8 | }, 9 | target: 'es2020', 10 | splitting: false, 11 | sourcemap: true, 12 | clean: true, 13 | dts: true, 14 | format: ['esm', 'cjs'], 15 | minify: !options.watch, 16 | onSuccess: process.platform === 'win32' ? 'xcopy /E /I public\\ dist\\' : 'cp -a public/. dist', 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /packages/umi-plugin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/umi-plugin/README.zh-CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [English](./README.md) | 简体中文 4 | 5 | # plugin-web-update-notification 6 |

7 | 8 | Gzip Size 9 | 10 | 11 | NPM Version 12 | 13 | NPM Downloads 14 | 15 | License 16 | 17 | 18 | discussions-image 19 | 20 |
21 |

22 | 23 | 检测网页更新并通知用户刷新,支持 vite、umijs 和 webpack 插件。 24 | 25 | > 以 git commit hash (也支持 svn revision number、package.json version、build timestamp、custom) 为版本号,打包时将版本号写入 json 文件。客户端轮询服务器上的版本号(浏览器窗口的 visibilitychange、focus 事件辅助),和本地作比较,如果不相同则通知用户刷新页面。 26 | 27 |

28 | 29 | 30 | 31 | 32 |

33 | **什么时候会检测更新(fetch version.json)** ? 34 | 35 | 1. 首次加载页面。 36 | 2. 轮询 (default: 10 * 60 * 1000 ms)。 37 | 3. script 脚本资源加载失败 (404 ?)。 38 | 4. 标签页 refocus or revisible。 39 | 40 | ## Why 41 | 42 | 部分用户(老板)没有关闭网页的习惯,在网页有新版本更新或问题修复时,用户继续使用旧的版本,影响用户体验和后端数据准确性。也有可能会出现报错(文件404)、白屏的情况。 43 | 44 | ## 安装 45 | 46 | ```bash 47 | # vite 48 | pnpm add @plugin-web-update-notification/vite -D 49 | 50 | # umijs 51 | pnpm add @plugin-web-update-notification/umijs -D 52 | 53 | # webpack plugin 54 | pnpm add @plugin-web-update-notification/webpack -D 55 | ``` 56 | 57 | ## 快速上手 58 | 59 | [vite](#vite) | [umi](#umijs) | [webpack](#webpack) 60 | 61 | ### 关键:禁用 `index.html` 缓存!!! 62 | 63 | 如果 `index.html` 存在缓存,可能刷新后,更新提示还会存在,所以需要禁用 `index.html` 的缓存。这也是 `SPA` 应用部署的一个最佳实践吧。 64 | 65 | 通过 `nginx` ,禁用缓存: 66 | 67 | ```nginx 68 | # nginx.conf 69 | location / { 70 | index index.html index.htm; 71 | 72 | if ( $uri = '/index.html' ) { # disabled index.html cache 73 | add_header Cache-Control "no-cache, no-store, must-revalidate"; 74 | } 75 | 76 | try_files $uri $uri/ /index.html; 77 | } 78 | ``` 79 | 80 | 直接通过 `html meta` 标签禁用缓存: 81 | 82 | ```html 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ``` 94 | 95 | ### Vite 96 | 97 | **基础使用** 98 | 99 | ```ts 100 | // vite.config.ts 101 | import { defineConfig } from 'vite' 102 | import vue from '@vitejs/plugin-vue' 103 | import { webUpdateNotice } from '@plugin-web-update-notification/vite' 104 | 105 | // https://vitejs.dev/config/ 106 | export default defineConfig({ 107 | plugins: [ 108 | vue(), 109 | webUpdateNotice({ 110 | logVersion: true, 111 | }), 112 | ] 113 | }) 114 | ``` 115 | 116 | **自定义通知栏文本** 117 | 118 | ```ts 119 | // vite.config.ts 120 | export default defineConfig({ 121 | plugins: [ 122 | vue(), 123 | webUpdateNotice({ 124 | notificationProps: { 125 | title: '标题', 126 | description: 'System update, please refresh the page', 127 | buttonText: '刷新', 128 | dismissButtonText: '忽略' 129 | }, 130 | }), 131 | ] 132 | }) 133 | ``` 134 | 135 | **国际化** 136 | 137 | ```ts 138 | // vite.config.ts 139 | export default defineConfig({ 140 | plugins: [ 141 | vue(), 142 | webUpdateNotice({ 143 | // plugin preset: zh_CN | zh_TW | en_US 144 | locale: "en_US", 145 | localeData: { 146 | en_US: { 147 | title: "📢 system update", 148 | description: "System update, please refresh the page", 149 | buttonText: "refresh", 150 | dismissButtonText: "dismiss", 151 | }, 152 | zh_CN: { 153 | ... 154 | }, 155 | ... 156 | }, 157 | }), 158 | ], 159 | }); 160 | 161 | 162 | // other file to set locale 163 | window.pluginWebUpdateNotice_.setLocale('zh_CN') 164 | ``` 165 | 166 | **取消默认的通知栏,监听更新事件自定义行为** 167 | ```ts 168 | // vite.config.ts 169 | export default defineConfig({ 170 | plugins: [ 171 | vue(), 172 | webUpdateNotice({ 173 | hiddenDefaultNotification: true 174 | }), 175 | ] 176 | }) 177 | 178 | // 在其他文件中监听自定义更新事件 179 | document.body.addEventListener('plugin_web_update_notice', (e) => { 180 | const { version, options } = e.detail 181 | // write some code, show your custom notification and etc. 182 | alert('System update!') 183 | }) 184 | ``` 185 | 186 | ### Umijs 187 | 188 | 不支持 `umi2`, `umi2` 可以尝试下通过 `chainWebpack` 配置 `webpack` 插件。 189 | 190 | ```ts 191 | // .umirc.ts 192 | import { defineConfig } from 'umi' 193 | import type { Options as WebUpdateNotificationOptions } from '@plugin-web-update-notification/umijs' 194 | 195 | export default { 196 | plugins: ['@plugin-web-update-notification/umijs'], 197 | webUpdateNotification: { 198 | logVersion: true, 199 | checkInterval: 0.5 * 60 * 1000, 200 | notificationProps: { 201 | title: 'system update', 202 | description: 'System update, please refresh the page', 203 | buttonText: 'refresh', 204 | dismissButtonText: 'dismiss', 205 | }, 206 | } as WebUpdateNotificationOptions 207 | } 208 | ``` 209 | 210 | ### webpack 211 | 212 | ```js 213 | // vue.config.js(vue-cli project) 214 | const { WebUpdateNotificationPlugin } = require('@plugin-web-update-notification/webpack') 215 | const { defineConfig } = require('@vue/cli-service') 216 | 217 | module.exports = defineConfig({ 218 | // ...other config 219 | configureWebpack: { 220 | plugins: [ 221 | new WebUpdateNotificationPlugin({ 222 | logVersion: true, 223 | }), 224 | ], 225 | }, 226 | }) 227 | ``` 228 | 229 | ## webUpdateNotice Options 230 | 231 | ```ts 232 | function webUpdateNotice(options?: Options): Plugin 233 | 234 | export interface Options { 235 | /** 236 | * support 'git_commit_hash' | 'svn_revision_number' | 'pkg_version' | 'build_timestamp' | 'custom' 237 | * * if repository type is 'Git', default is 'git_commit_hash' 238 | * * if repository type is 'SVN', default is 'svn_revision_number' 239 | * * if repository type is 'unknown', default is 'build_timestamp' 240 | * */ 241 | versionType?: VersionType 242 | /** 243 | * custom version, if versionType is 'custom', this option is required 244 | */ 245 | customVersion?: string 246 | /** polling interval(ms) 247 | * if set to 0, it will not polling 248 | * @default 10 * 60 * 1000 249 | */ 250 | checkInterval?: number 251 | /** 252 | * check update when window focus 253 | * @default true 254 | */ 255 | checkOnWindowFocus?: boolean 256 | /** 257 | * check update immediately after page loaded 258 | * @default true 259 | */ 260 | checkImmediately?: boolean 261 | /** 262 | * check update when load js file error 263 | * @default true 264 | */ 265 | checkOnLoadFileError?: boolean 266 | /** 267 | * whether to output version in console 268 | * 269 | * you can also pass a function to handle the version 270 | * ```ts 271 | * logVersion: (version) => { 272 | * console.log(`version: %c${version}`, 'color: #1890ff') // this is the default behavior 273 | * } 274 | * ``` 275 | * @default true 276 | */ 277 | logVersion?: boolean | ((version: string) => void) 278 | /** 279 | * whether to silence the notification. 280 | * such as when local version is v1.0, you can set this option to true and build a new version v1.0.1, then the notification will not show 281 | */ 282 | silence?: boolean 283 | /** 284 | * @deprecated 285 | */ 286 | customNotificationHTML?: string 287 | /** notificationProps have higher priority than locale */ 288 | notificationProps?: NotificationProps 289 | notificationConfig?: NotificationConfig 290 | /** 291 | * preset: zh_CN | zh_TW | en_US 292 | * @default 'zh_CN' 293 | * */ 294 | locale?: string 295 | /** 296 | * custom locale data 297 | * @link default data: https://github.com/GreatAuk/plugin-web-update-notification/blob/main/packages/core/src/locale.ts 298 | */ 299 | localeData?: LocaleData 300 | /** 301 | * Whether to hide the default notification, if you set it to true, you need to custom behavior by yourself 302 | * ```ts 303 | document.body.addEventListener('plugin_web_update_notice', (e) => { 304 | const { version, options } = e.detail 305 | // write some code, show your custom notification and etc. 306 | alert('System update!') 307 | }) 308 | * ``` 309 | * @default false 310 | */ 311 | hiddenDefaultNotification?: boolean 312 | /** 313 | * Whether to hide the dismiss button 314 | * @default false 315 | */ 316 | hiddenDismissButton?: boolean 317 | /** 318 | * After version 1.2.0, you not need to set this option, it will be automatically detected from the base of vite config、publicPath of webpack config or publicPath of umi config 319 | * 320 | * Base public path for inject file, Valid values include: 321 | * * Absolute URL pathname, e.g. /foo/ 322 | * * Full URL, e.g. https://foo.com/ 323 | * * Empty string(default) or ./ 324 | * 325 | * !!! Don't forget / at the end of the path 326 | */ 327 | injectFileBase?: string 328 | } 329 | 330 | export type VersionType = 'git_commit_hash' | 'pkg_version' | 'build_timestamp' | 'custom' 331 | 332 | export interface NotificationConfig { 333 | /** 334 | * refresh button color 335 | * @default '#1677ff' 336 | */ 337 | primaryColor?: string 338 | /** 339 | * dismiss button color 340 | * @default 'rgba(0,0,0,.25)' 341 | */ 342 | secondaryColor?: string 343 | /** @default 'bottomRight' */ 344 | placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' 345 | } 346 | 347 | export interface NotificationProps { 348 | title?: string 349 | description?: string 350 | /** refresh button text */ 351 | buttonText?: string 352 | /** dismiss button text */ 353 | dismissButtonText?: string 354 | } 355 | 356 | export type LocaleData = Record 357 | ``` 358 | 359 | ## 曝露的方法 360 | 361 | | name | params | describe | 362 | | ----------------------------------------------- | ----------------------------------- | ------------------------------------------------------------ | 363 | | window.pluginWebUpdateNotice_.setLocale | locale(preset: zh_CN、zh_TW、en_US) | set locale | 364 | | window.pluginWebUpdateNotice_.closeNotification | | close notification | 365 | | window.pluginWebUpdateNotice_.dismissUpdate | | dismiss current update and close notification,same behavior as dismiss button | 366 | | window.pluginWebUpdateNotice_.checkUpdate | | manual check update, a function wrap by debounce(5000ms) | 367 | ```ts 368 | interface Window { 369 | pluginWebUpdateNotice_: { 370 | /** 371 | * set language. 372 | * preset: zh_CN、zh_TW、en_US 373 | */ 374 | setLocale: (locale: string) => void 375 | /** 376 | * manual check update, a function wrap by debounce(5000ms) 377 | */ 378 | checkUpdate: () => void 379 | /** dismiss current update and close notification, same behavior as dismiss the button */ 380 | dismissUpdate: () => void 381 | /** close notification */ 382 | closeNotification: () => void 383 | /** 384 | * refresh button click event, if you set it, it will cover the default event (location.reload()) 385 | */ 386 | onClickRefresh?: (version: string) => void 387 | /** 388 | * dismiss button click event, if you set it, it will cover the default event (dismissUpdate()) 389 | */ 390 | onClickDismiss?: (version: string) => void 391 | } 392 | } 393 | ``` 394 | 395 | ## 变动了哪些内容 396 | 397 | ![inject_content](https://raw.githubusercontent.com/GreatAuk/plugin-web-update-notification/main/images/inject_content.webp) 398 | 399 | ## Q&A 400 | 401 | 1. `TypeScript` 的智能提示, 如果你想使用 `window.pluginWebUpdateNotice_.` 或监听自定义更新事件。 402 | 403 | ```ts 404 | // src/shim.d.ts 405 | 406 | // if you use vite plugin 407 | /// 408 | 409 | // if you use umi plugin 410 | /// 411 | 412 | // if you use webpack plugin 413 | /// 414 | ``` 415 | 416 | 2. 请求 `version.json` 文件提示 `404 error`。 417 | 418 | 上传打包内容到 cdn 服务器: 419 | 420 | ```ts 421 | // vite.config.ts 422 | 423 | const prod = process.env.NODE_ENV === 'production' 424 | 425 | const cdnServerUrl = 'https://foo.com/' 426 | 427 | export default defineConfig({ 428 | base: prod ? cdnServerUrl : '/', 429 | plugins: [ 430 | vue(), 431 | webUpdateNotice({ 432 | injectFileBase: cdnServerUrl 433 | }) 434 | ] 435 | }) 436 | ``` 437 | 438 | 在非根目录下部署的项目: 439 | 440 | ```ts 441 | // vite.config.ts 442 | 443 | const prod = process.env.NODE_ENV === 'production' 444 | 445 | const base = '/folder/' // https://example.com/folder/ 446 | 447 | export default defineConfig({ 448 | base, 449 | plugins: [ 450 | vue(), 451 | webUpdateNotice({ 452 | injectFileBase: base 453 | }) 454 | ] 455 | }) 456 | ``` 457 | 458 | > After version 1.2.0, you not need to set this option, it will be automatically detected from the base of vite config、publicPath of webpack config or publicPath of umi config 459 | 460 | 3. 自定义 `notification` 的刷新和忽略按钮事件。 461 | 462 | ```ts 463 | // refresh button click event, if you set it, it will cover the default event (location.reload()) 464 | window.pluginWebUpdateNotice_.onClickRefresh = (version) => { alert(`click refresh btn: ${version}`) } 465 | 466 | // dismiss button click event, if you set it, it will cover the default event (dismissUpdate()) 467 | window.pluginWebUpdateNotice_.onClickDismiss = (version) => { alert(`click dismiss btn: ${version}`) } 468 | ``` 469 | 470 | 4. 自定义 notification 样式。 471 | 472 | 你可以通过更高的权重覆盖默认样式。([default css file](https://github.com/GreatAuk/plugin-web-update-notification/blob/main/packages/core/public/webUpdateNoticeInjectStyle.css)) 473 | 474 | ```html 475 | 476 | 477 |
478 |
479 |
480 |
481 | 📢 system update 482 |
483 |
484 | System update, please refresh the page 485 |
486 |
487 | dismiss 488 | 489 | refresh 490 | 491 |
492 |
493 |
494 |
495 | ``` 496 | 497 | 5. 手动检测更新 498 | 499 | ```ts 500 | // vue-router check update before each route change 501 | router.beforeEach((to, from, next) => { 502 | window.pluginWebUpdateNotice_.checkUpdate() 503 | next() 504 | }) 505 | ``` 506 | 507 | 6. 部分版本不通知。如客户版本是 `v1.0`, 你需要更新 `v1.0.1`, 但不想显示更新提示。 508 | 509 | ```ts 510 | webUpdateNotice({ 511 | ... 512 | silence: true 513 | }) 514 | ``` 515 | 516 | 517 | ## 文章 518 | * https://juejin.cn/post/7209234917288886331 519 | 520 | 521 | ## License 522 | 523 | [MIT](./LICENSE) 524 | -------------------------------------------------------------------------------- /packages/umi-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plugin-web-update-notification/umijs", 3 | "version": "2.0.0", 4 | "description": "Umi plugin for detect web page updates and notify.", 5 | "author": "Utopia", 6 | "license": "MIT", 7 | "homepage": "https://github.com/GreatAuk/plugin-web-update-notification", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/GreatAuk/plugin-web-update-notification", 11 | "directory": "packages/umi-plugin" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/GreatAuk/plugin-web-update-notification/issues" 15 | }, 16 | "keywords": [ 17 | "umi", 18 | "umi-plugin", 19 | "@plugin-web-update-notification/core", 20 | "web-update-notification" 21 | ], 22 | "sideEffects": false, 23 | "exports": { 24 | ".": { 25 | "types": "./dist/index.d.ts", 26 | "require": "./dist/index.js", 27 | "import": "./dist/index.mjs" 28 | } 29 | }, 30 | "main": "dist/index.js", 31 | "module": "dist/index.mjs", 32 | "types": "dist/index.d.ts", 33 | "files": [ 34 | "dist" 35 | ], 36 | "scripts": { 37 | "start": "tsx src/index.ts", 38 | "build": "tsup", 39 | "dev": "tsup --watch" 40 | }, 41 | "peerDependencies": { 42 | "umi": "*" 43 | }, 44 | "dependencies": { 45 | "@plugin-web-update-notification/core": "workspace:*" 46 | }, 47 | "devDependencies": { 48 | "umi": "^4.0.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/umi-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'fs' 3 | import type { IApi } from 'umi' 4 | import type { Options } from '@plugin-web-update-notification/core' 5 | import { 6 | DIRECTORY_NAME, 7 | INJECT_SCRIPT_FILE_NAME, 8 | INJECT_STYLE_FILE_NAME, 9 | JSON_FILE_NAME, 10 | NOTIFICATION_ANCHOR_CLASS_NAME, 11 | generateJSONFileContent, 12 | generateJsFileContent, 13 | getFileHash, 14 | getVersion, 15 | } from '@plugin-web-update-notification/core' 16 | import { name as pkgName } from '../package.json' 17 | 18 | export type { Options } from '@plugin-web-update-notification/core' 19 | 20 | const injectVersionTpl = (version: string) => { 21 | return `window.pluginWebUpdateNotice_version = '${version}';` 22 | } 23 | 24 | export default (api: IApi) => { 25 | api.describe({ 26 | key: 'webUpdateNotification', 27 | config: { 28 | schema(Joi) { 29 | return Joi.object({ 30 | versionType: Joi.string(), 31 | customVersion: Joi.string(), 32 | /** polling interval(ms), default 10*60*1000 */ 33 | checkInterval: Joi.number(), 34 | /** whether to output version in console */ 35 | logVersion: Joi.boolean(), 36 | checkOnWindowFocus: Joi.boolean(), 37 | checkImmediately: Joi.boolean(), 38 | checkOnLoadFileError: Joi.boolean(), 39 | injectFileBase: Joi.string(), 40 | customNotificationHTML: Joi.string(), 41 | notificationProps: { 42 | title: Joi.string(), 43 | description: Joi.string(), 44 | buttonText: Joi.string(), 45 | dismissButtonText: Joi.string(), 46 | }, 47 | notificationConfig: { 48 | primaryColor: Joi.string(), 49 | secondaryColor: Joi.string(), 50 | placement: Joi.string(), 51 | }, 52 | silence: Joi.boolean(), 53 | locale: Joi.string(), 54 | localeData: Joi.object(), 55 | hiddenDefaultNotification: Joi.boolean(), 56 | hiddenDismissButton: Joi.boolean(), 57 | }) 58 | }, 59 | }, 60 | enableBy() { 61 | return api.env === 'production' && api?.userConfig.webUpdateNotification 62 | }, 63 | }) 64 | 65 | const webUpdateNotificationOptions = (api.userConfig?.webUpdateNotification || {}) as Options 66 | if (webUpdateNotificationOptions.injectFileBase === undefined) 67 | webUpdateNotificationOptions.injectFileBase = api.userConfig.publicPath || '/' 68 | 69 | const { versionType, customNotificationHTML, hiddenDefaultNotification, injectFileBase = '/', customVersion, silence } = webUpdateNotificationOptions 70 | 71 | let version = '' 72 | if (versionType === 'custom') 73 | version = getVersion(versionType, customVersion!) 74 | else 75 | version = getVersion(versionType!) 76 | 77 | // 插件只在生产环境时生效 78 | if (!version || api.env !== 'production') 79 | return 80 | 81 | const jsFlePath = resolve('node_modules', pkgName, 'dist', `${INJECT_SCRIPT_FILE_NAME}.js`) 82 | const jsFileContent = generateJsFileContent( 83 | readFileSync(jsFlePath, 'utf8').toString(), 84 | version, 85 | webUpdateNotificationOptions, 86 | ) 87 | /** inject script file hash */ 88 | const jsFileHash = getFileHash(jsFileContent) 89 | 90 | const cssFilePath = resolve('node_modules', pkgName, 'dist', `${INJECT_STYLE_FILE_NAME}.css`) 91 | /** inject css file hash */ 92 | const cssFileHash = getFileHash(readFileSync(cssFilePath, 'utf8').toString()) 93 | 94 | api.addHTMLLinks(() => { 95 | if (customNotificationHTML || hiddenDefaultNotification) 96 | return [] 97 | 98 | return [ 99 | { 100 | rel: 'stylesheet', 101 | href: `${injectFileBase}${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css`, 102 | }, 103 | ] 104 | }) 105 | 106 | api.addHTMLHeadScripts(() => { 107 | const scriptList = [] 108 | scriptList.push({ 109 | content: injectVersionTpl(version), 110 | }) 111 | scriptList.push({ 112 | src: `${injectFileBase}${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js`, 113 | }) 114 | return scriptList 115 | }) 116 | 117 | api.onBuildComplete(() => { 118 | const outputPath = resolve(api.userConfig.outputPath || 'dist') 119 | mkdirSync(`${outputPath}/${DIRECTORY_NAME}`) 120 | 121 | // copy file from @plugin-web-update-notification/core/dist/??.css */ to dist/ 122 | copyFileSync(cssFilePath, `${outputPath}/${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css`) 123 | 124 | // write js file to dist/ 125 | writeFileSync(`${outputPath}/${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js`, jsFileContent) 126 | 127 | // write version json file to dist/ 128 | writeFileSync(`${outputPath}/${DIRECTORY_NAME}/${JSON_FILE_NAME}.json`, generateJSONFileContent(version, silence)) 129 | }) 130 | 131 | api.modifyHTML(($) => { 132 | if (!hiddenDefaultNotification) 133 | $('body').append(`
`) 134 | return $ 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /packages/umi-plugin/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'tsup' 3 | import { pkgName } from '@plugin-web-update-notification/core' 4 | 5 | export default defineConfig((options) => { // The options here is derived from CLI flags. 6 | return { 7 | entry: { 8 | index: 'src/index.ts', 9 | }, 10 | splitting: false, 11 | sourcemap: true, 12 | clean: true, 13 | dts: true, 14 | format: ['cjs', 'esm'], 15 | minify: !options.watch, 16 | onSuccess: `node ${resolve('node_modules', pkgName, 'dist', 'pluginBuildScript.js')}`, 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /packages/vite-plugin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/vite-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plugin-web-update-notification/vite", 3 | "type": "module", 4 | "version": "2.0.0", 5 | "description": "Vite plugin for detect web page updates and notify.", 6 | "author": "Utopia", 7 | "license": "MIT", 8 | "homepage": "https://github.com/GreatAuk/plugin-web-update-notification", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/GreatAuk/plugin-web-update-notification", 12 | "directory": "packages/vite-plugin" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/GreatAuk/plugin-web-update-notification/issues" 16 | }, 17 | "keywords": [ 18 | "vite", 19 | "vite-plugin", 20 | "@plugin-web-update-notification/vite", 21 | "web-update-notification" 22 | ], 23 | "sideEffects": false, 24 | "exports": { 25 | ".": { 26 | "types": "./dist/index.d.ts", 27 | "import": "./dist/index.js", 28 | "default": "./dist/index.js" 29 | } 30 | }, 31 | "main": "dist/index.js", 32 | "module": "dist/index.js", 33 | "types": "dist/index.d.ts", 34 | "files": [ 35 | "dist" 36 | ], 37 | "scripts": { 38 | "start": "tsx src/index.ts", 39 | "dev": "tsup --watch", 40 | "build": "tsup" 41 | }, 42 | "peerDependencies": { 43 | "vite": "^6.0.0 || ^5.0.0 || >4.0.0 || ^3.0.0" 44 | }, 45 | "dependencies": { 46 | "@plugin-web-update-notification/core": "workspace:*" 47 | }, 48 | "devDependencies": { 49 | "vite": "^5.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/vite-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { resolve } from 'path' 3 | import type { Plugin, ResolvedConfig } from 'vite' 4 | import type { Options } from '@plugin-web-update-notification/core' 5 | 6 | import { 7 | DIRECTORY_NAME, 8 | INJECT_SCRIPT_FILE_NAME, 9 | INJECT_STYLE_FILE_NAME, 10 | JSON_FILE_NAME, 11 | NOTIFICATION_ANCHOR_CLASS_NAME, 12 | generateJSONFileContent, 13 | generateJsFileContent, 14 | getFileHash, 15 | getVersion, 16 | get__Dirname, 17 | } from '@plugin-web-update-notification/core' 18 | 19 | // /** 20 | // * Get the version of the current Vite 21 | // * 22 | // * if the viteVersion is undefined, we assume that vite is less than v3.0(after v3.0, vite export version) 23 | // */ 24 | // async function getViteVersion(): Promise { 25 | // return await import('vite').then(({ version }) => version) 26 | // } 27 | 28 | /** 29 | * It injects the hash into the HTML, and injects the notification anchor and the stylesheet and the 30 | * script into the HTML 31 | * @param {string} html - The original HTML of the page 32 | * @param {string} version - The hash of the current commit 33 | * @param {Options} options - Options 34 | * @returns The html of the page with the injected script and css. 35 | */ 36 | function injectPluginHtml( 37 | html: string, 38 | version: string, 39 | options: Options, 40 | { cssFileHash, jsFileHash }: { jsFileHash: string; cssFileHash: string }, 41 | ) { 42 | const { customNotificationHTML, hiddenDefaultNotification, injectFileBase = '' } = options 43 | 44 | const versionScript = `` 45 | const cssLinkHtml = customNotificationHTML || hiddenDefaultNotification ? '' : `` 46 | let res = html 47 | 48 | res = res.replace( 49 | '', 50 | ` 51 | ${cssLinkHtml} 52 | 53 | ${versionScript}`, 54 | ) 55 | 56 | if (!hiddenDefaultNotification) { 57 | res = res.replace( 58 | '', 59 | `
`, 60 | ) 61 | } 62 | 63 | return res 64 | } 65 | 66 | export function webUpdateNotice(options: Options = {}): Plugin { 67 | let viteConfig: ResolvedConfig 68 | // let viteVersion: string | undefined 69 | 70 | const { versionType, customVersion, silence } = options 71 | let version = '' 72 | if (versionType === 'custom') 73 | version = getVersion(versionType, customVersion!) 74 | else 75 | version = getVersion(versionType!) 76 | 77 | /** inject script file hash */ 78 | let jsFileHash = '' 79 | /** inject css file hash */ 80 | let cssFileHash = '' 81 | 82 | const cssFileSource = readFileSync(`${resolve(get__Dirname(), INJECT_STYLE_FILE_NAME)}.css`, 'utf8').toString() 83 | cssFileHash = getFileHash(cssFileSource) 84 | 85 | let jsFileSource = '' 86 | 87 | return { 88 | name: 'vue-vite-web-update-notice', 89 | apply: 'build', 90 | enforce: 'post', 91 | async configResolved(resolvedConfig: ResolvedConfig) { 92 | // 存储最终解析的配置 93 | viteConfig = resolvedConfig 94 | if (options.injectFileBase === undefined) 95 | options.injectFileBase = viteConfig.base 96 | 97 | jsFileSource = generateJsFileContent( 98 | readFileSync(`${resolve(get__Dirname(), INJECT_SCRIPT_FILE_NAME)}.js`, 'utf8').toString(), 99 | version, 100 | options, 101 | ) 102 | jsFileHash = getFileHash(jsFileSource) 103 | 104 | // viteVersion = await getViteVersion() 105 | }, 106 | generateBundle(_, bundle = {}) { 107 | if (!version) 108 | return 109 | // inject version json file 110 | bundle[JSON_FILE_NAME] = { 111 | // @ts-expect-error: for Vite 3 support, Vite 4 has removed `isAsset` property 112 | isAsset: true, 113 | type: 'asset', 114 | name: undefined, 115 | source: generateJSONFileContent(version, silence), 116 | fileName: `${DIRECTORY_NAME}/${JSON_FILE_NAME}.json`, 117 | } 118 | 119 | // inject css file 120 | bundle[INJECT_STYLE_FILE_NAME] = { 121 | // @ts-expect-error: for Vite 3 support, Vite 4 has removed `isAsset` property 122 | isAsset: true, 123 | type: 'asset', 124 | name: undefined, 125 | source: cssFileSource, 126 | fileName: `${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css`, 127 | } 128 | 129 | // inject js file 130 | bundle[INJECT_SCRIPT_FILE_NAME] = { 131 | // @ts-expect-error: for Vite 3 support, Vite 4 has removed `isAsset` property 132 | isAsset: true, 133 | type: 'asset', 134 | name: undefined, 135 | source: jsFileSource, 136 | fileName: `${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js`, 137 | } 138 | }, 139 | transformIndexHtml: 140 | // if the viteVersion is undefined, we assume that vite is less than v3.0(after v3.0, vite export version) 141 | // viteVersion === undefined 142 | // ? { 143 | 144 | { 145 | order: 'post', 146 | handler(html: string, { chunk }) { 147 | if (version && chunk) 148 | return injectPluginHtml(html, version, options, { jsFileHash, cssFileHash }) 149 | return html 150 | }, 151 | enforce: 'post', // deprecated since Vite 4 152 | async transform(html: string) { // deprecated since Vite 4 153 | if (version) 154 | return injectPluginHtml(html, version, options, { jsFileHash, cssFileHash }) 155 | return html 156 | }, 157 | }, 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /packages/vite-plugin/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'tsup' 3 | import { pkgName } from '@plugin-web-update-notification/core' 4 | 5 | export default defineConfig((options) => { // The options here is derived from CLI flags. 6 | return { 7 | entry: { 8 | index: 'src/index.ts', 9 | }, 10 | splitting: false, 11 | sourcemap: true, 12 | clean: true, 13 | dts: true, 14 | format: ['esm'], 15 | minify: !options.watch, 16 | // after bundle success, run script, copy inject file from @plugin-web-update-notification/core 17 | onSuccess: `node ${resolve('node_modules', pkgName, 'dist', 'pluginBuildScript.js')}`, 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /packages/webpack-plugin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plugin-web-update-notification/webpack", 3 | "type": "module", 4 | "version": "2.0.0", 5 | "description": "Webpack plugin for detect web page updates and notify.", 6 | "author": "Utopia", 7 | "license": "MIT", 8 | "homepage": "https://github.com/GreatAuk/plugin-web-update-notification", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/GreatAuk/plugin-web-update-notification", 12 | "directory": "packages/vite-plugin" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/GreatAuk/plugin-web-update-notification/issues" 16 | }, 17 | "keywords": [ 18 | "webpack", 19 | "webpack-plugin", 20 | "@plugin-web-update-notification/webpack", 21 | "web-update-notification" 22 | ], 23 | "sideEffects": false, 24 | "exports": { 25 | ".": { 26 | "types": "./dist/index.d.ts", 27 | "require": "./dist/index.cjs", 28 | "import": "./dist/index.js" 29 | } 30 | }, 31 | "main": "dist/index.cjs", 32 | "module": "dist/index.js", 33 | "types": "dist/index.d.ts", 34 | "files": [ 35 | "dist" 36 | ], 37 | "scripts": { 38 | "start": "tsx src/index.ts", 39 | "dev": "tsup --watch", 40 | "build": "tsup" 41 | }, 42 | "peerDependencies": { 43 | "webpack": "^4.0.0 || ^5.0.0" 44 | }, 45 | "dependencies": { 46 | "@plugin-web-update-notification/core": "workspace:*" 47 | }, 48 | "devDependencies": { 49 | "webpack": "^5.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/webpack-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { accessSync, constants, readFileSync, writeFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import type { Options } from '@plugin-web-update-notification/core' 5 | import { 6 | DIRECTORY_NAME, 7 | INJECT_SCRIPT_FILE_NAME, 8 | INJECT_STYLE_FILE_NAME, 9 | JSON_FILE_NAME, 10 | NOTIFICATION_ANCHOR_CLASS_NAME, 11 | generateJSONFileContent, 12 | generateJsFileContent, 13 | getFileHash, 14 | getVersion, 15 | get__Dirname, 16 | } from '@plugin-web-update-notification/core' 17 | import type { Compilation, Compiler } from 'webpack' 18 | 19 | const pluginName = 'WebUpdateNotificationPlugin' 20 | 21 | type PluginOptions = Options & { 22 | /** index.html file path, by default, we will look up path.resolve(webpackOutputPath, './index.html') */ 23 | indexHtmlFilePath?: string 24 | } 25 | 26 | /** 27 | * It injects the hash into the HTML, and injects the notification anchor and the stylesheet and the 28 | * script into the HTML 29 | * @param {string} html - The original HTML of the page 30 | * @param {string} version - The hash of the current commit 31 | * @param {Options} options - Options 32 | * @returns The html of the page with the injected script and css. 33 | */ 34 | function injectPluginHtml( 35 | html: string, 36 | version: string, 37 | options: Options, 38 | { cssFileHash, jsFileHash }: { jsFileHash: string; cssFileHash: string }, 39 | ) { 40 | const { customNotificationHTML, hiddenDefaultNotification, injectFileBase = '/' } = options 41 | 42 | const versionScript = `` 43 | const cssLinkHtml = customNotificationHTML || hiddenDefaultNotification ? '' : `` 44 | let res = html 45 | 46 | res = res.replace( 47 | '', 48 | ` 49 | ${cssLinkHtml} 50 | 51 | 52 | ${versionScript}`, 53 | ) 54 | 55 | if (!hiddenDefaultNotification) { 56 | res = res.replace( 57 | '', 58 | `
`, 59 | ) 60 | } 61 | 62 | return res 63 | } 64 | 65 | class WebUpdateNotificationPlugin { 66 | options: PluginOptions 67 | constructor(options: PluginOptions) { 68 | this.options = options || {} 69 | } 70 | 71 | apply(compiler: Compiler) { 72 | /** inject script file hash */ 73 | let jsFileHash = '' 74 | /** inject css file hash */ 75 | let cssFileHash = '' 76 | 77 | const { publicPath } = compiler.options.output 78 | if (this.options.injectFileBase === undefined) 79 | this.options.injectFileBase = typeof publicPath === 'string' ? publicPath : '/' 80 | 81 | const { hiddenDefaultNotification, versionType, indexHtmlFilePath, customVersion, silence } = this.options 82 | let version = '' 83 | if (versionType === 'custom') 84 | version = getVersion(versionType, customVersion!) 85 | else 86 | version = getVersion(versionType!) 87 | 88 | compiler.hooks.emit.tap(pluginName, (compilation: Compilation) => { 89 | // const outputPath = compiler.outputPath 90 | const jsonFileContent = generateJSONFileContent(version, silence) 91 | // @ts-expect-error 92 | compilation.assets[`${DIRECTORY_NAME}/${JSON_FILE_NAME}.json`] = { 93 | source: () => jsonFileContent, 94 | size: () => jsonFileContent.length, 95 | } 96 | if (!hiddenDefaultNotification) { 97 | const injectStyleContent = readFileSync(`${get__Dirname()}/${INJECT_STYLE_FILE_NAME}.css`, 'utf8') 98 | cssFileHash = getFileHash(injectStyleContent) 99 | 100 | // @ts-expect-error 101 | compilation.assets[`${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css`] = { 102 | source: () => injectStyleContent, 103 | size: () => injectStyleContent.length, 104 | } 105 | } 106 | 107 | const filePath = resolve(`${get__Dirname()}/${INJECT_SCRIPT_FILE_NAME}.js`) 108 | const injectScriptContent = generateJsFileContent( 109 | readFileSync(filePath, 'utf8').toString(), 110 | version, 111 | this.options, 112 | ) 113 | jsFileHash = getFileHash(injectScriptContent) 114 | 115 | // @ts-expect-error 116 | compilation.assets[`${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js`] = { 117 | source: () => injectScriptContent, 118 | size: () => injectScriptContent.length, 119 | } 120 | }) 121 | 122 | compiler.hooks.afterEmit.tap(pluginName, () => { 123 | const htmlFilePath = resolve(compiler.outputPath, indexHtmlFilePath || './index.html') 124 | try { 125 | accessSync(htmlFilePath, constants.F_OK) 126 | 127 | let html = readFileSync(htmlFilePath, 'utf8') 128 | html = injectPluginHtml( 129 | html, 130 | version, 131 | this.options, 132 | { 133 | jsFileHash, 134 | cssFileHash, 135 | }, 136 | ) 137 | writeFileSync(htmlFilePath, html) 138 | } 139 | catch (error) { 140 | console.error(error) 141 | console.error(`${pluginName} failed to inject the plugin into the HTML file. index.html(${htmlFilePath}) not found.`) 142 | } 143 | }) 144 | } 145 | } 146 | 147 | export { WebUpdateNotificationPlugin } 148 | -------------------------------------------------------------------------------- /packages/webpack-plugin/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'tsup' 3 | import { pkgName } from '@plugin-web-update-notification/core' 4 | 5 | export default defineConfig((options) => { // The options here is derived from CLI flags. 6 | return { 7 | entry: { 8 | index: 'src/index.ts', 9 | }, 10 | splitting: false, 11 | sourcemap: true, 12 | clean: true, 13 | dts: true, 14 | format: ['cjs', 'esm'], 15 | minify: !options.watch, 16 | // after bundle success, run script, copy inject file from @plugin-web-update-notification/core 17 | onSuccess: `node ${resolve('node_modules', pkgName, 'dist', 'pluginBuildScript.js')}`, 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - example/* 3 | - packages/* 4 | -------------------------------------------------------------------------------- /scripts/copyFile.ts: -------------------------------------------------------------------------------- 1 | import { copyFileSync } from 'fs' 2 | import { resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | // relative to scripts directory 6 | const destinations = [ 7 | ['../LICENSE', '../packages/core/LICENSE'], 8 | ['../README.md', '../packages/core/README.md'], 9 | ['../README.zh-CN.md', '../packages/core/README.zh-CN.md'], 10 | ['../LICENSE', '../packages/vite-plugin/LICENSE'], 11 | ['../README.md', '../packages/vite-plugin/README.md'], 12 | ['../README.zh-CN.md', '../packages/vite-plugin/README.zh-CN.md'], 13 | ['../LICENSE', '../packages/umi-plugin/LICENSE'], 14 | ['../README.md', '../packages/umi-plugin/README.md'], 15 | ['../README.zh-CN.md', '../packages/umi-plugin/README.zh-CN.md'], 16 | ['../LICENSE', '../packages/webpack-plugin/LICENSE'], 17 | ['../README.md', '../packages/webpack-plugin/README.md'], 18 | ['../README.zh-CN.md', '../packages/webpack-plugin/README.zh-CN.md'], 19 | ] 20 | 21 | const _filename = import.meta.url ? fileURLToPath(import.meta.url) : __filename 22 | 23 | destinations.forEach(([src, dest]) => { 24 | copyFileSync(resolve(_filename, '..', src), resolve(_filename, '..', dest)) 25 | }) 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "esnext", 5 | "lib": ["esnext", "DOM"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "skipDefaultLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { configDefaults, defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | exclude: [...configDefaults.exclude, 'example/**'], 6 | }, 7 | }) 8 | --------------------------------------------------------------------------------