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.
@@ -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,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 { resolve as resolveImport } from "import-meta-resolve";
15
- // @ts-expect-error missing types
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 = resolveImport(specifier, parentUrl.toString());
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 = async ({ filePath, code }/*: { filePath: string, code: string }*/)/*: Promise<TTranspileResult>*/ => {
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
- const result = await transpileCode({ code });
148
- transpiled = result.transpiledCode;
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.code === "ENOENT") {
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 } = await maybeTranspile({
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 fileType = determineFileTypeByPath({ filePath: req.url });
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 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.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
- "commentscript": "^0.0.10",
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/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
  },