@jsenv/core 40.6.2 → 40.7.1

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.
Files changed (36) hide show
  1. package/dist/build/browserslist_index/browserslist_index.js +62 -48
  2. package/dist/build/build.js +412 -185
  3. package/dist/build/jsenv_core_packages.js +103 -105
  4. package/dist/client/directory_listing/js/directory_listing.js +41 -26
  5. package/dist/client/ribbon/ribbon.js +40 -37
  6. package/dist/jsenv_core.js +4 -0
  7. package/dist/start_build_server/jsenv_core_packages.js +29 -29
  8. package/dist/start_dev_server/jsenv_core_packages.js +103 -105
  9. package/dist/start_dev_server/start_dev_server.js +412 -182
  10. package/package.json +21 -12
  11. package/src/build/build.js +9 -9
  12. package/src/build/build_specifier_manager.js +3 -3
  13. package/src/build/build_urls_generator.js +2 -2
  14. package/src/dev/start_dev_server.js +11 -8
  15. package/src/helpers/web_url_converter.js +2 -2
  16. package/src/kitchen/errors.js +1 -1
  17. package/src/kitchen/kitchen.js +2 -0
  18. package/src/kitchen/out_directory_url.js +2 -2
  19. package/src/kitchen/url_graph/url_graph.js +1 -0
  20. package/src/kitchen/url_graph/url_info_injections.js +172 -0
  21. package/src/kitchen/url_graph/url_info_transformations.js +28 -7
  22. package/src/main.js +1 -1
  23. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +2 -2
  24. package/src/plugins/chrome_devtools_json/jsenv_plugin_chrome_devtools_json.js +1 -0
  25. package/src/plugins/global_scenarios/jsenv_plugin_global_scenarios.js +4 -9
  26. package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +2 -0
  27. package/src/plugins/injections/jsenv_plugin_injections.js +51 -85
  28. package/src/plugins/plugin_controller.js +28 -7
  29. package/src/plugins/plugins.js +3 -1
  30. package/src/plugins/protocol_file/client/directory_listing.jsx +42 -23
  31. package/src/plugins/protocol_file/file_and_server_urls_converter.js +2 -5
  32. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +65 -49
  33. package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +36 -3
  34. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +3 -0
  35. package/src/plugins/ribbon/client/ribbon.js +40 -37
  36. package/src/plugins/injections/internal/inject_globals.js +0 -52
@@ -1,100 +1,66 @@
1
- import { createMagicSource } from "@jsenv/sourcemap";
2
1
  import { URL_META } from "@jsenv/url-meta";
3
- import { asUrlWithoutSearch } from "@jsenv/urls";
2
+ import { asUrlWithoutSearch, urlToRelativeUrl } from "@jsenv/urls";
3
+ import { INJECTIONS } from "../../kitchen/url_graph/url_info_injections.js";
4
4
 
