@jsenv/core 39.10.1 → 39.11.0

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.
@@ -27,6 +27,12 @@
27
27
  position: relative;
28
28
  }
29
29
 
30
+ .directory_empty_message {
31
+ color: #bbb;
32
+ margin: 1em;
33
+ padding: .5em;
34
+ }
35
+
30
36
  .directory_content {
31
37
  border-radius: 3px;
32
38
  margin: 10px 15px;
@@ -27,6 +27,12 @@
27
27
  position: relative;
28
28
  }
29
29
 
30
+ .directory_empty_message {
31
+ color: #bbb;
32
+ margin: 1em;
33
+ padding: .5em;
34
+ }
35
+
30
36
  .directory_content {
31
37
  border-radius: 3px;
32
38
  margin: 10px 15px;
@@ -188,7 +194,7 @@
188
194
  No entry on the filesystem for <code>/${fileRelativeUrl}</code> (at
189
195
  ${fileUrl})
190
196
  <br>
191
- Content of the ancestor directory is listed below:
197
+ Content of first ancestor directory is listed below:
192
198
  </span>
193
199
  </p>
194
200
  <h1 class="directory_nav">${ancestorDirectoryNav}</h1>
@@ -12648,9 +12648,9 @@ const createEventEmitter = () => {
12648
12648
  callbackSet.delete(callback);
12649
12649
  };
12650
12650
  const emit = (...args) => {
12651
- callbackSet.forEach((callback) => {
12651
+ for (const callback of callbackSet) {
12652
12652
  callback(...args);
12653
- });
12653
+ }
12654
12654
  };
12655
12655
  return { on, off, emit };
12656
12656
  };
