Repository: guMcrey/version-rocket Branch: main Commit: ea358a871636 Files: 25 Total size: 233.3 KB Directory structure: gitextract_zilyrdgy/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.en-US.md ├── CHANGELOG.zh-CN.md ├── LICENSE.md ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── components/ │ ├── images.d.ts │ ├── version-tip-dialog.css │ ├── versionTipDialog.ts │ └── versionTipTheme.ts ├── index.ts ├── jest.config.ts ├── package.json ├── scripts/ │ ├── createVersionFile.js │ ├── sendMessageToLark.js │ └── sendMessageToWeCom.js ├── tests/ │ ├── index.mock.ts │ └── index.test.ts ├── tsconfig.json └── utils/ └── index.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true max_line_length = 80 trim_trailing_whitespace = true [*.md] max_line_length = 0 trim_trailing_whitespace = false [COMMIT_EDITMSG] max_line_length = 0 ================================================ FILE: .gitattributes ================================================ *.js eol=lf *.json eol=lf *.ts eol=lf ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: push: branches: ['main'] pull_request: branches: ['main'] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - name: Run a multi-line script run: | npm ci npm run build npm run test:coverage # npm run codecov - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ .history node_modules coverage components/*.js components/version*.d.ts utils/*.js utils/*.d.ts index.js index.d.ts *.tgz ================================================ FILE: .npmignore ================================================ .github .history tests assets/*.jpg assets/*.gif .editorconfig .gitattributes .gitignore CHANGELOG.zh-CN.md coverage tsconfig.json jest.config.ts ================================================ FILE: CHANGELOG.en-US.md ================================================ #### Release cycle - Revision number: hotfix fixed - Minor Version Number: Releases a backward compatible version with new features - Major version number: Contains breaking updates and new features, not in the release cycle --- ## 1.7.4 `2024-10-15` [#52](https://github.com/guMcrey/version-rocket/issues/52) (Thanks to [wjp980108](https://github.com/wjp980108) for the feedback) - 🪲 Fix the `config.check-origin-specified-files-url` ## 1.7.3 `2024-09-19` [#50](https://github.com/guMcrey/version-rocket/issues/50) (Thanks to [Jolie](https://github.com/newives) for the feedback) - 🪲 Fix the syntax error of the expression `[...new Set(config.checkOriginSpecifiedFilesUrl)] || []` in TypeScript. ## 1.7.2 `2024-08-09` [#43](https://github.com/guMcrey/version-rocket/issues/43) (Thanks to [Banana-energy](https://github.com/Banana-energy) for the suggestion) - 🪲 Fixed the issue where the worker value was not cleared when `unCheckVersion({closeWorker: true})` was called, causing it to not work properly when reassigned. ## 1.7.1 `2023-11-02` [#35](https://github.com/guMcrey/version-rocket/issues/35) (Thank you for the suggestion, [fickleness-youth](https://github.com/fickleness-youth)) - 💄 Optimization: The fetch request parameter for real-time checking of updates in a specified file address's content can be enhanced by adding `{method: "HEAD", cache: "no-cache"}`. This configuration instructs the server to only return the response headers without the actual response body, allowing for faster retrieval of ETag or Last-Modified information. ## 1.7.0 `2023-07-03` - 🆕 Another method for real-time detection of web application version: By leveraging the browser's cache negotiation mechanism, it checks whether the content of the specified file address has been updated to determine if a new version is available. - Added `checkOriginSpecifiedFilesUrl` configuration option: After setting this property, the version will be monitored by "checking whether the specified file has been updated" instead of "managing the version number". Pass in a list of file addresses to be monitored, usually the index.html file under a domain name (string array type). - Added `checkOriginSpecifiedFilesUrlMode` configuration option: Supports two modes: 'one' / 'all'. 'one' means that if the content of one of the file addresses in the list changes, it will prompt for an update; 'all' means that it will prompt for an update only when the content of all file addresses in the list changes. (Only effective when checkOriginSpecifiedFilesUrl is configured) - Added `enable` configuration option: Whether to enable version monitoring. With this configuration option, version monitoring can be enabled only in specified environments (default is true). - Added `clearIntervalOnDialog` configuration option: Whether to clear the timer when the new version prompt dialog appears. - 💄 update README.md and README.zh-CN.md ## 1.6.7 `2023-06-06` - 💄 update .npmignore file list ## 1.6.6 `2023-06-06` - 🪲 fix .npmignore file filtering out jest.config.ts file ## 1.6.5 `2023-06-06` - 🪲 fix .npmignore file filtering out utils folder ## 1.6.3 `2023-06-05` - 💄 optimize the size of npm packages, and filter files that do not need to be packaged through .npmignore files ## 1.6.2 `2023-02-13` - 🆕 lark-message-config added `headerBgColor` variable, support set card header's background color, default is turquoise. available values: blue、wathet、turquoise(default)、green、yellow、orange、red、carmine、violet、purple、indigo、grey - 💄 update README.md and README.zh-CN.md ## 1.6.1 `2023-02-05` [#22](https://github.com/guMcrey/version-rocket/issues/22) - 🆕 generate-version-file added the `EXTERNAL_PATH` environment variable, which supports passing in the path of a file. It is recommended to use it when a large amount of additional information needs to be written into `version.json`. When `EXTERNAL` and `EXTERNAL_PATH` are set at the same time, the priority is lower than that of `EXTERNAL` - 💄 update README.md and README.zh-CN.md ## 1.6.0 `2023-02-04` [#22](https://github.com/guMcrey/version-rocket/issues/22) - 🆕 generate-version-file add EXTERNAL env,can be used to display richer content when customizing the popup UI. Such as current version updates or other information - 💄 update README.md and README.zh-CN.md ## 1.5.0 `2023-01-17` [#15](https://github.com/guMcrey/version-rocket/issues/15) - 🆕 the checkVersion method adds immediate to support immediate version monitoring when visiting the home page, and then polling at custom longer intervals (reducing the number of requests to the server) - 🆕 new version monitoring pop-up window allows closing function: cancelButtonText is used to customize button text; CancelMode enumerates the frequency of the next update after canceling the update; CancelUpdateAndStopWorker is used to set whether to close the worker at the same time when canceling the update - 🆕 new callback functions for version monitoring: onRefresh for confirming refresh; onCancel for canceling the callback after refresh - 💄 update README.md and README.zh-CN.md API ## 1.4.0 `2022-12-03` - 🆕 add unCheckVersion function, to support that real-time monitor of the version to be terminated when need. [#15](https://github.com/guMcrey/version-rocket/issues/15) - 🪲 fix checkVersion function be called multiple, will create multiple worker processes. - 💄 update README.md and README.zh-CN.md ## 1.3.2 `2022-11-08` - 🪲 fix setDeployInfoInMainCard is true in send-lark-message.config, remark no display [#12](https://github.com/guMcrey/version-rocket/issues/12) ## 1.3.1 `2022-11-07` - 💄 send-lark-message.config add new field setDeployInfoInMainCard to support deploy info display in main card [#12](https://github.com/guMcrey/version-rocket/issues/12) ## 1.3.0 `2022-08-21` - 🆕 Supports sending deployment messages to enterprise WeChat bots [#8](https://github.com/guMcrey/version-rocket/issues/8) - 💄 Update README.md and README.zh-CN.md documentation ## 1.2.4 `2022-07-28` - 🆕 Send lark message supports incoming runtime fields [#3](https://github.com/guMcrey/version-rocket/issues/3) ## 1.2.1 `2022-07-15` - 🆕 Support custom version update pop-up theme [#1](https://github.com/guMcrey/version-rocket/issues/1) ================================================ FILE: CHANGELOG.zh-CN.md ================================================ #### 发布周期 - 修订版本号:hotfix 修复 - 次版本号:发布带有新特性的向下兼容的版本 - 主版本号:含有破坏性更新和新特性,不在发布周期内 --- ## 1.7.4 `2024-10-15` [#52](https://github.com/guMcrey/version-rocket/issues/52) (感谢 [wjp980108](https://github.com/wjp980108) 同学的反馈) - 🪲 Fix the warning message when the `config.check-origin-specified-files-url` field is not provided. ## 1.7.3 `2024-09-19` [#50](https://github.com/guMcrey/version-rocket/issues/50) (感谢 [Jolie](https://github.com/newives) 同学的反馈) - 🪲 修复 `[...new Set(config.checkOriginSpecifiedFilesUrl)] || []` 表达式在 Typescript 中的语法错误 ## 1.7.2 `2024-08-09` [#43](https://github.com/guMcrey/version-rocket/issues/43) (感谢 [Banana-energy](https://github.com/Banana-energy) 同学的建议) - 🪲 修复 `unCheckVersion({closeWorker: true})` 时 worker 值未被清空导致再次赋值时不生效的问题 ## 1.7.1 `2023-11-02` [#35](https://github.com/guMcrey/version-rocket/issues/35) (感谢 [fickleness-youth](https://github.com/fickleness-youth) 同学的建议) - 💄 优化: Web 应用版本实时检测方法**指定文件地址内容是否有更新**的 fetch 请求参数, 通过添加 `{method: "HEAD", cache: "no-cache"}` 来让服务器将只返回响应头信息而不返回实际的响应体,因此可以更快地获取到 ETag 或 Last-Modified 等响应头信息 ## 1.7.0 `2023-07-03` - 🆕 支持 Web 应用版本实时检测的另一种方法: 通过浏览器的协商缓存原理, 检测**指定文件地址内容是否有更新**来判断是否有新版本可用 - 新增 `checkOriginSpecifiedFilesUrl` 配置项: 设置该属性后将使用 “通过检测指定文件是否有更新” 而不是 “通过管理版本号” 来监测版本, 传入希望监测的文件地址列表, 通常情况为某个域名下的 index.html 文件 (字符串数组类型) - 新增 `checkOriginSpecifiedFilesUrlMode` 配置项: 支持两种模式 'one' / 'all'. 'one' 表示列表中文件地址只要有一个内容发生改变即提示更新; 'all' 表示列表中文件地址都发生改变时才提示更新. (当 checkOriginSpecifiedFilesUrl 配置后才生效 - 新增 `enable` 配置项: 是否启用版本监测, 通过该配置项可以设置版本监测只在指定环境下开启 (默认 true) - 新增 `clearIntervalOnDialog` 配置项: 当发现新版本提示弹窗出现后, 是否清空定时器 - 💄 更新 README.md 和 README.zh-CN.md 文档 ## 1.6.7 `2023-06-06` - 💄 更新 .npmignore 文件列表 ## 1.6.6 `2023-06-06` - 🪲 修复 .npmignore 文件过滤掉了 jest.config.ts 文件 ## 1.6.5 `2023-06-06` - 🪲 修复 .npmignore 文件过滤掉了 utils 文件夹 ## 1.6.3 `2023-06-05` - 💄 优化 npm 包体积, 通过 .npmignore 文件过滤不需要打包的文件 ## 1.6.2 `2023-02-13` - 🆕 lark-message-config 新增 `headerBgColor` 变量,支持自定义消息卡片头部背景色, 以方便区分部署成功或失败的消息推送. 取值范围: blue、wathet、turquoise(默认)、green、yellow、orange、red、carmine、violet、purple、indigo、grey - 💄 更新 README.md 和 README.zh-CN.md 文档 ## 1.6.1 `2023-02-05` [#22](https://github.com/guMcrey/version-rocket/issues/22) - 🆕 generate-version-file 新增 `EXTERNAL_PATH` 环境变量,支持传入文本文件路径,推荐在需要将大量额外信息写入 `version.json` 中时使用. 当同时设置了 `EXTERNAL` 和 `EXTERNAL_PATH` 时,优先级低于 `EXTERNAL`。 - 💄 更新 README.md 和 README.zh-CN.md 文档 ## 1.6.0 `2023-02-04` [#22](https://github.com/guMcrey/version-rocket/issues/22) - 🆕 generate-version-file 新增 `EXTERNAL` 环境变量,可用于在自定义弹窗 UI 时展示更丰富内容。如当前版本更新内容或其他信息 - 💄 更新 README.md 和 README.zh-CN.md 文档 ## 1.5.0 `2023-01-17` [#15](https://github.com/guMcrey/version-rocket/issues/15) - 🆕 checkVersion 方法新增 immediate, 以支持首页访问时, 立即触发版本监测, 之后按自定义的较长时间间隔轮询 (减少请求服务器次数) - 🆕 新增版本监测弹窗允许关闭功能: cancelButtonText 用于自定义按钮文案; cancelMode 枚举取消更新后, 下一次更新的频率; cancelUpdateAndStopWorker 用于设置是否在取消更新时同时关闭 worker - 🆕 新增版本监测回调函数: onRefresh 用于确认刷新后的回调; onCancel 用于取消刷新后的回调 - 💄 更新 README.md 和 README.zh-CN.md 文档 API 部分 ## 1.4.0 `2022-12-03` - 🆕 新增 unCheckVersion 函数, 以支持在需要时终止版本实时监测 [#15](https://github.com/guMcrey/version-rocket/issues/15) - 🪲 修复 checkVersion 函数在重复调用时, 创建多个 worker 进程 - 💄 更新 README.md 和 README.zh-CN.md 文档 ## 1.3.2 `2022-11-08` - 🪲 修复 send-lark-message.config 中 setDeployInfoInMainCard 为真时, remark 未显示 [#12](https://github.com/guMcrey/version-rocket/issues/12) ## 1.3.1 `2022-11-07` - 💄 send-lark-message.config 新增字段 setDeployInfoInMainCard 以支持 Deploy 信息显示在主卡片中 [#12](https://github.com/guMcrey/version-rocket/issues/12) ## 1.3.0 `2022-08-21` - 🆕 支持发送部署消息至企业微信机器人 [#8](https://github.com/guMcrey/version-rocket/issues/8) - 💄 更新 README.md 和 README.zh-CN.md 文档 ## 1.2.4 `2022-07-28` - 🆕 发送 lark 消息支持传入运行时字段 [#3](https://github.com/guMcrey/version-rocket/issues/3) ## 1.2.1 `2022-07-15` - 🆕 支持自定义版本更新弹窗主题 [#1](https://github.com/guMcrey/version-rocket/issues/1) ================================================ FILE: LICENSE.md ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # 🔔 version-rocket 🚀 English | [简体中文](./README.zh-CN.md) > A tool library for web application version detection and deployment notification. [![](https://img.shields.io/npm/v/version-rocket)](https://www.npmjs.com/package/version-rocket) [![](https://img.shields.io/npm/dm/version-rocket.svg)](https://npmcharts.com/compare/version-rocket?minimal=true) [![](https://codecov.io/gh/guMcrey/version-rocket/main/graph/badge.svg)](https://codecov.io/gh/guMcrey/version-rocket) [![](https://github.com/guMcrey/version-rocket/actions/workflows/main.yml/badge.svg)](https://github.com/guMcrey/version-rocket/actions/) [![](https://img.shields.io/npm/l/version-rocket)](https://www.npmjs.com/package/version-rocket) **Catalogue** - [About](#about) - [Features](#features) - [Implementation Principle](#implementation-principle) - [Install](#install) - [Quick Start](#quick-start) - [Web application version real-time detection](#web-application-version-real-time-detection) - [Personalize the theme](#personalize-the-theme) - [Screenshot](#screenshot) - [Automatically send deployment messages to Lark or Wecom group chat](#automatically-send-deployment-messages-to-lark-or-wecom-group-chat) - [Lark](#lark) - [Set dynamic text](#set-dynamic-text) - [Custom message card](#custom-message-card) - [Screenshot](#screenshot-1) - [WeCom](#wecom) - [Set dynamic text](#set-dynamic-text-1) - [Custom message card](#custom-message-card-1) - [Screenshot](#screenshot-2) - [API](#api) - [Test](#test) - [Links](#links) - [License](#license) - [Other interesting open source projects](#other-interesting-open-source-projects) --- ## About **version-rocket** contains two functional modules: **Web application version real-time detection**, **Automatic deployment message to lark or WeCom group chat** > You can use a module separately according to the needs, or use it together When is it suitable to use the **web application version real-time detection**? -The scene: This kind of situation often happens. When the user opens a web application in the browser for a long time and has not refresh the page. When the application has a new version update or the problem repair, the user will not know that there is a new version of the release, which will lead to the user. Continue to use old versions to affect user experience and back-end data accuracy. - **version-rocket** will detect the application version in real time. When a new version is found, the display version updates the pop-up window, prompting the user to refresh the page to update the application. When is it suitable to use **to automatically send deployment messages to Lark or WeCom group chat**? -The scene: There may be such a situation in team cooperation. As a front-end engineer, you need to verbally communicate with team members after each deployment. There are no deployment records to follow. - **version-rocket** Use the `Webhook` method. After the application deployment is successful, through group chat robots, the news of" successful deployment "will be automatically pushed to the group chat. *If you have the push needs of other platforms, you can mention issues* ## Features - Support all modern browsers - Real-time detection of available versions is provided in two ways: 1. through managing version numbers; 2. by detecting updates in specified file contents - Managing version numbers supports any version format, such as 1.1.0, 1.1.1.0, 1.1.0-beta, etc. - Detecting updates in specified file contents supports any file on a remote server `v1.7.0` - Support personalized version popup text and theme, also support custom UI - Sync deployment message to Lark or WeCom group chat after successful deploy - Card text and templates for deployment messages support customization, and support the dynamically generated fields. - Support TypeScript - [Npm package support](https://www.npmjs.com/package/version-rocket) - Support Node 14+ 🐰 ## Implementation Principle - **Web application version real-time detection:** 1. Through version number management: **version-rocket** compares the version in the user's current browser with the version file on the remote server. We use the `Web Worker API` based on JavaScript to perform monitoring polling, which does not affect the browser rendering process. 2. By detecting updates in specified file contents: **version-rocket** uses the browser's conditional cache mechanism to determine whether the specified file content has changed. We use the `Web Worker API` based on JavaScript to perform monitoring polling, which does not affect the browser rendering process. `v1.7.0` - **Automatically send deployment messages to Lark or WeCom group chat:** **version-rocket** call the webhook method provided by collaborative office software to trigger group chat robots send messages. ## Install [![version-rocket](https://nodei.co/npm/version-rocket.png)](https://www.npmjs.com/package/version-rocket) ```bash # Choose a package manager you prefer # npm npm install version-rocket --save # yarn yarn add version-rocket # pnpm pnpm install version-rocket ``` ### Quick Start ### Web application version real-time detection: Through version number management Step 1: Import `checkVersion()`, and use it ```javascript // Entry file: such as App.vue or App.jsx, etc import { checkVersion } from 'version-rocket' // It is recommended to use the version field in package.json, or you can customize versions import { version } from '../package.json' checkVersion({ localPackageVersion: version, originVersionFileUrl: `${location.origin}/version.json`, // Refer to API for more configuration options }) // To terminate version detection, call the unCheckVersion method during the destruction life cycle. For details, see the API unCheckVersion({closeDialog: false}) ``` Step 2: after executing the `generate-version-file` custom command, generate the `version.json` file, used to deploy to a remote server - `VERSION` (optional): when **custom version** is required, it is passed in. The default value is package.json version field - File output directory (optional): **user defined version.json output directory**, which is the dist directory by default - `EXTERNAL` (optional): when you want to save more information to `version.json`, such as the modified content of the current version or other things that need to be displayed on the pop-up (used in onVersionUpdate custom UI) `v1.6.0` - `EXTERNAL_PATH` (optional):Accepts a file path, recommended when a lot of extra information needs to be written to `version.json`. When both `EXTERNAL` and `EXTERNAL_PATH` are set, the priority is lower than `EXTERNAL`(used in onVersionUpdate custom UI)`v1.6.1` **VERSION usage** ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac or Linux system "generate:version": "VERSION=1.1.0-beta generate-version-file dist public" // Windows system: install cross-env first // npm install cross-env -D "generate:version": "cross-env VERSION=1.1.0-beta generate-version-file dist public" ... }, ... } ``` **EXTERNAL `v1.6.0` and EXTERNAL_PATH `v1.6.1` usage** JSON format please use this tool to escape [click here](https://codebeautify.org/json-encode-online) ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac or Linux (simple text) "generate:version": "EXTERNAL='some text' generate-version-file dist public" // Mac or Linux (JSON text) "generate:version": "EXTERNAL='{\"update\":\"fix bugs\",\"content\":\"some tips\"}' generate-version-file dist public" // Mac or Linux (JSON file, e.g. version-external.json) "generate:version": "EXTERNAL_PATH=version-external.json generate-version-file dist public" // Windows (simple text) "generate:version": "set EXTERNAL=some text && generate-version-file dist public" // Windows (JSON text) "generate:version": "set EXTERNAL={\"update\":\"fix bugs\",\"content\":\"some tips\"} && generate-version-file dist public" // Windows (JSON file, e.g. version-external.json) "generate:version": "set EXTERNAL_PATH=version-external.json && generate-version-file dist public" ... }, ... } ``` ```javascript // version-external.json { "update": [ "fix some bugs", "improve home page", "update docs" ], "content": "please update to latest version" } ```
⚠️ Notice: If your project is connected to CDN, it is strongly recommended that you set the `version.json` file is set to always no caching (configure in nginx or turn off the function of CDN ignoring the parameter cache) ``` shell // nginx example server { ... location / { ... if ($request_filename ~* .*\/version\.(json)$) { add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate"; } ... } ... } ```
*Complete the above two steps, the version monitoring function (through version number management) can be used normally 🎉🎉* ### Web application version real-time detection: By detecting updates in specified file contents `v1.7.0` > ⚠️ Friendly reminder: This method does not support displaying "current version changes or other information that needs to be shown in the prompt window". If you have such a requirement, please use the "version management" method. import `checkVersion()`, and use it ```javascript // Entry file: such as App.vue or App.jsx, etc import { checkVersion } from 'version-rocket' // Call checkVersion in the lifecycle hook checkVersion({ // The list of files to be monitored usually includes the index.html file under a certain domain checkOriginSpecifiedFilesUrl: [`${location.origin}/index.html`], // The validation mode for the list of monitored files: 'one' (default) or 'all' checkOriginSpecifiedFilesUrlMode: 'one', // Whether to enable version monitoring (default true) enable: process.env.NODE_ENV !== 'development' }) // If you need to terminate version checking, call the unCheckVersion method in the destroy lifecycle. For more details, see the API documentation unCheckVersion({closeDialog: false}) ``` *After completing the above steps, the version monitoring feature (by detecting updates in specified file contents) can be used normally 🎉🎉* #### Personalize the theme ```javascript // Entry file: such as App.vue or App.jsx, etc import { checkVersion } from 'version-rocket' // It is recommended to use the version field in package.json, or you can customize versions import { version } from '../package.json' checkVersion( { localPackageVersion: version, originVersionFileUrl: `${location.origin}/version.json`, }, { title: 'Title', description: 'Description', primaryColor: '#758bfd', rocketColor: '#ff8600', buttonText: 'Button Text', } ) ``` Or set prompt picture ``` javascript // Entry file: such as App.vue or App.jsx, etc import { checkVersion } from 'version-rocket' // It is recommended to use the version field in package.json, or you can customize versions import { version } from '../package.json' checkVersion( { localPackageVersion: version, originVersionFileUrl: `${location.origin}/version.json`, }, { imageUrl: 'https://avatars.githubusercontent.com/u/26329117', } ) ``` #### Screenshot

