live-server.nvim/lua/live-server/init.lua
Barrett Ruth c5c69e9ec2
Some checks are pending
quality / changes (push) Waiting to run
quality / Lua Format Check (push) Blocked by required conditions
quality / Lua Lint Check (push) Blocked by required conditions
quality / Lua Type Check (push) Blocked by required conditions
quality / Markdown Format Check (push) Blocked by required conditions
fix: prevent infinite loop when started from non-filesystem buffer (#35)
## Problem

Calling \`:LiveServerStart\` from an oil.nvim buffer (or any URI-scheme
buffer) caused two issues: first, \`find_cached_dir\` entered an
infinite loop as \`fnamemodify(cur, ':h')\` degenerated to \`.\`,
freezing Neovim and pegging the CPU; second, even after fixing the loop,
the server would error out instead of doing the right thing — serving
the directory being browsed.

## Solution

Add a progress check to \`find_cached_dir\` so it bails if the path
stops changing. Fix \`resolve_dir\` to detect URI-scheme buffer names
(e.g. \`oil:///path/to/dir\`) and extract the real filesystem path from
them, so \`:LiveServerStart\` correctly serves the browsed directory.
Also corrects the bug report repro template (\`require('lazy')\`, \`lazy
= false\`, no deprecated \`opts\`) and ignores \`repro.lua\`.

Closes #34
2026-03-06 11:58:46 -05:00

244 lines
5.2 KiB
Lua

local server = require('live-server.server')
local M = {}
---@type boolean
local initialized = false
---@type table<string, live_server.Instance>
local instances = {}
---@class live_server.Config
---@field port? integer
---@field browser? boolean
---@field debounce? integer
---@field ignore? string[]
---@field css_inject? boolean
---@field debug? boolean
---@type live_server.Config
local defaults = {
port = 5500,
browser = true,
debounce = 120,
ignore = {},
css_inject = true,
debug = false,
}
---@type live_server.Config
local config = vim.deepcopy(defaults)
---@param message string
---@param level string
local function log(message, level)
vim.notify(('live-server.nvim: %s'):format(message), vim.log.levels[level])
end
---@type table<string, boolean>
local UNSUPPORTED_FLAGS = {
['--host'] = true,
['--open'] = true,
['--browser'] = true,
['--quiet'] = true,
['--entry-file'] = true,
['--spa'] = true,
['--mount'] = true,
['--proxy'] = true,
['--htpasswd'] = true,
['--cors'] = true,
['--https'] = true,
['--https-module'] = true,
['--middleware'] = true,
['--ignorePattern'] = true,
}
---@param user_config table
---@return table
local function migrate_args(user_config)
if not user_config.args then
return user_config
end
vim.deprecate(
'`vim.g.live_server.args`',
'`:h live-server-config`',
'v0.2.0',
'live-server.nvim',
false
)
local migrated = {}
for k, v in pairs(user_config) do
if k ~= 'args' then
migrated[k] = v
end
end
for _, arg in ipairs(user_config.args) do
local port = arg:match('%-%-port=(%d+)')
if port then
migrated.port = tonumber(port)
elseif arg == '--no-browser' then
migrated.browser = false
elseif arg == '--no-css-inject' then
migrated.css_inject = false
else
local wait = arg:match('%-%-wait=(%d+)')
if wait then
migrated.debounce = tonumber(wait)
else
local ignore_val = arg:match('%-%-ignore=(.*)')
if ignore_val then
migrated.ignore = migrated.ignore or {}
for pattern in ignore_val:gmatch('[^,]+') do
migrated.ignore[#migrated.ignore + 1] = vim.trim(pattern)
end
else
local flag = arg:match('^(%-%-[%w-]+)')
if flag and UNSUPPORTED_FLAGS[flag] then
log(('flag `%s` is not supported and will be ignored'):format(arg), 'WARN')
end
end
end
end
end
return migrated
end
local function init()
if initialized then
return
end
local user_config = vim.g.live_server or {}
user_config = migrate_args(user_config)
config = vim.tbl_deep_extend('force', defaults, user_config)
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
for dir, inst in pairs(instances) do
server.stop(inst)
instances[dir] = nil
end
end,
})
initialized = true
end
---@param dir? string
---@return string?
local function find_cached_dir(dir)
if not dir then
return nil
end
local cur = dir
while not instances[cur] do
if cur == '/' or cur:match('^[A-Z]:\\$') then
return nil
end
local parent = vim.fn.fnamemodify(cur, ':h')
if parent == cur then
return nil
end
cur = parent
end
return cur
end
---@param dir string
---@return live_server.Instance?
local function is_running(dir)
local cached_dir = find_cached_dir(dir)
return cached_dir and instances[cached_dir]
end
---@param dir? string
---@return string
local function resolve_dir(dir)
if not dir or dir == '' then
local bufname = vim.api.nvim_buf_get_name(0)
local uri_path = bufname:match('^%a+://(/.*)')
dir = uri_path or '%:p:h'
end
return vim.fn.expand(vim.fn.fnamemodify(vim.fn.expand(dir), ':p'))
end
---@param dir? string
function M.start(dir)
init()
dir = resolve_dir(dir)
if is_running(dir) then
log('already running', 'INFO')
return
end
local root_real = vim.uv.fs_realpath(dir)
if not root_real then
log(('directory does not exist: %s'):format(dir), 'ERROR')
return
end
local inst = server.start({
port = config.port,
root_real = root_real,
debounce = config.debounce,
ignore = config.ignore,
css_inject = config.css_inject,
debug = config.debug,
})
instances[dir] = inst
log(('started on 127.0.0.1:%d'):format(config.port), 'INFO')
if config.browser then
vim.ui.open(('http://127.0.0.1:%d/'):format(config.port))
end
end
---@param dir? string
function M.stop(dir)
dir = resolve_dir(dir)
local cached_dir = find_cached_dir(dir)
if cached_dir and instances[cached_dir] then
server.stop(instances[cached_dir])
instances[cached_dir] = nil
log('stopped', 'INFO')
end
end
---@param dir? string
function M.toggle(dir)
dir = resolve_dir(dir)
if is_running(dir) then
M.stop(dir)
else
M.start(dir)
end
end
---@deprecated Use `vim.g.live_server` instead
---@param user_config? live_server.Config
function M.setup(user_config)
vim.deprecate(
'`require("live-server").setup()`',
'`vim.g.live_server`',
'v0.1.0',
'live-server.nvim',
false
)
if user_config then
vim.g.live_server = vim.tbl_deep_extend('force', vim.g.live_server or {}, user_config)
end
initialized = false
init()
end
return M