diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 8db3f25..e81bcf8 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -65,11 +65,11 @@ body:
value: |
vim.env.LAZY_STDPATH = '.repro'
load(vim.fn.system('curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua'))()
- require('lazy').setup({
+ require('lazy.nvim').setup({
spec = {
{
'barrett-ruth/live-server.nvim',
- lazy = false,
+ opts = {},
},
},
})
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..83cc1c7
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,61 @@
+name: ci
+on:
+ workflow_call:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+
+jobs:
+ changes:
+ runs-on: ubuntu-latest
+ outputs:
+ lua: ${{ steps.changes.outputs.lua }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: changes
+ with:
+ filters: |
+ lua:
+ - 'lua/**'
+ - 'plugin/**'
+ - '*.lua'
+ - '.luarc.json'
+ - 'stylua.toml'
+ - 'selene.toml'
+
+ lua-format:
+ runs-on: ubuntu-latest
+ needs: changes
+ if: ${{ needs.changes.outputs.lua == 'true' }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: JohnnyMorganz/stylua-action@v4
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ version: 2.1.0
+ args: --check .
+
+ lua-lint:
+ runs-on: ubuntu-latest
+ needs: changes
+ if: ${{ needs.changes.outputs.lua == 'true' }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: NTBBloodbath/selene-action@v1.0.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ args: --display-style quiet .
+
+ lua-typecheck:
+ runs-on: ubuntu-latest
+ needs: changes
+ if: ${{ needs.changes.outputs.lua == 'true' }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: mrcjkb/lua-typecheck-action@v0
+ with:
+ checklevel: Warning
+ directories: lua
+ configpath: .luarc.json
diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml
index aca31a3..bac492d 100644
--- a/.github/workflows/quality.yaml
+++ b/.github/workflows/quality.yaml
@@ -24,7 +24,6 @@ jobs:
- '*.lua'
- '.luarc.json'
- '*.toml'
- - 'vim.yaml'
markdown:
- '*.md'
- 'doc/**/*.md'
@@ -36,8 +35,11 @@ jobs:
if: ${{ needs.changes.outputs.lua == 'true' }}
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@v31
- - run: nix develop --command stylua --check .
+ - uses: JohnnyMorganz/stylua-action@v4
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ version: 2.1.0
+ args: --check .
lua-lint:
name: Lua Lint Check
@@ -46,8 +48,11 @@ jobs:
if: ${{ needs.changes.outputs.lua == 'true' }}
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@v31
- - run: nix develop --command selene --display-style quiet .
+ - name: Lint with Selene
+ uses: NTBBloodbath/selene-action@v1.0.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ args: --display-style quiet .
lua-typecheck:
name: Lua Type Check
@@ -70,5 +75,15 @@ jobs:
if: ${{ needs.changes.outputs.markdown == 'true' }}
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@v31
- - run: nix develop --command prettier --check .
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 8
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ - name: Install prettier
+ run: pnpm add -g prettier@3.1.0
+ - name: Check markdown formatting with prettier
+ run: prettier --check .
diff --git a/.gitignore b/.gitignore
index c7b0274..c70d0ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,3 @@ doc/tags
CLAUDE.md
.claude/
node_modules/
-
-.envrc
-.direnv
-
-.repro
-repro.lua
diff --git a/.luarc.json b/.luarc.json
index e240dd5..3ccfeda 100644
--- a/.luarc.json
+++ b/.luarc.json
@@ -1,9 +1,8 @@
{
- "runtime.version": "LuaJIT",
+ "runtime.version": "Lua 5.1",
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
"diagnostics.globals": ["vim"],
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
"workspace.checkThirdParty": false,
- "workspace.ignoreDir": [".direnv"],
"completion.callSnippet": "Replace"
}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fdff634..10afe3f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,4 +14,4 @@ repos:
hooks:
- id: prettier
name: prettier
- files: \.(md|toml|yaml|yml|sh)$
+ files: \.(md|toml|ya?ml|sh)$
diff --git a/.styluaignore b/.styluaignore
deleted file mode 100644
index 9b42106..0000000
--- a/.styluaignore
+++ /dev/null
@@ -1 +0,0 @@
-.direnv/
diff --git a/README.md b/README.md
deleted file mode 100644
index 6aee784..0000000
--- a/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# live-server.nvim
-
-Live reload HTML, CSS, and JavaScript files inside Neovim. No external
-dependencies — the server runs entirely in Lua using Neovim's built-in libuv
-bindings.
-
-## Dependencies
-
-- Neovim >= 0.10
-
-## Installation
-
-Install with your package manager or via
-[luarocks](https://luarocks.org/modules/barrettruth/live-server.nvim):
-
-```
-luarocks install live-server.nvim
-```
-
-## Documentation
-
-```vim
-:help live-server.nvim
-```
-
-## Known Limitations
-
-- **No recursive file watching on Linux**: libuv's `uv_fs_event` only supports
- recursive directory watching on macOS and Windows. On Linux (inotify), the
- `recursive` flag is silently ignored, so only files in the served root
- directory trigger hot-reload. Files in subdirectories (e.g. `css/style.css`)
- will not be detected. See
- [libuv#1778](https://github.com/libuv/libuv/issues/1778).
diff --git a/doc/live-server.txt b/doc/live-server.txt
index 8fcad84..a32bdf0 100644
--- a/doc/live-server.txt
+++ b/doc/live-server.txt
@@ -1,152 +1,43 @@
-*live-server.txt* Live reload for web development
+live-server *live-server.txt*
Author: Barrett Ruth
Homepage:
===============================================================================
-INTRODUCTION *live-server.nvim*
+INTRODUCTION *live-server.nvim*
live-server.nvim automatically reloads HTML, CSS, and JavaScript files in the
-browser via a local development server. No external dependencies required —
-the server runs entirely in Lua using Neovim's built-in libuv bindings.
+browser via a local development server.
===============================================================================
-REQUIREMENTS *live-server-requirements*
-
-- Neovim >= 0.10
-
-===============================================================================
-CONFIGURATION *live-server-config*
-
- *vim.g.live_server*
-Configure via `vim.g.live_server` before the plugin loads: >lua
+CONFIGURATION *live-server.config*
+Configure via `vim.g.live_server` before the plugin loads:
+>lua
vim.g.live_server = {
- port = 8080,
- browser = false,
+ args = { '--port=5555' },
}
<
- *live_server.Config*
-Fields: ~
+Options: ~
- {port} (`integer?`) Port for the HTTP server. Default: `5500`
- {browser} (`boolean?`) Auto-open browser on start. Default: `true`
- {debounce} (`integer?`) Debounce interval in ms for file changes.
- Default: `120`
- {ignore} (`string[]?`) Lua patterns for file paths to ignore when
- watching for changes. Default: `{}`
- {css_inject} (`boolean?`) Hot-swap CSS without full page reload.
- Default: `true`
- {debug} (`boolean?`) Log each step of the hot-reload chain via
- |vim.notify()| at DEBUG level. Default: `false`
+ {args} `(string[])`: Arguments passed to live-server via `vim.fn.jobstart()`.
+ Run `live-server --help` to see available options.
+ Default: { '--port=5555' }
===============================================================================
-COMMANDS *live-server-commands*
+COMMANDS *live-server.commands*
- *:LiveServerStart*
+ *LiveServerStart*
:LiveServerStart [dir] Start the live server. If `dir` is provided, the
- server starts in the specified directory. Otherwise,
- uses the directory of the current file.
+ server starts in the specified directory.
- *:LiveServerStop*
+ *LiveServerStop*
:LiveServerStop [dir] Stop the live server. If `dir` is provided, stops
the server running in the specified directory.
- *:LiveServerToggle*
+ *LiveServerToggle*
:LiveServerToggle [dir] Toggle the live server on or off. If `dir` is
provided, toggles the server in that directory.
-===============================================================================
-MAPPINGS *live-server-mappings*
-
- *(live-server-start)*
-(live-server-start) Start the live server. Equivalent to
- |:LiveServerStart| with no arguments.
-
- *(live-server-stop)*
-(live-server-stop) Stop the live server. Equivalent to
- |:LiveServerStop| with no arguments.
-
- *(live-server-toggle)*
-(live-server-toggle) Toggle the live server. Equivalent to
- |:LiveServerToggle| with no arguments.
-
-Example configuration: >lua
- vim.keymap.set('n', 'ls', '(live-server-start)')
- vim.keymap.set('n', 'lx', '(live-server-stop)')
- vim.keymap.set('n', 'lt', '(live-server-toggle)')
-<
-
-===============================================================================
-API *live-server-api*
-
-All functions accept an optional `dir` parameter. When omitted, the directory
-of the current file is used.
-
-live_server.start({dir}) *live_server.start()*
- Start the live server in the specified directory.
-
- Parameters: ~
- {dir} (`string?`) Directory to serve
-
-live_server.stop({dir}) *live_server.stop()*
- Stop the live server running in the specified directory.
-
- Parameters: ~
- {dir} (`string?`) Directory of running server
-
-live_server.toggle({dir}) *live_server.toggle()*
- Toggle the live server on or off.
-
- Parameters: ~
- {dir} (`string?`) Directory to toggle
-
-live_server.setup({user_config}) *live_server.setup()*
- Deprecated: Use |vim.g.live_server| instead.
-
- Parameters: ~
- {user_config} (|live_server.Config|?) Configuration table
-
-===============================================================================
-EXAMPLES *live-server-examples*
-
-Start server in project root: >vim
-
- :LiveServerStart ~/projects/my-website
-<
-Lua keybinding to toggle server: >lua
-
- vim.keymap.set('n', 'ls', '(live-server-toggle)')
-<
-Configuration with custom port and no auto-open: >lua
-
- vim.g.live_server = {
- port = 3000,
- browser = false,
- }
-<
-Ignore node_modules and dotfiles: >lua
-
- vim.g.live_server = {
- ignore = { 'node_modules', '^%.' },
- }
-<
-===============================================================================
-KNOWN LIMITATIONS *live-server-limitations*
-
-No Recursive File Watching on Linux ~
- *live-server-linux-recursive*
-libuv's `uv_fs_event` only supports recursive directory watching on macOS
-(FSEvents) and Windows (ReadDirectoryChangesW). On Linux, the `recursive`
-flag is silently accepted but ignored because inotify does not support it
-natively. Only files directly in the served root directory trigger
-hot-reload; files in subdirectories (e.g. `css/style.css`) will not be
-detected.
-
-See https://github.com/libuv/libuv/issues/1778
-
-Enable `debug` in |live-server-config| to verify whether `fs_event`
-callbacks fire for a given file change.
-
-------------------------------------------------------------------------------
-vim:tw=78:ft=help:norl:
+vim:tw=80:ft=help:
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index b6f667a..0000000
--- a/flake.lock
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "nodes": {
- "nixpkgs": {
- "locked": {
- "lastModified": 1771423170,
- "narHash": "sha256-K7Dg9TQ0mOcAtWTO/FX/FaprtWQ8BmEXTpLIaNRhEwU=",
- "owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "bcc4a9d9533c033d806a46b37dc444f9b0da49dd",
- "type": "github"
- },
- "original": {
- "owner": "NixOS",
- "ref": "nixpkgs-unstable",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
- "root": {
- "inputs": {
- "nixpkgs": "nixpkgs",
- "systems": "systems"
- }
- },
- "systems": {
- "locked": {
- "lastModified": 1681028828,
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
- "owner": "nix-systems",
- "repo": "default",
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
- "type": "github"
- },
- "original": {
- "owner": "nix-systems",
- "repo": "default",
- "type": "github"
- }
- }
- },
- "root": "root",
- "version": 7
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index e95338d..0000000
--- a/flake.nix
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- description = "live-server.nvim — live reload for web development in Neovim";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
- systems.url = "github:nix-systems/default";
- };
-
- outputs =
- {
- nixpkgs,
- systems,
- ...
- }:
- let
- forEachSystem =
- f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system});
- in
- {
- formatter = forEachSystem (pkgs: pkgs.nixfmt-tree);
-
- devShells = forEachSystem (pkgs: {
- default = pkgs.mkShell {
- packages = [
- (pkgs.luajit.withPackages (
- ps: with ps; [
- busted
- nlua
- ]
- ))
- pkgs.prettier
- pkgs.stylua
- pkgs.selene
- pkgs.lua-language-server
- ];
- };
- });
- };
-}
diff --git a/lua/live-server.lua b/lua/live-server.lua
new file mode 100644
index 0000000..db335e7
--- /dev/null
+++ b/lua/live-server.lua
@@ -0,0 +1,148 @@
+local M = {}
+
+local initialized = false
+local job_cache = {}
+
+local defaults = {
+ args = { '--port=5555' },
+}
+
+local config = vim.deepcopy(defaults)
+
+local function log(message, level)
+ vim.notify(string.format('live-server.nvim: %s', message), vim.log.levels[level])
+end
+
+local function is_windows()
+ return vim.loop.os_uname().version:match('Windows')
+end
+
+local function init()
+ if initialized then
+ return true
+ end
+
+ local user_config = vim.g.live_server or {}
+ config = vim.tbl_deep_extend('force', defaults, user_config)
+
+ if
+ not vim.fn.executable('live-server')
+ and not (is_windows() and vim.fn.executable('live-server.cmd'))
+ then
+ log('live-server is not executable. Ensure the npm module is properly installed', 'ERROR')
+ return false
+ end
+
+ initialized = true
+ return true
+end
+
+local function find_cached_dir(dir)
+ if not dir then
+ return nil
+ end
+
+ local cur = dir
+ while not job_cache[cur] do
+ if cur == '/' or cur:match('^[A-Z]:\\$') then
+ return nil
+ end
+ cur = vim.fn.fnamemodify(cur, ':h')
+ end
+ return cur
+end
+
+local function is_running(dir)
+ local cached_dir = find_cached_dir(dir)
+ return cached_dir and job_cache[cached_dir]
+end
+
+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
+
+function M.start(dir)
+ if not init() then
+ return
+ end
+
+ dir = resolve_dir(dir)
+
+ if is_running(dir) then
+ log('live-server already running', 'INFO')
+ return
+ end
+
+ local cmd_exe = is_windows() and 'live-server.cmd' or 'live-server'
+ local cmd = { cmd_exe, dir }
+ vim.list_extend(cmd, config.args)
+
+ local job_id = vim.fn.jobstart(cmd, {
+ on_stderr = function(_, data)
+ if not data or data[1] == '' then
+ return
+ end
+ log(data[1]:match('.-m(.-)\27') or data[1], 'ERROR')
+ end,
+ on_exit = function(_, exit_code)
+ job_cache[dir] = nil
+ if exit_code == 143 then
+ return
+ end
+ log(string.format('stopped with code %s', exit_code), 'INFO')
+ end,
+ })
+
+ local port = 'unknown'
+ for _, arg in ipairs(config.args) do
+ local p = arg:match('%-%-port=(%d+)')
+ if p then
+ port = p
+ break
+ end
+ end
+
+ log(string.format('live-server started on 127.0.0.1:%s', port), 'INFO')
+ job_cache[dir] = job_id
+end
+
+function M.stop(dir)
+ dir = resolve_dir(dir)
+ local cached_dir = find_cached_dir(dir)
+ if cached_dir and job_cache[cached_dir] then
+ vim.fn.jobstop(job_cache[cached_dir])
+ job_cache[cached_dir] = nil
+ log('live-server stopped', 'INFO')
+ end
+end
+
+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
+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
+
+ init()
+end
+
+return M
diff --git a/lua/live-server/health.lua b/lua/live-server/health.lua
deleted file mode 100644
index 1225a1a..0000000
--- a/lua/live-server/health.lua
+++ /dev/null
@@ -1,42 +0,0 @@
-local M = {}
-
-function M.check()
- vim.health.start('live-server.nvim')
-
- if vim.fn.has('nvim-0.10') == 1 then
- vim.health.ok('Neovim >= 0.10')
- else
- vim.health.error(
- 'Neovim >= 0.10 is required',
- { 'Upgrade Neovim or pin live-server.nvim to v0.1.6' }
- )
- end
-
- if vim.uv then
- vim.health.ok('vim.uv is available')
- else
- vim.health.error('vim.uv is not available', { 'Neovim >= 0.10 provides vim.uv' })
- end
-
- local user_config = vim.g.live_server or {}
- if user_config.args then
- vim.health.warn(
- 'deprecated `args` config detected',
- { 'See `:h live-server-config` for the new format' }
- )
- else
- vim.health.ok('no deprecated config detected')
- end
-
- if jit.os == 'Linux' then
- vim.health.warn('recursive file watching is not supported on Linux', {
- 'Only files in the root directory will trigger reload. See `:h live-server-linux-recursive`',
- })
- end
-
- if vim.fn.executable('live-server') == 1 then
- vim.health.info('npm `live-server` is installed but no longer required')
- end
-end
-
-return M
diff --git a/lua/live-server/init.lua b/lua/live-server/init.lua
deleted file mode 100644
index ff84dc2..0000000
--- a/lua/live-server/init.lua
+++ /dev/null
@@ -1,244 +0,0 @@
-local server = require('live-server.server')
-
-local M = {}
-
----@type boolean
-local initialized = false
-
----@type table
-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
-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
diff --git a/lua/live-server/server.lua b/lua/live-server/server.lua
deleted file mode 100644
index 61484d9..0000000
--- a/lua/live-server/server.lua
+++ /dev/null
@@ -1,636 +0,0 @@
-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
-local MIME_TYPES = {
- html = 'text/html; charset=utf-8',
- htm = 'text/html; charset=utf-8',
- css = 'text/css; charset=utf-8',
- js = 'application/javascript; charset=utf-8',
- mjs = 'application/javascript; charset=utf-8',
- json = 'application/json; charset=utf-8',
- xml = 'application/xml; charset=utf-8',
- svg = 'image/svg+xml',
- png = 'image/png',
- jpg = 'image/jpeg',
- jpeg = 'image/jpeg',
- gif = 'image/gif',
- ico = 'image/x-icon',
- webp = 'image/webp',
- woff = 'font/woff',
- woff2 = 'font/woff2',
- ttf = 'font/ttf',
- otf = 'font/otf',
- txt = 'text/plain; charset=utf-8',
- md = 'text/plain; charset=utf-8',
- wasm = 'application/wasm',
-}
-
----@type table
-local REASON_PHRASES = {
- [200] = 'OK',
- [301] = 'Moved Permanently',
- [400] = 'Bad Request',
- [404] = 'Not Found',
- [405] = 'Method Not Allowed',
- [500] = 'Internal Server Error',
-}
-
----@type integer
-local CHUNK_SIZE = 65536
-
----@type string
-local CLIENT_JS = [[
-(function() {
- var es = new EventSource('/__live/events');
- es.addEventListener('reload', function(e) {
- var data = JSON.parse(e.data);
- if (data.css) {
- var links = document.querySelectorAll('link[rel="stylesheet"]');
- for (var i = 0; i < links.length; i++) {
- var href = links[i].href.replace(/[?&]_lr=\d+/, '');
- links[i].href = href + (href.indexOf('?') >= 0 ? '&' : '?') + '_lr=' + Date.now();
- }
- } else {
- location.reload();
- }
- });
-})();
-]]
-
----@type string
-local INJECT_TAG = ''
-
----@param str string
----@return string
-local function url_decode(str)
- return (str:gsub('%%(%x%x)', function(hex)
- return string.char(tonumber(hex, 16))
- end))
-end
-
----@param path string
----@return string
-local function get_mime(path)
- local ext = path:match('%.([^%.]+)$')
- if ext then
- return MIME_TYPES[ext:lower()] or 'application/octet-stream'
- end
- return 'application/octet-stream'
-end
-
----@param path string
----@return boolean
-local function is_html(path)
- local ext = path:match('%.([^%.]+)$')
- return ext and (ext:lower() == 'html' or ext:lower() == 'htm')
-end
-
----@param status integer
----@return string
-local function response_line(status)
- return ('HTTP/1.1 %d %s\r\n'):format(status, REASON_PHRASES[status] or 'Unknown')
-end
-
----@param sock uv.uv_tcp_t
----@param status integer
----@param headers table
----@param body? string
-local function write_response(sock, status, headers, body)
- local parts = { response_line(status) }
- headers['Connection'] = 'close'
- if body then
- headers['Content-Length'] = tostring(#body)
- end
- for k, v in pairs(headers) do
- parts[#parts + 1] = ('%s: %s\r\n'):format(k, v)
- end
- parts[#parts + 1] = '\r\n'
- if body then
- parts[#parts + 1] = body
- end
- local ok = pcall(sock.write, sock, table.concat(parts), 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
-
----@param sock uv.uv_tcp_t
----@param status integer
-local function error_response(sock, status)
- local phrase = REASON_PHRASES[status] or 'Error'
- local body = ([[%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('
-Index of %s
-
-%s
-')
- 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] = '
'):format(prefix, d, d)
- end
- for _, f in ipairs(files) do
- entries[#entries + 1] = ('