--- ### Automatically send deployment messages to Lark or WeCom group chat #### Lark Step 1: - **Create the `lark-message-config.json`** file in the project root directory to set the text of the message card - **execute the send-lark-message** custom command - `MESSAGE_PATH` (optional): passed if you need to customize the file path or filename (this parameter is useful if you need to differentiate the deployment environment). By default, the lark-message-config.json file in the root directory is used - `PACKAGE_JSON_PATH` (optional): passed if you need to customize the path to the package.json file (this parameter may be useful for deployments of monorepo projects). The default is to get the package.json file in the root path ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac or Linux system "send-lark-message:test": "MESSAGE_PATH=./lark-message-staging-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-lark-message" // Windows system: install cross-env first // npm install cross-env -D "send-lark-message:test": "cross-env MESSAGE_PATH=./lark-message-staging-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-lark-message" ... }, ... } ``` Step 2: Set `lark-message-config.json` ``` javascript // lark-message-config.json { // optional: card header's background color, default is turquoise, v1.6.2 // available values: blue | wathet | turquoise | green | yellow | orange | red | carmine | violet | purple | indigo | grey "headerBgColor": "red", // card title "title": "TEST FE Deployed Successfully", // project name label "projectNameLabel": "Project name label", // deploy project name "projectName": "TEST", // project branch label "branchLabel": "Branch label", // deploy branch name "branch": "Staging", // version label "versionLabel": "Version label", // version "version": "1.1.1.0", // project access url label "accessUrlLabel": "Access URL label", // project access url "accessUrl": "https://test.com", // remind group chat members label "isNotifyAllLabel": "Is notify all label", // remind group chat members: true/false "isNotifyAll": true, // lark robot webhook url "larkWebHook": "https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxx", // deploy type description "deployToolsText": "Deploy tools text", // deploy type "deployTools": "Jenkins", // the deploy time zone that you want to display, default "Asia/Shanghai" "expectConvertToTimezone": "America/New_York" // more information want to show "remark": "Trigger by bob, fix xxx bug" } ``` #### Set dynamic text If your card copy will be generated according to conditions, you can pass in `MESSAGE_JSON` field is self-defined, such as version, title, etc *Note: `MESSAGE_JSON` needs to be escaped* ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac or Linux system "send-lark-message:test": "MESSAGE_JSON='{\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true}' send-lark-message" // Windows system "send-lark-message:test": "set MESSAGE_JSON={\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true} && send-lark-message" ... }, ... } ``` Or after export variables, quote in package.json (not support Windows) ```javascript // ci file sh "npm run build" sh "export messageJSON='{\"title\": \"This is a title\"}'" // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... "send-lark-message:test": "MESSAGE_JSON=${messageJSON} send-lark-message" ... }, ... } ``` #### Custom message card ```javascript // lark-message-config.json { // Message card content "message": { "msg_type": "text", "content": { "text": "New message reminder" } }, // Lark robot's webhook link "larkWebHook": "https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxx" } ``` #### Screenshot

