feat: replace npm live-server with pure-Lua HTTP server (#29)
## 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.
This commit is contained in:
parent
baeb211719
commit
f42f958c24
17 changed files with 1097 additions and 298 deletions
238
lua/live-server/init.lua
Normal file
238
lua/live-server/init.lua
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue