wurzel 0.0.15 → 0.0.16

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.
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import express from "express";
4
- import { expressRouter as wurzel } from "../lib/index.js";
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("/", wurzel({ express, baseFolder: rootFolder }));
11
+ app.use("/", expressRouter({ express, baseFolder: rootFolder }));
12
12
 
13
13
  const port = 8080;
14
14
 
package/dist/lib/index.js CHANGED
@@ -11,7 +11,7 @@ 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 { resolve as resolveImport } from "import-meta-resolve";
14
+ import * as importMetaResolve from "import-meta-resolve";
15
15
  // @ts-expect-error missing types
16
16
  import { transpileCode } from "commentscript";
17
17
  import { LRUCache } from "lru-cache";
@@ -41,7 +41,7 @@ const defaultResolveImportPath/*: TResolveImportPathFunc*/ = async ({ importer,
41
41
  let resolvedUrl/*: string | undefined*/ = undefined;
42
42
 
43
43
  try {
44
- resolvedUrl = resolveImport(specifier, parentUrl.toString());
44
+ resolvedUrl = importMetaResolve.resolve(specifier, parentUrl.toString());
45
45
  } catch (error) {
46
46
  return {
47
47
  error: error /*as Error*/
@@ -67,6 +67,40 @@ const defaultResolveImportPath/*: TResolveImportPathFunc*/ = async ({ importer,
67
67
  };
68
68
  };
69
69
 
70
+ // es6-debug-server rejects uris containing "//" or "..", and a file path must not contain null bytes,
71
+ // so such uris have to be answered before reaching es6-debug-server
72
+ const isMalformedUri = ({ uri }/*: { uri: string }*/) => {
73
+ return uri.includes("//") || uri.split("/").includes("..") || uri.includes("\0");
74
+ };
75
+
76
+ const decodePath = ({ path }/*: { path: string }*/) => {
77
+ try {
78
+ return decodeURIComponent(path);
79
+ } catch (ex) {
80
+ const error/*: Error & { status?: number }*/ = Error(`failed to decode path "${path}"`, { cause: ex });
81
+ // express answers errors with their status
82
+ // eslint-disable-next-line immutable/no-mutation
83
+ error.status = 400;
84
+ throw error;
85
+ }
86
+ };
87
+
88
+ const encodePath = ({ path }/*: { path: string }*/) => {
89
+ return path.split("/").map((segment) => {
90
+ return encodeURIComponent(segment);
91
+ }).join("/");
92
+ };
93
+
94
+ const queryOf = ({ url }/*: { url: string }*/) => {
95
+ const queryStart = url.indexOf("?");
96
+ return queryStart < 0 ? "" : url.substring(queryStart);
97
+ };
98
+
99
+ // ENOTDIR means that a parent folder of the path is a file, so there is no such file either
100
+ const isFileNotFoundError = ({ error }/*: { error: NodeJS.ErrnoException }*/) => {
101
+ return error.code === "ENOENT" || error.code === "ENOTDIR";
102
+ };
103
+
70
104
  const defaultScriptFileEndings = [".js", ".ts", ".cjs", ".mjs", ".cts", ".mts"];
71
105
 
72
106
  const defaultDetermineFileTypeByPath = ({ filePath }/*: { filePath: string }*/)/*: TFileType*/ => {
@@ -99,7 +133,6 @@ const expressRouter = ({
99
133
  maxTranspileCacheSize?: number,
100
134
 
101
135
  analyzeCode?: TCodeAnalyzeFunc,
102
- // eslint-disable-next-line no-unused-vars
103
136
  determineFileTypeByPath?: (args: { filePath: string }) => TFileType,
104
137
  resolveImportPath?: TResolveImportPathFunc
105
138
  // eslint-disable-next-line complexity
@@ -107,15 +140,19 @@ const expressRouter = ({
107
140
 
108
141
  const router = express.Router();
109
142
 
143
+ // eslint-disable-next-line k13-engineering/no-new
110
144
  const transpileCache = new LRUCache/*<string, string>*/({
111
145
  maxSize: maxTranspileCacheSize,
146
+ // eslint-disable-next-line k13-engineering/prefer-single-object-parameters
112
147
  sizeCalculation: (value, key) => {
113
148
  return key.length + value.length;
114
149
  }
115
150
  });
116
151
 
152
+ // eslint-disable-next-line k13-engineering/no-new
117
153
  const analyzeCache = new LRUCache/*<string, ICodeAnalyzeResult>*/({
118
154
  maxSize: maxAnalyzeCacheSize,
155
+ // eslint-disable-next-line k13-engineering/prefer-single-object-parameters
119
156
  sizeCalculation: (value, key) => {
120
157
  return key.length + JSON.stringify(value).length;
121
158
  }
@@ -177,7 +214,7 @@ const expressRouter = ({
177
214
  });
178
215
 
179
216
  if (readError !== undefined) {
180
- if (readError.code === "ENOENT") {
217
+ if (isFileNotFoundError({ error: readError })) {
181
218
 
182
219
  return {
183
220
  error: createReadError({
@@ -244,45 +281,57 @@ const expressRouter = ({
244
281
  resolveImportPath
245
282
  });
246
283
 
284
+ const serveScript = ({ filePath, req, res }/*: { filePath: string, req: Express.Request, res: Express.Response }*/) => {
285
+
286
+ if (isMalformedUri({ uri: filePath })) {
287
+ res.status(400).end("bad request");
288
+ return;
289
+ }
290
+
291
+ server.handleRequest({
292
+ uri: filePath,
293
+
294
+ // a relative redirect is resolved by the client against the url it requested, which keeps it below
295
+ // wherever the router is reachable, even below a path prefix of a reverse proxy the router cannot see
296
+ handleRedirect: ({ relativeUri }) => {
297
+ const redirectLocation = `${encodePath({ path: relativeUri })}${queryOf({ url: req.url })}`;
298
+ res.redirect(redirectLocation);
299
+ },
300
+
301
+ handleContent: ({ contentType, content }) => {
302
+ res.writeHead(200, {
303
+ "Content-Type": contentType
304
+ });
305
+ res.end(content);
306
+ },
307
+
308
+ handleFileNotFound: () => {
309
+ res.status(404).end("not found");
310
+ },
311
+
312
+ handleInternalError: ({ error }) => {
313
+ console.error(error);
314
+ res.status(500).end("internal server error");
315
+ }
316
+ });
317
+ };
318
+
319
+ // eslint-disable-next-line k13-engineering/prefer-single-object-parameters
247
320
  router.use((req, res, next) => {
248
321
 
249
322
  if (req.method === "HEAD") {
250
323
  throw Error("HEAD not supported yet");
251
324
  }
252
325
 
253
- const fileType = determineFileTypeByPath({ filePath: req.url });
326
+ const filePath = decodePath({ path: req.path });
327
+ const fileType = determineFileTypeByPath({ filePath });
254
328
 
255
329
  if (fileType === "script-resource") {
256
330
  throw Error(`file type ${fileType} is not supported yet`);
257
331
  }
258
332
 
259
333
  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
-
334
+ serveScript({ filePath, req, res });
286
335
  return;
287
336
  }
288
337
 
@@ -1,5 +1,5 @@
1
1
  import {
2
- expressRouter as wurzelExpressRouter,
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("/", wurzelExpressRouter({
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.15",
2
+ "version": "0.0.16",
3
3
  "name": "wurzel",
4
4
  "type": "module",
5
5
  "description": "A tool to serve script files from different directories",
@@ -9,20 +9,24 @@
9
9
  "main": "dist/lib/index.js",
10
10
  "dependencies": {
11
11
  "commentscript": "^0.0.10",
12
- "es6-debug-server": "^0.0.13",
12
+ "es6-debug-server": "^0.0.14",
13
13
  "esm-resource": "^0.0.3",
14
14
  "import-meta-resolve": "^4.0.0",
15
15
  "lru-cache": "^10.2.1"
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/node": "^20.12.7",
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-eslint": "^8.58.2"
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": "true",
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
  },