@edwinencomienda/live-reloader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/dist/index.js +232 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Live Reloader
|
|
2
|
+
|
|
3
|
+
A lightweight live-reload development server built with Bun. Serves static files and automatically reloads connected browsers when files change.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install globally using npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g live-reloader
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or using bun:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun install -g live-reloader
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Note:** This package requires Bun to be installed on your system. [Install Bun](https://bun.sh/docs/installation) first if you haven't already.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Serve current directory
|
|
25
|
+
live-reloader
|
|
26
|
+
|
|
27
|
+
# Serve a specific directory
|
|
28
|
+
live-reloader ./public
|
|
29
|
+
|
|
30
|
+
# Change the port
|
|
31
|
+
live-reloader --port 5173
|
|
32
|
+
|
|
33
|
+
# Custom host and port
|
|
34
|
+
live-reloader ./public --host 0.0.0.0 --port 8080
|
|
35
|
+
|
|
36
|
+
# Check version
|
|
37
|
+
live-reloader --version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
### Prerequisites
|
|
43
|
+
|
|
44
|
+
- [Bun](https://bun.sh/docs/installation) installed on your system
|
|
45
|
+
|
|
46
|
+
### Getting Started
|
|
47
|
+
|
|
48
|
+
Install dependencies:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Run in development mode (with hot reload):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bun run dev
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run from source:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun run start
|
|
64
|
+
|
|
65
|
+
# With options
|
|
66
|
+
bun run start ./public --port 5173
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Building
|
|
70
|
+
|
|
71
|
+
**Build for npm publishing:**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bun run build
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This creates `dist/index.js` and is automatically run before publishing.
|
|
78
|
+
|
|
79
|
+
**Build standalone binary:**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bun run build:binary
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This creates a standalone executable at `dist/live-reloader` that includes the Bun runtime.
|
|
86
|
+
|
|
87
|
+
## Publishing to npm
|
|
88
|
+
|
|
89
|
+
### First Time Setup
|
|
90
|
+
|
|
91
|
+
1. Create an npm account at [npmjs.com](https://www.npmjs.com/) if you don't have one
|
|
92
|
+
2. Login to npm from your terminal:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm login
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Publishing Updates
|
|
99
|
+
|
|
100
|
+
1. **Update the version** in `package.json`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Patch release (0.1.4 -> 0.1.5)
|
|
104
|
+
npm version patch
|
|
105
|
+
|
|
106
|
+
# Minor release (0.1.4 -> 0.2.0)
|
|
107
|
+
npm version minor
|
|
108
|
+
|
|
109
|
+
# Major release (0.1.4 -> 1.0.0)
|
|
110
|
+
npm version major
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This automatically creates a git commit and tag.
|
|
114
|
+
|
|
115
|
+
2. **Publish to npm**:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm publish
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `prepublishOnly` script automatically builds the dist folder before publishing.
|
|
122
|
+
|
|
123
|
+
3. **Push changes to GitHub**:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
git push && git push --tags
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Testing Before Publishing
|
|
130
|
+
|
|
131
|
+
Test the package locally before publishing:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Build the package
|
|
135
|
+
bun run build
|
|
136
|
+
|
|
137
|
+
# Create a test link
|
|
138
|
+
npm link
|
|
139
|
+
|
|
140
|
+
# Test the CLI
|
|
141
|
+
live-reloader --version
|
|
142
|
+
|
|
143
|
+
# Unlink when done
|
|
144
|
+
npm unlink -g live-reloader
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
Built with [Bun](https://bun.sh) 🚀
|
|
150
|
+
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// index.ts
|
|
5
|
+
var {serve } = globalThis.Bun;
|
|
6
|
+
import { watch } from "fs";
|
|
7
|
+
import { networkInterfaces } from "os";
|
|
8
|
+
import { resolve, sep } from "path";
|
|
9
|
+
var VERSION = "0.1.4";
|
|
10
|
+
var clients = new Set;
|
|
11
|
+
var encoder = new TextEncoder;
|
|
12
|
+
function parseArgs() {
|
|
13
|
+
const argv = Bun.argv.slice(2);
|
|
14
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
15
|
+
console.log(`live-reloader v${VERSION}`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
let port = 3000;
|
|
19
|
+
let hostname = "0.0.0.0";
|
|
20
|
+
let rootDir;
|
|
21
|
+
for (let i = 0;i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === "--port" || a === "-p") {
|
|
24
|
+
const v = argv[i + 1];
|
|
25
|
+
if (v) {
|
|
26
|
+
const n = Number(v);
|
|
27
|
+
if (Number.isFinite(n) && n > 0)
|
|
28
|
+
port = n;
|
|
29
|
+
}
|
|
30
|
+
i += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (a.startsWith("--port=")) {
|
|
34
|
+
const n = Number(a.slice("--port=".length));
|
|
35
|
+
if (Number.isFinite(n) && n > 0)
|
|
36
|
+
port = n;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (a === "--host" || a === "-h") {
|
|
40
|
+
const v = argv[i + 1];
|
|
41
|
+
if (v && !v.startsWith("-")) {
|
|
42
|
+
hostname = v;
|
|
43
|
+
}
|
|
44
|
+
i += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (a.startsWith("--host=")) {
|
|
48
|
+
hostname = a.slice("--host=".length);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!a.startsWith("-") && !rootDir) {
|
|
52
|
+
rootDir = a;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { port, hostname, rootDir: resolve(rootDir ?? process.cwd()) };
|
|
56
|
+
}
|
|
57
|
+
var { port, hostname, rootDir } = parseArgs();
|
|
58
|
+
function now() {
|
|
59
|
+
return new Date().toISOString();
|
|
60
|
+
}
|
|
61
|
+
function log(line) {
|
|
62
|
+
console.log(`[${now()}] ${line}`);
|
|
63
|
+
}
|
|
64
|
+
function formatPath(url) {
|
|
65
|
+
return `${url.pathname}${url.search}`;
|
|
66
|
+
}
|
|
67
|
+
function getLocalIP() {
|
|
68
|
+
const interfaces = networkInterfaces();
|
|
69
|
+
for (const name of Object.keys(interfaces)) {
|
|
70
|
+
const iface = interfaces[name];
|
|
71
|
+
if (!iface)
|
|
72
|
+
continue;
|
|
73
|
+
for (const addr of iface) {
|
|
74
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
75
|
+
return addr.address;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
var server;
|
|
82
|
+
try {
|
|
83
|
+
server = serve({
|
|
84
|
+
port,
|
|
85
|
+
hostname,
|
|
86
|
+
async fetch(req) {
|
|
87
|
+
const url = new URL(req.url);
|
|
88
|
+
const reqPath = formatPath(url);
|
|
89
|
+
if (url.pathname === "/__reload") {
|
|
90
|
+
let controllerRef = null;
|
|
91
|
+
const stream = new ReadableStream({
|
|
92
|
+
start(controller) {
|
|
93
|
+
controllerRef = controller;
|
|
94
|
+
clients.add(controller);
|
|
95
|
+
controller.enqueue(encoder.encode(`retry: 1000
|
|
96
|
+
|
|
97
|
+
`));
|
|
98
|
+
log(`SSE connected (clients=${clients.size})`);
|
|
99
|
+
},
|
|
100
|
+
cancel() {
|
|
101
|
+
if (controllerRef)
|
|
102
|
+
clients.delete(controllerRef);
|
|
103
|
+
log(`SSE disconnected (clients=${clients.size})`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const res2 = new Response(stream, {
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
109
|
+
"Cache-Control": "no-cache, no-transform",
|
|
110
|
+
Connection: "keep-alive"
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
log(`${req.method} ${reqPath} -> ${res2.status}`);
|
|
114
|
+
return res2;
|
|
115
|
+
}
|
|
116
|
+
let pathname;
|
|
117
|
+
try {
|
|
118
|
+
pathname = decodeURIComponent(url.pathname);
|
|
119
|
+
} catch {
|
|
120
|
+
const res2 = new Response("Bad Request", { status: 400 });
|
|
121
|
+
log(`${req.method} ${reqPath} -> ${res2.status}`);
|
|
122
|
+
return res2;
|
|
123
|
+
}
|
|
124
|
+
if (pathname === "/index.html") {
|
|
125
|
+
const res2 = Response.redirect(url.origin + "/" + url.search, 301);
|
|
126
|
+
log(`${req.method} ${reqPath} -> ${res2.status} (redirect to /)`);
|
|
127
|
+
return res2;
|
|
128
|
+
}
|
|
129
|
+
if (pathname === "/")
|
|
130
|
+
pathname = "/index.html";
|
|
131
|
+
const rootPrefix = rootDir.endsWith(sep) ? rootDir : rootDir + sep;
|
|
132
|
+
function isInsideRoot(p) {
|
|
133
|
+
return p === rootDir || p.startsWith(rootPrefix);
|
|
134
|
+
}
|
|
135
|
+
let resolvedPath = resolve(rootDir, `.${pathname}`);
|
|
136
|
+
if (!isInsideRoot(resolvedPath)) {
|
|
137
|
+
const res2 = new Response("Forbidden", { status: 403 });
|
|
138
|
+
log(`${req.method} ${reqPath} -> ${res2.status}`);
|
|
139
|
+
return res2;
|
|
140
|
+
}
|
|
141
|
+
let file = Bun.file(resolvedPath);
|
|
142
|
+
if (!await file.exists()) {
|
|
143
|
+
const hasExtension = pathname.includes(".") && !pathname.endsWith("/");
|
|
144
|
+
if (!hasExtension) {
|
|
145
|
+
const htmlPath = resolve(rootDir, `.${pathname}.html`);
|
|
146
|
+
if (isInsideRoot(htmlPath)) {
|
|
147
|
+
const htmlFile = Bun.file(htmlPath);
|
|
148
|
+
if (await htmlFile.exists()) {
|
|
149
|
+
resolvedPath = htmlPath;
|
|
150
|
+
file = htmlFile;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!await file.exists()) {
|
|
156
|
+
const res2 = new Response("Not Found", { status: 404 });
|
|
157
|
+
log(`${req.method} ${reqPath} -> ${res2.status}`);
|
|
158
|
+
return res2;
|
|
159
|
+
}
|
|
160
|
+
if (file.type.startsWith("text/html")) {
|
|
161
|
+
const t = await file.text();
|
|
162
|
+
const injected = `<script>
|
|
163
|
+
const es=new EventSource('/__reload');
|
|
164
|
+
es.onmessage=()=>location.reload();
|
|
165
|
+
</script>`;
|
|
166
|
+
const body = t.includes("</body>") ? t.replace("</body>", `${injected}</body>`) : `${t}
|
|
167
|
+
${injected}
|
|
168
|
+
`;
|
|
169
|
+
const res2 = new Response(body, {
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": file.type || "text/html; charset=utf-8"
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
log(`${req.method} ${reqPath} -> ${res2.status} (${resolvedPath})`);
|
|
175
|
+
return res2;
|
|
176
|
+
}
|
|
177
|
+
const res = new Response(file);
|
|
178
|
+
log(`${req.method} ${reqPath} -> ${res.status} (${resolvedPath})`);
|
|
179
|
+
return res;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
184
|
+
if (msg.includes("EADDRINUSE") || msg.toLowerCase().includes("in use")) {
|
|
185
|
+
log(`Port ${port} is already in use.`);
|
|
186
|
+
console.log(` Try using a different port: live-reloader --port ${port + 1}`);
|
|
187
|
+
} else {
|
|
188
|
+
log(`Failed to start server: ${msg}`);
|
|
189
|
+
}
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(`\x1B[36m\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\x1B[0m`);
|
|
194
|
+
console.log(`\x1B[32m live-reloader v${VERSION}\x1B[0m`);
|
|
195
|
+
console.log(`\x1B[36m\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\x1B[0m`);
|
|
196
|
+
console.log(`\x1B[33m\uD83D\uDCC1 Root:\x1B[0m ${rootDir}`);
|
|
197
|
+
console.log(`\x1B[33m\uD83D\uDD17 Local:\x1B[0m \x1B[1mhttp://localhost:${server.port}\x1B[0m`);
|
|
198
|
+
var localIP = getLocalIP();
|
|
199
|
+
if (localIP) {
|
|
200
|
+
console.log(`\x1B[33m\uD83D\uDD17 Network:\x1B[0m \x1B[1mhttp://${localIP}:${server.port}\x1B[0m`);
|
|
201
|
+
}
|
|
202
|
+
console.log(`\x1B[33m\uD83D\uDC40 Watching for changes...\x1B[0m`);
|
|
203
|
+
console.log(`\x1B[36m\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\x1B[0m`);
|
|
204
|
+
console.log("");
|
|
205
|
+
var reloadTimer;
|
|
206
|
+
var watcher = watch(rootDir, { recursive: true }, (eventType, filename) => {
|
|
207
|
+
const label = filename ? String(filename) : "(unknown file)";
|
|
208
|
+
log(`fs.watch ${eventType}: ${label}`);
|
|
209
|
+
if (reloadTimer)
|
|
210
|
+
clearTimeout(reloadTimer);
|
|
211
|
+
reloadTimer = setTimeout(() => {
|
|
212
|
+
const msg = encoder.encode(`data: reload
|
|
213
|
+
|
|
214
|
+
`);
|
|
215
|
+
let ok = 0;
|
|
216
|
+
for (const c of clients) {
|
|
217
|
+
try {
|
|
218
|
+
c.enqueue(msg);
|
|
219
|
+
ok += 1;
|
|
220
|
+
} catch {
|
|
221
|
+
clients.delete(c);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (ok > 0)
|
|
225
|
+
log(`reload broadcast -> ${ok} client(s)`);
|
|
226
|
+
}, 75);
|
|
227
|
+
});
|
|
228
|
+
watcher.on("error", (e) => {
|
|
229
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
230
|
+
log(`fs.watch error: ${msg}`);
|
|
231
|
+
});
|
|
232
|
+
log(`Watching "${rootDir}" for changes (recursive)`);
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@edwinencomienda/live-reloader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A fast, simple live-reload development server with file watching",
|
|
5
|
+
"author": "Edwin Encomienda",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"live-reload",
|
|
9
|
+
"dev-server",
|
|
10
|
+
"file-watcher",
|
|
11
|
+
"development",
|
|
12
|
+
"hot-reload",
|
|
13
|
+
"bun",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"live-reloader": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"module": "index.ts",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "bun run --hot index.ts",
|
|
27
|
+
"start": "bun run index.ts",
|
|
28
|
+
"build": "bun build index.ts --outfile dist/index.js --target bun",
|
|
29
|
+
"build:binary": "bun build index.ts --compile --minify --outfile dist/live-reloader",
|
|
30
|
+
"prepublishOnly": "bun run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"typescript": "^5"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/yourusername/live-reloader"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/yourusername/live-reloader/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/yourusername/live-reloader#readme"
|
|
46
|
+
}
|