local uv = vim.uv
---@class live_server.Instance
---@field handle uv.uv_tcp_t
---@field port integer
---@field root_real string
---@field sse_clients uv.uv_tcp_t[]
---@field debounce_timer uv.uv_timer_t
---@field fs_event uv.uv_fs_event_t?
---@field ignore_patterns string[]
---@field debounce_ms integer
---@field css_inject boolean
---@field debug boolean
---@class live_server.StartConfig
---@field port integer
---@field root_real string
---@field debounce? integer
---@field ignore? string[]
---@field css_inject? boolean
---@field debug? boolean
local S = {}
local function dbg(inst, msg)
if not inst.debug then
return
end
vim.schedule(function()
vim.notify(('[live-server] %s'):format(msg), vim.log.levels.DEBUG)
end)
end
---@type table%d %s
]]):format(status, phrase)
write_response(sock, status, { ['Content-Type'] = 'text/html; charset=utf-8' }, body)
end
---@param root string
---@param request_path string
---@return string?
local function resolve_path(root, request_path)
local decoded = url_decode(request_path)
local joined = ('%s/%s'):format(root, decoded)
local real = uv.fs_realpath(joined)
if not real then
return nil
end
if real:sub(1, #root) ~= root then
return nil
end
return real
end
---@param sock uv.uv_tcp_t
---@param filepath string
local function serve_file_streaming(sock, filepath)
uv.fs_open(filepath, 'r', 438, function(err_open, fd)
if err_open or not fd then
vim.schedule(function()
error_response(sock, 500)
end)
return
end
uv.fs_fstat(fd, function(err_stat, stat)
if err_stat or not stat then
uv.fs_close(fd)
vim.schedule(function()
error_response(sock, 500)
end)
return
end
local mime = get_mime(filepath)
local size = stat.size
if is_html(filepath) then
uv.fs_read(fd, size, 0, function(err_read, data)
uv.fs_close(fd)
if err_read or not data then
vim.schedule(function()
error_response(sock, 500)
end)
return
end
local lower = data:lower()
local inject_pos = lower:find('
') if inject_pos then data = ('%s%s\n%s'):format( data:sub(1, inject_pos - 1), INJECT_TAG, data:sub(inject_pos) ) else data = ('%s\n%s'):format(data, INJECT_TAG) end local response = ('%s\z Content-Type: %s\r\n\z Content-Length: %d\r\n\z Connection: close\r\n\z \r\n\z %s'):format(response_line(200), mime, #data, data) local ok = pcall(sock.write, sock, response, function() pcall(sock.shutdown, sock, function() if not sock:is_closing() then sock:close() end end) end) if not ok and not sock:is_closing() then sock:close() end end) return end local header = ('%s\z Content-Type: %s\r\n\z Content-Length: %d\r\n\z Connection: close\r\n\z \r\n'):format(response_line(200), mime, size) local offset = 0 ---@type fun() local function read_chunk() local to_read = math.min(CHUNK_SIZE, size - offset) if to_read <= 0 then uv.fs_close(fd) pcall(sock.shutdown, sock, function() if not sock:is_closing() then sock:close() end end) return end uv.fs_read(fd, to_read, offset, function(err_chunk, chunk) if err_chunk or not chunk or #chunk == 0 then uv.fs_close(fd) if not sock:is_closing() then sock:close() end return end offset = offset + #chunk local wok = pcall(sock.write, sock, chunk, function() read_chunk() end) if not wok then uv.fs_close(fd) if not sock:is_closing() then sock:close() end end end) end local ok = pcall(sock.write, sock, header, function() read_chunk() end) if not ok then uv.fs_close(fd) if not sock:is_closing() then sock:close() end end end) end) end ---@param sock uv.uv_tcp_t ---@param dirpath string ---@param url_path string ---@param root string local function serve_directory_listing(sock, dirpath, url_path, root) uv.fs_scandir(dirpath, function(err, handle) if err or not handle then vim.schedule(function() error_response(sock, 500) end) return end ---@type string[] local dirs = {} ---@type string[] local files = {} while true do local name, typ = uv.fs_scandir_next(handle) if not name then break end if typ == 'directory' then dirs[#dirs + 1] = name else files[#files + 1] = name end end table.sort(dirs) table.sort(files) local prefix = url_path if prefix:sub(-1) ~= '/' then prefix = prefix .. '/' end ---@type string[] local entries = {} if dirpath ~= root then entries[#entries + 1] = '
' end for _, d in ipairs(dirs) do entries[#entries + 1] = ('
'):format(prefix, d, d) end for _, f in ipairs(files) do entries[#entries + 1] = ('
'):format(prefix, f, f) end local body = ([[