@xysfe/vite-plugin-dev-proxy 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 +193 -0
- package/dist/index.cjs +236 -0
- package/dist/index.d.cts +17 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.mjs +229 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 aiwa
|
|
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,193 @@
|
|
|
1
|
+
# vite-plugin-dev-proxy
|
|
2
|
+
|
|
3
|
+
A Vite plugin for development environment proxy that automatically proxies remote server requests and handles HTML responses.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- π Automatically proxy remote server requests
|
|
8
|
+
- π¦ Smart HTML response handling, replacing remote scripts and stylesheets with local development versions
|
|
9
|
+
- πͺ Automatic cookie rewriting, removing Secure and Domain attributes
|
|
10
|
+
- π Redirect handling with protocol mismatch fixes
|
|
11
|
+
- βοΈ Support for custom static resource prefixes
|
|
12
|
+
- π Debug logging support
|
|
13
|
+
- π Merge with other proxy configurations
|
|
14
|
+
- π§ TypeScript support with full type definitions
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install vite-plugin-dev-proxy --save-dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
yarn add vite-plugin-dev-proxy --dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add vite-plugin-dev-proxy --save-dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Basic Usage
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// vite.config.js
|
|
36
|
+
import { defineConfig } from "vite";
|
|
37
|
+
import viteDevProxy from "vite-plugin-dev-proxy";
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
plugins: [
|
|
41
|
+
viteDevProxy({
|
|
42
|
+
appHost: "example.com",
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Full Configuration
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// vite.config.js
|
|
52
|
+
import { defineConfig } from "vite";
|
|
53
|
+
import viteDevProxy from "vite-plugin-dev-proxy";
|
|
54
|
+
|
|
55
|
+
export default defineConfig({
|
|
56
|
+
plugins: [
|
|
57
|
+
viteDevProxy({
|
|
58
|
+
appHost: "example.com",
|
|
59
|
+
https: true,
|
|
60
|
+
staticPrefix: "/dev/static",
|
|
61
|
+
bypassPrefixes: ["/static"],
|
|
62
|
+
scriptCssPrefix: "/static/global",
|
|
63
|
+
entry: "/src/main.js",
|
|
64
|
+
debug: true,
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Coexisting with Other Proxy Configurations
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// vite.config.js
|
|
74
|
+
export default defineConfig({
|
|
75
|
+
plugins: [
|
|
76
|
+
viteDevProxy({
|
|
77
|
+
appHost: "example.com",
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
server: {
|
|
81
|
+
proxy: {
|
|
82
|
+
"/api": {
|
|
83
|
+
target: "http://localhost:8080",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API
|
|
91
|
+
|
|
92
|
+
### `viteDevProxy(options?)`
|
|
93
|
+
|
|
94
|
+
#### Options
|
|
95
|
+
|
|
96
|
+
| Parameter | Type | Default | Required | Description |
|
|
97
|
+
| ----------------- | ---------- | ---------------- | -------- | ----------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `appHost` | `string` | - | β
| Target server address |
|
|
99
|
+
| `https` | `boolean` | `true` | - | Whether to use HTTPS for the target server |
|
|
100
|
+
| `staticPrefix` | `string` | `''` | - | Static resource prefix, used to build local entry path |
|
|
101
|
+
| `bypassPrefixes` | `string[]` | `['/static']` | - | List of prefixes to bypass proxy, requests matching these prefixes will access remote resources |
|
|
102
|
+
| `scriptCssPrefix` | `string` | `''` | - | Script/CSS prefix, used to precisely match remote scripts and stylesheets to remove |
|
|
103
|
+
| `entry` | `string` | `'/src/main.js'` | - | Local development entry file path |
|
|
104
|
+
| `isLib` | `boolean` | `false` | - | Whether in component library mode, returns local HTML file when true |
|
|
105
|
+
| `localIndexHtml` | `string` | `'index.html'` | - | Local HTML file path (only used when isLib=true) |
|
|
106
|
+
| `debug` | `boolean` | `false` | - | Whether to enable debug logging |
|
|
107
|
+
|
|
108
|
+
## How It Works
|
|
109
|
+
|
|
110
|
+
### 1. Proxy Configuration
|
|
111
|
+
|
|
112
|
+
The plugin injects `server.proxy` configuration through Vite's `config` hook, proxying all requests to the target server.
|
|
113
|
+
|
|
114
|
+
### 2. HTML Processing
|
|
115
|
+
|
|
116
|
+
When detecting a browser page navigation request and the response is HTML:
|
|
117
|
+
|
|
118
|
+
- Removes remote `type="module" crossorigin` script tags
|
|
119
|
+
- Removes remote `crossorigin` stylesheet link tags
|
|
120
|
+
- Inserts local development script entry
|
|
121
|
+
|
|
122
|
+
### 3. Cookie Rewriting
|
|
123
|
+
|
|
124
|
+
Automatically removes `Secure`, `Domain`, and `SameSite` attributes from cookies to ensure proper functioning in the local development environment.
|
|
125
|
+
|
|
126
|
+
### 4. Redirect Handling
|
|
127
|
+
|
|
128
|
+
- Replaces remote domain names in redirect URLs with local domain names
|
|
129
|
+
- Fixes protocol mismatch issues (https://localhost -> http://localhost)
|
|
130
|
+
|
|
131
|
+
## Debugging
|
|
132
|
+
|
|
133
|
+
Enable the `debug` option to view detailed logs:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
viteDevProxy({
|
|
137
|
+
appHost: "example.com",
|
|
138
|
+
debug: true,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Log output example:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
vite-plugin-dev-proxy: staticPrefix /dev/static
|
|
146
|
+
vite-plugin-dev-proxy: scriptCssPrefix /static/global
|
|
147
|
+
Proxy request: /admin/index (5ms)
|
|
148
|
+
HTML processed: /admin/index (23ms)
|
|
149
|
+
Bypass proxy: /static/js/app.js
|
|
150
|
+
Redirect handled: https://example.com/login -> http://localhost:3003/login (3ms)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## TypeScript Support
|
|
154
|
+
|
|
155
|
+
This plugin is written in TypeScript and provides full type definitions. You can import types for better development experience:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import viteDevProxy, { ProxyOptions } from "vite-plugin-dev-proxy";
|
|
159
|
+
|
|
160
|
+
const config: ProxyOptions = {
|
|
161
|
+
appHost: "example.com",
|
|
162
|
+
https: true,
|
|
163
|
+
debug: true,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export default defineConfig({
|
|
167
|
+
plugins: [viteDevProxy(config)],
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Notes
|
|
172
|
+
|
|
173
|
+
1. `appHost` is a required parameter, not providing it will throw an error
|
|
174
|
+
2. The plugin will override `server.proxy` configuration in `vite.config.js`
|
|
175
|
+
3. Ensure the local development server port matches the port in redirect handling
|
|
176
|
+
4. Use `scriptCssPrefix` to precisely control which remote scripts and stylesheets to remove
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- Vite 5.0+
|
|
181
|
+
- Node.js 18+
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT Β© [aiwa](https://cnlhb.github.io/blog/)
|
|
186
|
+
|
|
187
|
+
## Contributing
|
|
188
|
+
|
|
189
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
190
|
+
|
|
191
|
+
## Issues
|
|
192
|
+
|
|
193
|
+
If you encounter any issues, please report them on [GitHub Issues](https://github.com/CNLHB/vite-plugin-dev-proxy/issues).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zlib = require('zlib');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
8
|
+
|
|
9
|
+
const zlib__default = /*#__PURE__*/_interopDefaultCompat(zlib);
|
|
10
|
+
const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
11
|
+
|
|
12
|
+
function createProxyConfig(options) {
|
|
13
|
+
const {
|
|
14
|
+
https = true,
|
|
15
|
+
appHost = "",
|
|
16
|
+
isLib = false,
|
|
17
|
+
localIndexHtml = "index.html",
|
|
18
|
+
staticPrefix = "",
|
|
19
|
+
bypassPrefixes = ["/static"],
|
|
20
|
+
// scriptCssPrefix = "",
|
|
21
|
+
developmentAgentOccupancy = "",
|
|
22
|
+
clearScriptCssPrefixes = "",
|
|
23
|
+
entry = "/src/main.js",
|
|
24
|
+
debug = false
|
|
25
|
+
} = options;
|
|
26
|
+
if (!appHost) {
|
|
27
|
+
throw new Error("vite-plugin-dev-proxy: appHost is required");
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(bypassPrefixes)) {
|
|
30
|
+
throw new Error("vite-plugin-dev-proxy: bypassPrefixes must be an array");
|
|
31
|
+
}
|
|
32
|
+
const log = debug ? console.log : () => {
|
|
33
|
+
};
|
|
34
|
+
const logError = debug ? console.error : () => {
|
|
35
|
+
};
|
|
36
|
+
const normalizedStaticPrefix = staticPrefix.endsWith("/") ? staticPrefix.slice(0, -1) : staticPrefix;
|
|
37
|
+
log("vite-plugin-dev-proxy: staticPrefix", normalizedStaticPrefix);
|
|
38
|
+
const fullEntry = normalizedStaticPrefix + entry;
|
|
39
|
+
const scriptLinkRegex = /<(?:script[^>]*>.*?<\/script>|link[^>]*>)/g;
|
|
40
|
+
const assetRegex = /\.(js|mjs|ts|tsx|jsx|css|scss|sass|less|vue|json|woff2?|ttf|eot|ico|png|jpe?g|gif|svg|webp)(\?.*)?$/i;
|
|
41
|
+
const staticPathRegex = /^\/(static|assets|public|images|css|js)\//i;
|
|
42
|
+
const bypassRegex = /\.(vue|js|mjs|ts|tsx|jsx|css|scss|sass|less|json|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot)$/i;
|
|
43
|
+
return {
|
|
44
|
+
"/": {
|
|
45
|
+
target: `${https ? "https" : "http"}://${appHost}`,
|
|
46
|
+
changeOrigin: true,
|
|
47
|
+
secure: false,
|
|
48
|
+
cookieDomainRewrite: { "*": "localhost" },
|
|
49
|
+
selfHandleResponse: true,
|
|
50
|
+
configure: (proxy, options2) => {
|
|
51
|
+
const rewriteCookies = (headers) => {
|
|
52
|
+
const setCookie = headers["set-cookie"];
|
|
53
|
+
if (setCookie) {
|
|
54
|
+
headers["set-cookie"] = setCookie.map((cookie) => {
|
|
55
|
+
let rewrittenCookie = cookie.replace(/;\s*secure\s*(;|$)/gi, "$1").replace(/;\s*domain\s*=[^;]+(;|$)/gi, "$1").replace(/;\s*samesite\s*=[^;]+(;|$)/gi, "$1").replace(/;+/g, ";").replace(/;\s*$/g, "");
|
|
56
|
+
log("vite-plugin-dev-proxy: rewrittenCookie", rewrittenCookie);
|
|
57
|
+
return rewrittenCookie;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return headers;
|
|
61
|
+
};
|
|
62
|
+
proxy.on(
|
|
63
|
+
"proxyRes",
|
|
64
|
+
(proxyRes, req, res) => {
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
const contentType = proxyRes.headers["content-type"] || "";
|
|
67
|
+
const redirectUrl = proxyRes.headers.location;
|
|
68
|
+
const requestUrl = req.url || "";
|
|
69
|
+
const acceptHeader = req.headers.accept || "";
|
|
70
|
+
const isRedirect = proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && redirectUrl;
|
|
71
|
+
if (isRedirect) {
|
|
72
|
+
const host = req.headers.host;
|
|
73
|
+
const regex = new RegExp(appHost, "gi");
|
|
74
|
+
let location = redirectUrl.replace(regex, host || "");
|
|
75
|
+
location = location.replace(
|
|
76
|
+
/https:\/\/(localhost|127\.0\.0\.1)(:\d+)?/gi,
|
|
77
|
+
"http://$1$2"
|
|
78
|
+
);
|
|
79
|
+
const headers2 = rewriteCookies({ ...proxyRes.headers });
|
|
80
|
+
headers2.location = location;
|
|
81
|
+
res.writeHead(proxyRes.statusCode, headers2);
|
|
82
|
+
res.end();
|
|
83
|
+
log(
|
|
84
|
+
`Redirect handled: ${redirectUrl} -> ${location} (${Date.now() - startTime}ms)`
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const isNavigationRequest = acceptHeader.includes("text/html");
|
|
89
|
+
const isAssetRequest = assetRegex.test(requestUrl);
|
|
90
|
+
const isStaticPath = staticPathRegex.test(requestUrl);
|
|
91
|
+
const shouldProcessHtml = contentType.includes("text/html") && isNavigationRequest && !isAssetRequest && !isStaticPath && !isRedirect;
|
|
92
|
+
if (shouldProcessHtml) {
|
|
93
|
+
if (isLib) {
|
|
94
|
+
try {
|
|
95
|
+
const indexHtml = fs__default.readFileSync(
|
|
96
|
+
path.resolve(__dirname, localIndexHtml),
|
|
97
|
+
"utf-8"
|
|
98
|
+
);
|
|
99
|
+
res.writeHead(200, {
|
|
100
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
101
|
+
});
|
|
102
|
+
res.end(indexHtml);
|
|
103
|
+
log(
|
|
104
|
+
`Local HTML served: ${localIndexHtml}\uFF09 (${Date.now() - startTime}ms)`
|
|
105
|
+
);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
logError("Failed to read local HTML:", err);
|
|
108
|
+
res.writeHead(500);
|
|
109
|
+
res.end("Failed to read local HTML");
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const encoding = proxyRes.headers["content-encoding"];
|
|
114
|
+
const chunks = [];
|
|
115
|
+
proxyRes.on(
|
|
116
|
+
"data",
|
|
117
|
+
(chunk) => {
|
|
118
|
+
if (chunk) {
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
proxyRes.on("end", () => {
|
|
124
|
+
try {
|
|
125
|
+
let buffer = Buffer.concat(chunks);
|
|
126
|
+
const decompress = () => {
|
|
127
|
+
if (encoding === "gzip") {
|
|
128
|
+
return zlib__default.gunzipSync(buffer);
|
|
129
|
+
} else if (encoding === "deflate") {
|
|
130
|
+
return zlib__default.inflateSync(buffer);
|
|
131
|
+
} else if (encoding === "br") {
|
|
132
|
+
return zlib__default.brotliDecompressSync(buffer);
|
|
133
|
+
}
|
|
134
|
+
return buffer;
|
|
135
|
+
};
|
|
136
|
+
const decompressed = decompress();
|
|
137
|
+
let html = decompressed.toString("utf-8");
|
|
138
|
+
if (developmentAgentOccupancy) {
|
|
139
|
+
html = html.replace(
|
|
140
|
+
developmentAgentOccupancy,
|
|
141
|
+
`<script crossorigin type="module" src="${fullEntry}"><\/script>`
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
html = html.replace(
|
|
145
|
+
/<div[^>]*id=["']app["'][^>]*><\/div>/g,
|
|
146
|
+
(match) => `${match}<script crossorigin type="module" src="${fullEntry}"><\/script>`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
clearScriptCssPrefixes;
|
|
150
|
+
html = html.replace(scriptLinkRegex, (match) => {
|
|
151
|
+
const srcMatch = match.match(/src="([^"]+)"/i);
|
|
152
|
+
const hrefMatch = match.match(/href="([^"]+)"/i);
|
|
153
|
+
const srcOrHref = srcMatch ? srcMatch[1] : hrefMatch ? hrefMatch[1] : null;
|
|
154
|
+
if (typeof clearScriptCssPrefixes === "string") {
|
|
155
|
+
if (srcOrHref?.startsWith(clearScriptCssPrefixes)) {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(clearScriptCssPrefixes)) {
|
|
160
|
+
if (clearScriptCssPrefixes.some(
|
|
161
|
+
(prefix) => srcOrHref?.startsWith(prefix)
|
|
162
|
+
)) {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (clearScriptCssPrefixes instanceof RegExp) {
|
|
167
|
+
return clearScriptCssPrefixes.test(match) ? "" : match;
|
|
168
|
+
}
|
|
169
|
+
if (typeof clearScriptCssPrefixes === "function") {
|
|
170
|
+
return clearScriptCssPrefixes(match) ? "" : match;
|
|
171
|
+
}
|
|
172
|
+
return match;
|
|
173
|
+
});
|
|
174
|
+
if (html.indexOf(fullEntry) === -1) {
|
|
175
|
+
html = html.replace(
|
|
176
|
+
/<!--\sS ε
¬ε
±η»δ»Ά ζη€ΊδΏ‘ζ―\s-->/g,
|
|
177
|
+
`<script crossorigin type="module" src="${fullEntry}"><\/script>`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const headers2 = rewriteCookies({ ...proxyRes.headers });
|
|
181
|
+
headers2["content-type"] = "text/html; charset=utf-8";
|
|
182
|
+
delete headers2["content-encoding"];
|
|
183
|
+
delete headers2["content-length"];
|
|
184
|
+
res.writeHead(200, headers2);
|
|
185
|
+
res.end(html);
|
|
186
|
+
log(
|
|
187
|
+
`HTML processed: ${requestUrl} (${Date.now() - startTime}ms)`
|
|
188
|
+
);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logError("Decompress error:", err);
|
|
191
|
+
logError("Request URL:", requestUrl);
|
|
192
|
+
logError("Response headers:", proxyRes.headers);
|
|
193
|
+
res.writeHead(500);
|
|
194
|
+
res.end("Decompress error");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const headers = rewriteCookies({ ...proxyRes.headers });
|
|
200
|
+
res.writeHead(proxyRes.statusCode, headers);
|
|
201
|
+
proxyRes.pipe(res);
|
|
202
|
+
log(`Proxy request: ${requestUrl} (${Date.now() - startTime}ms)`);
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
bypass: (req) => {
|
|
207
|
+
const url = req.url || "";
|
|
208
|
+
const pathname = url.split("?")[0];
|
|
209
|
+
if ((normalizedStaticPrefix && url.startsWith(`${normalizedStaticPrefix}`) || url.startsWith("/@") || url.startsWith("/src") || url.startsWith("/node_modules") || url.includes(".hot-update.") || url === "/" || bypassRegex.test(pathname)) && !bypassPrefixes.some((prefix) => url.startsWith(prefix))) {
|
|
210
|
+
log(`Bypass proxy: ${url}`);
|
|
211
|
+
return url;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function viteDevProxy(options = {}) {
|
|
218
|
+
return {
|
|
219
|
+
name: "vite-plugin-dev-proxy",
|
|
220
|
+
config: (viteConfig) => {
|
|
221
|
+
const pluginProxy = createProxyConfig(options);
|
|
222
|
+
const existingProxy = viteConfig.server?.proxy || {};
|
|
223
|
+
const mergedProxy = {
|
|
224
|
+
...existingProxy,
|
|
225
|
+
...pluginProxy
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
server: {
|
|
229
|
+
proxy: mergedProxy
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = viteDevProxy;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface ProxyOptions {
|
|
4
|
+
https?: boolean;
|
|
5
|
+
appHost?: string;
|
|
6
|
+
isLib?: boolean;
|
|
7
|
+
localIndexHtml?: string;
|
|
8
|
+
staticPrefix?: string;
|
|
9
|
+
bypassPrefixes?: string[];
|
|
10
|
+
clearScriptCssPrefixes?: string | string[] | Function | RegExp;
|
|
11
|
+
developmentAgentOccupancy?: string;
|
|
12
|
+
entry?: string;
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function viteDevProxy(options?: ProxyOptions): Plugin;
|
|
16
|
+
|
|
17
|
+
export { viteDevProxy as default };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface ProxyOptions {
|
|
4
|
+
https?: boolean;
|
|
5
|
+
appHost?: string;
|
|
6
|
+
isLib?: boolean;
|
|
7
|
+
localIndexHtml?: string;
|
|
8
|
+
staticPrefix?: string;
|
|
9
|
+
bypassPrefixes?: string[];
|
|
10
|
+
clearScriptCssPrefixes?: string | string[] | Function | RegExp;
|
|
11
|
+
developmentAgentOccupancy?: string;
|
|
12
|
+
entry?: string;
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function viteDevProxy(options?: ProxyOptions): Plugin;
|
|
16
|
+
|
|
17
|
+
export { viteDevProxy as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface ProxyOptions {
|
|
4
|
+
https?: boolean;
|
|
5
|
+
appHost?: string;
|
|
6
|
+
isLib?: boolean;
|
|
7
|
+
localIndexHtml?: string;
|
|
8
|
+
staticPrefix?: string;
|
|
9
|
+
bypassPrefixes?: string[];
|
|
10
|
+
clearScriptCssPrefixes?: string | string[] | Function | RegExp;
|
|
11
|
+
developmentAgentOccupancy?: string;
|
|
12
|
+
entry?: string;
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function viteDevProxy(options?: ProxyOptions): Plugin;
|
|
16
|
+
|
|
17
|
+
export { viteDevProxy as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import zlib from 'zlib';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
function createProxyConfig(options) {
|
|
6
|
+
const {
|
|
7
|
+
https = true,
|
|
8
|
+
appHost = "",
|
|
9
|
+
isLib = false,
|
|
10
|
+
localIndexHtml = "index.html",
|
|
11
|
+
staticPrefix = "",
|
|
12
|
+
bypassPrefixes = ["/static"],
|
|
13
|
+
// scriptCssPrefix = "",
|
|
14
|
+
developmentAgentOccupancy = "",
|
|
15
|
+
clearScriptCssPrefixes = "",
|
|
16
|
+
entry = "/src/main.js",
|
|
17
|
+
debug = false
|
|
18
|
+
} = options;
|
|
19
|
+
if (!appHost) {
|
|
20
|
+
throw new Error("vite-plugin-dev-proxy: appHost is required");
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(bypassPrefixes)) {
|
|
23
|
+
throw new Error("vite-plugin-dev-proxy: bypassPrefixes must be an array");
|
|
24
|
+
}
|
|
25
|
+
const log = debug ? console.log : () => {
|
|
26
|
+
};
|
|
27
|
+
const logError = debug ? console.error : () => {
|
|
28
|
+
};
|
|
29
|
+
const normalizedStaticPrefix = staticPrefix.endsWith("/") ? staticPrefix.slice(0, -1) : staticPrefix;
|
|
30
|
+
log("vite-plugin-dev-proxy: staticPrefix", normalizedStaticPrefix);
|
|
31
|
+
const fullEntry = normalizedStaticPrefix + entry;
|
|
32
|
+
const scriptLinkRegex = /<(?:script[^>]*>.*?<\/script>|link[^>]*>)/g;
|
|
33
|
+
const assetRegex = /\.(js|mjs|ts|tsx|jsx|css|scss|sass|less|vue|json|woff2?|ttf|eot|ico|png|jpe?g|gif|svg|webp)(\?.*)?$/i;
|
|
34
|
+
const staticPathRegex = /^\/(static|assets|public|images|css|js)\//i;
|
|
35
|
+
const bypassRegex = /\.(vue|js|mjs|ts|tsx|jsx|css|scss|sass|less|json|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot)$/i;
|
|
36
|
+
return {
|
|
37
|
+
"/": {
|
|
38
|
+
target: `${https ? "https" : "http"}://${appHost}`,
|
|
39
|
+
changeOrigin: true,
|
|
40
|
+
secure: false,
|
|
41
|
+
cookieDomainRewrite: { "*": "localhost" },
|
|
42
|
+
selfHandleResponse: true,
|
|
43
|
+
configure: (proxy, options2) => {
|
|
44
|
+
const rewriteCookies = (headers) => {
|
|
45
|
+
const setCookie = headers["set-cookie"];
|
|
46
|
+
if (setCookie) {
|
|
47
|
+
headers["set-cookie"] = setCookie.map((cookie) => {
|
|
48
|
+
let rewrittenCookie = cookie.replace(/;\s*secure\s*(;|$)/gi, "$1").replace(/;\s*domain\s*=[^;]+(;|$)/gi, "$1").replace(/;\s*samesite\s*=[^;]+(;|$)/gi, "$1").replace(/;+/g, ";").replace(/;\s*$/g, "");
|
|
49
|
+
log("vite-plugin-dev-proxy: rewrittenCookie", rewrittenCookie);
|
|
50
|
+
return rewrittenCookie;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return headers;
|
|
54
|
+
};
|
|
55
|
+
proxy.on(
|
|
56
|
+
"proxyRes",
|
|
57
|
+
(proxyRes, req, res) => {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
const contentType = proxyRes.headers["content-type"] || "";
|
|
60
|
+
const redirectUrl = proxyRes.headers.location;
|
|
61
|
+
const requestUrl = req.url || "";
|
|
62
|
+
const acceptHeader = req.headers.accept || "";
|
|
63
|
+
const isRedirect = proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && redirectUrl;
|
|
64
|
+
if (isRedirect) {
|
|
65
|
+
const host = req.headers.host;
|
|
66
|
+
const regex = new RegExp(appHost, "gi");
|
|
67
|
+
let location = redirectUrl.replace(regex, host || "");
|
|
68
|
+
location = location.replace(
|
|
69
|
+
/https:\/\/(localhost|127\.0\.0\.1)(:\d+)?/gi,
|
|
70
|
+
"http://$1$2"
|
|
71
|
+
);
|
|
72
|
+
const headers2 = rewriteCookies({ ...proxyRes.headers });
|
|
73
|
+
headers2.location = location;
|
|
74
|
+
res.writeHead(proxyRes.statusCode, headers2);
|
|
75
|
+
res.end();
|
|
76
|
+
log(
|
|
77
|
+
`Redirect handled: ${redirectUrl} -> ${location} (${Date.now() - startTime}ms)`
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const isNavigationRequest = acceptHeader.includes("text/html");
|
|
82
|
+
const isAssetRequest = assetRegex.test(requestUrl);
|
|
83
|
+
const isStaticPath = staticPathRegex.test(requestUrl);
|
|
84
|
+
const shouldProcessHtml = contentType.includes("text/html") && isNavigationRequest && !isAssetRequest && !isStaticPath && !isRedirect;
|
|
85
|
+
if (shouldProcessHtml) {
|
|
86
|
+
if (isLib) {
|
|
87
|
+
try {
|
|
88
|
+
const indexHtml = fs.readFileSync(
|
|
89
|
+
resolve(__dirname, localIndexHtml),
|
|
90
|
+
"utf-8"
|
|
91
|
+
);
|
|
92
|
+
res.writeHead(200, {
|
|
93
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
94
|
+
});
|
|
95
|
+
res.end(indexHtml);
|
|
96
|
+
log(
|
|
97
|
+
`Local HTML served: ${localIndexHtml}\uFF09 (${Date.now() - startTime}ms)`
|
|
98
|
+
);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
logError("Failed to read local HTML:", err);
|
|
101
|
+
res.writeHead(500);
|
|
102
|
+
res.end("Failed to read local HTML");
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const encoding = proxyRes.headers["content-encoding"];
|
|
107
|
+
const chunks = [];
|
|
108
|
+
proxyRes.on(
|
|
109
|
+
"data",
|
|
110
|
+
(chunk) => {
|
|
111
|
+
if (chunk) {
|
|
112
|
+
chunks.push(chunk);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
proxyRes.on("end", () => {
|
|
117
|
+
try {
|
|
118
|
+
let buffer = Buffer.concat(chunks);
|
|
119
|
+
const decompress = () => {
|
|
120
|
+
if (encoding === "gzip") {
|
|
121
|
+
return zlib.gunzipSync(buffer);
|
|
122
|
+
} else if (encoding === "deflate") {
|
|
123
|
+
return zlib.inflateSync(buffer);
|
|
124
|
+
} else if (encoding === "br") {
|
|
125
|
+
return zlib.brotliDecompressSync(buffer);
|
|
126
|
+
}
|
|
127
|
+
return buffer;
|
|
128
|
+
};
|
|
129
|
+
const decompressed = decompress();
|
|
130
|
+
let html = decompressed.toString("utf-8");
|
|
131
|
+
if (developmentAgentOccupancy) {
|
|
132
|
+
html = html.replace(
|
|
133
|
+
developmentAgentOccupancy,
|
|
134
|
+
`<script crossorigin type="module" src="${fullEntry}"><\/script>`
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
html = html.replace(
|
|
138
|
+
/<div[^>]*id=["']app["'][^>]*><\/div>/g,
|
|
139
|
+
(match) => `${match}<script crossorigin type="module" src="${fullEntry}"><\/script>`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
clearScriptCssPrefixes;
|
|
143
|
+
html = html.replace(scriptLinkRegex, (match) => {
|
|
144
|
+
const srcMatch = match.match(/src="([^"]+)"/i);
|
|
145
|
+
const hrefMatch = match.match(/href="([^"]+)"/i);
|
|
146
|
+
const srcOrHref = srcMatch ? srcMatch[1] : hrefMatch ? hrefMatch[1] : null;
|
|
147
|
+
if (typeof clearScriptCssPrefixes === "string") {
|
|
148
|
+
if (srcOrHref?.startsWith(clearScriptCssPrefixes)) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(clearScriptCssPrefixes)) {
|
|
153
|
+
if (clearScriptCssPrefixes.some(
|
|
154
|
+
(prefix) => srcOrHref?.startsWith(prefix)
|
|
155
|
+
)) {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (clearScriptCssPrefixes instanceof RegExp) {
|
|
160
|
+
return clearScriptCssPrefixes.test(match) ? "" : match;
|
|
161
|
+
}
|
|
162
|
+
if (typeof clearScriptCssPrefixes === "function") {
|
|
163
|
+
return clearScriptCssPrefixes(match) ? "" : match;
|
|
164
|
+
}
|
|
165
|
+
return match;
|
|
166
|
+
});
|
|
167
|
+
if (html.indexOf(fullEntry) === -1) {
|
|
168
|
+
html = html.replace(
|
|
169
|
+
/<!--\sS ε
¬ε
±η»δ»Ά ζη€ΊδΏ‘ζ―\s-->/g,
|
|
170
|
+
`<script crossorigin type="module" src="${fullEntry}"><\/script>`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const headers2 = rewriteCookies({ ...proxyRes.headers });
|
|
174
|
+
headers2["content-type"] = "text/html; charset=utf-8";
|
|
175
|
+
delete headers2["content-encoding"];
|
|
176
|
+
delete headers2["content-length"];
|
|
177
|
+
res.writeHead(200, headers2);
|
|
178
|
+
res.end(html);
|
|
179
|
+
log(
|
|
180
|
+
`HTML processed: ${requestUrl} (${Date.now() - startTime}ms)`
|
|
181
|
+
);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
logError("Decompress error:", err);
|
|
184
|
+
logError("Request URL:", requestUrl);
|
|
185
|
+
logError("Response headers:", proxyRes.headers);
|
|
186
|
+
res.writeHead(500);
|
|
187
|
+
res.end("Decompress error");
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const headers = rewriteCookies({ ...proxyRes.headers });
|
|
193
|
+
res.writeHead(proxyRes.statusCode, headers);
|
|
194
|
+
proxyRes.pipe(res);
|
|
195
|
+
log(`Proxy request: ${requestUrl} (${Date.now() - startTime}ms)`);
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
bypass: (req) => {
|
|
200
|
+
const url = req.url || "";
|
|
201
|
+
const pathname = url.split("?")[0];
|
|
202
|
+
if ((normalizedStaticPrefix && url.startsWith(`${normalizedStaticPrefix}`) || url.startsWith("/@") || url.startsWith("/src") || url.startsWith("/node_modules") || url.includes(".hot-update.") || url === "/" || bypassRegex.test(pathname)) && !bypassPrefixes.some((prefix) => url.startsWith(prefix))) {
|
|
203
|
+
log(`Bypass proxy: ${url}`);
|
|
204
|
+
return url;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function viteDevProxy(options = {}) {
|
|
211
|
+
return {
|
|
212
|
+
name: "vite-plugin-dev-proxy",
|
|
213
|
+
config: (viteConfig) => {
|
|
214
|
+
const pluginProxy = createProxyConfig(options);
|
|
215
|
+
const existingProxy = viteConfig.server?.proxy || {};
|
|
216
|
+
const mergedProxy = {
|
|
217
|
+
...existingProxy,
|
|
218
|
+
...pluginProxy
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
server: {
|
|
222
|
+
proxy: mergedProxy
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { viteDevProxy as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xysfe/vite-plugin-dev-proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Vite plugin for development environment proxy that automatically proxies remote server requests and handles HTML responses",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.mjs",
|
|
10
|
+
"module": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "npx unbuild",
|
|
28
|
+
"prepack": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": ""
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"vite",
|
|
36
|
+
"vite-plugin",
|
|
37
|
+
"proxy",
|
|
38
|
+
"dev-proxy",
|
|
39
|
+
"development",
|
|
40
|
+
"html-proxy",
|
|
41
|
+
"cookie",
|
|
42
|
+
"redirect"
|
|
43
|
+
],
|
|
44
|
+
"author": "aiwa",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.11.5",
|
|
51
|
+
"typescript": "^5.3.3",
|
|
52
|
+
"unbuild": "^2.0.0",
|
|
53
|
+
"vite": "^5.0.12"
|
|
54
|
+
},
|
|
55
|
+
"packageManager": "pnpm"
|
|
56
|
+
}
|