@microbt/pwa 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/sw.js ADDED
@@ -0,0 +1,194 @@
1
+ // src/sw.ts
2
+ import { CacheableResponsePlugin } from "workbox-cacheable-response";
3
+ import { clientsClaim, skipWaiting } from "workbox-core";
4
+ import { ExpirationPlugin } from "workbox-expiration";
5
+ import { registerRoute, setDefaultHandler } from "workbox-routing";
6
+ import { CacheFirst, NetworkFirst, NetworkOnly, StaleWhileRevalidate } from "workbox-strategies";
7
+ import "./sw-polyfills";
8
+ import {
9
+ JS_CSS_CACHE_NAME_PREFIX,
10
+ purgeMismatchedCacheEntries,
11
+ STATIC_ASSETS_CACHE_NAME_PREFIX,
12
+ ValidateResponsePlugin
13
+ } from "./sw-cache-validation";
14
+ import { ChunkDiagnosticsPlugin } from "./sw-chunk-diagnostics";
15
+ import { createSwRejectionPolicy, serializeSwRejection } from "./sw-error-filter";
16
+ import { isHttpOrHttpsRequest } from "./sw-request-filter";
17
+ function setupServiceWorker(options) {
18
+ clientsClaim();
19
+ skipWaiting();
20
+ setDefaultHandler(new NetworkOnly());
21
+ registerRoute(
22
+ ({ request }) => request.mode === "navigate" && isHttpOrHttpsRequest(request.url),
23
+ new NetworkFirst({
24
+ cacheName: `cache-pages-${self.location.origin}`,
25
+ networkTimeoutSeconds: 3,
26
+ plugins: [
27
+ new CacheableResponsePlugin({
28
+ statuses: [0, 200]
29
+ }),
30
+ new ExpirationPlugin({
31
+ maxEntries: 10,
32
+ maxAgeSeconds: 60 * 60 * 24 * 3,
33
+ // 3 天,须短于 chunk 的 7 天
34
+ purgeOnQuotaError: true
35
+ })
36
+ ]
37
+ })
38
+ );
39
+ class CORSPlugin {
40
+ async requestWillFetch({ request }) {
41
+ return new Request(request, {
42
+ mode: "cors",
43
+ credentials: "same-origin"
44
+ });
45
+ }
46
+ }
47
+ function shouldNotCacheImage(url) {
48
+ try {
49
+ const urlObj = new URL(url);
50
+ const authParams = [
51
+ "Signature",
52
+ "OSSAccessKeyId",
53
+ "Expires",
54
+ "token",
55
+ "auth",
56
+ "key",
57
+ "apikey",
58
+ "api_key",
59
+ "access_token",
60
+ "x-oss-signature"
61
+ ];
62
+ for (const param of authParams) {
63
+ if (urlObj.searchParams.has(param)) {
64
+ console.log(`图片URL包含授权参数 ${param},不进行缓存: ${url}`);
65
+ return true;
66
+ }
67
+ }
68
+ if (urlObj.search && urlObj.search.length > 20) {
69
+ console.log(`图片URL包含较长查询参数,可能是授权URL,不进行缓存: ${url}`);
70
+ return true;
71
+ }
72
+ return false;
73
+ } catch (error) {
74
+ console.error(`URL解析错误,不进行缓存: ${url}`, error);
75
+ return true;
76
+ }
77
+ }
78
+ registerRoute(
79
+ ({ request }) => {
80
+ if (!isHttpOrHttpsRequest(request.url)) {
81
+ return false;
82
+ }
83
+ if (request.destination === "image") {
84
+ return !shouldNotCacheImage(request.url);
85
+ }
86
+ return request.destination === "font";
87
+ },
88
+ new CacheFirst({
89
+ cacheName: `${STATIC_ASSETS_CACHE_NAME_PREFIX}${self.location.origin}`,
90
+ plugins: [
91
+ // new CORSPlugin(),
92
+ new CacheableResponsePlugin({
93
+ statuses: [0, 200]
94
+ }),
95
+ new ValidateResponsePlugin(),
96
+ new ExpirationPlugin({
97
+ maxEntries: 50,
98
+ // 减少最大缓存条目数量
99
+ maxAgeSeconds: 60 * 60 * 24 * 7,
100
+ // 减少缓存时间为7天
101
+ purgeOnQuotaError: true
102
+ })
103
+ ]
104
+ })
105
+ );
106
+ registerRoute(
107
+ ({ request }) => {
108
+ if (!isHttpOrHttpsRequest(request.url)) {
109
+ return false;
110
+ }
111
+ return request.destination === "script" || request.destination === "style";
112
+ },
113
+ new StaleWhileRevalidate({
114
+ cacheName: `${JS_CSS_CACHE_NAME_PREFIX}${self.location.origin}`,
115
+ plugins: [
116
+ new CORSPlugin(),
117
+ new CacheableResponsePlugin({
118
+ statuses: [0, 200]
119
+ }),
120
+ new ValidateResponsePlugin(),
121
+ new ExpirationPlugin({
122
+ // 单版本构建实测 43 个 js/css 文件,30 上限必然误逐当前版本 chunk
123
+ // (LRU 逐出后弱网/旧版本清理场景直接白屏),须大于单版本全量
124
+ maxEntries: 60,
125
+ maxAgeSeconds: 60 * 60 * 24 * 7,
126
+ // 减少缓存时间为7天
127
+ purgeOnQuotaError: true
128
+ }),
129
+ // 放在 CORS 改写和缓存过滤之后,只观察最终请求/缓存结果。
130
+ new ChunkDiagnosticsPlugin({ release: options.release })
131
+ ]
132
+ })
133
+ );
134
+ self.addEventListener("error", (event) => {
135
+ var _a;
136
+ const errorInfo = {
137
+ type: "error",
138
+ message: event.message,
139
+ filename: event.filename,
140
+ line: event.lineno,
141
+ column: event.colno,
142
+ error: event.error,
143
+ stack: ((_a = event.error) == null ? void 0 : _a.stack) || new Error().stack
144
+ };
145
+ console.error("[ServiceWorker Error]", errorInfo);
146
+ self.clients.matchAll().then((clients) => {
147
+ clients.forEach((client) => {
148
+ client.postMessage({
149
+ type: "SW_ERROR",
150
+ payload: errorInfo
151
+ });
152
+ });
153
+ }).catch((err) => {
154
+ console.error("[ServiceWorker] Error posting error to clients:", err);
155
+ });
156
+ });
157
+ const classifyRejection = createSwRejectionPolicy();
158
+ self.addEventListener("unhandledrejection", (event) => {
159
+ const details = serializeSwRejection(event.reason);
160
+ const rejectionInfo = {
161
+ // 保持 reason 为可读文本,旧页面也能消费新 SW 消息。
162
+ reason: details.name ? `${details.name}: ${details.message}` : details.message,
163
+ stack: details.stack || new Error().stack,
164
+ ...classifyRejection(details)
165
+ };
166
+ if (rejectionInfo.reportError) {
167
+ console.error("[ServiceWorker Unhandled Rejection]", rejectionInfo);
168
+ }
169
+ self.clients.matchAll().then((clients) => {
170
+ clients.forEach((client) => {
171
+ client.postMessage({
172
+ type: "SW_UNHANDLED_REJECTION",
173
+ payload: rejectionInfo
174
+ });
175
+ });
176
+ }).catch((err) => {
177
+ console.error("[ServiceWorker] Error posting unhandled rejection to clients:", err);
178
+ });
179
+ });
180
+ self.addEventListener("activate", (event) => {
181
+ event.waitUntil(
182
+ Promise.all([
183
+ purgeMismatchedCacheEntries(),
184
+ // 清理 pageCache recipe 时代的遗留 HTML 缓存:无过期机制,且新 HTML
185
+ // 路由已改用带 ExpirationPlugin 的 cache-pages-(旧条目若无时间戳
186
+ // 会被 ExpirationPlugin 视为永不过期,不能改名复用)
187
+ caches.delete("pages")
188
+ ])
189
+ );
190
+ });
191
+ }
192
+ export {
193
+ setupServiceWorker
194
+ };
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
2
  "name": "@microbt/pwa",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "microbt service worker runtime and page-side registration for web apps",