#### WeCom Step 1: - **Create the `message-config.json`** file in the project root directory to set the text of the message card - **execute the send-wecom-message** custom command - `MESSAGE_PATH` (optional): passed when you need to customize the file path or filename (this parameter is useful if you need to differentiate the deployment environment). The default is to use the message-config.json file in the root directory - `PACKAGE_JSON_PATH` (optional): passed when a custom path to the package.json file is required (this parameter may be useful for deployments of monorepo projects). The default is to get the package.json file in the root path ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac or Linux system "send-wecom-message:test": "MESSAGE_PATH=./message-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-wecom-message" // Windows system: install cross-env first // npm install cross-env -D "send-wecom-message:test": "cross-env MESSAGE_PATH=./message-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-wecom-message" ... }, ... } ``` Step 2: Set `message-config.json` ``` javascript { // card title "title": "TEST FE Deployed Successfully", // project name label "projectNameLabel": "Project name label", // deploy project name "projectName": "TEST", // project branch label "branchLabel": "Branch label", // deploy branch name "branch": "Staging", // version label "versionLabel": "Version label", // version "version": "1.1.1.0", // project access url label "accessUrlLabel": "Access URL label", // project access url "accessUrl": "https://test.com", // remind group chat members label "isNotifyAllLabel": "Is notify all label", // remind group chat members: true/false "isNotifyAll": true, // WeCom robot webhook url "webHook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxxxxx", // deploy type description "deployToolsText": "Deploy tools text", // deploy type "deployTools": "Jenkins", // the deploy time zone that you want to display, default "Asia/Shanghai" "expectConvertToTimezone": "America/New_York" // more information want to show "remark": "Trigger by bob, fix xxx bug" } ``` #### Set dynamic text If your card copy will be generated according to conditions, you can pass in `MESSAGE_JSON` field is self-defined, such as version, title, etc *Note: `MESSAGE_JSON ` needs to be escaped* ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac or Linux system "send-wecom-message:test": "MESSAGE_JSON='{\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true}' send-wecom-message" // Windows system "send-wecom-message:test": "set MESSAGE_JSON={\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true} && send-wecom-message" ... }, ... } ``` Or after export variables, quote in package.json (not support Windows) ```javascript // ci file sh "npm run build" sh "export messageJSON='{\"title\": \"This is a title\"}'" // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... "send-wecom-message:test": "MESSAGE_JSON=${messageJSON} send-wecom-message" ... }, ... } ``` #### Custom message card ```javascript // message-config.json { // message card template content "message": { "msgtype": "text", "text": { "content": "This is a custom message" } } // webhook link for the WeCom bot "webHook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxx" } ``` #### Screenshot --- ## API **checkVersion Function** > Enable real-time app version detection | Params | Type | Description | Default | Required | | --- | --- | --- | --- | --- | | config | object | Version monitoring configuration item | | Yes | | config.originVersionFileUrl | string | The path to the version.json file on the remote server | | Yes | | config.localPackageVersion | string | The version of the current application usually takes the version field of package.json for comparison with the version.json file of the remote server | | Yes | | config.pollingTime | number | Time interval for polling monitoring, in ms | 5000 | No | | config.immediate | boolean | On the first visit, version monitoring will be triggered immediately, and then polling will be conducted at a customized time interval **`v1.5.0`** | false | No | | config.checkOriginSpecifiedFilesUrl | array | Setting this property will use 'detecting updates in specified file contents' instead of 'version number management' to monitor versions. Pass in the list of file addresses to be monitored, usually the index.html file under a certain domain (Automatic deduplication) **`v1.7.0`** | | false | | config.checkOriginSpecifiedFilesUrlMode | 'one' / 'all' | 'one' means that if the content of any file address in the list changes, a prompt for an update will be displayed; 'all' means that a prompt for an update will only be displayed when the content of all file addresses in the list changes. (This only takes effect when checkOriginSpecifiedFilesUrl is configured) **`v1.7.0`** | 'one' | false | | config.enable | boolean | Whether to enable version monitoring. This configuration item can be used to enable version monitoring only in specified environments **`v1.7.0`** | true | 否 | | config.clearIntervalOnDialog | boolean | When the prompt dialog for a new version appears, clear the timer **`v1.7.0`** | false | 否 | | config.onVersionUpdate | function(data) | Callback function for custom version hint UI (if you want to customize the popup UI, you can get the return value through the callback function to control the appearance of the popup) | | No | | config.onRefresh | function(data) | Confirm update: the callback function of the custom refresh event, where data is the latest version **`v1.5.0`** | | No | | config.onCancel | function(data) | Cancel update: the callback function of the custom cancel event, where data is the latest version **`v1.5.0`** | | No | | options | object | Configuration items for popup text and themes (not customize the popup UI, but use it if you need to modify the text and themes) | | No | | options.title | string | Popup title | Update | No | | options.description | string | Popup description | V xxx is available | No | | options.buttonText | string | Popup button text | Refresh | No | | options.cancelButtonText | string | Text to close pop-up button (add this option, if you want the pop-up to be allowed to be close) **`v1.5.0`** | | No | | options.cancelMode | ignore-current-version / ignore-today / ignore-current-window | Close pop-up mode (It takes effect when cancelButtonText is set) **`v1.5.0`** | ignore-current-version | No | | options.cancelUpdateAndStopWorker | boolean | When the popup is cancelled, the worker is also stopped (It takes effect when cancelButtonText is set) **`v1.5.0`** | false | 否 | | options.imageUrl | string | Popup image | | No | | options.rocketColor | string | The popup picture's theme color of the rocket, after setting Options.imageUrl is invalid | | No | | options.primaryColor | string | The theme color of the popup, it will affect the hint image background color and button background color, after setting imageUrl is invalid | | No | | options.buttonStyle | string | The CSS configuration of pop-up buttons can override the default button style | | No | **unCheckVersion Function** > Terminate the `worker` process created after calling `checkVersion` | Params | Type | Description | Default | Required | | --- | --- | --- | --- | --- | | closeDialog | boolean | Whether to close the version update prompt pop-up window | - | Yes | | closeWorker | boolean | Whether to close the worker | true | No | ## Test ```shell npm run test ``` ## Links - [Timezone List](https://jp.cybozu.help/general/zh/admin/list_systemadmin/list_localization/timezone.html) - [JSON Escape](https://codebeautify.org/json-encode-online) - [Lark Card Builder](https://open.larksuite.com/tool/cardbuilder?from=howtoguide) - [WeCom Chat Bot Doc](https://developer.work.weixin.qq.com/document/path/91770) ## License version-rocket is open source software with [Apache License 2.0](./LICENSE.md) ## Other interesting open source projects **[web-authn-completed-app](https://github.com/guMcrey/web-authn-completed-app)** 💻 [Online preview](https://web-authn.x-dev.club) > A complete application based on WebAuthn API, which allows **websites to authenticate users with the built-in authenticator** in the browser/system (such as Apple TouchID and Windows Hello or biometric sensor of mobile devices). It will **replace passwords**, which is the future of online authentication. ================================================ FILE: README.zh-CN.md ================================================ # 🔔 version-rocket 🚀 简体中文 | [English](./README.md) > 一个用于 web 应用版本检测和部署通知的工具库。 [![](https://img.shields.io/npm/v/version-rocket)](https://www.npmjs.com/package/version-rocket) [![](https://img.shields.io/npm/dm/version-rocket.svg)](https://npmcharts.com/compare/version-rocket?minimal=true) [![](https://codecov.io/gh/guMcrey/version-rocket/main/graph/badge.svg)](https://codecov.io/gh/guMcrey/version-rocket) [![](https://github.com/guMcrey/version-rocket/actions/workflows/main.yml/badge.svg)](https://github.com/guMcrey/version-rocket/actions/) [![](https://img.shields.io/npm/l/version-rocket)](https://www.npmjs.com/package/version-rocket) 目录 - [简介](#简介) - [功能特点](#功能特点) - [实现原理](#实现原理) - [安装](#安装) - [快速开始](#快速开始) - [Web 应用版本实时检测](#web-应用版本实时检测) - [个性化设置主题](#个性化设置主题) - [效果截图](#效果截图) - [自动发送部署消息到飞书 (Lark) 或企业微信 (WeCom) 群聊](#自动发送部署消息到飞书-lark-或企业微信-wecom-群聊) - [飞书 (Lark)](#飞书-lark) - [设置动态文案](#设置动态文案) - [自定义消息卡片](#自定义消息卡片) - [效果截图](#效果截图-1) - [企业微信 (WeCom)](#企业微信-wecom) - [设置动态文案](#设置动态文案-1) - [自定义消息卡片](#自定义消息卡片-1) - [效果截图](#效果截图-2) - [API](#api) - [测试](#测试) - [相关链接](#相关链接) - [许可证](#许可证) - [其他有趣的开源项目](#其他有趣的开源项目) --- ## 简介 **version-rocket** 包含两个功能模块: **Web 应用版本实时检测**、**自动发送部署消息到飞书 (Lark) 或企业微信 (WeCom) 群聊。** > 你可以根据需求单独使用某个模块, 或一起使用 什么时候适合使用 **Web 应用版本实时检测**? - 场景: 经常会发生这样的情况, 当用户在浏览器中打开某 web 应用很长时间且未刷新页面, 在应用有新版本更新或问题修复时, 用户会无法及时知晓有新版发布, 导致用户继续使用旧的版本, 影响用户体验和后端数据准确性。 - **version-rocket** 会实时检测应用版本, 当发现新版本时, 展示版本更新提示弹窗, 提示用户刷新页面来更新应用。 什么时候适合使用 **自动发送部署消息到飞书 (Lark) 或企业微信 (WeCom) 群聊**? - 场景: 在团队合作中可能会有这样的情况, 你作为前端工程师, 在联调测试或部署上线时, 每次部署后都需要跟团队成员口头传达已经部署成功, 增加了沟通成本, 不够自动化, 也没有部署记录以有迹可循。 - **version-rocket** 利用 `WebHook` 方式, 在应用部署成功后, 通过群聊机器人, 自动帮你推送“部署成功”的消息到群聊中。 *如果有其他平台的推送需求, 可以提 issue* ## 功能特点 - 支持所有现代浏览器 - **可用版本实时检测**提供两种方式: 1. 通过**管理版本号**; 2. 通过检测**指定文件内容是否有更新** 1. 通过管理版本号: 支持任意版本格式, 例如: 1.1.0、1.1.1.0、1.1.0-beta 等等 2. 通过检测指定文件内容是否有更新: 支持任意远程服务器中的文件 `v1.7.0` - 支持**个性化设置**版本提示弹窗的文案和**主题**, 也支持自定义 UI - 部署成功后,将**部署消息同步给群聊机器人**, 目前支持飞书 (Lark) 和企业微信 (WeCom) - 部署信息卡片的文案和消息模版支持自定义, 并支持动态生成的字段传入 - 支持 TypeScript - 支持 Node 14+ 🐰 ## 实现原理 - **Web 应用版本实时检测:** 1. 通过管理版本号: **version-rocket** 将用户当前浏览器中的版本与远程服务器中的版本文件进行比较。我们使用基于 javascript 的 `Web Worker API` 来做监测轮询,不会影响浏览器渲染进程。 2. 通过检测指定文件内容是否有更新: **version-rocket** 将依赖浏览器的协商缓存原理来判断指定的文件内容是否发生了改变。我们使用基于 javascript 的 `Web Worker API` 来做监测轮询,不会影响浏览器渲染进程。`v1.7.0` - **自动发送部署消息到飞书 (Lark) 或企业微信 (WeCom) 群聊:** **version-rocket** 调用协同办公软件提供的 WebHook 方式, 触发群聊机器人发送消息。 ## 安装 [![version-rocket](https://nodei.co/npm/version-rocket.png)](https://www.npmjs.com/package/version-rocket) ```bash # 选择一个你喜欢的包管理器 # npm npm install version-rocket --save # yarn yarn add version-rocket # pnpm pnpm install version-rocket ``` ### 快速开始 ### Web 应用版本实时检测: 通过管理版本号 第一步: 导入 `checkVersion()`, 并调用 ```javascript // 入口文件: 如 App.vue 或 App.jsx 等 import { checkVersion } from 'version-rocket' // 推荐使用 package.json 中的 version 字段, 也可自定义 version import { version } from '../package.json' // 在生命周期钩子中调用 checkVersion checkVersion({ localPackageVersion: version, originVersionFileUrl: `${location.origin}/version.json`, // 更多配置选项请参考 API }) // 如需终止版本检测时, 在销毁生命周期中, 调用 unCheckVersion 方法进行终止, 详情参见 API unCheckVersion({closeDialog: false}) ``` 第二步: 执行 `generate-version-file` 自定义命令后,在 dist 目录生成 `version.json` 文件, 用于部署到远程服务器 - `VERSION` (参数可选): 需要**自定义 version** 时传入, 默认取 package.json 的 version 字段 - 文件输出目录 (参数可选): 需要**自定义 version.json 输出目录**时传入, 默认为 dist 目录 - `EXTERNAL` (参数可选):希望将更多信息存到 `version.json` 中时传入,如当前版本的修改内容或其他需要展示在提示弹窗上时 (用于 onVersionUpdate 自定义 UI 时)`v1.6.0` - `EXTERNAL_PATH` (参数可选):接收一个文件路径, 推荐在需要将大量额外信息写入 `version.json` 中时使用. 当同时设置了 `EXTERNAL` 和 `EXTERNAL_PATH` 时,优先级低于 `EXTERNAL` (用于 onVersionUpdate 自定义 UI 时)`v1.6.1` **VERSION 环境变量设置方式** ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac 或 Linux 系统 "generate:version": "VERSION=1.1.0-beta generate-version-file dist public" // Windows 系统先安装 cross-env // npm install cross-env -D "generate:version": "cross-env VERSION=1.1.0-beta generate-version-file dist public" ... }, ... } ``` **EXTERNAL `v1.6.0` 和 EXTERNAL_PATH `v1.6.1` 环境变量设置方式** JSON 格式可以通过 [这里](https://codebeautify.org/json-encode-online) 转义后再使用 ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac 或 Linux 系统 (简单文本) "generate:version": "EXTERNAL='some text' generate-version-file dist public" // Mac 或 Linux 系统 (JSON 文本) "generate:version": "EXTERNAL='{\"update\":\"fix bugs\",\"content\":\"some tips\"}' generate-version-file dist public" // Mac 或 Linux 系统 (JSON 文件, 如 version-external.json) "generate:version": "EXTERNAL_PATH=version-external.json generate-version-file dist public" // Windows 系统 (简单文本) "generate:version": "set EXTERNAL=some text && generate-version-file dist public" // Windows 系统 (JSON 文本) "generate:version": "set EXTERNAL={\"update\":\"fix bugs\",\"content\":\"some tips\"} && generate-version-file dist public" // Windows 系统 (JSON 文件, 如 version-external.json) "generate:version": "set EXTERNAL_PATH=version-external.json && generate-version-file dist public" ... }, ... } ``` ```javascript // version-external.json 示例 { "update": [ "fix some bugs", "improve home page", "update docs" ], "content": "please update to latest version" } ```
⚠️ 注意事项 如果你的项目接入了 CDN, 强烈建议你将 version.json 文件设置为强制不缓存 (在 nginx 中配置或关闭 CDN 忽略参数缓存的功能) ``` shell // nginx 配置示例 server { ... location / { ... if ($request_filename ~* .*\/version\.(json)$) { add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate"; } ... } ... } ```
*完成以上两个步骤, 版本监测功能(通过管理版本号)可以正常使用了 🎉🎉* ### Web 应用版本实时检测: 通过检测指定文件内容是否有更新 `v1.7.0` > ⚠️ 温馨提示: 该方式不支持 "当前版本的修改内容或其他需要展示在提示弹窗上"的内容 (如有这样的需求, 请使用 “管理版本号” 的方式) 导入 `checkVersion()`, 并调用 ```javascript // 入口文件: 如 App.vue 或 App.jsx 等 import { checkVersion } from 'version-rocket' // 在生命周期钩子中调用 checkVersion checkVersion({ // 要监听的文件列表, 一般监测某个域名下的 index.html 文件 checkOriginSpecifiedFilesUrl: [`${location.origin}/index.html`], // 监听的文件列表的校验模式: 'one'(默认) | 'all' checkOriginSpecifiedFilesUrlMode: 'one', // 是否启用版本监测 (默认 true) enable: process.env.NODE_ENV !== 'development' }) // 如需终止版本检测时, 在销毁生命周期中, 调用 unCheckVersion 方法进行终止, 详情参见 API unCheckVersion({closeDialog: false}) ``` *完成以上步骤, 版本监测功能(通过检测指定文件内容是否有更新)可以正常使用了 🎉🎉* #### 个性化设置主题 ```javascript // 入口文件: 如 App.vue 或 App.jsx 等 import { checkVersion } from 'version-rocket' // 推荐使用 package.json 中的 version 字段, 也可自定义 version import { version } from '../package.json' checkVersion( { localPackageVersion: version, originVersionFileUrl: `${location.origin}/version.json`, }, { title: 'Title', description: 'Description', primaryColor: '#758bfd', rocketColor: '#ff8600', buttonText: 'Button Text', } ) ``` 或设置提示图片 ``` javascript // 入口文件: 如 App.vue 或 App.jsx 等 import { checkVersion } from 'version-rocket' // 推荐使用 package.json 中的 version 字段, 也可自定义 version import { version } from '../package.json' checkVersion( { localPackageVersion: version, originVersionFileUrl: `${location.origin}/version.json`, }, { imageUrl: 'https://avatars.githubusercontent.com/u/26329117', } ) ``` #### 效果截图

