wurzel 0.0.15 → 0.0.18
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/bin/wurzel.js +2 -2
- package/dist/lib/index.js +101 -41
- package/dist/samples/express/main.js +2 -2
- package/package.json +11 -7
package/dist/bin/wurzel.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import express from "express";
|
|
4
|
-
import { expressRouter
|
|
4
|
+
import { expressRouter } from "../lib/index.js";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
7
|
const app = express();
|
|
8
8
|
|
|
9
9
|
const rootFolder = path.resolve(".");
|
|
10
10
|
|
|
11
|
-
app.use("/",
|
|
11
|
+
app.use("/", expressRouter({ express, baseFolder: rootFolder }));
|
|
12
12
|
|
|
13
13
|
const port = 8080;
|
|
14
14
|
|
package/dist/lib/index.js
CHANGED
|
@@ -11,9 +11,8 @@ import {
|
|
|
11
11
|
import { readFile } from "node:fs/promises";
|
|
12
12
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import { transpileCode } from "commentscript";
|
|
14
|
+
import * as importMetaResolve from "import-meta-resolve";
|
|
15
|
+
import tsBlankSpace from "ts-blank-space";
|
|
17
16
|
import { LRUCache } from "lru-cache";
|
|
18
17
|
/*import type Express from "express";*/
|
|
19
18
|
|
|
@@ -41,7 +40,7 @@ const defaultResolveImportPath/*: TResolveImportPathFunc*/ = async ({ importer,
|
|
|
41
40
|
let resolvedUrl/*: string | undefined*/ = undefined;
|
|
42
41
|
|
|
43
42
|
try {
|
|
44
|
-
resolvedUrl =
|
|
43
|
+
resolvedUrl = importMetaResolve.resolve(specifier, parentUrl.toString());
|
|
45
44
|
} catch (error) {
|
|
46
45
|
return {
|
|
47
46
|
error: error /*as Error*/
|
|
@@ -67,6 +66,40 @@ const defaultResolveImportPath/*: TResolveImportPathFunc*/ = async ({ importer,
|
|
|
67
66
|
};
|
|
68
67
|
};
|
|
69
68
|
|
|
69
|
+
// es6-debug-server rejects uris containing "//" or "..", and a file path must not contain null bytes,
|
|
70
|
+
// such uris are answered right away, whatever else es6-debug-server rejects is caught in serveScript
|
|
71
|
+
const isMalformedUri = ({ uri }/*: { uri: string }*/) => {
|
|
72
|
+
return uri.includes("//") || uri.split("/").includes("..") || uri.includes("\0");
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const decodePath = ({ path }/*: { path: string }*/) => {
|
|
76
|
+
try {
|
|
77
|
+
return decodeURIComponent(path);
|
|
78
|
+
} catch (ex) {
|
|
79
|
+
const error/*: Error & { status?: number }*/ = Error(`failed to decode path "${path}"`, { cause: ex });
|
|
80
|
+
// express answers errors with their status
|
|
81
|
+
// eslint-disable-next-line immutable/no-mutation
|
|
82
|
+
error.status = 400;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const encodePath = ({ path }/*: { path: string }*/) => {
|
|
88
|
+
return path.split("/").map((segment) => {
|
|
89
|
+
return encodeURIComponent(segment);
|
|
90
|
+
}).join("/");
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const queryOf = ({ url }/*: { url: string }*/) => {
|
|
94
|
+
const queryStart = url.indexOf("?");
|
|
95
|
+
return queryStart < 0 ? "" : url.substring(queryStart);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ENOTDIR means that a parent folder of the path is a file, so there is no such file either
|
|
99
|
+
const isFileNotFoundError = ({ error }/*: { error: NodeJS.ErrnoException }*/) => {
|
|
100
|
+
return error.code === "ENOENT" || error.code === "ENOTDIR";
|
|
101
|
+
};
|
|
102
|
+
|
|
70
103
|
const defaultScriptFileEndings = [".js", ".ts", ".cjs", ".mjs", ".cts", ".mts"];
|
|
71
104
|
|
|
72
105
|
const defaultDetermineFileTypeByPath = ({ filePath }/*: { filePath: string }*/)/*: TFileType*/ => {
|
|
@@ -99,7 +132,6 @@ const expressRouter = ({
|
|
|
99
132
|
maxTranspileCacheSize?: number,
|
|
100
133
|
|
|
101
134
|
analyzeCode?: TCodeAnalyzeFunc,
|
|
102
|
-
// eslint-disable-next-line no-unused-vars
|
|
103
135
|
determineFileTypeByPath?: (args: { filePath: string }) => TFileType,
|
|
104
136
|
resolveImportPath?: TResolveImportPathFunc
|
|
105
137
|
// eslint-disable-next-line complexity
|
|
@@ -107,22 +139,26 @@ const expressRouter = ({
|
|
|
107
139
|
|
|
108
140
|
const router = express.Router();
|
|
109
141
|
|
|
142
|
+
// eslint-disable-next-line k13-engineering/no-new
|
|
110
143
|
const transpileCache = new LRUCache/*<string, string>*/({
|
|
111
144
|
maxSize: maxTranspileCacheSize,
|
|
145
|
+
// eslint-disable-next-line k13-engineering/prefer-single-object-parameters
|
|
112
146
|
sizeCalculation: (value, key) => {
|
|
113
147
|
return key.length + value.length;
|
|
114
148
|
}
|
|
115
149
|
});
|
|
116
150
|
|
|
151
|
+
// eslint-disable-next-line k13-engineering/no-new
|
|
117
152
|
const analyzeCache = new LRUCache/*<string, ICodeAnalyzeResult>*/({
|
|
118
153
|
maxSize: maxAnalyzeCacheSize,
|
|
154
|
+
// eslint-disable-next-line k13-engineering/prefer-single-object-parameters
|
|
119
155
|
sizeCalculation: (value, key) => {
|
|
120
156
|
return key.length + JSON.stringify(value).length;
|
|
121
157
|
}
|
|
122
158
|
});
|
|
123
159
|
|
|
124
160
|
// eslint-disable-next-line complexity
|
|
125
|
-
const maybeTranspile =
|
|
161
|
+
const maybeTranspile = ({ filePath, code }/*: { filePath: string, code: string }*/)/*: TTranspileResult*/ => {
|
|
126
162
|
if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) {
|
|
127
163
|
return {
|
|
128
164
|
error: undefined,
|
|
@@ -144,18 +180,17 @@ const expressRouter = ({
|
|
|
144
180
|
let transpiled/*: string | undefined*/ = undefined;
|
|
145
181
|
|
|
146
182
|
try {
|
|
147
|
-
|
|
148
|
-
transpiled =
|
|
183
|
+
// ts-blank-space reports syntax that cannot simply be blanked out, e.g. enums, instead of failing
|
|
184
|
+
transpiled = tsBlankSpace(code, (node) => {
|
|
185
|
+
const syntax = code.substring(node.pos, node.end).trim();
|
|
186
|
+
throw Error(`unsupported TypeScript syntax "${syntax}"`);
|
|
187
|
+
});
|
|
149
188
|
} catch (ex) {
|
|
150
189
|
return {
|
|
151
190
|
error: ex /*as Error*/,
|
|
152
191
|
};
|
|
153
192
|
}
|
|
154
193
|
|
|
155
|
-
if (transpiled === undefined) {
|
|
156
|
-
throw Error("BUG: transpiled code is undefined");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
194
|
transpileCache.set(hash, transpiled);
|
|
160
195
|
|
|
161
196
|
return {
|
|
@@ -168,6 +203,13 @@ const expressRouter = ({
|
|
|
168
203
|
|
|
169
204
|
scriptRootFolder: baseFolder,
|
|
170
205
|
|
|
206
|
+
// es6-debug-server serves only the scripts in the script root and what they import, which files are
|
|
207
|
+
// scripts has to agree with the routing below, or scripts classified by a custom determineFileTypeByPath
|
|
208
|
+
// are routed to es6-debug-server but not served
|
|
209
|
+
isScriptFile: ({ filePath }) => {
|
|
210
|
+
return determineFileTypeByPath({ filePath }) === "script";
|
|
211
|
+
},
|
|
212
|
+
|
|
171
213
|
tryReadScriptAsString: async ({ filePath }) => {
|
|
172
214
|
|
|
173
215
|
const { error: readError, content } = await readFile(filePath, "utf8").then((fileContent) => {
|
|
@@ -177,7 +219,7 @@ const expressRouter = ({
|
|
|
177
219
|
});
|
|
178
220
|
|
|
179
221
|
if (readError !== undefined) {
|
|
180
|
-
if (readError
|
|
222
|
+
if (isFileNotFoundError({ error: readError })) {
|
|
181
223
|
|
|
182
224
|
return {
|
|
183
225
|
error: createReadError({
|
|
@@ -197,7 +239,7 @@ const expressRouter = ({
|
|
|
197
239
|
};
|
|
198
240
|
}
|
|
199
241
|
|
|
200
|
-
const { error: transpileError, transpiled } =
|
|
242
|
+
const { error: transpileError, transpiled } = maybeTranspile({
|
|
201
243
|
filePath,
|
|
202
244
|
code: content
|
|
203
245
|
});
|
|
@@ -244,45 +286,63 @@ const expressRouter = ({
|
|
|
244
286
|
resolveImportPath
|
|
245
287
|
});
|
|
246
288
|
|
|
289
|
+
const serveScript = ({ filePath, req, res }/*: { filePath: string, req: Express.Request, res: Express.Response }*/) => {
|
|
290
|
+
|
|
291
|
+
if (isMalformedUri({ uri: filePath })) {
|
|
292
|
+
res.status(400).end("bad request");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// up to es6-debug-server 0.0.15, handleRequest is declared as returning unknown, not as a promise
|
|
297
|
+
Promise.resolve(server.handleRequest({
|
|
298
|
+
uri: filePath,
|
|
299
|
+
|
|
300
|
+
// a relative redirect is resolved by the client against the url it requested, which keeps it below
|
|
301
|
+
// wherever the router is reachable, even below a path prefix of a reverse proxy the router cannot see
|
|
302
|
+
handleRedirect: ({ relativeUri }) => {
|
|
303
|
+
const redirectLocation = `${encodePath({ path: relativeUri })}${queryOf({ url: req.url })}`;
|
|
304
|
+
res.redirect(redirectLocation);
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
handleContent: ({ contentType, content }) => {
|
|
308
|
+
res.writeHead(200, {
|
|
309
|
+
"Content-Type": contentType
|
|
310
|
+
});
|
|
311
|
+
res.end(content);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
handleFileNotFound: () => {
|
|
315
|
+
res.status(404).end("not found");
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
handleInternalError: ({ error }) => {
|
|
319
|
+
console.error(error);
|
|
320
|
+
res.status(500).end("internal server error");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// es6-debug-server rejects a uri it takes for malformed, e.g. one with a ".." segment between backslashes,
|
|
324
|
+
// unanswered that rejection would take down the whole process
|
|
325
|
+
})).catch(() => {
|
|
326
|
+
res.status(400).end("bad request");
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// eslint-disable-next-line k13-engineering/prefer-single-object-parameters
|
|
247
331
|
router.use((req, res, next) => {
|
|
248
332
|
|
|
249
333
|
if (req.method === "HEAD") {
|
|
250
334
|
throw Error("HEAD not supported yet");
|
|
251
335
|
}
|
|
252
336
|
|
|
253
|
-
const
|
|
337
|
+
const filePath = decodePath({ path: req.path });
|
|
338
|
+
const fileType = determineFileTypeByPath({ filePath });
|
|
254
339
|
|
|
255
340
|
if (fileType === "script-resource") {
|
|
256
341
|
throw Error(`file type ${fileType} is not supported yet`);
|
|
257
342
|
}
|
|
258
343
|
|
|
259
344
|
if (fileType === "script") {
|
|
260
|
-
|
|
261
|
-
server.handleRequest({
|
|
262
|
-
uri: req.url,
|
|
263
|
-
|
|
264
|
-
handleRedirect: ({ uri }) => {
|
|
265
|
-
const redirectLocation = `${req.baseUrl}${uri}`;
|
|
266
|
-
res.redirect(redirectLocation);
|
|
267
|
-
},
|
|
268
|
-
|
|
269
|
-
handleContent: ({ contentType, content }) => {
|
|
270
|
-
res.writeHead(200, {
|
|
271
|
-
"Content-Type": contentType
|
|
272
|
-
});
|
|
273
|
-
res.end(content);
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
handleFileNotFound: () => {
|
|
277
|
-
res.status(404).end("not found");
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
handleInternalError: ({ error }) => {
|
|
281
|
-
console.error(error);
|
|
282
|
-
res.status(500).end("internal server error");
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
345
|
+
serveScript({ filePath, req, res });
|
|
286
346
|
return;
|
|
287
347
|
}
|
|
288
348
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
expressRouter
|
|
2
|
+
expressRouter,
|
|
3
3
|
defaultResolveImportPath,
|
|
4
4
|
/*type TResolveImportPathFunc
|
|
5
5
|
*/} from "../../lib/index.js";
|
|
@@ -27,7 +27,7 @@ const resolveImportPath/*: TResolveImportPathFunc*/ = async ({ importer, specifi
|
|
|
27
27
|
|
|
28
28
|
const app = express();
|
|
29
29
|
|
|
30
|
-
app.use("/",
|
|
30
|
+
app.use("/", expressRouter({
|
|
31
31
|
express,
|
|
32
32
|
baseFolder,
|
|
33
33
|
resolveImportPath
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.18",
|
|
3
3
|
"name": "wurzel",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A tool to serve script files from different directories",
|
|
@@ -8,21 +8,25 @@
|
|
|
8
8
|
],
|
|
9
9
|
"main": "dist/lib/index.js",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"
|
|
12
|
-
"es6-debug-server": "^0.0.13",
|
|
11
|
+
"es6-debug-server": "^0.0.15",
|
|
13
12
|
"esm-resource": "^0.0.3",
|
|
14
13
|
"import-meta-resolve": "^4.0.0",
|
|
15
|
-
"lru-cache": "^10.2.1"
|
|
14
|
+
"lru-cache": "^10.2.1",
|
|
15
|
+
"ts-blank-space": "^0.9.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@eslint/js": "^10.0.1",
|
|
19
|
+
"@k13engineering/eslint-rules": "^0.0.6",
|
|
19
20
|
"@k13engineering/releasetool": "^0.0.5",
|
|
20
21
|
"@types/express": "^4.17.21",
|
|
21
|
-
"@types/
|
|
22
|
+
"@types/mocha": "^10.0.10",
|
|
23
|
+
"@types/node": "^25.6.0",
|
|
24
|
+
"c8": "^11.0.0",
|
|
22
25
|
"deno-node": "^0.0.12",
|
|
23
26
|
"express": "^5.2.1",
|
|
27
|
+
"mocha": "^12.0.2",
|
|
24
28
|
"npm-check-updates": "^20.0.2",
|
|
25
|
-
"typescript
|
|
29
|
+
"typescript": "^6.0.3"
|
|
26
30
|
},
|
|
27
31
|
"bin": {
|
|
28
32
|
"wurzel": "dist/bin/wurzel.js"
|
|
@@ -30,7 +34,7 @@
|
|
|
30
34
|
"scripts": {
|
|
31
35
|
"build": "rm -rf dist/ && deno-node-build --root . --out dist/ --entry lib/index.ts --entry bin/wurzel.ts --entry samples/express/main.ts && cp -r samples/express/frontend dist/samples/express/",
|
|
32
36
|
"lint": "eslint .",
|
|
33
|
-
"test": "
|
|
37
|
+
"test": "c8 --reporter lcov --reporter html --reporter text --all --src lib/ --exclude 'lib/**/*.spec.ts' mocha 'lib/**/*.spec.ts'",
|
|
34
38
|
"type-check": "tsc --noEmit",
|
|
35
39
|
"update-deps": "npm-check-updates -u"
|
|
36
40
|
},
|