asljs-server 0.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/LICENSE.md +21 -0
- package/README.md +38 -0
- package/cli.js +3 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +102 -0
- package/dist/send.d.ts +13 -0
- package/dist/send.js +95 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +199 -0
- package/dist/watch.d.ts +1 -0
- package/dist/watch.js +21 -0
- package/package.json +49 -0
- package/server.d.ts +8 -0
- package/server.js +3 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alexandrite Software Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# asljs-server
|
|
2
|
+
|
|
3
|
+
A lightweight development-time static files server with live-reload (SSE) and
|
|
4
|
+
a filesystem read/write API.
|
|
5
|
+
|
|
6
|
+
## CLI
|
|
7
|
+
|
|
8
|
+
After installing:
|
|
9
|
+
|
|
10
|
+
- `asljs-server`
|
|
11
|
+
|
|
12
|
+
Or without installing:
|
|
13
|
+
|
|
14
|
+
- `npx asljs-server`
|
|
15
|
+
|
|
16
|
+
### Examples
|
|
17
|
+
|
|
18
|
+
- Serve current folder on port 3000:
|
|
19
|
+
- `asljs-server`
|
|
20
|
+
|
|
21
|
+
- Serve a specific folder:
|
|
22
|
+
- `asljs-server ./public`
|
|
23
|
+
- `asljs-server --root ./public`
|
|
24
|
+
|
|
25
|
+
- Change port/host:
|
|
26
|
+
- `asljs-server --port 8080`
|
|
27
|
+
- `asljs-server --host 0.0.0.0 --port 8080`
|
|
28
|
+
|
|
29
|
+
## JSON API
|
|
30
|
+
|
|
31
|
+
- `GET /api/json?file=name[.json]` returns the JSON file
|
|
32
|
+
- `PUT|POST /api/json?file=name[.json]` writes JSON (pretty-printed)
|
|
33
|
+
|
|
34
|
+
## Live reload
|
|
35
|
+
|
|
36
|
+
HTML pages get a small client injected that listens on:
|
|
37
|
+
|
|
38
|
+
- `GET /__events`
|
package/cli.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { startServer } from './server.js';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
function printHelp() {
|
|
6
|
+
process.stdout.write(`asljs-server
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
asljs-server [--root <dir>] [--port <number>] [--host <name>]
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--root <dir> Root directory to serve (default: .)
|
|
13
|
+
--port <number> Port to listen on (default: 3000)
|
|
14
|
+
--host <name> Host/interface to bind (default: localhost)
|
|
15
|
+
--help Show this help
|
|
16
|
+
--version Print version
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
function readVersion() {
|
|
20
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const packageJsonPath = path.join(here, '..', 'package.json');
|
|
22
|
+
const raw = readFileSync(packageJsonPath, 'utf8');
|
|
23
|
+
return JSON.parse(raw)
|
|
24
|
+
.version;
|
|
25
|
+
}
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const options = { root: '.',
|
|
28
|
+
port: 3000,
|
|
29
|
+
host: 'localhost' };
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg === '--help'
|
|
33
|
+
|| arg === '-h') {
|
|
34
|
+
return { kind: 'help' };
|
|
35
|
+
}
|
|
36
|
+
if (arg === '--version'
|
|
37
|
+
|| arg === '-v') {
|
|
38
|
+
return { kind: 'version' };
|
|
39
|
+
}
|
|
40
|
+
if (arg === '--root') {
|
|
41
|
+
const value = argv[++i];
|
|
42
|
+
if (!value) {
|
|
43
|
+
throw new TypeError('Missing value for --root');
|
|
44
|
+
}
|
|
45
|
+
options.root = value;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === '--port') {
|
|
49
|
+
const value = argv[++i];
|
|
50
|
+
if (!value) {
|
|
51
|
+
throw new TypeError('Missing value for --port');
|
|
52
|
+
}
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
if (!Number.isFinite(parsed)
|
|
55
|
+
|| parsed <= 0) {
|
|
56
|
+
throw new TypeError('Invalid --port');
|
|
57
|
+
}
|
|
58
|
+
options.port = parsed;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg === '--host') {
|
|
62
|
+
const value = argv[++i];
|
|
63
|
+
if (!value) {
|
|
64
|
+
throw new TypeError('Missing value for --host');
|
|
65
|
+
}
|
|
66
|
+
options.host = value;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg.startsWith('-')) {
|
|
70
|
+
throw new TypeError(`Unknown option: ${arg}`);
|
|
71
|
+
}
|
|
72
|
+
// Positional = root directory
|
|
73
|
+
options.root = arg;
|
|
74
|
+
}
|
|
75
|
+
return { kind: 'run', options };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
79
|
+
switch (parsed.kind) {
|
|
80
|
+
case 'help':
|
|
81
|
+
printHelp();
|
|
82
|
+
process.exitCode = 0;
|
|
83
|
+
break;
|
|
84
|
+
case 'version':
|
|
85
|
+
process.stdout.write(`${readVersion() ?? ''}\n`);
|
|
86
|
+
process.exitCode = 0;
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
startServer(parsed.options);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = (error
|
|
95
|
+
&& typeof error === 'object'
|
|
96
|
+
&& 'message' in error)
|
|
97
|
+
? String(error.message)
|
|
98
|
+
: String(error);
|
|
99
|
+
process.stderr.write(`${message}\n\n`);
|
|
100
|
+
printHelp();
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
package/dist/send.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OutgoingHttpHeaders } from 'node:http';
|
|
2
|
+
import type { ServerResponse } from 'node:http';
|
|
3
|
+
export declare function options(response: ServerResponse, headers: OutgoingHttpHeaders): void;
|
|
4
|
+
export declare function api(response: ServerResponse, payload: unknown): void;
|
|
5
|
+
export declare function apiError(response: ServerResponse, error: unknown): void;
|
|
6
|
+
export declare function json(response: ServerResponse, status: number, jsonString: unknown): void;
|
|
7
|
+
export declare function html(response: ServerResponse, htmlString: string): void;
|
|
8
|
+
export declare function badRequest(response: ServerResponse, message?: string): void;
|
|
9
|
+
export declare function forbidden(response: ServerResponse, message?: string): void;
|
|
10
|
+
export declare function notFound(response: ServerResponse, message?: string): void;
|
|
11
|
+
export declare function methodNotAllowed(response: ServerResponse, allowed?: string): void;
|
|
12
|
+
export declare function error(response: ServerResponse, err: unknown): void;
|
|
13
|
+
export declare function file(response: ServerResponse, filePath: string): Promise<void>;
|
package/dist/send.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const CONTENT_TYPES = new Map([['.html', 'text/html; charset=utf-8'],
|
|
5
|
+
['.htm', 'text/html; charset=utf-8'],
|
|
6
|
+
['.js', 'text/javascript; charset=utf-8'],
|
|
7
|
+
['.mjs', 'text/javascript; charset=utf-8'],
|
|
8
|
+
['.css', 'text/css; charset=utf-8'],
|
|
9
|
+
['.json', 'application/json; charset=utf-8'],
|
|
10
|
+
['.txt', 'text/plain; charset=utf-8'],
|
|
11
|
+
['.svg', 'image/svg+xml'],
|
|
12
|
+
['.png', 'image/png'],
|
|
13
|
+
['.jpg', 'image/jpeg'],
|
|
14
|
+
['.jpeg', 'image/jpeg'],
|
|
15
|
+
['.gif', 'image/gif'],
|
|
16
|
+
['.webp', 'image/webp'],
|
|
17
|
+
['.ico', 'image/x-icon'],
|
|
18
|
+
['.woff', 'font/woff'],
|
|
19
|
+
['.woff2', 'font/woff2']]);
|
|
20
|
+
function contentTypeFor(filePath) {
|
|
21
|
+
const ext = path.extname(filePath)
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
return CONTENT_TYPES.get(ext)
|
|
24
|
+
|| 'application/octet-stream';
|
|
25
|
+
}
|
|
26
|
+
export function options(response, headers) {
|
|
27
|
+
response.writeHead(204, headers);
|
|
28
|
+
response.end();
|
|
29
|
+
}
|
|
30
|
+
export function api(response, payload) {
|
|
31
|
+
response.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
32
|
+
response.end(JSON.stringify(payload));
|
|
33
|
+
}
|
|
34
|
+
export function apiError(response, error) {
|
|
35
|
+
response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
|
|
36
|
+
const message = (error && typeof error === 'object' && 'message' in error)
|
|
37
|
+
? String(error.message)
|
|
38
|
+
: String(error);
|
|
39
|
+
response.end(JSON.stringify({ error: message }));
|
|
40
|
+
}
|
|
41
|
+
export function json(response, status, jsonString) {
|
|
42
|
+
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
43
|
+
response.end(typeof jsonString === 'string'
|
|
44
|
+
? jsonString
|
|
45
|
+
: JSON.stringify(jsonString));
|
|
46
|
+
}
|
|
47
|
+
export function html(response, htmlString) {
|
|
48
|
+
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
49
|
+
response.end(htmlString);
|
|
50
|
+
}
|
|
51
|
+
export function badRequest(response, message = 'Bad request') {
|
|
52
|
+
response.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' });
|
|
53
|
+
response.end(message);
|
|
54
|
+
}
|
|
55
|
+
export function forbidden(response, message = 'Forbidden') {
|
|
56
|
+
response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
57
|
+
response.end(message);
|
|
58
|
+
}
|
|
59
|
+
export function notFound(response, message = 'Not found') {
|
|
60
|
+
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
61
|
+
response.end(message);
|
|
62
|
+
}
|
|
63
|
+
export function methodNotAllowed(response, allowed = 'GET') {
|
|
64
|
+
response.writeHead(405, { 'content-type': 'text/plain; charset=utf-8',
|
|
65
|
+
'allow': allowed });
|
|
66
|
+
response.end('Method not allowed');
|
|
67
|
+
}
|
|
68
|
+
export function error(response, err) {
|
|
69
|
+
response.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
70
|
+
const message = (err && typeof err === 'object' && 'stack' in err)
|
|
71
|
+
? String(err.stack)
|
|
72
|
+
: (err && typeof err === 'object' && 'message' in err)
|
|
73
|
+
? String(err.message)
|
|
74
|
+
: String(err);
|
|
75
|
+
response.end(message);
|
|
76
|
+
}
|
|
77
|
+
export async function file(response, filePath) {
|
|
78
|
+
try {
|
|
79
|
+
const stat = await fsp.stat(filePath);
|
|
80
|
+
response.writeHead(200, { 'content-type': contentTypeFor(filePath),
|
|
81
|
+
'content-length': stat.size });
|
|
82
|
+
fs.createReadStream(filePath)
|
|
83
|
+
.on('error', err => error(response, err))
|
|
84
|
+
.pipe(response);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err
|
|
88
|
+
&& typeof err === 'object'
|
|
89
|
+
&& 'code' in err
|
|
90
|
+
&& err.code === 'ENOENT') {
|
|
91
|
+
return notFound(response);
|
|
92
|
+
}
|
|
93
|
+
return error(response, err);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
export type ServerLogger = {
|
|
3
|
+
log: (...args: unknown[]) => void;
|
|
4
|
+
error: (...args: unknown[]) => void;
|
|
5
|
+
};
|
|
6
|
+
export type StartServerOptions = {
|
|
7
|
+
root?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
host?: string;
|
|
10
|
+
logger?: ServerLogger;
|
|
11
|
+
};
|
|
12
|
+
export declare function startServer({ root, port, host, logger }?: StartServerOptions): Server;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { URL, pathToFileURL } from 'node:url';
|
|
5
|
+
import * as send from './send.js';
|
|
6
|
+
import { watchStaticTree } from './watch.js';
|
|
7
|
+
function isPathInside(child, parent) {
|
|
8
|
+
const resolved = path.resolve(child);
|
|
9
|
+
return resolved === parent
|
|
10
|
+
|| resolved.startsWith(parent + path.sep);
|
|
11
|
+
}
|
|
12
|
+
// inject minimal client into served HTML
|
|
13
|
+
const RELOAD_SNIPPET = `<script>
|
|
14
|
+
try {
|
|
15
|
+
const es = new EventSource('/__events');
|
|
16
|
+
es.addEventListener('reload', () => location.reload());
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
</script>`;
|
|
19
|
+
const safeJsonName = (name) => typeof name === 'string'
|
|
20
|
+
&& /^[a-zA-Z0-9._/-]+$/.test(name)
|
|
21
|
+
? name
|
|
22
|
+
: null;
|
|
23
|
+
function readBody(request, limit) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let size = 0;
|
|
26
|
+
const chunks = [];
|
|
27
|
+
request.on('data', chunk => {
|
|
28
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
29
|
+
? chunk
|
|
30
|
+
: Buffer.from(chunk);
|
|
31
|
+
size += buffer.length;
|
|
32
|
+
if (size > limit) {
|
|
33
|
+
reject(new Error('Payload too large'));
|
|
34
|
+
request.destroy();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
chunks.push(buffer);
|
|
38
|
+
});
|
|
39
|
+
request.on('end', () => resolve(Buffer.concat(chunks)
|
|
40
|
+
.toString('utf8')));
|
|
41
|
+
request.on('error', reject);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const options = { 'access-control-allow-origin': '*',
|
|
45
|
+
'access-control-allow-methods': 'GET,POST,PUT,OPTIONS',
|
|
46
|
+
'access-control-allow-headers': 'content-type' };
|
|
47
|
+
export function startServer({ root = '.', port = 3000, host = 'localhost', logger = console } = {}) {
|
|
48
|
+
const FILES_DIR = path.resolve(root);
|
|
49
|
+
// ---------- hot reload (SSE)
|
|
50
|
+
const sseClients = new Set();
|
|
51
|
+
const sseHeaders = { 'content-type': 'text/event-stream',
|
|
52
|
+
'cache-control': 'no-cache',
|
|
53
|
+
'connection': 'keep-alive',
|
|
54
|
+
'x-accel-buffering': 'no' };
|
|
55
|
+
const serveSSE = (req, res) => {
|
|
56
|
+
res.writeHead(200, sseHeaders);
|
|
57
|
+
res.write(': connected\n\n'); // comment = keep-alive
|
|
58
|
+
sseClients.add(res);
|
|
59
|
+
const ping = setInterval(() => res.write(': ping\n\n'), 30000);
|
|
60
|
+
req.on('close', () => {
|
|
61
|
+
clearInterval(ping);
|
|
62
|
+
sseClients.delete(res);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
let reloadPending = false;
|
|
66
|
+
const broadcastReload = () => {
|
|
67
|
+
if (reloadPending)
|
|
68
|
+
return;
|
|
69
|
+
reloadPending = true;
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
reloadPending = false;
|
|
72
|
+
for (const res of sseClients) {
|
|
73
|
+
res.write('event: reload\n');
|
|
74
|
+
res.write('data: now\n\n');
|
|
75
|
+
}
|
|
76
|
+
}, 50);
|
|
77
|
+
};
|
|
78
|
+
watchStaticTree(FILES_DIR, () => broadcastReload());
|
|
79
|
+
// ---------- JSON API helpers
|
|
80
|
+
const jsonFilePath = (name) => {
|
|
81
|
+
const base = safeJsonName(name);
|
|
82
|
+
if (!base)
|
|
83
|
+
return null;
|
|
84
|
+
const file = base.endsWith('.json')
|
|
85
|
+
? base
|
|
86
|
+
: (base + '.json');
|
|
87
|
+
const full = path.join(FILES_DIR, file);
|
|
88
|
+
return isPathInside(full, FILES_DIR)
|
|
89
|
+
? full
|
|
90
|
+
: null;
|
|
91
|
+
};
|
|
92
|
+
async function handleJsonApi(request, response, url) {
|
|
93
|
+
await fsp.mkdir(FILES_DIR, { recursive: true });
|
|
94
|
+
const name = url.searchParams.get('file');
|
|
95
|
+
const file = jsonFilePath(name);
|
|
96
|
+
if (!file) {
|
|
97
|
+
return send.badRequest(response, 'Invalid "file" name. Allowed: [a-zA-Z0-9._-] and optional .json');
|
|
98
|
+
}
|
|
99
|
+
if (request.method === 'GET') {
|
|
100
|
+
try {
|
|
101
|
+
return send.json(response, 200, await fsp.readFile(file, 'utf8'));
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT')
|
|
105
|
+
return send.notFound(response, 'JSON file not found');
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (request.method === 'PUT'
|
|
110
|
+
|| request.method === 'POST') {
|
|
111
|
+
try {
|
|
112
|
+
// 1MB limit
|
|
113
|
+
const body = await readBody(request, 1000000);
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(body);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return send.badRequest(response, 'Invalid JSON');
|
|
120
|
+
}
|
|
121
|
+
await fsp.writeFile(file, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
|
|
122
|
+
return send.api(response, { file: path.basename(file) });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return send.apiError(response, error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return send.methodNotAllowed(response, 'GET, PUT, POST');
|
|
129
|
+
}
|
|
130
|
+
async function serveStatic(request, response, url) {
|
|
131
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
132
|
+
// SSE endpoint
|
|
133
|
+
if (pathname === '/__events') {
|
|
134
|
+
return serveSSE(request, response);
|
|
135
|
+
}
|
|
136
|
+
// JSON API: /api/json?file=name[.json]
|
|
137
|
+
if (pathname === '/api/json') {
|
|
138
|
+
return handleJsonApi(request, response, url);
|
|
139
|
+
}
|
|
140
|
+
// Resolve file path
|
|
141
|
+
let filePath = path.normalize(path.join(FILES_DIR, pathname));
|
|
142
|
+
if (!isPathInside(filePath, FILES_DIR)) {
|
|
143
|
+
return send.forbidden(response);
|
|
144
|
+
}
|
|
145
|
+
// If path is dir, try index.html
|
|
146
|
+
try {
|
|
147
|
+
const stat = await fsp.stat(filePath);
|
|
148
|
+
if (stat.isDirectory()) {
|
|
149
|
+
filePath =
|
|
150
|
+
path.join(filePath, 'index.html');
|
|
151
|
+
await fsp.stat(filePath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return send.notFound(response);
|
|
156
|
+
}
|
|
157
|
+
const ext = path.extname(filePath)
|
|
158
|
+
.toLowerCase();
|
|
159
|
+
// HTML: inject hot-reload snippet
|
|
160
|
+
if (ext === '.html'
|
|
161
|
+
&& request.method === 'GET') {
|
|
162
|
+
try {
|
|
163
|
+
const html = await fsp.readFile(filePath, 'utf8');
|
|
164
|
+
const htmlWithReload = html.includes('/__events')
|
|
165
|
+
? html
|
|
166
|
+
: html.replace(/<\/body\s*>/i, m => `${RELOAD_SNIPPET}\n${m}`) +
|
|
167
|
+
(!html.match(/<\/body\s*>/i)
|
|
168
|
+
? `\n${RELOAD_SNIPPET}`
|
|
169
|
+
: '');
|
|
170
|
+
return send.html(response, htmlWithReload);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return send.error(response, error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return send.file(response, filePath);
|
|
177
|
+
}
|
|
178
|
+
const server = http.createServer((request, response) => {
|
|
179
|
+
if (request.method === 'OPTIONS') {
|
|
180
|
+
return send.options(response, options);
|
|
181
|
+
}
|
|
182
|
+
logger.log(`${request.method} ${request.url}`);
|
|
183
|
+
serveStatic(request, response, new URL(request.url || '/', `http://${request.headers.host || `${host}:${port}`}`))
|
|
184
|
+
.catch(error => {
|
|
185
|
+
logger.error(error);
|
|
186
|
+
send.error(response, error);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
server.listen(port, host, () => {
|
|
190
|
+
logger.log(`FILES: ${FILES_DIR}`);
|
|
191
|
+
logger.log(`URL: http://${host}:${port}`);
|
|
192
|
+
});
|
|
193
|
+
return server;
|
|
194
|
+
}
|
|
195
|
+
const entryFile = process.argv[1];
|
|
196
|
+
if (entryFile
|
|
197
|
+
&& import.meta.url === pathToFileURL(entryFile).href) {
|
|
198
|
+
startServer();
|
|
199
|
+
}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function watchStaticTree(rootDir: string, onChange: () => void): () => void;
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
export function watchStaticTree(rootDir, onChange) {
|
|
3
|
+
// Best-effort cross-platform watcher.
|
|
4
|
+
// On Windows/macOS, recursive fs.watch works.
|
|
5
|
+
// On Linux, recursive is not supported, but this package is intended for dev-time usage.
|
|
6
|
+
let watcher;
|
|
7
|
+
try {
|
|
8
|
+
watcher =
|
|
9
|
+
fs.watch(rootDir, { recursive: true }, () => onChange());
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
watcher =
|
|
13
|
+
fs.watch(rootDir, () => onChange());
|
|
14
|
+
}
|
|
15
|
+
return () => {
|
|
16
|
+
try {
|
|
17
|
+
watcher.close();
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
};
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "asljs-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A lightweight development-time static files server. Supports live-reload. Provides filesystem read-write API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"types": "server.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./server.d.ts",
|
|
11
|
+
"default": "./server.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"asljs-server": "./cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**",
|
|
19
|
+
"server.js",
|
|
20
|
+
"server.d.ts",
|
|
21
|
+
"cli.js",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p .",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"lint:fix": "eslint . --fix",
|
|
29
|
+
"prepack": "npm run build",
|
|
30
|
+
"test": "npm run build && node --test",
|
|
31
|
+
"test:watch": "npm run build && node --watch --test",
|
|
32
|
+
"coverage": "npm run build && NODE_V8_COVERAGE=.coverage node --test && node -e \"console.log('Coverage in .coverage (use c8/istanbul if you want reports)')\""
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"server",
|
|
36
|
+
"development",
|
|
37
|
+
"live-reload"
|
|
38
|
+
],
|
|
39
|
+
"author": "Alex Netkachov <alex.netkachov@gmail.com>",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"homepage": "https://github.com/AlexandriteSoftware/asljs#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/AlexandriteSoftware/asljs/issues"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/AlexandriteSoftware/asljs.git"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/server.d.ts
ADDED
package/server.js
ADDED