@microbt/pwa 0.0.0-stage → 0.1.0

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.
Files changed (40) hide show
  1. package/README.md +87 -2
  2. package/dist/esm/index.d.ts +2 -0
  3. package/dist/esm/index.js +7 -0
  4. package/dist/esm/protocol.d.ts +2 -0
  5. package/dist/esm/protocol.js +11 -0
  6. package/dist/esm/recovery.d.ts +3 -0
  7. package/dist/esm/recovery.js +11 -0
  8. package/dist/esm/shared/chunk-diagnostic-outbox.d.ts +13 -0
  9. package/dist/esm/shared/chunk-diagnostic-outbox.js +137 -0
  10. package/dist/esm/shared/chunk-diagnostic-payload.d.ts +17 -0
  11. package/dist/esm/shared/chunk-diagnostic-payload.js +162 -0
  12. package/dist/esm/shared/chunk-diagnostic-reporter.d.ts +9 -0
  13. package/dist/esm/shared/chunk-diagnostic-reporter.js +114 -0
  14. package/dist/esm/shared/chunk-diagnostics.d.ts +26 -0
  15. package/dist/esm/shared/chunk-diagnostics.js +18 -0
  16. package/dist/esm/shared/chunk-error-recovery.d.ts +51 -0
  17. package/dist/esm/shared/chunk-error-recovery.js +225 -0
  18. package/dist/esm/shared/pwa-registration-telemetry.d.ts +4 -0
  19. package/dist/esm/shared/pwa-registration-telemetry.js +38 -0
  20. package/dist/esm/shared/pwa-retry.d.ts +26 -0
  21. package/dist/esm/shared/pwa-retry.js +133 -0
  22. package/dist/esm/shared/pwa.d.ts +5 -0
  23. package/dist/esm/shared/pwa.js +231 -0
  24. package/dist/esm/shared/sw-registration.d.ts +11 -0
  25. package/dist/esm/shared/sw-registration.js +35 -0
  26. package/dist/esm/shared/sw-script-url.d.ts +42 -0
  27. package/dist/esm/shared/sw-script-url.js +49 -0
  28. package/dist/esm/sw-cache-validation.d.ts +51 -0
  29. package/dist/esm/sw-cache-validation.js +111 -0
  30. package/dist/esm/sw-chunk-diagnostics.d.ts +19 -0
  31. package/dist/esm/sw-chunk-diagnostics.js +124 -0
  32. package/dist/esm/sw-error-filter.d.ts +58 -0
  33. package/dist/esm/sw-error-filter.js +87 -0
  34. package/dist/esm/sw-polyfills.d.ts +1 -0
  35. package/dist/esm/sw-polyfills.js +14 -0
  36. package/dist/esm/sw-request-filter.d.ts +13 -0
  37. package/dist/esm/sw-request-filter.js +7 -0
  38. package/dist/esm/sw.d.ts +4 -0
  39. package/dist/esm/sw.js +194 -0
  40. package/package.json +90 -4
