@jsenv/core 39.9.7 → 39.10.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.
@@ -15,6 +15,7 @@
15
15
  }
16
16
 
17
17
  .directory_nav {
18
+ gap: .3em;
18
19
  margin: 20px 25px 15px;
19
20
  font-size: 16px;
20
21
  font-weight: bold;
@@ -26,14 +27,6 @@
26
27
  position: relative;
27
28
  }
28
29
 
29
- .directory_separator {
30
- margin: 0 .3em;
31
- }
32
-
33
- .directory_separator:first-child {
34
- margin-left: 0;
35
- }
36
-
37
30
  .directory_content {
38
31
  border-radius: 3px;
39
32
  margin: 10px 15px;
@@ -15,6 +15,7 @@
15
15
  }
16
16
 
17
17
  .directory_nav {
18
+ gap: .3em;
18
19
  margin: 20px 25px 15px;
19
20
  font-size: 16px;
20
21
  font-weight: bold;
@@ -26,14 +27,6 @@
26
27
  position: relative;
27
28
  }
28
29
 
29
- .directory_separator {
30
- margin: 0 .3em;
31
- }
32
-
33
- .directory_separator:first-child {
34
- margin-left: 0;
35
- }
36
-
37
30
  .directory_content {
38
31
  border-radius: 3px;
39
32
  margin: 10px 15px;
@@ -6981,6 +6981,14 @@ const startServer = async ({
6981
6981
  nodeResponse.end();
6982
6982
  return;
6983
6983
  }
6984
+ try {
6985
+ // eslint-disable-next-line no-new
6986
+ new URL(nodeRequest.url, "http://example.com/");
6987
+ } catch {
6988
+ nodeResponse.writeHead(400, "Request url is not supported");
6989
+ nodeResponse.end();
6990
+ return;
6991
+ }
6984
6992
 
6985
6993
  const receiveRequestOperation = Abort.startOperation();
6986
6994
  receiveRequestOperation.addAbortSource((abort) => {
@@ -8053,6 +8061,7 @@ const fetchFileSystem = async (
8053
8061
  : "no-store",
8054
8062
  canReadDirectory = false,
8055
8063
  rootDirectoryUrl, // = `${pathToFileURL(process.cwd())}/`,
8064
+ ENOENTFallback = () => {},
8056
8065
  } = {},
8057
8066
  ) => {
8058
8067
  const urlString = asUrlString(filesystemUrl);
@@ -8110,100 +8119,109 @@ const fetchFileSystem = async (
8110
8119
  };
8111
8120
  }
8112
8121
 
8113
- const sourceUrl = `file://${new URL(urlString).pathname}`;
8114
- try {
8115
- const [readStatTiming, sourceStat] = await timeFunction(
8116
- "file service>read file stat",
8117
- () => statSync(new URL(sourceUrl)),
8118
- );
8119
- if (sourceStat.isDirectory()) {
8120
- if (canReadDirectory) {
8121
- return serveDirectory(urlString, {
8122
+ const serveFile = async (fileUrl) => {
8123
+ try {
8124
+ const [readStatTiming, fileStat] = timeFunction(
8125
+ "file service>read file stat",
8126
+ () => statSync(new URL(fileUrl)),
8127
+ );
8128
+ if (fileStat.isDirectory()) {
8129
+ if (canReadDirectory) {
8130
+ return serveDirectory(fileUrl, {
8131
+ headers,
8132
+ canReadDirectory,
8133
+ rootDirectoryUrl,
8134
+ });
8135
+ }
8136
+ return {
8137
+ status: 403,
8138
+ statusText: "not allowed to read directory",
8139
+ };
8140
+ }
8141
+ // not a file, give up
8142
+ if (!fileStat.isFile()) {
8143
+ return {
8144
+ status: 404,
8145
+ timing: readStatTiming,
8146
+ };
8147
+ }
8148
+
8149
+ const clientCacheResponse = await getClientCacheResponse({
8150
+ headers,
8151
+ etagEnabled,
8152
+ etagMemory,
8153
+ etagMemoryMaxSize,
8154
+ mtimeEnabled,
8155
+ fileStat,
8156
+ fileUrl,
8157
+ });
8158
+
8159
+ // send 304 (redirect response to client cache)
8160
+ // because the response body does not have to be transmitted
8161
+ if (clientCacheResponse.status === 304) {
8162
+ return composeTwoResponses(
8163
+ {
8164
+ timing: readStatTiming,
8165
+ headers: {
8166
+ ...(cacheControl ? { "cache-control": cacheControl } : {}),
8167
+ },
8168
+ },
8169
+ clientCacheResponse,
8170
+ );
8171
+ }
8172
+
8173
+ let response;
8174
+ if (compressionEnabled && fileStat.size >= compressionSizeThreshold) {
8175
+ const compressedResponse = await getCompressedResponse({
8122
8176
  headers,
8123
- canReadDirectory,
8124
- rootDirectoryUrl,
8177
+ fileUrl,
8178
+ });
8179
+ if (compressedResponse) {
8180
+ response = compressedResponse;
8181
+ }
8182
+ }
8183
+ if (!response) {
8184
+ response = await getRawResponse({
8185
+ fileStat,
8186
+ fileUrl,
8125
8187
  });
8126
8188
  }
8127
- return {
8128
- status: 403,
8129
- statusText: "not allowed to read directory",
8130
- };
8131
- }
8132
- // not a file, give up
8133
- if (!sourceStat.isFile()) {
8134
- return {
8135
- status: 404,
8136
- timing: readStatTiming,
8137
- };
8138
- }
8139
-
8140
- const clientCacheResponse = await getClientCacheResponse({
8141
- headers,
8142
- etagEnabled,
8143
- etagMemory,
8144
- etagMemoryMaxSize,
8145
- mtimeEnabled,
8146
- sourceStat,
8147
- sourceUrl,
8148
- });
8149
8189
 
8150
- // send 304 (redirect response to client cache)
8151
- // because the response body does not have to be transmitted
8152
- if (clientCacheResponse.status === 304) {
8153
- return composeTwoResponses(
8190
+ const intermediateResponse = composeTwoResponses(
8154
8191
  {
8155
8192
  timing: readStatTiming,
8156
8193
  headers: {
8157
8194
  ...(cacheControl ? { "cache-control": cacheControl } : {}),
8195
+ // even if client cache is disabled, server can still
8196
+ // send his own cache control but client should just ignore it
8197
+ // and keep sending cache-control: 'no-store'
8198
+ // if not, uncomment the line below to preserve client
8199
+ // desire to ignore cache
8200
+ // ...(headers["cache-control"] === "no-store" ? { "cache-control": "no-store" } : {}),
8158
8201
  },
8159
8202
  },
8160
- clientCacheResponse,
8203
+ response,
8161
8204
  );
8162
- }
8163
-
8164
- let response;
8165
- if (compressionEnabled && sourceStat.size >= compressionSizeThreshold) {
8166
- const compressedResponse = await getCompressedResponse({
8167
- headers,
8168
- sourceUrl,
8169
- });
8170
- if (compressedResponse) {
8171
- response = compressedResponse;
8205
+ return composeTwoResponses(intermediateResponse, clientCacheResponse);
8206
+ } catch (e) {
8207
+ if (e.code === "ENOENT") {
8208
+ const fallbackFileUrl = ENOENTFallback();
8209
+ if (fallbackFileUrl) {
8210
+ return serveFile(fallbackFileUrl);
8211
+ }
8172
8212
  }
8213
+ return composeTwoResponses(
8214
+ {
8215
+ headers: {
8216
+ ...(cacheControl ? { "cache-control": cacheControl } : {}),
8217
+ },
8218
+ },
8219
+ convertFileSystemErrorToResponseProperties(e) || {},
8220
+ );
8173
8221
  }
8174
- if (!response) {
8175
- response = await getRawResponse({
8176
- sourceStat,
8177
- sourceUrl,
8178
- });
8179
- }
8222
+ };
8180
8223
 
8181
- const intermediateResponse = composeTwoResponses(
8182
- {
8183
- timing: readStatTiming,
8184
- headers: {
8185
- ...(cacheControl ? { "cache-control": cacheControl } : {}),
8186
- // even if client cache is disabled, server can still
8187
- // send his own cache control but client should just ignore it
8188
- // and keep sending cache-control: 'no-store'
8189
- // if not, uncomment the line below to preserve client
8190
- // desire to ignore cache
8191
- // ...(headers["cache-control"] === "no-store" ? { "cache-control": "no-store" } : {}),
8192
- },
8193
- },
8194
- response,
8195
- );
8196
- return composeTwoResponses(intermediateResponse, clientCacheResponse);
8197
- } catch (e) {
8198
- return composeTwoResponses(
8199
- {
8200
- headers: {
8201
- ...(cacheControl ? { "cache-control": cacheControl } : {}),
8202
- },
8203
- },
8204
- convertFileSystemErrorToResponseProperties(e) || {},
8205
- );
8206
- }
8224
+ return serveFile(`file://${new URL(urlString).pathname}`);
8207
8225
  };
8208
8226
 
8209
8227
  const create500Response = (message) => {
@@ -8223,8 +8241,8 @@ const getClientCacheResponse = async ({
8223
8241
  etagMemory,
8224
8242
  etagMemoryMaxSize,
8225
8243
  mtimeEnabled,
8226
- sourceStat,
8227
- sourceUrl,
8244
+ fileStat,
8245
+ fileUrl,
8228
8246
  }) => {
8229
8247
  // here you might be tempted to add || headers["cache-control"] === "no-cache"
8230
8248
  // but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
@@ -8243,15 +8261,15 @@ const getClientCacheResponse = async ({
8243
8261
  headers,
8244
8262
  etagMemory,
8245
8263
  etagMemoryMaxSize,
8246
- sourceStat,
8247
- sourceUrl,
8264
+ fileStat,
8265
+ fileUrl,
8248
8266
  });
8249
8267
  }
8250
8268
 
8251
8269
  if (mtimeEnabled) {
8252
8270
  return getMtimeResponse({
8253
8271
  headers,
8254
- sourceStat,
8272
+ fileStat,
8255
8273
  });
8256
8274
  }
8257
8275
 
@@ -8262,8 +8280,8 @@ const getEtagResponse = async ({
8262
8280
  headers,
8263
8281
  etagMemory,
8264
8282
  etagMemoryMaxSize,
8265
- sourceUrl,
8266
- sourceStat,
8283
+ fileUrl,
8284
+ fileStat,
8267
8285
  }) => {
8268
8286
  const [computeEtagTiming, fileContentEtag] = await timeFunction(
8269
8287
  "file service>generate file etag",
@@ -8271,8 +8289,8 @@ const getEtagResponse = async ({
8271
8289
  computeEtag({
8272
8290
  etagMemory,
8273
8291
  etagMemoryMaxSize,
8274
- sourceUrl,
8275
- sourceStat,
8292
+ fileUrl,
8293
+ fileStat,
8276
8294
  }),
8277
8295
  );
8278
8296
 
@@ -8300,20 +8318,20 @@ const ETAG_MEMORY_MAP = new Map();
8300
8318
  const computeEtag = async ({
8301
8319
  etagMemory,
8302
8320
  etagMemoryMaxSize,
8303
- sourceUrl,
8304
- sourceStat,
8321
+ fileUrl,
8322
+ fileStat,
8305
8323
  }) => {
8306
8324
  if (etagMemory) {
8307
- const etagMemoryEntry = ETAG_MEMORY_MAP.get(sourceUrl);
8325
+ const etagMemoryEntry = ETAG_MEMORY_MAP.get(fileUrl);
8308
8326
  if (
8309
8327
  etagMemoryEntry &&
8310
- fileStatAreTheSame(etagMemoryEntry.sourceStat, sourceStat)
8328
+ fileStatAreTheSame(etagMemoryEntry.fileStat, fileStat)
8311
8329
  ) {
8312
8330
  return etagMemoryEntry.eTag;
8313
8331
  }
8314
8332
  }
8315
8333
  const fileContentAsBuffer = await new Promise((resolve, reject) => {
8316
- readFile(new URL(sourceUrl), (error, buffer) => {
8334
+ readFile(new URL(fileUrl), (error, buffer) => {
8317
8335
  if (error) {
8318
8336
  reject(error);
8319
8337
  } else {
@@ -8327,7 +8345,7 @@ const computeEtag = async ({
8327
8345
  const firstKey = Array.from(ETAG_MEMORY_MAP.keys())[0];
8328
8346
  ETAG_MEMORY_MAP.delete(firstKey);
8329
8347
  }
8330
- ETAG_MEMORY_MAP.set(sourceUrl, { sourceStat, eTag });
8348
+ ETAG_MEMORY_MAP.set(fileUrl, { fileStat, eTag });
8331
8349
  }
8332
8350
  return eTag;
8333
8351
  };
@@ -8352,7 +8370,7 @@ const fileStatKeysToCompare = [
8352
8370
  "blksize",
8353
8371
  ];
8354
8372
 
8355
- const getMtimeResponse = async ({ headers, sourceStat }) => {
8373
+ const getMtimeResponse = async ({ headers, fileStat }) => {
8356
8374
  if ("if-modified-since" in headers) {
8357
8375
  let cachedModificationDate;
8358
8376
  try {
@@ -8364,7 +8382,7 @@ const getMtimeResponse = async ({ headers, sourceStat }) => {
8364
8382
  };
8365
8383
  }
8366
8384
 
8367
- const actualModificationDate = dateToSecondsPrecision(sourceStat.mtime);
8385
+ const actualModificationDate = dateToSecondsPrecision(fileStat.mtime);
8368
8386
  if (Number(cachedModificationDate) >= Number(actualModificationDate)) {
8369
8387
  return {
8370
8388
  status: 304,
@@ -8375,12 +8393,12 @@ const getMtimeResponse = async ({ headers, sourceStat }) => {
8375
8393
  return {
8376
8394
  status: 200,
8377
8395
  headers: {
8378
- "last-modified": dateToUTCString(sourceStat.mtime),
8396
+ "last-modified": dateToUTCString(fileStat.mtime),
8379
8397
  },
8380
8398
  };
8381
8399
  };
8382
8400
 
8383
- const getCompressedResponse = async ({ sourceUrl, headers }) => {
8401
+ const getCompressedResponse = async ({ fileUrl, headers }) => {
8384
8402
  const acceptedCompressionFormat = pickContentEncoding(
8385
8403
  { headers },
8386
8404
  Object.keys(availableCompressionFormats),
@@ -8389,7 +8407,7 @@ const getCompressedResponse = async ({ sourceUrl, headers }) => {
8389
8407
  return null;
8390
8408
  }
8391
8409
 
8392
- const fileReadableStream = fileUrlToReadableStream(sourceUrl);
8410
+ const fileReadableStream = fileUrlToReadableStream(fileUrl);
8393
8411
  const body =
8394
8412
  await availableCompressionFormats[acceptedCompressionFormat](
8395
8413
  fileReadableStream,
@@ -8398,7 +8416,7 @@ const getCompressedResponse = async ({ sourceUrl, headers }) => {
8398
8416
  return {
8399
8417
  status: 200,
8400
8418
  headers: {
8401
- "content-type": CONTENT_TYPE.fromUrlExtension(sourceUrl),
8419
+ "content-type": CONTENT_TYPE.fromUrlExtension(fileUrl),
8402
8420
  "content-encoding": acceptedCompressionFormat,
8403
8421
  "vary": "accept-encoding",
8404
8422
  },
@@ -8428,14 +8446,14 @@ const availableCompressionFormats = {
8428
8446
  },
8429
8447
  };
8430
8448
 
8431
- const getRawResponse = async ({ sourceUrl, sourceStat }) => {
8449
+ const getRawResponse = async ({ fileUrl, fileStat }) => {
8432
8450
  return {
8433
8451
  status: 200,
8434
8452
  headers: {
8435
- "content-type": CONTENT_TYPE.fromUrlExtension(sourceUrl),
8436
- "content-length": sourceStat.size,
8453
+ "content-type": CONTENT_TYPE.fromUrlExtension(fileUrl),
8454
+ "content-length": fileStat.size,
8437
8455
  },
8438
- body: fileUrlToReadableStream(sourceUrl),
8456
+ body: fileUrlToReadableStream(fileUrl),
8439
8457
  };
8440
8458
  };
8441
8459
 
@@ -19180,6 +19198,14 @@ const jsenvPluginFsRedirection = ({
19180
19198
  if (reference.subtype === "new_url_second_arg") {
19181
19199
  return `ignore:${reference.url}`;
19182
19200
  }
19201
+ if (reference.specifierPathname.endsWith("/...")) {
19202
+ const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19203
+ const directoryUrl = new URL(
19204
+ reference.specifierPathname.replace("/...", "/").slice(1),
19205
+ rootDirectoryUrl,
19206
+ ).href;
19207
+ return directoryUrl;
19208
+ }
19183
19209
  // ignore "./" on new URL("./")
19184
19210
  // if (
19185
19211
  // reference.subtype === "new_url_first_arg" &&
@@ -19331,6 +19357,14 @@ const jsenvPluginProtocolFile = ({
19331
19357
  if (!generatedUrl.startsWith("file:")) {
19332
19358
  return null;
19333
19359
  }
19360
+ if (reference.original) {
19361
+ const originalSpecifierPathname =
19362
+ reference.original.specifierPathname;
19363
+
19364
+ if (originalSpecifierPathname.endsWith("/...")) {
19365
+ return originalSpecifierPathname;
19366
+ }
19367
+ }
19334
19368
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19335
19369
  if (urlIsInsideOf(generatedUrl, rootDirectoryUrl)) {
19336
19370
  const result = `/${urlToRelativeUrl(generatedUrl, rootDirectoryUrl)}`;
@@ -19483,14 +19517,28 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19483
19517
  const parts = isDir
19484
19518
  ? relativeUrlWithRoot.slice(0, -1).split("/")
19485
19519
  : relativeUrlWithRoot.split("/");
19520
+ const items = [];
19521
+ items.push({
19522
+ href: "/",
19523
+ text: "/",
19524
+ });
19486
19525
  let dirPartsHtml = "";
19487
19526
  let i = 0;
19488
19527
  while (i < parts.length) {
19489
19528
  const part = parts[i];
19490
- const href = i === 0 ? "/" : `/${parts.slice(1, i + 1).join("/")}/`;
19529
+ const href = i === 0 ? "/..." : `/${parts.slice(1, i + 1).join("/")}/`;
19491
19530
  const text = part;
19492
19531
  const isLastPart = i === parts.length - 1;
19493
- if (isLastPart) {
19532
+ items.push({
19533
+ href,
19534
+ text,
19535
+ isCurrent: isLastPart,
19536
+ });
19537
+ i++;
19538
+ }
19539
+ i = 0;
19540
+ for (const { href, text, isCurrent } of items) {
19541
+ if (isCurrent) {
19494
19542
  dirPartsHtml += `
19495
19543
  <span class="directory_nav_item" data-current>
19496
19544
  ${text}
@@ -19501,13 +19549,15 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19501
19549
  <a class="directory_nav_item" href="${href}">
19502
19550
  ${text}
19503
19551
  </a>`;
19504
- dirPartsHtml += `
19505
- <span class="directory_separator">/</span>`;
19552
+ if (i > 0) {
19553
+ dirPartsHtml += `
19554
+ <span class="directory_separator">/</span>`;
19555
+ }
19506
19556
  i++;
19507
19557
  }
19508
19558
  if (isDir) {
19509
19559
  dirPartsHtml += `
19510
- <span class="directory_separator">/</span>`;
19560
+ <span class="directory_separator">/</span>`;
19511
19561
  }
19512
19562
  return dirPartsHtml;
19513
19563
  };
@@ -24497,19 +24547,26 @@ const createBuildFilesService = ({ buildDirectoryUrl, buildMainFilePath }) => {
24497
24547
  resource: `/${buildMainFilePath}`,
24498
24548
  };
24499
24549
  }
24500
- return fetchFileSystem(
24501
- new URL(request.resource.slice(1), buildDirectoryUrl),
24502
- {
24503
- headers: request.headers,
24504
- cacheControl: urlIsVersioned
24505
- ? `private,max-age=${SECONDS_IN_30_DAYS},immutable`
24506
- : "private,max-age=0,must-revalidate",
24507
- etagEnabled: true,
24508
- compressionEnabled: !request.pathname.endsWith(".mp4"),
24509
- rootDirectoryUrl: buildDirectoryUrl,
24510
- canReadDirectory: true,
24550
+ const urlObject = new URL(request.resource.slice(1), buildDirectoryUrl);
24551
+ return fetchFileSystem(urlObject, {
24552
+ headers: request.headers,
24553
+ cacheControl: urlIsVersioned
24554
+ ? `private,max-age=${SECONDS_IN_30_DAYS},immutable`
24555
+ : "private,max-age=0,must-revalidate",
24556
+ etagEnabled: true,
24557
+ compressionEnabled: !request.pathname.endsWith(".mp4"),
24558
+ rootDirectoryUrl: buildDirectoryUrl,
24559
+ canReadDirectory: true,
24560
+ ENOENTFallback: () => {
24561
+ if (
24562
+ !urlToExtension$1(urlObject) &&
24563
+ !urlToPathname$1(urlObject).endsWith("/")
24564
+ ) {
24565
+ return new URL(buildMainFilePath, buildDirectoryUrl);
24566
+ }
24567
+ return null;
24511
24568
  },
24512
- );
24569
+ });
24513
24570
  };
24514
24571
  };
24515
24572
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "39.9.7",
3
+ "version": "39.10.1",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -81,7 +81,7 @@
81
81
  "@jsenv/plugin-supervisor": "1.6.3",
82
82
  "@jsenv/plugin-transpilation": "1.4.92",
83
83
  "@jsenv/runtime-compat": "1.3.1",
84
- "@jsenv/server": "15.3.3",
84
+ "@jsenv/server": "15.4.0",
85
85
  "@jsenv/sourcemap": "1.2.30",
86
86
  "@jsenv/url-meta": "8.5.2",
87
87
  "@jsenv/urls": "2.6.0",
@@ -23,6 +23,7 @@ import {
23
23
  jsenvServiceErrorHandler,
24
24
  startServer,
25
25
  } from "@jsenv/server";
26
+ import { urlToExtension, urlToPathname } from "@jsenv/urls";
26
27
  import { existsSync } from "node:fs";
27
28
 
28
29
  /**
@@ -168,19 +169,26 @@ const createBuildFilesService = ({ buildDirectoryUrl, buildMainFilePath }) => {
168
169
  resource: `/${buildMainFilePath}`,
169
170
  };
170
171
  }
171
- return fetchFileSystem(
172
- new URL(request.resource.slice(1), buildDirectoryUrl),
173
- {
174
- headers: request.headers,
175
- cacheControl: urlIsVersioned
176
- ? `private,max-age=${SECONDS_IN_30_DAYS},immutable`
177
- : "private,max-age=0,must-revalidate",
178
- etagEnabled: true,
179
- compressionEnabled: !request.pathname.endsWith(".mp4"),
180
- rootDirectoryUrl: buildDirectoryUrl,
181
- canReadDirectory: true,
172
+ const urlObject = new URL(request.resource.slice(1), buildDirectoryUrl);
173
+ return fetchFileSystem(urlObject, {
174
+ headers: request.headers,
175
+ cacheControl: urlIsVersioned
176
+ ? `private,max-age=${SECONDS_IN_30_DAYS},immutable`
177
+ : "private,max-age=0,must-revalidate",
178
+ etagEnabled: true,
179
+ compressionEnabled: !request.pathname.endsWith(".mp4"),
180
+ rootDirectoryUrl: buildDirectoryUrl,
181
+ canReadDirectory: true,
182
+ ENOENTFallback: () => {
183
+ if (
184
+ !urlToExtension(urlObject) &&
185
+ !urlToPathname(urlObject).endsWith("/")
186
+ ) {
187
+ return new URL(buildMainFilePath, buildDirectoryUrl);
188
+ }
189
+ return null;
182
190
  },
183
- );
191
+ });
184
192
  };
185
193
  };
186
194
 
@@ -20,17 +20,12 @@ button {
20
20
  font-weight: bold;
21
21
  margin: 20px 25px 15px 25px;
22
22
  display: flex;
23
+ gap: 0.3em;
23
24
  }
24
25
  .directory_nav_item {
25
26
  text-decoration: none;
26
27
  position: relative;
27
28
  }
28
- .directory_separator {
29
- margin: 0 0.3em;
30
- }
31
- .directory_separator:first-child {
32
- margin-left: 0;
33
- }
34
29
  .directory_content {
35
30
  margin: 10px 15px 10px 15px;
36
31
  list-style-type: none;
@@ -30,6 +30,14 @@ export const jsenvPluginFsRedirection = ({
30
30
  if (reference.subtype === "new_url_second_arg") {
31
31
  return `ignore:${reference.url}`;
32
32
  }
33
+ if (reference.specifierPathname.endsWith("/...")) {
34
+ const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
35
+ const directoryUrl = new URL(
36
+ reference.specifierPathname.replace("/...", "/").slice(1),
37
+ rootDirectoryUrl,
38
+ ).href;
39
+ return directoryUrl;
40
+ }
33
41
  // ignore "./" on new URL("./")
34
42
  // if (
35
43
  // reference.subtype === "new_url_first_arg" &&
@@ -69,6 +69,14 @@ export const jsenvPluginProtocolFile = ({
69
69
  if (!generatedUrl.startsWith("file:")) {
70
70
  return null;
71
71
  }
72
+ if (reference.original) {
73
+ const originalSpecifierPathname =
74
+ reference.original.specifierPathname;
75
+
76
+ if (originalSpecifierPathname.endsWith("/...")) {
77
+ return originalSpecifierPathname;
78
+ }
79
+ }
72
80
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
73
81
  if (urlIsInsideOf(generatedUrl, rootDirectoryUrl)) {
74
82
  const result = `/${urlToRelativeUrl(generatedUrl, rootDirectoryUrl)}`;
@@ -221,14 +229,28 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
221
229
  const parts = isDir
222
230
  ? relativeUrlWithRoot.slice(0, -1).split("/")
223
231
  : relativeUrlWithRoot.split("/");
232
+ const items = [];
233
+ items.push({
234
+ href: "/",
235
+ text: "/",
236
+ });
224
237
  let dirPartsHtml = "";
225
238
  let i = 0;
226
239
  while (i < parts.length) {
227
240
  const part = parts[i];
228
- const href = i === 0 ? "/" : `/${parts.slice(1, i + 1).join("/")}/`;
241
+ const href = i === 0 ? "/..." : `/${parts.slice(1, i + 1).join("/")}/`;
229
242
  const text = part;
230
243
  const isLastPart = i === parts.length - 1;
231
- if (isLastPart) {
244
+ items.push({
245
+ href,
246
+ text,
247
+ isCurrent: isLastPart,
248
+ });
249
+ i++;
250
+ }
251
+ i = 0;
252
+ for (const { href, text, isCurrent } of items) {
253
+ if (isCurrent) {
232
254
  dirPartsHtml += `
233
255
  <span class="directory_nav_item" data-current>
234
256
  ${text}
@@ -239,13 +261,15 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
239
261
  <a class="directory_nav_item" href="${href}">
240
262
  ${text}
241
263
  </a>`;
242
- dirPartsHtml += `
243
- <span class="directory_separator">/</span>`;
264
+ if (i > 0) {
265
+ dirPartsHtml += `
266
+ <span class="directory_separator">/</span>`;
267
+ }
244
268
  i++;
245
269
  }
246
270
  if (isDir) {
247
271
  dirPartsHtml += `
248
- <span class="directory_separator">/</span>`;
272
+ <span class="directory_separator">/</span>`;
249
273
  }
250
274
  return dirPartsHtml;
251
275
  };