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