An open API service indexing awesome lists of open source software.

https://github.com/selimacerbas/live-server.nvim

A tiny, zero-dependency local web server for Neovim — written in pure Lua with `vim.loop`. Start a server on any file or folder, auto-reload the browser on save, and quickly reopen existing ports.
https://github.com/selimacerbas/live-server.nvim

http-server live-server lua neovim neovim-plugin sse

Last synced: 8 days ago
JSON representation

A tiny, zero-dependency local web server for Neovim — written in pure Lua with `vim.loop`. Start a server on any file or folder, auto-reload the browser on save, and quickly reopen existing ports.

Awesome Lists containing this project

README

          

# live-server.nvim

A tiny, zero-dependency **local web server** for Neovim — written in pure Lua with `vim.loop`.
Start a server on any file or folder, auto-reload the browser on save, and quickly reopen existing ports.

* **Pure Lua**: no npm, no Python, no binaries.
* **Local-only**: binds to `127.0.0.1` (loopback).
* **SSE live-reload**: instant page refresh on file changes (debounced).
* **CSS hot-inject**: stylesheet changes apply instantly without a full page reload.
* **Directory listing**: clean index when no `index.html` exists.
* **Telescope UX**: pick a path (file or directory) and a port from a friendly picker.
* **Which-key friendly**: group label in `init`, real mappings in `keys`, no conflicts.
* **Same-port retargeting**: starting on the same port updates the served root/index (reuses the same browser tab/URL).
* **Auto-start**: optionally start a server when you open an HTML file.
* **Statusline**: show active servers in your statusline/lualine.

> This plugin serves **only** on `127.0.0.1`. It's meant for local dev previews, not production.

---

## Requirements