@@ -12817,7 +12817,7 @@ const createDependencies = (ownerUrlInfo) => {
12817
12817
 
12818
12818
  const stopCollecting = () => {
12819
12819
  for (const prevReferenceToOther of prevReferenceToOthersSet) {
12820
- applyDependencyRemovalEffects(prevReferenceToOther);
12820
+ checkForDependencyRemovalEffects(prevReferenceToOther);
12821
12821
  }
12822
12822
  prevReferenceToOthersSet.clear();
12823
12823
  };
@@ -13322,7 +13322,7 @@ ${ownerUrlInfo.url}`,
13322
13322
  implicitRef.remove();
13323
13323
  }
13324
13324
  ownerUrlInfo.referenceToOthersSet.delete(reference);
13325
- return applyDependencyRemovalEffects(reference);
13325
+ return checkForDependencyRemovalEffects(reference);
13326
13326
  };
13327
13327
 
13328
13328
  const canAddOrRemoveReference = (reference) => {
@@ -13351,7 +13351,7 @@ const canAddOrRemoveReference = (reference) => {
13351
13351
  return false;
13352
13352
  };
13353
13353
 
13354
- const applyDependencyRemovalEffects = (reference) => {
13354
+ const checkForDependencyRemovalEffects = (reference) => {
13355
13355
  const { ownerUrlInfo } = reference;
13356
13356
  const { referenceToOthersSet } = ownerUrlInfo;
13357
13357
  if (reference.isImplicit && !reference.isInline) {
@@ -13386,6 +13386,7 @@ const applyDependencyRemovalEffects = (reference) => {
13386
13386
  referencedUrlInfo.referenceFromOthersSet.delete(reference);
13387
13387
 
13388
13388
  let firstReferenceFromOther;
13389
+ let wasInlined;
13389
13390
  for (const referenceFromOther of referencedUrlInfo.referenceFromOthersSet) {
13390
13391
  if (referenceFromOther.urlInfo !== referencedUrlInfo) {
13391
13392
  continue;
@@ -13400,7 +13401,8 @@ const applyDependencyRemovalEffects = (reference) => {
13400
13401
  if (referenceFromOther.type === "http_request") {
13401
13402
  continue;
13402
13403
  }
13403
- if (referenceFromOther.gotInlined()) {
13404
+ wasInlined = referenceFromOther.gotInlined();
13405
+ if (wasInlined) {
13404
13406
  // the url info was inlined, an other reference is required
13405
13407
  // to consider the non-inlined urlInfo as used
13406
13408
  continue;
@@ -13418,6 +13420,9 @@ const applyDependencyRemovalEffects = (reference) => {
13418
13420
  }
13419
13421
  return false;
13420
13422
  }
13423
+ if (wasInlined) {
13424
+ return false;
13425
+ }
13421
13426
  // referencedUrlInfo.firstReference = null;
13422
13427
  // referencedUrlInfo.lastReference = null;
13423
13428
  referencedUrlInfo.onDereferenced(reference);
@@ -19176,6 +19181,7 @@ const jsenvPluginVersionSearchParam = () => {
19176
19181
  };
19177
19182
 
19178
19183
  const jsenvPluginFsRedirection = ({
19184
+ directoryContentMagicName,
19179
19185
  magicExtensions = ["inherit", ".js"],
19180
19186
  magicDirectoryIndex = true,
19181
19187
  preserveSymlinks = false,
@@ -19198,10 +19204,14 @@ const jsenvPluginFsRedirection = ({
19198
19204
  if (reference.subtype === "new_url_second_arg") {
19199
19205
  return `ignore:${reference.url}`;
19200
19206
  }
19201
- if (reference.specifierPathname.endsWith("/...")) {
19207
+ if (
19208
+ reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
19209
+ ) {
19202
19210
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19203
19211
  const directoryUrl = new URL(
19204
- reference.specifierPathname.replace("/...", "/").slice(1),
19212
+ reference.specifierPathname
19213
+ .replace(`/${directoryContentMagicName}`, "/")
19214
+ .slice(1),
19205
19215
  rootDirectoryUrl,
19206
19216
  ).href;
19207
19217
  return directoryUrl;
@@ -19312,6 +19322,7 @@ const htmlFileUrlForDirectory = new URL(
19312
19322
  "./html/directory.html",
19313
19323
  import.meta.url,
19314
19324
  );
19325
+ const directoryContentMagicName = "...";
19315
19326
 
19316
19327
  const jsenvPluginProtocolFile = ({
19317
19328
  magicExtensions,
@@ -19321,6 +19332,7 @@ const jsenvPluginProtocolFile = ({
19321
19332
  }) => {
19322
19333
  return [
19323
19334
  jsenvPluginFsRedirection({
19335
+ directoryContentMagicName,
19324
19336
  magicExtensions,
19325
19337
  magicDirectoryIndex,
19326
19338
  preserveSymlinks,
@@ -19360,8 +19372,9 @@ const jsenvPluginProtocolFile = ({
19360
19372
  if (reference.original) {
19361
19373
  const originalSpecifierPathname =
19362
19374
  reference.original.specifierPathname;
19363
-
19364
- if (originalSpecifierPathname.endsWith("/...")) {
19375
+ if (
19376
+ originalSpecifierPathname.endsWith(`/${directoryContentMagicName}`)
19377
+ ) {
19365
19378
  return originalSpecifierPathname;
19366
19379
  }
19367
19380
  }
@@ -19467,11 +19480,14 @@ const generateHtmlForDirectory = (directoryContentItems) => {
19467
19480
  directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
19468
19481
 
19469
19482
  const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
19470
- const directoryRelativeUrl = urlToRelativeUrl(directoryUrl, rootDirectoryUrl);
19471
19483
  const replacers = {
19472
19484
  directoryUrl,
19473
19485
  directoryNav: () =>
19474
- generateDirectoryNav(directoryRelativeUrl, rootDirectoryUrl),
19486
+ generateDirectoryNav(directoryUrl, {
19487
+ rootDirectoryUrl,
19488
+ rootDirectoryUrlForServer:
19489
+ directoryContentItems.rootDirectoryUrlForServer,
19490
+ }),
19475
19491
  directoryContent: () => generateDirectoryContent(directoryContentItems),
19476
19492
  };
19477
19493
  const html = replacePlaceholders$1(htmlForDirectory, replacers);
@@ -19501,44 +19517,60 @@ const generateHtmlForENOENT = (
19501
19517
  ancestorDirectoryUrl,
19502
19518
  ancestorDirectoryRelativeUrl,
19503
19519
  ancestorDirectoryNav: () =>
19504
- generateDirectoryNav(ancestorDirectoryRelativeUrl, rootDirectoryUrl),
19520
+ generateDirectoryNav(ancestorDirectoryUrl, {
19521
+ rootDirectoryUrl,
19522
+ rootDirectoryUrlForServer:
19523
+ directoryContentItems.rootDirectoryUrlForServer,
19524
+ }),
19505
19525
  ancestorDirectoryContent: () =>
19506
19526
  generateDirectoryContent(directoryContentItems),
19507
19527
  };
19508
19528
  const html = replacePlaceholders$1(htmlFor404AndAncestorDir, replacers);
19509
19529
  return html;
19510
19530
  };
19511
- const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19531
+ const generateDirectoryNav = (
19532
+ entryDirectoryUrl,
19533
+ { rootDirectoryUrl, rootDirectoryUrlForServer },
19534
+ ) => {
19535
+ const entryDirectoryRelativeUrl = urlToRelativeUrl(
19536
+ entryDirectoryUrl,
19537
+ rootDirectoryUrl,
19538
+ );
19539
+ const isDir = entryDirectoryRelativeUrl.endsWith("/");
19512
19540
  const rootDirectoryUrlName = urlToFilename$1(rootDirectoryUrl);
19513
- const relativeUrlWithRoot = relativeUrl
19514
- ? `${rootDirectoryUrlName}/${relativeUrl}`
19515
- : `${rootDirectoryUrlName}/`;
19516
- const isDir = relativeUrlWithRoot.endsWith("/");
19517
- const parts = isDir
19518
- ? relativeUrlWithRoot.slice(0, -1).split("/")
19519
- : relativeUrlWithRoot.split("/");
19520
19541
  const items = [];
19521
- items.push({
19522
- href: "/",
19523
- text: "/",
19524
- });
19525
19542
  let dirPartsHtml = "";
19543
+ const parts =
19544
+ `${rootDirectoryUrlName}/${entryDirectoryRelativeUrl.slice(0, -1)}`.split(
19545
+ "/",
19546
+ );
19526
19547
  let i = 0;
19527
19548
  while (i < parts.length) {
19528
19549
  const part = parts[i];
19529
- const href = i === 0 ? "/..." : `/${parts.slice(1, i + 1).join("/")}/`;
19550
+ const directoryRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
19551
+ const directoryUrl =
19552
+ directoryRelativeUrl === ""
19553
+ ? rootDirectoryUrl
19554
+ : new URL(`${directoryRelativeUrl}/`, rootDirectoryUrl).href;
19555
+ let href =
19556
+ directoryUrl === rootDirectoryUrlForServer ||
19557
+ urlIsInsideOf(directoryUrl, rootDirectoryUrlForServer)
19558
+ ? urlToRelativeUrl(directoryUrl, rootDirectoryUrlForServer)
19559
+ : directoryUrl;
19560
+ if (href === "") {
19561
+ href = `/${directoryContentMagicName}`;
19562
+ }
19530
19563
  const text = part;
19531
- const isLastPart = i === parts.length - 1;
19532
19564
  items.push({
19533
19565
  href,
19534
19566
  text,
19535
- isCurrent: isLastPart,
19536
19567
  });
19537
19568
  i++;
19538
19569
  }
19539
19570
  i = 0;
19540
- for (const { href, text, isCurrent } of items) {
19541
- if (isCurrent) {
19571
+ for (const { href, text } of items) {
19572
+ const isLastPart = i === parts.length - 1;
19573
+ if (isLastPart) {
19542
19574
  dirPartsHtml += `
19543
19575
  <span class="directory_nav_item" data-current>
19544
19576
  ${text}
@@ -19549,10 +19581,8 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19549
19581
  <a class="directory_nav_item" href="${href}">
19550
19582
  ${text}
19551
19583
  </a>`;
19552
- if (i > 0) {
19553
- dirPartsHtml += `
19584
+ dirPartsHtml += `
19554
19585
  <span class="directory_separator">/</span>`;
19555
- }
19556
19586
  i++;
