## Problem The plugin requires users to install Node.js and the `live-server` npm package globally. This is a heavyweight external dependency for what amounts to a simple local dev-server workflow, and it creates friction for users who don't otherwise need Node.js. ## Solution Replace the npm shell-out with a pure-Lua HTTP server built on `vim.uv` (libuv bindings), eliminating all external dependencies. The new server supports static file serving, SSE-based live reload, CSS hot-swap without full page reload, directory listings, and recursive file watching with configurable debounce. Minimum Neovim version is bumped to 0.10 for `vim.uv` and `vim.ui.open`. The old `args`-based config is automatically migrated with a deprecation warning. Closes #28.
238 lines
5 KiB
Lua
238 lines
5 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
|
|
cur = vim.fn.fnamemodify(cur, ':h')
|
|
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
|
|
dir = '%: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
|