├── .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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 | 
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 |
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 |
12 | Hello Vite + React!
13 |
14 | setCount((count) => count + 1)}>
15 | count is: {count}
16 |
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 |
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 |
9 | Clicks: {count}
10 |
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 |
12 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/example/vue-cli/src/App.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation .
8 |
9 |
Installed CLI Plugins
10 |
14 |
Essential Links
15 |
22 |
Ecosystem
23 |
30 |
31 |
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 |
15 |
34 |
35 |
36 |
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 |
8 |
9 |
{{ msg }}
10 |
11 | You’ve successfully created a project with
12 | Vite +
13 | Vue 3 . What's next?
14 |
15 |
16 |
17 |
18 |
41 |
--------------------------------------------------------------------------------
/example/vue-vite3/src/components/TheWelcome.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Documentation
16 |
17 | Vue’s
18 | official documentation
19 | provides you with all information you need to get started.
20 |
21 |
22 |
23 |
24 |
25 |
26 | Tooling
27 |
28 | This project is served and bundled with
29 | Vite . The recommended IDE
30 | setup is VSCode +
31 | Volar . If you need to test
32 | your components and web pages, check out
33 | Cypress and
34 | Cypress Component Testing .
37 |
38 |
39 |
40 | More instructions are available in README.md
.
41 |
42 |
43 |
44 |
45 |
46 |
47 | Ecosystem
48 |
49 | Get official tools and libraries for your project:
50 | Pinia ,
51 | Vue Router ,
52 | Vue Test Utils , and
53 | Vue Dev Tools . If you need more
54 | resources, we suggest paying
55 | Awesome Vue
56 | a visit.
57 |
58 |
59 |
60 |
61 |
62 |
63 | Community
64 |
65 | Got stuck? Ask your question on
66 | Vue Land , our official Discord server, or
67 | StackOverflow .
68 | You should also subscribe to
69 | our mailing list and follow the official
70 | @vuejs
71 | twitter account for latest news in the Vue world.
72 |
73 |
74 |
75 |
76 |
77 |
78 | Support Vue
79 |
80 | As an independent project, Vue relies on community backing for its sustainability. You can help
81 | us by
82 | becoming a sponsor .
83 |
84 |
85 |
--------------------------------------------------------------------------------
/example/vue-vite3/src/components/WelcomeItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite3/src/components/icons/IconDocumentation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite3/src/components/icons/IconEcosystem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite3/src/components/icons/IconSupport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite3/src/components/icons/IconTooling.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
18 |
19 |
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 |
2 |
3 |
This is an about page
4 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/example/vue-vite3/src/views/HomeView.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
8 |
9 |
{{ msg }}
10 |
11 | You’ve successfully created a project with
12 | Vite +
13 | Vue 3 .
14 |
15 |
16 |
17 |
18 |
42 |
--------------------------------------------------------------------------------
/example/vue-vite5/src/components/TheWelcome.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Documentation
16 |
17 | Vue’s
18 | official documentation
19 | provides you with all information you need to get started.
20 |
21 |
22 |
23 |
24 |
25 |
26 | Tooling
27 |
28 | This project is served and bundled with
29 | Vite . The
30 | recommended IDE setup is
31 | VSCode +
32 | Volar . If
33 | you need to test your components and web pages, check out
34 | Cypress and
35 | Cypress Component Testing .
38 |
39 |
40 |
41 | More instructions are available in README.md
.
42 |
43 |
44 |
45 |
46 |
47 |
48 | Ecosystem
49 |
50 | Get official tools and libraries for your project:
51 | Pinia ,
52 | Vue Router ,
53 | Vue Test Utils , and
54 | Vue Dev Tools . If
55 | you need more resources, we suggest paying
56 | Awesome Vue
57 | a visit.
58 |
59 |
60 |
61 |
62 |
63 |
64 | Community
65 |
66 | Got stuck? Ask your question on
67 | Vue Land , our official
68 | Discord server, or
69 | StackOverflow . You should also subscribe to
72 | our mailing list and follow
73 | the official
74 | @vuejs
75 | twitter account for latest news in the Vue world.
76 |
77 |
78 |
79 |
80 |
81 |
82 | Support Vue
83 |
84 | As an independent project, Vue relies on community backing for its sustainability. You can help
85 | us by
86 | becoming a sponsor .
87 |
88 |
89 |
--------------------------------------------------------------------------------
/example/vue-vite5/src/components/WelcomeItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
88 |
--------------------------------------------------------------------------------
/example/vue-vite5/src/components/icons/IconCommunity.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite5/src/components/icons/IconDocumentation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite5/src/components/icons/IconEcosystem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite5/src/components/icons/IconSupport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/vue-vite5/src/components/icons/IconTooling.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
18 |
19 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 | 
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 |
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 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 | 
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 |
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 |
--------------------------------------------------------------------------------