* Neovim **0.8+** (tested on 0.9 / 0.10).
* Linux, macOS, or Windows.
* [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) **recommended** for the best picking UX (falls back to `vim.ui.select/input` if missing).
* [which-key.nvim](https://github.com/folke/which-key.nvim) recommended.

---

## Installation (lazy.nvim)

```lua
-- lua/plugins/live-server.lua
return {
"selimacerbas/live-server.nvim",
dependencies = {
"folke/which-key.nvim",
"nvim-telescope/telescope.nvim", -- recommended for path picker
},
init = function()
-- which-key group label only (best practice)
local ok, wk = pcall(require, "which-key")
if ok then wk.add({ { "l", group = "LiveServer" } }) end
end,
opts = {
default_port = 8000,
live_reload = { enabled = true, inject_script = true, debounce = 120, css_inject = true },
directory_listing = { enabled = true, show_hidden = false },
},
-- map to user commands (robust lazy-loading)
keys = {
{ "ls", "LiveServerStart", desc = "Start (pick path & port)" },
{ "lo", "LiveServerOpen", desc = "Open existing port in browser" },
{ "lr", "LiveServerReload", desc = "Force reload (pick port)" },
{ "lt", "LiveServerToggleLive", desc = "Toggle live-reload (pick port)" },
{ "li", "LiveServerStatus", desc = "Show server status" },
{ "lS", "LiveServerStop", desc = "Stop one (pick port)" },
{ "lA", "LiveServerStopAll", desc = "Stop all" },
},
config = function(_, opts)
require("live_server").setup(opts)
end,
}
```

---

## Usage

### Start a server

* Press **`ls`** (or run `:LiveServerStart`).
* Pick a **path** (file or directory), then pick a **port** (default `8000`).
* Your browser opens `http://127.0.0.1:/`.

> If you pick a **file**, the server serves the file's folder with that file as the default index.
> If you pick the **same port** again later, the server **retargets** to the new root/index instead of creating a new instance.

### Other commands

| Command | Description |
| --- | --- |
| `:LiveServerStart` | Pick a path and port, start serving |
| `:LiveServerOpen` | Pick a port, open its URL in browser |
| `:LiveServerReload` | Force reload all connected clients |
| `:LiveServerToggleLive` | Enable/disable file watching for a port |
| `:LiveServerStatus` | Show running servers (port, root, uptime, clients) |
| `:LiveServerStop` | Pick a port to stop |
| `:LiveServerStopAll` | Stop all servers |

---

## Options

Configured via `require("live_server").setup({...})` or `opts = { ... }` in your lazy spec.

```lua
{
default_port = 8000, -- default suggestion in the port picker
open_on_start = true, -- open browser after start/retarget
notify = true, -- use vim.notify for events
notify_on_reload = false, -- notify on every live-reload event
headers = { ["Cache-Control"] = "no-cache" }, -- extra response headers
cors = false, -- true/"*" or origin string (e.g. "http://localhost:3000")
index_names = { "index.html", "index.htm" }, -- index files to try in order

auto_start = nil, -- set to auto-start on filetype, e.g.:
-- auto_start = { filetypes = { "html" }, port = 8000 },

live_reload = {
enabled = true, -- watch files under the served root
inject_script = true, -- injects
debounce = 120, -- ms debounce for rapid changes
css_inject = true, -- hot-swap CSS without full page reload
},

directory_listing = {
enabled = true, -- render an index page if no index.html
show_hidden = false, -- include dotfiles in listing
},
}
```

---

## Features

### CSS hot-inject

When `css_inject` is enabled (default), editing a `.css` file triggers an instant stylesheet swap in the browser — no full page reload, no DOM state lost. All other file changes still trigger a full reload.

### Auto-start

Set `auto_start` to automatically start a server when you open a matching filetype:

```lua
auto_start = { filetypes = { "html" }, port = 8000 }
```

The server starts once per directory — opening another HTML file in the same folder won't spawn a duplicate.

### `.liveignore`

Create a `.liveignore` file in your served root to skip file-watcher noise. One pattern per line, `*` as wildcard, `#` for comments:

```
# Don't reload on these
node_modules
*.log
.git
dist
```

### CORS

Enable cross-origin headers for all responses:

```lua
cors = true, -- Access-Control-Allow-Origin: *
cors = "http://localhost:3000", -- specific origin
```

Useful when your frontend (on the live server) makes API calls to a separate backend.

### Statusline

Show active servers in lualine or any statusline:

```lua
-- lualine example
sections = {
lualine_x = {
{ require("live_server").statusline },
},
}
```

Returns `"[LS :8000]"` when a server is running, or `""` when idle.

### Styled error pages

404 and 400 errors display a clean, dark-mode-aware HTML page instead of raw text — easier to spot during development.

---

## Keymaps (default)

All under the which-key group **`<leader>l`**:

| Key | Action |
| ------------ | ------------------------------ |
| `<leader>ls` | Start (pick path & port) |
| `<leader>lo` | Open existing port in browser |
| `<leader>lr` | Force reload (pick port) |
| `<leader>lt` | Toggle live-reload (pick port) |
| `<leader>li` | Show server status |
| `<leader>lS` | Stop one (pick port) |
| `<leader>lA` | Stop all |

> We register only the **group label** in `init`, and return actual mappings in `keys` — the recommended pattern for Folke's ecosystem to avoid conflicts and enable lazy-loading on keypress.

---

## Design notes

* **Local by default**: binds to `127.0.0.1`. If you want LAN, you can change the bind address in `server.lua` (not recommended for security).
* **Path safety**: requests are realpath-checked to prevent escaping the served root.
* **Index resolution**: root directory → `default_index` (if starting from a file) → `index_names` in order → directory listing. Subdirectories always use their own index files.
* **Port 0 (OS-assigned)**: pass `port = 0` to let the OS pick a free port. The actual port is available via `inst.port` after `server.start()`.
* **Same port, new path**: reusing the same port retargets the server → same URL, so browsers typically reuse the same tab.
* **Event injection**: `GET /__live/inject?event=<type>&data=<json>` lets external processes broadcast SSE events to connected clients.
* **Graceful exit**: all servers are automatically stopped on `VimLeavePre`.

---

## Troubleshooting

* **"Port in use or failed to bind"**
Another process is using that port (or a previous server didn't exit cleanly). Pick a different port, or stop the other process.
You can stop live-server instances via `:LiveServerStop` or `:LiveServerStopAll`.

* **"start() bad argument #2 to 'start' (table expected, got number)"**
Some `luv` builds expect `fs_event:start(path, {recursive=true}, cb)` while others accept `start(path, cb)`. The plugin tries both. Make sure you're on the **latest** plugin files.

* **Browser didn't open**
We try `vim.ui.open` (NVIM 0.10) and fall back to `xdg-open`/`open`/`start`. If none work, copy the URL from the message and open manually.

* **Live-reload didn't trigger**

* It only injects into **HTML** pages.
* Ensure the served root actually changed (the watcher is per root).
* Check `.liveignore` isn't excluding the file.
* Try `:LiveServerToggleLive` off/on, or `:LiveServerReload` to force.

---

## API (for lua configs)

```lua
local ls = require("live_server")

ls.setup({ ... }) -- configure defaults
ls.start_picker() -- UI flow: pick path, then port
ls.open_existing() -- pick a port → open in browser
ls.force_reload() -- broadcast reload to clients
ls.toggle_livereload() -- enable/disable live-reload for a port
ls.status() -- print running server info
ls.statusline() -- returns "[LS :8000]" or ""
ls.stop_one() -- pick a port → stop
ls.stop_all() -- stop everything
```

### Server-level API (for plugin authors)

```lua
local server = require("live_server.server")

local inst = server.start({ port = 0, root = "/path", ... }) -- port 0 = OS-assigned
server.send_event(inst, "scroll", '{"line":42}') -- broadcast custom SSE event
server.reload(inst, "file.html") -- broadcast reload event
server.update_target(inst, new_root, new_index) -- retarget without restart
server.connected_client_count(inst) -- number of SSE clients
server.stop(inst) -- shut down
```

### HTTP event injection

External processes can inject SSE events via HTTP:

```
GET /__live/inject?event=<type>&data=<url-encoded-json>
```

This broadcasts the event to all connected SSE clients. Used by [markdown-preview.nvim](https://github.com/selimacerbas/markdown-preview.nvim) for cross-instance scroll sync.

---

## Roadmap

* Optional LAN binding with allowlist.
* Pluggable middlewares (custom headers, rewrites).
* Directory listing customization (sorting, columns).

---

## Contributing

PRs and issues are welcome!
Please include your **OS**, **Neovim version**, and (if relevant) **`vim.loop`/luv** version when reporting bugs. Repro steps make fixes fast.

---

## License

MIT © Selim Acerbaş