返回 slidev
useServerDetector.ts
根目录 / packages / vscode / src / composables / useServerDetector.ts
1 import type { SlidevProject } from '../projects'
2 import { createControlledPromise } from '@antfu/utils'
3 import { getPort as getPortPlease } from 'get-port-please'
4 import { computed, defineService, onScopeDispose, reactive, watch } from 'reactive-vscode'
5 import { config } from '../configs'
6 import { activeProject, askAddProject, projects, scannedProjects } from '../projects'
7 import { isSamePath } from '../utils/isSamePath'
8 import { logger } from '../views/logger'
9
10 const versionRE = /<meta (?:name|property)="slidev:version" content="([^"]+)">/
11 const entryRE = /<meta (?:property|charset)="slidev:entry" content="([^"]+)">/
12 const RE_SLIDEV = /slidev/i
13
14 export interface DetectedServerState {
15 port: number
16 ready: boolean
17 message: string
18 compatMode: boolean
19 entry: string | null
20 pending: Promise<boolean> | null
21 }
22
23 export const useServerDetector = defineService(() => {
24 const detectedPorts = reactive(new Map<number, DetectedServerState>())
25
26 async function redetect(port: number) {
27 const state = detectedPorts.get(port) || reactive<DetectedServerState>({
28 port,
29 ready: false,
30 message: '',
31 compatMode: false,
32 entry: null,
33 pending: null,
34 })
35 detectedPorts.set(port, state)
36
37 if (state.pending)
38 return state.pending
39 const { resolve } = state.pending = createControlledPromise()
40
41 async function pingUrl(url: string) {
42 try {
43 const text = await (await fetch(url)).text()
44 if (!RE_SLIDEV.test(text))
45 return false
46 // Use verkit to compare in the future
47 state.compatMode = !text.match(versionRE)
48 const detectedEntry = text.match(entryRE)?.[1]
49 if (detectedEntry) {
50 const entry = [...projects.keys()]
51 .find(projectEntry => isSamePath(projectEntry, detectedEntry)) ?? detectedEntry
52 state.entry = entry
53 logger.info(`[Slidev] Detected Slidev server entry: ${detectedEntry} on port ${port}`)
54 if (scannedProjects.value) {
55 askAddProject(entry, `A Slidev server is detected running on localhost:${port}.`)
56 }
57 }
58 return true
59 }
60 catch (err) {
61 state.message = String(err)
62 }
63 return false
64 }
65
66 // We can't use `localhost:` here
67 state.ready = await pingUrl(`http://[::1]:${port}`) || await pingUrl(`http://127.0.0.1:${port}`)
68
69 state.pending = null
70 resolve(state.ready)
71 return state.ready
72 }
73
74 const portsToDetect = computed(() => {
75 const ports = new Set([config.port])
76 for (const project of projects.values()) {
77 if (project.port.value)
78 ports.add(project.port.value)
79 }
80 return ports
81 })
82
83 watch(portsToDetect, (ports, oldPorts) => {
84 for (const port of ports) {
85 if (!oldPorts?.has(port))
86 redetect(port)
87 }
88 }, { immediate: true })
89
90 const interval = setInterval(() => {
91 const activePort = activeProject.value?.port.value
92 if (activePort) {
93 redetect(activePort)
94 }
95 }, 4000)
96 onScopeDispose(() => clearInterval(interval))
97
98 function getDetected(project: SlidevProject) {
99 const port = project.port.value || config.port
100 const detected = detectedPorts.get(port)
101 if (detected?.entry === project.entry)
102 return detected
103 for (const state of detectedPorts.values()) {
104 if (state.entry === project.entry)
105 return state
106 }
107 return null
108 }
109
110 async function allocPort() {
111 const usedPorts = [...projects.values()].map(project => project.port.value ?? 0)
112 const minPort = Math.max(3029, ...usedPorts) + 1
113 return await getPortPlease({
114 portRange: [minPort, 4000],
115 })
116 }
117
118 return {
119 redetect,
120 getDetected,
121 allocPort,
122 }
123 })
124
124 lines TYPESCRIPT