@jsenv/core 39.7.5 → 39.7.7

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.
@@ -195,10 +195,10 @@
195
195
  No entry on the filesystem for <code>/${fileRelativeUrl}</code> (at
196
196
  ${fileUrl})
197
197
  <br>
198
- Content of the parent directory is listed below:
198
+ Content of the ancestor directory is listed below:
199
199
  </span>
200
200
  </p>
201
- <h1 class="directory_nav">${parentDirectoryNav}</h1>
202
- ${parentDirectoryContent}
201
+ <h1 class="directory_nav">${ancestorDirectoryNav}</h1>
202
+ ${ancestorDirectoryContent}
203
203
  </body>
204
204
  </html>
@@ -3,7 +3,7 @@ import os, { networkInterfaces } from "node:os";
3
3
  import tty from "node:tty";
4
4
  import stringWidth from "string-width";
5
5
  import { pathToFileURL, fileURLToPath } from "node:url";
6
- import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFileSync, writeFileSync as writeFileSync$1, mkdirSync, unlink, openSync, closeSync, rmdir, watch, readdirSync, createReadStream, readFile, existsSync, realpathSync } from "node:fs";
6
+ import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFileSync, writeFileSync as writeFileSync$1, mkdirSync, unlink, openSync, closeSync, rmdir, unlinkSync, readdirSync, rmdirSync, watch, createReadStream, readFile, existsSync, realpathSync } from "node:fs";
7
7
  import { extname } from "node:path";
8
8
  import crypto, { createHash } from "node:crypto";
9
9
  import cluster from "node:cluster";
