httpath 1.2.0 → 1.3.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/dist/index.cjs +238 -83
  2. package/dist/index.mjs +237 -82
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- let e=require(`node:util`),t=require(`node:path`),n=require(`node:http`),r=require(`node:fs`),i=require(`node:fs/promises`),a=require(`node:url`),o=require(`node:child_process`);const s=(0,t.dirname)((0,a.fileURLToPath)(require(`url`).pathToFileURL(__filename).href)),c={html:`text/html`,css:`text/css`,js:`text/javascript`,mjs:`text/javascript`,json:`application/json`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,svg:`image/svg+xml`,ico:`image/x-icon`,txt:`text/plain`,md:`text/markdown`,ts:`video/mp2t`},l={directory:s,port:8080,ignorePatterns:[`.git`,`node_modules`,`.DS_Store`],enableDirectoryListing:!0,logLevel:`info`,enableLiveReload:!0,restartOnChange:!1};let u=null,d=!1;const f=new Set,p=n=>{let{values:r}=(0,e.parseArgs)({args:n,options:{dir:{type:`string`,short:`d`,default:l.directory},port:{type:`string`,short:`p`,default:l.port.toString()},ignore:{type:`string`,short:`i`,default:l.ignorePatterns.join(`,`)},"no-listing":{type:`boolean`,default:!1},"no-live-reload":{type:`boolean`,default:!1},"restart-on-change":{type:`boolean`,short:`r`,default:!1},log:{type:`string`,default:l.logLevel},help:{type:`boolean`,short:`h`,default:!1}},allowPositionals:!0});r.help&&(console.log(`
2
+ let e=require(`node:util`),t=require(`node:path`),n=require(`node:http`),r=require(`node:fs`),i=require(`node:fs/promises`),a=require(`node:url`),o=require(`node:child_process`);const s=(0,t.dirname)((0,a.fileURLToPath)(require(`url`).pathToFileURL(__filename).href)),c={html:`text/html`,css:`text/css`,js:`text/javascript`,mjs:`text/javascript`,json:`application/json`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,svg:`image/svg+xml`,ico:`image/x-icon`,txt:`text/plain`,md:`text/markdown`,ts:`video/mp2t`},l={directory:s,port:8080,ignorePatterns:[`.git`,`node_modules`,`.DS_Store`],enableDirectoryListing:!0,logLevel:`info`,enableLiveReload:!0,restartOnChange:!1};let u=null,d=!1;const f=new Set,p=n=>{let{values:r}=(0,e.parseArgs)({args:n,options:{dir:{type:`string`,short:`d`,default:l.directory},port:{type:`string`,short:`p`,default:l.port.toString()},ignore:{type:`string`,short:`i`,default:l.ignorePatterns.join(`,`)},"no-listing":{type:`boolean`,default:!1},"no-live-reload":{type:`boolean`,default:!1},"restart-on-change":{type:`boolean`,default:!1},log:{type:`string`,default:l.logLevel},help:{type:`boolean`,short:`h`,default:!1}},allowPositionals:!0});r.help&&(console.log(`
3
3
  Static File Server with Auto-Reload (Node.js)
4
4
 
5
5
  Usage: httpreload [OPTIONS]
@@ -10,7 +10,7 @@ Options:
10
10
  -i, --ignore <patterns> Comma-separated patterns to ignore
11
11
  --no-listing Disable directory listing
12
12
  --no-live-reload Disable live reload feature
13
- -r --restart-on-change Restart server process on file changes
13
+ --restart-on-change Restart server process on file changes
14
14
  --log <level> Log level: info, debug, error
15
15
  -h, --help Show this help message
16
16
  `),process.exit(0));let i=parseInt(r.port||`8080`,10);if(isNaN(i)||i<1||i>65535)throw Error(`Port must be a valid number between 1 and 65535`);return{directory:(0,t.resolve)(r.dir||`.`),port:i,ignorePatterns:(r.ignore||``).split(`,`).map(e=>e.trim()).filter(Boolean),enableDirectoryListing:!r[`no-listing`],logLevel:r.log||l.logLevel,enableLiveReload:!r[`no-live-reload`],restartOnChange:!!r[`restart-on-change`]}},m=(e,t=`info`)=>{let n=new Date().toISOString(),r=t.toUpperCase().padEnd(5);console.log(`[${n}] ${r} ${e}`)},h=(e,t)=>t.some(t=>e.includes(t)||e.endsWith(t)),g=e=>new Promise(t=>{u&&clearTimeout(u),u=setTimeout(()=>{u=null,t()},e)}),_=e=>c[(0,t.extname)(e).slice(1).toLowerCase()]||`application/octet-stream`,v=e=>`
@@ -43,95 +43,250 @@ Options:
43
43
 
44
44
  connect();
45
45
  })();
46
- <\/script>`,y=(e,t)=>{let n=v(t);return e.includes(`</body>`)?e.replace(`</body>`,`${n}\n</body>`):e.includes(`</html>`)?e.replace(`</html>`,`${n}\n</html>`):e+n},b=async(e,t)=>{t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.write(`data: connected
46
+ <\/script>`,y=(e,t)=>{let n=v(t);return e.includes(`</body>`)?e.replace(`</body>`,`${n}\n</body>`):e.includes(`</html>`)?e.replace(`</html>`,`${n}\n</html>`):e+n},b=(e,t)=>{t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.write(`data: connected
47
47
 
48
- `),f.add(t),e.on(`close`,()=>{f.delete(t)})},x=async(e=`change`)=>{if(f.size!==0){m(`Reloading ${f.size} clients (${e})`,`debug`);for(let e of f)try{e.write(`data: reload
48
+ `),f.add(t),e.on(`close`,()=>{f.delete(t)})},x=(e=`change`)=>{if(f.size!==0){m(`Reloading ${f.size} clients (${e})`,`debug`);for(let e of f)try{e.write(`data: reload
49
49
 
50
50
  `)}catch{}}},S=()=>`
51
- :root{
52
- --bg: #f6f8fa;
53
- --card: #ffffff;
51
+ :root { --bg-page: #f2f2f2; --bg-article: #bbc3db; --color-title: #333; --color-paragraph: #333; --link-color: #1a0dab; --link-hover-color: #d93025; --toggle-color: #0f172b; --fill-icons: white; }
52
+ :root:has(#dark:checked) { --bg-page: #333; --bg-article: #444; --color-title: #eee; --color-paragraph: #ddd; --link-color: #bb86fc; --link-hover-color: #ff79c6; }
53
+ body { font-family: monospace; font-size: 1.3em; margin: 0.5em; padding: 1em; background-color: var(--bg-page); color: var(--color-paragraph); &:has(#dark:checked) { background-color: var(--bg-article); color: var(--color-title); } }
54
+ h1 { font-size: 2em; margin-bottom: 0.5em; }
55
+ a { text-decoration: none; color: var(--link-color); &:hover { text-decoration: underline; color: var(--link-hover-color); } }
56
+ .toggle { --width: 3em; --height: calc(var(--width) / 2); --border-radius: calc(var(--height) / 2); display: inline-block; cursor: pointer; .toggle__input { display: none; &:checked + .toggle__fill { background: #009578; } &:checked + .toggle__fill::after { transform: translateX(var(--height)); } } .toggle__fill { position: relative; width: var(--width); height: var(--height); border-radius: var(--border-radius); background-color: var(--toggle-color); transition: background-color 0.3s ease-in-out; &::after { content: ""; position: absolute; top: 0; left: 0; width: var(--height); height: var(--height); border-radius: var(--border-radius); background-color: var(--fill-icons); box-shadow: 0 0 0.2em rgba(0, 0, 0, 0.2); transition: transform 0.3s ease-in-out; } } }
57
+ :root {
58
+ --bg-page: #f6f8fb;
59
+ --bg-panel: #ffffff;
54
60
  --muted: #6b7280;
55
- --accent: #2563eb;
56
- --text: #0f172a;
57
- --elev: 0 6px 18px rgba(15,23,42,0.06);
58
- --btn-border: rgba(15,23,42,0.06);
59
- }
60
- /* Respect user's system preference */
61
- @media (prefers-color-scheme: dark){
62
- :root{
63
- --bg: #0b1220;
64
- --card: #0f1724;
65
- --muted: #9ca3af;
66
- --accent: #60a5fa;
67
- --text: #e6eef8;
68
- --elev: 0 6px 22px rgba(2,6,23,0.6);
69
- --btn-border: rgba(255,255,255,0.08);
70
- }
71
- }
72
- /* Toggle using hidden checkbox (works without :has()) */
73
- #dark-toggle:checked ~ .container{
74
- --bg: #0b1220;
75
- --card: #0f1724;
61
+ --title: #0f172a;
62
+ --link-color: #2563eb;
63
+ --link-hover-color: #1e40af;
64
+ --accent: #10b981;
65
+ --card-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
66
+ --radius: 10px;
67
+ --gap: 12px;
68
+ --toggle-track: #e6e9ee;
69
+ --toggle-knob: #ffffff;
70
+ }
71
+
72
+ :root:has(#dark:checked) {
73
+ --bg-page: #0b1220;
74
+ --bg-panel: #0f1724;
76
75
  --muted: #9ca3af;
77
- --accent: #60a5fa;
78
- --text: #e6eef8;
79
- --elev: 0 6px 22px rgba(2,6,23,0.6);
80
- }
81
-
82
- .box-sizing{box-sizing:border-box}
83
- *{box-sizing:border-box}
84
- html{color-scheme: light dark; background-color:var(--bg)}
85
- body{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial,sans-serif;font-size:14px;background:var(--bg);color:var(--text)}
86
- .container{max-width:980px;margin:28px auto;padding:20px}
87
- .card{background:var(--card);border-radius:10px;padding:18px;box-shadow:var(--elev)}
88
- .header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:12px}
89
- .title{font-size:18px;font-weight:600;color:var(--text);margin:0}
90
- .path{font-size:12px;color:var(--muted)}
91
- .controls{display:flex;align-items:center;gap:8px}
92
- .toggle-btn{background:transparent;border:1px solid var(--btn-border);padding:6px 8px;border-radius:8px;cursor:pointer;font-size:14px;color:var(--text);backdrop-filter: blur(4px);transition:all 180ms ease}
93
- .toggle-btn:hover{opacity:0.95}
94
-
95
- /* Visible active state when dark mode is enabled */
96
- #dark-toggle:checked ~ .container .toggle-btn{background:var(--accent);color:#fff;border-color:transparent;transform:translateY(-1px)}
97
- .file-list{list-style:none;padding:0;margin:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
98
- .file{display:flex;align-items:center;padding:10px;border-radius:8px;border:1px solid rgba(15,23,42,0.04);background:linear-gradient(180deg,rgba(0,0,0,0.01),transparent)}
99
- .file a{display:flex;align-items:center;gap:12px;width:100%;color:inherit;text-decoration:none}
100
- .icon{width:44px;height:44px;border-radius:8px;display:flex;align-items:center;justify-content:center;background:rgba(37,99,235,0.08);font-size:20px}
101
- .name{font-weight:500}
102
- .meta{margin-left:auto;font-size:12px;color:var(--muted)}
103
- .parent{grid-column:1/-1;padding:0 4px}
104
- .empty{padding:18px;text-align:center;color:var(--muted)}
105
- @media (max-width:600px){.file-list{grid-template-columns:1fr}}
106
- `,C=(e,n)=>{let r=n===`/`?null:{name:`..`,url:(0,t.join)(n,`../`).replace(/\\/g,`/`),isDirectory:!0},i=e.slice().sort((e,t)=>e.isDirectory===t.isDirectory?e.name.toLowerCase().localeCompare(t.name.toLowerCase()):e.isDirectory?-1:1).map(e=>{let t=e.isDirectory?`📁`:`📄`;return`<li class="file"><a href="${e.url}"><span class="icon">${t}</span><span class="name">${e.name}</span><span class="meta">${e.isDirectory?`Dir`:`File`}</span></a></li>`}).join(`
107
- `),a=r?`<li class="file parent"><a href="../"><span class="icon">⬆️</span><span class="name">..</span><span class="meta">Parent</span></a></li>`:``,o=i||`<div class="empty">This folder is empty</div>`;return`
108
- <!doctype html>
109
- <html lang="en">
76
+ --title: #e6eef8;
77
+ --link-color: #7c9cff;
78
+ --link-hover-color: #9fb7ff;
79
+ --accent: #34d399;
80
+ --card-shadow: 0 6px 18px rgba(2,6,23,0.6);
81
+ --toggle-track: #1f2937;
82
+ --toggle-knob: #0b1220;
83
+ }
84
+
85
+ * { box-sizing: border-box; }
86
+
87
+ body {
88
+ margin: 0;
89
+ padding: 2rem;
90
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif;
91
+ background-color: var(--bg-page);
92
+ color: var(--title);
93
+ -webkit-font-smoothing: antialiased;
94
+ -moz-osx-font-smoothing: grayscale;
95
+ line-height: 1.4;
96
+ display: flex;
97
+ justify-content: center;
98
+ align-items: flex-start;
99
+ min-height: 100vh;
100
+ }
101
+
102
+ .container {
103
+ width: 100%;
104
+ max-width: 980px;
105
+ background: linear-gradient(180deg, rgba(255,255,255,0.6), rgba(255,255,255,0.4));
106
+ background-color: var(--bg-panel);
107
+ border-radius: var(--radius);
108
+ box-shadow: var(--card-shadow);
109
+ padding: 1.25rem;
110
+ gap: var(--gap);
111
+ }
112
+
113
+ /* Header */
114
+ .header {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: space-between;
118
+ gap: 1rem;
119
+ margin-bottom: 0.75rem;
120
+ }
121
+ .header .title {
122
+ font-size: 1.6rem;
123
+ font-weight: 600;
124
+ }
125
+ .header .subtitle {
126
+ color: var(--muted);
127
+ font-size: 0.95rem;
128
+ }
129
+
130
+ /* Dark toggle */
131
+ .toggle {
132
+ --width: 48px;
133
+ --height: 24px;
134
+ display: inline-flex;
135
+ align-items: center;
136
+ cursor: pointer;
137
+ }
138
+ .toggle__input {
139
+ position: absolute;
140
+ opacity: 0;
141
+ pointer-events: none;
142
+ }
143
+ .toggle__fill {
144
+ width: var(--width);
145
+ height: var(--height);
146
+ background: var(--toggle-track);
147
+ border-radius: 999px;
148
+ position: relative;
149
+ transition: background 200ms ease;
150
+ }
151
+ .toggle__fill::after {
152
+ content: "";
153
+ position: absolute;
154
+ top: 3px;
155
+ left: 3px;
156
+ width: calc(var(--height) - 6px);
157
+ height: calc(var(--height) - 6px);
158
+ border-radius: 50%;
159
+ background: var(--toggle-knob);
160
+ box-shadow: 0 2px 6px rgba(2,6,23,0.08);
161
+ transition: transform 200ms ease;
162
+ }
163
+ .toggle__input:checked + .toggle__fill {
164
+ background: linear-gradient(90deg, rgba(16,185,129,0.9), rgba(34,197,94,0.9));
165
+ }
166
+ .toggle__input:checked + .toggle__fill::after {
167
+ transform: translateX(calc(var(--width) - var(--height)));
168
+ }
169
+
170
+ /* Listing - improved file/folder presentation */
171
+ .listing {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 0.5rem;
175
+ margin-top: 0.5rem;
176
+ width: 100%;
177
+ }
178
+
179
+ /* Each item becomes a clear row with icon, name, and meta */
180
+ .listing a.item {
181
+ display: grid;
182
+ grid-template-columns: 44px 1fr auto;
183
+ align-items: center;
184
+ gap: 0.75rem;
185
+ padding: 0.75rem 1rem;
186
+ border-radius: 10px;
187
+ color: var(--title);
188
+ text-decoration: none;
189
+ background: transparent;
190
+ transition: background 180ms ease, transform 150ms ease, box-shadow 180ms ease;
191
+ font-size: 1rem;
192
+ overflow: hidden;
193
+ border: 1px solid transparent;
194
+ }
195
+
196
+ /* Hover and focus styles */
197
+ .listing a.item:hover,
198
+ .listing a.item:focus {
199
+ background: color-mix(in srgb, var(--link-hover-color) 6%, transparent);
200
+ transform: translateY(-4px);
201
+ box-shadow: 0 10px 24px color-mix(in srgb, var(--title) 6%, transparent);
202
+ color: var(--link-hover-color);
203
+ outline: none;
204
+ border-color: color-mix(in srgb, var(--link-hover-color) 12%, transparent);
205
+ }
206
+
207
+ /* Icon badge */
208
+ .listing a.item .icon {
209
+ display: inline-grid;
210
+ place-items: center;
211
+ width: 44px;
212
+ height: 44px;
213
+ border-radius: 10px;
214
+ font-size: 1.1rem;
215
+ flex: 0 0 auto;
216
+ background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(0,0,0,0.02));
217
+ box-shadow: 0 2px 8px rgba(2,6,23,0.04);
218
+ color: var(--fill-icons, #fff);
219
+ }
220
+
221
+ /* Different tones for directories vs files - still respecting variables */
222
+ .listing a.item.dir .icon {
223
+ background: linear-gradient(180deg, rgba(124,58,237,0.12), rgba(124,58,237,0.06));
224
+ color: white;
225
+ }
226
+ .listing a.item.file .icon {
227
+ background: linear-gradient(180deg, rgba(37,99,235,0.08), rgba(37,99,235,0.03));
228
+ color: white;
229
+ }
230
+
231
+ /* Name column */
232
+ .listing a.item .name {
233
+ font-weight: 600;
234
+ color: var(--title);
235
+ min-width: 0; /* allow ellipsis */
236
+ overflow: hidden;
237
+ text-overflow: ellipsis;
238
+ white-space: nowrap;
239
+ }
240
+
241
+ /* Meta column (size/type/date or just type for now) */
242
+ .listing a.item .meta {
243
+ color: var(--muted);
244
+ font-size: 0.82rem;
245
+ margin-left: 0.6rem;
246
+ text-align: right;
247
+ white-space: nowrap;
248
+ flex-shrink: 0;
249
+ background: color-mix(in srgb, var(--muted) 8%, transparent);
250
+ padding: 4px 8px;
251
+ border-radius: 999px;
252
+ }
253
+
254
+ /* Parent link styling - present as subtle pill */
255
+ .parent {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 0.5rem;
259
+ padding: 0.35rem 0.6rem;
260
+ border-radius: 8px;
261
+ margin-bottom: 0.35rem;
262
+ color: var(--muted);
263
+ font-size: 0.95rem;
264
+ background: transparent;
265
+ }
266
+ .parent a { color: inherit; text-decoration: none; }
267
+
268
+ /* Small screens adjustments */
269
+ @media (max-width: 640px) {
270
+ .container { padding: 0.8rem; margin: 1rem; }
271
+ .header .title { font-size: 1.25rem; }
272
+ .listing a.item { padding: 0.5rem; font-size: 0.95rem; grid-template-columns: 36px 1fr auto; }
273
+ .listing a.item .icon { width: 36px; height: 36px; border-radius: 8px; }
274
+ }
275
+ `,C=(e,t)=>{let n=t===`/`?``:`<div class="parent"><a href="../">../</a></div>`,r=e.slice().sort((e,t)=>e.isDirectory===t.isDirectory?0:e.isDirectory?-1:1).sort((e,t)=>e.isDirectory===t.isDirectory?e.name.localeCompare(t.name):0).map(e=>{let t=e.isDirectory?`📁`:`📄`,n=e.url,r=e.isDirectory?`dir`:`file`,i=e.isDirectory?`Directory`:e.name.includes(`.`)?e.name.split(`.`).pop().toUpperCase():`File`;return`<a class="item ${r}" href="${n}"><span class="icon">${t}</span><span class="name">${e.name}</span><span class="meta">${i}</span></a>`}).join(`
276
+ `);return`
277
+ <!DOCTYPE html>
278
+ <html>
110
279
  <head>
111
- <meta charset="utf-8" />
112
- <meta name="viewport" content="width=device-width,initial-scale=1" />
113
- <title>Listing of ${n}</title>
280
+ <meta charset="utf-8">
281
+ <title>Listing of ${t}</title>
114
282
  <style>${S()}</style>
115
- <style>/* small reset for injected scripts */ body > script{display:none}</style>
116
283
  </head>
117
284
  <body>
118
- <input type="checkbox" id="dark-toggle" aria-hidden style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;border:0;padding:0;margin:0" />
119
- <div class="container">
120
- <div class="card">
121
- <div class="header">
122
- <div>
123
- <h1 class="title">Listing of ${n}</h1>
124
- <div class="path">${n}</div>
125
- </div>
126
- <div class="controls">
127
- <label for="dark-toggle" class="toggle-btn" title="Toggle dark mode">🌓</label>
128
- </div>
129
- </div>
130
- <ul class="file-list">
131
- ${a}
132
- ${o}
133
- </ul>
134
- </div>
285
+ <label class="toggle" for="dark"><input type="checkbox" id="dark" class="toggle__input" checked><span class="toggle__fill"></span></label>
286
+ <h1>Listing of ${t}</h1>
287
+ <div class="listing">
288
+ ${n}
289
+ ${r}
135
290
  </div>
136
291
  </body>
137
- </html>`},w=async(e,t,n)=>{let a=_(e);if(n.enableLiveReload&&a.includes(`text/html`))try{let r=await(0,i.open)(e);try{let e=y(await r.readFile({encoding:`utf-8`}),n.port);t.writeHead(200,{"Content-Type":a}),t.end(e);return}finally{await r.close()}}catch{}t.writeHead(200,{"Content-Type":a});let o=(0,r.createReadStream)(e);o.pipe(t),o.on(`error`,e=>{t.writeHead(500),t.end(e.message)})},T=async(e,n,r,a)=>{try{let o=C((await(0,i.readdir)(e,{withFileTypes:!0})).filter(e=>!h(e.name,a.ignorePatterns)).map(e=>({name:e.name,isDirectory:e.isDirectory(),url:(0,t.join)(n,e.name).replace(/\\/g,`/`)})),n);a.enableLiveReload&&(o=y(o,a.port)),r.writeHead(200,{"Content-Type":`text/html`}),r.end(o)}catch{r.writeHead(500),r.end(`Error reading directory`)}},E=e=>async(n,r)=>{let a=n.headers.host||`localhost:${e.port}`,o=new URL(n.url||`/`,`http://${a}`),s=decodeURIComponent(o.pathname);if(e.enableLiveReload&&s===`/livereload`)return b(n,r);let c=(0,t.resolve)(e.directory,`.${s}`);if(!c.startsWith(e.directory))return r.writeHead(403),r.end(`Forbidden`);try{let n=await(0,i.stat)(c);if(n.isFile())return await w(c,r,e);if(n.isDirectory()){if(e.enableDirectoryListing)return await T(c,s,r,e);{let n=(0,t.join)(c,`index.html`);try{return await(0,i.stat)(n),await w(n,r,e)}catch{return r.writeHead(403),r.end(`Listing disabled`)}}}else return r.writeHead(404),r.end(`Not Found`)}catch(e){if(e&&e.code===`ENOENT`)return r.writeHead(404),r.end(`Not Found`);r.writeHead(500),r.end(e?.message||`Server error`)}},D=()=>{m(`Reloading server process...`),(0,o.spawn)(process.argv[0],process.argv.slice(1),{stdio:`inherit`,detached:!0}).unref(),process.exit(0)},O=async e=>{m(`Watching: ${e.directory}`);try{(0,r.watch)(e.directory,{recursive:!0},async(t,n)=>{if(!n||h(n,e.ignorePatterns)||d)return;d=!0,m(`Change detected: ${n} (${t})`),await g(500);let r=/\.(json|js|mjs|ts)$/.test(n),i=/\.(html|css|png|jpg|jpeg|gif|svg)$/.test(n);e.restartOnChange||r?(e.enableLiveReload&&x(`restart`),D()):e.enableLiveReload&&i&&x(`asset change`),setTimeout(()=>{d=!1},1e3)})}catch(e){m(`Watcher error: ${e?.message}`,`error`)}};(()=>{try{let e=p(process.argv.slice(2));(0,n.createServer)(E(e)).listen(e.port,()=>{m(`Server running at http://localhost:${e.port}`),m(`Root: ${e.directory}`),O(e)}),[`SIGINT`,`SIGTERM`].forEach(e=>{process.on(e,()=>{m(`Shutting down...`),process.exit(0)})})}catch(e){m(`Startup Error: ${e?.message}`,`error`),process.exit(1)}})();
292
+ </html>`},w=async(e,t,n)=>{let a=_(e);if(n.enableLiveReload&&a.includes(`text/html`))try{let r=await(0,i.open)(e);try{let e=y(await r.readFile({encoding:`utf-8`}),n.port);t.writeHead(200,{"Content-Type":a}),t.end(e);return}finally{await r.close()}}catch{}t.writeHead(200,{"Content-Type":a});let o=(0,r.createReadStream)(e);o.pipe(t),o.on(`error`,e=>{t.writeHead(500),t.end(e.message)})},T=async(e,n,r,a)=>{try{let o=C((await(0,i.readdir)(e,{withFileTypes:!0})).filter(e=>!h(e.name,a.ignorePatterns)).map(e=>({name:e.name,isDirectory:e.isDirectory(),url:(0,t.join)(n,e.name).replace(/\\/g,`/`)})),n);a.enableLiveReload&&(o=y(o,a.port)),r.writeHead(200,{"Content-Type":`text/html`}),r.end(o)}catch{r.writeHead(500),r.end(`Error reading directory`)}},E=e=>async(n,r)=>{let a=n.headers.host||`localhost:${e.port}`,o=new URL(n.url||`/`,`http://${a}`),s=decodeURIComponent(o.pathname);if(e.enableLiveReload&&s===`/livereload`)return b(n,r);let c=(0,t.resolve)(e.directory,`.${s}`);if(!c.startsWith(e.directory))return r.writeHead(403),r.end(`Forbidden`);try{let n=await(0,i.stat)(c);if(n.isFile())return await w(c,r,e);if(n.isDirectory()){if(e.enableDirectoryListing)return await T(c,s,r,e);{let n=(0,t.join)(c,`index.html`);try{return await(0,i.stat)(n),await w(n,r,e)}catch{return r.writeHead(403),r.end(`Listing disabled`)}}}else return r.writeHead(404),r.end(`Not Found`)}catch(e){if(e&&e.code===`ENOENT`)return r.writeHead(404),r.end(`Not Found`);r.writeHead(500),r.end(e?.message||`Server error`)}},D=()=>{m(`Reloading server process...`),(0,o.spawn)(process.argv[0],process.argv.slice(1),{stdio:`inherit`,detached:!0}).unref(),process.exit(0)},O=e=>{m(`Watching: ${e.directory}`);try{(0,r.watch)(e.directory,{recursive:!0},async(t,n)=>{if(!n||h(n,e.ignorePatterns)||d)return;d=!0,m(`Change detected: ${n} (${t})`),await g(500);let r=/\.(json|js|mjs|ts)$/.test(n),i=/\.(html|css|png|jpg|jpeg|gif|svg)$/.test(n);e.restartOnChange||r?(e.enableLiveReload&&x(`restart`),D()):e.enableLiveReload&&i&&x(`asset change`),setTimeout(()=>{d=!1},1e3)})}catch(e){m(`Watcher error: ${e?.message}`,`error`)}};(()=>{try{let e=p(process.argv.slice(2));(0,n.createServer)(E(e)).listen(e.port,()=>{m(`Server running at http://localhost:${e.port}`),m(`Root: ${e.directory}`),O(e)}),[`SIGINT`,`SIGTERM`].forEach(e=>{process.on(e,()=>{m(`Shutting down...`),process.exit(0)})})}catch(e){m(`Startup Error: ${e?.message}`,`error`),process.exit(1)}})();
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{parseArgs as e}from"node:util";import{dirname as t,extname as n,join as r,resolve as i}from"node:path";import{createServer as a}from"node:http";import{createReadStream as o,watch as s}from"node:fs";import{open as c,readdir as l,stat as u}from"node:fs/promises";import{fileURLToPath as d}from"node:url";import{spawn as f}from"node:child_process";const p=t(d(import.meta.url)),m={html:`text/html`,css:`text/css`,js:`text/javascript`,mjs:`text/javascript`,json:`application/json`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,svg:`image/svg+xml`,ico:`image/x-icon`,txt:`text/plain`,md:`text/markdown`,ts:`video/mp2t`},h={directory:p,port:8080,ignorePatterns:[`.git`,`node_modules`,`.DS_Store`],enableDirectoryListing:!0,logLevel:`info`,enableLiveReload:!0,restartOnChange:!1};let g=null,_=!1;const v=new Set,y=t=>{let{values:n}=e({args:t,options:{dir:{type:`string`,short:`d`,default:h.directory},port:{type:`string`,short:`p`,default:h.port.toString()},ignore:{type:`string`,short:`i`,default:h.ignorePatterns.join(`,`)},"no-listing":{type:`boolean`,default:!1},"no-live-reload":{type:`boolean`,default:!1},"restart-on-change":{type:`boolean`,short:`r`,default:!1},log:{type:`string`,default:h.logLevel},help:{type:`boolean`,short:`h`,default:!1}},allowPositionals:!0});n.help&&(console.log(`
2
+ import{parseArgs as e}from"node:util";import{dirname as t,extname as n,join as r,resolve as i}from"node:path";import{createServer as a}from"node:http";import{createReadStream as o,watch as s}from"node:fs";import{open as c,readdir as l,stat as u}from"node:fs/promises";import{fileURLToPath as d}from"node:url";import{spawn as f}from"node:child_process";const p=t(d(import.meta.url)),m={html:`text/html`,css:`text/css`,js:`text/javascript`,mjs:`text/javascript`,json:`application/json`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,svg:`image/svg+xml`,ico:`image/x-icon`,txt:`text/plain`,md:`text/markdown`,ts:`video/mp2t`},h={directory:p,port:8080,ignorePatterns:[`.git`,`node_modules`,`.DS_Store`],enableDirectoryListing:!0,logLevel:`info`,enableLiveReload:!0,restartOnChange:!1};let g=null,_=!1;const v=new Set,y=t=>{let{values:n}=e({args:t,options:{dir:{type:`string`,short:`d`,default:h.directory},port:{type:`string`,short:`p`,default:h.port.toString()},ignore:{type:`string`,short:`i`,default:h.ignorePatterns.join(`,`)},"no-listing":{type:`boolean`,default:!1},"no-live-reload":{type:`boolean`,default:!1},"restart-on-change":{type:`boolean`,default:!1},log:{type:`string`,default:h.logLevel},help:{type:`boolean`,short:`h`,default:!1}},allowPositionals:!0});n.help&&(console.log(`
3
3
  Static File Server with Auto-Reload (Node.js)
4
4
 
5
5
  Usage: httpreload [OPTIONS]
@@ -10,7 +10,7 @@ Options:
10
10
  -i, --ignore <patterns> Comma-separated patterns to ignore
11
11
  --no-listing Disable directory listing
12
12
  --no-live-reload Disable live reload feature
13
- -r --restart-on-change Restart server process on file changes
13
+ --restart-on-change Restart server process on file changes
14
14
  --log <level> Log level: info, debug, error
15
15
  -h, --help Show this help message
16
16
  `),process.exit(0));let r=parseInt(n.port||`8080`,10);if(isNaN(r)||r<1||r>65535)throw Error(`Port must be a valid number between 1 and 65535`);return{directory:i(n.dir||`.`),port:r,ignorePatterns:(n.ignore||``).split(`,`).map(e=>e.trim()).filter(Boolean),enableDirectoryListing:!n[`no-listing`],logLevel:n.log||h.logLevel,enableLiveReload:!n[`no-live-reload`],restartOnChange:!!n[`restart-on-change`]}},b=(e,t=`info`)=>{let n=new Date().toISOString(),r=t.toUpperCase().padEnd(5);console.log(`[${n}] ${r} ${e}`)},x=(e,t)=>t.some(t=>e.includes(t)||e.endsWith(t)),S=e=>new Promise(t=>{g&&clearTimeout(g),g=setTimeout(()=>{g=null,t()},e)}),C=e=>m[n(e).slice(1).toLowerCase()]||`application/octet-stream`,w=e=>`
@@ -43,95 +43,250 @@ Options:
43
43
 
44
44
  connect();
45
45
  })();
46
- <\/script>`,T=(e,t)=>{let n=w(t);return e.includes(`</body>`)?e.replace(`</body>`,`${n}\n</body>`):e.includes(`</html>`)?e.replace(`</html>`,`${n}\n</html>`):e+n},E=async(e,t)=>{t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.write(`data: connected
46
+ <\/script>`,T=(e,t)=>{let n=w(t);return e.includes(`</body>`)?e.replace(`</body>`,`${n}\n</body>`):e.includes(`</html>`)?e.replace(`</html>`,`${n}\n</html>`):e+n},E=(e,t)=>{t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.write(`data: connected
47
47
 
48
- `),v.add(t),e.on(`close`,()=>{v.delete(t)})},D=async(e=`change`)=>{if(v.size!==0){b(`Reloading ${v.size} clients (${e})`,`debug`);for(let e of v)try{e.write(`data: reload
48
+ `),v.add(t),e.on(`close`,()=>{v.delete(t)})},D=(e=`change`)=>{if(v.size!==0){b(`Reloading ${v.size} clients (${e})`,`debug`);for(let e of v)try{e.write(`data: reload
49
49
 
50
50
  `)}catch{}}},O=()=>`
51
- :root{
52
- --bg: #f6f8fa;
53
- --card: #ffffff;
51
+ :root { --bg-page: #f2f2f2; --bg-article: #bbc3db; --color-title: #333; --color-paragraph: #333; --link-color: #1a0dab; --link-hover-color: #d93025; --toggle-color: #0f172b; --fill-icons: white; }
52
+ :root:has(#dark:checked) { --bg-page: #333; --bg-article: #444; --color-title: #eee; --color-paragraph: #ddd; --link-color: #bb86fc; --link-hover-color: #ff79c6; }
53
+ body { font-family: monospace; font-size: 1.3em; margin: 0.5em; padding: 1em; background-color: var(--bg-page); color: var(--color-paragraph); &:has(#dark:checked) { background-color: var(--bg-article); color: var(--color-title); } }
54
+ h1 { font-size: 2em; margin-bottom: 0.5em; }
55
+ a { text-decoration: none; color: var(--link-color); &:hover { text-decoration: underline; color: var(--link-hover-color); } }
56
+ .toggle { --width: 3em; --height: calc(var(--width) / 2); --border-radius: calc(var(--height) / 2); display: inline-block; cursor: pointer; .toggle__input { display: none; &:checked + .toggle__fill { background: #009578; } &:checked + .toggle__fill::after { transform: translateX(var(--height)); } } .toggle__fill { position: relative; width: var(--width); height: var(--height); border-radius: var(--border-radius); background-color: var(--toggle-color); transition: background-color 0.3s ease-in-out; &::after { content: ""; position: absolute; top: 0; left: 0; width: var(--height); height: var(--height); border-radius: var(--border-radius); background-color: var(--fill-icons); box-shadow: 0 0 0.2em rgba(0, 0, 0, 0.2); transition: transform 0.3s ease-in-out; } } }
57
+ :root {
58
+ --bg-page: #f6f8fb;
59
+ --bg-panel: #ffffff;
54
60
  --muted: #6b7280;
55
- --accent: #2563eb;
56
- --text: #0f172a;
57
- --elev: 0 6px 18px rgba(15,23,42,0.06);
58
- --btn-border: rgba(15,23,42,0.06);
59
- }
60
- /* Respect user's system preference */
61
- @media (prefers-color-scheme: dark){
62
- :root{
63
- --bg: #0b1220;
64
- --card: #0f1724;
65
- --muted: #9ca3af;
66
- --accent: #60a5fa;
67
- --text: #e6eef8;
68
- --elev: 0 6px 22px rgba(2,6,23,0.6);
69
- --btn-border: rgba(255,255,255,0.08);
70
- }
71
- }
72
- /* Toggle using hidden checkbox (works without :has()) */
73
- #dark-toggle:checked ~ .container{
74
- --bg: #0b1220;
75
- --card: #0f1724;
61
+ --title: #0f172a;
62
+ --link-color: #2563eb;
63
+ --link-hover-color: #1e40af;
64
+ --accent: #10b981;
65
+ --card-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
66
+ --radius: 10px;
67
+ --gap: 12px;
68
+ --toggle-track: #e6e9ee;
69
+ --toggle-knob: #ffffff;
70
+ }
71
+
72
+ :root:has(#dark:checked) {
73
+ --bg-page: #0b1220;
74
+ --bg-panel: #0f1724;
76
75
  --muted: #9ca3af;
77
- --accent: #60a5fa;
78
- --text: #e6eef8;
79
- --elev: 0 6px 22px rgba(2,6,23,0.6);
80
- }
81
-
82
- .box-sizing{box-sizing:border-box}
83
- *{box-sizing:border-box}
84
- html{color-scheme: light dark; background-color:var(--bg)}
85
- body{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial,sans-serif;font-size:14px;background:var(--bg);color:var(--text)}
86
- .container{max-width:980px;margin:28px auto;padding:20px}
87
- .card{background:var(--card);border-radius:10px;padding:18px;box-shadow:var(--elev)}
88
- .header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:12px}
89
- .title{font-size:18px;font-weight:600;color:var(--text);margin:0}
90
- .path{font-size:12px;color:var(--muted)}
91
- .controls{display:flex;align-items:center;gap:8px}
92
- .toggle-btn{background:transparent;border:1px solid var(--btn-border);padding:6px 8px;border-radius:8px;cursor:pointer;font-size:14px;color:var(--text);backdrop-filter: blur(4px);transition:all 180ms ease}
93
- .toggle-btn:hover{opacity:0.95}
94
-
95
- /* Visible active state when dark mode is enabled */
96
- #dark-toggle:checked ~ .container .toggle-btn{background:var(--accent);color:#fff;border-color:transparent;transform:translateY(-1px)}
97
- .file-list{list-style:none;padding:0;margin:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
98
- .file{display:flex;align-items:center;padding:10px;border-radius:8px;border:1px solid rgba(15,23,42,0.04);background:linear-gradient(180deg,rgba(0,0,0,0.01),transparent)}
99
- .file a{display:flex;align-items:center;gap:12px;width:100%;color:inherit;text-decoration:none}
100
- .icon{width:44px;height:44px;border-radius:8px;display:flex;align-items:center;justify-content:center;background:rgba(37,99,235,0.08);font-size:20px}
101
- .name{font-weight:500}
102
- .meta{margin-left:auto;font-size:12px;color:var(--muted)}
103
- .parent{grid-column:1/-1;padding:0 4px}
104
- .empty{padding:18px;text-align:center;color:var(--muted)}
105
- @media (max-width:600px){.file-list{grid-template-columns:1fr}}
106
- `,k=(e,t)=>{let n=t===`/`?null:{name:`..`,url:r(t,`../`).replace(/\\/g,`/`),isDirectory:!0},i=e.slice().sort((e,t)=>e.isDirectory===t.isDirectory?e.name.toLowerCase().localeCompare(t.name.toLowerCase()):e.isDirectory?-1:1).map(e=>{let t=e.isDirectory?`📁`:`📄`;return`<li class="file"><a href="${e.url}"><span class="icon">${t}</span><span class="name">${e.name}</span><span class="meta">${e.isDirectory?`Dir`:`File`}</span></a></li>`}).join(`
107
- `),a=n?`<li class="file parent"><a href="../"><span class="icon">⬆️</span><span class="name">..</span><span class="meta">Parent</span></a></li>`:``,o=i||`<div class="empty">This folder is empty</div>`;return`
108
- <!doctype html>
109
- <html lang="en">
76
+ --title: #e6eef8;
77
+ --link-color: #7c9cff;
78
+ --link-hover-color: #9fb7ff;
79
+ --accent: #34d399;
80
+ --card-shadow: 0 6px 18px rgba(2,6,23,0.6);
81
+ --toggle-track: #1f2937;
82
+ --toggle-knob: #0b1220;
83
+ }
84
+
85
+ * { box-sizing: border-box; }
86
+
87
+ body {
88
+ margin: 0;
89
+ padding: 2rem;
90
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif;
91
+ background-color: var(--bg-page);
92
+ color: var(--title);
93
+ -webkit-font-smoothing: antialiased;
94
+ -moz-osx-font-smoothing: grayscale;
95
+ line-height: 1.4;
96
+ display: flex;
97
+ justify-content: center;
98
+ align-items: flex-start;
99
+ min-height: 100vh;
100
+ }
101
+
102
+ .container {
103
+ width: 100%;
104
+ max-width: 980px;
105
+ background: linear-gradient(180deg, rgba(255,255,255,0.6), rgba(255,255,255,0.4));
106
+ background-color: var(--bg-panel);
107
+ border-radius: var(--radius);
108
+ box-shadow: var(--card-shadow);
109
+ padding: 1.25rem;
110
+ gap: var(--gap);
111
+ }
112
+
113
+ /* Header */
114
+ .header {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: space-between;
118
+ gap: 1rem;
119
+ margin-bottom: 0.75rem;
120
+ }
121
+ .header .title {
122
+ font-size: 1.6rem;
123
+ font-weight: 600;
124
+ }
125
+ .header .subtitle {
126
+ color: var(--muted);
127
+ font-size: 0.95rem;
128
+ }
129
+
130
+ /* Dark toggle */
131
+ .toggle {
132
+ --width: 48px;
133
+ --height: 24px;
134
+ display: inline-flex;
135
+ align-items: center;
136
+ cursor: pointer;
137
+ }
138
+ .toggle__input {
139
+ position: absolute;
140
+ opacity: 0;
141
+ pointer-events: none;
142
+ }
143
+ .toggle__fill {
144
+ width: var(--width);
145
+ height: var(--height);
146
+ background: var(--toggle-track);
147
+ border-radius: 999px;
148
+ position: relative;
149
+ transition: background 200ms ease;
150
+ }
151
+ .toggle__fill::after {
152
+ content: "";
153
+ position: absolute;
154
+ top: 3px;
155
+ left: 3px;
156
+ width: calc(var(--height) - 6px);
157
+ height: calc(var(--height) - 6px);
158
+ border-radius: 50%;
159
+ background: var(--toggle-knob);
160
+ box-shadow: 0 2px 6px rgba(2,6,23,0.08);
161
+ transition: transform 200ms ease;
162
+ }
163
+ .toggle__input:checked + .toggle__fill {
164
+ background: linear-gradient(90deg, rgba(16,185,129,0.9), rgba(34,197,94,0.9));
165
+ }
166
+ .toggle__input:checked + .toggle__fill::after {
167
+ transform: translateX(calc(var(--width) - var(--height)));
168
+ }
169
+
170
+ /* Listing - improved file/folder presentation */
171
+ .listing {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 0.5rem;
175
+ margin-top: 0.5rem;
176
+ width: 100%;
177
+ }
178
+
179
+ /* Each item becomes a clear row with icon, name, and meta */
180
+ .listing a.item {
181
+ display: grid;
182
+ grid-template-columns: 44px 1fr auto;
183
+ align-items: center;
184
+ gap: 0.75rem;
185
+ padding: 0.75rem 1rem;
186
+ border-radius: 10px;
187
+ color: var(--title);
188
+ text-decoration: none;
189
+ background: transparent;
190
+ transition: background 180ms ease, transform 150ms ease, box-shadow 180ms ease;
191
+ font-size: 1rem;
192
+ overflow: hidden;
193
+ border: 1px solid transparent;
194
+ }
195
+
196
+ /* Hover and focus styles */
197
+ .listing a.item:hover,
198
+ .listing a.item:focus {
199
+ background: color-mix(in srgb, var(--link-hover-color) 6%, transparent);
200
+ transform: translateY(-4px);
201
+ box-shadow: 0 10px 24px color-mix(in srgb, var(--title) 6%, transparent);
202
+ color: var(--link-hover-color);
203
+ outline: none;
204
+ border-color: color-mix(in srgb, var(--link-hover-color) 12%, transparent);
205
+ }
206
+
207
+ /* Icon badge */
208
+ .listing a.item .icon {
209
+ display: inline-grid;
210
+ place-items: center;
211
+ width: 44px;
212
+ height: 44px;
213
+ border-radius: 10px;
214
+ font-size: 1.1rem;
215
+ flex: 0 0 auto;
216
+ background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(0,0,0,0.02));
217
+ box-shadow: 0 2px 8px rgba(2,6,23,0.04);
218
+ color: var(--fill-icons, #fff);
219
+ }
220
+
221
+ /* Different tones for directories vs files - still respecting variables */
222
+ .listing a.item.dir .icon {
223
+ background: linear-gradient(180deg, rgba(124,58,237,0.12), rgba(124,58,237,0.06));
224
+ color: white;
225
+ }
226
+ .listing a.item.file .icon {
227
+ background: linear-gradient(180deg, rgba(37,99,235,0.08), rgba(37,99,235,0.03));
228
+ color: white;
229
+ }
230
+
231
+ /* Name column */
232
+ .listing a.item .name {
233
+ font-weight: 600;
234
+ color: var(--title);
235
+ min-width: 0; /* allow ellipsis */
236
+ overflow: hidden;
237
+ text-overflow: ellipsis;
238
+ white-space: nowrap;
239
+ }
240
+
241
+ /* Meta column (size/type/date or just type for now) */
242
+ .listing a.item .meta {
243
+ color: var(--muted);
244
+ font-size: 0.82rem;
245
+ margin-left: 0.6rem;
246
+ text-align: right;
247
+ white-space: nowrap;
248
+ flex-shrink: 0;
249
+ background: color-mix(in srgb, var(--muted) 8%, transparent);
250
+ padding: 4px 8px;
251
+ border-radius: 999px;
252
+ }
253
+
254
+ /* Parent link styling - present as subtle pill */
255
+ .parent {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 0.5rem;
259
+ padding: 0.35rem 0.6rem;
260
+ border-radius: 8px;
261
+ margin-bottom: 0.35rem;
262
+ color: var(--muted);
263
+ font-size: 0.95rem;
264
+ background: transparent;
265
+ }
266
+ .parent a { color: inherit; text-decoration: none; }
267
+
268
+ /* Small screens adjustments */
269
+ @media (max-width: 640px) {
270
+ .container { padding: 0.8rem; margin: 1rem; }
271
+ .header .title { font-size: 1.25rem; }
272
+ .listing a.item { padding: 0.5rem; font-size: 0.95rem; grid-template-columns: 36px 1fr auto; }
273
+ .listing a.item .icon { width: 36px; height: 36px; border-radius: 8px; }
274
+ }
275
+ `,k=(e,t)=>{let n=t===`/`?``:`<div class="parent"><a href="../">../</a></div>`,r=e.slice().sort((e,t)=>e.isDirectory===t.isDirectory?0:e.isDirectory?-1:1).sort((e,t)=>e.isDirectory===t.isDirectory?e.name.localeCompare(t.name):0).map(e=>{let t=e.isDirectory?`📁`:`📄`,n=e.url,r=e.isDirectory?`dir`:`file`,i=e.isDirectory?`Directory`:e.name.includes(`.`)?e.name.split(`.`).pop().toUpperCase():`File`;return`<a class="item ${r}" href="${n}"><span class="icon">${t}</span><span class="name">${e.name}</span><span class="meta">${i}</span></a>`}).join(`
276
+ `);return`
277
+ <!DOCTYPE html>
278
+ <html>
110
279
  <head>
111
- <meta charset="utf-8" />
112
- <meta name="viewport" content="width=device-width,initial-scale=1" />
280
+ <meta charset="utf-8">
113
281
  <title>Listing of ${t}</title>
114
282
  <style>${O()}</style>
115
- <style>/* small reset for injected scripts */ body > script{display:none}</style>
116
283
  </head>
117
284
  <body>
118
- <input type="checkbox" id="dark-toggle" aria-hidden style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;border:0;padding:0;margin:0" />
119
- <div class="container">
120
- <div class="card">
121
- <div class="header">
122
- <div>
123
- <h1 class="title">Listing of ${t}</h1>
124
- <div class="path">${t}</div>
125
- </div>
126
- <div class="controls">
127
- <label for="dark-toggle" class="toggle-btn" title="Toggle dark mode">🌓</label>
128
- </div>
129
- </div>
130
- <ul class="file-list">
131
- ${a}
132
- ${o}
133
- </ul>
134
- </div>
285
+ <label class="toggle" for="dark"><input type="checkbox" id="dark" class="toggle__input" checked><span class="toggle__fill"></span></label>
286
+ <h1>Listing of ${t}</h1>
287
+ <div class="listing">
288
+ ${n}
289
+ ${r}
135
290
  </div>
136
291
  </body>
137
- </html>`},A=async(e,t,n)=>{let r=C(e);if(n.enableLiveReload&&r.includes(`text/html`))try{let i=await c(e);try{let e=T(await i.readFile({encoding:`utf-8`}),n.port);t.writeHead(200,{"Content-Type":r}),t.end(e);return}finally{await i.close()}}catch{}t.writeHead(200,{"Content-Type":r});let i=o(e);i.pipe(t),i.on(`error`,e=>{t.writeHead(500),t.end(e.message)})},j=async(e,t,n,i)=>{try{let a=k((await l(e,{withFileTypes:!0})).filter(e=>!x(e.name,i.ignorePatterns)).map(e=>({name:e.name,isDirectory:e.isDirectory(),url:r(t,e.name).replace(/\\/g,`/`)})),t);i.enableLiveReload&&(a=T(a,i.port)),n.writeHead(200,{"Content-Type":`text/html`}),n.end(a)}catch{n.writeHead(500),n.end(`Error reading directory`)}},M=e=>async(t,n)=>{let a=t.headers.host||`localhost:${e.port}`,o=new URL(t.url||`/`,`http://${a}`),s=decodeURIComponent(o.pathname);if(e.enableLiveReload&&s===`/livereload`)return E(t,n);let c=i(e.directory,`.${s}`);if(!c.startsWith(e.directory))return n.writeHead(403),n.end(`Forbidden`);try{let t=await u(c);if(t.isFile())return await A(c,n,e);if(t.isDirectory()){if(e.enableDirectoryListing)return await j(c,s,n,e);{let t=r(c,`index.html`);try{return await u(t),await A(t,n,e)}catch{return n.writeHead(403),n.end(`Listing disabled`)}}}else return n.writeHead(404),n.end(`Not Found`)}catch(e){if(e&&e.code===`ENOENT`)return n.writeHead(404),n.end(`Not Found`);n.writeHead(500),n.end(e?.message||`Server error`)}},N=()=>{b(`Reloading server process...`),f(process.argv[0],process.argv.slice(1),{stdio:`inherit`,detached:!0}).unref(),process.exit(0)},P=async e=>{b(`Watching: ${e.directory}`);try{s(e.directory,{recursive:!0},async(t,n)=>{if(!n||x(n,e.ignorePatterns)||_)return;_=!0,b(`Change detected: ${n} (${t})`),await S(500);let r=/\.(json|js|mjs|ts)$/.test(n),i=/\.(html|css|png|jpg|jpeg|gif|svg)$/.test(n);e.restartOnChange||r?(e.enableLiveReload&&D(`restart`),N()):e.enableLiveReload&&i&&D(`asset change`),setTimeout(()=>{_=!1},1e3)})}catch(e){b(`Watcher error: ${e?.message}`,`error`)}};(()=>{try{let e=y(process.argv.slice(2));a(M(e)).listen(e.port,()=>{b(`Server running at http://localhost:${e.port}`),b(`Root: ${e.directory}`),P(e)}),[`SIGINT`,`SIGTERM`].forEach(e=>{process.on(e,()=>{b(`Shutting down...`),process.exit(0)})})}catch(e){b(`Startup Error: ${e?.message}`,`error`),process.exit(1)}})();export{};
292
+ </html>`},A=async(e,t,n)=>{let r=C(e);if(n.enableLiveReload&&r.includes(`text/html`))try{let i=await c(e);try{let e=T(await i.readFile({encoding:`utf-8`}),n.port);t.writeHead(200,{"Content-Type":r}),t.end(e);return}finally{await i.close()}}catch{}t.writeHead(200,{"Content-Type":r});let i=o(e);i.pipe(t),i.on(`error`,e=>{t.writeHead(500),t.end(e.message)})},j=async(e,t,n,i)=>{try{let a=k((await l(e,{withFileTypes:!0})).filter(e=>!x(e.name,i.ignorePatterns)).map(e=>({name:e.name,isDirectory:e.isDirectory(),url:r(t,e.name).replace(/\\/g,`/`)})),t);i.enableLiveReload&&(a=T(a,i.port)),n.writeHead(200,{"Content-Type":`text/html`}),n.end(a)}catch{n.writeHead(500),n.end(`Error reading directory`)}},M=e=>async(t,n)=>{let a=t.headers.host||`localhost:${e.port}`,o=new URL(t.url||`/`,`http://${a}`),s=decodeURIComponent(o.pathname);if(e.enableLiveReload&&s===`/livereload`)return E(t,n);let c=i(e.directory,`.${s}`);if(!c.startsWith(e.directory))return n.writeHead(403),n.end(`Forbidden`);try{let t=await u(c);if(t.isFile())return await A(c,n,e);if(t.isDirectory()){if(e.enableDirectoryListing)return await j(c,s,n,e);{let t=r(c,`index.html`);try{return await u(t),await A(t,n,e)}catch{return n.writeHead(403),n.end(`Listing disabled`)}}}else return n.writeHead(404),n.end(`Not Found`)}catch(e){if(e&&e.code===`ENOENT`)return n.writeHead(404),n.end(`Not Found`);n.writeHead(500),n.end(e?.message||`Server error`)}},N=()=>{b(`Reloading server process...`),f(process.argv[0],process.argv.slice(1),{stdio:`inherit`,detached:!0}).unref(),process.exit(0)},P=e=>{b(`Watching: ${e.directory}`);try{s(e.directory,{recursive:!0},async(t,n)=>{if(!n||x(n,e.ignorePatterns)||_)return;_=!0,b(`Change detected: ${n} (${t})`),await S(500);let r=/\.(json|js|mjs|ts)$/.test(n),i=/\.(html|css|png|jpg|jpeg|gif|svg)$/.test(n);e.restartOnChange||r?(e.enableLiveReload&&D(`restart`),N()):e.enableLiveReload&&i&&D(`asset change`),setTimeout(()=>{_=!1},1e3)})}catch(e){b(`Watcher error: ${e?.message}`,`error`)}};(()=>{try{let e=y(process.argv.slice(2));a(M(e)).listen(e.port,()=>{b(`Server running at http://localhost:${e.port}`),b(`Root: ${e.directory}`),P(e)}),[`SIGINT`,`SIGTERM`].forEach(e=>{process.on(e,()=>{b(`Shutting down...`),process.exit(0)})})}catch(e){b(`Startup Error: ${e?.message}`,`error`),process.exit(1)}})();export{};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "httpath",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
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
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",