httpath 1.1.0 → 1.2.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.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`,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`,short:`r`,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
- --restart-on-change Restart server process on file changes
13
+ -r --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=>`
@@ -48,24 +48,90 @@ Options:
48
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
49
49
 
50
50
  `)}catch{}}},S=()=>`
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
- `,C=(e,t)=>{let n=t===`/`?``:`<a href="../">../</a><br>`,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?`📁`:`📄`;return`<a href="${e.url}">${t} ${e.name}</a>`}).join(`<br>`);return`
58
- <!DOCTYPE html>
59
- <html>
51
+ :root{
52
+ --bg: #f6f8fa;
53
+ --card: #ffffff;
54
+ --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;
76
+ --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">
60
110
  <head>
61
- <meta charset="utf-8">
62
- <title>Listing of ${t}</title>
111
+ <meta charset="utf-8" />
112
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
113
+ <title>Listing of ${n}</title>
63
114
  <style>${S()}</style>
115
+ <style>/* small reset for injected scripts */ body > script{display:none}</style>
64
116
  </head>
65
117
  <body>
66
- <label class="toggle" for="dark"><input type="checkbox" id="dark" class="toggle__input" checked><span class="toggle__fill"></span></label>
67
- <h1>Listing of ${t}</h1>
68
- ${n}
69
- ${r}
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>
135
+ </div>
70
136
  </body>
71
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)}})();
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`,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`,short:`r`,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
- --restart-on-change Restart server process on file changes
13
+ -r --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=>`
@@ -48,24 +48,90 @@ Options:
48
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
49
49
 
50
50
  `)}catch{}}},O=()=>`
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
- `,k=(e,t)=>{let n=t===`/`?``:`<a href="../">../</a><br>`,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?`📁`:`📄`;return`<a href="${e.url}">${t} ${e.name}</a>`}).join(`<br>`);return`
58
- <!DOCTYPE html>
59
- <html>
51
+ :root{
52
+ --bg: #f6f8fa;
53
+ --card: #ffffff;
54
+ --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;
76
+ --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">
60
110
  <head>
61
- <meta charset="utf-8">
111
+ <meta charset="utf-8" />
112
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
62
113
  <title>Listing of ${t}</title>
63
114
  <style>${O()}</style>
115
+ <style>/* small reset for injected scripts */ body > script{display:none}</style>
64
116
  </head>
65
117
  <body>
66
- <label class="toggle" for="dark"><input type="checkbox" id="dark" class="toggle__input" checked><span class="toggle__fill"></span></label>
67
- <h1>Listing of ${t}</h1>
68
- ${n}
69
- ${r}
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>
135
+ </div>
70
136
  </body>
71
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{};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "httpath",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",