--- ### 自动发送部署消息到飞书 (Lark) 或企业微信 (WeCom) 群聊 #### 飞书 (Lark) 第一步: - 在项目根目录下**创建 lark-message-config.json**文件,用于设置消息卡片的文案 - **执行 send-lark-message**自定义命令 - `MESSAGE_PATH` (参数可选): 需要自定义文件路径或文件名时传入 (此参数对有区分部署环境的需求时, 非常有用)。默认使用根目录下的 lark-message-config.json 文件 - `PACKAGE_JSON_PATH` (参数可选): 需要自定义 package.json 文件路径时传入 (此参数对于 monorepo 项目的部署时, 可能有用)。默认获取根路径下的 package.json 文件 ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac 或 Linux 系统 "send-lark-message:test": "MESSAGE_PATH=./lark-message-staging-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-lark-message" // Windows 系统先安装 cross-env // npm install cross-env -D "send-lark-message:test": "cross-env MESSAGE_PATH=./lark-message-staging-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-lark-message" ... }, ... } ``` 第二步: 配置 `lark-message-config.json` 文件 ``` javascript // lark-message-config.json { // 可选: 消息卡片头部背景色, 用于设置标题背景颜色, 默认 turquoise, v1.6.2 // 取值范围: blue | wathet | turquoise | green | yellow | orange | red | carmine | violet | purple | indigo | grey "headerBgColor": "red", // 消息卡片标题 "title": "TEST FE Deployed Successfully", // 项目名称标签 "projectNameLabel": "Project name label", // 项目名称 "projectName": "TEST", // 项目分支标签 "branchLabel": "Branch label", // 项目分支, 可用于区别部署环境 "branch": "Staging", // 版本标签 "versionLabel": "Version label", // 版本 "version": "1.1.1.0", // 项目可访问地址标签 "accessUrlLabel": "Access URL label", // 项目可访问地址 "accessUrl": "https://test.com", // 是否@所有人标签 "isNotifyAllLabel": "Is notify all label", // 是否@所有人: true / false "isNotifyAll": true, // Lark 机器人的 webhook 链接 "larkWebHook": "https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxx", // 可选: 部署工具描述 "deployToolsText": "Deploy tools text", // 可选: 部署所使用的方式或平台 "deployTools": "Jenkins", // 可选: 部署时间想要转换成的时区,默认 "Asia/Shanghai" (当你的项目要部署的目标服务器与你所在时区不同, 可以设置此字段来转换时区) "expectConvertToTimezone": "America/New_York" // 可选: 想要展示除模版之外的更多信息 "remark": "Trigger by bob, fix xxx bug" } ``` #### 设置动态文案 如果你的卡片文案会根据条件来生成时, 可以传入 `MESSAGE_JSON` 字段来自定义, 如 version, title 等. *注意: `MESSAGE_JSON` 的值需要做转义* ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac 或 Linux 系统 "send-lark-message:test": "MESSAGE_JSON='{\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true}' send-lark-message" // Windows 系统 "send-lark-message:test": "set MESSAGE_JSON={\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true} && send-lark-message" ... }, ... } ``` 或 export 变量后, 在 package.json 中引用 (不支持 Windows) ```javascript // ci file sh "npm run build" sh "export messageJSON='{\"title\": \"This is a title\"}'" // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... "send-lark-message:test": "MESSAGE_JSON=${messageJSON} send-lark-message" ... }, ... } ``` #### 自定义消息卡片 ```javascript // lark-message-config.json { // 消息卡片内容 "message": { "msg_type": "text", "content": { "text": "New message reminder" } }, // Lark 机器人的 webhook 链接 "larkWebHook": "https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxx" } ``` #### 效果截图

