httpath 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/dist/index.mjs ADDED
@@ -0,0 +1,532 @@
1
+ #!/usr/bin/env node
2
+ import{parseArgs as e}from"node:util";import{extname as t,join as n,normalize as r,resolve as i,sep as a}from"node:path";import{createServer as o}from"node:http";import{access as s,readFile as c,readdir as l,stat as u}from"node:fs/promises";import{createReadStream as d,watch as f}from"node:fs";import{spawn as p}from"node:child_process";import{EventEmitter as m}from"node:events";const h=e=>({success:!0,data:e}),g=e=>({success:!1,error:e}),_=e=>e.success===!0,v=(e,t)=>{try{return h(e())}catch(e){return g(t?t(e):e)}},y=async(e,t)=>{try{return h(await e())}catch(e){return g(t?t(e):e)}};var b=class extends Error{constructor(e,t,n,r){super(e),this.code=t,this.statusCode=n,this.path=r,this.name=`HTTPathError`}},ee=class extends b{constructor(e,t){super(e,`FS_ERROR`,500,t),this.name=`FileSystemError`}},te=class extends b{constructor(e,t){super(e,`SECURITY_ERROR`,403,t),this.name=`SecurityError`}},ne=class extends b{constructor(e){super(e,`CONFIG_ERROR`,500),this.name=`ConfigurationError`}},re=class extends b{constructor(e){super(e,`NETWORK_ERROR`,500),this.name=`NetworkError`}};const x=e=>(t,...n)=>t instanceof e?t:new e(t instanceof Error?t.message:String(t),...n),S=x(ee),C=x(te),ie=x(ne),w=x(re);globalThis.success=h,globalThis.failure=g,globalThis.isSuccess=_;const T={port:8080,rootPath:process.cwd(),reload:!1,ignorePatterns:[`node_modules`,`.git`,`.DS_Store`],enableDirectoryListing:!0,restartOnChange:!1,logLevel:`info`,debounceMs:500},ae={port:{type:`string`,short:`p`,default:T.port.toString()},path:{type:`string`,short:`d`,default:T.rootPath},reload:{type:`boolean`,short:`r`,default:T.reload},ignore:{type:`string`,short:`i`,default:(T.ignorePatterns||[]).join(`,`)},"no-listing":{type:`boolean`,default:!T.enableDirectoryListing},"restart-on-change":{type:`boolean`,default:T.restartOnChange},log:{type:`string`,default:T.logLevel},help:{type:`boolean`,short:`h`,default:!1},version:{type:`boolean`,short:`v`,default:!1}},E=`
3
+ HTTPath - A minimalist Node.js file server with hot-reload capabilities
4
+
5
+ Usage: httpath [options]
6
+
7
+ Options:
8
+ -p, --port <number> Port number to listen on (default: 8080)
9
+ -d, --path <directory> Directory to serve files from (default: current directory)
10
+ -r, --reload Enable hot-reload functionality (default: false)
11
+ -i, --ignore <patterns> Comma-separated patterns to ignore (default: node_modules,.git,.DS_Store)
12
+ --no-listing Disable directory listing (default: false)
13
+ --restart-on-change Restart server process on file changes (default: false)
14
+ --log <level> Log level: debug, info, warn, error (default: info)
15
+ -h, --help Show this help message
16
+ -v, --version Show version number
17
+
18
+ Examples:
19
+ httpath # Start server on port 8080 in current directory
20
+ httpath --port 3000 # Start server on port 3000
21
+ httpath --path ./public # Serve files from ./public directory
22
+ httpath --reload # Enable hot-reload for development
23
+ httpath -p 3000 -d ./dist -r # Combined options
24
+
25
+ Documentation: https://github.com/MetalbolicX/httpath
26
+ `,D={name:`HTTPath`,version:`0.1.0`,description:`A minimalist Node.js file server with hot-reload capabilities`,author:`José Martínez Santana`,license:`MIT`},O=(t=process.argv.slice(2))=>{let n=v(()=>{let{values:n}=e({args:t,options:ae,allowPositionals:!0});n.help&&(console.log(E),process.exit(0)),n.version&&(console.log(`${D.name} v${D.version}\n${D.description}`),process.exit(0));let r=parseInt(n.port,10);if(isNaN(r)||r<1||r>65535)throw Error(`Invalid port number: ${n.port}. Port must be between 1 and 65535.`);let a=i(n.path),o=n.ignore?n.ignore.split(`,`).map(e=>e.trim()).filter(Boolean):T.ignorePatterns;return{port:r,rootPath:a,reload:!!n.reload,ignorePatterns:o,enableDirectoryListing:!n[`no-listing`],restartOnChange:!!n[`restart-on-change`],logLevel:n.log||T.logLevel}},ie);return n.success||(console.error(`❌ Error parsing arguments: ${n.error.message}`),console.log(E),process.exit(1)),n},k=e=>!Number.isInteger(e.port)||e.port<1||e.port>65535?(console.error(`❌ Invalid port: ${e.port}. Port must be between 1 and 65535.`),!1):typeof e.rootPath!=`string`||e.rootPath.length===0?(console.error(`❌ Invalid root path: ${e.rootPath}`),!1):typeof e.reload==`boolean`?!0:(console.error(`❌ Invalid reload flag: ${e.reload}. Must be boolean.`),!1),oe={allowDotFiles:!1,maxPathLength:1e3,blockedPatterns:`../,..\\,%2e%2e%2f,%2e%2e%5c,..%2f,..%5c,\0,%00,\r,
27
+ , ,CON,PRN,AUX,NUL,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9`.split(`,`)},se=new Set([`windows`,`system32`,`program files`,`program files (x86)`,`users`,`documents and settings`,`boot`,`etc`,`bin`,`sbin`,`usr`,`var`,`proc`,`sys`,`dev`,`root`,`home`]),ce=(e,t,n={})=>{let o={...oe,...n};if(!e||typeof e!=`string`)return failure(C(Error({isValid:!1,resolvedPath:``,error:`Invalid path: path must be a non-empty string`}.error)));if(!t||typeof t!=`string`)return failure(C(Error({isValid:!1,resolvedPath:``,error:`Invalid root path: root path must be a non-empty string`}.error)));if(e.length>o.maxPathLength){let e={isValid:!1,resolvedPath:``,error:`Path too long: exceeds ${o.maxPathLength} characters`};return failure(C(Error(e.error)))}let s=v(()=>decodeURIComponent(e),()=>C(Error(`Invalid URL encoding in path`)));if(!s.success)return failure(s.error);let c=s.data,l=c.toLowerCase();for(let e of o.blockedPatterns)if(l.includes(e.toLowerCase())){let t={isValid:!1,resolvedPath:``,error:`Blocked pattern detected: ${e}`};return failure(C(Error(t.error)))}let u=c.replace(/\\/g,`/`),d=v(()=>i(t,u.startsWith(`/`)?u.slice(1):u),C);if(!d.success)return failure(d.error);let f=d.data,p=r(t),m=r(f);if(!m.startsWith(p+a)&&m!==p)return failure(C(Error({isValid:!1,resolvedPath:``,error:`Path traversal detected: path is outside root directory`}.error)));if(!o.allowDotFiles){let e=u.split(`/`);for(let t of e)if(t.startsWith(`.`)&&t!==`.`&&t!==`..`)return failure(C(Error({isValid:!1,resolvedPath:``,error:`Dot files not allowed`}.error)))}let h=m.toLowerCase().split(a);for(let e of h)if(se.has(e)){let t={isValid:!1,resolvedPath:``,error:`Access to protected directory denied: ${e}`};return failure(C(Error(t.error)))}let g={isValid:!0,resolvedPath:m};return success(g)},le={".html":`text/html`,".htm":`text/html`,".css":`text/css`,".js":`text/javascript`,".mjs":`text/javascript`,".ts":`text/typescript`,".jsx":`text/jsx`,".tsx":`text/tsx`,".json":`application/json`,".xml":`application/xml`,".rss":`application/rss+xml`,".atom":`application/atom+xml`,".png":`image/png`,".jpg":`image/jpeg`,".jpeg":`image/jpeg`,".gif":`image/gif`,".svg":`image/svg+xml`,".ico":`image/x-icon`,".webp":`image/webp`,".bmp":`image/bmp`,".tiff":`image/tiff`,".tif":`image/tiff`,".woff":`font/woff`,".woff2":`font/woff2`,".ttf":`font/ttf`,".otf":`font/otf`,".eot":`application/vnd.ms-fontobject`,".pdf":`application/pdf`,".doc":`application/msword`,".docx":`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,".xls":`application/vnd.ms-excel`,".xlsx":`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,".ppt":`application/vnd.ms-powerpoint`,".pptx":`application/vnd.openxmlformats-officedocument.presentationml.presentation`,".txt":`text/plain`,".md":`text/markdown`,".csv":`text/csv`,".log":`text/plain`,".yaml":`text/yaml`,".yml":`text/yaml`,".toml":`text/plain`,".ini":`text/plain`,".conf":`text/plain`,".cfg":`text/plain`,".zip":`application/zip`,".rar":`application/vnd.rar`,".7z":`application/x-7z-compressed`,".tar":`application/x-tar`,".gz":`application/gzip`,".bz2":`application/x-bzip2`,".mp3":`audio/mpeg`,".wav":`audio/wav`,".ogg":`audio/ogg`,".m4a":`audio/mp4`,".aac":`audio/aac`,".flac":`audio/flac`,".mp4":`video/mp4`,".avi":`video/x-msvideo`,".mov":`video/quicktime`,".wmv":`video/x-ms-wmv`,".flv":`video/x-flv`,".webm":`video/webm`,".mkv":`video/x-matroska`,".exe":`application/octet-stream`,".msi":`application/x-msdownload`,".deb":`application/vnd.debian.binary-package`,".rpm":`application/x-rpm`,".dmg":`application/x-apple-diskimage`,".iso":`application/x-iso9660-image`,".map":`application/json`,".lock":`text/plain`,".gitignore":`text/plain`,".env":`text/plain`,".dockerfile":`text/plain`,".makefile":`text/plain`},ue=new Set([`text/html`,`text/css`,`text/javascript`,`text/plain`,`text/markdown`,`text/xml`,`application/json`,`application/xml`,`image/svg+xml`]),de=(e,t={})=>{let{defaultType:n=`application/octet-stream`,customMappings:r={}}=t,i=e.startsWith(`.`)?e.toLowerCase():`.${e.toLowerCase()}`;return r[i]||le[i]||n},fe=e=>ue.has(e)||e.startsWith(`text/`),pe=e=>{let t=e.lastIndexOf(`.`);return t===-1||t===0?``:e.slice(t).toLowerCase()},A=(e,t={})=>de(pe(e),t),j={showHidden:!1,sortBy:`name`,sortOrder:`asc`},M={customCSS:``,customJS:``,favicon:`data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">📁</text></svg>`,theme:`auto`},N=async(e,t={})=>{let r={...j,...t},i=await y(()=>l(e),S);if(!i.success)return g(i.error);let a=[];for(let t of i.data){if(!r.showHidden&&t.startsWith(`.`))continue;let i=n(e,t),o=await y(()=>u(i),S);o.success?a=[...a,{name:t,isDir:o.data.isDirectory(),size:o.data.size,lastModified:o.data.mtime}]:console.warn(`Warning: Cannot access file ${t}:`,o.error)}return a.sort((e,t)=>{if(e.isDir&&!t.isDir)return-1;if(!e.isDir&&t.isDir)return 1;let n=0;switch(r.sortBy){case`size`:n=(e.size||0)-(t.size||0);break;case`date`:n=(e.lastModified?.getTime()||0)-(t.lastModified?.getTime()||0);break;default:n=e.name.localeCompare(t.name);break}return r.sortOrder===`desc`?-n:n}),h(a)},P=async(e,t,n={})=>{let r={...j,...n},i={...M,...n},a=await N(e,r);return a.success?h(z({title:`Directory listing for ${t}`,path:t,files:a.data,parentPath:t===`/`?void 0:F(t),serverInfo:{name:`HTTPath`,version:`0.1.0`,uptime:process.uptime()}},i)):h(B(`Error reading directory`,a.error.message))},F=e=>{let t=e.split(`/`).filter(e=>e);return t.pop(),t.length>0?`/`+t.join(`/`):`/`},I=e=>{if(e===0)return`0 B`;let t=[`B`,`KB`,`MB`,`GB`,`TB`],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/1024**n).toFixed(n===0?0:1)} ${t[n]}`},L=e=>e.toLocaleString(`en-US`,{year:`numeric`,month:`short`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`}),R=e=>{if(e.isDir)return`📁`;let n=t(e.name).toLowerCase();return{".html":`🌐`,".htm":`🌐`,".css":`🎨`,".js":`⚡`,".mjs":`⚡`,".ts":`📘`,".jsx":`⚛️`,".tsx":`⚛️`,".json":`📋`,".xml":`📋`,".txt":`📄`,".md":`📝`,".pdf":`📕`,".doc":`📘`,".docx":`📘`,".xls":`📗`,".xlsx":`📗`,".png":`🖼️`,".jpg":`🖼️`,".jpeg":`🖼️`,".gif":`🖼️`,".svg":`🎨`,".mp3":`🎵`,".wav":`🎵`,".mp4":`🎬`,".avi":`🎬`,".zip":`📦`,".tar":`📦`,".gz":`📦`}[n]||`📄`},z=(e,t)=>{let{title:r,path:i,files:a,parentPath:o}=e,s=o?`<tr><td><a href="${o}" class="parent-link">📁 ..</a></td><td>-</td><td>-</td></tr>`:``,c=a.map(e=>{let t=n(i,e.name).replace(/\\/g,`/`),r=e.isDir?`${e.name}/`:e.name,a=R(e),o=e.isDir?`-`:I(e.size||0),s=e.lastModified?L(e.lastModified):`-`;return`
28
+ <tr>
29
+ <td><a href="${t}" class="${e.isDir?`directory`:`file`}">${a} ${r}</a></td>
30
+ <td>${o}</td>
31
+ <td>${s}</td>
32
+ </tr>`}).join(``);return`
33
+ <!DOCTYPE html>
34
+ <html lang="en" class="${t.theme===`dark`?`theme-dark`:t.theme===`light`?`theme-light`:``}">
35
+ <head>
36
+ <meta charset="utf-8">
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
+ <title>${r}</title>
39
+ <link rel="icon" href="${t.favicon}">
40
+ <style>
41
+ :root {
42
+ --bg-color: #ffffff;
43
+ --text-color: #333333;
44
+ --border-color: #e1e5e9;
45
+ --hover-color: #f8f9fa;
46
+ --link-color: #0066cc;
47
+ --header-bg: #f8f9fa;
48
+ }
49
+
50
+ .theme-dark {
51
+ --bg-color: #1a1a1a;
52
+ --text-color: #e1e1e1;
53
+ --border-color: #333333;
54
+ --hover-color: #2a2a2a;
55
+ --link-color: #4da6ff;
56
+ --header-bg: #2a2a2a;
57
+ }
58
+
59
+ @media (prefers-color-scheme: dark) {
60
+ :root {
61
+ --bg-color: #1a1a1a;
62
+ --text-color: #e1e1e1;
63
+ --border-color: #333333;
64
+ --hover-color: #2a2a2a;
65
+ --link-color: #4da6ff;
66
+ --header-bg: #2a2a2a;
67
+ }
68
+ }
69
+
70
+ body {
71
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
72
+ line-height: 1.6;
73
+ margin: 0;
74
+ padding: 20px;
75
+ background-color: var(--bg-color);
76
+ color: var(--text-color);
77
+ }
78
+
79
+ .container {
80
+ max-width: 1200px;
81
+ margin: 0 auto;
82
+ }
83
+
84
+ h1 {
85
+ border-bottom: 2px solid var(--border-color);
86
+ padding-bottom: 10px;
87
+ margin-bottom: 20px;
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 10px;
91
+ }
92
+
93
+ .breadcrumb {
94
+ font-size: 0.9em;
95
+ color: var(--link-color);
96
+ margin-bottom: 15px;
97
+ }
98
+
99
+ .breadcrumb a {
100
+ color: var(--link-color);
101
+ text-decoration: none;
102
+ }
103
+
104
+ .breadcrumb a:hover {
105
+ text-decoration: underline;
106
+ }
107
+
108
+ table {
109
+ width: 100%;
110
+ border-collapse: collapse;
111
+ margin-bottom: 20px;
112
+ background: var(--bg-color);
113
+ border-radius: 8px;
114
+ overflow: hidden;
115
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
116
+ }
117
+
118
+ th {
119
+ background-color: var(--header-bg);
120
+ padding: 12px 15px;
121
+ text-align: left;
122
+ font-weight: 600;
123
+ border-bottom: 1px solid var(--border-color);
124
+ }
125
+
126
+ td {
127
+ padding: 10px 15px;
128
+ border-bottom: 1px solid var(--border-color);
129
+ }
130
+
131
+ tr:hover {
132
+ background-color: var(--hover-color);
133
+ }
134
+
135
+ tr:last-child td {
136
+ border-bottom: none;
137
+ }
138
+
139
+ a {
140
+ color: var(--link-color);
141
+ text-decoration: none;
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 8px;
145
+ }
146
+
147
+ a:hover {
148
+ text-decoration: underline;
149
+ }
150
+
151
+ .parent-link {
152
+ font-weight: 600;
153
+ opacity: 0.8;
154
+ }
155
+
156
+ .directory {
157
+ font-weight: 500;
158
+ }
159
+
160
+ .footer {
161
+ margin-top: 30px;
162
+ padding-top: 20px;
163
+ border-top: 1px solid var(--border-color);
164
+ text-align: center;
165
+ color: var(--text-color);
166
+ opacity: 0.7;
167
+ font-size: 0.9em;
168
+ }
169
+
170
+ .stats {
171
+ display: flex;
172
+ justify-content: space-between;
173
+ margin-bottom: 20px;
174
+ font-size: 0.9em;
175
+ color: var(--text-color);
176
+ opacity: 0.8;
177
+ }
178
+
179
+ @media (max-width: 768px) {
180
+ body {
181
+ padding: 10px;
182
+ }
183
+
184
+ table {
185
+ font-size: 0.9em;
186
+ }
187
+
188
+ .stats {
189
+ flex-direction: column;
190
+ gap: 5px;
191
+ }
192
+ }
193
+
194
+ ${t.customCSS}
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <div class="container">
199
+ <h1>📁 Directory listing for ${i}</h1>
200
+
201
+ <div class="stats">
202
+ <span>${a.length} items</span>
203
+ <span>Served by HTTPath v${e.serverInfo?.version}</span>
204
+ </div>
205
+
206
+ <table>
207
+ <thead>
208
+ <tr>
209
+ <th>Name</th>
210
+ <th>Size</th>
211
+ <th>Modified</th>
212
+ </tr>
213
+ </thead>
214
+ <tbody>
215
+ ${s}
216
+ ${c}
217
+ </tbody>
218
+ </table>
219
+
220
+ <div class="footer">
221
+ <p>HTTPath - A minimalist Node.js file server</p>
222
+ </div>
223
+ </div>
224
+
225
+ ${t.customJS?`<script>${t.customJS}<\/script>`:``}
226
+ </body>
227
+ </html>`},B=(e,t)=>`
228
+ <!DOCTYPE html>
229
+ <html lang="en">
230
+ <head>
231
+ <meta charset="utf-8">
232
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
233
+ <title>Error - HTTPath</title>
234
+ <style>
235
+ body {
236
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
237
+ display: flex;
238
+ justify-content: center;
239
+ align-items: center;
240
+ min-height: 100vh;
241
+ margin: 0;
242
+ background-color: #f8f9fa;
243
+ color: #333;
244
+ }
245
+ .error-container {
246
+ text-align: center;
247
+ padding: 40px;
248
+ background: white;
249
+ border-radius: 8px;
250
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
251
+ }
252
+ .error-icon {
253
+ font-size: 4em;
254
+ margin-bottom: 20px;
255
+ }
256
+ h1 {
257
+ color: #dc3545;
258
+ margin-bottom: 10px;
259
+ }
260
+ p {
261
+ color: #666;
262
+ line-height: 1.6;
263
+ }
264
+ </style>
265
+ </head>
266
+ <body>
267
+ <div class="error-container">
268
+ <div class="error-icon">❌</div>
269
+ <h1>${e}</h1>
270
+ <p>${t}</p>
271
+ </div>
272
+ </body>
273
+ </html>`,V=async e=>h((await y(()=>s(e),S)).success),H=async e=>await y(()=>u(e),S),me=async e=>await y(()=>c(e,`utf8`),S),he=e=>d(e),ge=e=>fe(A(e))?`buffer`:`stream`,U={watchPath:process.cwd(),ignored:[`node_modules`,`.git`,`.vscode`,`.idea`,`dist`,`build`,`.next`,`.nuxt`,`coverage`,`.nyc_output`,`*.log`,`.DS_Store`,`Thumbs.db`],debounceMs:500,restartOnChange:!1},W=`
274
+ <script>
275
+ (function() {
276
+ 'use strict';
277
+
278
+ let eventSource;
279
+ let reconnectAttempts = 0;
280
+ const maxReconnectAttempts = 10;
281
+ const baseReconnectDelay = 1000;
282
+
283
+ function connect() {
284
+ console.log('[HTTPath] Connecting to hot-reload server...');
285
+
286
+ eventSource = new EventSource('/__reload__');
287
+
288
+ eventSource.onopen = function() {
289
+ console.log('[HTTPath] Hot-reload connected');
290
+ reconnectAttempts = 0;
291
+
292
+ // Show connection indicator
293
+ showConnectionStatus('connected');
294
+ };
295
+
296
+ eventSource.onmessage = function(event) {
297
+ const data = event.data;
298
+ console.log('[HTTPath] Received reload signal:', data);
299
+
300
+ if (data === 'reload') {
301
+ console.log('[HTTPath] Reloading page...');
302
+ showReloadNotification();
303
+
304
+ // Small delay to show notification
305
+ setTimeout(() => {
306
+ window.location.reload();
307
+ }, 200);
308
+ }
309
+ };
310
+
311
+ eventSource.onerror = function() {
312
+ console.warn('[HTTPath] Hot-reload connection lost');
313
+ eventSource.close();
314
+
315
+ showConnectionStatus('disconnected');
316
+
317
+ if (reconnectAttempts < maxReconnectAttempts) {
318
+ reconnectAttempts++;
319
+ const delay = baseReconnectDelay * Math.pow(1.5, reconnectAttempts - 1);
320
+
321
+ console.log(\`[HTTPath] Reconnecting in \${delay}ms... (attempt \${reconnectAttempts})\`);
322
+
323
+ setTimeout(connect, delay);
324
+ } else {
325
+ console.error('[HTTPath] Max reconnection attempts reached. Please refresh the page.');
326
+ showConnectionStatus('failed');
327
+ }
328
+ };
329
+ }
330
+
331
+ function showConnectionStatus(status) {
332
+ const indicator = getOrCreateIndicator();
333
+
334
+ switch (status) {
335
+ case 'connected':
336
+ indicator.style.background = '#28a745';
337
+ indicator.title = 'Hot-reload connected';
338
+ indicator.textContent = '🔄';
339
+ break;
340
+ case 'disconnected':
341
+ indicator.style.background = '#ffc107';
342
+ indicator.title = 'Hot-reload disconnected - attempting to reconnect...';
343
+ indicator.textContent = '⏳';
344
+ break;
345
+ case 'failed':
346
+ indicator.style.background = '#dc3545';
347
+ indicator.title = 'Hot-reload failed - refresh page to reconnect';
348
+ indicator.textContent = '❌';
349
+ break;
350
+ }
351
+ }
352
+
353
+ function showReloadNotification() {
354
+ const notification = document.createElement('div');
355
+ notification.style.cssText = \`
356
+ position: fixed;
357
+ top: 20px;
358
+ right: 20px;
359
+ background: #007bff;
360
+ color: white;
361
+ padding: 12px 20px;
362
+ border-radius: 6px;
363
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
364
+ font-size: 14px;
365
+ font-weight: 500;
366
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
367
+ z-index: 10000;
368
+ animation: slideIn 0.3s ease-out;
369
+ \`;
370
+
371
+ notification.textContent = '🔄 Reloading...';
372
+
373
+ const style = document.createElement('style');
374
+ style.textContent = \`
375
+ @keyframes slideIn {
376
+ from { transform: translateX(100%); opacity: 0; }
377
+ to { transform: translateX(0); opacity: 1; }
378
+ }
379
+ \`;
380
+
381
+ document.head.appendChild(style);
382
+ document.body.appendChild(notification);
383
+ }
384
+
385
+ function getOrCreateIndicator() {
386
+ let indicator = document.getElementById('httpath-reload-indicator');
387
+
388
+ if (!indicator) {
389
+ indicator = document.createElement('div');
390
+ indicator.id = 'httpath-reload-indicator';
391
+ indicator.style.cssText = \`
392
+ position: fixed;
393
+ bottom: 20px;
394
+ right: 20px;
395
+ width: 40px;
396
+ height: 40px;
397
+ border-radius: 50%;
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ font-size: 16px;
402
+ color: white;
403
+ font-weight: bold;
404
+ cursor: pointer;
405
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
406
+ z-index: 9999;
407
+ transition: all 0.3s ease;
408
+ user-select: none;
409
+ \`;
410
+
411
+ indicator.addEventListener('click', () => {
412
+ if (eventSource && eventSource.readyState === EventSource.OPEN) {
413
+ window.location.reload();
414
+ } else {
415
+ connect();
416
+ }
417
+ });
418
+
419
+ document.body.appendChild(indicator);
420
+ }
421
+
422
+ return indicator;
423
+ }
424
+
425
+ // Start connection when DOM is ready
426
+ if (document.readyState === 'loading') {
427
+ document.addEventListener('DOMContentLoaded', connect);
428
+ } else {
429
+ connect();
430
+ }
431
+
432
+ // Cleanup on page unload
433
+ window.addEventListener('beforeunload', () => {
434
+ if (eventSource) {
435
+ eventSource.close();
436
+ }
437
+ });
438
+ })();
439
+ <\/script>`;var G=class extends m{constructor(e={}){super(),this.watcher=null,this.clients=new Map,this.debounceTimer=null,this.options={...U,...e}}start(){this.watcher&&this.stop();let e=v(()=>{this.watcher=f(this.options.watchPath,{recursive:!0},(e,t)=>{t&&this.handleFileChange(e,t)}),console.log(`🔄 Hot-reload watching: ${this.options.watchPath}`),process.on(`SIGINT`,()=>this.stop()),process.on(`SIGTERM`,()=>this.stop())},S);return e.success?h(void 0):(console.warn(`File watching not supported on this system:`,e.error),g(e.error))}stop(){this.watcher&&=(this.watcher.close(),null);for(let e of this.clients.values())this.removeClient(e.id);this.debounceTimer&&=(clearTimeout(this.debounceTimer),null),console.log(`🔄 Hot-reload stopped`)}handleSSEConnection(e,t){let n=this.generateClientId();t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`,"Access-Control-Allow-Origin":`*`,"Access-Control-Allow-Headers":`Cache-Control`,"Access-Control-Allow-Methods":`GET, OPTIONS`}),t.write(`data: connected
440
+
441
+ `);let r={response:t,id:n,connectedAt:new Date};this.clients.set(n,r),e.on(`close`,()=>{this.removeClient(n)}),e.on(`aborted`,()=>{this.removeClient(n)}),t.on(`error`,e=>{console.warn(`SSE client error for ${n}:`,e.message),this.removeClient(n)}),console.log(`🔗 Hot-reload client connected: ${n} (${this.clients.size} total)`),this.emit(`client-connected`,n)}broadcastReload(e){if(this.clients.size===0)return;let t=[];for(let[e,n]of this.clients)try{n.response.write(`data: reload
442
+
443
+ `)}catch(n){console.warn(`Failed to send reload signal to client ${e}:`,n),t=[...t,e]}for(let e of t)this.removeClient(e);console.log(`📡 Reload signal sent to ${this.clients.size} clients`),e&&this.emit(`reload-triggered`,e)}injectScript(e){return e.includes(`</body>`)?e.replace(`</body>`,`${W}</body>`):e.includes(`</html>`)?e.replace(`</html>`,`${W}</html>`):e+W}getClientCount(){return this.clients.size}getClientInfo(){return Array.from(this.clients.values()).map(e=>({id:e.id,connectedAt:e.connectedAt}))}handleFileChange(e,t){this.shouldIgnoreFile(t)||(this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{console.log(`📝 File changed: ${t}`);let n={type:this.getEventType(e),path:t,timestamp:new Date},r=this.shouldRestartServer([t]),i=this.shouldTriggerBrowserReload([t]);if(this.options.restartOnChange||r){console.log(`🔁 Restart requested due to change: ${t}`);try{this.restartProcess()}catch(e){console.warn(`Failed to restart process:`,e)}return}i&&this.broadcastReload(n),this.emit(`file-changed`,n)},this.options.debounceMs))}shouldIgnoreFile(e){return this.options.ignored.some(t=>t.includes(`*`)?new RegExp(t.replace(/\*/g,`.*`),`i`).test(e):e.includes(t))}getEventType(e){switch(e){case`change`:return`file-changed`;case`rename`:return`file-added`;default:return`file-changed`}}shouldRestartServer(e){let t=[/\.ts$/i,/\.js$/i,/\.mjs$/i,/\.json$/i,/\.toml$/i,/\.ya?ml$/i,/deno\.json/i,/deno\.lock/i,/package\.json/i];return e.some(e=>t.some(t=>t.test(e)))}shouldTriggerBrowserReload(e){let t=[/\.html?$/i,/\.css$/i,/\.s[ac]ss$/i,/\.less$/i,/\.js$/i,/\.jsx$/i,/\.ts$/i,/\.tsx$/i,/\.vue$/i,/\.svelte$/i,/\.md$/i,/\.(png|jpe?g|gif|svg|webp|ico)$/i,/\.(woff2?|ttf|eot)$/i,/\.json$/i];return e.some(e=>t.some(t=>t.test(e)))}restartProcess(){let e=process.argv.slice(1);p(process.execPath,e,{detached:!0,stdio:`inherit`}).unref(),console.log(`🔁 Spawned replacement process, exiting current process...`),process.exit(0)}generateClientId(){return`client_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}removeClient(e){let t=this.clients.get(e);t&&(v(()=>t.response.end(),()=>Error(`Error closing connection`)),this.clients.delete(e),console.log(`🔗 Hot-reload client disconnected: ${e} (${this.clients.size} remaining)`),this.emit(`client-disconnected`,e))}};const _e=e=>new G(e),ve={level:`info`,format:`simple`,includeTimestamp:!0,colorize:!0},K={reset:`\x1B[0m`,bright:`\x1B[1m`,dim:`\x1B[2m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,white:`\x1B[37m`,gray:`\x1B[90m`},q={debug:0,info:1,warn:2,error:3},ye={debug:K.gray,info:K.blue,warn:K.yellow,error:K.red};var be=class{constructor(e={}){this.options={...ve,...e},this.startTime=Date.now()}debug(e,...t){this.log(`debug`,e,...t)}info(e,...t){this.log(`info`,e,...t)}warn(e,...t){this.log(`warn`,e,...t)}error(e,...t){this.log(`error`,e,...t)}logRequest(e,t,n){let r=this.formatTimestamp(),i=`${this.colorize(e,K.cyan)} ${this.colorize(t,K.white)}`;this.options.format===`detailed`&&n&&(i+=this.colorize(` - ${n}`,K.gray)),this.writeLog(`info`,i,r)}logResponse(e,t){let n=this.formatTimestamp(),r=this.getStatusColor(e),i=`Response: ${this.colorize(e.toString(),r)}`;if(t!==void 0){let e=t>1e3?K.yellow:t>500?K.cyan:K.green,n=this.colorize(`${t}ms`,e);i+=` in ${n}`}this.writeLog(`info`,i,n)}createLogEntry(e,t,n){return{level:e,message:t,timestamp:new Date,...n}}setLevel(e){this.options.level=e}getLevel(){return this.options.level}shouldLog(e){return q[e]>=q[this.options.level]}getUptime(){return Date.now()-this.startTime}formatUptime(){let e=this.getUptime(),t=Math.floor(e/1e3)%60,n=Math.floor(e/(1e3*60))%60,r=Math.floor(e/(1e3*60*60));return r>0?`${r}h ${n}m ${t}s`:n>0?`${n}m ${t}s`:`${t}s`}log(e,t,...n){if(!this.shouldLog(e))return;let r=this.formatTimestamp(),i=this.formatMessage(t,n);this.writeLog(e,i,r)}writeLog(e,t,n){let r=``;if(this.options.includeTimestamp&&n&&(r+=this.colorize(`[${n}] `,K.gray)),this.options.format!==`simple`){let t=e.toUpperCase().padEnd(5);r+=this.colorize(`${t} `,ye[e])}r+=t,(e===`error`?process.stderr:process.stdout).write(r+`
444
+ `)}formatMessage(e,t){if(t.length===0)return e;let n=e;for(let e of t)typeof e==`object`?n+=` `+JSON.stringify(e,null,2):n+=` `+String(e);return n}formatTimestamp(){let e=new Date;return this.options.format===`json`?e.toISOString():e.toLocaleTimeString(`en-US`,{hour12:!1})}colorize(e,t){return!this.options.colorize||!process.stdout.isTTY?e:t+e+K.reset}getStatusColor(e){return e>=200&&e<300?K.green:e>=300&&e<400?K.cyan:e>=400&&e<500?K.yellow:e>=500?K.red:K.white}};const J=e=>new be(e);J();const xe={startPort:8080,endPort:8180,timeout:2e3},Se=(e,t=2e3)=>new Promise(n=>{let r=o(),i=!1,a=setTimeout(()=>{i||(i=!0,r.close(),n(h(!1)))},t);r.once(`error`,e=>{i||(i=!0,clearTimeout(a),r.close(),e.code===`EADDRINUSE`?n(h(!1)):n(g(w(e))))}),r.once(`listening`,()=>{i||(i=!0,clearTimeout(a),r.close(()=>{n(h(!0))}))}),r.listen(e)}),Y=async(e={})=>{let t={...xe,...e};if(t.startPort<1||t.startPort>65535)return g(w(Error(`Invalid start port: ${t.startPort}. Must be between 1 and 65535.`)));if(t.endPort<t.startPort||t.endPort>65535)return g(w(Error(`Invalid end port: ${t.endPort}. Must be between ${t.startPort} and 65535.`)));for(let e=t.startPort;e<=t.endPort;e++){let n=await Se(e,t.timeout);if(_(n)&&n.data)return h(e)}return g(w(Error(`No available ports found in range ${t.startPort}-${t.endPort}`)))};var Ce=class{constructor(e){this.server=null,this.hotReload=null,this.logger=J(),this.isRunning=!1,this.config=e,e.reload&&(this.hotReload=new G({watchPath:e.rootPath,ignored:e.ignorePatterns||void 0,debounceMs:e.debounceMs||void 0,restartOnChange:e.restartOnChange||!1}))}async start(){if(this.isRunning)throw Error(`Server is already running`);let e=await Y({startPort:this.config.port});if(!_(e))throw Error(`Failed to find available port: ${e.error.message}`);let t=e.data;if(this.server=o((e,t)=>{this.handleRequest(e,t)}),this.hotReload){let e=this.hotReload.start();_(e)?this.logger.info(`🔄 Hot-reload enabled`):this.logger.warn(`Hot-reload failed to start:`,e.error)}return new Promise((e,n)=>{this.server.listen(t,()=>{this.isRunning=!0,this.logger.info(`🚀 Server running at http://localhost:${t}`),this.logger.info(`📁 Serving files from: ${this.config.rootPath}`),this.logger.info(`
445
+ Press Ctrl+C to stop the server
446
+ `),e({port:t,server:this.server,config:this.config,stop:()=>this.stop()})}),this.server.on(`error`,e=>{e.code===`EADDRINUSE`?n(Error(`Port ${t} is already in use`)):n(e)})})}async stop(){if(!(!this.isRunning||!this.server))return this.logger.info(`
447
+
448
+ 👋 Shutting down gracefully...`),this.hotReload&&this.hotReload.stop(),new Promise(e=>{this.server.close(()=>{this.isRunning=!1,this.logger.info(`✅ Server stopped`),e()})})}async handleRequest(e,t){let n=Date.now(),r=e.url||`/`,i=e.method||`GET`,a=500;try{if(this.logger.logRequest(i,r),r===`/__reload__`&&this.hotReload){this.hotReload.handleSSEConnection(e,t),a=200;return}if(i!==`GET`){this.sendError(t,405,`Method Not Allowed`),a=405;return}let n=ce(r,this.config.rootPath);if(!_(n)){this.logger.warn(`Security violation: ${n.error.message} - ${r}`),this.sendError(t,403,`Forbidden - Access denied`),a=403;return}let o=n.data;if(!o.isValid){this.logger.warn(`Security violation: ${o.error} - ${r}`),this.sendError(t,403,`Forbidden - Access denied`),a=403;return}let s=o.resolvedPath,c=await V(s);if(!_(c)||!c.data){this.sendError(t,404,`Not Found`),a=404;return}let l=await H(s);if(!_(l)){this.sendError(t,500,`Internal Server Error`),a=500;return}l.data.isDirectory()?await this.handleDirectoryRequest(s,r,t):await this.handleFileRequest(s,t),a=200}catch(e){let n=e;n instanceof Error?this.logger.error(`Unhandled exception during request handling:`,n.message,n.stack):this.logger.error(`Unhandled exception during request handling:`,n),t.headersSent||this.sendError(t,500,`Internal Server Error`),a=500}finally{let e=Date.now()-n;this.logger.logResponse(a,e)}}async handleDirectoryRequest(e,t,r){let i=n(e,`index.html`),a=await V(i);if(_(a)&&a.data){await this.handleFileRequest(i,r);return}if(this.config&&this.config.enableDirectoryListing===!1){this.sendError(r,403,`Directory listing disabled`);return}let o=await P(e,t);if(!_(o)){this.sendError(r,500,`Error generating directory listing`);return}let s=o.data;this.hotReload&&(s=this.hotReload.injectScript(s)),r.writeHead(200,{"Content-Type":`text/html; charset=utf-8`,"Content-Length":Buffer.byteLength(s,`utf8`).toString()}),r.end(s)}async handleFileRequest(e,n){let r=await H(e);if(!_(r)){this.sendError(n,500,`Internal Server Error`);return}let i=r.data,a=t(e).toLowerCase(),o=A(e),s=ge(e),c={"Content-Type":o,"Content-Length":i.size.toString(),"Last-Modified":i.mtime.toUTCString(),"Cache-Control":`public, max-age=0`};if(s===`buffer`&&(a===`.html`||a===`.htm`)&&this.hotReload){let t=await me(e);if(_(t)){let e=this.hotReload.injectScript(t.data);c[`Content-Length`]=Buffer.byteLength(e,`utf8`).toString(),n.writeHead(200,c),n.end(e);return}else this.logger.warn(`Failed to read HTML file for script injection, falling back to streaming`)}n.writeHead(200,c);let l=he(e);l.pipe(n),l.on(`error`,e=>{this.logger.error(`File stream error:`,e),n.headersSent||this.sendError(n,500,`Internal Server Error`)}),n.on(`error`,e=>{this.logger.error(`Response error:`,e)})}sendError(e,t,n){if(e.headersSent)return;let r=this.generateErrorPage(t,n);e.writeHead(t,{"Content-Type":`text/html; charset=utf-8`,"Content-Length":Buffer.byteLength(r,`utf8`).toString()}),e.end(r)}generateErrorPage(e,t){return`
449
+ <!DOCTYPE html>
450
+ <html lang="en">
451
+ <head>
452
+ <meta charset="utf-8">
453
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
454
+ <title>${e} ${t} - HTTPath</title>
455
+ <style>
456
+ body {
457
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
458
+ display: flex;
459
+ justify-content: center;
460
+ align-items: center;
461
+ min-height: 100vh;
462
+ margin: 0;
463
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
464
+ color: white;
465
+ }
466
+ .error-container {
467
+ text-align: center;
468
+ padding: 60px 40px;
469
+ background: rgba(255, 255, 255, 0.1);
470
+ backdrop-filter: blur(10px);
471
+ border-radius: 16px;
472
+ border: 1px solid rgba(255, 255, 255, 0.2);
473
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
474
+ max-width: 500px;
475
+ }
476
+ .error-code {
477
+ font-size: 6rem;
478
+ font-weight: 700;
479
+ margin-bottom: 20px;
480
+ text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
481
+ }
482
+ .error-title {
483
+ font-size: 2rem;
484
+ margin-bottom: 16px;
485
+ font-weight: 600;
486
+ }
487
+ .error-message {
488
+ font-size: 1.1rem;
489
+ margin-bottom: 30px;
490
+ opacity: 0.9;
491
+ line-height: 1.6;
492
+ }
493
+ .error-footer {
494
+ font-size: 0.9rem;
495
+ opacity: 0.7;
496
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
497
+ padding-top: 20px;
498
+ margin-top: 30px;
499
+ }
500
+ .back-link {
501
+ display: inline-block;
502
+ margin-top: 20px;
503
+ padding: 12px 24px;
504
+ background: rgba(255, 255, 255, 0.2);
505
+ border: 1px solid rgba(255, 255, 255, 0.3);
506
+ border-radius: 8px;
507
+ color: white;
508
+ text-decoration: none;
509
+ font-weight: 500;
510
+ transition: all 0.3s ease;
511
+ }
512
+ .back-link:hover {
513
+ background: rgba(255, 255, 255, 0.3);
514
+ transform: translateY(-2px);
515
+ }
516
+ </style>
517
+ </head>
518
+ <body>
519
+ <div class="error-container">
520
+ <div class="error-code">${e}</div>
521
+ <h1 class="error-title">${t}</h1>
522
+ <p class="error-message">${{403:`You do not have permission to access this resource.`,404:`The requested file or directory was not found.`,405:`The requested method is not allowed for this resource.`,500:`An internal server error occurred.`}[e]||`An error occurred.`}</p>
523
+ <a href="/" class="back-link">← Go Home</a>
524
+ <div class="error-footer">
525
+ HTTPath Server
526
+ </div>
527
+ </div>
528
+ </body>
529
+ </html>`}};const X=e=>new Ce(e),Z=J({level:`info`,format:`simple`,colorize:!0});let Q=null;const $=async()=>{we();let e=O();_(e)||(Z.error(`❌ Failed to parse CLI arguments:`,e.error.message),process.exit(1));let t=e.data;k(t)||process.exit(1);try{Q=await X(t).start(),Te()}catch(e){Z.error(`❌ Failed to start server:`,e instanceof Error?e.message:String(e)),process.exit(1)}},we=()=>{console.log(`
530
+ 🚀 ${D.name} v${D.version}
531
+ ${D.description}
532
+ `)},Te=()=>{for(let e of[`SIGINT`,`SIGTERM`])process.on(e,async()=>{if(Z.info(`\n\n👋 Received ${e}, shutting down gracefully...`),Q)try{await Q.stop(),Z.info(`✅ Server stopped successfully`),process.exit(0)}catch(e){Z.error(`❌ Error stopping server:`,e),process.exit(1)}else process.exit(0)});process.on(`uncaughtException`,e=>{Z.error(`💥 Uncaught Exception:`,e),Q?Q.stop().finally(()=>process.exit(1)):process.exit(1)}),process.on(`unhandledRejection`,(e,t)=>{Z.error(`💥 Unhandled Promise Rejection:`,e),Z.debug(`Promise:`,t),Q?Q.stop().finally(()=>process.exit(1)):process.exit(1)})};process.argv[1]&&(process.argv[1].endsWith(`/index.mjs`)||process.argv[1].endsWith(`\\index.mjs`)||process.argv[1].includes(`dist`))&&$().catch(e=>{Z.error(`💥 Application crashed:`,e),process.exit(1)});export{D as VERSION_INFO,X as createHTTPServer,_e as createHotReloadService,J as createLogger,Y as findAvailablePort,$ as main,O as parseCliArgs,k as validateConfig};
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "httpath",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, feature-rich static file server similar to Python's `python -m http.server` but with modern Node.js features and more.",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.mts",
7
+ "main": "./dist/index.umd.js",
8
+ "bin": {
9
+ "httpath": "./dist/index.mjs"
10
+ },
11
+ "exports": {
12
+ "imports": {
13
+ "types": "./dist/index.d.mts",
14
+ "default": "./dist/index.mjs"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "type": "module",
22
+ "author": {
23
+ "name": "José Martínez Santana"
24
+ },
25
+ "repository": {
26
+ "type": "github",
27
+ "url": "https://github.com/MetalbolicX/httpath.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/MetalbolicX/httpath/issues"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "license": "MIT",
36
+ "devDependencies": {
37
+ "@types/node": "^25.1.0",
38
+ "tsdown": "^0.20.1",
39
+ "typescript": "^5.9.3"
40
+ },
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "start": "node ./dist/index.mjs",
44
+ "dev": "node ./dist/index.mjs --reload",
45
+ "serve": "node ./dist/index.mjs --port 3000 --path .",
46
+ "test": "node test-server.mjs"
47
+ }
48
+ }