s4d 0.1.2 → 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/dist/DevServer.d.ts +1 -15
- package/dist/DevServer.js +52 -69
- package/package.json +6 -5
package/dist/DevServer.d.ts
CHANGED
|
@@ -7,11 +7,6 @@ export interface DevServerOptions {
|
|
|
7
7
|
host?: string;
|
|
8
8
|
spa?: boolean;
|
|
9
9
|
}
|
|
10
|
-
interface FileCacheEntry {
|
|
11
|
-
contents: string | Buffer;
|
|
12
|
-
contentType: string;
|
|
13
|
-
version: string;
|
|
14
|
-
}
|
|
15
10
|
export declare class DevServer {
|
|
16
11
|
protected webroot: string;
|
|
17
12
|
protected port: number;
|
|
@@ -20,7 +15,6 @@ export declare class DevServer {
|
|
|
20
15
|
protected server: http.Server;
|
|
21
16
|
protected webSocketServer: WebSocketServer;
|
|
22
17
|
protected fileWatcher?: fs.FSWatcher;
|
|
23
|
-
protected fileCache: Map<string, Promise<FileCacheEntry>>;
|
|
24
18
|
protected webSockets: Set<WebSocket>;
|
|
25
19
|
constructor(options: DevServerOptions);
|
|
26
20
|
static cli(argv: string[]): Promise<1 | undefined>;
|
|
@@ -31,15 +25,7 @@ export declare class DevServer {
|
|
|
31
25
|
handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
|
32
26
|
startWatching(): Promise<void>;
|
|
33
27
|
broadcast(message: Record<string, unknown>): void;
|
|
34
|
-
transformFileContents(file: string, contents: string | Buffer): Promise<string | Buffer>;
|
|
35
28
|
getClientScript(): string;
|
|
36
|
-
|
|
37
|
-
resolveFile(file: string): Promise<{
|
|
38
|
-
contents: string | Buffer;
|
|
39
|
-
contentType: string;
|
|
40
|
-
version: string;
|
|
41
|
-
}>;
|
|
42
|
-
invalidateFile(file: string): void;
|
|
29
|
+
stat(file: string): Promise<fs.Stats | null>;
|
|
43
30
|
close(): Promise<void>;
|
|
44
31
|
}
|
|
45
|
-
export {};
|
package/dist/DevServer.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as crypto from 'crypto';
|
|
2
1
|
import * as fs from 'fs';
|
|
3
2
|
import * as http from 'http';
|
|
4
3
|
import mime from 'mime';
|
|
@@ -12,7 +11,6 @@ export class DevServer {
|
|
|
12
11
|
server;
|
|
13
12
|
webSocketServer;
|
|
14
13
|
fileWatcher;
|
|
15
|
-
fileCache = new Map();
|
|
16
14
|
webSockets = new Set();
|
|
17
15
|
constructor(options) {
|
|
18
16
|
this.webroot = options.webroot;
|
|
@@ -53,6 +51,7 @@ export class DevServer {
|
|
|
53
51
|
throw new Error('--host requires an argument');
|
|
54
52
|
}
|
|
55
53
|
host = argv[++i];
|
|
54
|
+
break;
|
|
56
55
|
case '--spa':
|
|
57
56
|
spa = true;
|
|
58
57
|
break;
|
|
@@ -126,48 +125,67 @@ export class DevServer {
|
|
|
126
125
|
return;
|
|
127
126
|
}
|
|
128
127
|
const url = new URL(req.url ?? '/', this.getBaseURL());
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
throw err;
|
|
136
|
-
});
|
|
137
|
-
if (req.headers['if-none-match'] === version) {
|
|
128
|
+
let file = path.join(this.webroot, path.resolve('.', url.pathname));
|
|
129
|
+
if (file.match(/__DEV__\.js$/)) {
|
|
130
|
+
const clientScript = this.getClientScript();
|
|
131
|
+
const etag = `W/${clientScript.length.toString(16)}`;
|
|
132
|
+
if (req.headers['if-none-match'] === etag) {
|
|
138
133
|
res.writeHead(304);
|
|
139
134
|
res.end();
|
|
140
135
|
return;
|
|
141
136
|
}
|
|
142
|
-
res.setHeader('content-type',
|
|
143
|
-
res.setHeader('etag',
|
|
137
|
+
res.setHeader('content-type', 'application/javascript');
|
|
138
|
+
res.setHeader('etag', etag);
|
|
144
139
|
res.writeHead(200);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
res.end(contents);
|
|
150
|
-
}
|
|
140
|
+
res.end(this.getClientScript());
|
|
141
|
+
return;
|
|
151
142
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
143
|
+
let stats = await this.stat(file);
|
|
144
|
+
if (stats?.isDirectory()) {
|
|
145
|
+
file = path.join(file, 'index.html');
|
|
146
|
+
stats = await this.stat(file);
|
|
147
|
+
}
|
|
148
|
+
if (!stats && this.spa && !url.pathname.includes('.')) {
|
|
149
|
+
file = path.join(this.webroot, 'index.html');
|
|
150
|
+
stats = await this.stat(file);
|
|
151
|
+
}
|
|
152
|
+
if (!stats) {
|
|
153
|
+
res.setHeader('content-type', 'text/plain');
|
|
154
|
+
res.writeHead(404);
|
|
155
|
+
res.end('Not found');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const etag = `W/"${stats.size.toString(16)}-${stats.mtimeMs.toString(16)}"`;
|
|
159
|
+
if (req.headers['if-none-match'] === etag) {
|
|
160
|
+
res.writeHead(304);
|
|
161
|
+
res.end();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const contentType = mime.getType(file) ?? 'application/octet-stream';
|
|
165
|
+
res.setHeader('content-type', contentType);
|
|
166
|
+
res.setHeader('etag', etag);
|
|
167
|
+
res.writeHead(200);
|
|
168
|
+
if (req.method === 'HEAD') {
|
|
169
|
+
res.end();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const out = fs.createReadStream(file);
|
|
173
|
+
const ext = path.extname(file);
|
|
174
|
+
out.on('end', () => {
|
|
175
|
+
if (ext === '.html') {
|
|
176
|
+
res.end('<script type="module" src="__DEV__.js"></script>');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
res.end();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
out.pipe(res, { end: false });
|
|
164
183
|
}
|
|
165
184
|
}
|
|
166
185
|
async startWatching() {
|
|
167
186
|
this.fileWatcher = fs.watch(this.webroot, { recursive: true });
|
|
168
187
|
this.fileWatcher.on('change', (_, filename) => {
|
|
169
188
|
if (typeof filename === 'string') {
|
|
170
|
-
this.invalidateFile(path.join(this.webroot, filename));
|
|
171
189
|
this.broadcast({ type: 'change', url: filename });
|
|
172
190
|
}
|
|
173
191
|
});
|
|
@@ -183,13 +201,6 @@ export class DevServer {
|
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
|
-
async transformFileContents(file, contents) {
|
|
187
|
-
const ext = path.extname(file);
|
|
188
|
-
if (ext === '.html') {
|
|
189
|
-
return `${contents}<script type="module" src="__DEV__.js"></script>`;
|
|
190
|
-
}
|
|
191
|
-
return contents;
|
|
192
|
-
}
|
|
193
204
|
getClientScript() {
|
|
194
205
|
return `if (!navigator.webdriver) {
|
|
195
206
|
const wsProtocol = location.protocol === 'http:' ? 'ws://' : 'wss://';
|
|
@@ -275,36 +286,8 @@ export class DevServer {
|
|
|
275
286
|
return candidate.match(target.replace(/^\\.?\\//, ''));
|
|
276
287
|
}`;
|
|
277
288
|
}
|
|
278
|
-
async
|
|
279
|
-
|
|
280
|
-
if (cached)
|
|
281
|
-
return cached;
|
|
282
|
-
const promise = this.resolveFile(file);
|
|
283
|
-
this.fileCache.set(file, promise);
|
|
284
|
-
return promise;
|
|
285
|
-
}
|
|
286
|
-
async resolveFile(file) {
|
|
287
|
-
if (file.match(/__DEV__\.js$/)) {
|
|
288
|
-
return {
|
|
289
|
-
contents: this.getClientScript(),
|
|
290
|
-
contentType: 'application/javascript',
|
|
291
|
-
version: '1',
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
const stat = await fs.promises.lstat(file);
|
|
295
|
-
if (stat.isDirectory()) {
|
|
296
|
-
file = path.join(file, 'index.html');
|
|
297
|
-
}
|
|
298
|
-
const buffer = await fs.promises.readFile(file);
|
|
299
|
-
const contents = await this.transformFileContents(file, buffer);
|
|
300
|
-
const contentType = mime.getType(file) ?? 'application/octet-stream';
|
|
301
|
-
const version = crypto.createHash('sha1').update(contents).digest('base64');
|
|
302
|
-
return { contents, contentType, version };
|
|
303
|
-
}
|
|
304
|
-
invalidateFile(file) {
|
|
305
|
-
this.fileCache.delete(file);
|
|
306
|
-
this.fileCache.delete(file.replace(/index\.html$/, ''));
|
|
307
|
-
this.fileCache.delete(file.replace(/\/index\.html$/, ''));
|
|
289
|
+
async stat(file) {
|
|
290
|
+
return fs.promises.stat(file).catch(() => null);
|
|
308
291
|
}
|
|
309
292
|
async close() {
|
|
310
293
|
this.server.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s4d",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Minimal web development server with live reload",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"local",
|
|
@@ -41,13 +41,14 @@
|
|
|
41
41
|
"ws": "^8.16.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^9.9.0",
|
|
44
45
|
"@playwright/test": "^1.44.1",
|
|
46
|
+
"@types/eslint__js": "^8.42.3",
|
|
45
47
|
"@types/node": "^20.11.30",
|
|
46
48
|
"@types/ws": "^8.5.10",
|
|
47
|
-
"
|
|
48
|
-
"@typescript-eslint/parser": "^7.7.1",
|
|
49
|
-
"eslint": "^8.57.0",
|
|
49
|
+
"eslint": "^9.9.0",
|
|
50
50
|
"prettier": "^3.3.1",
|
|
51
|
-
"typescript": "^5.4.5"
|
|
51
|
+
"typescript": "^5.4.5",
|
|
52
|
+
"typescript-eslint": "^8.2.0"
|
|
52
53
|
}
|
|
53
54
|
}
|