@@ -0,0 +1,58 @@
1
+ /** 固定计数项,避免把 URL、错误文案等高基数内容用作监控指标名。 */
2
+ export declare const SW_REJECTION_METRICS: {
3
+ readonly 'cache-write-network': "SW-CACHE-WRITE-NETWORK";
4
+ readonly 'cache-entry-exists': "SW-CACHE-ENTRY-EXISTS";
5
+ readonly 'cache-expiration-cursor': "SW-CACHE-EXPIRATION-CURSOR";
6
+ readonly 'feedback-media-no-response': "SW-FEEDBACK-MEDIA-NO-RESPONSE";
7
+ };
8
+ export interface SwRejectionDetails {
9
+ name: string;
10
+ message: string;
11
+ stack: string;
12
+ }
13
+ /** 在 SW 内转成纯字符串再 postMessage,保留 DOMException 信息并避免克隆失败。 */
14
+ export declare function serializeSwRejection(reason: unknown): SwRejectionDetails;
15
+ /**
16
+ * SW unhandledrejection 上报原则:影响不明就保留异常,已知重复故障才减少 error 频率。
17
+ * 本策略只处理未被业务捕获的 Promise 拒绝;SW 的 error 事件仍走独立的全量上报。
18
+ *
19
+ * 哪些上报:
20
+ * 1. 普通 NetworkError、Failed to fetch、AbortError、非反馈媒体的
21
+ * no-response 和所有未知错误:每次报 error。全局事件不能证明主动取消
22
+ * 或缓存已兜底;网络失败也可能是配置问题,非反馈媒体的 no-response
23
+ * (CDN chunk、页面导航)可能是资源最终加载失败(白屏信号),不能
24
+ * 当成用户断网直接忽略。
25
+ * cache-put-with-no-response 是另一种错误,同样每次报,不能用子串混为一类。
26
+ * 2. getCategory 精确匹配的四类故障:Cache.put 网络写入失败、缓存条目已存在、
27
+ * IndexedDB 游标异常、签名反馈媒体 URL 的 no-response(反馈图片/视频
28
+ * 请求失败,原因未明——日志只有 error:{},无法区分网络、取消或 CORS;
29
+ * 此类失败不伤 app 外壳)。每次通知页面记 sum;每类首次及距离上次允许
30
+ * 上报满 60 秒的下一次故障,同时报 error。其他错误名或文案变体回到第 1 条。
31
+ *
32
+ * 哪些不报 error:只有第 2 条中距上次允许上报不足 60 秒的同类重复故障。
33
+ * 此时仍通知页面记 sum,并非完全丢弃;当前没有仅凭错误名就彻底忽略的类别。
34
+ * 60 秒是控制重复量的固定窗口,不代表故障已恢复;重复事件不会推迟窗口,
35
+ * 到期也不会主动补报,必须有下一次故障才再次报 error。
36
+ *
37
+ * 限频的本质是故障详情抽样,不是"重复去重、信号不丢":限频键只有
38
+ * category,同类中不同 URL 的故障共享窗口——视频 A 首次报错后,视频 B
39
+ * 在一分钟内首次失败只剩 sum,B 的 URL 和堆栈不会上报。同时 SW 仍向
40
+ * 所有页面广播,多页面打开时一次故障可能产生多份 error/sum,本策略不
41
+ * 解决广播重复。接受这两个取舍的理由是此类故障原因未明且不伤外壳,
42
+ * 保留频次与代表性样本即可,不需要每一次详情。
43
+ *
44
+ * 为什么仍保留缓存故障:写入失败影响后续离线使用,游标异常可能中断缓存淘汰。
45
+ * 即使源于浏览器问题,也需保留频次和代表性堆栈;本策略不负责修复底层故障。
46
+ * 限频按 SW 实例、按类别独立保存,重启或时钟回拨后重新允许上报。
47
+ * sum 是页面接收通知的次数,多页面会重复、无客户端会漏收,不是准确的故障总数。
48
+ * 新旧 SW/页面共存时,旧页面不识别限频字段会继续全量报 error,待页面更新后生效。
49
+ * 若以后要忽略主动取消或成功兜底的请求,应在掌握请求结果的调用点处理,
50
+ * 不应在这里扩充关键词黑名单。NetworkFirst 超时只尝试缓存兜底,不会 abort 请求。
51
+ */
52
+ export declare function createSwRejectionPolicy(): (details: SwRejectionDetails, now?: number) => {
53
+ reportError: boolean;
54
+ category?: undefined;
55
+ } | {
56
+ category: "cache-write-network" | "cache-entry-exists" | "cache-expiration-cursor" | "feedback-media-no-response";
57
+ reportError: boolean;
58
+ };
@@ -0,0 +1,87 @@
1
+ // src/sw-error-filter.ts
2
+ var SW_REJECTION_METRICS = {
3
+ "cache-write-network": "SW-CACHE-WRITE-NETWORK",
4
+ "cache-entry-exists": "SW-CACHE-ENTRY-EXISTS",
5
+ "cache-expiration-cursor": "SW-CACHE-EXPIRATION-CURSOR",
6
+ "feedback-media-no-response": "SW-FEEDBACK-MEDIA-NO-RESPONSE"
7
+ };
8
+ function serializeSwRejection(reason) {
9
+ try {
10
+ if (reason !== null && typeof reason === "object") {
11
+ const error = reason;
12
+ return {
13
+ name: typeof error.name === "string" ? error.name : "",
14
+ message: typeof error.message === "string" ? error.message : JSON.stringify(reason) || String(reason),
15
+ stack: typeof error.stack === "string" ? error.stack : ""
16
+ };
17
+ }
18
+ return { name: "", message: String(reason), stack: "" };
19
+ } catch {
20
+ return {
21
+ name: "",
22
+ message: "[Unserializable rejection reason]",
23
+ stack: ""
24
+ };
25
+ }
26
+ }
27
+ var NO_RESPONSE_MESSAGE_PREFIX = "no-response :: ";
28
+ var FEEDBACK_MEDIA_HOST = "super-acme-online.oss-cn-hangzhou.aliyuncs.com";
29
+ var FEEDBACK_MEDIA_PATH_PREFIXES = ["/saas/feedbackVideo/", "/user-center/feedback/"];
30
+ function isSignedFeedbackMediaNoResponse(message) {
31
+ var _a;
32
+ if (!message.startsWith(NO_RESPONSE_MESSAGE_PREFIX)) {
33
+ return false;
34
+ }
35
+ try {
36
+ const details = JSON.parse(message.slice(NO_RESPONSE_MESSAGE_PREFIX.length));
37
+ const url = Array.isArray(details) ? (_a = details[0]) == null ? void 0 : _a.url : void 0;
38
+ if (typeof url !== "string") {
39
+ return false;
40
+ }
41
+ const parsed = new URL(url);
42
+ if (parsed.hostname !== FEEDBACK_MEDIA_HOST) {
43
+ return false;
44
+ }
45
+ if (!FEEDBACK_MEDIA_PATH_PREFIXES.some((prefix) => parsed.pathname.startsWith(prefix))) {
46
+ return false;
47
+ }
48
+ return parsed.searchParams.has("Expires") && parsed.searchParams.has("Signature");
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function getCategory({ name, message }) {
54
+ if (name === "NetworkError" && message === "Failed to execute 'put' on 'Cache': Cache.put() encountered a network error") {
55
+ return "cache-write-network";
56
+ }
57
+ if (name === "InvalidAccessError" && message === "Failed to execute 'put' on 'Cache': Entry already exists.") {
58
+ return "cache-entry-exists";
59
+ }
60
+ if (name === "UnknownError" && message === "Attempt to iterate a cursor that doesn't exist") {
61
+ return "cache-expiration-cursor";
62
+ }
63
+ if (name === "no-response" && isSignedFeedbackMediaNoResponse(message)) {
64
+ return "feedback-media-no-response";
65
+ }
66
+ return void 0;
67
+ }
68
+ function createSwRejectionPolicy() {
69
+ const lastReported = /* @__PURE__ */ new Map();
70
+ return (details, now = Date.now()) => {
71
+ const category = getCategory(details);
72
+ if (!category) {
73
+ return { reportError: true };
74
+ }
75
+ const previous = lastReported.get(category);
76
+ const reportError = previous === void 0 || now < previous || now - previous >= 6e4;
77
+ if (reportError) {
78
+ lastReported.set(category, now);
79
+ }
80
+ return { category, reportError };
81
+ };
82
+ }
83
+ export {
84
+ SW_REJECTION_METRICS,
85
+ createSwRejectionPolicy,
86
+ serializeSwRejection
87
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ // src/sw-polyfills.ts
2
+ if (typeof Promise.allSettled !== "function") {
3
+ Promise.allSettled = function(values) {
4
+ return Promise.all(
5
+ Array.from(
6
+ values,
7
+ (item) => Promise.resolve(item).then(
8
+ (value) => ({ status: "fulfilled", value }),
9
+ (reason) => ({ status: "rejected", reason })
10
+ )
11
+ )
12
+ );
13
+ };
14
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SW 缓存路由的协议守卫:仅 http(s) 请求允许进入 workbox 缓存路由。
3
+ *
4
+ * 注:workbox-routing 已在路由匹配前排除非 http(s) 请求——
5
+ * Router.handleRequest 中 `!url.protocol.startsWith('http')` 直接 return,
6
+ * blob:/data: 不会 respondWith,由浏览器本地解析。本守卫与框架行为等价,
7
+ * 属防御性加固:防止未来改用自定义 fetch 监听或路由重构时,blob:/data:
8
+ * 被误引入缓存语义。
9
+ *
10
+ * 线上 blob: 图片加载失败(SLS 202609,Android WebView ×3)与本守卫无关,
11
+ * 根因未定位,需单独排查。
12
+ */
13
+ export declare function isHttpOrHttpsRequest(url: string): boolean;
@@ -0,0 +1,7 @@
1
+ // src/sw-request-filter.ts
2
+ function isHttpOrHttpsRequest(url) {
3
+ return url.startsWith("https://") || url.startsWith("http://");
4
+ }
5
+ export {
6
+ isHttpOrHttpsRequest
7
+ };
@@ -0,0 +1,4 @@
1
+ import './sw-polyfills';
2
+ export declare function setupServiceWorker(options: {
3
+ release: string;
4
+ }): void;
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,92 @@
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.1.0",
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
+ "./recovery": {
26
+ "types": "./dist/esm/recovery.d.ts",
27
+ "import": "./dist/esm/recovery.js"
28
+ }
29
+ },
30
+ "main": "dist/esm/index.js",
31
+ "module": "dist/esm/index.js",
32
+ "types": "dist/esm/index.d.ts",
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "build": "father build",
38
+ "build:deps": "father prebundle",
39
+ "dev": "father dev",
40
+ "format": "prettier --cache --write .",
41
+ "lint": "eslint . --fix ",
42
+ "prepare": "husky install",
43
+ "prepublishOnly": "father doctor && npm run build",
44
+ "postpublish": "cnpm sync @microbt/pwa",
45
+ "test": "vitest run"
46
+ },
47
+ "lint-staged": {
48
+ "*.{md,json}": [
49
+ "prettier --cache --write"
50
+ ],
51
+ "*.{js,jsx}": [
52
+ "eslint --fix",
53
+ "prettier --cache --write"
54
+ ],
55
+ "*.ts?(x)": [
56
+ "eslint --fix",
57
+ "prettier --cache --parser=typescript --write"
58
+ ]
59
+ },
60
+ "dependencies": {
61
+ "workbox-cacheable-response": "^7.0.0",
62
+ "workbox-core": "^7.0.0",
63
+ "workbox-expiration": "^7.0.0",
64
+ "workbox-routing": "^7.0.0",
65
+ "workbox-strategies": "^7.0.0",
66
+ "workbox-window": "^7.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "@commitlint/cli": "^17.4.4",
70
+ "@commitlint/config-conventional": "^17.4.4",
71
+ "@microbt/environment": "^1.2.14",
72
+ "@types/node": "^22.13.4",
73
+ "@typescript-eslint/eslint-plugin": "^5.54.1",
74
+ "@typescript-eslint/parser": "^5.54.1",
75
+ "eslint": "^8.36.0",
76
+ "father": "^4.1.0",
77
+ "husky": "^8.0.3",
78
+ "lint-staged": "^13.2.0",
79
+ "prettier": "^2.8.4",
80
+ "prettier-plugin-organize-imports": "^3.2.2",
81
+ "prettier-plugin-packagejson": "^2.4.3",
82
+ "typescript": "^5.4.2",
83
+ "vitest": "^1.6.1"
84
+ },
85
+ "peerDependencies": {
86
+ "@microbt/environment": "^1.2.12"
87
+ },
88
+ "publishConfig": {
89
+ "access": "public",
90
+ "registry": "https://registry.npmjs.org"
91
+ }
92
+ }