5
5
  export const jsenvPluginInjections = (rawAssociations) => {
6
- let resolvedAssociations;
6
+ const getDefaultInjections = (urlInfo) => {
7
+ if (urlInfo.context.dev && urlInfo.type === "html") {
8
+ const relativeUrl = urlToRelativeUrl(
9
+ urlInfo.url,
10
+ urlInfo.context.rootDirectoryUrl,
11
+ );
12
+ return {
13
+ HTML_ROOT_PATHNAME: INJECTIONS.global(`/${relativeUrl}`),
14
+ };
15
+ }
16
+ return null;
17
+ };
18
+ let getInjections = null;
7
19
 
8
20
  return {
9
21
  name: "jsenv:injections",
10
22
  appliesDuring: "*",
11
23
  init: (context) => {
12
- resolvedAssociations = URL_META.resolveAssociations(
13
- { injectionsGetter: rawAssociations },
14
- context.rootDirectoryUrl,
15
- );
24
+ if (rawAssociations && Object.keys(rawAssociations).length > 0) {
25
+ const resolvedAssociations = URL_META.resolveAssociations(
26
+ { injectionsGetter: rawAssociations },
27
+ context.rootDirectoryUrl,
28
+ );
29
+ getInjections = (urlInfo) => {
30
+ const { injectionsGetter } = URL_META.applyAssociations({
31
+ url: asUrlWithoutSearch(urlInfo.url),
32
+ associations: resolvedAssociations,
33
+ });
34
+ if (!injectionsGetter) {
35
+ return null;
36
+ }
37
+ if (typeof injectionsGetter !== "function") {
38
+ throw new TypeError("injectionsGetter must be a function");
39
+ }
40
+ return injectionsGetter(urlInfo);
41
+ };
42
+ }
16
43
  },
17
44
  transformUrlContent: async (urlInfo) => {
18
- const { injectionsGetter } = URL_META.applyAssociations({
19
- url: asUrlWithoutSearch(urlInfo.url),
20
- associations: resolvedAssociations,
21
- });
22
- if (!injectionsGetter) {
23
- return null;
24
- }
25
- if (typeof injectionsGetter !== "function") {
26
- throw new TypeError("injectionsGetter must be a function");
27
- }
28
- const injections = await injectionsGetter(urlInfo);
29
- if (!injections) {
30
- return null;
45
+ const defaultInjections = getDefaultInjections(urlInfo);
46
+ if (!getInjections) {
47
+ return {
48
+ contentInjections: defaultInjections,
49
+ };
31
50
  }
32
- const keys = Object.keys(injections);
33
- if (keys.length === 0) {
34
- return null;
51
+ const injectionsResult = getInjections(urlInfo);
52
+ if (!injectionsResult) {
53
+ return {
54
+ contentInjections: defaultInjections,
55
+ };
35
56
  }
36
- return replacePlaceholders(urlInfo.content, injections, urlInfo);
57
+ const injections = await injectionsResult;
58
+ return {
59
+ contentInjections: {
60
+ ...defaultInjections,
61
+ ...injections,
62
+ },
63
+ };
37
64
  },
38
65
  };
39
66
  };
40
-
41
- const injectionSymbol = Symbol.for("jsenv_injection");
42
- export const INJECTIONS = {
43
- optional: (value) => {
44
- return { [injectionSymbol]: "optional", value };
45
- },
46
- };
47
-
48
- // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
49
- export const replacePlaceholders = (content, replacements, urlInfo) => {
50
- const magicSource = createMagicSource(content);
51
- for (const key of Object.keys(replacements)) {
52
- let index = content.indexOf(key);
53
- const replacement = replacements[key];
54
- let isOptional;
55
- let value;
56
- if (replacement && replacement[injectionSymbol]) {
57
- const valueBehindSymbol = replacement[injectionSymbol];
58
- isOptional = valueBehindSymbol === "optional";
59
- value = replacement.value;
60
- } else {
61
- value = replacement;
62
- }
63
- if (index === -1) {
64
- if (!isOptional) {
65
- urlInfo.context.logger.warn(
66
- `placeholder "${key}" not found in ${urlInfo.url}.
67
- --- suggestion a ---
68
- Add "${key}" in that file.
69
- --- suggestion b ---
70
- Fix eventual typo in "${key}"?
71
- --- suggestion c ---
72
- Mark injection as optional using INJECTIONS.optional():
73
- import { INJECTIONS } from "@jsenv/core";
74
-
75
- return {
76
- "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
77
- };`,
78
- );
79
- }
80
- continue;
81
- }
82
-
83
- while (index !== -1) {
84
- const start = index;
85
- const end = index + key.length;
86
- magicSource.replace({
87
- start,
88
- end,
89
- replacement:
90
- urlInfo.type === "js_classic" ||
91
- urlInfo.type === "js_module" ||
92
- urlInfo.type === "html"
93
- ? JSON.stringify(value, null, " ")
94
- : value,
95
- });
96
- index = content.indexOf(key, end);
97
- }
98
- }
99
- return magicSource.toContentAndSourcemap();
100
- };
@@ -1,10 +1,19 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
  import { jsenvPluginHtmlSyntaxErrorFallback } from "./html_syntax_error_fallback/jsenv_plugin_html_syntax_error_fallback.js";
3
3
 
4
- export const createPluginStore = (plugins) => {
4
+ export const createPluginStore = async (plugins) => {
5
5
  const allDevServerRoutes = [];
6
+ const allDevServerServices = [];
6
7
  const pluginArray = [];
7
- const addPlugin = (plugin) => {
8
+
9
+ const pluginPromises = [];
10
+ const addPlugin = async (plugin) => {
11
+ if (plugin && typeof plugin.then === "function") {
12
+ pluginPromises.push(plugin);
13
+ const value = await plugin;
14
+ addPlugin(value);
15
+ return;
16
+ }
8
17
  if (Array.isArray(plugin)) {
9
18
  for (const subplugin of plugin) {
10
19
  addPlugin(subplugin);
@@ -23,21 +32,28 @@ export const createPluginStore = (plugins) => {
23
32
  allDevServerRoutes.push(devServerRoute);
24
33
  }
25
34
  }
35
+ if (plugin.devServerServices) {
36
+ const devServerServices = plugin.devServerServices;
37
+ for (const devServerService of devServerServices) {
38
+ allDevServerServices.push(devServerService);
39
+ }
40
+ }
26
41
  pluginArray.push(plugin);
27
42
  };
28
43
  addPlugin(jsenvPluginHtmlSyntaxErrorFallback());
29
44
  for (const plugin of plugins) {
30
45
  addPlugin(plugin);
31
46
  }
47
+ await Promise.all(pluginPromises);
32
48
 
33
49
  return {
34
50
  pluginArray,
35
-
36
51
  allDevServerRoutes,
52
+ allDevServerServices,
37
53
  };
38
54
  };
39
55
 
40
- export const createPluginController = (
56
+ export const createPluginController = async (
41
57
  pluginStore,
42
58
  kitchen,
43
59
  { initialPuginsMeta = {} } = {},
@@ -60,7 +76,7 @@ export const createPluginController = (
60
76
  pluginCandidate.destroy?.();
61
77
  continue;
62
78
  }
63
- const initPluginResult = initPlugin(pluginCandidate, kitchen);
79
+ const initPluginResult = await initPlugin(pluginCandidate, kitchen);
64
80
  if (!initPluginResult) {
65
81
  pluginCandidate.destroy?.();
66
82
  continue;
@@ -112,6 +128,7 @@ export const createPluginController = (
112
128
  key === "serverEvents" ||
113
129
  key === "mustStayFirst" ||
114
130
  key === "devServerRoutes" ||
131
+ key === "devServerServices" ||
115
132
  key === "effect"
116
133
  ) {
117
134
  continue;
@@ -285,6 +302,7 @@ export const createPluginController = (
285
302
  const HOOK_NAMES = [
286
303
  "init",
287
304
  "devServerRoutes", // is called only during dev/tests
305
+ "devServerServices", // is called only during dev/tests
288
306
  "resolveReference",
289
307
  "redirectReference",
290
308
  "transformReferenceSearchParams",
@@ -339,12 +357,12 @@ const testAppliesDuring = (plugin, kitchen) => {
339
357
  `"appliesDuring" must be an object or a string, got ${appliesDuring}`,
340
358
  );
341
359
  };
342
- const initPlugin = (plugin, kitchen) => {
360
+ const initPlugin = async (plugin, kitchen) => {
343
361
  const { init } = plugin;
344
362
  if (!init) {
345
363
  return true;
346
364
  }
347
- const initReturnValue = init(kitchen.context, { plugin });
365
+ const initReturnValue = await init(kitchen.context, { plugin });
348
366
  if (initReturnValue === false) {
349
367
  return false;
350
368
  }
@@ -423,6 +441,9 @@ const returnValueAssertions = [
423
441
  return undefined;
424
442
  }
425
443
  if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
444
+ if (Object.hasOwn(valueReturned, "contentInjections")) {
445
+ return undefined;
446
+ }
426
447
  throw new Error(
427
448
  `Unexpected "content" returned by "${hook.plugin.name}" ${hook.name} hook: it must be a string or a buffer; got ${content}`,
428
449
  );
@@ -46,6 +46,7 @@ export const getCorePlugins = ({
46
46
  transpilation = true,
47
47
  inlining = true,
48
48
  http = false,
49
+ spa,
49
50
 
50
51
  clientAutoreload,
51
52
  clientAutoreloadOnServerRestart,
@@ -75,7 +76,7 @@ export const getCorePlugins = ({
75
76
 
76
77
  return [
77
78
  jsenvPluginReferenceAnalysis(referenceAnalysis),
78
- ...(injections ? [jsenvPluginInjections(injections)] : []),
79
+ jsenvPluginInjections(injections),
79
80
  jsenvPluginTranspilation(transpilation),
80
81
  // "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
81
82
  ...(inlining ? [jsenvPluginInlining()] : []),
@@ -88,6 +89,7 @@ export const getCorePlugins = ({
88
89
  */
89
90
  jsenvPluginProtocolHttp(http),
90
91
  jsenvPluginProtocolFile({
92
+ spa,
91
93
  magicExtensions,
92
94
  magicDirectoryIndex,
93
95
  directoryListing,
@@ -6,7 +6,7 @@ const fileIconUrl = import.meta.resolve("./assets/file.png");
6
6
  const homeIconUrl = import.meta.resolve("./assets/home.svg#root");
7
7
 
8
8
  let {
9
- navItems,
9
+ breadcrumb,
10
10
  mainFilePath,
11
11
  directoryContentItems,
12
12
  enoentDetails,
@@ -49,38 +49,51 @@ const DirectoryListing = () => {
49
49
  return (
50
50
  <>
51
51
  {enoentDetails ? <ErrorMessage /> : null}
52
- <Nav />
52
+ <Breadcrumb items={breadcrumb} />
53
53
  <DirectoryContent items={directoryItems} />
54
54
  </>
55
55
  );
56
56
  };
57
57
 
58
58
  const ErrorMessage = () => {
59
- const { fileUrl, filePathExisting, filePathNotFound } = enoentDetails;
59
+ const { filePathExisting, filePathNotFound } = enoentDetails;
60
+
61
+ let errorText;
62
+ let errorSuggestion;
63
+ errorText = (
64
+ <>
65
+ <strong>File not found:</strong>&nbsp;
66
+ <code>
67
+ <span className="file_path_good">{filePathExisting}</span>
68
+ <span className="file_path_bad">{filePathNotFound}</span>
69
+ </code>{" "}
70
+ does not exist on the server.
71
+ </>
72
+ );
73
+ errorSuggestion = (
74
+ <>
75
+ <span className="icon">🔍</span> Check available routes in{" "}
76
+ <a href="/.internal/route_inspector">route inspector</a>
77
+ </>
78
+ );
60
79
 
61
80
  return (
62
- <p className="error_message">
63
- <span className="error_text">
64
- No filesystem entry at{" "}
65
- <code title={fileUrl}>
66
- <span className="file_path_good">{filePathExisting}</span>
67
- <span className="file_path_bad">{filePathNotFound}</span>
68
- </code>
69
- .
70
- </span>
71
- <br />
72
- <span className="error_text" style="font-size: 70%;">
73
- See also available routes in the{" "}
74
- <a href="/.internal/route_inspector">route inspector</a>.
75
- </span>
76
- </p>
81
+ <div className="error_message">
82
+ <p className="error_text">{errorText}</p>
83
+ <p
84
+ className="error_suggestion"
85
+ style="font-size: 0.8em; margin-top: 10px;"
86
+ >
87
+ {errorSuggestion}
88
+ </p>
89
+ </div>
77
90
  );
78
91
  };
79
92
 
80
- const Nav = () => {
93
+ const Breadcrumb = ({ items }) => {
81
94
  return (
82
95
  <h1 className="nav">
83
- {navItems.map((navItem) => {
96
+ {items.map((navItem) => {
84
97
  const {
85
98
  url,
86
99
  urlRelativeToServer,
@@ -91,7 +104,7 @@ const Nav = () => {
91
104
  const isDirectory = new URL(url).pathname.endsWith("/");
92
105
  return (
93
106
  <>
94
- <NavItem
107
+ <BreadcrumbItem
95
108
  key={url}
96
109
  url={urlRelativeToServer}
97
110
  isCurrent={isCurrent}
@@ -99,7 +112,7 @@ const Nav = () => {
99
112
  iconLinkUrl={isServerRootDirectory ? `/${mainFilePath}` : ""}
100
113
  >
101
114
  {name}
102
- </NavItem>
115
+ </BreadcrumbItem>
103
116
  {isDirectory ? (
104
117
  <span className="directory_separator">/</span>
105
118
  ) : null}
@@ -109,7 +122,13 @@ const Nav = () => {
109
122
  </h1>
110
123
  );
111
124
  };
112
- const NavItem = ({ url, iconImageUrl, iconLinkUrl, isCurrent, children }) => {
125
+ const BreadcrumbItem = ({
126
+ url,
127
+ iconImageUrl,
128
+ iconLinkUrl,
129
+ isCurrent,
130
+ children,
131
+ }) => {
113
132
  return (
114
133
  <span className="nav_item" data-current={isCurrent ? "" : undefined}>
115
134
  {iconLinkUrl ? (
@@ -1,11 +1,8 @@
1
- import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
1
+ import { urlIsOrIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
2
2
 
3
3
  export const FILE_AND_SERVER_URLS_CONVERTER = {
4
4
  asServerUrl: (fileUrl, serverRootDirectoryUrl) => {
5
- if (fileUrl === serverRootDirectoryUrl) {
6
- return "/";
7
- }
8
- if (urlIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
5
+ if (urlIsOrIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
9
6
  const urlRelativeToServer = urlToRelativeUrl(
10
7
  fileUrl,
11
8
  serverRootDirectoryUrl,
@@ -31,13 +31,12 @@ import { pickContentType, WebSocketResponse } from "@jsenv/server";
31
31
  import {
32
32
  asUrlWithoutSearch,
33
33
  ensurePathnameTrailingSlash,
34
- urlIsInsideOf,
34
+ urlIsOrIsInsideOf,
35
35
  urlToFilename,
36
36
  urlToRelativeUrl,
37
37
  } from "@jsenv/urls";
38
38
  import { existsSync, lstatSync, readdirSync } from "node:fs";
39
39
  import { getDirectoryWatchPatterns } from "../../helpers/watch_source_files.js";
40
- import { replacePlaceholders } from "../injections/jsenv_plugin_injections.js";
41
40
  import { FILE_AND_SERVER_URLS_CONVERTER } from "./file_and_server_urls_converter.js";
42
41
 
43
42
  const htmlFileUrlForDirectory = import.meta.resolve(
@@ -45,6 +44,7 @@ const htmlFileUrlForDirectory = import.meta.resolve(
45
44
  );
46
45
 
47
46
  export const jsenvPluginDirectoryListing = ({
47
+ spa,
48
48
  urlMocks = false,
49
49
  autoreload = true,
50
50
  directoryContentMagicName,
@@ -86,7 +86,7 @@ export const jsenvPluginDirectoryListing = ({
86
86
  return null;
87
87
  }
88
88
  }
89
- return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}&enoent`;
89
+ return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(requestedUrl)}&enoent`;
90
90
  }
91
91
  const isDirectory = fsStat?.isDirectory();
92
92
  if (!isDirectory) {
@@ -112,34 +112,35 @@ export const jsenvPluginDirectoryListing = ({
112
112
  if (urlWithoutSearch !== String(htmlFileUrlForDirectory)) {
113
113
  return null;
114
114
  }
115
- const requestedUrl = urlInfo.searchParams.get("url");
116
- if (!requestedUrl) {
115
+ const urlNotFound = urlInfo.searchParams.get("url");
116
+ if (!urlNotFound) {
117
117
  return null;
118
118
  }
119
+
119
120
  urlInfo.headers["cache-control"] = "no-cache";
120
121
  const enoent = urlInfo.searchParams.has("enoent");
121
122
  if (enoent) {
122
123
  urlInfo.status = 404;
123
- urlInfo.headers["cache-control"] = "no-cache";
124
124
  }
125
125
  const request = urlInfo.context.request;
126
126
  const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
127
- return replacePlaceholders(
128
- urlInfo.content,
127
+ const directoryListingInjections = generateDirectoryListingInjection(
128
+ urlNotFound,
129
129
  {
130
- ...generateDirectoryListingInjection(requestedUrl, {
131
- autoreload,
132
- request,
133
- urlMocks,
134
- directoryContentMagicName,
135
- rootDirectoryUrl,
136
- mainFilePath,
137
- packageDirectory,
138
- enoent,
139
- }),
130
+ spa,
131
+ autoreload,
132
+ request,
133
+ urlMocks,
134
+ directoryContentMagicName,
135
+ rootDirectoryUrl,
136
+ mainFilePath,
137
+ packageDirectory,
138
+ enoent,
140
139
  },
141
- urlInfo,
142
140
  );
141
+ return {
142
+ contentInjections: directoryListingInjections,
143
+ };
143
144
  },
144
145
  },
145
146
  devServerRoutes: [
@@ -158,8 +159,10 @@ export const jsenvPluginDirectoryListing = ({
158
159
  directoryRelativeUrl,
159
160
  rootDirectoryUrl,
160
161
  );
161
- const closestDirectoryUrl =
162
- getFirstExistingDirectoryUrl(requestedUrl);
162
+ const closestDirectoryUrl = getFirstExistingDirectoryUrl(
163
+ requestedUrl,
164
+ rootDirectoryUrl,
165
+ );
163
166
  const sendMessage = (message) => {
164
167
  websocket.send(JSON.stringify(message));
165
168
  };
@@ -218,8 +221,9 @@ export const jsenvPluginDirectoryListing = ({
218
221
  };
219
222
 
220
223
  const generateDirectoryListingInjection = (
221
- requestedUrl,
224
+ urlNotFound,
222
225
  {
226
+ spa,
223
227
  rootDirectoryUrl,
224
228
  mainFilePath,
225
229
  packageDirectory,
@@ -232,13 +236,13 @@ const generateDirectoryListingInjection = (
232
236
  ) => {
233
237
  let serverRootDirectoryUrl = rootDirectoryUrl;
234
238
  const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
235
- requestedUrl,
239
+ urlNotFound,
236
240
  serverRootDirectoryUrl,
237
241
  );
238
242
  const directoryContentItems = getDirectoryContentItems({
239
243
  serverRootDirectoryUrl,
240
244
  mainFilePath,
241
- requestedUrl,
245
+ requestedUrl: urlNotFound,
242
246
  firstExistingDirectoryUrl,
243
247
  });
244
248
  package_workspaces: {
@@ -284,8 +288,8 @@ const generateDirectoryListingInjection = (
284
288
  const { host } = new URL(request.url);
285
289
  const websocketUrl = `${websocketScheme}://${host}/.internal/directory_content.websocket?directory=${encodeURIComponent(directoryUrlRelativeToServer)}`;
286
290
 
287
- const navItems = [];
288
- nav_items: {
291
+ const generateBreadcrumb = () => {
292
+ const breadcrumb = [];
289
293
  const lastItemUrl = firstExistingDirectoryUrl;
290
294
  const lastItemRelativeUrl = urlToRelativeUrl(lastItemUrl, rootDirectoryUrl);
291
295
  const rootDirectoryUrlName = urlToFilename(rootDirectoryUrl);
@@ -295,7 +299,6 @@ const generateDirectoryListingInjection = (
295
299
  } else {
296
300
  parts = [rootDirectoryUrlName];
297
301
  }
298
-
299
302
  let i = 0;
300
303
  while (i < parts.length) {
301
304
  const part = parts[i];
@@ -316,7 +319,7 @@ const generateDirectoryListingInjection = (
316
319
  navItemUrl,
317
320
  serverRootDirectoryUrl,
318
321
  );
319
- let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, requestedUrl);
322
+ let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, urlNotFound);
320
323
  const isServerRootDirectory = navItemUrl === serverRootDirectoryUrl;
321
324
  if (isServerRootDirectory) {
322
325
  urlRelativeToServer = `/${directoryContentMagicName}`;
@@ -324,7 +327,7 @@ const generateDirectoryListingInjection = (
324
327
  }
325
328
  const name = part;
326
329
  const isCurrent = navItemUrl === String(firstExistingDirectoryUrl);
327
- navItems.push({
330
+ breadcrumb.push({
328
331
  url: navItemUrl,
329
332
  urlRelativeToServer,
330
333
  urlRelativeToDocument,
@@ -334,34 +337,47 @@ const generateDirectoryListingInjection = (
334
337
  });
335
338
  i++;
336
339
  }
337
- }
340
+ return breadcrumb;
341
+ };
342
+ const breadcrumb = generateBreadcrumb(urlNotFound);
338
343
 
339
344
  let enoentDetails = null;
340
345
  if (enoent) {
346
+ const buildEnoentPathInfo = (urlBase, closestExistingUrl) => {
347
+ let filePathExisting;
348
+ let filePathNotFound;
349
+ const existingIndex = String(closestExistingUrl).length;
350
+ filePathExisting = urlToRelativeUrl(
351
+ closestExistingUrl,
352
+ serverRootDirectoryUrl,
353
+ );
354
+ filePathNotFound = urlBase.slice(existingIndex);
355
+ return [filePathExisting, filePathNotFound];
356
+ };
341
357
  const fileRelativeUrl = urlToRelativeUrl(
342
- requestedUrl,
358
+ urlNotFound,
343
359
  serverRootDirectoryUrl,
344
360
  );
345
- let filePathExisting;
346
- let filePathNotFound;
347
- const existingIndex = String(firstExistingDirectoryUrl).length;
348
- filePathExisting = urlToRelativeUrl(
349
- firstExistingDirectoryUrl,
350
- serverRootDirectoryUrl,
351
- );
352
- filePathNotFound = requestedUrl.slice(existingIndex);
353
361
  enoentDetails = {
354
- fileUrl: requestedUrl,
362
+ fileUrl: urlNotFound,
355
363
  fileRelativeUrl,
364
+ };
365
+
366
+ const [filePathExisting, filePathNotFound] = buildEnoentPathInfo(
367
+ urlNotFound,
368
+ firstExistingDirectoryUrl,
369
+ );
370
+ Object.assign(enoentDetails, {
356
371
  filePathExisting: `/${filePathExisting}`,
357
372
  filePathNotFound,
358
- };
373
+ });
359
374
  }
360
375
 
361
376
  return {
362
377
  __DIRECTORY_LISTING__: {
378
+ spa,
363
379
  enoentDetails,
364
- navItems,
380
+ breadcrumb,
365
381
  urlMocks,
366
382
  directoryContentMagicName,
367
383
  directoryUrl: firstExistingDirectoryUrl,
@@ -374,16 +390,16 @@ const generateDirectoryListingInjection = (
374
390
  },
375
391
  };
376
392
  };
377
- const getFirstExistingDirectoryUrl = (requestedUrl, serverRootDirectoryUrl) => {
378
- let firstExistingDirectoryUrl = new URL("./", requestedUrl);
379
- while (!existsSync(firstExistingDirectoryUrl)) {
380
- firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
381
- if (!urlIsInsideOf(firstExistingDirectoryUrl, serverRootDirectoryUrl)) {
382
- firstExistingDirectoryUrl = new URL(serverRootDirectoryUrl);
393
+ const getFirstExistingDirectoryUrl = (urlBase, serverRootDirectoryUrl) => {
394
+ let directoryUrlCandidate = new URL("./", urlBase);
395
+ while (!existsSync(directoryUrlCandidate)) {
396
+ directoryUrlCandidate = new URL("../", directoryUrlCandidate);
397
+ if (!urlIsOrIsInsideOf(directoryUrlCandidate, serverRootDirectoryUrl)) {
398
+ directoryUrlCandidate = new URL(serverRootDirectoryUrl);
383
399
  break;
384
400
  }
385
401
  }
386
- return firstExistingDirectoryUrl;
402
+ return directoryUrlCandidate;
387
403
  };
388
404
  const getDirectoryContentItems = ({
389
405
  serverRootDirectoryUrl,