@jsenv/core 39.7.6 → 39.8.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.
@@ -3,7 +3,7 @@ import os, { networkInterfaces } from "node:os";
3
3
  import tty from "node:tty";
4
4
  import stringWidth from "string-width";
5
5
  import { pathToFileURL, fileURLToPath } from "node:url";
6
- import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFileSync, writeFileSync as writeFileSync$1, mkdirSync, unlink, openSync, closeSync, rmdir, watch, readdirSync, createReadStream, readFile, existsSync, realpathSync } from "node:fs";
6
+ import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, 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";
@@ -2058,6 +2058,42 @@ const extractDriveLetter = (resource) => {
2058
2058
  return null;
2059
2059
  };
2060
2060
 
2061
+ const getParentDirectoryUrl = (url) => {
2062
+ if (url.startsWith("file://")) {
2063
+ // With node.js new URL('../', 'file:///C:/').href
2064
+ // returns "file:///C:/" instead of "file:///"
2065
+ const resource = url.slice("file://".length);
2066
+ const slashLastIndex = resource.lastIndexOf("/");
2067
+ if (slashLastIndex === -1) {
2068
+ return url;
2069
+ }
2070
+ const lastCharIndex = resource.length - 1;
2071
+ if (slashLastIndex === lastCharIndex) {
2072
+ const slashBeforeLastIndex = resource.lastIndexOf(
2073
+ "/",
2074
+ slashLastIndex - 1,
2075
+ );
2076
+ if (slashBeforeLastIndex === -1) {
2077
+ return url;
2078
+ }
2079
+ return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
2080
+ }
2081
+ return `file://${resource.slice(0, slashLastIndex + 1)}`;
2082
+ }
2083
+ return new URL(url.endsWith("/") ? "../" : "./", url).href;
2084
+ };
2085
+
2086
+ const findAncestorDirectoryUrl = (url, callback) => {
2087
+ url = String(url);
2088
+ while (url !== "file:///") {
2089
+ if (callback(url)) {
2090
+ return url;
2091
+ }
2092
+ url = getParentDirectoryUrl(url);
2093
+ }
2094
+ return null;
2095
+ };
2096
+
2061
2097
  const createCallbackListNotifiedOnce = () => {
2062
2098
  let callbacks = [];
2063
2099
  let status = "waiting";
@@ -3610,7 +3646,284 @@ const normalizeMediaType = (value) => {
3610
3646
  return value;
3611
3647
  };
3612
3648
 
3613
- const writeFileSync = (destination, content = "") => {
3649
+ const removeEntrySync = (
3650
+ source,
3651
+ {
3652
+ allowUseless = false,
3653
+ recursive = false,
3654
+ maxRetries = 3,
3655
+ retryDelay = 100,
3656
+ onlyContent = false,
3657
+ } = {},
3658
+ ) => {
3659
+ const sourceUrl = assertAndNormalizeFileUrl(source);
3660
+ const sourceStats = readEntryStatSync(sourceUrl, {
3661
+ nullIfNotFound: true,
3662
+ followLink: false,
3663
+ });
3664
+ if (!sourceStats) {
3665
+ if (allowUseless) {
3666
+ return;
3667
+ }
3668
+ throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`);
3669
+ }
3670
+
3671
+ // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats
3672
+ // FIFO and socket are ignored, not sure what they are exactly and what to do with them
3673
+ // other libraries ignore them, let's do the same.
3674
+ if (
3675
+ sourceStats.isFile() ||
3676
+ sourceStats.isSymbolicLink() ||
3677
+ sourceStats.isCharacterDevice() ||
3678
+ sourceStats.isBlockDevice()
3679
+ ) {
3680
+ removeNonDirectory$1(
3681
+ sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl);
3682
+ } else if (sourceStats.isDirectory()) {
3683
+ const directoryUrl = ensurePathnameTrailingSlash(sourceUrl);
3684
+ removeDirectorySync$1(directoryUrl, {
3685
+ recursive,
3686
+ maxRetries,
3687
+ retryDelay,
3688
+ onlyContent,
3689
+ });
3690
+ }
3691
+ };
3692
+
3693
+ const removeNonDirectory$1 = (sourceUrl) => {
3694
+ const sourcePath = urlToFileSystemPath(sourceUrl);
3695
+ const attempt = () => {
3696
+ unlinkSyncNaive(sourcePath);
3697
+ };
3698
+ attempt();
3699
+ };
3700
+
3701
+ const unlinkSyncNaive = (sourcePath, { handleTemporaryError = null } = {}) => {
3702
+ try {
3703
+ unlinkSync(sourcePath);
3704
+ } catch (error) {
3705
+ if (error.code === "ENOENT") {
3706
+ return;
3707
+ }
3708
+ if (
3709
+ handleTemporaryError &&
3710
+ (error.code === "EBUSY" ||
3711
+ error.code === "EMFILE" ||
3712
+ error.code === "ENFILE" ||
3713
+ error.code === "ENOENT")
3714
+ ) {
3715
+ handleTemporaryError(error);
3716
+ return;
3717
+ }
3718
+ throw error;
3719
+ }
3720
+ };
3721
+
3722
+ const removeDirectorySync$1 = (
3723
+ rootDirectoryUrl,
3724
+ { maxRetries, retryDelay, recursive, onlyContent },
3725
+ ) => {
3726
+ const visit = (sourceUrl) => {
3727
+ const sourceStats = readEntryStatSync(sourceUrl, {
3728
+ nullIfNotFound: true,
3729
+ followLink: false,
3730
+ });
3731
+
3732
+ // file/directory not found
3733
+ if (sourceStats === null) {
3734
+ return;
3735
+ }
3736
+
3737
+ if (
3738
+ sourceStats.isFile() ||
3739
+ sourceStats.isCharacterDevice() ||
3740
+ sourceStats.isBlockDevice()
3741
+ ) {
3742
+ visitFile(sourceUrl);
3743
+ } else if (sourceStats.isSymbolicLink()) {
3744
+ visitSymbolicLink(sourceUrl);
3745
+ } else if (sourceStats.isDirectory()) {
3746
+ visitDirectory(`${sourceUrl}/`);
3747
+ }
3748
+ };
3749
+
3750
+ const visitDirectory = (directoryUrl) => {
3751
+ const directoryPath = urlToFileSystemPath(directoryUrl);
3752
+ const optionsFromRecursive = recursive
3753
+ ? {
3754
+ handleNotEmptyError: () => {
3755
+ removeDirectoryContent(directoryUrl);
3756
+ visitDirectory(directoryUrl);
3757
+ },
3758
+ }
3759
+ : {};
3760
+ removeDirectorySyncNaive(directoryPath, {
3761
+ ...optionsFromRecursive,
3762
+ // Workaround for https://github.com/joyent/node/issues/4337
3763
+ ...(process.platform === "win32"
3764
+ ? {
3765
+ handlePermissionError: (error) => {
3766
+ console.error(
3767
+ `trying to fix windows EPERM after readir on ${directoryPath}`,
3768
+ );
3769
+
3770
+ let openOrCloseError;
3771
+ try {
3772
+ const fd = openSync(directoryPath);
3773
+ closeSync(fd);
3774
+ } catch (e) {
3775
+ openOrCloseError = e;
3776
+ }
3777
+
3778
+ if (openOrCloseError) {
3779
+ if (openOrCloseError.code === "ENOENT") {
3780
+ return;
3781
+ }
3782
+ console.error(
3783
+ `error while trying to fix windows EPERM after readir on ${directoryPath}: ${openOrCloseError.stack}`,
3784
+ );
3785
+ throw error;
3786
+ }
3787
+ removeDirectorySyncNaive(directoryPath, {
3788
+ ...optionsFromRecursive,
3789
+ });
3790
+ },
3791
+ }
3792
+ : {}),
3793
+ });
3794
+ };
3795
+
3796
+ const removeDirectoryContent = (directoryUrl) => {
3797
+ const entryNames = readdirSync(new URL(directoryUrl));
3798
+ for (const entryName of entryNames) {
3799
+ const url = resolveUrl$1(entryName, directoryUrl);
3800
+ visit(url);
3801
+ }
3802
+ };
3803
+
3804
+ const visitFile = (fileUrl) => {
3805
+ removeNonDirectory$1(fileUrl);
3806
+ };
3807
+
3808
+ const visitSymbolicLink = (symbolicLinkUrl) => {
3809
+ removeNonDirectory$1(symbolicLinkUrl);
3810
+ };
3811
+
3812
+ if (onlyContent) {
3813
+ removeDirectoryContent(rootDirectoryUrl);
3814
+ } else {
3815
+ visitDirectory(rootDirectoryUrl);
3816
+ }
3817
+ };
3818
+
3819
+ const removeDirectorySyncNaive = (
3820
+ directoryPath,
3821
+ { handleNotEmptyError = null, handlePermissionError = null } = {},
3822
+ ) => {
3823
+ try {
3824
+ rmdirSync(directoryPath);
3825
+ } catch (error) {
3826
+ if (handlePermissionError && error.code === "EPERM") {
3827
+ handlePermissionError(error);
3828
+ return;
3829
+ }
3830
+ if (error.code === "ENOENT") {
3831
+ return;
3832
+ }
3833
+ if (
3834
+ handleNotEmptyError &&
3835
+ // linux os
3836
+ (error.code === "ENOTEMPTY" ||
3837
+ // SunOS
3838
+ error.code === "EEXIST")
3839
+ ) {
3840
+ handleNotEmptyError(error);
3841
+ return;
3842
+ }
3843
+ throw error;
3844
+ }
3845
+ };
3846
+
3847
+ const removeDirectorySync = (url, options = {}) => {
3848
+ return removeEntrySync(url, {
3849
+ ...options,
3850
+ recursive: true,
3851
+ });
3852
+ };
3853
+
3854
+ const writeDirectorySync = (
3855
+ destination,
3856
+ { recursive = true, allowUseless = false, force } = {},
3857
+ ) => {
3858
+ const destinationUrl = assertAndNormalizeDirectoryUrl(destination);
3859
+ const destinationPath = urlToFileSystemPath(destinationUrl);
3860
+
3861
+ let destinationStats;
3862
+ try {
3863
+ destinationStats = readEntryStatSync(destinationUrl, {
3864
+ nullIfNotFound: true,
3865
+ followLink: false,
3866
+ });
3867
+ } catch (e) {
3868
+ if (e.code === "ENOTDIR") {
3869
+ let previousNonDirUrl = destinationUrl;
3870
+ // we must try all parent directories as long as it fails with ENOTDIR
3871
+ findAncestorDirectoryUrl(destinationUrl, (ancestorUrl) => {
3872
+ try {
3873
+ statSync(new URL(ancestorUrl));
3874
+ return true;
3875
+ } catch (e) {
3876
+ if (e.code === "ENOTDIR") {
3877
+ previousNonDirUrl = ancestorUrl;
3878
+ return false;
3879
+ }
3880
+ throw e;
3881
+ }
3882
+ });
3883
+ if (force) {
3884
+ unlinkSync(
3885
+ new URL(
3886
+ previousNonDirUrl
3887
+ // remove trailing slash
3888
+ .slice(0, -1),
3889
+ ),
3890
+ );
3891
+ } else {
3892
+ throw new Error(
3893
+ `cannot write directory at ${destinationPath} because there is a file at ${urlToFileSystemPath(
3894
+ previousNonDirUrl,
3895
+ )}`,
3896
+ );
3897
+ }
3898
+ } else {
3899
+ throw e;
3900
+ }
3901
+ }
3902
+
3903
+ if (destinationStats) {
3904
+ if (destinationStats.isDirectory()) {
3905
+ if (allowUseless) {
3906
+ return;
3907
+ }
3908
+ throw new Error(`directory already exists at ${destinationPath}`);
3909
+ }
3910
+ const destinationType = statsToType(destinationStats);
3911
+ throw new Error(
3912
+ `cannot write directory at ${destinationPath} because there is a ${destinationType}`,
3913
+ );
3914
+ }
3915
+
3916
+ try {
3917
+ mkdirSync(destinationPath, { recursive });
3918
+ } catch (error) {
3919
+ if (allowUseless && error.code === "EEXIST") {
3920
+ return;
3921
+ }
3922
+ throw error;
3923
+ }
3924
+ };
3925
+
3926
+ const writeFileSync = (destination, content = "", { force } = {}) => {
3614
3927
  const destinationUrl = assertAndNormalizeFileUrl(destination);
3615
3928
  const destinationUrlObject = new URL(destinationUrl);
3616
3929
  if (content && content instanceof URL) {
@@ -3619,8 +3932,18 @@ const writeFileSync = (destination, content = "") => {
3619
3932
  try {
3620
3933
  writeFileSync$1(destinationUrlObject, content);
3621
3934
  } catch (error) {
3622
- if (error.code === "ENOENT") {
3623
- mkdirSync(new URL("./", destinationUrlObject), {
3935
+ if (error.code === "EISDIR") {
3936
+ // happens when directory existed but got deleted and now it's a file
3937
+ if (force) {
3938
+ removeDirectorySync(destinationUrlObject);
3939
+ writeFileSync$1(destinationUrlObject, content);
3940
+ } else {
3941
+ throw error;
3942
+ }
3943
+ }
3944
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") {
3945
+ writeDirectorySync(new URL("./", destinationUrlObject), {
3946
+ force,
3624
3947
  recursive: true,
3625
3948
  });
3626
3949
  writeFileSync$1(destinationUrlObject, content);
@@ -11159,39 +11482,10 @@ const jsenvPluginTranspilation = ({
11159
11482
  };
11160
11483
 
11161
11484
  const lookupPackageDirectory = (currentUrl) => {
11162
- if (currentUrl === "file:///") {
11163
- return null;
11164
- }
11165
- const packageJsonFileUrl = `${currentUrl}package.json`;
11166
- if (existsSync(new URL(packageJsonFileUrl))) {
11167
- return currentUrl;
11168
- }
11169
- return lookupPackageDirectory(getParentUrl$1(currentUrl));
11170
- };
11171
-
11172
- const getParentUrl$1 = (url) => {
11173
- if (url.startsWith("file://")) {
11174
- // With node.js new URL('../', 'file:///C:/').href
11175
- // returns "file:///C:/" instead of "file:///"
11176
- const resource = url.slice("file://".length);
11177
- const slashLastIndex = resource.lastIndexOf("/");
11178
- if (slashLastIndex === -1) {
11179
- return url;
11180
- }
11181
- const lastCharIndex = resource.length - 1;
11182
- if (slashLastIndex === lastCharIndex) {
11183
- const slashBeforeLastIndex = resource.lastIndexOf(
11184
- "/",
11185
- slashLastIndex - 1,
11186
- );
11187
- if (slashBeforeLastIndex === -1) {
11188
- return url;
11189
- }
11190
- return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
11191
- }
11192
- return `file://${resource.slice(0, slashLastIndex + 1)}`;
11193
- }
11194
- return new URL(url.endsWith("/") ? "../" : "./", url).href;
11485
+ return findAncestorDirectoryUrl(currentUrl, (ancestorDirectoryUrl) => {
11486
+ const potentialPackageJsonFileUrl = `${ancestorDirectoryUrl}package.json`;
11487
+ return existsSync(new URL(potentialPackageJsonFileUrl));
11488
+ });
11195
11489
  };
11196
11490
 
11197
11491
  const watchSourceFiles = (
@@ -14163,7 +14457,7 @@ const createUrlInfoTransformer = ({
14163
14457
  contentIsInlined = false;
14164
14458
  }
14165
14459
  if (!contentIsInlined) {
14166
- writeFileSync(new URL(generatedUrl), urlInfo.content);
14460
+ writeFileSync(new URL(generatedUrl), urlInfo.content, { force: true });
14167
14461
  }
14168
14462
  const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
14169
14463
  if (sourcemapGeneratedUrl && sourcemapReference) {
@@ -18959,6 +19253,7 @@ const jsenvPluginProtocolFile = ({
18959
19253
  if (!urlInfo.url.startsWith("file:")) {
18960
19254
  return null;
18961
19255
  }
19256
+ const { rootDirectoryUrl } = urlInfo.context;
18962
19257
  const generateContent = () => {
18963
19258
  const urlObject = new URL(urlInfo.url);
18964
19259
  const { firstReference } = urlInfo;
@@ -18977,11 +19272,12 @@ const jsenvPluginProtocolFile = ({
18977
19272
  : false;
18978
19273
  if (acceptsHtml) {
18979
19274
  firstReference.expectedType = "html";
18980
- const html = generateHtmlForDirectory(
18981
- urlObject.href,
18982
- directoryContentArray,
18983
- urlInfo.context.rootDirectoryUrl,
19275
+ const directoryUrl = urlObject.href;
19276
+ const directoryContentItems = generateDirectoryContentItems(
19277
+ directoryUrl,
19278
+ rootDirectoryUrl,
18984
19279
  );
19280
+ const html = generateHtmlForDirectory(directoryContentItems);
18985
19281
  return {
18986
19282
  type: "html",
18987
19283
  contentType: "text/html",
@@ -19014,32 +19310,13 @@ const jsenvPluginProtocolFile = ({
19014
19310
  if (e.code !== "ENOENT") {
19015
19311
  throw e;
19016
19312
  }
19017
- const rootDirectoryUrl = urlInfo.context.rootDirectoryUrl;
19018
- let firstExistingAncestorDirectoryUrl = new URL("./", urlInfo.url);
19019
- while (!existsSync(firstExistingAncestorDirectoryUrl)) {
19020
- firstExistingAncestorDirectoryUrl = new URL(
19021
- "../",
19022
- firstExistingAncestorDirectoryUrl,
19023
- );
19024
- if (
19025
- !urlIsInsideOf(
19026
- firstExistingAncestorDirectoryUrl,
19027
- rootDirectoryUrl,
19028
- )
19029
- ) {
19030
- firstExistingAncestorDirectoryUrl = rootDirectoryUrl;
19031
- break;
19032
- }
19033
- }
19034
-
19035
- const firstExistingAncestorDirectoryContent = readdirSync(
19036
- new URL(firstExistingAncestorDirectoryUrl),
19313
+ const directoryContentItems = generateDirectoryContentItems(
19314
+ urlInfo.url,
19315
+ rootDirectoryUrl,
19037
19316
  );
19038
19317
  const html = generateHtmlForENOENT(
19039
19318
  urlInfo.url,
19040
- firstExistingAncestorDirectoryContent,
19041
- firstExistingAncestorDirectoryUrl,
19042
- urlInfo.context.rootDirectoryUrl,
19319
+ directoryContentItems,
19043
19320
  directoryListingUrlMocks,
19044
19321
  );
19045
19322
  return {
@@ -19058,11 +19335,9 @@ const jsenvPluginProtocolFile = ({
19058
19335
  ];
19059
19336
  };
19060
19337
 
19061
- const generateHtmlForDirectory = (
19062
- directoryUrl,
19063
- directoryContentArray,
19064
- rootDirectoryUrl,
19065
- ) => {
19338
+ const generateHtmlForDirectory = (directoryContentItems) => {
19339
+ let directoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19340
+ const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19066
19341
  directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
19067
19342
 
19068
19343
  const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
@@ -19071,23 +19346,19 @@ const generateHtmlForDirectory = (
19071
19346
  directoryUrl,
19072
19347
  directoryNav: () =>
19073
19348
  generateDirectoryNav(directoryRelativeUrl, rootDirectoryUrl),
19074
- directoryContent: () =>
19075
- generateDirectoryContent(
19076
- directoryContentArray,
19077
- directoryUrl,
19078
- rootDirectoryUrl,
19079
- ),
19349
+ directoryContent: () => generateDirectoryContent(directoryContentItems),
19080
19350
  };
19081
19351
  const html = replacePlaceholders$1(htmlForDirectory, replacers);
19082
19352
  return html;
19083
19353
  };
19084
19354
  const generateHtmlForENOENT = (
19085
19355
  url,
19086
- ancestorDirectoryContentArray,
19087
- ancestorDirectoryUrl,
19088
- rootDirectoryUrl,
19356
+ directoryContentItems,
19089
19357
  directoryListingUrlMocks,
19090
19358
  ) => {
19359
+ const ancestorDirectoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19360
+ const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19361
+
19091
19362
  const htmlFor404AndAncestorDir = String(
19092
19363
  readFileSync(html404AndAncestorDirFileUrl),
19093
19364
  );
@@ -19106,11 +19377,7 @@ const generateHtmlForENOENT = (
19106
19377
  ancestorDirectoryNav: () =>
19107
19378
  generateDirectoryNav(ancestorDirectoryRelativeUrl, rootDirectoryUrl),
19108
19379
  ancestorDirectoryContent: () =>
19109
- generateDirectoryContent(
19110
- ancestorDirectoryContentArray,
19111
- ancestorDirectoryUrl,
19112
- rootDirectoryUrl,
19113
- ),
19380
+ generateDirectoryContent(directoryContentItems),
19114
19381
  };
19115
19382
  const html = replacePlaceholders$1(htmlFor404AndAncestorDir, replacers);
19116
19383
  return html;
@@ -19152,31 +19419,95 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19152
19419
  }
19153
19420
  return dirPartsHtml;
19154
19421
  };
19155
- const generateDirectoryContent = (
19156
- directoryContentArray,
19157
- directoryUrl,
19158
- rootDirectoryUrl,
19159
- ) => {
19160
- if (directoryContentArray.length === 0) {
19161
- return `<p>Directory is empty</p>`;
19422
+ const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
19423
+ let firstExistingDirectoryUrl = new URL("./", directoryUrl);
19424
+ while (!existsSync(firstExistingDirectoryUrl)) {
19425
+ firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
19426
+ if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrl)) {
19427
+ firstExistingDirectoryUrl = new URL(rootDirectoryUrl);
19428
+ break;
19429
+ }
19162
19430
  }
19163
- const sortedNames = [];
19431
+ const directoryContentArray = readdirSync(firstExistingDirectoryUrl);
19432
+ const fileUrls = [];
19164
19433
  for (const filename of directoryContentArray) {
19165
- const fileUrlObject = new URL(filename, directoryUrl);
19166
- if (lstatSync(fileUrlObject).isDirectory()) {
19167
- sortedNames.push(`${filename}/`);
19434
+ const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
19435
+ fileUrls.push(fileUrlObject);
19436
+ }
19437
+ package_workspaces: {
19438
+ if (String(firstExistingDirectoryUrl) !== String(rootDirectoryUrl)) {
19439
+ break package_workspaces;
19440
+ }
19441
+ const packageDirectoryUrl = lookupPackageDirectory(rootDirectoryUrl);
19442
+ if (!packageDirectoryUrl) {
19443
+ break package_workspaces;
19444
+ }
19445
+ if (String(packageDirectoryUrl) === String(rootDirectoryUrl)) {
19446
+ break package_workspaces;
19447
+ }
19448
+ let packageContent;
19449
+ try {
19450
+ packageContent = JSON.parse(
19451
+ readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
19452
+ );
19453
+ } catch {
19454
+ break package_workspaces;
19455
+ }
19456
+ const { workspaces } = packageContent;
19457
+ if (Array.isArray(workspaces)) {
19458
+ for (const workspace of workspaces) {
19459
+ const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
19460
+ const workspaceUrl = workspaceUrlObject.href;
19461
+ if (workspaceUrl.endsWith("*")) {
19462
+ const directoryUrl = ensurePathnameTrailingSlash(
19463
+ workspaceUrl.slice(0, -1),
19464
+ );
19465
+ fileUrls.push(new URL(directoryUrl));
19466
+ } else {
19467
+ fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
19468
+ }
19469
+ }
19470
+ }
19471
+ }
19472
+
19473
+ const sortedUrls = [];
19474
+ for (let fileUrl of fileUrls) {
19475
+ if (lstatSync(fileUrl).isDirectory()) {
19476
+ sortedUrls.push(ensurePathnameTrailingSlash(fileUrl));
19168
19477
  } else {
19169
- sortedNames.push(filename);
19478
+ sortedUrls.push(fileUrl);
19170
19479
  }
19171
19480
  }
19172
- sortedNames.sort(comparePathnames);
19173
- let html = `<ul class="directory_content">`;
19174
- for (const filename of sortedNames) {
19175
- const fileUrlObject = new URL(filename, directoryUrl);
19176
- const fileUrl = String(fileUrlObject);
19177
- const fileUrlRelativeToParent = urlToRelativeUrl(fileUrl, directoryUrl);
19178
- const fileUrlRelativeToRoot = urlToRelativeUrl(fileUrl, rootDirectoryUrl);
19481
+ sortedUrls.sort((a, b) => {
19482
+ return comparePathnames(a.pathname, b.pathname);
19483
+ });
19484
+
19485
+ const items = [];
19486
+ for (const sortedUrl of sortedUrls) {
19487
+ const fileUrlRelativeToParent = urlToRelativeUrl(
19488
+ sortedUrl,
19489
+ firstExistingDirectoryUrl,
19490
+ );
19491
+ const fileUrlRelativeToRoot = urlToRelativeUrl(sortedUrl, rootDirectoryUrl);
19179
19492
  const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
19493
+ items.push({
19494
+ type,
19495
+ fileUrlRelativeToParent,
19496
+ fileUrlRelativeToRoot,
19497
+ });
19498
+ }
19499
+ items.rootDirectoryUrl = rootDirectoryUrl;
19500
+ items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
19501
+ return items;
19502
+ };
19503
+ const generateDirectoryContent = (directoryContentItems) => {
19504
+ if (directoryContentItems.length === 0) {
19505
+ return `<p>Directory is empty</p>`;
19506
+ }
19507
+ let html = `<ul class="directory_content">`;
19508
+ for (const directoryContentItem of directoryContentItems) {
19509
+ const { type, fileUrlRelativeToParent, fileUrlRelativeToRoot } =
19510
+ directoryContentItem;
19180
19511
  html += `
19181
19512
  <li class="directory_child" data-type="${type}">
19182
19513
  <a href="/${fileUrlRelativeToRoot}">${fileUrlRelativeToParent}</a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "39.7.6",
3
+ "version": "39.8.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -70,7 +70,7 @@
70
70
  "@financial-times/polyfill-useragent-normaliser": "1.10.2",
71
71
  "@jsenv/abort": "4.3.0",
72
72
  "@jsenv/ast": "6.4.1",
73
- "@jsenv/filesystem": "4.11.0",
73
+ "@jsenv/filesystem": "4.12.0",
74
74
  "@jsenv/humanize": "1.2.8",
75
75
  "@jsenv/importmap": "1.2.1",
76
76
  "@jsenv/integrity": "0.0.2",
@@ -1,37 +1,9 @@
1
+ import { findAncestorDirectoryUrl } from "@jsenv/filesystem";
1
2
  import { existsSync } from "node:fs";
2
3
 
3
4
  export const lookupPackageDirectory = (currentUrl) => {
4
- if (currentUrl === "file:///") {
5
- return null;
6
- }
7
- const packageJsonFileUrl = `${currentUrl}package.json`;
8
- if (existsSync(new URL(packageJsonFileUrl))) {
9
- return currentUrl;
10
- }
11
- return lookupPackageDirectory(getParentUrl(currentUrl));
12
- };
13
-
14
- const getParentUrl = (url) => {
15
- if (url.startsWith("file://")) {
16
- // With node.js new URL('../', 'file:///C:/').href
17
- // returns "file:///C:/" instead of "file:///"
18
- const resource = url.slice("file://".length);
19
- const slashLastIndex = resource.lastIndexOf("/");
20
- if (slashLastIndex === -1) {
21
- return url;
22
- }
23
- const lastCharIndex = resource.length - 1;
24
- if (slashLastIndex === lastCharIndex) {
25
- const slashBeforeLastIndex = resource.lastIndexOf(
26
- "/",
27
- slashLastIndex - 1,
28
- );
29
- if (slashBeforeLastIndex === -1) {
30
- return url;
31
- }
32
- return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
33
- }
34
- return `file://${resource.slice(0, slashLastIndex + 1)}`;
35
- }
36
- return new URL(url.endsWith("/") ? "../" : "./", url).href;
5
+ return findAncestorDirectoryUrl(currentUrl, (ancestorDirectoryUrl) => {
6
+ const potentialPackageJsonFileUrl = `${ancestorDirectoryUrl}package.json`;
7
+ return existsSync(new URL(potentialPackageJsonFileUrl));
8
+ });
37
9
  };
@@ -275,7 +275,7 @@ export const createUrlInfoTransformer = ({
275
275
  contentIsInlined = false;
276
276
  }
277
277
  if (!contentIsInlined) {
278
- writeFileSync(new URL(generatedUrl), urlInfo.content);
278
+ writeFileSync(new URL(generatedUrl), urlInfo.content, { force: true });
279
279
  }
280
280
  const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
281
281
  if (sourcemapGeneratedUrl && sourcemapReference) {
@@ -11,6 +11,7 @@ import {
11
11
  } from "@jsenv/urls";
12
12
  import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
13
13
  import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
14
+ import { lookupPackageDirectory } from "../../helpers/lookup_package_directory.js";
14
15
  import { jsenvCoreDirectoryUrl } from "../../jsenv_core_directory_url.js";
15
16
  import { jsenvPluginFsRedirection } from "./jsenv_plugin_fs_redirection.js";
16
17
 
@@ -83,6 +84,7 @@ export const jsenvPluginProtocolFile = ({
83
84
  if (!urlInfo.url.startsWith("file:")) {
84
85
  return null;
85
86
  }
87
+ const { rootDirectoryUrl } = urlInfo.context;
86
88
  const generateContent = () => {
87
89
  const urlObject = new URL(urlInfo.url);
88
90
  const { firstReference } = urlInfo;
@@ -101,11 +103,12 @@ export const jsenvPluginProtocolFile = ({
101
103
  : false;
102
104
  if (acceptsHtml) {
103
105
  firstReference.expectedType = "html";
104
- const html = generateHtmlForDirectory(
105
- urlObject.href,
106
- directoryContentArray,
107
- urlInfo.context.rootDirectoryUrl,
106
+ const directoryUrl = urlObject.href;
107
+ const directoryContentItems = generateDirectoryContentItems(
108
+ directoryUrl,
109
+ rootDirectoryUrl,
108
110
  );
111
+ const html = generateHtmlForDirectory(directoryContentItems);
109
112
  return {
110
113
  type: "html",
111
114
  contentType: "text/html",
@@ -138,32 +141,13 @@ export const jsenvPluginProtocolFile = ({
138
141
  if (e.code !== "ENOENT") {
139
142
  throw e;
140
143
  }
141
- const rootDirectoryUrl = urlInfo.context.rootDirectoryUrl;
142
- let firstExistingAncestorDirectoryUrl = new URL("./", urlInfo.url);
143
- while (!existsSync(firstExistingAncestorDirectoryUrl)) {
144
- firstExistingAncestorDirectoryUrl = new URL(
145
- "../",
146
- firstExistingAncestorDirectoryUrl,
147
- );
148
- if (
149
- !urlIsInsideOf(
150
- firstExistingAncestorDirectoryUrl,
151
- rootDirectoryUrl,
152
- )
153
- ) {
154
- firstExistingAncestorDirectoryUrl = rootDirectoryUrl;
155
- break;
156
- }
157
- }
158
-
159
- const firstExistingAncestorDirectoryContent = readdirSync(
160
- new URL(firstExistingAncestorDirectoryUrl),
144
+ const directoryContentItems = generateDirectoryContentItems(
145
+ urlInfo.url,
146
+ rootDirectoryUrl,
161
147
  );
162
148
  const html = generateHtmlForENOENT(
163
149
  urlInfo.url,
164
- firstExistingAncestorDirectoryContent,
165
- firstExistingAncestorDirectoryUrl,
166
- urlInfo.context.rootDirectoryUrl,
150
+ directoryContentItems,
167
151
  directoryListingUrlMocks,
168
152
  );
169
153
  return {
@@ -182,11 +166,9 @@ export const jsenvPluginProtocolFile = ({
182
166
  ];
183
167
  };
184
168
 
185
- const generateHtmlForDirectory = (
186
- directoryUrl,
187
- directoryContentArray,
188
- rootDirectoryUrl,
189
- ) => {
169
+ const generateHtmlForDirectory = (directoryContentItems) => {
170
+ let directoryUrl = directoryContentItems.firstExistingDirectoryUrl;
171
+ const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
190
172
  directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
191
173
 
192
174
  const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
@@ -195,23 +177,19 @@ const generateHtmlForDirectory = (
195
177
  directoryUrl,
196
178
  directoryNav: () =>
197
179
  generateDirectoryNav(directoryRelativeUrl, rootDirectoryUrl),
198
- directoryContent: () =>
199
- generateDirectoryContent(
200
- directoryContentArray,
201
- directoryUrl,
202
- rootDirectoryUrl,
203
- ),
180
+ directoryContent: () => generateDirectoryContent(directoryContentItems),
204
181
  };
205
182
  const html = replacePlaceholders(htmlForDirectory, replacers);
206
183
  return html;
207
184
  };
208
185
  const generateHtmlForENOENT = (
209
186
  url,
210
- ancestorDirectoryContentArray,
211
- ancestorDirectoryUrl,
212
- rootDirectoryUrl,
187
+ directoryContentItems,
213
188
  directoryListingUrlMocks,
214
189
  ) => {
190
+ const ancestorDirectoryUrl = directoryContentItems.firstExistingDirectoryUrl;
191
+ const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
192
+
215
193
  const htmlFor404AndAncestorDir = String(
216
194
  readFileSync(html404AndAncestorDirFileUrl),
217
195
  );
@@ -230,11 +208,7 @@ const generateHtmlForENOENT = (
230
208
  ancestorDirectoryNav: () =>
231
209
  generateDirectoryNav(ancestorDirectoryRelativeUrl, rootDirectoryUrl),
232
210
  ancestorDirectoryContent: () =>
233
- generateDirectoryContent(
234
- ancestorDirectoryContentArray,
235
- ancestorDirectoryUrl,
236
- rootDirectoryUrl,
237
- ),
211
+ generateDirectoryContent(directoryContentItems),
238
212
  };
239
213
  const html = replacePlaceholders(htmlFor404AndAncestorDir, replacers);
240
214
  return html;
@@ -276,31 +250,95 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
276
250
  }
277
251
  return dirPartsHtml;
278
252
  };
279
- const generateDirectoryContent = (
280
- directoryContentArray,
281
- directoryUrl,
282
- rootDirectoryUrl,
283
- ) => {
284
- if (directoryContentArray.length === 0) {
285
- return `<p>Directory is empty</p>`;
253
+ const generateDirectoryContentItems = (directoryUrl, rootDirectoryUrl) => {
254
+ let firstExistingDirectoryUrl = new URL("./", directoryUrl);
255
+ while (!existsSync(firstExistingDirectoryUrl)) {
256
+ firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
257
+ if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrl)) {
258
+ firstExistingDirectoryUrl = new URL(rootDirectoryUrl);
259
+ break;
260
+ }
286
261
  }
287
- const sortedNames = [];
262
+ const directoryContentArray = readdirSync(firstExistingDirectoryUrl);
263
+ const fileUrls = [];
288
264
  for (const filename of directoryContentArray) {
289
- const fileUrlObject = new URL(filename, directoryUrl);
290
- if (lstatSync(fileUrlObject).isDirectory()) {
291
- sortedNames.push(`${filename}/`);
265
+ const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
266
+ fileUrls.push(fileUrlObject);
267
+ }
268
+ package_workspaces: {
269
+ if (String(firstExistingDirectoryUrl) !== String(rootDirectoryUrl)) {
270
+ break package_workspaces;
271
+ }
272
+ const packageDirectoryUrl = lookupPackageDirectory(rootDirectoryUrl);
273
+ if (!packageDirectoryUrl) {
274
+ break package_workspaces;
275
+ }
276
+ if (String(packageDirectoryUrl) === String(rootDirectoryUrl)) {
277
+ break package_workspaces;
278
+ }
279
+ let packageContent;
280
+ try {
281
+ packageContent = JSON.parse(
282
+ readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
283
+ );
284
+ } catch {
285
+ break package_workspaces;
286
+ }
287
+ const { workspaces } = packageContent;
288
+ if (Array.isArray(workspaces)) {
289
+ for (const workspace of workspaces) {
290
+ const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
291
+ const workspaceUrl = workspaceUrlObject.href;
292
+ if (workspaceUrl.endsWith("*")) {
293
+ const directoryUrl = ensurePathnameTrailingSlash(
294
+ workspaceUrl.slice(0, -1),
295
+ );
296
+ fileUrls.push(new URL(directoryUrl));
297
+ } else {
298
+ fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ const sortedUrls = [];
305
+ for (let fileUrl of fileUrls) {
306
+ if (lstatSync(fileUrl).isDirectory()) {
307
+ sortedUrls.push(ensurePathnameTrailingSlash(fileUrl));
292
308
  } else {
293
- sortedNames.push(filename);
309
+ sortedUrls.push(fileUrl);
294
310
  }
295
311
  }
296
- sortedNames.sort(comparePathnames);
297
- let html = `<ul class="directory_content">`;
298
- for (const filename of sortedNames) {
299
- const fileUrlObject = new URL(filename, directoryUrl);
300
- const fileUrl = String(fileUrlObject);
301
- const fileUrlRelativeToParent = urlToRelativeUrl(fileUrl, directoryUrl);
302
- const fileUrlRelativeToRoot = urlToRelativeUrl(fileUrl, rootDirectoryUrl);
312
+ sortedUrls.sort((a, b) => {
313
+ return comparePathnames(a.pathname, b.pathname);
314
+ });
315
+
316
+ const items = [];
317
+ for (const sortedUrl of sortedUrls) {
318
+ const fileUrlRelativeToParent = urlToRelativeUrl(
319
+ sortedUrl,
320
+ firstExistingDirectoryUrl,
321
+ );
322
+ const fileUrlRelativeToRoot = urlToRelativeUrl(sortedUrl, rootDirectoryUrl);
303
323
  const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
324
+ items.push({
325
+ type,
326
+ fileUrlRelativeToParent,
327
+ fileUrlRelativeToRoot,
328
+ });
329
+ }
330
+ items.rootDirectoryUrl = rootDirectoryUrl;
331
+ items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
332
+ return items;
333
+ };
334
+ const generateDirectoryContent = (directoryContentItems) => {
335
+ if (directoryContentItems.length === 0) {
336
+ return `<p>Directory is empty</p>`;
337
+ }
338
+ let html = `<ul class="directory_content">`;
339
+ for (const directoryContentItem of directoryContentItems) {
340
+ const { type, fileUrlRelativeToParent, fileUrlRelativeToRoot } =
341
+ directoryContentItem;
304
342
  html += `
305
343
  <li class="directory_child" data-type="${type}">
306
344
  <a href="/${fileUrlRelativeToRoot}">${fileUrlRelativeToParent}</a>