/** * * @author yutent * @date 2022/04/09 18:26:35 */ const vsc = require('vscode') const { join, basename } = require('path') const { parse } = require('url') const http = require('http') const http2 = require('http2') const fs = require('iofs') const { WebSocketServer } = require('ws') const decode = decodeURIComponent const statItem = vsc.window.createStatusBarItem(vsc.StatusBarAlignment.Left, 0) const MIME_TYPES = { html: 'text/html;charset=utf-8', txt: 'text/plain;charset=utf-8', css: 'text/css;charset=utf-8', xml: 'text/xml;charset=utf-8', gif: 'image/gif', jpg: 'image/jpeg', webp: 'image/webp', tiff: 'image/tiff', png: 'image/png', svg: 'image/svg+xml', ico: 'image/x-icon', bmp: 'image/x-ms-bmp', js: 'application/javascript;charset=utf-8', json: 'application/json;charset=utf-8', mp3: 'audio/mpeg', ogg: 'audio/ogg', m4a: 'audio/x-m4a', mp4: 'video/mp4', webm: 'video/webm', ttf: 'font/font-ttf', woff: 'font/font-woff', woff2: 'font/font-woff2', wast: 'application/wast', wasm: 'application/wasm', other: 'application/octet-stream' } const SERVER_OPTIONS = { allowHTTP1: true } const COMMON_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', 'Cache-Control': 'no-store', 'X-Powered-By': 'VS Code simple.http' } const CONFIG_FILE = '.httpserver' MIME_TYPES.htm = MIME_TYPES.html MIME_TYPES.jpeg = MIME_TYPES.jpg MIME_TYPES.tif = MIME_TYPES.tiff let root let enabled = false let port = 23333 let baseUrl = 'http://127.0.0.1:' + port let server let ws = null let domain let isHttps = false const HMR_SCRIPT = ` ` class WebSocket { #ws = null // ws实例 #queue = [] // 消息队列 constructor(server) { if (server.listening) { let conn = new WebSocketServer({ server, path: '/ws-http-hmr' }) conn.on('connection', ws => { this.#ws = ws if (this.#queue.length) { this.#queue = [] this.send() } }) } } send(msg = {}) { if (this.#ws) { this.#ws.send(JSON.stringify(msg)) } else { this.#queue.push(msg) } } } function createServer() { if (port > 65535) { enabled = false return } console.log(`尝试使用${port}端口...`) server = (isHttps ? http2.createSecureServer : http.createServer)( isHttps ? SERVER_OPTIONS : void 0 ) server .on('request', function (req, res) { let pathname = parse(req.url).pathname.slice(1) let paths = pathname.split('/') pathname = decode(pathname) || 'index.html' if (pathname.endsWith('/')) { pathname += 'index.html' } let file = join(root, pathname) let stat = fs.stat(file) let ext = pathname.split('.').pop() let code for (let k in COMMON_HEADERS) { res.setHeader(k, COMMON_HEADERS[k]) } if (stat.isFile()) { res.setHeader('accept-ranges', 'bytes') res.setHeader('content-type', MIME_TYPES[ext] || MIME_TYPES.other) if (ext === 'html') { code = fs .cat(file) .toString() .replace('', HMR_SCRIPT + '') res.setHeader('content-length', Buffer.byteLength(code)) } else { res.setHeader('content-length', stat.size) } res.writeHead(200, isHttps ? void 0 : 'OK') if (ext === 'html') { res.end(code) } else { fs.origin.createReadStream(file).pipe(res) } } else { if (paths.length > 1 && paths[0].endsWith('.html')) { let rootFile = join(root, paths[0]) if (fs.isfile(rootFile)) { code = fs .cat(rootFile) .toString() .replace('', HMR_SCRIPT + '') res.setHeader('accept-ranges', 'bytes') res.setHeader('content-type', MIME_TYPES.html) res.setHeader('content-length', Buffer.byteLength(code)) res.writeHead(200, isHttps ? void 0 : 'OK') res.end(code) return } } else { let indexFile = join(root, 'index.html') if (fs.isfile(indexFile)) { code = fs .cat(indexFile) .toString() .replace('', HMR_SCRIPT + '') res.setHeader('accept-ranges', 'bytes') res.setHeader('content-type', MIME_TYPES.html) res.setHeader('content-length', Buffer.byteLength(code)) res.writeHead(200, isHttps ? void 0 : 'OK') res.end(code) return } } res.setHeader('content-length', 0) res.setHeader('x-url', file) res.writeHead(404, isHttps ? void 0 : 'Not Found') res.end(null) } }) .on('error', err => { console.log(`${port}端口被占用~~~`) port++ createServer() }) .on('listening', _ => { baseUrl = `http${isHttps ? 's' : ''}://${domain || '127.0.0.1'}:${port}` ws = new WebSocket(server) statItem.text = `http.server (🟢)` statItem.show() }) .listen(port) return server } function __init__() { let folders = vsc.workspace.workspaceFolders let commonConfig = vsc.workspace.getConfiguration('HttpServer') enabled = false statItem.hide() if (folders && folders.length) { root = folders[0].uri.fsPath } if (server) { server.close() server.closeAllConnections() server = null } if (root) { let file = join(root, CONFIG_FILE) // if (fs.isfile(file)) { let conf = JSON.parse(fs.cat(file).toString()) if (conf.root) { root = join(root, conf.root) } isHttps = conf.https || conf.http2 if (conf.headers) { for (let k in conf.headers) { let _k = k.toLowerCase() if (['accept-ranges', 'content-type', 'content-length'].includes(_k)) { continue } COMMON_HEADERS[_k] = conf.headers[k] } } if (isHttps) { let key = conf.key || commonConfig.key let cert = conf.cert || commonConfig.cert if (key) { SERVER_OPTIONS.key = fs.cat(key) } if (cert) { SERVER_OPTIONS.cert = fs.cat(cert) } } domain = conf.domain || commonConfig.domain if (conf.enabled) { enabled = true port = conf.port || 23333 createServer() } else { statItem.text = `http.server (🔴)` statItem.show() } } } } export function deactivate() {} export function activate(ctx) { __init__() vsc.workspace.onDidChangeConfiguration(__init__) vsc.workspace.onDidSaveTextDocument(doc => { let file = doc.fileName if (file?.startsWith(root)) { if (basename(file) === CONFIG_FILE) { return __init__() } if (enabled) { ws.send() } } }) let cmd = vsc.commands.registerCommand('HttpServer.open', _ => { if (enabled) { let editor = vsc.window.activeTextEditor if (editor) { let pathname = editor.document.uri.fsPath.slice(root.length) vsc.commands.executeCommand('vscode.open', baseUrl + pathname) } } }) ctx.subscriptions.push(cmd) }