返回 slidev
basePathServer.ts
根目录 / cypress / basePathServer.ts
1 import type { Server } from 'node:http'
2 import type { AddressInfo } from 'node:net'
3 import { execFileSync } from 'node:child_process'
4 import { createReadStream } from 'node:fs'
5 import { stat } from 'node:fs/promises'
6 import { createServer } from 'node:http'
7 import { extname, join, normalize, sep } from 'node:path'
8 import process from 'node:process'
9
10 type RouterMode = 'history' | 'hash'
11
12 const servers = new Map<RouterMode, { server: Server, baseUrl: string }>()
13 const base = '/deck/'
14 const contentTypes = new Map([
15 ['.html', 'text/html; charset=utf-8'],
16 ['.js', 'text/javascript; charset=utf-8'],
17 ['.css', 'text/css; charset=utf-8'],
18 ['.png', 'image/png'],
19 ['.svg', 'image/svg+xml'],
20 ['.woff', 'font/woff'],
21 ['.woff2', 'font/woff2'],
22 ['.ttf', 'font/ttf'],
23 ['.json', 'application/json'],
24 ])
25
26 async function fileExists(file: string) {
27 try {
28 const info = await stat(file)
29 return info.isFile()
30 }
31 catch {
32 return false
33 }
34 }
35
36 /**
37 * Builds the basic fixture with a non-root `--base` in the given router mode and
38 * serves the static output (with SPA fallback) on an ephemeral port.
39 *
40 * Returns the served base URL, e.g. `http://127.0.0.1:52341/deck/`.
41 */
42 export async function startBasePathServer(routerMode: RouterMode = 'history'): Promise<string> {
43 const existing = servers.get(routerMode)
44 if (existing)
45 return existing.baseUrl
46
47 const outDir = `dist-${routerMode}`
48 const root = join(import.meta.dirname, 'fixtures/basic', outDir)
49
50 // Cypress runs its node events inside Electron; strip its env so the
51 // spawned build runs under plain Node.
52 const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
53 const env = { ...process.env }
54 delete env.ELECTRON_RUN_AS_NODE
55 delete env.NODE_OPTIONS
56
57 execFileSync(pnpm, ['--filter', './cypress/fixtures/basic', 'build', '--base', base, '--router-mode', routerMode, '--out', outDir], {
58 cwd: join(import.meta.dirname, '..'),
59 env,
60 stdio: 'inherit',
61 })
62
63 const server = createServer(async (req, res) => {
64 const url = new URL(req.url ?? '/', 'http://127.0.0.1')
65
66 if (!url.pathname.startsWith(base)) {
67 res.writeHead(404).end('Not found')
68 return
69 }
70
71 const rel = decodeURIComponent(url.pathname.slice(base.length)) || 'index.html'
72 let file = normalize(join(root, rel))
73 if (file !== root && !file.startsWith(root + sep)) {
74 res.writeHead(403).end('Forbidden')
75 return
76 }
77
78 if (!await fileExists(file))
79 file = join(root, 'index.html')
80
81 res.writeHead(200, { 'content-type': contentTypes.get(extname(file)) ?? 'application/octet-stream' })
82 createReadStream(file).pipe(res)
83 })
84
85 await new Promise<void>((resolve, reject) => {
86 server.once('error', reject)
87 server.listen(0, '127.0.0.1', resolve)
88 })
89
90 const { port } = server.address() as AddressInfo
91 const baseUrl = `http://127.0.0.1:${port}${base}`
92 servers.set(routerMode, { server, baseUrl })
93 return baseUrl
94 }
95
96 export async function stopBasePathServer() {
97 await Promise.all(
98 [...servers.values()].map(({ server }) =>
99 new Promise<void>((resolve, reject) => {
100 server.close(error => (error ? reject(error) : resolve()))
101 }),
102 ),
103 )
104 servers.clear()
105 return null
106 }
107
107 lines TYPESCRIPT