#### 企业微信 (WeCom) 第一步: - 在项目根目录下**创建 message-config.json**文件,用于设置消息卡片的文案 - **执行 send-wecom-message**自定义命令 - `MESSAGE_PATH` (参数可选): 需要自定义文件路径或文件名时传入 (此参数对有区分部署环境的需求时, 非常有用)。默认使用根目录下的 message-config.json 文件 - `PACKAGE_JSON_PATH` (参数可选): 需要自定义 package.json 文件路径时传入 (此参数对于 monorepo 项目的部署时, 可能有用)。默认获取根路径下的 package.json 文件 ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac 或 Linux 系统 "send-wecom-message:test": "MESSAGE_PATH=./message-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-wecom-message" // Windows 系统先安装 cross-env // npm install cross-env -D "send-wecom-message:test": "cross-env MESSAGE_PATH=./message-config.json PACKAGE_JSON_PATH=./packages/test/package.json send-wecom-message" ... }, ... } ``` 第二步: 配置 `message-config.json` 文件 ``` javascript { // 消息卡片标题 "title": "TEST FE Deployed Successfully", // 可选: 项目名称标签, 默认 Project Name "projectNameLabel": "Project name label", // 项目名称 "projectName": "TEST", // 可选: 项目分支标签, 默认 Branch "branchLabel": "Branch label", // 项目分支, 可用于区别部署环境 "branch": "Staging", // 可选: 版本标签, 默认 Version "versionLabel": "Version label", // 版本 "version": "1.1.1.0", // 可选: 项目可访问地址标签, 默认 URL "accessUrlLabel": "Access URL label", // 项目可访问地址 "accessUrl": "https://test.com", // 是否@所有人: true / false "isNotifyAll": true, // 企业微信机器人的 webhook 链接 "webHook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxx", // 可选: 部署工具描述 "deployToolsText": "Deploy tools text", // 可选: 部署时间想要转换成的时区,默认 "Asia/Shanghai" (当你的项目要部署的目标服务器与你所在时区不同, 可以设置此字段来转换时区) "expectConvertToTimezone": "America/New_York" // 可选: 想要展示除模版之外的更多信息 "remark": "Trigger by bob, fix xxx bug" } ``` #### 设置动态文案 如果你的卡片文案会根据条件来生成时, 可以传入 `MESSAGE_JSON` 字段来自定义, 如 version, title 等. *注意: `MESSAGE_JSON` 的值需要做转义* ```javascript // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... // Mac 或 Linux 系统 "send-wecom-message:test": "MESSAGE_JSON='{\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true}' send-wecom-message" // Windows 系统 "send-wecom-message:test": "set MESSAGE_JSON={\"title\":\"This is a dynamically generated title\",\"version\":\"1.1.0-beta\",\"accessUrl\":\"http://test.example.com\",\"isNotifyAll\":true} && send-wecom-message" ... }, ... } ``` 或 export 变量后, 在 `package.json` 中引用 (不支持 Windows) ```javascript // ci file sh "npm run build" sh "export messageJSON='{\"title\": \"This is a title\"}'" // package.json { "name": "test", "description": "test", "private": true, "version": "0.0.1", "scripts": { ... "send-wecom-message:test": "MESSAGE_JSON=${messageJSON} send-wecom-message" ... }, ... } ``` #### 自定义消息卡片 ```javascript // message-config.json { // 消息卡片内容 "message": { "msgtype": "text", "text": { "content": "This is a custom message" } } // 企业微信机器人的 webhook 链接 "webHook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxx" } ``` #### 效果截图 ## API **checkVersion 方法** > 开启应用版本实时检测功能 | 参数 | 类型 | 描述 | 默认值 | 必需 | | --- | --- | --- | --- | --- | | config | object | 版本监测配置项 | | 是 | | config.originVersionFileUrl | string | 远程服务器上的 version.json 文件路径 | | 否 **`v1.7.0`** | | config.localPackageVersion | string | 当前应用版本号, 通常取 package.json 的 version 字段, 用于与远程服务器的 version.json 文件比较 | | 否 **`v1.7.0`** | | config.pollingTime | number | 轮询监测的时间间隔, 单位 ms | 5000 | 否 | | config.immediate | boolean | 第一次访问时, 立即触发版本监测, 之后按自定义时间间隔轮询 **`v1.5.0`** | false | 否 | | config.checkOriginSpecifiedFilesUrl | array | 设置该属性后将使用 “通过检测指定文件是否有更新” 而不是 “通过管理版本号” 来监测版本, 传入希望监测的文件地址列表, 通常情况为某个域名下的 index.html 文件 (自动去重) **`v1.7.0`** | | 否 | | config.checkOriginSpecifiedFilesUrlMode | 'one' / 'all' | 'one' 表示列表中文件地址只要有一个内容发生改变即提示更新; 'all' 表示列表中文件地址都发生改变时才提示更新. (当 checkOriginSpecifiedFilesUrl 配置后才生效) **`v1.7.0`** | 'one' | 否 | | config.enable | boolean | 是否启用版本监测, 通过该配置项可以设置版本监测只在指定环境下开启 **`v1.7.0`** | true | 否 | | config.clearIntervalOnDialog | boolean | 当发现新版本提示弹窗出现后, 清空定时器 **`v1.7.0`** | false | 否 | | config.onVersionUpdate | function(data) | 自定义版本提示 UI 的回调函数 (如果你想自定义弹窗 UI, 通过回调函数可以拿到返回值来控制弹窗的显隐 ) | | 否 | | config.onRefresh | function(data) | 确认更新: 自定义 refresh 事件的回调函数, data 为最新版本号 **`v1.5.0`** | | 否 | | config.onCancel | function(data) | 取消更新: 自定义 cancel 事件的回调函数, data 为最新版本号 **`v1.5.0`** | | 否 | | options | object | 弹窗文案和主题的配置项 (不自定义弹窗 UI, 但有修改文案和主题的需求时使用) | | 否 | | options.title | string | 弹窗的标题 | Update | 否 | | options.description | string | 弹窗的描述 | V xxx is available | 否 | | options.buttonText | string | 弹窗按钮的文案 | Refresh | 否 | | options.cancelButtonText | string | 关闭弹窗按钮的文案 (如果你希望弹窗允许被关闭, 请添加此选项) **`v1.5.0`** | | 否 | | options.cancelMode | ignore-current-version (当前版本不再提示, 通过管理版本号监测版本更新的默认配置, 该配置只支持管理版本号的方式) / ignore-today (今天不再提示) / ignore-current-window (当前窗口不再提示, 通过监测指定文件内容是否有更新方式的默认配置) | 关闭弹窗的模式 (当 cancelButtonText 设置后生效) **`v1.5.0`** | ignore-current-version | 否 | | options.cancelUpdateAndStopWorker | boolean | 关闭弹窗时, 也关闭 worker (当 cancelButtonText 设置后生效) **`v1.5.0`** | false | 否 | | options.imageUrl | string | 弹窗的提示图片 | | 否 | | options.rocketColor | string | 弹窗提示图片中火箭的主题色, 设置后 options.imageUrl 无效 | | 否 | | options.primaryColor | string | 弹窗的主题色, 会作用到提示图片背景色和按钮背景色, 设置后 imageUrl 无效 | | 否 | | options.buttonStyle | string | 弹窗按钮的 css 配置, 可以覆盖掉默认的按钮样式 | | 否 | **unCheckVersion 方法** > 终止在调用 `checkVersion` 后创建的 `worker` 进程 | 参数 | 类型 | 描述 | 默认值 | 必需 | | --- | --- | --- | --- | --- | | closeDialog | boolean | 是否关闭版本更新提示弹窗 | false | 是 | | closeWorker | boolean | 是否停止 worker 轮询 | true | 否 | ## 测试 ```shell npm run test ``` ## 相关链接 - [时区参照表](https://jp.cybozu.help/general/zh/admin/list_systemadmin/list_localization/timezone.html) - [JSON 在线转义工具](https://codebeautify.org/json-encode-online) - [Lark 消息卡片搭建工具](https://open.larksuite.com/tool/cardbuilder?from=howtoguide) - [企业微信群聊机器人文档](https://developer.work.weixin.qq.com/document/path/91770) ## 许可证 version-rocket 是开源软件, 许可证为 [Apache License 2.0](./LICENSE.md) ## 其他有趣的开源项目 **[web-authn-completed-app](https://github.com/guMcrey/web-authn-completed-app)** 💻 [在线体验](https://web-authn.x-dev.club) > 一个基于 WebAuthn API 实现的完整应用, 它允许**网站使用浏览器/系统内置的认证器**(如 Apple TouchID 和 Windows Hello 或移动设备的生物识别传感器)对用户进行**身份认证**. 它将会**代替密码**, 是在线身份认证的未来. ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript', ], }; ================================================ FILE: components/images.d.ts ================================================ declare module '*.png' declare module '*.svg' ================================================ FILE: components/version-tip-dialog.css ================================================ #version-rocket .version-area { box-sizing: border-box; position: fixed; right: 20px; bottom: 20px; z-index: 99999; background-color: #fff; border: 1px solid #ebeef5; width: 140px; box-shadow: 0 10px 20px 0 rgba(0,0,0,0.12); border-radius: 12px; animation: fadeInUp 1s ease; animation-iteration-count: 1; } #version-rocket .version-img { margin-top: -70px; width: 100%; height: 100%; } #version-rocket .version-content { padding: 6px 12px 9px; text-align: center; background-color: #fff; font-family: inherit; border-radius: 12px; } #version-rocket .version-title { font-size: 13px; font-weight: 600; color: #101010; line-height: 13px; } #version-rocket .version-subtitle { margin-top: 8px; font-size: 12px; color: rgba(0,0,0,0.7); line-height: 13px; } #version-rocket .refresh-button { margin-top: 12px; width: 100%; background-color: #fd8079; border-color: #fd8079; transition: 0.2s; animation: refreshAnimation 2s linear infinite; color: #fff; padding: 5px 0; font-size: 15px; border-radius: 15px; font-weight: 500; cursor: pointer; } #version-rocket .refresh-button:hover { background-color: rgba(253,128,121,0.9); } #version-rocket .cancel-button { margin-top: 5px; font-size: 12px; font-weight: 500; color: #888; text-decoration: underline; cursor: pointer; } #version-rocket .cancel-button:hover { color: #999; } @-moz-keyframes refreshAnimation { 0% { transform: scale(0.9); } 50% { transform: scale(1.05); } 100% { transform: scale(0.9); } } @-webkit-keyframes refreshAnimation { 0% { transform: scale(0.9); } 50% { transform: scale(1.05); } 100% { transform: scale(0.9); } } @-o-keyframes refreshAnimation { 0% { transform: scale(0.9); } 50% { transform: scale(1.05); } 100% { transform: scale(0.9); } } @keyframes refreshAnimation { 0% { transform: scale(0.9); } 50% { transform: scale(1.05); } 100% { transform: scale(0.9); } } @-moz-keyframes fadeInUp { from { opacity: 0; transform: translate3d(0, 100%, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } } @-webkit-keyframes fadeInUp { from { opacity: 0; transform: translate3d(0, 100%, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } } @-o-keyframes fadeInUp { from { opacity: 0; transform: translate3d(0, 100%, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } } @keyframes fadeInUp { from { opacity: 0; transform: translate3d(0, 100%, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } } ================================================ FILE: components/versionTipDialog.ts ================================================ import versionBg from './../assets/version-bg.png' import './version-tip-dialog.css' import {setVersionTipTheme} from './versionTipTheme' import {unCheckVersion} from '../index' const defaultParams = { title: 'Update', description: 'is available', buttonText: 'Refresh', } export const versionTipDialog = (params: { title?: string description?: string buttonText?: string cancelButtonText?: string cancelMode?: string imageUrl?: string rocketColor?: string primaryColor?: string buttonStyle?: string newVersion?: string needRefresh?: boolean onRefresh?: (event: any) => void onCancel?: (event: any) => void }) => { const dialogElement = document.querySelector('#version-rocket') if (dialogElement) return const template = `
${ params.primaryColor || params.rocketColor ? `
${setVersionTipTheme( params.primaryColor, params.rocketColor )}
` : `version` }
${params.title || defaultParams.title}
${ params.description || (params.newVersion ? `V ${params.newVersion} ${defaultParams.description}` : `A new version ${defaultParams.description}`) }
${params.buttonText || defaultParams.buttonText}
${ params.cancelButtonText ? `
${params.cancelButtonText}
` : '' }
` let rootNode = document.createElement('div') rootNode.innerHTML = template document.body.appendChild(rootNode) // refresh const refreshBtnNode = document.querySelector( '#version-rocket .refresh-button' ) as HTMLElement refreshBtnNode.onclick = () => { if (typeof params?.onRefresh === 'function') { params.onRefresh({ newVersion: params.newVersion, needRefresh: params.needRefresh || false, }) } else { window.location.reload() } } // cancel const cancelBtnNode = document.querySelector( '#version-rocket .cancel-button' ) as HTMLElement if (!cancelBtnNode) return cancelBtnNode.onclick = () => { if (typeof params?.onCancel === 'function') { params.onCancel({ newVersion: params.newVersion, needRefresh: params.needRefresh || false, }) return } const cancelMode = params?.cancelMode || 'ignore-current-version' switch (cancelMode) { case 'ignore-current-version': localStorage.setItem( 'version-rocket:cancelled', params.newVersion || '' ) break case 'ignore-today': localStorage.setItem( 'version-rocket:cancelled', new Date().toLocaleDateString() ) break case 'ignore-current-window': sessionStorage.setItem('version-rocket:cancelled', 'true') break default: break } unCheckVersion({closeDialog: true, closeWorker: false}) } } ================================================ FILE: components/versionTipTheme.ts ================================================ export const setVersionTipTheme = ( primaryColor = '#FA8D88', rocketColor = '#FE7D66' ) => { return ` version-rokect ` } ================================================ FILE: index.ts ================================================ import {versionTipDialog} from './components/versionTipDialog' import { createWorker, createWorkerFunc, cancelUpdateFunc, checkVersionTypeFunc, } from './utils/index' /** * Polling monitoring version update (No longer maintain) * * @param {string} localPackageVersion the current version of the page in the browser * @param {string} originVersionFileUrl remote server version file address * @param {number} [pollingTime = 5000] polling interval, in ms (Optional) * @param {function} onVersionUpdate callback when updating version, used when customizing UI (Optional) * @return {object} { refreshPageVersion } new version number */ export const pollingCompareVersion = ( localPackageVersion: string, originVersionFileUrl: string, pollingTime: number, onVersionUpdate?: (event: any) => void ) => { const worker = createWorker(createWorkerFunc) worker.postMessage({ 'version-key': localPackageVersion, 'polling-time': pollingTime, 'origin-version-file-url': originVersionFileUrl, }) worker.onmessage = (event: any) => { // custom version tip UI if (typeof onVersionUpdate === 'function') { onVersionUpdate(event.data) } else { // default version tip ui versionTipDialog({newVersion: event.data.refreshPageVersion}) } } } /** * Polling monitoring version update v2 (Recommended) * * @param {object} config Polling the configuration parameters of the monitoring version * @param {string} config.originVersionFileUrl remote server version file address (Required) * @param {string} config.localPackageVersion the current version of the page in the browser * @param {number} [config.pollingTime = 5000] polling interval, in ms (Optional) * @param {function} config.onVersionUpdate callback when updating version, used when customizing UI (Optional) * * @param {object} options Customize version update popup copy and themes * @param {string} [options.title = 'Update'] popup title (Optional) * @param {string} [options.description = 'V xxx is available'] popup description (Optional) * @param {string} [options.buttonText = 'Refresh'] popup button text (Optional) * @param {string} [options.cancelButtonText] close popup button text (Optional) * @param {string} [options.cancelMode = 'ignore-current-version'] close popup button mode * @param {string} [options.cancelUpdateAndStopWorker = false] close popup and stop worker * @param {string} options.imageUrl custom popup image address (Optional) * @param {string} options.rocketColor custom popup rocket color in the picture (Optional) * @param {string} options.primaryColor custom popup primary color, act on image background color and button background color (Optional) * @param {string} options.buttonStyle custom popup button style (Optional) * * @return {object} { refreshPageVersion } new version number */ let worker: Worker | undefined = undefined export const checkVersion = ( config: { checkOriginSpecifiedFilesUrl?: string[] checkOriginSpecifiedFilesUrlMode?: 'one' | 'all' originVersionFileUrl?: string localPackageVersion?: string pollingTime?: number immediate?: boolean enable?: boolean clearIntervalOnDialog?: boolean onVersionUpdate?: (event: any) => void onRefresh?: (event: any) => void onCancel?: (event: any) => void }, options?: { title?: string description?: string buttonText?: string cancelButtonText?: string cancelMode?: string cancelUpdateAndStopWorker?: boolean imageUrl?: string rocketColor?: string primaryColor?: string buttonStyle?: string } ) => { if (config.enable === false) return if (!worker) { worker = createWorker(createWorkerFunc, [checkVersionTypeFunc]) } const processUserInput = (input: any) => { if (!input) { return } if (!Array.isArray(input)) { console.warn('Invalid input: Expected an array.') return [] } return [...new Set(input)].filter((item) => item != null) } worker.postMessage({ 'version-key': config.localPackageVersion || '', 'polling-time': config.pollingTime || 5000, immediate: config.immediate || false, 'origin-version-file-url': config.originVersionFileUrl || '', 'check-origin-specified-files-url': processUserInput( config.checkOriginSpecifiedFilesUrl ), 'check-origin-specified-files-url-mode': config.checkOriginSpecifiedFilesUrlMode || 'one', 'clear-interval-on-dialog': config.clearIntervalOnDialog || false, }) worker.onmessage = (event: any) => { const cancelUpdateLock = cancelUpdateFunc( options?.cancelMode, event.data?.refreshPageVersion, options?.cancelUpdateAndStopWorker, worker ) if (cancelUpdateLock) return localStorage.removeItem('version-rocket:cancelled') sessionStorage.removeItem('version-rocket:cancelled') // custom version tip UI if (typeof config.onVersionUpdate === 'function') { config.onVersionUpdate(event.data) } else { // default version tip ui const { title, description, buttonText, cancelButtonText, cancelMode, imageUrl, rocketColor, primaryColor, buttonStyle, } = options || {} const {onRefresh, onCancel} = config || {} versionTipDialog({ title, description, buttonText, cancelButtonText, cancelMode, imageUrl, rocketColor, primaryColor, buttonStyle, newVersion: event.data.refreshPageVersion, needRefresh: event.data.refreshPageVersion, onRefresh, onCancel, }) } } } /** * destroy checkVersion */ export const unCheckVersion = ({closeDialog = false, closeWorker = true}) => { if (closeWorker) { worker?.terminate() worker = undefined } if (closeDialog) { const dialogElement = document.querySelector('#version-rocket') const dialogElementParent = dialogElement?.parentElement if (dialogElement && dialogElementParent) { dialogElementParent.removeChild(dialogElement) } } } ================================================ FILE: jest.config.ts ================================================ import type {Config} from '@jest/types' // Sync object const config: Config.InitialOptions = { verbose: true, testPathIgnorePatterns: ['node_modules', '.history'], testEnvironment: 'jsdom', coveragePathIgnorePatterns: ['node_modules', '\\.mock\\.ts$'], } export default config ================================================ FILE: package.json ================================================ { "name": "version-rocket", "version": "1.7.4", "description": "Tools to check version monitoring (updates) for web application. web 应用版本监测(更新)工具", "main": "index.js", "scripts": { "build": "tsc", "prepublish": "npm run build", "test": "jest", "test:coverage": "jest --coverage", "codecov": "codecov" }, "repository": { "type": "git", "url": "git@github.com:guMcrey/version-rocket.git" }, "bin": { "generate-version-file": "./scripts/createVersionFile.js", "send-lark-message": "./scripts/sendMessageToLark.js", "send-wecom-message": "./scripts/sendMessageToWeCom.js" }, "keywords": [ "web", "version", "update", "versioning", "check version", "update version", "version check", "upgrade" ], "author": "hakuna", "license": "ISC", "dependencies": { "axios": "^1.6.0", "dayjs": "^1.11.3" }, "devDependencies": { "@babel/preset-env": "^7.18.10", "@babel/preset-typescript": "^7.18.6", "@jest/types": "^28.1.3", "@types/jest": "^28.1.6", "codecov": "^3.8.3", "jest": "^28.1.3", "jest-environment-jsdom": "^28.1.3", "ts-jest": "^28.0.7", "ts-node": "^10.9.1", "typescript": "^4.7.4" } } ================================================ FILE: scripts/createVersionFile.js ================================================ #!/usr/bin/env node /** * Gets the version in package.json and writes it to the dist directory of the application * usage: Add the shortcut command "generate-version-file" to the required place. */ const fs = require('fs'); const path = require('path'); const outputDir = process.argv.length > 2 ? process.argv.splice(2) : ['dist'] const getExternal = () => { const externalJSONPath = process.env.EXTERNAL_PATH?.trim() ? path.join(process.cwd(), process.env.EXTERNAL_PATH.trim()) : ''; const externalJSONObject = externalJSONPath ? fs.readFileSync(externalJSONPath).toString() : ''; const external = process.env.EXTERNAL?.trim() || externalJSONObject; try { JSON.parse(external) return external } catch (error) { return `"${external}"` } } outputDir.forEach((val) => { const outputVersionPath = path.join(process.cwd(), `${val}/version.json`); const packageJsonPath = path.join(process.cwd(), 'package.json'); const packageJsonObject = JSON.parse(fs.readFileSync(packageJsonPath).toString()); fs.writeFile(outputVersionPath, `{ "version": "${process.env.VERSION || packageJsonObject.version}", "external": ${getExternal()} }`, () => { console.log(`created ${val}/version file`, process.env.VERSION || packageJsonObject.version); }) }) ================================================ FILE: scripts/sendMessageToLark.js ================================================ #!/usr/bin/env node /** * send messages to lark web hook */ const axios = require('axios'); const fs = require('fs'); const path = require('path'); const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone'); dayjs.extend(utc); dayjs.extend(timezone); console.log('MESSAGE_PATH', process.env.MESSAGE_PATH); // lark-message-config-*.json const configFileName = process.env.MESSAGE_PATH ? `${process.env.MESSAGE_PATH}` : 'lark-message-config.json' const messageConfigPath = path.join(process.cwd(), configFileName); const messageConfigObject = JSON.parse(fs.readFileSync(messageConfigPath).toString()); // package.json const packageJsonName = process.env.PACKAGE_JSON_PATH ? `${process.env.PACKAGE_JSON_PATH}` : 'package.json' const packageJsonPath = path.join(process.cwd(), packageJsonName); console.log('PACKAGE_JSON_FULL_PATH', packageJsonPath) const packageJsonObject = JSON.parse(fs.readFileSync(packageJsonPath).toString()); // MESSAGE_JSON process.env.MESSAGE_JSON ? Object.assign(messageConfigObject, JSON.parse(process.env.MESSAGE_JSON)) : messageConfigObject; // https://jp.cybozu.help/general/zh/admin/list_systemadmin/list_localization/timezone.html // default: Asia/Shanghai const covertToTimezone = messageConfigObject.expectConvertToTimezone || 'Asia/Shanghai' const larkMessageJSON = (messageConfigObject.message && messageConfigObject.larkWebHook) ? messageConfigObject.message : { "msg_type": "interactive", "card": { "config": { "wide_screen_mode": true }, "header": { "template": messageConfigObject?.headerBgColor || "turquoise", "title": { "content": `🚀 ${messageConfigObject.title || ''}`, "tag": "plain_text" } }, "elements": [ { "fields": [ { "is_short": true, "text": { "content": `**📁 ${messageConfigObject.projectNameLabel || 'Project Name'}:**\n${messageConfigObject.projectName || ''}`, "tag": "lark_md" } }, { "is_short": true, "text": { "content": `**🔱 ${messageConfigObject.branchLabel || 'Branch'}:**\n${messageConfigObject.branch || ''}`, "tag": "lark_md" } }, { "is_short": false, "text": { "content": "", "tag": "lark_md" } }, { "is_short": true, "text": { "content": `**🎯 ${messageConfigObject.versionLabel || 'Version'}:**\n${messageConfigObject.version || packageJsonObject.version || ''}`, "tag": "lark_md" } }, { "is_short": true, "text": { "content": `**🔗 ${messageConfigObject.accessUrlLabel || 'URL'}:**\n${messageConfigObject.accessUrl || ''}`, "tag": "lark_md" } }, { "is_short": false, "text": { "content": "", "tag": "lark_md" } }, { "is_short": true, "text": { "content": `**🕐 ${messageConfigObject.timeLabel || 'Time'}:**\n${dayjs.tz(new Date(), covertToTimezone).format('YYYY-MM-DD HH:mm:ss')}`, "tag": "lark_md" } }, ], "tag": "div" } ] } } if (messageConfigObject.isNotifyAll) { const notifyAllObj = { "is_short": true, "text": { "content": `${messageConfigObject.isNotifyAll ? `**🔔 ${messageConfigObject.isNotifyAllLabel || 'Notify group members'}:**\n` : ""}`, "tag": "lark_md" } } const brObj = { "is_short": false, "text": { "content": "", "tag": "lark_md" } } larkMessageJSON.card.elements[0]?.fields.push(notifyAllObj); larkMessageJSON.card.elements[0]?.fields.push(brObj); } // TODO: dynamically card if (messageConfigObject.setDeployInfoInMainCard) { const deployObj = { "is_short": true, "text": { "content": `**🔨 ${messageConfigObject.setDeployInfoInMainCard && messageConfigObject.deployToolsLabel || 'Deploy Tools'}:**\n${messageConfigObject.deployTools || ''}`, "tag": "lark_md" } } larkMessageJSON.card.elements[0]?.fields.push(deployObj); } else if (messageConfigObject.deployToolsText || messageConfigObject.deployTools || messageConfigObject.remark) { const hrObj = { "tag": "hr" } const deployDefaultObj = { "elements": [ { "content": `${messageConfigObject.deployToolsText || `${messageConfigObject.deployTools ? `Deploy through ${messageConfigObject.deployTools}` : ''}`} ${(messageConfigObject.deployTools || messageConfigObject.deployToolsText) && messageConfigObject.remark ? '\n' : ''}${messageConfigObject.remark || ''}`, "tag": "plain_text" } ], "tag": "note" } larkMessageJSON.card.elements?.push(hrObj) larkMessageJSON.card.elements?.push(deployDefaultObj) } if (messageConfigObject.remark) { const hrObj = { "tag": "hr" } const remarkObj = { "elements": [ { "content": `${messageConfigObject.remark || ''}`, "tag": "plain_text" } ], "tag": "note" } if (messageConfigObject.setDeployInfoInMainCard) { larkMessageJSON.card.elements?.push(hrObj) larkMessageJSON.card.elements?.push(remarkObj) } } axios.post(messageConfigObject.larkWebHook, larkMessageJSON).catch((e) => { console.warn('send lark error', e.response?.data || e) }); ================================================ FILE: scripts/sendMessageToWeCom.js ================================================ #!/usr/bin/env node /** * send messages to WeCom */ const axios = require('axios'); const fs = require('fs'); const path = require('path'); const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone'); dayjs.extend(utc); dayjs.extend(timezone); console.log('MESSAGE_PATH', process.env.MESSAGE_PATH); // message-config-*.json const configFileName = process.env.MESSAGE_PATH ? `${process.env.MESSAGE_PATH}` : 'message-config.json' const messageConfigPath = path.join(process.cwd(), configFileName); const messageConfigObject = JSON.parse(fs.readFileSync(messageConfigPath).toString()); // package.json const packageJsonName = process.env.PACKAGE_JSON_PATH ? `${process.env.PACKAGE_JSON_PATH}` : 'package.json' const packageJsonPath = path.join(process.cwd(), packageJsonName); console.log('PACKAGE_JSON_FULL_PATH', packageJsonPath) const packageJsonObject = JSON.parse(fs.readFileSync(packageJsonPath).toString()); // TODO: MESSAGE_JSON process.env.MESSAGE_JSON ? Object.assign(messageConfigObject, JSON.parse(process.env.MESSAGE_JSON)) : messageConfigObject; // https://jp.cybozu.help/general/zh/admin/list_systemadmin/list_localization/timezone.html // default: Asia/Shanghai const covertToTimezone = messageConfigObject.expectConvertToTimezone || 'Asia/Shanghai' const larkMessageJSON = (messageConfigObject.message && messageConfigObject.webHook) ? messageConfigObject.message : `{ "msgtype": "markdown", "markdown": { "content": "**🚀 ${messageConfigObject.title}**\n > ${messageConfigObject.projectNameLabel || 'Project Name'}: ${messageConfigObject.projectName || ''} > ${messageConfigObject.branchLabel || 'Branch'}: ${messageConfigObject.branch || ''} > ${messageConfigObject.versionLabel || 'Version'}: ${messageConfigObject.version || packageJsonObject.version || ''} > ${messageConfigObject.accessUrlLabel || 'URL'}: [${messageConfigObject.accessUrl || ''}](${messageConfigObject.accessUrl || ''}) > ${messageConfigObject.timeLabel || 'Time'}: ${dayjs.tz(new Date(), covertToTimezone).format('YYYY-MM-DD HH:mm:ss')} >${messageConfigObject.isNotifyAll ? '@all' : ''} ${messageConfigObject.deployToolsText || messageConfigObject.remark ? '\n' : ''} >${messageConfigObject.deployToolsText || ''} >${messageConfigObject.remark || ''}" } }` axios.post(messageConfigObject.webHook, larkMessageJSON).catch((e) => { console.warn('send WeCom error', e.response?.data || e) }); ================================================ FILE: tests/index.mock.ts ================================================ const _global: any = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {} export const mockCreateObjectUrl = () => { if (!_global.window) { _global.window = Object.create(window) } Object.defineProperty(window, 'URL', { value: { createObjectURL: (blob: Blob): string => '7584ef69-b1ca-46aa-95ac-35472a7a6b06', }, }) } export const mockWorker = () => { class Worker { url: string onmessage: (msg: string) => void constructor(url: string) { this.url = url this.onmessage = () => {} } postMessage(msg: string) { this.onmessage(msg) } terminate() { console.log('stop') } } if (!_global.window) { _global.window = Object.create(window) } window.Worker = Worker as any } export const mockSetInterval = () => { if (!_global.window) { ;(_global as any).window = Object.create(window) } ;(window as any).setInterval = (func: () => void) => { func() } } export const mockFetch = () => { if (!_global.window) { _global.window = Object.create(window) as any } ;(window as any).fetch = (url: string) => { return new Promise((resolve, reject) => { resolve({ json: () => { return Promise.resolve({version: '1.2.0'}) }, }) }) } } export const mockFetchHeader = (options?: { mode?: 'etag' | 'last-modified' }) => { const hashMap = {} if (!_global.window) { _global.window = Object.create(window) as any } ;(window as any).fetch = (url: string) => { const useEtag = url.endsWith('?useEtag') return new Promise((resolve, reject) => { resolve({ headers: { get: (header) => { hashMap[url] = hashMap[url] !== undefined ? hashMap[url] + 1 : hashMap[url] + 0 if (header === 'ETag') { return useEtag ? Promise.resolve(`${url}:test-etag:${hashMap[url]}`) : undefined } else if (header === 'Last-Modified') { return !useEtag ? Promise.resolve(`${url}:test-last-modified:${hashMap[url]}`) : undefined } }, }, }) }) } } export const mockDateToLocaleDateString = () => { if (!_global.window) { _global.window = Object.create(window) } class MyDate { toLocaleDateString() { return '2023/1/18' } } Object.defineProperty(window, 'Date', { value: MyDate, }) } ================================================ FILE: tests/index.test.ts ================================================ /** * @jest-environment jsdom */ import { createWorker, createWorkerFunc, cancelUpdateFunc, checkVersionTypeFunc, } from '../utils/index' import { mockCreateObjectUrl, mockWorker, mockSetInterval, mockFetch, mockDateToLocaleDateString, mockFetchHeader, } from './index.mock' test('create web worker', () => { mockCreateObjectUrl() mockWorker() expect(createWorker(() => console.log(1)) instanceof Worker).toBeTruthy() }) test('create worker function', () => { mockSetInterval() mockFetch() const temp = createWorkerFunc() as any temp.onmessage({ data: { 'version-key': '1.1.0', 'polling-time': 0, immediate: false, 'origin-version-file-url': 'https://www.example.com', 'clear-interval-on-dialog': true, }, }) temp.postMessage = (obj: {refreshPageVersion: string}) => { expect(typeof obj === 'object' && obj.refreshPageVersion).toBeTruthy() } }) test('cancel update function', () => { mockWorker() const worker = new Worker('https://test.com') // ignore-current-version -> true localStorage.setItem('version-rocket:cancelled', '1.0.0') expect( cancelUpdateFunc('ignore-current-version', '1.0.0', true, worker) ).toBeTruthy() expect( cancelUpdateFunc('ignore-current-version', '1.0.0', true, undefined) ).toBeTruthy() expect(cancelUpdateFunc(undefined, '1.0.0', true, undefined)).toBeTruthy() // ignore-current-version -> false localStorage.setItem('version-rocket:cancelled', '1.1.0') expect( cancelUpdateFunc('ignore-current-version', '1.0.0', false, worker) ).toBeFalsy() // ignore-today -> true mockDateToLocaleDateString() localStorage.setItem('version-rocket:cancelled', '2023/1/18') expect(cancelUpdateFunc('ignore-today', '1.0.0', true, worker)).toBeTruthy() expect( cancelUpdateFunc('ignore-today', '1.0.0', true, undefined) ).toBeTruthy() // ignore-today -> false localStorage.setItem('version-rocket:cancelled', '2023/1/19') expect(cancelUpdateFunc('ignore-today', '1.0.0', false, worker)).toBeFalsy() // ignore-current-version -> true sessionStorage.setItem('version-rocket:cancelled', 'true') expect( cancelUpdateFunc('ignore-current-window', '1.0.0', true, worker) ).toBeTruthy() expect( cancelUpdateFunc('ignore-current-window', '1.0.0', true, undefined) ).toBeTruthy() // ignore-current-version -> false sessionStorage.setItem('version-rocket:cancelled', '') expect( cancelUpdateFunc('ignore-current-window', '1.0.0', false, worker) ).toBeFalsy() // default expect(cancelUpdateFunc('ignore-test', '1.0.0', false, worker)).toBeFalsy() // no args localStorage.setItem('version-rocket:cancelled', '1.1.0') expect(cancelUpdateFunc(undefined, '', undefined, undefined)).toBeFalsy() localStorage.setItem('version-rocket:cancelled', '') expect(cancelUpdateFunc('ignore-today', '', undefined, undefined)).toBeFalsy() sessionStorage.setItem('version-rocket:cancelled', '') expect(cancelUpdateFunc('ignore-today', '', undefined, undefined)).toBeFalsy() }) test('check version type', () => { // check-version expect( checkVersionTypeFunc('1.0.0', 'https://test.com/version.json') ).toEqual('check-version') // check-specific-files expect(checkVersionTypeFunc('', '', ['https://test.com/index.html'])).toEqual( 'check-specified-files' ) // undefined expect(checkVersionTypeFunc('', '', [])).toEqual(undefined) expect(checkVersionTypeFunc()).toEqual(undefined) }) test('check origin specified files url one mode', async () => { mockSetInterval() mockFetchHeader() const temp = createWorkerFunc() as any temp.onmessage({ data: { 'polling-time': 0, immediate: true, 'check-origin-specified-files-url': [ 'https://www.example.com/index.html', ], 'check-origin-specified-files-url-mode': 'one', 'clear-interval-on-dialog': true, }, }) temp.postMessage = (obj: { refreshPageVisible: string refreshPageVersion: string external: string }) => { expect( typeof obj === 'object' && obj.refreshPageVisible && obj.refreshPageVersion === '' && obj.external === '' ).toBeTruthy() } }) test('check origin specified files url all mode', async () => { mockSetInterval() mockFetchHeader() const temp = createWorkerFunc() as any temp.onmessage({ data: { 'version-key': '1.1.0', 'polling-time': 0, immediate: true, 'check-origin-specified-files-url': [ 'https://www.example.com/index.html', 'https://www.example.com/index.html?useEtag', ], 'check-origin-specified-files-url-mode': 'all', 'clear-interval-on-dialog': true, }, }) temp.postMessage = (obj: { refreshPageVisible: string refreshPageVersion: string external: string }) => { console.log('obj', obj) expect( typeof obj === 'object' && obj.refreshPageVisible && obj.refreshPageVersion === '' && obj.external === '' ).toBeTruthy() } }) test('Not found localPackageVersion, originVersionFileUrl or originSpecifiedFilesUrl', async () => { mockSetInterval() mockFetchHeader() const temp = createWorkerFunc() as any temp.postMessage = (obj: {invalidParams}) => { expect(typeof obj === 'object' && obj.invalidParams).toBeTruthy() } temp.onmessage({ data: { 'version-key': '', 'polling-time': 0, immediate: true, 'check-origin-specified-files-url-mode': 'all', }, }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ "module": "ESNext", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ "types": ["jest"], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outDir": "./", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true, /* Skip type checking all .d.ts files. */ }, "include": ["./"], "exclude": ["./tests", "./jest.config.ts"] } ================================================ FILE: utils/index.ts ================================================ // create package version worker export const createWorker = (func: () => void, deps?: Array<() => void>) => { const depsFuncStr = `${deps?.map((_) => _.toString()).join(';\n\n') || ''}` const blob = new Blob([ ` ${depsFuncStr}; (${func.toString()})(); `, ]) const url = window.URL.createObjectURL(blob) const worker = new Worker(url) return worker } export const createWorkerFunc = () => { let oldVersion = '' const flagSet = new Set() let intervalTime = 5000 let immediate = false let originFileUrl = '' let checkOriginSpecifiedFilesUrl: string[] = [] let checkOriginSpecifiedFilesUrlMode: 'one' | 'all' = 'one' let timer: any = null let clearIntervalOnDialog = false const temp: Worker = self as any temp.onmessage = (event: any) => { oldVersion = event.data['version-key'] intervalTime = event.data['polling-time'] immediate = event.data['immediate'] originFileUrl = event.data['origin-version-file-url'] checkOriginSpecifiedFilesUrl = event.data['check-origin-specified-files-url'] checkOriginSpecifiedFilesUrlMode = event.data['check-origin-specified-files-url-mode'] clearIntervalOnDialog = event.data['clear-interval-on-dialog'] const checkVersionType = checkVersionTypeFunc( oldVersion, originFileUrl, checkOriginSpecifiedFilesUrl ) if (!checkVersionType) { temp.postMessage({ invalidParams: true, }) } const doFetch = () => { if (checkVersionType === 'check-version') { fetch(`${originFileUrl}?${+new Date()}`) .then((res) => { return res.json() }) .then((versionJsonFile) => { if (oldVersion !== versionJsonFile.version) { temp.postMessage({ refreshPageVisible: true, refreshPageVersion: `${versionJsonFile.version}`, external: versionJsonFile.external, }) if (clearIntervalOnDialog) { clearInterval(timer) } } }) } if (checkVersionType === 'check-specified-files') { if (!checkOriginSpecifiedFilesUrl?.length) return checkOriginSpecifiedFilesUrl.forEach((url: string) => { fetch(url, { method: 'HEAD', cache: 'no-cache', }) .then((res) => { return res.headers.get('ETag') || res.headers.get('Last-Modified') }) .then((flag: string | null) => { if (flag) { flagSet.add(flag) } if ( checkOriginSpecifiedFilesUrlMode === 'one' && flagSet.size > checkOriginSpecifiedFilesUrl.length ) { temp.postMessage({ refreshPageVisible: true, refreshPageVersion: '', external: '', }) flagSet.clear() if (clearIntervalOnDialog) { clearInterval(timer) } } if ( checkOriginSpecifiedFilesUrlMode === 'all' && flagSet.size === checkOriginSpecifiedFilesUrl.length * 2 ) { temp.postMessage({ refreshPageVisible: true, refreshPageVersion: '', external: '', }) flagSet.clear() if (clearIntervalOnDialog) { clearInterval(timer) } } }) }) } } if (immediate) { doFetch() } timer = setInterval(doFetch, intervalTime) } return temp } // cancel update export const cancelUpdateFunc = ( cancelMode: string | undefined, newVersion: string | undefined, cancelUpdateAndStopWorker: boolean | undefined, worker: Worker | undefined ) => { const cancelModeType = cancelMode || (newVersion ? 'ignore-current-version' : 'ignore-current-window') const cancelModeTypeValue = localStorage.getItem('version-rocket:cancelled') || '' const todayDate = new Date().toLocaleDateString() const cancelModeTypeValueInSession = sessionStorage.getItem('version-rocket:cancelled') || '' const isStopWorker = cancelUpdateAndStopWorker || false switch (cancelModeType) { case 'ignore-current-version': if (newVersion && cancelModeTypeValue === newVersion) { isStopWorker && worker?.terminate() return true } break case 'ignore-today': if (cancelModeTypeValue === todayDate) { isStopWorker && worker?.terminate() return true } break case 'ignore-current-window': if (cancelModeTypeValueInSession) { isStopWorker && worker?.terminate() return true } break default: break } return false } // check version type export function checkVersionTypeFunc( oldVersion?: string | undefined, originFileUrl?: string | undefined, checkOriginSpecifiedFilesUrl?: string[] | undefined ) { const checkVersionType = oldVersion && originFileUrl ? 'check-version' : checkOriginSpecifiedFilesUrl?.length ? 'check-specified-files' : '' if (!checkVersionType) return console.log( 'Not found localPackageVersion, originVersionFileUrl or originSpecifiedFilesUrl' ) console.log('You are use check version type is', checkVersionType) return checkVersionType }