s4d 0.1.1 → 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 -9
- package/dist/DevServer.js +52 -76
- package/package.json +6 -5
package/dist/DevServer.d.ts
CHANGED
|
@@ -15,7 +15,6 @@ export declare class DevServer {
|
|
|
15
15
|
protected server: http.Server;
|
|
16
16
|
protected webSocketServer: WebSocketServer;
|
|
17
17
|
protected fileWatcher?: fs.FSWatcher;
|
|
18
|
-
protected fileCache: Map<any, any>;
|
|
19
18
|
protected webSockets: Set<WebSocket>;
|
|
20
19
|
constructor(options: DevServerOptions);
|
|
21
20
|
static cli(argv: string[]): Promise<1 | undefined>;
|
|
@@ -26,14 +25,7 @@ export declare class DevServer {
|
|
|
26
25
|
handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
|
27
26
|
startWatching(): Promise<void>;
|
|
28
27
|
broadcast(message: Record<string, unknown>): void;
|
|
29
|
-
transformFileContents(file: string, contents: string | Buffer): Promise<string | Buffer>;
|
|
30
28
|
getClientScript(): string;
|
|
31
|
-
|
|
32
|
-
resolveFile(file: string): Promise<{
|
|
33
|
-
contents: string | Buffer;
|
|
34
|
-
contentType: string;
|
|
35
|
-
version: string;
|
|
36
|
-
}>;
|
|
37
|
-
invalidateFile(file: string): void;
|
|
29
|
+
stat(file: string): Promise<fs.Stats | null>;
|
|
38
30
|
close(): Promise<void>;
|
|
39
31
|
}
|
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,43 +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
|
-
|
|
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) {
|
|
133
133
|
res.writeHead(304);
|
|
134
134
|
res.end();
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
-
res.setHeader('content-type',
|
|
138
|
-
res.setHeader('etag',
|
|
137
|
+
res.setHeader('content-type', 'application/javascript');
|
|
138
|
+
res.setHeader('etag', etag);
|
|
139
139
|
res.writeHead(200);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
res.end(contents);
|
|
145
|
-
}
|
|
140
|
+
res.end(this.getClientScript());
|
|
141
|
+
return;
|
|
146
142
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 });
|
|
159
183
|
}
|
|
160
184
|
}
|
|
161
185
|
async startWatching() {
|
|
162
186
|
this.fileWatcher = fs.watch(this.webroot, { recursive: true });
|
|
163
187
|
this.fileWatcher.on('change', (_, filename) => {
|
|
164
188
|
if (typeof filename === 'string') {
|
|
165
|
-
this.invalidateFile(path.join(this.webroot, filename));
|
|
166
189
|
this.broadcast({ type: 'change', url: filename });
|
|
167
190
|
}
|
|
168
191
|
});
|
|
@@ -178,13 +201,6 @@ export class DevServer {
|
|
|
178
201
|
}
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
|
-
async transformFileContents(file, contents) {
|
|
182
|
-
const ext = path.extname(file);
|
|
183
|
-
if (ext === '.html') {
|
|
184
|
-
return `${contents}<script type="module" src="__DEV__.js"></script>`;
|
|
185
|
-
}
|
|
186
|
-
return contents;
|
|
187
|
-
}
|
|
188
204
|
getClientScript() {
|
|
189
205
|
return `if (!navigator.webdriver) {
|
|
190
206
|
const wsProtocol = location.protocol === 'http:' ? 'ws://' : 'wss://';
|
|
@@ -270,48 +286,8 @@ export class DevServer {
|
|
|
270
286
|
return candidate.match(target.replace(/^\\.?\\//, ''));
|
|
271
287
|
}`;
|
|
272
288
|
}
|
|
273
|
-
async
|
|
274
|
-
|
|
275
|
-
if (cached)
|
|
276
|
-
return cached;
|
|
277
|
-
const promise = this.resolveFile(file);
|
|
278
|
-
this.fileCache.set(file, promise);
|
|
279
|
-
return promise;
|
|
280
|
-
}
|
|
281
|
-
async resolveFile(file) {
|
|
282
|
-
if (file.match(/__DEV__\.js$/)) {
|
|
283
|
-
return {
|
|
284
|
-
contents: this.getClientScript(),
|
|
285
|
-
contentType: 'application/javascript',
|
|
286
|
-
version: '1',
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
let contents;
|
|
290
|
-
try {
|
|
291
|
-
const stat = await fs.promises.lstat(file);
|
|
292
|
-
if (stat.isDirectory()) {
|
|
293
|
-
file = path.join(file, 'index.html');
|
|
294
|
-
}
|
|
295
|
-
contents = await fs.promises.readFile(file);
|
|
296
|
-
}
|
|
297
|
-
catch (err) {
|
|
298
|
-
if (this.spa && err.code === 'ENOENT') {
|
|
299
|
-
file = path.join(this.webroot, 'index.html');
|
|
300
|
-
contents = await fs.promises.readFile(file);
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
throw err;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
contents = await this.transformFileContents(file, contents);
|
|
307
|
-
const contentType = mime.getType(file) ?? 'application/octet-stream';
|
|
308
|
-
const version = crypto.createHash('sha1').update(contents).digest('base64');
|
|
309
|
-
return { contents, contentType, version };
|
|
310
|
-
}
|
|
311
|
-
invalidateFile(file) {
|
|
312
|
-
this.fileCache.delete(file);
|
|
313
|
-
this.fileCache.delete(file.replace(/index.html$/, ''));
|
|
314
|
-
this.fileCache.delete(file.replace(/\/index.html$/, ''));
|
|
289
|
+
async stat(file) {
|
|
290
|
+
return fs.promises.stat(file).catch(() => null);
|
|
315
291
|
}
|
|
316
292
|
async close() {
|
|
317
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
|
}
|