UNPKG

@mintlify/previewing

Version:

Preview Mintlify docs locally

190 lines (189 loc) • 7.95 kB
import { jsx as _jsx } from "react/jsx-runtime"; import open from 'better-opn'; import express from 'express'; import { createServer } from 'http'; import { Server as SocketServer } from 'socket.io'; import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../constants.js'; import { addLog, removeSpinnerLogs } from '../logging-state.js'; import { LaunchLog, UpdateLog } from '../logs.js'; import { maybeFixMissingWindowsEnvVar } from '../util.js'; import { addDevTimingLog, startDevTiming, timeDev } from './dev-timing.js'; import listener, { initializeDocsConfigHashCache, initializeFrontmatterHashCache, initializeImportCache, } from './listener/index.js'; import { refreshTrackedReferencedFiles } from './listener/referencedFiles.js'; import { getLocalNetworkIp } from './network.js'; import { ensurePageCompiled, getFailedCompileEvents, markOnDemandPagesReady, setCompileListener, } from './on-demand-page.js'; import { setupNext } from './setupNext.js'; const PROXY_RESPONSE_HEADER_DENYLIST = new Set([ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-encoding', 'content-length', ]); const ASSISTANT_MESSAGE_JSON_LIMIT = '5mb'; function applyUpstreamProxyHeaders(upstream, res) { const headers = upstream.headers; const hasGetSetCookie = typeof headers.getSetCookie === 'function'; const setCookies = hasGetSetCookie ? headers.getSetCookie() : undefined; headers.forEach((value, key) => { const lower = key.toLowerCase(); if (PROXY_RESPONSE_HEADER_DENYLIST.has(lower)) return; if (lower === 'set-cookie' && hasGetSetCookie) return; res.setHeader(key, value); }); if (setCookies?.length) { for (const cookie of setCookies) { res.appendHeader('Set-Cookie', cookie); } } } export const run = async (argv) => { const port = argv.port || '3000'; const currentPort = parseInt(port, 10) || 3000; const app = express(); const server = createServer(app); const io = new SocketServer(server); const requestHandler = await timeDev('setup Next handler', () => setupNext()); const localIp = getLocalNetworkIp(); const apiUrl = argv.apiUrl ?? process.env.MINTLIFY_API_URL ?? 'http://localhost:5000'; const accessToken = argv.accessToken; const subdomain = argv.subdomain; process.env.MINTLIFY_CLI_ACCESS_TOKEN = accessToken ?? ''; process.env.MINTLIFY_API_URL = apiUrl; if (subdomain) process.env.MINTLIFY_CLI_SUBDOMAIN = subdomain; // next-server is bugged, public files added after starting aren't served app.use('/', express.static(NEXT_PUBLIC_PATH)); app.use((req, _res, next) => { if (req.method !== 'GET' && req.method !== 'HEAD') { next(); return; } const silent = req.headers['next-router-prefetch'] !== undefined || req.headers['purpose'] === 'prefetch' || req.headers['sec-purpose'] === 'prefetch'; ensurePageCompiled(req.path, { silent }).then(() => next(), () => next()); }); app.post('/_mintlify/api-public/search/:subdomain', express.json(), async (req, res) => { const targetSubdomain = subdomain ?? req.params.subdomain; try { const upstream = await fetch(`${apiUrl}/api/cli/${targetSubdomain}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify(req.body), }); const data = await upstream.json(); res.status(upstream.status).json(data); } catch { res.status(502).json({ error: 'search proxy failed' }); } }); app.post('/_mintlify/api-public/assistant/:subdomain/message', express.json({ limit: ASSISTANT_MESSAGE_JSON_LIMIT }), async (req, res) => { const targetSubdomain = subdomain ?? req.params.subdomain; try { const upstream = await fetch(`${apiUrl}/api/cli/${targetSubdomain}/assistant/message`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify(req.body), }); res.status(upstream.status); applyUpstreamProxyHeaders(upstream, res); if (upstream.body) { const reader = upstream.body.getReader(); const pump = async () => { const { done, value } = await reader.read(); if (done) { res.end(); return; } res.write(value); return pump(); }; await pump(); } else { res.end(); } } catch { if (!res.headersSent) { res.status(502).json({ error: 'assistant proxy failed' }); } else { res.destroy(); } } }); app.all('*', (req, res) => requestHandler(req, res)); const onChange = () => { io.emit('reload'); }; setCompileListener((event) => { io.emit('compiling', event); }); io.on('connection', (socket) => { for (const event of getFailedCompileEvents()) { socket.emit('compiling', event); } }); const finishServerListenTiming = startDevTiming('server ready'); server.listen(currentPort, () => { finishServerListenTiming(); removeSpinnerLogs(); if (argv.needsUpdate) { addLog(_jsx(UpdateLog, { updateCommand: `${argv.packageName} update` })); } addLog(_jsx(LaunchLog, { localUrl: `http://localhost:${port}`, networkUrl: localIp ? `http://${localIp}:${port}` : undefined })); /** * We're running into a known bug with the `open` package, where Windows machines error out because process.env.SYSTEMROOT is not set: * https://github.com/sindresorhus/open/issues/292 * * Let's use the same workaround that this project did: * https://github.com/sanity-io/sanity/pull/4221/files#diff-aeb574e1becf61f21fdf87fbea709669c93d604d660dad4b0f9e24527a2fb54bR256-R262 */ maybeFixMissingWindowsEnvVar(); if (argv.open) { void open(`http://localhost:${port}`); } // exit with successful status const onExit = () => { process.exit(0); }; process.on('SIGINT', onExit); process.on('SIGTERM', onExit); }); // start watching immediately so edits made during cache initialization are // not lost; handlers wait on the gate until the caches they rely on exist let releaseWatcherGate = () => { }; const watcherGate = new Promise((resolve) => { releaseWatcherGate = resolve; }); addDevTimingLog('watching for file changes'); listener(onChange, { localSchema: argv['local-schema'], groups: argv.groups, disableOpenApi: argv.disableOpenapi, allowSourceRefs: true, lazyPages: true, }, watcherGate); await timeDev('initialize import cache', () => initializeImportCache(CMD_EXEC_PATH, argv.fileImportsMap)); markOnDemandPagesReady(); await timeDev('initialize frontmatter cache', () => initializeFrontmatterHashCache()); await timeDev('initialize docs config cache', () => initializeDocsConfigHashCache()); await timeDev('refresh referenced files', () => refreshTrackedReferencedFiles()); releaseWatcherGate(); };