5
+ "keywords": [
6
+ "microbt",
7
+ "pwa",
8
+ "service-worker",
9
+ "workbox"
10
+ ],
11
+ "license": "MIT",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/esm/index.d.ts",
15
+ "import": "./dist/esm/index.js"
16
+ },
17
+ "./protocol": {
18
+ "types": "./dist/esm/protocol.d.ts",
19
+ "import": "./dist/esm/protocol.js"
20
+ },
21
+ "./sw": {
22
+ "types": "./dist/esm/sw.d.ts",
23
+ "import": "./dist/esm/sw.js"
24
+ }
25
+ },
26
+ "main": "dist/esm/index.js",
27
+ "module": "dist/esm/index.js",
28
+ "types": "dist/esm/index.d.ts",
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "build": "father build",
34
+ "build:deps": "father prebundle",
35
+ "dev": "father dev",
36
+ "format": "prettier --cache --write .",
37
+ "lint": "eslint . --fix ",
38
+ "prepare": "husky install",
39
+ "prepublishOnly": "father doctor && npm run build",
40
+ "postpublish": "cnpm sync @microbt/pwa",
41
+ "test": "vitest run"
42
+ },
43
+ "lint-staged": {
44
+ "*.{md,json}": [
45
+ "prettier --cache --write"
46
+ ],
47
+ "*.{js,jsx}": [
48
+ "eslint --fix",
49
+ "prettier --cache --write"
50
+ ],
51
+ "*.ts?(x)": [
52
+ "eslint --fix",
53
+ "prettier --cache --parser=typescript --write"
54
+ ]
55
+ },
56
+ "dependencies": {
57
+ "workbox-cacheable-response": "^7.0.0",
58
+ "workbox-core": "^7.0.0",
59
+ "workbox-expiration": "^7.0.0",
60
+ "workbox-routing": "^7.0.0",
61
+ "workbox-strategies": "^7.0.0",
62
+ "workbox-window": "^7.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@commitlint/cli": "^17.4.4",
66
+ "@commitlint/config-conventional": "^17.4.4",
67
+ "@microbt/environment": "^1.2.14",
68
+ "@types/node": "^22.13.4",
69
+ "@typescript-eslint/eslint-plugin": "^5.54.1",
70
+ "@typescript-eslint/parser": "^5.54.1",
71
+ "eslint": "^8.36.0",
72
+ "father": "^4.1.0",
73
+ "husky": "^8.0.3",
74
+ "lint-staged": "^13.2.0",
75
+ "prettier": "^2.8.4",
76
+ "prettier-plugin-organize-imports": "^3.2.2",
77
+ "prettier-plugin-packagejson": "^2.4.3",
78
+ "typescript": "^5.4.2",
79
+ "vitest": "^1.6.1"
80
+ },
81
+ "peerDependencies": {
82
+ "@microbt/environment": "^1.2.12"
83
+ },
84
+ "publishConfig": {
85
+ "access": "public",
86
+ "registry": "https://registry.npmjs.org"
87
+ }
88
+ }