vitepress-spa-fallback 1.0.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.
- package/LICENSE +21 -0
- package/README.md +22 -0
- package/README.zh-CN.md +22 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.mjs +206 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 holazz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# vitepress-spa-fallback
|
|
2
|
+
|
|
3
|
+
Vite plugin for VitePress `2.0.0-alpha.20`. Adding it enables three build-time behaviors:
|
|
4
|
+
|
|
5
|
+
- clear the home page `#app` after SSG, keeping the rest of the HTML;
|
|
6
|
+
- load the full page module when that shell is empty; retry `hashmap.json` after a stale page-chunk failure;
|
|
7
|
+
- rewrite root-relative static file URLs (`href` / `src` / `withBase`) to the resolved asset prefix.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineConfig } from 'vitepress'
|
|
11
|
+
import { vitepressSpaFallback } from 'vitepress-spa-fallback'
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
vite: {
|
|
15
|
+
plugins: [vitepressSpaFallback()],
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Runtime patches apply only to production builds. Other prerendered pages are left intact.
|
|
21
|
+
|
|
22
|
+
Peer dependency is pinned to `vitepress@2.0.0-alpha.20`. The build fails if the expected runtime strings are missing, or if the plugin runs outside a VitePress build.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# vitepress-spa-fallback
|
|
2
|
+
|
|
3
|
+
面向 VitePress `2.0.0-alpha.20` 的 Vite 插件。加上即启用三件事:
|
|
4
|
+
|
|
5
|
+
- SSG 结束后清空首页 `#app`,外围 HTML 不动;
|
|
6
|
+
- 空壳加载完整页面模块;页面 chunk 过期时重试 `hashmap.json`;
|
|
7
|
+
- 把根路径静态资源(`href` / `src` / `withBase`)改到最终解析的资源前缀。
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineConfig } from 'vitepress'
|
|
11
|
+
import { vitepressSpaFallback } from 'vitepress-spa-fallback'
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
vite: {
|
|
15
|
+
plugins: [vitepressSpaFallback()],
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
运行时补丁只在生产构建生效。其他预渲染页面保持原样。
|
|
21
|
+
|
|
22
|
+
peer dependency 锁定 `vitepress@2.0.0-alpha.20`。对不上预期运行时字符串,或不是 VitePress 构建,会直接失败。
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 1. config: prefix type=public built URLs with assetsBase
|
|
7
|
+
* 2. configResolved: wrap buildEnd / transformHtml once (client and SSR share SiteConfig)
|
|
8
|
+
* 3. transform: patch withBase, empty-shell page modules, and hashmap retry
|
|
9
|
+
* 1. config:给 type=public 的构建 URL 接上 assetsBase
|
|
10
|
+
* 2. configResolved:挂一次 buildEnd / transformHtml(client 与 SSR 共用同一份 SiteConfig)
|
|
11
|
+
* 3. transform:补丁 withBase、空壳走完整页、hashmap 重试
|
|
12
|
+
*/
|
|
13
|
+
declare function vitepressSpaFallback(): Plugin;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { vitepressSpaFallback };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/html.ts
|
|
5
|
+
const RAW_TEXT_TAGS = new Set([
|
|
6
|
+
"script",
|
|
7
|
+
"style",
|
|
8
|
+
"textarea",
|
|
9
|
+
"title",
|
|
10
|
+
"xmp",
|
|
11
|
+
"iframe",
|
|
12
|
+
"noembed",
|
|
13
|
+
"noframes",
|
|
14
|
+
"noscript"
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Clear only #app's contents without reserializing or changing surrounding HTML.
|
|
18
|
+
* 仅清空 #app 内部,不重新序列化 HTML,保留外围内容与原有格式。
|
|
19
|
+
*/
|
|
20
|
+
function createFallbackHtml(html) {
|
|
21
|
+
const tags = /<!--[\s\S]*?-->|<![^>]*>|<\/?([a-z][\w:-]*)(?=[\s/>])(?:[^<>"']|"[^"]*"|'[^']*')*>/gi;
|
|
22
|
+
let divDepth = 0;
|
|
23
|
+
let appDepth = -1;
|
|
24
|
+
let contentStart = -1;
|
|
25
|
+
let contentEnd = -1;
|
|
26
|
+
for (let match = tags.exec(html); match; match = tags.exec(html)) {
|
|
27
|
+
if (!match[1]) continue;
|
|
28
|
+
const tag = match[1].toLowerCase();
|
|
29
|
+
if (match[0].startsWith("</")) {
|
|
30
|
+
if (tag === "div") {
|
|
31
|
+
if (contentStart >= 0 && contentEnd < 0 && divDepth === appDepth) contentEnd = match.index;
|
|
32
|
+
divDepth--;
|
|
33
|
+
}
|
|
34
|
+
if (tag === "body" && contentStart >= 0 && contentEnd < 0) throw new Error("[vitepress-spa-fallback] Unclosed #app in generated index.html.");
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (tag === "div") divDepth++;
|
|
38
|
+
const attributes = match[0].slice(1 + match[1].length, -1);
|
|
39
|
+
for (const attr of attributes.matchAll(/([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g)) if (attr[1].toLowerCase() === "id" && (attr[2] ?? attr[3] ?? attr[4]) === "app") {
|
|
40
|
+
if (contentStart >= 0) throw new Error("[vitepress-spa-fallback] Duplicate #app in generated index.html.");
|
|
41
|
+
if (tag !== "div") throw new Error("[vitepress-spa-fallback] Expected a div for #app in generated index.html.");
|
|
42
|
+
contentStart = tags.lastIndex;
|
|
43
|
+
appDepth = divDepth;
|
|
44
|
+
}
|
|
45
|
+
if (RAW_TEXT_TAGS.has(tag)) {
|
|
46
|
+
const endTag = new RegExp(`</${tag}\\s*>`, "gi");
|
|
47
|
+
endTag.lastIndex = tags.lastIndex;
|
|
48
|
+
if (!endTag.exec(html)) throw new Error(`[vitepress-spa-fallback] Unclosed ${tag} in generated index.html.`);
|
|
49
|
+
tags.lastIndex = endTag.lastIndex;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (contentStart < 0 || contentEnd < 0) throw new Error("[vitepress-spa-fallback] Missing or unclosed #app in generated index.html.");
|
|
53
|
+
return html.slice(0, contentStart) + html.slice(contentEnd);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/url.ts
|
|
58
|
+
const ASSET_EXTENSIONS = "css|js|mjs|json|svg|png|jpe?g|gif|webp|ico|woff2?|ttf|otf|eot";
|
|
59
|
+
const ROOT_ASSET_RE = new RegExp(`^/(?!/).+\\.(?:${ASSET_EXTENSIONS})(?:[?#].*)?$`, "i");
|
|
60
|
+
const HTML_ASSET_RE = new RegExp(`\\b(href|src)=("|')(/(?!/)[^"'?#]+\\.(?:${ASSET_EXTENSIONS})(?:[?#][^"']*)?)\\2`, "gi");
|
|
61
|
+
const EXTERNAL_URL_RE = /^(?:[a-z][a-z\d+\-.]*:|\/\/)/i;
|
|
62
|
+
/**
|
|
63
|
+
* Join assetsBase and a file path without duplicating slashes.
|
|
64
|
+
* 拼接 assetsBase 与文件路径,避免重复斜杠。
|
|
65
|
+
*/
|
|
66
|
+
function joinAssetUrl(assetsBase, path) {
|
|
67
|
+
return `${assetsBase.endsWith("/") ? assetsBase.slice(0, -1) : assetsBase}${path.startsWith("/") ? path : `/${path}`}`;
|
|
68
|
+
}
|
|
69
|
+
function isRootAssetPath(path) {
|
|
70
|
+
return ROOT_ASSET_RE.test(path);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Root-relative files go to assetsBase; page routes stay on site base.
|
|
74
|
+
* 根路径静态文件改到 assetsBase,页面路由仍走站点 base。
|
|
75
|
+
*/
|
|
76
|
+
function rewriteAssetUrl(path, assetsBase, pageBase = "/") {
|
|
77
|
+
if (!assetsBase || EXTERNAL_URL_RE.test(path) || path.startsWith(assetsBase)) return path;
|
|
78
|
+
const prefix = pageBase === "/" ? "" : pageBase.endsWith("/") ? pageBase : `${pageBase}/`;
|
|
79
|
+
const relative = prefix && path.startsWith(prefix) ? `/${path.slice(prefix.length)}` : path;
|
|
80
|
+
return isRootAssetPath(relative) ? joinAssetUrl(assetsBase, relative) : path;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Rewrite root-relative href/src values in generated HTML.
|
|
84
|
+
* 改写生成 HTML 中的根路径 href/src。
|
|
85
|
+
*/
|
|
86
|
+
function rewriteHtmlAssetUrls(html, assetsBase, pageBase = "/") {
|
|
87
|
+
if (!assetsBase) return html;
|
|
88
|
+
return html.replace(HTML_ASSET_RE, (match, attr, quote, path) => {
|
|
89
|
+
const next = rewriteAssetUrl(path, assetsBase, pageBase);
|
|
90
|
+
return next === path ? match : `${attr}=${quote}${next}${quote}`;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Prefix Vite `type=public` URLs; leave `type=asset` unchanged.
|
|
95
|
+
* 只改写 type=public 的构建 URL,不改 type=asset。
|
|
96
|
+
*/
|
|
97
|
+
function createRenderBuiltUrl(assetsBase, original) {
|
|
98
|
+
return (filename, context) => {
|
|
99
|
+
const originalResult = original?.(filename, context);
|
|
100
|
+
if (originalResult !== void 0) return originalResult;
|
|
101
|
+
if (context.type === "public" && assetsBase.value) return joinAssetUrl(assetsBase.value, filename);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/runtime.ts
|
|
107
|
+
const APP_MODULE = "/vitepress/dist/client/app/index.js";
|
|
108
|
+
const ROUTER_MODULE = "/vitepress/dist/client/app/router.js";
|
|
109
|
+
const UTILS_MODULE = "/vitepress/dist/client/app/utils.js";
|
|
110
|
+
const LEAN_PAGE = "if (isInitialPageLoad) pageFilePath = pageFilePath.replace(/\\.js$/, \".lean.js\");";
|
|
111
|
+
const HASHMAP_FETCH = "fetch(runtimeBase() + \"hashmap.json\")";
|
|
112
|
+
const WITH_BASE = "return EXTERNAL_URL_RE.test(path) || !path.startsWith(\"/\") ? path : joinPath(runtimeBase(), path);";
|
|
113
|
+
const WITH_BASE_PATCH = `return EXTERNAL_URL_RE.test(path) || !path.startsWith("/") || (__ASSETS_BASE__ && path.startsWith(__ASSETS_BASE__)) ? path : joinPath((__ASSETS_BASE__ && /\\.(?:${ASSET_EXTENSIONS})(?:[?#].*)?$/i.test(path) ? __ASSETS_BASE__ : runtimeBase()), path);`;
|
|
114
|
+
/**
|
|
115
|
+
* Require exactly one match in the pinned runtime instead of silently drifting.
|
|
116
|
+
* 固定版本的运行时必须唯一命中补丁,避免内部代码变化后静默失效。
|
|
117
|
+
*/
|
|
118
|
+
function replaceOnce(code, search, replacement, id) {
|
|
119
|
+
const index = code.indexOf(search);
|
|
120
|
+
if (index < 0 || code.includes(search, index + search.length)) throw new Error(`[vitepress-spa-fallback] Expected one patch target in ${id}; check VitePress 2.0.0-alpha.20 compatibility.`);
|
|
121
|
+
return code.slice(0, index) + replacement + code.slice(index + search.length);
|
|
122
|
+
}
|
|
123
|
+
function normalizeModuleId(id) {
|
|
124
|
+
return id.split("?", 1)[0].replace(/\\/g, "/");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Patch VitePress client modules. withBase also runs during SSR so prerendered URLs match the client.
|
|
128
|
+
* 改写 VitePress 客户端模块;withBase 在 SSR 中也会执行,保证预渲染地址与客户端一致。
|
|
129
|
+
*/
|
|
130
|
+
function patchRuntimeModule(code, id, options, consumer = "client") {
|
|
131
|
+
const moduleId = normalizeModuleId(id);
|
|
132
|
+
if (moduleId.endsWith(UTILS_MODULE)) return replaceOnce(code, WITH_BASE, WITH_BASE_PATCH, moduleId);
|
|
133
|
+
if (options?.ssr || consumer !== "client") return;
|
|
134
|
+
if (moduleId.endsWith(APP_MODULE)) return replaceOnce(code, LEAN_PAGE, "if (isInitialPageLoad && document.querySelector(\"#app\")?.hasChildNodes()) pageFilePath = pageFilePath.replace(/\\.js$/, \".lean.js\");", moduleId);
|
|
135
|
+
if (moduleId.endsWith(ROUTER_MODULE)) return replaceOnce(code, HASHMAP_FETCH, "fetch((__ASSETS_BASE__ || runtimeBase()) + \"hashmap.json\")", moduleId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/index.ts
|
|
140
|
+
const HOOKED = Symbol.for("vitepress-spa-fallback:hooks");
|
|
141
|
+
/**
|
|
142
|
+
* Empty the home page after the user's buildEnd hook.
|
|
143
|
+
* 在用户的 buildEnd 之后清空首页。
|
|
144
|
+
*/
|
|
145
|
+
function wrapBuildEnd(siteConfig) {
|
|
146
|
+
const original = siteConfig.buildEnd;
|
|
147
|
+
siteConfig.buildEnd = async (config) => {
|
|
148
|
+
await original?.(config);
|
|
149
|
+
const file = join(config.outDir, "index.html");
|
|
150
|
+
let html;
|
|
151
|
+
try {
|
|
152
|
+
html = await readFile(file, "utf8");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw new Error(`[vitepress-spa-fallback] Missing ${file}.`, { cause: error });
|
|
155
|
+
}
|
|
156
|
+
const shell = createFallbackHtml(html);
|
|
157
|
+
if (shell !== html) await writeFile(file, shell);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Rewrite leftover root-relative asset URLs in generated HTML.
|
|
162
|
+
* 改写生成 HTML 里残留的根路径静态资源地址。
|
|
163
|
+
*/
|
|
164
|
+
function wrapTransformHtml(siteConfig) {
|
|
165
|
+
const original = siteConfig.transformHtml;
|
|
166
|
+
siteConfig.transformHtml = async (code, id, context) => {
|
|
167
|
+
return rewriteHtmlAssetUrls(await original?.(code, id, context) ?? code, context.siteConfig.assetsBase ?? "", context.siteConfig.site.base);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 1. config: prefix type=public built URLs with assetsBase
|
|
172
|
+
* 2. configResolved: wrap buildEnd / transformHtml once (client and SSR share SiteConfig)
|
|
173
|
+
* 3. transform: patch withBase, empty-shell page modules, and hashmap retry
|
|
174
|
+
* 1. config:给 type=public 的构建 URL 接上 assetsBase
|
|
175
|
+
* 2. configResolved:挂一次 buildEnd / transformHtml(client 与 SSR 共用同一份 SiteConfig)
|
|
176
|
+
* 3. transform:补丁 withBase、空壳走完整页、hashmap 重试
|
|
177
|
+
*/
|
|
178
|
+
function vitepressSpaFallback() {
|
|
179
|
+
const assetsBase = { value: "" };
|
|
180
|
+
return {
|
|
181
|
+
name: "vitepress-spa-fallback",
|
|
182
|
+
apply: "build",
|
|
183
|
+
enforce: "pre",
|
|
184
|
+
config(userConfig) {
|
|
185
|
+
return { experimental: {
|
|
186
|
+
...userConfig.experimental,
|
|
187
|
+
renderBuiltUrl: createRenderBuiltUrl(assetsBase, userConfig.experimental?.renderBuiltUrl)
|
|
188
|
+
} };
|
|
189
|
+
},
|
|
190
|
+
configResolved(config) {
|
|
191
|
+
const siteConfig = config.vitepress;
|
|
192
|
+
if (!siteConfig) throw new Error("[vitepress-spa-fallback] Expected a VitePress build (config.vitepress is missing).");
|
|
193
|
+
assetsBase.value = siteConfig.assetsBase ?? "";
|
|
194
|
+
if (HOOKED in siteConfig) return;
|
|
195
|
+
Object.defineProperty(siteConfig, HOOKED, { value: true });
|
|
196
|
+
wrapBuildEnd(siteConfig);
|
|
197
|
+
wrapTransformHtml(siteConfig);
|
|
198
|
+
},
|
|
199
|
+
transform(code, id, options) {
|
|
200
|
+
return patchRuntimeModule(code, id, options, this.environment.config.consumer);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
export { vitepressSpaFallback };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vitepress-spa-fallback",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Vite plugin for VitePress SPA fallback.",
|
|
6
|
+
"author": "holazz",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/holazz/vitepress-spa-fallback#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/holazz/vitepress-spa-fallback"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/holazz/vitepress-spa-fallback/issues"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"import": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.mjs",
|
|
24
|
+
"module": "./dist/index.mjs",
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"files": [
|
|
27
|
+
"README.md",
|
|
28
|
+
"README.zh-CN.md",
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.19.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"vitepress": "2.0.0-alpha.20"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@antfu/eslint-config": "^4.19.0",
|
|
39
|
+
"@types/node": "^26.0.0",
|
|
40
|
+
"bumpp": "^10.2.3",
|
|
41
|
+
"eslint": "^9.39.4",
|
|
42
|
+
"simple-git-hooks": "^2.13.1",
|
|
43
|
+
"tsdown": "^0.16.8",
|
|
44
|
+
"typescript": "^5.8.3",
|
|
45
|
+
"vite": "8.2.2",
|
|
46
|
+
"vitepress": "2.0.0-alpha.20",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"simple-git-hooks": {
|
|
50
|
+
"pre-commit": "pnpm lint:fix && pnpm typecheck"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsdown",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"lint": "eslint .",
|
|
56
|
+
"lint:fix": "eslint . --fix",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"release": "bumpp"
|
|
59
|
+
}
|
|
60
|
+
}
|