@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.
Files changed (3) hide show
  1. package/README.md +150 -0
  2. package/dist/index.js +232 -0
  3. 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
+ }