@jsenv/core 38.4.16 → 38.4.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,5 +1,11 @@
1
- import { readFileSync, realpathSync, statSync } from "node:fs";
2
- import { serveDirectory } from "@jsenv/server";
1
+ import {
2
+ existsSync,
3
+ readFileSync,
4
+ realpathSync,
5
+ statSync,
6
+ lstatSync,
7
+ readdirSync,
8
+ } from "node:fs";
3
9
  import { pathToFileURL } from "node:url";
4
10
  import {
5
11
  urlIsInsideOf,
@@ -11,7 +17,19 @@ import {
11
17
  applyFileSystemMagicResolution,
12
18
  getExtensionsToTry,
13
19
  } from "@jsenv/node-esm-resolution";
20
+ import { pickContentType } from "@jsenv/server";
14
21
  import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
22
+ import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem";
23
+
24
+ const html404AndParentDirIsEmptyFileUrl = new URL(
25
+ "./html_404_and_parent_dir_is_empty.html",
26
+ import.meta.url,
27
+ );
28
+ const html404AndParentDirFileUrl = new URL(
29
+ "./html_404_and_parent_dir.html",
30
+ import.meta.url,
31
+ );
32
+ const htmlFileUrlForDirectory = new URL("./directory.html", import.meta.url);
15
33
 
16
34
  export const jsenvPluginProtocolFile = ({
17
35
  magicExtensions = ["inherit", ".js"],
@@ -99,7 +117,9 @@ export const jsenvPluginProtocolFile = ({
99
117
  reference.leadsToADirectory = stat && stat.isDirectory();
100
118
  if (reference.leadsToADirectory) {
101
119
  let actionForDirectory;
102
- if (
120
+ if (reference.type === "a_href") {
121
+ actionForDirectory = "ignore";
122
+ } else if (
103
123
  reference.type === "http_request" ||
104
124
  reference.type === "filesystem"
105
125
  ) {
@@ -184,17 +204,33 @@ export const jsenvPluginProtocolFile = ({
184
204
  urlInfo.filenameHint = `${urlToFilename(urlInfo.url)}/`;
185
205
  }
186
206
  }
187
- const { headers, body } = serveDirectory(urlObject.href, {
188
- headers: urlInfo.context.request
189
- ? urlInfo.context.request.headers
190
- : {},
191
- rootDirectoryUrl: urlInfo.context.rootDirectoryUrl,
192
- });
207
+ const directoryContentArray = readdirSync(urlObject);
208
+ if (urlInfo.firstReference.type === "filesystem") {
209
+ const content = JSON.stringify(directoryContentArray, null, " ");
210
+ return {
211
+ type: "directory",
212
+ contentType: "application/json",
213
+ content,
214
+ };
215
+ }
216
+ const acceptsHtml = urlInfo.context.request
217
+ ? pickContentType(urlInfo.context.request, ["text/html"])
218
+ : false;
219
+ if (acceptsHtml) {
220
+ const html = generateHtmlForDirectory(
221
+ urlObject.href,
222
+ directoryContentArray,
223
+ urlInfo.context.rootDirectoryUrl,
224
+ );
225
+ return {
226
+ contentType: "text/html",
227
+ content: html,
228
+ };
229
+ }
193
230
  return {
194
231
  type: "directory",
195
- contentType: headers["content-type"],
196
- contentLength: headers["content-length"],
197
- content: body,
232
+ contentType: "application/json",
233
+ content: JSON.stringify(directoryContentArray, null, " "),
198
234
  };
199
235
  }
200
236
  if (
@@ -204,20 +240,144 @@ export const jsenvPluginProtocolFile = ({
204
240
  urlInfo.dirnameHint =
205
241
  urlInfo.firstReference.ownerUrlInfo.filenameHint;
206
242
  }
207
- const fileBuffer = readFileSync(urlObject);
208
243
  const contentType = CONTENT_TYPE.fromUrlExtension(urlInfo.url);
244
+ if (contentType === "text/html") {
245
+ try {
246
+ const fileBuffer = readFileSync(urlObject);
247
+ const content = String(fileBuffer);
248
+ return {
249
+ content,
250
+ contentType,
251
+ contentLength: fileBuffer.length,
252
+ };
253
+ } catch (e) {
254
+ if (e.code !== "ENOENT") {
255
+ throw e;
256
+ }
257
+ const parentDirectoryUrl = new URL("./", urlInfo.url);
258
+ if (!existsSync(parentDirectoryUrl)) {
259
+ throw e;
260
+ }
261
+ const parentDirectoryContentArray = readdirSync(
262
+ new URL(parentDirectoryUrl),
263
+ );
264
+ const html = generateHtmlForENOENTOnHtmlFile(
265
+ urlInfo.url,
266
+ parentDirectoryContentArray,
267
+ parentDirectoryUrl,
268
+ urlInfo.context.rootDirectoryUrl,
269
+ );
270
+ return {
271
+ contentType: "text/html",
272
+ content: html,
273
+ };
274
+ }
275
+ }
276
+ const fileBuffer = readFileSync(urlObject);
209
277
  const content = CONTENT_TYPE.isTextual(contentType)
210
278
  ? String(fileBuffer)
211
279
  : fileBuffer;
212
280
  return {
213
281
  content,
214
282
  contentType,
283
+ contentLength: fileBuffer.length,
215
284
  };
216
285
  },
217
286
  },
218
287
  ];
219
288
  };
220
289
 
290
+ const generateHtmlForDirectory = (
291
+ directoryUrl,
292
+ directoryContentArray,
293
+ rootDirectoryUrl,
294
+ ) => {
295
+ directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
296
+ const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
297
+ const replacers = {
298
+ directoryRelativeUrl: urlToRelativeUrl(directoryUrl, rootDirectoryUrl),
299
+ directoryUrl,
300
+ directoryContent: () =>
301
+ generateDirectoryContent(
302
+ directoryContentArray,
303
+ directoryUrl,
304
+ rootDirectoryUrl,
305
+ ),
306
+ };
307
+ const html = replacePlaceholders(htmlForDirectory, replacers);
308
+ return html;
309
+ };
310
+ const generateHtmlForENOENTOnHtmlFile = (
311
+ url,
312
+ parentDirectoryContentArray,
313
+ parentDirectoryUrl,
314
+ rootDirectoryUrl,
315
+ ) => {
316
+ if (parentDirectoryContentArray.length === 0) {
317
+ const htmlFor404AndParentDirIsEmpty = String(
318
+ readFileSync(html404AndParentDirIsEmptyFileUrl),
319
+ );
320
+ return replacePlaceholders(htmlFor404AndParentDirIsEmpty, {
321
+ fileRelativeUrl: urlToRelativeUrl(url, rootDirectoryUrl),
322
+ parentDirectoryRelativeUrl: urlToRelativeUrl(
323
+ parentDirectoryUrl,
324
+ rootDirectoryUrl,
325
+ ),
326
+ });
327
+ }
328
+ const htmlFor404AndParentDir = String(
329
+ readFileSync(html404AndParentDirFileUrl),
330
+ );
331
+
332
+ const replacers = {
333
+ fileUrl: url,
334
+ fileRelativeUrl: urlToRelativeUrl(url, rootDirectoryUrl),
335
+ parentDirectoryUrl,
336
+ parentDirectoryRelativeUrl: urlToRelativeUrl(
337
+ parentDirectoryUrl,
338
+ rootDirectoryUrl,
339
+ ),
340
+ parentDirectoryContent: () =>
341
+ generateDirectoryContent(
342
+ parentDirectoryContentArray,
343
+ parentDirectoryUrl,
344
+ rootDirectoryUrl,
345
+ ),
346
+ };
347
+ const html = replacePlaceholders(htmlFor404AndParentDir, replacers);
348
+ return html;
349
+ };
350
+ const generateDirectoryContent = (
351
+ directoryContentArray,
352
+ directoryUrl,
353
+ rootDirectoryUrl,
354
+ ) => {
355
+ return directoryContentArray.map((filename) => {
356
+ const fileUrlObject = new URL(filename, directoryUrl);
357
+ const fileUrl = String(fileUrlObject);
358
+ let fileUrlRelative = urlToRelativeUrl(fileUrl, rootDirectoryUrl);
359
+ if (lstatSync(fileUrlObject).isDirectory()) {
360
+ fileUrlRelative += "/";
361
+ }
362
+ return `<li>
363
+ <a href="/${fileUrlRelative}">/${fileUrlRelative}</a>
364
+ </li>`;
365
+ }).join(`
366
+ `);
367
+ };
368
+ const replacePlaceholders = (html, replacers) => {
369
+ return html.replace(/\${([\w]+)}/g, (match, name) => {
370
+ const replacer = replacers[name];
371
+ if (replacer === undefined) {
372
+ return match;
373
+ }
374
+ if (typeof replacer === "function") {
375
+ return replacer();
376
+ }
377
+ return replacer;
378
+ });
379
+ };
380
+
221
381
  const resolveSymlink = (fileUrl) => {
222
382
  const urlObject = new URL(fileUrl);
223
383
  const realpath = realpathSync(urlObject);
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Syntax error in HTML</title>
5
+ <meta charset="utf-8" />
6
+ <link rel="icon" href="data:," />
7
+ </head>
8
+
9
+ <body>
10
+ <p>Syntax error: <strong>${reasonCode}</strong></p>
11
+ <a jsenv-ignore href="${errorLinkHref}">${errorLinkText}</a>
12
+ <pre>${syntaxError}</pre>
13
+ </body>
14
+ </html>