@@ -3668,7 +3668,7 @@ const removeEntry = async (
3668
3668
  sourceStats.isCharacterDevice() ||
3669
3669
  sourceStats.isBlockDevice()
3670
3670
  ) {
3671
- await removeNonDirectory(
3671
+ await removeNonDirectory$1(
3672
3672
  sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl,
3673
3673
  {
3674
3674
  maxRetries,
@@ -3689,7 +3689,7 @@ const removeEntry = async (
3689
3689
  }
3690
3690
  };
3691
3691
 
3692
- const removeNonDirectory = (sourceUrl, { maxRetries, retryDelay }) => {
3692
+ const removeNonDirectory$1 = (sourceUrl, { maxRetries, retryDelay }) => {
3693
3693
  const sourcePath = urlToFileSystemPath(sourceUrl);
3694
3694
 
3695
3695
  let retryCount = 0;
@@ -3828,11 +3828,11 @@ const removeDirectory = async (
3828
3828
  };
3829
3829
 
3830
3830
  const visitFile = async (fileUrl) => {
3831
- await removeNonDirectory(fileUrl, { maxRetries, retryDelay });
3831
+ await removeNonDirectory$1(fileUrl, { maxRetries, retryDelay });
3832
3832
  };
3833
3833
 
3834
3834
  const visitSymbolicLink = async (symbolicLinkUrl) => {
3835
- await removeNonDirectory(symbolicLinkUrl, { maxRetries, retryDelay });
3835
+ await removeNonDirectory$1(symbolicLinkUrl, { maxRetries, retryDelay });
3836
3836
  };
3837
3837
 
3838
3838
  try {
@@ -3877,6 +3877,204 @@ const removeDirectoryNaive = (
3877
3877
 
3878
3878
  process.platform === "win32";
3879
3879
 
3880
+ const removeEntrySync = (
3881
+ source,
3882
+ {
3883
+ allowUseless = false,
3884
+ recursive = false,
3885
+ maxRetries = 3,
3886
+ retryDelay = 100,
3887
+ onlyContent = false,
3888
+ } = {},
3889
+ ) => {
3890
+ const sourceUrl = assertAndNormalizeFileUrl(source);
3891
+ const sourceStats = readEntryStatSync(sourceUrl, {
3892
+ nullIfNotFound: true,
3893
+ followLink: false,
3894
+ });
3895
+ if (!sourceStats) {
3896
+ if (allowUseless) {
3897
+ return;
3898
+ }
3899
+ throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`);
3900
+ }
3901
+
3902
+ // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats
3903
+ // FIFO and socket are ignored, not sure what they are exactly and what to do with them
3904
+ // other libraries ignore them, let's do the same.
3905
+ if (
3906
+ sourceStats.isFile() ||
3907
+ sourceStats.isSymbolicLink() ||
3908
+ sourceStats.isCharacterDevice() ||
3909
+ sourceStats.isBlockDevice()
3910
+ ) {
3911
+ removeNonDirectory(
3912
+ sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl);
3913
+ } else if (sourceStats.isDirectory()) {
3914
+ const directoryUrl = ensurePathnameTrailingSlash(sourceUrl);
3915
+ removeDirectorySync$1(directoryUrl, {
3916
+ recursive,
3917
+ maxRetries,
3918
+ retryDelay,
3919
+ onlyContent,
3920
+ });
3921
+ }
3922
+ };
3923
+
3924
+ const removeNonDirectory = (sourceUrl) => {
3925
+ const sourcePath = urlToFileSystemPath(sourceUrl);
3926
+ const attempt = () => {
3927
+ unlinkSyncNaive(sourcePath);
3928
+ };
3929
+ attempt();
3930
+ };
3931
+
3932
+ const unlinkSyncNaive = (sourcePath, { handleTemporaryError = null } = {}) => {
3933
+ try {
3934
+ unlinkSync(sourcePath);
3935
+ } catch (error) {
3936
+ if (error.code === "ENOENT") {
3937
+ return;
3938
+ }
3939
+ if (
3940
+ handleTemporaryError &&
3941
+ (error.code === "EBUSY" ||
3942
+ error.code === "EMFILE" ||
3943
+ error.code === "ENFILE" ||
3944
+ error.code === "ENOENT")
3945
+ ) {
3946
+ handleTemporaryError(error);
3947
+ return;
3948
+ }
3949
+ throw error;
3950
+ }
3951
+ };
3952
+
3953
+ const removeDirectorySync$1 = (
3954
+ rootDirectoryUrl,
3955
+ { maxRetries, retryDelay, recursive, onlyContent },
3956
+ ) => {
3957
+ const visit = (sourceUrl) => {
3958
+ const sourceStats = readEntryStatSync(sourceUrl, {
3959
+ nullIfNotFound: true,
3960
+ followLink: false,
3961
+ });
3962
+
3963
+ // file/directory not found
3964
+ if (sourceStats === null) {
3965
+ return;
3966
+ }
3967
+
3968
+ if (
3969
+ sourceStats.isFile() ||
3970
+ sourceStats.isCharacterDevice() ||
3971
+ sourceStats.isBlockDevice()
3972
+ ) {
3973
+ visitFile(sourceUrl);
3974
+ } else if (sourceStats.isSymbolicLink()) {
3975
+ visitSymbolicLink(sourceUrl);
3976
+ } else if (sourceStats.isDirectory()) {
3977
+ visitDirectory(`${sourceUrl}/`);
3978
+ }
3979
+ };
3980
+
3981
+ const visitDirectory = (directoryUrl) => {
3982
+ const directoryPath = urlToFileSystemPath(directoryUrl);
3983
+ const optionsFromRecursive = recursive
3984
+ ? {
3985
+ handleNotEmptyError: () => {
3986
+ removeDirectoryContent(directoryUrl);
3987
+ visitDirectory(directoryUrl);
3988
+ },
3989
+ }
3990
+ : {};
3991
+ removeDirectorySyncNaive(directoryPath, {
3992
+ ...optionsFromRecursive,
3993
+ // Workaround for https://github.com/joyent/node/issues/4337
3994
+ ...(process.platform === "win32"
3995
+ ? {
3996
+ handlePermissionError: (error) => {
3997
+ console.error(
3998
+ `trying to fix windows EPERM after readir on ${directoryPath}`,
3999
+ );
4000
+
4001
+ let openOrCloseError;
4002
+ try {
4003
+ const fd = openSync(directoryPath);
4004
+ closeSync(fd);
4005
+ } catch (e) {
4006
+ openOrCloseError = e;
4007
+ }
4008
+
4009
+ if (openOrCloseError) {
4010
+ if (openOrCloseError.code === "ENOENT") {
4011
+ return;
4012
+ }
4013
+ console.error(
4014
+ `error while trying to fix windows EPERM after readir on ${directoryPath}: ${openOrCloseError.stack}`,
4015
+ );
4016
+ throw error;
4017
+ }
4018
+ removeDirectorySyncNaive(directoryPath, {
4019
+ ...optionsFromRecursive,
4020
+ });
4021
+ },
4022
+ }
4023
+ : {}),
4024
+ });
4025
+ };
4026
+
4027
+ const removeDirectoryContent = (directoryUrl) => {
4028
+ const entryNames = readdirSync(new URL(directoryUrl));
4029
+ for (const entryName of entryNames) {
4030
+ const url = resolveUrl$1(entryName, directoryUrl);
4031
+ visit(url);
4032
+ }
4033
+ };
4034
+
4035
+ const visitFile = (fileUrl) => {
4036
+ removeNonDirectory(fileUrl);
4037
+ };
4038
+
4039
+ const visitSymbolicLink = (symbolicLinkUrl) => {
4040
+ removeNonDirectory(symbolicLinkUrl);
4041
+ };
4042
+
4043
+ if (onlyContent) {
4044
+ removeDirectoryContent(rootDirectoryUrl);
4045
+ } else {
4046
+ visitDirectory(rootDirectoryUrl);
4047
+ }
4048
+ };
4049
+
4050
+ const removeDirectorySyncNaive = (
4051
+ directoryPath,
4052
+ { handleNotEmptyError = null, handlePermissionError = null } = {},
4053
+ ) => {
4054
+ try {
4055
+ rmdirSync(directoryPath);
4056
+ } catch (error) {
4057
+ if (handlePermissionError && error.code === "EPERM") {
4058
+ handlePermissionError(error);
4059
+ return;
4060
+ }
4061
+ if (error.code === "ENOENT") {
4062
+ return;
4063
+ }
4064
+ if (
4065
+ handleNotEmptyError &&
4066
+ // linux os
4067
+ (error.code === "ENOTEMPTY" ||
4068
+ // SunOS
4069
+ error.code === "EEXIST")
4070
+ ) {
4071
+ handleNotEmptyError(error);
4072
+ return;
4073
+ }
4074
+ throw error;
4075
+ }
4076
+ };
4077
+
3880
4078
  process.platform === "win32";
3881
4079
 
3882
4080
  const ensureEmptyDirectory = async (source) => {
@@ -3906,6 +4104,13 @@ const ensureEmptyDirectory = async (source) => {
3906
4104
  );
3907
4105
  };
3908
4106
 
4107
+ const removeDirectorySync = (url, options = {}) => {
4108
+ return removeEntrySync(url, {
4109
+ ...options,
4110
+ recursive: true,
4111
+ });
4112
+ };
4113
+
3909
4114
  const callOnceIdlePerFile = (callback, idleMs) => {
3910
4115
  const timeoutIdMap = new Map();
3911
4116
  return (fileEvent) => {
@@ -14163,7 +14368,18 @@ const createUrlInfoTransformer = ({
14163
14368
  contentIsInlined = false;
14164
14369
  }
14165
14370
  if (!contentIsInlined) {
14166
- writeFileSync(new URL(generatedUrl), urlInfo.content);
14371
+ try {
14372
+ writeFileSync(new URL(generatedUrl), urlInfo.content);
14373
+ } catch (e) {
14374
+ if (e.code === "EISDIR") {
14375
+ // happens when directory existed but got delete
14376
+ // we can safely remove that directory and write the new file
14377
+ removeDirectorySync(new URL(generatedUrl));
14378
+ writeFileSync(new URL(generatedUrl), urlInfo.content);
14379
+ } else {
14380
+ throw e;
14381
+ }
14382
+ }
14167
14383
  }
14168
14384
  const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
14169
14385
  if (sourcemapGeneratedUrl && sourcemapReference) {
@@ -18890,8 +19106,8 @@ const resolveSymlink = (fileUrl) => {
18890
19106
  return realUrlObject.href;
18891
19107
  };
18892
19108
 
18893
- const html404AndParentDirFileUrl = new URL(
18894
- "./html/html_404_and_parent_dir.html",
19109
+ const html404AndAncestorDirFileUrl = new URL(
19110
+ "./html/html_404_and_ancestor_dir.html",
18895
19111
  import.meta.url,
18896
19112
  );
18897
19113
  const htmlFileUrlForDirectory = new URL(
@@ -18959,68 +19175,86 @@ const jsenvPluginProtocolFile = ({
18959
19175
  if (!urlInfo.url.startsWith("file:")) {
18960
19176
  return null;
18961
19177
  }
18962
- const urlObject = new URL(urlInfo.url);
18963
- const { firstReference } = urlInfo;
18964
- if (firstReference.leadsToADirectory) {
18965
- const directoryContentArray = readdirSync(urlObject);
18966
- if (firstReference.type === "filesystem") {
18967
- const content = JSON.stringify(directoryContentArray, null, " ");
19178
+ const generateContent = () => {
19179
+ const urlObject = new URL(urlInfo.url);
19180
+ const { firstReference } = urlInfo;
19181
+ if (firstReference.leadsToADirectory) {
19182
+ const directoryContentArray = readdirSync(urlObject);
19183
+ if (firstReference.type === "filesystem") {
19184
+ const content = JSON.stringify(directoryContentArray, null, " ");
19185
+ return {
19186
+ type: "directory",
19187
+ contentType: "application/json",
19188
+ content,
19189
+ };
19190
+ }
19191
+ const acceptsHtml = urlInfo.context.request
19192
+ ? pickContentType(urlInfo.context.request, ["text/html"])
19193
+ : false;
19194
+ if (acceptsHtml) {
19195
+ firstReference.expectedType = "html";
19196
+ const html = generateHtmlForDirectory(
19197
+ urlObject.href,
19198
+ directoryContentArray,
19199
+ urlInfo.context.rootDirectoryUrl,
19200
+ );
19201
+ return {
19202
+ type: "html",
19203
+ contentType: "text/html",
19204
+ content: html,
19205
+ };
19206
+ }
18968
19207
  return {
18969
19208
  type: "directory",
18970
19209
  contentType: "application/json",
18971
- content,
18972
- };
18973
- }
18974
- const acceptsHtml = urlInfo.context.request
18975
- ? pickContentType(urlInfo.context.request, ["text/html"])
18976
- : false;
18977
- if (acceptsHtml) {
18978
- firstReference.expectedType = "html";
18979
- const html = generateHtmlForDirectory(
18980
- urlObject.href,
18981
- directoryContentArray,
18982
- urlInfo.context.rootDirectoryUrl,
18983
- );
18984
- return {
18985
- type: "html",
18986
- contentType: "text/html",
18987
- content: html,
19210
+ content: JSON.stringify(directoryContentArray, null, " "),
18988
19211
  };
18989
19212
  }
19213
+ const contentType = CONTENT_TYPE.fromUrlExtension(urlInfo.url);
19214
+ const fileBuffer = readFileSync(urlObject);
19215
+ const content = CONTENT_TYPE.isTextual(contentType)
19216
+ ? String(fileBuffer)
19217
+ : fileBuffer;
18990
19218
  return {
18991
- type: "directory",
18992
- contentType: "application/json",
18993
- content: JSON.stringify(directoryContentArray, null, " "),
19219
+ content,
19220
+ contentType,
19221
+ contentLength: fileBuffer.length,
18994
19222
  };
18995
- }
18996
- const contentType = CONTENT_TYPE.fromUrlExtension(urlInfo.url);
19223
+ };
19224
+
18997
19225
  const request = urlInfo.context.request;
18998
19226
  if (request && request.headers["sec-fetch-dest"] === "document") {
18999
19227
  try {
19000
- const fileBuffer = readFileSync(urlObject);
19001
- const content = CONTENT_TYPE.isTextual(contentType)
19002
- ? String(fileBuffer)
19003
- : fileBuffer;
19004
- return {
19005
- content,
19006
- contentType,
19007
- contentLength: fileBuffer.length,
19008
- };
19228
+ return generateContent();
19009
19229
  } catch (e) {
19010
19230
  if (e.code !== "ENOENT") {
19011
19231
  throw e;
19012
19232
  }
19013
- const parentDirectoryUrl = new URL("./", urlInfo.url);
19014
- if (!existsSync(parentDirectoryUrl)) {
19015
- throw e;
19233
+ const rootDirectoryUrl = urlInfo.context.rootDirectoryUrl;
19234
+ let firstExistingAncestorDirectoryUrl = new URL("./", urlInfo.url);
19235
+ while (!existsSync(firstExistingAncestorDirectoryUrl)) {
19236
+ firstExistingAncestorDirectoryUrl = new URL(
19237
+ "../",
19238
+ firstExistingAncestorDirectoryUrl,
19239
+ );
19240
+ if (
19241
+ !urlIsInsideOf(
19242
+ firstExistingAncestorDirectoryUrl,
19243
+ rootDirectoryUrl,
19244
+ )
19245
+ ) {
19246
+ firstExistingAncestorDirectoryUrl = rootDirectoryUrl;
19247
+ break;
19248
+ }
19016
19249
  }
19017
- const parentDirectoryContentArray = readdirSync(
19018
- new URL(parentDirectoryUrl),
19250
+
19251
+ const firstExistingAncestorDirectoryContent = readdirSync(
19252
+ new URL(firstExistingAncestorDirectoryUrl),
19019
19253
  );
19020
19254
  const html = generateHtmlForENOENT(
19021
19255
  urlInfo.url,
19022
- parentDirectoryContentArray,
19023
- parentDirectoryUrl,
19256
+ firstExistingAncestorDirectoryContent,
19257
+ firstExistingAncestorDirectoryUrl,
19024
19258
  urlInfo.context.rootDirectoryUrl,
19025
19259
  directoryListingUrlMocks,
19026
19260
  );
@@ -19034,15 +19268,7 @@ const jsenvPluginProtocolFile = ({
19034
19268
  };
19035
19269
  }
19036
19270
  }
19037
- const fileBuffer = readFileSync(urlObject);
19038
- const content = CONTENT_TYPE.isTextual(contentType)
19039
- ? String(fileBuffer)
19040
- : fileBuffer;
19041
- return {
19042
- content,
19043
- contentType,
19044
- contentLength: fileBuffer.length,
19045
- };
19271
+ return generateContent();
19046
19272
  },
19047
19273
  },
19048
19274
  ];
@@ -19073,17 +19299,17 @@ const generateHtmlForDirectory = (
19073
19299
  };
19074
19300
  const generateHtmlForENOENT = (
19075
19301
  url,
19076
- parentDirectoryContentArray,
19077
- parentDirectoryUrl,
19302
+ ancestorDirectoryContentArray,
19303
+ ancestorDirectoryUrl,
19078
19304
  rootDirectoryUrl,
19079
19305
  directoryListingUrlMocks,
19080
19306
  ) => {
19081
- const htmlFor404AndParentDir = String(
19082
- readFileSync(html404AndParentDirFileUrl),
19307
+ const htmlFor404AndAncestorDir = String(
19308
+ readFileSync(html404AndAncestorDirFileUrl),
19083
19309
  );
19084
19310
  const fileRelativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
19085
- const parentDirectoryRelativeUrl = urlToRelativeUrl(
19086
- parentDirectoryUrl,
19311
+ const ancestorDirectoryRelativeUrl = urlToRelativeUrl(
19312
+ ancestorDirectoryUrl,
19087
19313
  rootDirectoryUrl,
19088
19314
  );
19089
19315
  const replacers = {
@@ -19091,18 +19317,18 @@ const generateHtmlForENOENT = (
19091
19317
  ? `@jsenv/core/${urlToRelativeUrl(url, jsenvCoreDirectoryUrl)}`
19092
19318
  : url,
19093
19319
  fileRelativeUrl,
19094
- parentDirectoryUrl,
19095
- parentDirectoryRelativeUrl,
19096
- parentDirectoryNav: () =>
19097
- generateDirectoryNav(parentDirectoryRelativeUrl, rootDirectoryUrl),
19098
- parentDirectoryContent: () =>
19320
+ ancestorDirectoryUrl,
19321
+ ancestorDirectoryRelativeUrl,
19322
+ ancestorDirectoryNav: () =>
19323
+ generateDirectoryNav(ancestorDirectoryRelativeUrl, rootDirectoryUrl),
19324
+ ancestorDirectoryContent: () =>
19099
19325
  generateDirectoryContent(
19100
- parentDirectoryContentArray,
19101
- parentDirectoryUrl,
19326
+ ancestorDirectoryContentArray,
19327
+ ancestorDirectoryUrl,
19102
19328
  rootDirectoryUrl,
19103
19329
  ),
19104
19330
  };
19105
- const html = replacePlaceholders$1(htmlFor404AndParentDir, replacers);
19331
+ const html = replacePlaceholders$1(htmlFor404AndAncestorDir, replacers);
19106
19332
  return html;
19107
19333
  };
19108
19334
  const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "39.7.5",
3
+ "version": "39.7.7",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from "@jsenv/filesystem";
1
+ import { removeDirectorySync, writeFileSync } from "@jsenv/filesystem";
2
2
  import {
3
3
  composeTwoSourcemaps,
4
4
  generateSourcemapDataUrl,
@@ -275,7 +275,18 @@ export const createUrlInfoTransformer = ({
275
275
  contentIsInlined = false;
276
276
  }
277
277
  if (!contentIsInlined) {
278
- writeFileSync(new URL(generatedUrl), urlInfo.content);
278
+ try {
279
+ writeFileSync(new URL(generatedUrl), urlInfo.content);
280
+ } catch (e) {
281
+ if (e.code === "EISDIR") {
282
+ // happens when directory existed but got delete
283
+ // we can safely remove that directory and write the new file
284
+ removeDirectorySync(new URL(generatedUrl));
285
+ writeFileSync(new URL(generatedUrl), urlInfo.content);
286
+ } else {
287
+ throw e;
288
+ }
289
+ }
279
290
  }
280
291
  const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
281
292
  if (sourcemapGeneratedUrl && sourcemapReference) {
@@ -45,10 +45,10 @@
45
45
  No entry on the filesystem for <code>/${fileRelativeUrl}</code> (at
46
46
  ${fileUrl})
47
47
  <br />
48
- Content of the parent directory is listed below:
48
+ Content of the ancestor directory is listed below:
49
49
  </span>
50
50
  </p>
51
- <h1 class="directory_nav">${parentDirectoryNav}</h1>
52
- ${parentDirectoryContent}
51
+ <h1 class="directory_nav">${ancestorDirectoryNav}</h1>
52
+ ${ancestorDirectoryContent}
53
53
  </body>
54
54
  </html>
@@ -14,8 +14,8 @@ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
14
14
  import { jsenvCoreDirectoryUrl } from "../../jsenv_core_directory_url.js";
15
15
  import { jsenvPluginFsRedirection } from "./jsenv_plugin_fs_redirection.js";
16
16
 
17
- const html404AndParentDirFileUrl = new URL(
18
- "./client/html_404_and_parent_dir.html",
17
+ const html404AndAncestorDirFileUrl = new URL(
18
+ "./client/html_404_and_ancestor_dir.html",
19
19
  import.meta.url,
20
20
  );
21
21
  const htmlFileUrlForDirectory = new URL(
@@ -83,68 +83,86 @@ export const jsenvPluginProtocolFile = ({
83
83
  if (!urlInfo.url.startsWith("file:")) {
84
84
  return null;
85
85
  }
86
- const urlObject = new URL(urlInfo.url);
87
- const { firstReference } = urlInfo;
88
- if (firstReference.leadsToADirectory) {
89
- const directoryContentArray = readdirSync(urlObject);
90
- if (firstReference.type === "filesystem") {
91
- const content = JSON.stringify(directoryContentArray, null, " ");
86
+ const generateContent = () => {
87
+ const urlObject = new URL(urlInfo.url);
88
+ const { firstReference } = urlInfo;
89
+ if (firstReference.leadsToADirectory) {
90
+ const directoryContentArray = readdirSync(urlObject);
91
+ if (firstReference.type === "filesystem") {
92
+ const content = JSON.stringify(directoryContentArray, null, " ");
93
+ return {
94
+ type: "directory",
95
+ contentType: "application/json",
96
+ content,
97
+ };
98
+ }
99
+ const acceptsHtml = urlInfo.context.request
100
+ ? pickContentType(urlInfo.context.request, ["text/html"])
101
+ : false;
102
+ if (acceptsHtml) {
103
+ firstReference.expectedType = "html";
104
+ const html = generateHtmlForDirectory(
105
+ urlObject.href,
106
+ directoryContentArray,
107
+ urlInfo.context.rootDirectoryUrl,
108
+ );
109
+ return {
110
+ type: "html",
111
+ contentType: "text/html",
112
+ content: html,
113
+ };
114
+ }
92
115
  return {
93
116
  type: "directory",
94
117
  contentType: "application/json",
95
- content,
96
- };
97
- }
98
- const acceptsHtml = urlInfo.context.request
99
- ? pickContentType(urlInfo.context.request, ["text/html"])
100
- : false;
101
- if (acceptsHtml) {
102
- firstReference.expectedType = "html";
103
- const html = generateHtmlForDirectory(
104
- urlObject.href,
105
- directoryContentArray,
106
- urlInfo.context.rootDirectoryUrl,
107
- );
108
- return {
109
- type: "html",
110
- contentType: "text/html",
111
- content: html,
118
+ content: JSON.stringify(directoryContentArray, null, " "),
112
119
  };
113
120
  }
121
+ const contentType = CONTENT_TYPE.fromUrlExtension(urlInfo.url);
122
+ const fileBuffer = readFileSync(urlObject);
123
+ const content = CONTENT_TYPE.isTextual(contentType)
124
+ ? String(fileBuffer)
125
+ : fileBuffer;
114
126
  return {
115
- type: "directory",
116
- contentType: "application/json",
117
- content: JSON.stringify(directoryContentArray, null, " "),
127
+ content,
128
+ contentType,
129
+ contentLength: fileBuffer.length,
118
130
  };
119
- }
120
- const contentType = CONTENT_TYPE.fromUrlExtension(urlInfo.url);
131
+ };
132
+
121
133
  const request = urlInfo.context.request;
122
134
  if (request && request.headers["sec-fetch-dest"] === "document") {
123
135
  try {
124
- const fileBuffer = readFileSync(urlObject);
125
- const content = CONTENT_TYPE.isTextual(contentType)
126
- ? String(fileBuffer)
127
- : fileBuffer;
128
- return {
129
- content,
130
- contentType,
131
- contentLength: fileBuffer.length,
132
- };
136
+ return generateContent();
133
137
  } catch (e) {
134
138
  if (e.code !== "ENOENT") {
135
139
  throw e;
136
140
  }
137
- const parentDirectoryUrl = new URL("./", urlInfo.url);
138
- if (!existsSync(parentDirectoryUrl)) {
139
- throw e;
141
+ const rootDirectoryUrl = urlInfo.context.rootDirectoryUrl;
142
+ let firstExistingAncestorDirectoryUrl = new URL("./", urlInfo.url);
143
+ while (!existsSync(firstExistingAncestorDirectoryUrl)) {
144
+ firstExistingAncestorDirectoryUrl = new URL(
145
+ "../",
146
+ firstExistingAncestorDirectoryUrl,
147
+ );
148
+ if (
149
+ !urlIsInsideOf(
150
+ firstExistingAncestorDirectoryUrl,
151
+ rootDirectoryUrl,
152
+ )
153
+ ) {
154
+ firstExistingAncestorDirectoryUrl = rootDirectoryUrl;
155
+ break;
156
+ }
140
157
  }
141
- const parentDirectoryContentArray = readdirSync(
142
- new URL(parentDirectoryUrl),
158
+
159
+ const firstExistingAncestorDirectoryContent = readdirSync(
160
+ new URL(firstExistingAncestorDirectoryUrl),
143
161
  );
144
162
  const html = generateHtmlForENOENT(
145
163
  urlInfo.url,
146
- parentDirectoryContentArray,
147
- parentDirectoryUrl,
164
+ firstExistingAncestorDirectoryContent,
165
+ firstExistingAncestorDirectoryUrl,
148
166
  urlInfo.context.rootDirectoryUrl,
149
167
  directoryListingUrlMocks,
150
168
  );
@@ -158,15 +176,7 @@ export const jsenvPluginProtocolFile = ({
158
176
  };
159
177
  }
160
178
  }
161
- const fileBuffer = readFileSync(urlObject);
162
- const content = CONTENT_TYPE.isTextual(contentType)
163
- ? String(fileBuffer)
164
- : fileBuffer;
165
- return {
166
- content,
167
- contentType,
168
- contentLength: fileBuffer.length,
169
- };
179
+ return generateContent();
170
180
  },
171
181
  },
172
182
  ];
@@ -197,17 +207,17 @@ const generateHtmlForDirectory = (
197
207
  };
198
208
  const generateHtmlForENOENT = (
199
209
  url,
200
- parentDirectoryContentArray,
201
- parentDirectoryUrl,
210
+ ancestorDirectoryContentArray,
211
+ ancestorDirectoryUrl,
202
212
  rootDirectoryUrl,
203
213
  directoryListingUrlMocks,
204
214
  ) => {
205
- const htmlFor404AndParentDir = String(
206
- readFileSync(html404AndParentDirFileUrl),
215
+ const htmlFor404AndAncestorDir = String(
216
+ readFileSync(html404AndAncestorDirFileUrl),
207
217
  );
208
218
  const fileRelativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
209
- const parentDirectoryRelativeUrl = urlToRelativeUrl(
210
- parentDirectoryUrl,
219
+ const ancestorDirectoryRelativeUrl = urlToRelativeUrl(
220
+ ancestorDirectoryUrl,
211
221
  rootDirectoryUrl,
212
222
  );
213
223
  const replacers = {
@@ -215,18 +225,18 @@ const generateHtmlForENOENT = (
215
225
  ? `@jsenv/core/${urlToRelativeUrl(url, jsenvCoreDirectoryUrl)}`
216
226
  : url,
217
227
  fileRelativeUrl,
218
- parentDirectoryUrl,
219
- parentDirectoryRelativeUrl,
220
- parentDirectoryNav: () =>
221
- generateDirectoryNav(parentDirectoryRelativeUrl, rootDirectoryUrl),
222
- parentDirectoryContent: () =>
228
+ ancestorDirectoryUrl,
229
+ ancestorDirectoryRelativeUrl,
230
+ ancestorDirectoryNav: () =>
231
+ generateDirectoryNav(ancestorDirectoryRelativeUrl, rootDirectoryUrl),
232
+ ancestorDirectoryContent: () =>
223
233
  generateDirectoryContent(
224
- parentDirectoryContentArray,
225
- parentDirectoryUrl,
234
+ ancestorDirectoryContentArray,
235
+ ancestorDirectoryUrl,
226
236
  rootDirectoryUrl,
227
237
  ),
228
238
  };
229
- const html = replacePlaceholders(htmlFor404AndParentDir, replacers);
239
+ const html = replacePlaceholders(htmlFor404AndAncestorDir, replacers);
230
240
  return html;
231
241
  };
232
242
  const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {