@jsenv/core 39.7.7 → 39.8.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.
@@ -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, unlinkSync, readdirSync, rmdirSync, watch, createReadStream, readFile, existsSync, realpathSync } from "node:fs";
6
+ import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, unlinkSync, openSync, closeSync, readdirSync, rmdirSync, mkdirSync, readFileSync, writeFileSync as writeFileSync$1, unlink, rmdir, 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";
@@ -118,17 +118,29 @@ if (
118
118
  }
119
119
 
120
120
  function envForceColor() {
121
- if ('FORCE_COLOR' in env) {
122
- if (env.FORCE_COLOR === 'true') {
123
- return 1;
124
- }
121
+ if (!('FORCE_COLOR' in env)) {
122
+ return;
123
+ }
125
124
 
126
- if (env.FORCE_COLOR === 'false') {
127
- return 0;
128
- }
125
+ if (env.FORCE_COLOR === 'true') {
126
+ return 1;
127
+ }
129
128
 
130
- return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
129
+ if (env.FORCE_COLOR === 'false') {
130
+ return 0;
131
131
  }
132
+
133
+ if (env.FORCE_COLOR.length === 0) {
134
+ return 1;
135
+ }
136
+
137
+ const level = Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
138
+
139
+ if (![0, 1, 2, 3].includes(level)) {
140
+ return;
141
+ }
142
+
143
+ return level;
132
144
  }
133
145
 
134
146
  function translateLevel(level) {
@@ -199,11 +211,11 @@ function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) {
199
211
  }
200
212
 
201
213
  if ('CI' in env) {
202
- if ('GITHUB_ACTIONS' in env || 'GITEA_ACTIONS' in env) {
214
+ if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) {
203
215
  return 3;
204
216
  }
205
217
 
206
- if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
218
+ if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
207
219
  return 1;
208
220
  }
209
221
 
@@ -2058,6 +2070,42 @@ const extractDriveLetter = (resource) => {
2058
2070
  return null;
2059
2071
  };
2060
2072
 
2073
+ const getParentDirectoryUrl = (url) => {
2074
+ if (url.startsWith("file://")) {
2075
+ // With node.js new URL('../', 'file:///C:/').href
2076
+ // returns "file:///C:/" instead of "file:///"
2077
+ const resource = url.slice("file://".length);
2078
+ const slashLastIndex = resource.lastIndexOf("/");
2079
+ if (slashLastIndex === -1) {
2080
+ return url;
2081
+ }
2082
+ const lastCharIndex = resource.length - 1;
2083
+ if (slashLastIndex === lastCharIndex) {
2084
+ const slashBeforeLastIndex = resource.lastIndexOf(
2085
+ "/",
2086
+ slashLastIndex - 1,
2087
+ );
2088
+ if (slashBeforeLastIndex === -1) {
2089
+ return url;
2090
+ }
2091
+ return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
2092
+ }
2093
+ return `file://${resource.slice(0, slashLastIndex + 1)}`;
2094
+ }
2095
+ return new URL(url.endsWith("/") ? "../" : "./", url).href;
2096
+ };
2097
+
2098
+ const findAncestorDirectoryUrl = (url, callback) => {
2099
+ url = String(url);
2100
+ while (url !== "file:///") {
2101
+ if (callback(url)) {
2102
+ return url;
2103
+ }
2104
+ url = getParentDirectoryUrl(url);
2105
+ }
2106
+ return null;
2107
+ };
2108
+
2061
2109
  const createCallbackListNotifiedOnce = () => {
2062
2110
  let callbacks = [];
2063
2111
  let status = "waiting";
@@ -3610,30 +3658,9 @@ const normalizeMediaType = (value) => {
3610
3658
  return value;
3611
3659
  };
3612
3660
 
3613
- const writeFileSync = (destination, content = "") => {
3614
- const destinationUrl = assertAndNormalizeFileUrl(destination);
3615
- const destinationUrlObject = new URL(destinationUrl);
3616
- if (content && content instanceof URL) {
3617
- content = readFileSync(content);
3618
- }
3619
- try {
3620
- writeFileSync$1(destinationUrlObject, content);
3621
- } catch (error) {
3622
- if (error.code === "ENOENT") {
3623
- mkdirSync(new URL("./", destinationUrlObject), {
3624
- recursive: true,
3625
- });
3626
- writeFileSync$1(destinationUrlObject, content);
3627
- return;
3628
- }
3629
- throw error;
3630
- }
3631
- };
3632
-
3633
- const removeEntry = async (
3661
+ const removeEntrySync = (
3634
3662
  source,
3635
3663
  {
3636
- signal = new AbortController().signal,
3637
3664
  allowUseless = false,
3638
3665
  recursive = false,
3639
3666
  maxRetries = 3,
@@ -3642,110 +3669,74 @@ const removeEntry = async (
3642
3669
  } = {},
3643
3670
  ) => {
3644
3671
  const sourceUrl = assertAndNormalizeFileUrl(source);
3645
-
3646
- const removeOperation = Abort.startOperation();
3647
- removeOperation.addAbortSignal(signal);
3648
-
3649
- try {
3650
- removeOperation.throwIfAborted();
3651
- const sourceStats = await readEntryStat(sourceUrl, {
3652
- nullIfNotFound: true,
3653
- followLink: false,
3654
- });
3655
- if (!sourceStats) {
3656
- if (allowUseless) {
3657
- return;
3658
- }
3659
- throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`);
3672
+ const sourceStats = readEntryStatSync(sourceUrl, {
3673
+ nullIfNotFound: true,
3674
+ followLink: false,
3675
+ });
3676
+ if (!sourceStats) {
3677
+ if (allowUseless) {
3678
+ return;
3660
3679
  }
3680
+ throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`);
3681
+ }
3661
3682
 
3662
- // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats
3663
- // FIFO and socket are ignored, not sure what they are exactly and what to do with them
3664
- // other libraries ignore them, let's do the same.
3665
- if (
3666
- sourceStats.isFile() ||
3667
- sourceStats.isSymbolicLink() ||
3668
- sourceStats.isCharacterDevice() ||
3669
- sourceStats.isBlockDevice()
3670
- ) {
3671
- await removeNonDirectory$1(
3672
- sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl,
3673
- {
3674
- maxRetries,
3675
- retryDelay,
3676
- },
3677
- );
3678
- } else if (sourceStats.isDirectory()) {
3679
- await removeDirectory(ensurePathnameTrailingSlash(sourceUrl), {
3680
- signal: removeOperation.signal,
3681
- recursive,
3682
- maxRetries,
3683
- retryDelay,
3684
- onlyContent,
3685
- });
3686
- }
3687
- } finally {
3688
- await removeOperation.end();
3683
+ // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats
3684
+ // FIFO and socket are ignored, not sure what they are exactly and what to do with them
3685
+ // other libraries ignore them, let's do the same.
3686
+ if (
3687
+ sourceStats.isFile() ||
3688
+ sourceStats.isSymbolicLink() ||
3689
+ sourceStats.isCharacterDevice() ||
3690
+ sourceStats.isBlockDevice()
3691
+ ) {
3692
+ removeNonDirectory$1(
3693
+ sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl);
3694
+ } else if (sourceStats.isDirectory()) {
3695
+ const directoryUrl = ensurePathnameTrailingSlash(sourceUrl);
3696
+ removeDirectorySync$1(directoryUrl, {
3697
+ recursive,
3698
+ maxRetries,
3699
+ retryDelay,
3700
+ onlyContent,
3701
+ });
3689
3702
  }
3690
3703
  };
3691
3704
 
3692
- const removeNonDirectory$1 = (sourceUrl, { maxRetries, retryDelay }) => {
3705
+ const removeNonDirectory$1 = (sourceUrl) => {
3693
3706
  const sourcePath = urlToFileSystemPath(sourceUrl);
3694
-
3695
- let retryCount = 0;
3696
3707
  const attempt = () => {
3697
- return unlinkNaive(sourcePath, {
3698
- ...(retryCount >= maxRetries
3699
- ? {}
3700
- : {
3701
- handleTemporaryError: async () => {
3702
- retryCount++;
3703
- return new Promise((resolve) => {
3704
- setTimeout(() => {
3705
- resolve(attempt());
3706
- }, retryCount * retryDelay);
3707
- });
3708
- },
3709
- }),
3710
- });
3708
+ unlinkSyncNaive(sourcePath);
3711
3709
  };
3712
- return attempt();
3710
+ attempt();
3713
3711
  };
3714
3712
 
3715
- const unlinkNaive = (sourcePath, { handleTemporaryError = null } = {}) => {
3716
- return new Promise((resolve, reject) => {
3717
- unlink(sourcePath, (error) => {
3718
- if (error) {
3719
- if (error.code === "ENOENT") {
3720
- resolve();
3721
- } else if (
3722
- handleTemporaryError &&
3723
- (error.code === "EBUSY" ||
3724
- error.code === "EMFILE" ||
3725
- error.code === "ENFILE" ||
3726
- error.code === "ENOENT")
3727
- ) {
3728
- resolve(handleTemporaryError(error));
3729
- } else {
3730
- reject(error);
3731
- }
3732
- } else {
3733
- resolve();
3734
- }
3735
- });
3736
- });
3713
+ const unlinkSyncNaive = (sourcePath, { handleTemporaryError = null } = {}) => {
3714
+ try {
3715
+ unlinkSync(sourcePath);
3716
+ } catch (error) {
3717
+ if (error.code === "ENOENT") {
3718
+ return;
3719
+ }
3720
+ if (
3721
+ handleTemporaryError &&
3722
+ (error.code === "EBUSY" ||
3723
+ error.code === "EMFILE" ||
3724
+ error.code === "ENFILE" ||
3725
+ error.code === "ENOENT")
3726
+ ) {
3727
+ handleTemporaryError(error);
3728
+ return;
3729
+ }
3730
+ throw error;
3731
+ }
3737
3732
  };
3738
3733
 
3739
- const removeDirectory = async (
3734
+ const removeDirectorySync$1 = (
3740
3735
  rootDirectoryUrl,
3741
- { signal, maxRetries, retryDelay, recursive, onlyContent },
3736
+ { maxRetries, retryDelay, recursive, onlyContent },
3742
3737
  ) => {
3743
- const removeDirectoryOperation = Abort.startOperation();
3744
- removeDirectoryOperation.addAbortSignal(signal);
3745
-
3746
- const visit = async (sourceUrl) => {
3747
- removeDirectoryOperation.throwIfAborted();
3748
- const sourceStats = await readEntryStat(sourceUrl, {
3738
+ const visit = (sourceUrl) => {
3739
+ const sourceStats = readEntryStatSync(sourceUrl, {
3749
3740
  nullIfNotFound: true,
3750
3741
  followLink: false,
3751
3742
  });
@@ -3760,31 +3751,30 @@ const removeDirectory = async (
3760
3751
  sourceStats.isCharacterDevice() ||
3761
3752
  sourceStats.isBlockDevice()
3762
3753
  ) {
3763
- await visitFile(sourceUrl);
3754
+ visitFile(sourceUrl);
3764
3755
  } else if (sourceStats.isSymbolicLink()) {
3765
- await visitSymbolicLink(sourceUrl);
3756
+ visitSymbolicLink(sourceUrl);
3766
3757
  } else if (sourceStats.isDirectory()) {
3767
- await visitDirectory(`${sourceUrl}/`);
3758
+ visitDirectory(`${sourceUrl}/`);
3768
3759
  }
3769
3760
  };
3770
3761
 
3771
- const visitDirectory = async (directoryUrl) => {
3762
+ const visitDirectory = (directoryUrl) => {
3772
3763
  const directoryPath = urlToFileSystemPath(directoryUrl);
3773
3764
  const optionsFromRecursive = recursive
3774
3765
  ? {
3775
- handleNotEmptyError: async () => {
3776
- await removeDirectoryContent(directoryUrl);
3777
- await visitDirectory(directoryUrl);
3766
+ handleNotEmptyError: () => {
3767
+ removeDirectoryContent(directoryUrl);
3768
+ visitDirectory(directoryUrl);
3778
3769
  },
3779
3770
  }
3780
3771
  : {};
3781
- removeDirectoryOperation.throwIfAborted();
3782
- await removeDirectoryNaive(directoryPath, {
3772
+ removeDirectorySyncNaive(directoryPath, {
3783
3773
  ...optionsFromRecursive,
3784
3774
  // Workaround for https://github.com/joyent/node/issues/4337
3785
3775
  ...(process.platform === "win32"
3786
3776
  ? {
3787
- handlePermissionError: async (error) => {
3777
+ handlePermissionError: (error) => {
3788
3778
  console.error(
3789
3779
  `trying to fix windows EPERM after readir on ${directoryPath}`,
3790
3780
  );
@@ -3806,8 +3796,7 @@ const removeDirectory = async (
3806
3796
  );
3807
3797
  throw error;
3808
3798
  }
3809
-
3810
- await removeDirectoryNaive(directoryPath, {
3799
+ removeDirectorySyncNaive(directoryPath, {
3811
3800
  ...optionsFromRecursive,
3812
3801
  });
3813
3802
  },
@@ -3816,70 +3805,170 @@ const removeDirectory = async (
3816
3805
  });
3817
3806
  };
3818
3807
 
3819
- const removeDirectoryContent = async (directoryUrl) => {
3820
- removeDirectoryOperation.throwIfAborted();
3821
- const names = await readDirectory(directoryUrl);
3822
- await Promise.all(
3823
- names.map(async (name) => {
3824
- const url = resolveUrl$1(name, directoryUrl);
3825
- await visit(url);
3826
- }),
3827
- );
3808
+ const removeDirectoryContent = (directoryUrl) => {
3809
+ const entryNames = readdirSync(new URL(directoryUrl));
3810
+ for (const entryName of entryNames) {
3811
+ const url = resolveUrl$1(entryName, directoryUrl);
3812
+ visit(url);
3813
+ }
3828
3814
  };
3829
3815
 
3830
- const visitFile = async (fileUrl) => {
3831
- await removeNonDirectory$1(fileUrl, { maxRetries, retryDelay });
3816
+ const visitFile = (fileUrl) => {
3817
+ removeNonDirectory$1(fileUrl);
3832
3818
  };
3833
3819
 
3834
- const visitSymbolicLink = async (symbolicLinkUrl) => {
3835
- await removeNonDirectory$1(symbolicLinkUrl, { maxRetries, retryDelay });
3820
+ const visitSymbolicLink = (symbolicLinkUrl) => {
3821
+ removeNonDirectory$1(symbolicLinkUrl);
3836
3822
  };
3837
3823
 
3838
- try {
3839
- if (onlyContent) {
3840
- await removeDirectoryContent(rootDirectoryUrl);
3841
- } else {
3842
- await visitDirectory(rootDirectoryUrl);
3843
- }
3844
- } finally {
3845
- await removeDirectoryOperation.end();
3824
+ if (onlyContent) {
3825
+ removeDirectoryContent(rootDirectoryUrl);
3826
+ } else {
3827
+ visitDirectory(rootDirectoryUrl);
3846
3828
  }
3847
3829
  };
3848
3830
 
3849
- const removeDirectoryNaive = (
3831
+ const removeDirectorySyncNaive = (
3850
3832
  directoryPath,
3851
3833
  { handleNotEmptyError = null, handlePermissionError = null } = {},
3852
3834
  ) => {
3853
- return new Promise((resolve, reject) => {
3854
- rmdir(directoryPath, (error, lstatObject) => {
3855
- if (error) {
3856
- if (handlePermissionError && error.code === "EPERM") {
3857
- resolve(handlePermissionError(error));
3858
- } else if (error.code === "ENOENT") {
3859
- resolve();
3860
- } else if (
3861
- handleNotEmptyError &&
3862
- // linux os
3863
- (error.code === "ENOTEMPTY" ||
3864
- // SunOS
3865
- error.code === "EEXIST")
3866
- ) {
3867
- resolve(handleNotEmptyError(error));
3868
- } else {
3869
- reject(error);
3835
+ try {
3836
+ rmdirSync(directoryPath);
3837
+ } catch (error) {
3838
+ if (handlePermissionError && error.code === "EPERM") {
3839
+ handlePermissionError(error);
3840
+ return;
3841
+ }
3842
+ if (error.code === "ENOENT") {
3843
+ return;
3844
+ }
3845
+ if (
3846
+ handleNotEmptyError &&
3847
+ // linux os
3848
+ (error.code === "ENOTEMPTY" ||
3849
+ // SunOS
3850
+ error.code === "EEXIST")
3851
+ ) {
3852
+ handleNotEmptyError(error);
3853
+ return;
3854
+ }
3855
+ throw error;
3856
+ }
3857
+ };
3858
+
3859
+ const removeDirectorySync = (url, options = {}) => {
3860
+ return removeEntrySync(url, {
3861
+ ...options,
3862
+ recursive: true,
3863
+ });
3864
+ };
3865
+
3866
+ const writeDirectorySync = (
3867
+ destination,
3868
+ { recursive = true, allowUseless = false, force } = {},
3869
+ ) => {
3870
+ const destinationUrl = assertAndNormalizeDirectoryUrl(destination);
3871
+ const destinationPath = urlToFileSystemPath(destinationUrl);
3872
+
3873
+ let destinationStats;
3874
+ try {
3875
+ destinationStats = readEntryStatSync(destinationUrl, {
3876
+ nullIfNotFound: true,
3877
+ followLink: false,
3878
+ });
3879
+ } catch (e) {
3880
+ if (e.code === "ENOTDIR") {
3881
+ let previousNonDirUrl = destinationUrl;
3882
+ // we must try all parent directories as long as it fails with ENOTDIR
3883
+ findAncestorDirectoryUrl(destinationUrl, (ancestorUrl) => {
3884
+ try {
3885
+ statSync(new URL(ancestorUrl));
3886
+ return true;
3887
+ } catch (e) {
3888
+ if (e.code === "ENOTDIR") {
3889
+ previousNonDirUrl = ancestorUrl;
3890
+ return false;
3891
+ }
3892
+ throw e;
3870
3893
  }
3894
+ });
3895
+ if (force) {
3896
+ unlinkSync(
3897
+ new URL(
3898
+ previousNonDirUrl
3899
+ // remove trailing slash
3900
+ .slice(0, -1),
3901
+ ),
3902
+ );
3871
3903
  } else {
3872
- resolve(lstatObject);
3904
+ throw new Error(
3905
+ `cannot write directory at ${destinationPath} because there is a file at ${urlToFileSystemPath(
3906
+ previousNonDirUrl,
3907
+ )}`,
3908
+ );
3873
3909
  }
3874
- });
3875
- });
3910
+ } else {
3911
+ throw e;
3912
+ }
3913
+ }
3914
+
3915
+ if (destinationStats) {
3916
+ if (destinationStats.isDirectory()) {
3917
+ if (allowUseless) {
3918
+ return;
3919
+ }
3920
+ throw new Error(`directory already exists at ${destinationPath}`);
3921
+ }
3922
+ const destinationType = statsToType(destinationStats);
3923
+ throw new Error(
3924
+ `cannot write directory at ${destinationPath} because there is a ${destinationType}`,
3925
+ );
3926
+ }
3927
+
3928
+ try {
3929
+ mkdirSync(destinationPath, { recursive });
3930
+ } catch (error) {
3931
+ if (allowUseless && error.code === "EEXIST") {
3932
+ return;
3933
+ }
3934
+ throw error;
3935
+ }
3876
3936
  };
3877
3937
 
3878
- process.platform === "win32";
3938
+ const writeFileSync = (destination, content = "", { force } = {}) => {
3939
+ const destinationUrl = assertAndNormalizeFileUrl(destination);
3940
+ const destinationUrlObject = new URL(destinationUrl);
3941
+ if (content && content instanceof URL) {
3942
+ content = readFileSync(content);
3943
+ }
3944
+ try {
3945
+ writeFileSync$1(destinationUrlObject, content);
3946
+ } catch (error) {
3947
+ if (error.code === "EISDIR") {
3948
+ // happens when directory existed but got deleted and now it's a file
3949
+ if (force) {
3950
+ removeDirectorySync(destinationUrlObject);
3951
+ writeFileSync$1(destinationUrlObject, content);
3952
+ } else {
3953
+ throw error;
3954
+ }
3955
+ }
3956
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") {
3957
+ writeDirectorySync(new URL("./", destinationUrlObject), {
3958
+ force,
3959
+ recursive: true,
3960
+ });
3961
+ writeFileSync$1(destinationUrlObject, content);
3962
+ return;
3963
+ }
3964
+ throw error;
3965
+ }
3966
+ };
3879
3967
 
3880
- const removeEntrySync = (
3968
+ const removeEntry = async (
3881
3969
  source,
3882
3970
  {
3971
+ signal = new AbortController().signal,
3883
3972
  allowUseless = false,
3884
3973
  recursive = false,
3885
3974
  maxRetries = 3,
@@ -3888,74 +3977,110 @@ const removeEntrySync = (
3888
3977
  } = {},
3889
3978
  ) => {
3890
3979
  const sourceUrl = assertAndNormalizeFileUrl(source);
3891
- const sourceStats = readEntryStatSync(sourceUrl, {
3892
- nullIfNotFound: true,
3893
- followLink: false,
3894
- });
3895
- if (!sourceStats) {
3896
- if (allowUseless) {
3897
- return;
3980
+
3981
+ const removeOperation = Abort.startOperation();
3982
+ removeOperation.addAbortSignal(signal);
3983
+
3984
+ try {
3985
+ removeOperation.throwIfAborted();
3986
+ const sourceStats = await readEntryStat(sourceUrl, {
3987
+ nullIfNotFound: true,
3988
+ followLink: false,
3989
+ });
3990
+ if (!sourceStats) {
3991
+ if (allowUseless) {
3992
+ return;
3993
+ }
3994
+ throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`);
3898
3995
  }
3899
- throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`);
3996
+
3997
+ // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats
3998
+ // FIFO and socket are ignored, not sure what they are exactly and what to do with them
3999
+ // other libraries ignore them, let's do the same.
4000
+ if (
4001
+ sourceStats.isFile() ||
4002
+ sourceStats.isSymbolicLink() ||
4003
+ sourceStats.isCharacterDevice() ||
4004
+ sourceStats.isBlockDevice()
4005
+ ) {
4006
+ await removeNonDirectory(
4007
+ sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl,
4008
+ {
4009
+ maxRetries,
4010
+ retryDelay,
4011
+ },
4012
+ );
4013
+ } else if (sourceStats.isDirectory()) {
4014
+ await removeDirectory(ensurePathnameTrailingSlash(sourceUrl), {
4015
+ signal: removeOperation.signal,
4016
+ recursive,
4017
+ maxRetries,
4018
+ retryDelay,
4019
+ onlyContent,
4020
+ });
4021
+ }
4022
+ } finally {
4023
+ await removeOperation.end();
3900
4024
  }
4025
+ };
3901
4026
 
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,
4027
+ const removeNonDirectory = (sourceUrl, { maxRetries, retryDelay }) => {
4028
+ const sourcePath = urlToFileSystemPath(sourceUrl);
4029
+
4030
+ let retryCount = 0;
4031
+ const attempt = () => {
4032
+ return unlinkNaive(sourcePath, {
4033
+ ...(retryCount >= maxRetries
4034
+ ? {}
4035
+ : {
4036
+ handleTemporaryError: async () => {
4037
+ retryCount++;
4038
+ return new Promise((resolve) => {
4039
+ setTimeout(() => {
4040
+ resolve(attempt());
4041
+ }, retryCount * retryDelay);
4042
+ });
4043
+ },
4044
+ }),
3920
4045
  });
3921
- }
3922
- };
3923
-
3924
- const removeNonDirectory = (sourceUrl) => {
3925
- const sourcePath = urlToFileSystemPath(sourceUrl);
3926
- const attempt = () => {
3927
- unlinkSyncNaive(sourcePath);
3928
4046
  };
3929
- attempt();
4047
+ return attempt();
3930
4048
  };
3931
4049
 
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
- }
4050
+ const unlinkNaive = (sourcePath, { handleTemporaryError = null } = {}) => {
4051
+ return new Promise((resolve, reject) => {
4052
+ unlink(sourcePath, (error) => {
4053
+ if (error) {
4054
+ if (error.code === "ENOENT") {
4055
+ resolve();
4056
+ } else if (
4057
+ handleTemporaryError &&
4058
+ (error.code === "EBUSY" ||
4059
+ error.code === "EMFILE" ||
4060
+ error.code === "ENFILE" ||
4061
+ error.code === "ENOENT")
4062
+ ) {
4063
+ resolve(handleTemporaryError(error));
4064
+ } else {
4065
+ reject(error);
4066
+ }
4067
+ } else {
4068
+ resolve();
4069
+ }
4070
+ });
4071
+ });
3951
4072
  };
3952
4073
 
3953
- const removeDirectorySync$1 = (
4074
+ const removeDirectory = async (
3954
4075
  rootDirectoryUrl,
3955
- { maxRetries, retryDelay, recursive, onlyContent },
4076
+ { signal, maxRetries, retryDelay, recursive, onlyContent },
3956
4077
  ) => {
3957
- const visit = (sourceUrl) => {
3958
- const sourceStats = readEntryStatSync(sourceUrl, {
4078
+ const removeDirectoryOperation = Abort.startOperation();
4079
+ removeDirectoryOperation.addAbortSignal(signal);
4080
+
4081
+ const visit = async (sourceUrl) => {
4082
+ removeDirectoryOperation.throwIfAborted();
4083
+ const sourceStats = await readEntryStat(sourceUrl, {
3959
4084
  nullIfNotFound: true,
3960
4085
  followLink: false,
3961
4086
  });
@@ -3970,30 +4095,31 @@ const removeDirectorySync$1 = (
3970
4095
  sourceStats.isCharacterDevice() ||
3971
4096
  sourceStats.isBlockDevice()
3972
4097
  ) {
3973
- visitFile(sourceUrl);
4098
+ await visitFile(sourceUrl);
3974
4099
  } else if (sourceStats.isSymbolicLink()) {
3975
- visitSymbolicLink(sourceUrl);
4100
+ await visitSymbolicLink(sourceUrl);
3976
4101
  } else if (sourceStats.isDirectory()) {
3977
- visitDirectory(`${sourceUrl}/`);
4102
+ await visitDirectory(`${sourceUrl}/`);
3978
4103
  }
3979
4104
  };
3980
4105
 
3981
- const visitDirectory = (directoryUrl) => {
4106
+ const visitDirectory = async (directoryUrl) => {
3982
4107
  const directoryPath = urlToFileSystemPath(directoryUrl);
3983
4108
  const optionsFromRecursive = recursive
3984
4109
  ? {
3985
- handleNotEmptyError: () => {
3986
- removeDirectoryContent(directoryUrl);
3987
- visitDirectory(directoryUrl);
4110
+ handleNotEmptyError: async () => {
4111
+ await removeDirectoryContent(directoryUrl);
4112
+ await visitDirectory(directoryUrl);
3988
4113
  },
3989
4114
  }
3990
4115
  : {};
3991
- removeDirectorySyncNaive(directoryPath, {
4116
+ removeDirectoryOperation.throwIfAborted();
4117
+ await removeDirectoryNaive(directoryPath, {
3992
4118
  ...optionsFromRecursive,
3993
4119
  // Workaround for https://github.com/joyent/node/issues/4337
3994
4120
  ...(process.platform === "win32"
3995
4121
  ? {
3996
- handlePermissionError: (error) => {
4122
+ handlePermissionError: async (error) => {
3997
4123
  console.error(
3998
4124
  `trying to fix windows EPERM after readir on ${directoryPath}`,
3999
4125
  );
@@ -4015,7 +4141,8 @@ const removeDirectorySync$1 = (
4015
4141
  );
4016
4142
  throw error;
4017
4143
  }
4018
- removeDirectorySyncNaive(directoryPath, {
4144
+
4145
+ await removeDirectoryNaive(directoryPath, {
4019
4146
  ...optionsFromRecursive,
4020
4147
  });
4021
4148
  },
@@ -4024,59 +4151,69 @@ const removeDirectorySync$1 = (
4024
4151
  });
4025
4152
  };
4026
4153
 
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
- }
4154
+ const removeDirectoryContent = async (directoryUrl) => {
4155
+ removeDirectoryOperation.throwIfAborted();
4156
+ const names = await readDirectory(directoryUrl);
4157
+ await Promise.all(
4158
+ names.map(async (name) => {
4159
+ const url = resolveUrl$1(name, directoryUrl);
4160
+ await visit(url);
4161
+ }),
4162
+ );
4033
4163
  };
4034
4164
 
4035
- const visitFile = (fileUrl) => {
4036
- removeNonDirectory(fileUrl);
4165
+ const visitFile = async (fileUrl) => {
4166
+ await removeNonDirectory(fileUrl, { maxRetries, retryDelay });
4037
4167
  };
4038
4168
 
4039
- const visitSymbolicLink = (symbolicLinkUrl) => {
4040
- removeNonDirectory(symbolicLinkUrl);
4169
+ const visitSymbolicLink = async (symbolicLinkUrl) => {
4170
+ await removeNonDirectory(symbolicLinkUrl, { maxRetries, retryDelay });
4041
4171
  };
4042
4172
 
4043
- if (onlyContent) {
4044
- removeDirectoryContent(rootDirectoryUrl);
4045
- } else {
4046
- visitDirectory(rootDirectoryUrl);
4173
+ try {
4174
+ if (onlyContent) {
4175
+ await removeDirectoryContent(rootDirectoryUrl);
4176
+ } else {
4177
+ await visitDirectory(rootDirectoryUrl);
4178
+ }
4179
+ } finally {
4180
+ await removeDirectoryOperation.end();
4047
4181
  }
4048
4182
  };
4049
4183
 
4050
- const removeDirectorySyncNaive = (
4184
+ const removeDirectoryNaive = (
4051
4185
  directoryPath,
4052
4186
  { handleNotEmptyError = null, handlePermissionError = null } = {},
4053
4187
  ) => {
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
- }
4188
+ return new Promise((resolve, reject) => {
4189
+ rmdir(directoryPath, (error, lstatObject) => {
4190
+ if (error) {
4191
+ if (handlePermissionError && error.code === "EPERM") {
4192
+ resolve(handlePermissionError(error));
4193
+ } else if (error.code === "ENOENT") {
4194
+ resolve();
4195
+ } else if (
4196
+ handleNotEmptyError &&
4197
+ // linux os
4198
+ (error.code === "ENOTEMPTY" ||
4199
+ // SunOS
4200
+ error.code === "EEXIST")
4201
+ ) {
4202
+ resolve(handleNotEmptyError(error));
4203
+ } else {
4204
+ reject(error);
4205
+ }
4206
+ } else {
4207
+ resolve(lstatObject);
4208
+ }
4209
+ });
4210
+ });
4076
4211
  };
4077
4212
 
4078
4213
  process.platform === "win32";
4079
4214
 
4215
+ process.platform === "win32";
4216
+
4080
4217
  const ensureEmptyDirectory = async (source) => {
4081
4218
  const stats = await readEntryStat(source, {
4082
4219
  nullIfNotFound: true,
@@ -4104,13 +4241,6 @@ const ensureEmptyDirectory = async (source) => {
4104
4241
  );
4105
4242
  };
4106
4243
 
4107
- const removeDirectorySync = (url, options = {}) => {
4108
- return removeEntrySync(url, {
4109
- ...options,
4110
- recursive: true,
4111
- });
4112
- };
4113
-
4114
4244
  const callOnceIdlePerFile = (callback, idleMs) => {
4115
4245
  const timeoutIdMap = new Map();
4116
4246
  return (fileEvent) => {
@@ -11364,39 +11494,10 @@ const jsenvPluginTranspilation = ({
11364
11494
  };
11365
11495
 
11366
11496
  const lookupPackageDirectory = (currentUrl) => {
11367
- if (currentUrl === "file:///") {
11368
- return null;
11369
- }
11370
- const packageJsonFileUrl = `${currentUrl}package.json`;
11371
- if (existsSync(new URL(packageJsonFileUrl))) {
11372
- return currentUrl;
11373
- }
11374
- return lookupPackageDirectory(getParentUrl$1(currentUrl));
11375
- };
11376
-
11377
- const getParentUrl$1 = (url) => {
11378
- if (url.startsWith("file://")) {
11379
- // With node.js new URL('../', 'file:///C:/').href
11380
- // returns "file:///C:/" instead of "file:///"
11381
- const resource = url.slice("file://".length);
11382
- const slashLastIndex = resource.lastIndexOf("/");
11383
- if (slashLastIndex === -1) {
11384
- return url;
11385
- }
11386
- const lastCharIndex = resource.length - 1;
11387
- if (slashLastIndex === lastCharIndex) {
11388
- const slashBeforeLastIndex = resource.lastIndexOf(
11389
- "/",
11390
- slashLastIndex - 1,
11391
- );
11392
- if (slashBeforeLastIndex === -1) {
11393
- return url;
11394
- }
11395
- return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
11396
- }
11397
- return `file://${resource.slice(0, slashLastIndex + 1)}`;
11398
- }
11399
- return new URL(url.endsWith("/") ? "../" : "./", url).href;
11497
+ return findAncestorDirectoryUrl(currentUrl, (ancestorDirectoryUrl) => {
11498
+ const potentialPackageJsonFileUrl = `${ancestorDirectoryUrl}package.json`;
11499
+ return existsSync(new URL(potentialPackageJsonFileUrl));
11500
+ });
11400
11501
  };
11401
11502
 
11402
11503
  const watchSourceFiles = (
@@ -14368,18 +14469,7 @@ const createUrlInfoTransformer = ({
14368
14469
  contentIsInlined = false;
14369
14470
  }
14370
14471
  if (!contentIsInlined) {
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
- }
14472
+ writeFileSync(new URL(generatedUrl), urlInfo.content, { force: true });
14383
14473
  }
14384
14474
  const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
14385
14475
  if (sourcemapGeneratedUrl && sourcemapReference) {
@@ -19175,6 +19265,7 @@ const jsenvPluginProtocolFile = ({
19175
19265
  if (!urlInfo.url.startsWith("file:")) {
19176
19266
  return null;
19177
19267
  }
19268
+ const { rootDirectoryUrl } = urlInfo.context;
19178
19269
  const generateContent = () => {
19179
19270
  const urlObject = new URL(urlInfo.url);
19180
19271
  const { firstReference } = urlInfo;
@@ -19193,11 +19284,12 @@ const jsenvPluginProtocolFile = ({
19193
19284
  : false;
19194
19285
  if (acceptsHtml) {
19195
19286
  firstReference.expectedType = "html";
19196
- const html = generateHtmlForDirectory(
19197
- urlObject.href,
19198
- directoryContentArray,
19199
- urlInfo.context.rootDirectoryUrl,
19287
+ const directoryUrl = urlObject.href;
19288
+ const directoryContentItems = generateDirectoryContentItems(
19289
+ directoryUrl,
19290
+ rootDirectoryUrl,
19200
19291
  );
19292
+ const html = generateHtmlForDirectory(directoryContentItems);
19201
19293
  return {
19202
19294
  type: "html",
19203
19295
  contentType: "text/html",
@@ -19230,32 +19322,13 @@ const jsenvPluginProtocolFile = ({
19230
19322
  if (e.code !== "ENOENT") {
19231
19323
  throw e;
19232
19324
  }
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
- }
19249
- }
19250
-
19251
- const firstExistingAncestorDirectoryContent = readdirSync(
19252
- new URL(firstExistingAncestorDirectoryUrl),
19325
+ const directoryContentItems = generateDirectoryContentItems(
19326
+ urlInfo.url,
19327
+ rootDirectoryUrl,
19253
19328
  );
19254
19329
  const html = generateHtmlForENOENT(
19255
19330
  urlInfo.url,
19256
- firstExistingAncestorDirectoryContent,
19257
- firstExistingAncestorDirectoryUrl,
19258
- urlInfo.context.rootDirectoryUrl,
19331
+ directoryContentItems,
19259
19332
  directoryListingUrlMocks,
19260
19333
  );
19261
19334
  return {
@@ -19274,11 +19347,9 @@ const jsenvPluginProtocolFile = ({
19274
19347
  ];
19275
19348
  };
19276
19349
 
19277
- const generateHtmlForDirectory = (
19278
- directoryUrl,
19279
- directoryContentArray,
19280
- rootDirectoryUrl,
19281
- ) => {
19350
+ const generateHtmlForDirectory = (directoryContentItems) => {
19351
+ let directoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19352
+ const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19282
19353
  directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
19283
19354
 
19284
19355
  const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
@@ -19287,23 +19358,19 @@ const generateHtmlForDirectory = (
19287
19358
  directoryUrl,
19288
19359
  directoryNav: () =>
19289
19360
  generateDirectoryNav(directoryRelativeUrl, rootDirectoryUrl),
19290
- directoryContent: () =>
19291
- generateDirectoryContent(
19292
- directoryContentArray,
19293
- directoryUrl,
19294
- rootDirectoryUrl,
19295
- ),
19361
+ directoryContent: () => generateDirectoryContent(directoryContentItems),
19296
19362
  };
19297
19363
  const html = replacePlaceholders$1(htmlForDirectory, replacers);
19298
19364
  return html;
19299
19365
  };
19300
19366
  const generateHtmlForENOENT = (
19301
19367
  url,
19302
- ancestorDirectoryContentArray,
19303
- ancestorDirectoryUrl,
19304
- rootDirectoryUrl,
19368
+ directoryContentItems,
19305
19369
  directoryListingUrlMocks,
19306
19370
  ) => {
19371
+ const ancestorDirectoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19372
+ const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19373
+
19307
19374
  const htmlFor404AndAncestorDir = String(
19308
19375
  readFileSync(html404AndAncestorDirFileUrl),
19309
19376
  );
@@ -19322,11 +19389,7 @@ const generateHtmlForENOENT = (
19322
19389
  ancestorDirectoryNav: () =>
19323
19390
  generateDirectoryNav(ancestorDirectoryRelativeUrl, rootDirectoryUrl),
19324
19391
  ancestorDirectoryContent: () =>
19325
- generateDirectoryContent(
19326
- ancestorDirectoryContentArray,
19327
- ancestorDirectoryUrl,
19328
- rootDirectoryUrl,
19329
- ),
19392
+ generateDirectoryContent(directoryContentItems),
19330
19393
  };
19331
19394
  const html = replacePlaceholders$1(htmlFor404AndAncestorDir, replacers);
19332
19395
  return html;
@@ -19368,31 +19431,95 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19368
19431
  }
19369
19432
  return dirPartsHtml;
19370
19433
  };
19371
- const generateDirectoryContent = (
19372
- directoryContentArray,
19373
- directoryUrl,
19374
- rootDirectoryUrl,
19375
- ) => {
19376
- if (directoryContentArray.length === 0) {
19377
- return `<p>Directory is empty</p>`;
19434
+ const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
19435
+ let firstExistingDirectoryUrl = new URL("./", directoryUrl);
19436
+ while (!existsSync(firstExistingDirectoryUrl)) {
19437
+ firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
19438
+ if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrl)) {
19439
+ firstExistingDirectoryUrl = new URL(rootDirectoryUrl);
19440
+ break;
19441
+ }
19378
19442
  }
19379
- const sortedNames = [];
19443
+ const directoryContentArray = readdirSync(firstExistingDirectoryUrl);
19444
+ const fileUrls = [];
19380
19445
  for (const filename of directoryContentArray) {
19381
- const fileUrlObject = new URL(filename, directoryUrl);
19382
- if (lstatSync(fileUrlObject).isDirectory()) {
19383
- sortedNames.push(`${filename}/`);
19446
+ const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
19447
+ fileUrls.push(fileUrlObject);
19448
+ }
19449
+ package_workspaces: {
19450
+ if (String(firstExistingDirectoryUrl) !== String(rootDirectoryUrl)) {
19451
+ break package_workspaces;
19452
+ }
19453
+ const packageDirectoryUrl = lookupPackageDirectory(rootDirectoryUrl);
19454
+ if (!packageDirectoryUrl) {
19455
+ break package_workspaces;
19456
+ }
19457
+ if (String(packageDirectoryUrl) === String(rootDirectoryUrl)) {
19458
+ break package_workspaces;
19459
+ }
19460
+ let packageContent;
19461
+ try {
19462
+ packageContent = JSON.parse(
19463
+ readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
19464
+ );
19465
+ } catch {
19466
+ break package_workspaces;
19467
+ }
19468
+ const { workspaces } = packageContent;
19469
+ if (Array.isArray(workspaces)) {
19470
+ for (const workspace of workspaces) {
19471
+ const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
19472
+ const workspaceUrl = workspaceUrlObject.href;
19473
+ if (workspaceUrl.endsWith("*")) {
19474
+ const directoryUrl = ensurePathnameTrailingSlash(
19475
+ workspaceUrl.slice(0, -1),
19476
+ );
19477
+ fileUrls.push(new URL(directoryUrl));
19478
+ } else {
19479
+ fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
19480
+ }
19481
+ }
19482
+ }
19483
+ }
19484
+
19485
+ const sortedUrls = [];
19486
+ for (let fileUrl of fileUrls) {
19487
+ if (lstatSync(fileUrl).isDirectory()) {
19488
+ sortedUrls.push(ensurePathnameTrailingSlash(fileUrl));
19384
19489
  } else {
19385
- sortedNames.push(filename);
19490
+ sortedUrls.push(fileUrl);
19386
19491
  }
19387
19492
  }
19388
- sortedNames.sort(comparePathnames);
19389
- let html = `<ul class="directory_content">`;
19390
- for (const filename of sortedNames) {
19391
- const fileUrlObject = new URL(filename, directoryUrl);
19392
- const fileUrl = String(fileUrlObject);
19393
- const fileUrlRelativeToParent = urlToRelativeUrl(fileUrl, directoryUrl);
19394
- const fileUrlRelativeToRoot = urlToRelativeUrl(fileUrl, rootDirectoryUrl);
19493
+ sortedUrls.sort((a, b) => {
19494
+ return comparePathnames(a.pathname, b.pathname);
19495
+ });
19496
+
19497
+ const items = [];
19498
+ for (const sortedUrl of sortedUrls) {
19499
+ const fileUrlRelativeToParent = urlToRelativeUrl(
19500
+ sortedUrl,
19501
+ firstExistingDirectoryUrl,
19502
+ );
19503
+ const fileUrlRelativeToRoot = urlToRelativeUrl(sortedUrl, rootDirectoryUrl);
19395
19504
  const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
19505
+ items.push({
19506
+ type,
19507
+ fileUrlRelativeToParent,
19508
+ fileUrlRelativeToRoot,
19509
+ });
19510
+ }
19511
+ items.rootDirectoryUrl = rootDirectoryUrl;
19512
+ items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
19513
+ return items;
19514
+ };
19515
+ const generateDirectoryContent = (directoryContentItems) => {
19516
+ if (directoryContentItems.length === 0) {
19517
+ return `<p>Directory is empty</p>`;
19518
+ }
19519
+ let html = `<ul class="directory_content">`;
19520
+ for (const directoryContentItem of directoryContentItems) {
19521
+ const { type, fileUrlRelativeToParent, fileUrlRelativeToRoot } =
19522
+ directoryContentItem;
19396
19523
  html += `
19397
19524
  <li class="directory_child" data-type="${type}">
19398
19525
  <a href="/${fileUrlRelativeToRoot}">${fileUrlRelativeToParent}</a>