unplugin-https-reverse-proxy 1.0.0-beta.8 → 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.
@@ -1,15 +1,16 @@
1
1
  import { platform } from 'node:os'
2
2
  import process from 'node:process'
3
- import { chmodSync, createWriteStream, existsSync, mkdirSync, unlinkSync } from 'node:fs'
3
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
4
4
  import { spawn } from 'node:child_process'
5
+ import { readFile, unlink, writeFile } from 'node:fs/promises'
5
6
  import { got } from 'got-cjs'
6
7
  import { HttpProxyAgent } from 'http-proxy-agent'
7
8
  import { HttpsProxyAgent } from 'https-proxy-agent'
8
- import kill from 'kill-port'
9
+ import * as lockfile from 'proper-lockfile'
9
10
  import { chmodRecursive, consola, once } from '../utils'
10
11
  import { addHost, removeHost } from '../host'
11
- import { TEMP_DIR, caddyPath, supportList } from './constants'
12
- import { logProgress, logProgressOver, tryPort } from './utils'
12
+ import { TEMP_DIR, caddyFilePath, caddyLockFilePath, caddyPath, supportList } from './constants'
13
+ import { kill, logProgress, logProgressOver, tryPort } from './utils'
13
14
 
14
15
  export async function download() {
15
16
  if (await testCaddy())
@@ -103,10 +104,57 @@ export class CaddyInstant {
103
104
  private inited = false
104
105
  private stoped = false
105
106
 
107
+ /** has locked */
108
+ private locked = false
109
+ private caddyfile: string | undefined
110
+
111
+ private _caddyChild: ReturnType<typeof spawn> | undefined
112
+
113
+ private _source: string | undefined
114
+ private _target: string | undefined
115
+
106
116
  constructor() {
107
- testCaddy().then(() => {
117
+ testCaddy().then(async () => {
118
+ await this.getCaddyfile()
119
+
108
120
  this.inited = true
109
121
  })
122
+
123
+ this.locked = this.checkLock()
124
+ }
125
+
126
+ checkLock() {
127
+ if (!existsSync(caddyLockFilePath))
128
+ writeFileSync(caddyLockFilePath, '')
129
+ return lockfile.checkSync(caddyLockFilePath)
130
+ }
131
+
132
+ async getCaddyfile() {
133
+ if (!existsSync(caddyFilePath))
134
+ await this.initCaddyfile()
135
+
136
+ this.caddyfile = await readFile(caddyFilePath, 'utf-8')
137
+ return this.caddyfile
138
+ }
139
+
140
+ async initCaddyfile() {
141
+ const contet = `
142
+ {
143
+ debug
144
+ auto_https disable_redirects
145
+ }
146
+
147
+ `
148
+ await writeFile(caddyFilePath, contet)
149
+ this.caddyfile = contet
150
+ }
151
+
152
+ async writeCaddyfile() {
153
+ return await writeFile(caddyFilePath, this.caddyfile || '')
154
+ }
155
+
156
+ async deleteCaddyfile() {
157
+ return await unlink(caddyFilePath)
110
158
  }
111
159
 
112
160
  async init() {
@@ -128,111 +176,194 @@ export class CaddyInstant {
128
176
  https = false,
129
177
  } = options || {}
130
178
 
179
+ this._source = source
180
+ this._target = target
181
+
131
182
  if (!this.inited)
132
183
  await this.init()
133
184
 
134
- if (https && await tryPort(443))
135
- await kill(443, 'tcp')
136
- if ((process.platform === 'win32' || !https) && await tryPort(80))
137
- await kill(80, 'tcp')
138
-
139
185
  if (!await addHost(source.split(':')[0], target.split(':')[0]))
140
186
  throw new Error('update host failed')
141
187
  consola.success('update host success\n')
142
188
 
143
- return new Promise<void>((resolve, reject) => {
144
- // caddy reverse-proxy --from target --to source --internal-certs
145
- const child = process.platform !== 'win32'
146
- ? spawn('sudo', ['-E', caddyPath, 'reverse-proxy', '--from', `${target.split(':')[0]}${https ? '' : ':80'}`, '--to', `${source}`, '--internal-certs', '--insecure', '--disable-redirects'])
147
- : spawn(caddyPath, ['reverse-proxy', '--from', `${target.split(':')[0]}`, '--to', `${source}`, '--internal-certs'])
189
+ await this.getCaddyfile()
190
+ if (!(this.caddyfile || '').includes(`${target.split(':')[0]}${https ? '' : ':80'}`)) {
191
+ this.caddyfile = `${this.caddyfile}
192
+ ${target.split(':')[0]}${https ? '' : ':80'} {
193
+ ${https ? 'tls internal' : ''}
194
+ reverse_proxy http://${source} {
195
+ health_uri ${base}
196
+ health_interval 2s
197
+ health_timeout 5s
198
+ fail_duration 2s
199
+ unhealthy_status 502 503 504 404
200
+ lb_try_duration 3s
201
+ lb_try_interval 300ms
202
+ health_headers {
203
+ Accept text/html
204
+ Accept-Encoding gzip, deflate, br
205
+ User-Agent Caddy-Health-Check
206
+ }
148
207
 
149
- child.on('error', (err) => {
150
- return reject(err)
151
- })
208
+ @error status 502 503 504 404
209
+ handle_response @error {
210
+ respond * 503 {
211
+ body "Service Unavailable"
212
+ close
213
+ }
214
+ }
215
+ }
216
+ }
152
217
 
153
- process.on('SIGINT', async () => {
154
- if (!restore || this.stoped)
155
- return
218
+ `
156
219
 
157
- try {
158
- if (await removeHost(source.split(':')[0], target.split(':')[0]))
159
- consola.success('restore host success\n')
220
+ await this.writeCaddyfile()
221
+ }
222
+ if (!existsSync(caddyLockFilePath))
223
+ writeFileSync(caddyLockFilePath, '')
224
+
225
+ if (!this.locked) {
226
+ if (await tryPort(443))
227
+ await kill(443)
228
+ if ((process.platform === 'win32' || !https) && await tryPort(80))
229
+ await kill(80)
230
+ }
160
231
 
161
- else
162
- consola.fail('restore host failed\n')
163
- }
164
- catch (e) {
165
- consola.error(e)
166
- consola.fail('restore host failed\n')
167
- }
168
- finally {
169
- this.stoped = true
170
- process.nextTick(() => {
171
- process.exit()
172
- })
173
- }
174
- })
175
-
176
- const originalExit = process.exit.bind(process)
177
-
178
- // @ts-expect-error override
179
- process.exit = async (code?: number) => {
180
- const port = Number(source.split(':')[1])
181
- if (!Number.isNaN(port) && await tryPort(port))
182
- await kill(port, 'tcp')
183
-
184
- // fix `Error: EPERM: operation not permitted`
185
- const pwd = process.cwd()
186
- const viteCacheDir = `${pwd}/node_modules/.vite`
187
- if (existsSync(viteCacheDir))
188
- chmodRecursive(viteCacheDir, 0o777)
189
- const nuxtCacheDir = `${pwd}/.nuxt`
190
- if (existsSync(nuxtCacheDir))
191
- await chmodRecursive(nuxtCacheDir, 0o777)
192
- const webpackCacheDir = `${pwd}/node_modules/.cache`
193
- if (existsSync(webpackCacheDir))
194
- chmodRecursive(webpackCacheDir, 0o777)
195
-
196
- if (!restore || this.stoped)
197
- return originalExit(code)
198
-
199
- try {
200
- if (await removeHost(source.split(':')[0], target.split(':')[0]))
201
- consola.success('restore host success\n')
202
-
203
- else
204
- consola.fail('restore host failed\n')
205
- }
206
- catch (e) {
207
- consola.error(e)
208
- consola.fail('restore host failed\n')
209
- }
210
- finally {
211
- this.stoped = true
212
- process.nextTick(() => {
213
- originalExit(code)
214
- })
232
+ return new Promise<void>((resolve, reject) => {
233
+ const setupCleanup = () => {
234
+ process.on('SIGINT', async () => {
235
+ if (this.stoped)
236
+ return
237
+
238
+ process.stdin.resume()
239
+
240
+ try {
241
+ await this.baseCleanup(restore)
242
+ }
243
+ finally {
244
+ this.stoped = true
245
+ process.nextTick(() => {
246
+ process.exit()
247
+ })
248
+ }
249
+ })
250
+
251
+ const originalExit = process.exit.bind(process)
252
+
253
+ // @ts-expect-error override
254
+ process.exit = async (code?: number) => {
255
+ const port = Number(source.split(':')[1])
256
+ if (!Number.isNaN(port) && await tryPort(port))
257
+ await kill(port)
258
+
259
+ // fix `Error: EPERM: operation not permitted`
260
+ const pwd = process.cwd()
261
+ const viteCacheDir = `${pwd}/node_modules/.vite`
262
+ if (existsSync(viteCacheDir))
263
+ await chmodRecursive(viteCacheDir, 0o777).catch(() => { })
264
+ const nuxtCacheDir = `${pwd}/.nuxt`
265
+ if (existsSync(nuxtCacheDir))
266
+ await chmodRecursive(nuxtCacheDir, 0o777).catch(() => { })
267
+ const webpackCacheDir = `${pwd}/node_modules/.cache`
268
+ if (existsSync(webpackCacheDir))
269
+ await chmodRecursive(webpackCacheDir, 0o777).catch(() => { })
270
+
271
+ if (this.stoped)
272
+ return originalExit(code)
273
+
274
+ process.stdin.resume()
275
+
276
+ try {
277
+ await this.baseCleanup()
278
+ }
279
+
280
+ finally {
281
+ this.stoped = true
282
+ process.nextTick(() => {
283
+ originalExit(code)
284
+ })
285
+ }
215
286
  }
216
287
  }
217
288
 
218
- child.stderr.on('data', (data) => {
219
- const lines = (data.toString() as string).split('\n').map(line => line.trim())
220
- for (const line of lines) {
221
- // caddy log
222
- // eslint-disable-next-line no-console
223
- showCaddyLog && line && console.info(line)
224
- if (line.includes('Error:') || (line && JSON.parse(line).level === 'error')) {
225
- consola.error(line)
226
- // child.kill()
227
- return reject(line)
289
+ if (!this.locked) {
290
+ this._caddyChild = process.platform !== 'win32'
291
+ ? spawn('sudo', ['-E', caddyPath, 'run', '--config', caddyFilePath, '--watch'])
292
+ : spawn(caddyPath, ['run', '--config', caddyFilePath, '--watch'])
293
+
294
+ lockfile.lockSync(caddyLockFilePath)
295
+
296
+ this._caddyChild!.on('error', (err) => {
297
+ return reject(err)
298
+ })
299
+
300
+ this._caddyChild!.stderr?.on('data', async (data) => {
301
+ const lines = (data.toString() as string).split('\n').map(line => line.trim())
302
+ for (const line of lines) {
303
+ // caddy log
304
+ // eslint-disable-next-line no-console
305
+ showCaddyLog && line && console.info(line)
306
+ if (line.includes('Error:') || (line && JSON.parse(line).level === 'error')) {
307
+ if (line.includes('parsing caddyfile tokens')) {
308
+ consola.error('Caddyfile syntax error, please contact the author:\n', `${this.caddyfile}\n\n${line}\n`)
309
+ await this.baseCleanup()
310
+ process.exit()
311
+ }
312
+ else {
313
+ consola.error(`${line}\n`)
314
+ // child.kill()
315
+ return reject(line)
316
+ }
317
+ }
318
+ if (line.includes('config file changed'))
319
+ consola.success('Detect added a new reverse proxy config, restarted.\n')
320
+
321
+ if (line.includes('caddy proxying') || line.includes('serving initial'))
322
+ return resolve()
228
323
  }
229
- }
230
- })
324
+ })
325
+
326
+ this._caddyChild!.stdout?.on('data', (_data) => {
327
+ consola.info(_data.toString())
328
+ resolve()
329
+ })
231
330
 
232
- child.stdout.on('data', (_data) => {
233
- consola.info(_data.toString())
234
- resolve()
235
- })
331
+ setupCleanup()
332
+ }
333
+ else {
334
+ consola.info('Caddy is running in another process.')
335
+
336
+ setupCleanup()
337
+
338
+ return resolve()
339
+ }
236
340
  })
237
341
  }
342
+
343
+ async baseCleanup(restore: boolean = true) {
344
+ try {
345
+ if (restore && this._source && this._target) {
346
+ if (await removeHost(this._source.split(':')[0], this._target.split(':')[0]))
347
+ consola.success('Restore host success.\n')
348
+
349
+ else
350
+ consola.fail('Restore host failed.\n')
351
+ }
352
+ }
353
+ catch (e) {
354
+ consola.error(e)
355
+ consola.fail('Restore host failed.\n')
356
+ }
357
+ if (!this.locked && !this.stoped) {
358
+ this._caddyChild?.kill()
359
+ try {
360
+ await lockfile.unlock(caddyLockFilePath).catch(() => { })
361
+ await unlink(caddyFilePath).catch(() => { })
362
+ await unlink(caddyLockFilePath).catch(() => { })
363
+ }
364
+ catch (e) {
365
+ consola.error(e)
366
+ }
367
+ }
368
+ }
238
369
  }
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'node:net'
2
+ import { exec } from 'node:child_process'
2
3
  import ora from 'ora'
3
4
  import { consola } from '../utils'
4
5
 
@@ -28,6 +29,23 @@ export function tryPort(port: number) {
28
29
  })
29
30
  }
30
31
 
32
+ /**
33
+ * npx free-port :port
34
+ */
35
+ export async function kill(port: number) {
36
+ return new Promise<void>((resolve, reject) => {
37
+ exec(`npx @maxbo/free-port ${port} -s`, (err, stdout, stderr) => {
38
+ if (err) {
39
+ reject(err)
40
+ }
41
+ else {
42
+ consola.log(stdout)
43
+ resolve()
44
+ }
45
+ })
46
+ })
47
+ }
48
+
31
49
  function getNumber(number: number) {
32
50
  if (number <= 0)
33
51
  return '0%'
package/src/host/index.ts CHANGED
@@ -1,17 +1,13 @@
1
1
  import process from 'node:process'
2
2
  import { exec } from 'node:child_process'
3
3
  import { chmodSync } from 'node:fs'
4
+ import { promisify } from 'node:util'
4
5
 
5
- // @ts-expect-error no type
6
- import _Hosts from 'hosts-so-easy'
6
+ import hostile from 'hostile'
7
7
  import { consola } from '../utils'
8
8
 
9
- const Hosts = _Hosts.default || _Hosts
10
-
11
- const hostInstance = new Hosts({
12
- debounceTime: 50,
13
- hostsFile: getPathOfSystemHostsPath(),
14
- })
9
+ const _setHost = promisify(hostile.set)
10
+ const _removeHost = promisify(hostile.remove)
15
11
 
16
12
  function getPathOfSystemHostsPath() {
17
13
  return process.platform === 'win32'
@@ -38,20 +34,18 @@ async function syncHost() {
38
34
  export async function addHost(ip: string, host: string) {
39
35
  if (process.platform !== 'win32')
40
36
  chmodSync(getPathOfSystemHostsPath(), 0o777)
41
- hostInstance.add('127.0.0.1', 'localhost')
42
- hostInstance.add(ip, host)
37
+ await _setHost('127.0.0.1', 'localhost')
38
+ await _setHost(ip, host)
43
39
  if (ip === 'localhost')
44
- hostInstance.add('127.0.0.1', host)
45
- await hostInstance.updateFinish()
40
+ await _setHost('127.0.0.1', host)
46
41
  return await syncHost()
47
42
  }
48
43
 
49
44
  export async function removeHost(ip: string, host: string) {
50
45
  if (process.platform !== 'win32')
51
46
  chmodSync(getPathOfSystemHostsPath(), 0o777)
52
- hostInstance.remove(ip, host)
47
+ await _removeHost(ip, host)
53
48
  if (ip === 'localhost')
54
- hostInstance.remove('127.0.0.1', host)
55
- await hostInstance.updateFinish()
49
+ await _removeHost('127.0.0.1', host)
56
50
  return await syncHost()
57
51
  }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ let config: ResolvedConfig
11
11
 
12
12
  let caddy: CaddyInstant
13
13
 
14
- const pwd = process.cwd()
14
+ const cwd = process.cwd()
15
15
 
16
16
  export const vitePrintUrls = once((
17
17
  options: Options,
@@ -47,17 +47,18 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
47
47
  if (config.command !== 'serve')
48
48
  return
49
49
 
50
- if (!isAdmin()) {
51
- consola.warn('please run as administrator')
52
- return
53
- }
54
-
55
50
  const {
56
51
  enable = true,
57
52
  target = '',
58
53
  } = options
54
+
59
55
  if (!enable)
60
56
  return
57
+
58
+ if (!isAdmin()) {
59
+ consola.warn('please run as administrator')
60
+ return
61
+ }
61
62
  if (!target) {
62
63
  consola.fail('please provide target')
63
64
  return
@@ -81,17 +82,19 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
81
82
  if (process.env.NODE_ENV !== 'development')
82
83
  return
83
84
 
84
- if (!isAdmin()) {
85
- consola.warn('please run as administrator')
86
- return
87
- }
88
-
89
85
  const {
90
86
  enable = true,
91
87
  target = '',
92
88
  } = options
89
+
93
90
  if (!enable)
94
91
  return
92
+
93
+ if (!isAdmin()) {
94
+ consola.warn('please run as administrator')
95
+ return
96
+ }
97
+
95
98
  if (!target) {
96
99
  consola.fail('please provide target')
97
100
  return
@@ -102,7 +105,7 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
102
105
  port: 8080,
103
106
  ...compiler.options.devServer,
104
107
  // @ts-expect-error vuecli
105
- ...process.VUE_CLI_SERVICE.projectOptions.devServer,
108
+ ...(process.VUE_CLI_SERVICE?.projectOptions.devServer),
106
109
  }
107
110
  const source = `${devServer.host}:${devServer.port}`
108
111
 
@@ -137,6 +140,54 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
137
140
  throw e
138
141
  })
139
142
  },
143
+ async rspack(compiler) {
144
+ if (process.env.NODE_ENV !== 'development')
145
+ return
146
+
147
+ const {
148
+ enable = true,
149
+ target = '',
150
+ } = options
151
+
152
+ if (!enable)
153
+ return
154
+
155
+ if (!isAdmin()) {
156
+ consola.warn('please run as administrator')
157
+ return
158
+ }
159
+
160
+ if (!target) {
161
+ consola.fail('please provide target')
162
+ return
163
+ }
164
+
165
+ const devServer = {
166
+ host: 'localhost',
167
+ port: 8080,
168
+ ...compiler.options.devServer,
169
+ ...(await (await import('@rsbuild/core'))?.loadConfig({
170
+ cwd,
171
+ }))?.content?.server,
172
+ }
173
+
174
+ const source = `${devServer.host}:${devServer.port}`
175
+
176
+ if (caddy)
177
+ return
178
+
179
+ caddy = new CaddyInstant()
180
+
181
+ caddy.run(source, target, {
182
+ ...options,
183
+ }).then(() => {
184
+ const colorUrl = (url: string) => c.green(url.replace(/:(\d+)\//, (_, port) => `:${c.bold(port)}/`))
185
+
186
+ consola.success(` ${c.green('➜')} ${c.bold('run caddy reverse proxy success')}: ${colorUrl(`${options.https ? 'https' : 'http'}://${target}`)}`)
187
+ }).catch((e) => {
188
+ throw e
189
+ })
190
+ },
140
191
  })
141
192
 
142
193
  export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory)
package/src/nuxt.ts CHANGED
@@ -18,17 +18,19 @@ export default defineNuxtModule<ModuleOptions>({
18
18
  if (process.env.NODE_ENV !== 'development')
19
19
  return
20
20
 
21
- if (!isAdmin()) {
22
- consola.warn('please run as administrator')
23
- return
24
- }
25
-
26
21
  const {
27
22
  enable = true,
28
23
  target = '',
29
24
  } = options
25
+
30
26
  if (!enable)
31
27
  return
28
+
29
+ if (!isAdmin()) {
30
+ consola.warn('please run as administrator')
31
+ return
32
+ }
33
+
32
34
  if (!target) {
33
35
  consola.fail('please provide target')
34
36
  return
package/src/rspack.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Options } from './types'
2
+ import unplugin from '.'
3
+
4
+ // TODO: some upstream lib failed generate invalid dts, remove the any in the future
5
+ export default unplugin.rspack as (options?: Options) => any
@@ -1,63 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
- var __hasOwnProp = Object.prototype.hasOwnProperty;
4
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
- var __spreadValues = (a, b) => {
7
- for (var prop in b || (b = {}))
8
- if (__hasOwnProp.call(b, prop))
9
- __defNormalProp(a, prop, b[prop]);
10
- if (__getOwnPropSymbols)
11
- for (var prop of __getOwnPropSymbols(b)) {
12
- if (__propIsEnum.call(b, prop))
13
- __defNormalProp(a, prop, b[prop]);
14
- }
15
- return a;
16
- };
17
-
18
- // src/utils.ts
19
- import process from "process";
20
- import path from "path";
21
- import { chmod, readdir, stat } from "fs/promises";
22
- import { execSync } from "child_process";
23
- import { createConsola } from "consola";
24
- var consola = createConsola({
25
- level: 3
26
- }).withTag("reverse-proxy");
27
- async function chmodRecursive(_path, mode) {
28
- await chmod(_path, mode);
29
- if ((await stat(_path)).isDirectory()) {
30
- const children = await readdir(_path);
31
- for (const child of children)
32
- await chmodRecursive(path.join(_path, child), mode);
33
- }
34
- }
35
- function once(fn) {
36
- let called = false;
37
- return (...args) => {
38
- if (called)
39
- return;
40
- called = true;
41
- return fn(...args);
42
- };
43
- }
44
- function isAdmin() {
45
- if (process.platform === "win32") {
46
- try {
47
- execSync("net session", { stdio: "ignore" });
48
- return true;
49
- } catch (e) {
50
- return false;
51
- }
52
- } else {
53
- return process.getuid && process.getuid() === 0;
54
- }
55
- }
56
-
57
- export {
58
- __spreadValues,
59
- consola,
60
- chmodRecursive,
61
- once,
62
- isAdmin
63
- };
@@ -1,19 +0,0 @@
1
- import { existsSync, unlinkSync } from 'node:fs'
2
- import process from 'node:process'
3
- import { consola } from '../src/utils'
4
- import { download } from '../src/caddy'
5
- import { caddyPath } from '../src/caddy/constants'
6
-
7
- async function run() {
8
- try {
9
- if (process.env.UHRP_AUTO_INSTALL_CADDY) {
10
- existsSync(caddyPath) && unlinkSync(caddyPath)
11
- await download()
12
- }
13
- }
14
- catch (e: any) {
15
- consola.error(e.code)
16
- }
17
- }
18
-
19
- run()