19557
19587
  }
19558
19588
  if (isDir) {
@@ -19561,12 +19591,15 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19561
19591
  }
19562
19592
  return dirPartsHtml;
19563
19593
  };
19564
- const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
19594
+ const generateDirectoryContentItems = (
19595
+ directoryUrl,
19596
+ rootDirectoryUrlForServer,
19597
+ ) => {
19565
19598
  let firstExistingDirectoryUrl = new URL("./", directoryUrl);
19566
19599
  while (!existsSync(firstExistingDirectoryUrl)) {
19567
19600
  firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
19568
- if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrl)) {
19569
- firstExistingDirectoryUrl = new URL(rootDirectoryUrl);
19601
+ if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrlForServer)) {
19602
+ firstExistingDirectoryUrl = new URL(rootDirectoryUrlForServer);
19570
19603
  break;
19571
19604
  }
19572
19605
  }
@@ -19576,37 +19609,42 @@ const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
19576
19609
  const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
19577
19610
  fileUrls.push(fileUrlObject);
19578
19611
  }
19612
+ let rootDirectoryUrl = rootDirectoryUrlForServer;
19579
19613
  package_workspaces: {
19580
- if (String(firstExistingDirectoryUrl) !== String(rootDirectoryUrl)) {
19581
- break package_workspaces;
19582
- }
19583
- const packageDirectoryUrl = lookupPackageDirectory(rootDirectoryUrl);
19614
+ const packageDirectoryUrl = lookupPackageDirectory(
19615
+ rootDirectoryUrlForServer,
19616
+ );
19584
19617
  if (!packageDirectoryUrl) {
19585
19618
  break package_workspaces;
19586
19619
  }
19587
- if (String(packageDirectoryUrl) === String(rootDirectoryUrl)) {
19620
+ if (String(packageDirectoryUrl) === String(rootDirectoryUrlForServer)) {
19588
19621
  break package_workspaces;
19589
19622
  }
19590
- let packageContent;
19591
- try {
19592
- packageContent = JSON.parse(
19593
- readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
19594
- );
19595
- } catch {
19596
- break package_workspaces;
19597
- }
19598
- const { workspaces } = packageContent;
19599
- if (Array.isArray(workspaces)) {
19600
- for (const workspace of workspaces) {
19601
- const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
19602
- const workspaceUrl = workspaceUrlObject.href;
19603
- if (workspaceUrl.endsWith("*")) {
19604
- const directoryUrl = ensurePathnameTrailingSlash(
19605
- workspaceUrl.slice(0, -1),
19606
- );
19607
- fileUrls.push(new URL(directoryUrl));
19608
- } else {
19609
- fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
19623
+ rootDirectoryUrl = packageDirectoryUrl;
19624
+ if (
19625
+ String(firstExistingDirectoryUrl) === String(rootDirectoryUrlForServer)
19626
+ ) {
19627
+ let packageContent;
19628
+ try {
19629
+ packageContent = JSON.parse(
19630
+ readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
19631
+ );
19632
+ } catch {
19633
+ break package_workspaces;
19634
+ }
19635
+ const { workspaces } = packageContent;
19636
+ if (Array.isArray(workspaces)) {
19637
+ for (const workspace of workspaces) {
19638
+ const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
19639
+ const workspaceUrl = workspaceUrlObject.href;
19640
+ if (workspaceUrl.endsWith("*")) {
19641
+ const directoryUrl = ensurePathnameTrailingSlash(
19642
+ workspaceUrl.slice(0, -1),
19643
+ );
19644
+ fileUrls.push(new URL(directoryUrl));
19645
+ } else {
19646
+ fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
19647
+ }
19610
19648
  }
19611
19649
  }
19612
19650
  }
@@ -19630,29 +19668,37 @@ const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
19630
19668
  sortedUrl,
19631
19669
  firstExistingDirectoryUrl,
19632
19670
  );
19633
- const fileUrlRelativeToRoot = urlToRelativeUrl(sortedUrl, rootDirectoryUrl);
19671
+ const fileUrlRelativeToServer = urlToRelativeUrl(
19672
+ sortedUrl,
19673
+ rootDirectoryUrlForServer,
19674
+ );
19634
19675
  const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
19635
19676
  items.push({
19636
19677
  type,
19637
19678
  fileUrlRelativeToParent,
19638
- fileUrlRelativeToRoot,
19679
+ fileUrlRelativeToServer,
19639
19680
  });
19640
19681
  }
19682
+ items.rootDirectoryUrlForServer = rootDirectoryUrlForServer;
19641
19683
  items.rootDirectoryUrl = rootDirectoryUrl;
19642
19684
  items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
19643
19685
  return items;
19644
19686
  };
19645
19687
  const generateDirectoryContent = (directoryContentItems) => {
19646
19688
  if (directoryContentItems.length === 0) {
19647
- return `<p>Directory is empty</p>`;
19689
+ return `<p class="directory_empty_message">Directory is empty</p>`;
19648
19690
  }
19649
19691
  let html = `<ul class="directory_content">`;
19650
19692
  for (const directoryContentItem of directoryContentItems) {
19651
- const { type, fileUrlRelativeToParent, fileUrlRelativeToRoot } =
19693
+ const { type, fileUrlRelativeToParent, fileUrlRelativeToServer } =
19652
19694
  directoryContentItem;
19695
+ let href = fileUrlRelativeToServer;
19696
+ if (href === "") {
19697
+ href = `${directoryContentMagicName}`;
19698
+ }
19653
19699
  html += `
19654
19700
  <li class="directory_child" data-type="${type}">
19655
- <a href="/${fileUrlRelativeToRoot}">${fileUrlRelativeToParent}</a>
19701
+ <a href="/${href}">${fileUrlRelativeToParent}</a>
19656
19702
  </li>`;
19657
19703
  }
19658
19704
  html += `\n </ul>`;
@@ -20827,6 +20873,11 @@ const jsenvPluginAutoreloadServer = ({
20827
20873
  // are lost and sourcemap is considered as pruned
20828
20874
  continue;
20829
20875
  }
20876
+ if (lastReferenceFromOther.type === "http_request") {
20877
+ // no need to tell client to reload when a http request is pruned
20878
+ // happens when reloading the current html page for instance
20879
+ continue;
20880
+ }
20830
20881
  const { ownerUrlInfo } = lastReferenceFromOther;
20831
20882
  if (!ownerUrlInfo.isUsed()) {
20832
20883
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "39.10.1",
3
+ "version": "39.11.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -10,9 +10,9 @@ export const createEventEmitter = () => {
10
10
  callbackSet.delete(callback);
11
11
  };
12
12
  const emit = (...args) => {
13
- callbackSet.forEach((callback) => {
13
+ for (const callback of callbackSet) {
14
14
  callback(...args);
15
- });
15
+ }
16
16
  };
17
17
  return { on, off, emit };
18
18
  };
@@ -22,7 +22,7 @@ export const createDependencies = (ownerUrlInfo) => {
22
22
 
23
23
  const stopCollecting = () => {
24
24
  for (const prevReferenceToOther of prevReferenceToOthersSet) {
25
- applyDependencyRemovalEffects(prevReferenceToOther);
25
+ checkForDependencyRemovalEffects(prevReferenceToOther);
26
26
  }
27
27
  prevReferenceToOthersSet.clear();
28
28
  };
@@ -527,7 +527,7 @@ ${ownerUrlInfo.url}`,
527
527
  implicitRef.remove();
528
528
  }
529
529
  ownerUrlInfo.referenceToOthersSet.delete(reference);
530
- return applyDependencyRemovalEffects(reference);
530
+ return checkForDependencyRemovalEffects(reference);
531
531
  };
532
532
 
533
533
  const canAddOrRemoveReference = (reference) => {
@@ -556,7 +556,7 @@ const canAddOrRemoveReference = (reference) => {
556
556
  return false;
557
557
  };
558
558
 
559
- const applyDependencyRemovalEffects = (reference) => {
559
+ const checkForDependencyRemovalEffects = (reference) => {
560
560
  const { ownerUrlInfo } = reference;
561
561
  const { referenceToOthersSet } = ownerUrlInfo;
562
562
  if (reference.isImplicit && !reference.isInline) {
@@ -591,6 +591,7 @@ const applyDependencyRemovalEffects = (reference) => {
591
591
  referencedUrlInfo.referenceFromOthersSet.delete(reference);
592
592
 
593
593
  let firstReferenceFromOther;
594
+ let wasInlined;
594
595
  for (const referenceFromOther of referencedUrlInfo.referenceFromOthersSet) {
595
596
  if (referenceFromOther.urlInfo !== referencedUrlInfo) {
596
597
  continue;
@@ -605,7 +606,8 @@ const applyDependencyRemovalEffects = (reference) => {
605
606
  if (referenceFromOther.type === "http_request") {
606
607
  continue;
607
608
  }
608
- if (referenceFromOther.gotInlined()) {
609
+ wasInlined = referenceFromOther.gotInlined();
610
+ if (wasInlined) {
609
611
  // the url info was inlined, an other reference is required
610
612
  // to consider the non-inlined urlInfo as used
611
613
  continue;
@@ -623,6 +625,9 @@ const applyDependencyRemovalEffects = (reference) => {
623
625
  }
624
626
  return false;
625
627
  }
628
+ if (wasInlined) {
629
+ return false;
630
+ }
626
631
  // referencedUrlInfo.firstReference = null;
627
632
  // referencedUrlInfo.lastReference = null;
628
633
  referencedUrlInfo.onDereferenced(reference);
@@ -238,6 +238,11 @@ export const jsenvPluginAutoreloadServer = ({
238
238
  // are lost and sourcemap is considered as pruned
239
239
  continue;
240
240
  }
241
+ if (lastReferenceFromOther.type === "http_request") {
242
+ // no need to tell client to reload when a http request is pruned
243
+ // happens when reloading the current html page for instance
244
+ continue;
245
+ }
241
246
  const { ownerUrlInfo } = lastReferenceFromOther;
242
247
  if (!ownerUrlInfo.isUsed()) {
243
248
  continue;
@@ -26,6 +26,11 @@ button {
26
26
  text-decoration: none;
27
27
  position: relative;
28
28
  }
29
+ .directory_empty_message {
30
+ margin: 1em;
31
+ padding: 0.5em;
32
+ color: #bbbbbb;
33
+ }
29
34
  .directory_content {
30
35
  margin: 10px 15px 10px 15px;
31
36
  list-style-type: none;
@@ -45,7 +45,7 @@
45
45
  No entry on the filesystem for <code>/${fileRelativeUrl}</code> (at
46
46
  ${fileUrl})
47
47
  <br />
48
- Content of the ancestor directory is listed below:
48
+ Content of first ancestor directory is listed below:
49
49
  </span>
50
50
  </p>
51
51
  <h1 class="directory_nav">${ancestorDirectoryNav}</h1>
@@ -8,6 +8,7 @@ import { realpathSync } from "node:fs";
8
8
  import { pathToFileURL } from "node:url";
9
9
 
10
10
  export const jsenvPluginFsRedirection = ({
11
+ directoryContentMagicName,
11
12
  magicExtensions = ["inherit", ".js"],
12
13
  magicDirectoryIndex = true,
13
14
  preserveSymlinks = false,
@@ -30,10 +31,14 @@ export const jsenvPluginFsRedirection = ({
30
31
  if (reference.subtype === "new_url_second_arg") {
31
32
  return `ignore:${reference.url}`;
32
33
  }
33
- if (reference.specifierPathname.endsWith("/...")) {
34
+ if (
35
+ reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
36
+ ) {
34
37
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
35
38
  const directoryUrl = new URL(
36
- reference.specifierPathname.replace("/...", "/").slice(1),
39
+ reference.specifierPathname
40
+ .replace(`/${directoryContentMagicName}`, "/")
41
+ .slice(1),
37
42
  rootDirectoryUrl,
38
43
  ).href;
39
44
  return directoryUrl;
@@ -24,6 +24,7 @@ const htmlFileUrlForDirectory = new URL(
24
24
  "./client/directory.html",
25
25
  import.meta.url,
26
26
  );
27
+ const directoryContentMagicName = "...";
27
28
 
28
29
  export const jsenvPluginProtocolFile = ({
29
30
  magicExtensions,
@@ -33,6 +34,7 @@ export const jsenvPluginProtocolFile = ({
33
34
  }) => {
34
35
  return [
35
36
  jsenvPluginFsRedirection({
37
+ directoryContentMagicName,
36
38
  magicExtensions,
37
39
  magicDirectoryIndex,
38
40
  preserveSymlinks,
@@ -72,8 +74,9 @@ export const jsenvPluginProtocolFile = ({
72
74
  if (reference.original) {
73
75
  const originalSpecifierPathname =
74
76
  reference.original.specifierPathname;
75
-
76
- if (originalSpecifierPathname.endsWith("/...")) {
77
+ if (
78
+ originalSpecifierPathname.endsWith(`/${directoryContentMagicName}`)
79
+ ) {
77
80
  return originalSpecifierPathname;
78
81
  }
79
82
  }
@@ -179,11 +182,14 @@ const generateHtmlForDirectory = (directoryContentItems) => {
179
182
  directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
180
183
 
181
184
  const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
182
- const directoryRelativeUrl = urlToRelativeUrl(directoryUrl, rootDirectoryUrl);
183
185
  const replacers = {
184
186
  directoryUrl,
185
187
  directoryNav: () =>
186
- generateDirectoryNav(directoryRelativeUrl, rootDirectoryUrl),
188
+ generateDirectoryNav(directoryUrl, {
189
+ rootDirectoryUrl,
190
+ rootDirectoryUrlForServer:
191
+ directoryContentItems.rootDirectoryUrlForServer,
192
+ }),
187
193
  directoryContent: () => generateDirectoryContent(directoryContentItems),
188
194
  };
189
195
  const html = replacePlaceholders(htmlForDirectory, replacers);
@@ -213,44 +219,60 @@ const generateHtmlForENOENT = (
213
219
  ancestorDirectoryUrl,
214
220
  ancestorDirectoryRelativeUrl,
215
221
  ancestorDirectoryNav: () =>
216
- generateDirectoryNav(ancestorDirectoryRelativeUrl, rootDirectoryUrl),
222
+ generateDirectoryNav(ancestorDirectoryUrl, {
223
+ rootDirectoryUrl,
224
+ rootDirectoryUrlForServer:
225
+ directoryContentItems.rootDirectoryUrlForServer,
226
+ }),
217
227
  ancestorDirectoryContent: () =>
218
228
  generateDirectoryContent(directoryContentItems),
219
229
  };
220
230
  const html = replacePlaceholders(htmlFor404AndAncestorDir, replacers);
221
231
  return html;
222
232
  };
223
- const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
233
+ const generateDirectoryNav = (
234
+ entryDirectoryUrl,
235
+ { rootDirectoryUrl, rootDirectoryUrlForServer },
236
+ ) => {
237
+ const entryDirectoryRelativeUrl = urlToRelativeUrl(
238
+ entryDirectoryUrl,
239
+ rootDirectoryUrl,
240
+ );
241
+ const isDir = entryDirectoryRelativeUrl.endsWith("/");
224
242
  const rootDirectoryUrlName = urlToFilename(rootDirectoryUrl);
225
- const relativeUrlWithRoot = relativeUrl
226
- ? `${rootDirectoryUrlName}/${relativeUrl}`
227
- : `${rootDirectoryUrlName}/`;
228
- const isDir = relativeUrlWithRoot.endsWith("/");
229
- const parts = isDir
230
- ? relativeUrlWithRoot.slice(0, -1).split("/")
231
- : relativeUrlWithRoot.split("/");
232
243
  const items = [];
233
- items.push({
234
- href: "/",
235
- text: "/",
236
- });
237
244
  let dirPartsHtml = "";
245
+ const parts =
246
+ `${rootDirectoryUrlName}/${entryDirectoryRelativeUrl.slice(0, -1)}`.split(
247
+ "/",
248
+ );
238
249
  let i = 0;
239
250
  while (i < parts.length) {
240
251
  const part = parts[i];
241
- const href = i === 0 ? "/..." : `/${parts.slice(1, i + 1).join("/")}/`;
252
+ const directoryRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
253
+ const directoryUrl =
254
+ directoryRelativeUrl === ""
255
+ ? rootDirectoryUrl
256
+ : new URL(`${directoryRelativeUrl}/`, rootDirectoryUrl).href;
257
+ let href =
258
+ directoryUrl === rootDirectoryUrlForServer ||
259
+ urlIsInsideOf(directoryUrl, rootDirectoryUrlForServer)
260
+ ? urlToRelativeUrl(directoryUrl, rootDirectoryUrlForServer)
261
+ : directoryUrl;
262
+ if (href === "") {
263
+ href = `/${directoryContentMagicName}`;
264
+ }
242
265
  const text = part;
243
- const isLastPart = i === parts.length - 1;
244
266
  items.push({
245
267
  href,
246
268
  text,
247
- isCurrent: isLastPart,
248
269
  });
249
270
  i++;
250
271
  }
251
272
  i = 0;
252
- for (const { href, text, isCurrent } of items) {
253
- if (isCurrent) {
273
+ for (const { href, text } of items) {
274
+ const isLastPart = i === parts.length - 1;
275
+ if (isLastPart) {
254
276
  dirPartsHtml += `
255
277
  <span class="directory_nav_item" data-current>
256
278
  ${text}
@@ -261,10 +283,8 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
261
283
  <a class="directory_nav_item" href="${href}">
262
284
  ${text}
263
285
  </a>`;
264
- if (i > 0) {
265
- dirPartsHtml += `
286
+ dirPartsHtml += `
266
287
  <span class="directory_separator">/</span>`;
267
- }
268
288
  i++;
269
289
  }
270
290
  if (isDir) {
@@ -273,12 +293,15 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
273
293
  }
274
294
  return dirPartsHtml;
275
295
  };
276
- const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
296
+ const generateDirectoryContentItems = (
297
+ directoryUrl,
298
+ rootDirectoryUrlForServer,
299
+ ) => {
277
300
  let firstExistingDirectoryUrl = new URL("./", directoryUrl);
278
301
  while (!existsSync(firstExistingDirectoryUrl)) {
279
302
  firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
280
- if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrl)) {
281
- firstExistingDirectoryUrl = new URL(rootDirectoryUrl);
303
+ if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrlForServer)) {
304
+ firstExistingDirectoryUrl = new URL(rootDirectoryUrlForServer);
282
305
  break;
283
306
  }
284
307
  }
@@ -288,37 +311,42 @@ const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
288
311
  const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
289
312
  fileUrls.push(fileUrlObject);
290
313
  }
314
+ let rootDirectoryUrl = rootDirectoryUrlForServer;
291
315
  package_workspaces: {
292
- if (String(firstExistingDirectoryUrl) !== String(rootDirectoryUrl)) {
293
- break package_workspaces;
294
- }
295
- const packageDirectoryUrl = lookupPackageDirectory(rootDirectoryUrl);
316
+ const packageDirectoryUrl = lookupPackageDirectory(
317
+ rootDirectoryUrlForServer,
318
+ );
296
319
  if (!packageDirectoryUrl) {
297
320
  break package_workspaces;
298
321
  }
299
- if (String(packageDirectoryUrl) === String(rootDirectoryUrl)) {
300
- break package_workspaces;
301
- }
302
- let packageContent;
303
- try {
304
- packageContent = JSON.parse(
305
- readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
306
- );
307
- } catch {
322
+ if (String(packageDirectoryUrl) === String(rootDirectoryUrlForServer)) {
308
323
  break package_workspaces;
309
324
  }
310
- const { workspaces } = packageContent;
311
- if (Array.isArray(workspaces)) {
312
- for (const workspace of workspaces) {
313
- const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
314
- const workspaceUrl = workspaceUrlObject.href;
315
- if (workspaceUrl.endsWith("*")) {
316
- const directoryUrl = ensurePathnameTrailingSlash(
317
- workspaceUrl.slice(0, -1),
318
- );
319
- fileUrls.push(new URL(directoryUrl));
320
- } else {
321
- fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
325
+ rootDirectoryUrl = packageDirectoryUrl;
326
+ if (
327
+ String(firstExistingDirectoryUrl) === String(rootDirectoryUrlForServer)
328
+ ) {
329
+ let packageContent;
330
+ try {
331
+ packageContent = JSON.parse(
332
+ readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
333
+ );
334
+ } catch {
335
+ break package_workspaces;
336
+ }
337
+ const { workspaces } = packageContent;
338
+ if (Array.isArray(workspaces)) {
339
+ for (const workspace of workspaces) {
340
+ const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
341
+ const workspaceUrl = workspaceUrlObject.href;
342
+ if (workspaceUrl.endsWith("*")) {
343
+ const directoryUrl = ensurePathnameTrailingSlash(
344
+ workspaceUrl.slice(0, -1),
345
+ );
346
+ fileUrls.push(new URL(directoryUrl));
347
+ } else {
348
+ fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
349
+ }
322
350
  }
323
351
  }
324
352
  }
@@ -342,29 +370,37 @@ const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
342
370
  sortedUrl,
343
371
  firstExistingDirectoryUrl,
344
372
  );
345
- const fileUrlRelativeToRoot = urlToRelativeUrl(sortedUrl, rootDirectoryUrl);
373
+ const fileUrlRelativeToServer = urlToRelativeUrl(
374
+ sortedUrl,
375
+ rootDirectoryUrlForServer,
376
+ );
346
377
  const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
347
378
  items.push({
348
379
  type,
349
380
  fileUrlRelativeToParent,
350
- fileUrlRelativeToRoot,
381
+ fileUrlRelativeToServer,
351
382
  });
352
383
  }
384
+ items.rootDirectoryUrlForServer = rootDirectoryUrlForServer;
353
385
  items.rootDirectoryUrl = rootDirectoryUrl;
354
386
  items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
355
387
  return items;
356
388
  };
357
389
  const generateDirectoryContent = (directoryContentItems) => {
358
390
  if (directoryContentItems.length === 0) {
359
- return `<p>Directory is empty</p>`;
391
+ return `<p class="directory_empty_message">Directory is empty</p>`;
360
392
  }
361
393
  let html = `<ul class="directory_content">`;
362
394
  for (const directoryContentItem of directoryContentItems) {
363
- const { type, fileUrlRelativeToParent, fileUrlRelativeToRoot } =
395
+ const { type, fileUrlRelativeToParent, fileUrlRelativeToServer } =
364
396
  directoryContentItem;
397
+ let href = fileUrlRelativeToServer;
398
+ if (href === "") {
399
+ href = `${directoryContentMagicName}`;
400
+ }
365
401
  html += `
366
402
  <li class="directory_child" data-type="${type}">
367
- <a href="/${fileUrlRelativeToRoot}">${fileUrlRelativeToParent}</a>
403
+ <a href="/${href}">${fileUrlRelativeToParent}</a>
368
404
  </li>`;
369
405
  }
370
406
  html += `\n </ul>`;