@jsenv/core 39.9.6 → 39.10.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.
@@ -1485,6 +1485,14 @@ const isValidUrl$1 = (url) => {
1485
1485
  }
1486
1486
  };
1487
1487
 
1488
+ const asSpecifierWithoutSearch = (specifier) => {
1489
+ if (isValidUrl$1(specifier)) {
1490
+ return asUrlWithoutSearch(specifier);
1491
+ }
1492
+ const [beforeQuestion] = specifier.split("?");
1493
+ return beforeQuestion;
1494
+ };
1495
+
1488
1496
  // normalize url search params:
1489
1497
  // Using URLSearchParams to alter the url search params
1490
1498
  // can result into "file:///file.css?css_module"
@@ -6973,6 +6981,14 @@ const startServer = async ({
6973
6981
  nodeResponse.end();
6974
6982
  return;
6975
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
+ }
6976
6992
 
6977
6993
  const receiveRequestOperation = Abort.startOperation();
6978
6994
  receiveRequestOperation.addAbortSource((abort) => {
@@ -8045,6 +8061,7 @@ const fetchFileSystem = async (
8045
8061
  : "no-store",
8046
8062
  canReadDirectory = false,
8047
8063
  rootDirectoryUrl, // = `${pathToFileURL(process.cwd())}/`,
8064
+ ENOENTFallback = () => {},
8048
8065
  } = {},
8049
8066
  ) => {
8050
8067
  const urlString = asUrlString(filesystemUrl);
@@ -8102,100 +8119,109 @@ const fetchFileSystem = async (
8102
8119
  };
8103
8120
  }
8104
8121
 
8105
- const sourceUrl = `file://${new URL(urlString).pathname}`;
8106
- try {
8107
- const [readStatTiming, sourceStat] = await timeFunction(
8108
- "file service>read file stat",
8109
- () => statSync(new URL(sourceUrl)),
8110
- );
8111
- if (sourceStat.isDirectory()) {
8112
- if (canReadDirectory) {
8113
- 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({
8114
8176
  headers,
8115
- canReadDirectory,
8116
- rootDirectoryUrl,
8177
+ fileUrl,
8178
+ });
8179
+ if (compressedResponse) {
8180
+ response = compressedResponse;
8181
+ }
8182
+ }
8183
+ if (!response) {
8184
+ response = await getRawResponse({
8185
+ fileStat,
8186
+ fileUrl,
8117
8187
  });
8118
8188
  }
8119
- return {
8120
- status: 403,
8121
- statusText: "not allowed to read directory",
8122
- };
8123
- }
8124
- // not a file, give up
8125
- if (!sourceStat.isFile()) {
8126
- return {
8127
- status: 404,
8128
- timing: readStatTiming,
8129
- };
8130
- }
8131
-
8132
- const clientCacheResponse = await getClientCacheResponse({
8133
- headers,
8134
- etagEnabled,
8135
- etagMemory,
8136
- etagMemoryMaxSize,
8137
- mtimeEnabled,
8138
- sourceStat,
8139
- sourceUrl,
8140
- });
8141
8189
 
8142
- // send 304 (redirect response to client cache)
8143
- // because the response body does not have to be transmitted
8144
- if (clientCacheResponse.status === 304) {
8145
- return composeTwoResponses(
8190
+ const intermediateResponse = composeTwoResponses(
8146
8191
  {
8147
8192
  timing: readStatTiming,
8148
8193
  headers: {
8149
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" } : {}),
8150
8201
  },
8151
8202
  },
8152
- clientCacheResponse,
8203
+ response,
8153
8204
  );
8154
- }
8155
-
8156
- let response;
8157
- if (compressionEnabled && sourceStat.size >= compressionSizeThreshold) {
8158
- const compressedResponse = await getCompressedResponse({
8159
- headers,
8160
- sourceUrl,
8161
- });
8162
- if (compressedResponse) {
8163
- 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
+ }
8164
8212
  }
8213
+ return composeTwoResponses(
8214
+ {
8215
+ headers: {
8216
+ ...(cacheControl ? { "cache-control": cacheControl } : {}),
8217
+ },
8218
+ },
8219
+ convertFileSystemErrorToResponseProperties(e) || {},
8220
+ );
8165
8221
  }
8166
- if (!response) {
8167
- response = await getRawResponse({
8168
- sourceStat,
8169
- sourceUrl,
8170
- });
8171
- }
8222
+ };
8172
8223
 
8173
- const intermediateResponse = composeTwoResponses(
8174
- {
8175
- timing: readStatTiming,
8176
- headers: {
8177
- ...(cacheControl ? { "cache-control": cacheControl } : {}),
8178
- // even if client cache is disabled, server can still
8179
- // send his own cache control but client should just ignore it
8180
- // and keep sending cache-control: 'no-store'
8181
- // if not, uncomment the line below to preserve client
8182
- // desire to ignore cache
8183
- // ...(headers["cache-control"] === "no-store" ? { "cache-control": "no-store" } : {}),
8184
- },
8185
- },
8186
- response,
8187
- );
8188
- return composeTwoResponses(intermediateResponse, clientCacheResponse);
8189
- } catch (e) {
8190
- return composeTwoResponses(
8191
- {
8192
- headers: {
8193
- ...(cacheControl ? { "cache-control": cacheControl } : {}),
8194
- },
8195
- },
8196
- convertFileSystemErrorToResponseProperties(e) || {},
8197
- );
8198
- }
8224
+ return serveFile(`file://${new URL(urlString).pathname}`);
8199
8225
  };
8200
8226
 
8201
8227
  const create500Response = (message) => {
@@ -8215,8 +8241,8 @@ const getClientCacheResponse = async ({
8215
8241
  etagMemory,
8216
8242
  etagMemoryMaxSize,
8217
8243
  mtimeEnabled,
8218
- sourceStat,
8219
- sourceUrl,
8244
+ fileStat,
8245
+ fileUrl,
8220
8246
  }) => {
8221
8247
  // here you might be tempted to add || headers["cache-control"] === "no-cache"
8222
8248
  // but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
@@ -8235,15 +8261,15 @@ const getClientCacheResponse = async ({
8235
8261
  headers,
8236
8262
  etagMemory,
8237
8263
  etagMemoryMaxSize,
8238
- sourceStat,
8239
- sourceUrl,
8264
+ fileStat,
8265
+ fileUrl,
8240
8266
  });
8241
8267
  }
8242
8268
 
8243
8269
  if (mtimeEnabled) {
8244
8270
  return getMtimeResponse({
8245
8271
  headers,
8246
- sourceStat,
8272
+ fileStat,
8247
8273
  });
8248
8274
  }
8249
8275
 
@@ -8254,8 +8280,8 @@ const getEtagResponse = async ({
8254
8280
  headers,
8255
8281
  etagMemory,
8256
8282
  etagMemoryMaxSize,
8257
- sourceUrl,
8258
- sourceStat,
8283
+ fileUrl,
8284
+ fileStat,
8259
8285
  }) => {
8260
8286
  const [computeEtagTiming, fileContentEtag] = await timeFunction(
8261
8287
  "file service>generate file etag",
@@ -8263,8 +8289,8 @@ const getEtagResponse = async ({
8263
8289
  computeEtag({
8264
8290
  etagMemory,
8265
8291
  etagMemoryMaxSize,
8266
- sourceUrl,
8267
- sourceStat,
8292
+ fileUrl,
8293
+ fileStat,
8268
8294
  }),
8269
8295
  );
8270
8296
 
@@ -8292,20 +8318,20 @@ const ETAG_MEMORY_MAP = new Map();
8292
8318
  const computeEtag = async ({
8293
8319
  etagMemory,
8294
8320
  etagMemoryMaxSize,
8295
- sourceUrl,
8296
- sourceStat,
8321
+ fileUrl,
8322
+ fileStat,
8297
8323
  }) => {
8298
8324
  if (etagMemory) {
8299
- const etagMemoryEntry = ETAG_MEMORY_MAP.get(sourceUrl);
8325
+ const etagMemoryEntry = ETAG_MEMORY_MAP.get(fileUrl);
8300
8326
  if (
8301
8327
  etagMemoryEntry &&
8302
- fileStatAreTheSame(etagMemoryEntry.sourceStat, sourceStat)
8328
+ fileStatAreTheSame(etagMemoryEntry.fileStat, fileStat)
8303
8329
  ) {
8304
8330
  return etagMemoryEntry.eTag;
8305
8331
  }
8306
8332
  }
8307
8333
  const fileContentAsBuffer = await new Promise((resolve, reject) => {
8308
- readFile(new URL(sourceUrl), (error, buffer) => {
8334
+ readFile(new URL(fileUrl), (error, buffer) => {
8309
8335
  if (error) {
8310
8336
  reject(error);
8311
8337
  } else {
@@ -8319,7 +8345,7 @@ const computeEtag = async ({
8319
8345
  const firstKey = Array.from(ETAG_MEMORY_MAP.keys())[0];
8320
8346
  ETAG_MEMORY_MAP.delete(firstKey);
8321
8347
  }
8322
- ETAG_MEMORY_MAP.set(sourceUrl, { sourceStat, eTag });
8348
+ ETAG_MEMORY_MAP.set(fileUrl, { fileStat, eTag });
8323
8349
  }
8324
8350
  return eTag;
8325
8351
  };
@@ -8344,7 +8370,7 @@ const fileStatKeysToCompare = [
8344
8370
  "blksize",
8345
8371
  ];
8346
8372
 
8347
- const getMtimeResponse = async ({ headers, sourceStat }) => {
8373
+ const getMtimeResponse = async ({ headers, fileStat }) => {
8348
8374
  if ("if-modified-since" in headers) {
8349
8375
  let cachedModificationDate;
8350
8376
  try {
@@ -8356,7 +8382,7 @@ const getMtimeResponse = async ({ headers, sourceStat }) => {
8356
8382
  };
8357
8383
  }
8358
8384
 
8359
- const actualModificationDate = dateToSecondsPrecision(sourceStat.mtime);
8385
+ const actualModificationDate = dateToSecondsPrecision(fileStat.mtime);
8360
8386
  if (Number(cachedModificationDate) >= Number(actualModificationDate)) {
8361
8387
  return {
8362
8388
  status: 304,
@@ -8367,12 +8393,12 @@ const getMtimeResponse = async ({ headers, sourceStat }) => {
8367
8393
  return {
8368
8394
  status: 200,
8369
8395
  headers: {
8370
- "last-modified": dateToUTCString(sourceStat.mtime),
8396
+ "last-modified": dateToUTCString(fileStat.mtime),
8371
8397
  },
8372
8398
  };
8373
8399
  };
8374
8400
 
8375
- const getCompressedResponse = async ({ sourceUrl, headers }) => {
8401
+ const getCompressedResponse = async ({ fileUrl, headers }) => {
8376
8402
  const acceptedCompressionFormat = pickContentEncoding(
8377
8403
  { headers },
8378
8404
  Object.keys(availableCompressionFormats),
@@ -8381,7 +8407,7 @@ const getCompressedResponse = async ({ sourceUrl, headers }) => {
8381
8407
  return null;
8382
8408
  }
8383
8409
 
8384
- const fileReadableStream = fileUrlToReadableStream(sourceUrl);
8410
+ const fileReadableStream = fileUrlToReadableStream(fileUrl);
8385
8411
  const body =
8386
8412
  await availableCompressionFormats[acceptedCompressionFormat](
8387
8413
  fileReadableStream,
@@ -8390,7 +8416,7 @@ const getCompressedResponse = async ({ sourceUrl, headers }) => {
8390
8416
  return {
8391
8417
  status: 200,
8392
8418
  headers: {
8393
- "content-type": CONTENT_TYPE.fromUrlExtension(sourceUrl),
8419
+ "content-type": CONTENT_TYPE.fromUrlExtension(fileUrl),
8394
8420
  "content-encoding": acceptedCompressionFormat,
8395
8421
  "vary": "accept-encoding",
8396
8422
  },
@@ -8420,14 +8446,14 @@ const availableCompressionFormats = {
8420
8446
  },
8421
8447
  };
8422
8448
 
8423
- const getRawResponse = async ({ sourceUrl, sourceStat }) => {
8449
+ const getRawResponse = async ({ fileUrl, fileStat }) => {
8424
8450
  return {
8425
8451
  status: 200,
8426
8452
  headers: {
8427
- "content-type": CONTENT_TYPE.fromUrlExtension(sourceUrl),
8428
- "content-length": sourceStat.size,
8453
+ "content-type": CONTENT_TYPE.fromUrlExtension(fileUrl),
8454
+ "content-length": fileStat.size,
8429
8455
  },
8430
- body: fileUrlToReadableStream(sourceUrl),
8456
+ body: fileUrlToReadableStream(fileUrl),
8431
8457
  };
8432
8458
  };
8433
8459
 
@@ -13085,6 +13111,7 @@ const createReference = ({
13085
13111
  );
13086
13112
  }
13087
13113
  }
13114
+
13088
13115
  const reference = {
13089
13116
  id: ++referenceId,
13090
13117
  ownerUrlInfo,
@@ -13107,6 +13134,9 @@ const createReference = ({
13107
13134
  integrity,
13108
13135
  crossorigin,
13109
13136
  specifier,
13137
+ get specifierPathname() {
13138
+ return asSpecifierWithoutSearch(reference.specifier);
13139
+ },
13110
13140
  specifierStart,
13111
13141
  specifierEnd,
13112
13142
  specifierLine,
@@ -15602,7 +15632,7 @@ const jsenvPluginDirectoryReferenceEffect = (
15602
15632
  reference.filenameHint = `${
15603
15633
  reference.ownerUrlInfo.filenameHint
15604
15634
  }${urlToFilename$1(reference.url)}/`;
15605
- } else if (reference.specifier.endsWith("./")) ; else {
15635
+ } else if (reference.specifierPathname.endsWith("./")) ; else {
15606
15636
  reference.filenameHint = `${urlToFilename$1(reference.url)}/`;
15607
15637
  }
15608
15638
  let actionForDirectory;
@@ -18890,7 +18920,7 @@ const createNodeEsmResolver = ({
18890
18920
  return reference.specifier;
18891
18921
  }
18892
18922
  const { ownerUrlInfo } = reference;
18893
- if (reference.specifier[0] === "/") {
18923
+ if (reference.specifierPathname[0] === "/") {
18894
18924
  const url = new URL(
18895
18925
  reference.specifier.slice(1),
18896
18926
  ownerUrlInfo.context.rootDirectoryUrl,
@@ -19092,7 +19122,7 @@ const jsenvPluginWebResolution = () => {
19092
19122
  appliesDuring: "*",
19093
19123
  resolveReference: (reference) => {
19094
19124
  const { ownerUrlInfo } = reference;
19095
- if (reference.specifier[0] === "/") {
19125
+ if (reference.specifierPathname[0] === "/") {
19096
19126
  const url = new URL(
19097
19127
  reference.specifier.slice(1),
19098
19128
  ownerUrlInfo.context.rootDirectoryUrl,
@@ -19168,6 +19198,14 @@ const jsenvPluginFsRedirection = ({
19168
19198
  if (reference.subtype === "new_url_second_arg") {
19169
19199
  return `ignore:${reference.url}`;
19170
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
+ }
19171
19209
  // ignore "./" on new URL("./")
19172
19210
  // if (
19173
19211
  // reference.subtype === "new_url_first_arg" &&
@@ -19319,6 +19357,14 @@ const jsenvPluginProtocolFile = ({
19319
19357
  if (!generatedUrl.startsWith("file:")) {
19320
19358
  return null;
19321
19359
  }
19360
+ if (reference.original) {
19361
+ const originalSpecifierPathname =
19362
+ reference.original.specifierPathname;
19363
+
19364
+ if (originalSpecifierPathname.endsWith("/...")) {
19365
+ return originalSpecifierPathname;
19366
+ }
19367
+ }
19322
19368
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19323
19369
  if (urlIsInsideOf(generatedUrl, rootDirectoryUrl)) {
19324
19370
  const result = `/${urlToRelativeUrl(generatedUrl, rootDirectoryUrl)}`;
@@ -19475,7 +19521,7 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
19475
19521
  let i = 0;
19476
19522
  while (i < parts.length) {
19477
19523
  const part = parts[i];
19478
- const href = i === 0 ? "/" : `/${parts.slice(1, i + 1).join("/")}/`;
19524
+ const href = i === 0 ? "/..." : `/${parts.slice(1, i + 1).join("/")}/`;
19479
19525
  const text = part;
19480
19526
  const isLastPart = i === parts.length - 1;
19481
19527
  if (isLastPart) {
@@ -21133,7 +21179,7 @@ const getCorePlugins = ({
21133
21179
  appliesDuring: "*",
21134
21180
  resolveReference: (reference) => {
21135
21181
  const { ownerUrlInfo } = reference;
21136
- if (reference.specifier === "/") {
21182
+ if (reference.specifierPathname === "/") {
21137
21183
  const { mainFilePath, rootDirectoryUrl } = ownerUrlInfo.context;
21138
21184
  const url = new URL(mainFilePath, rootDirectoryUrl);
21139
21185
  return url;
@@ -21586,7 +21632,7 @@ const createBuildSpecifierManager = ({
21586
21632
  const url = new URL(reference.specifier, ownerRawUrl).href;
21587
21633
  return url;
21588
21634
  }
21589
- if (reference.specifier[0] === "/") {
21635
+ if (reference.specifierPathname[0] === "/") {
21590
21636
  const url = new URL(reference.specifier.slice(1), sourceDirectoryUrl)
21591
21637
  .href;
21592
21638
  return url;
@@ -21711,6 +21757,7 @@ const createBuildSpecifierManager = ({
21711
21757
  type: reference.type,
21712
21758
  expectedType: reference.expectedType,
21713
21759
  specifier: reference.specifier,
21760
+ specifierPathname: reference.specifierPathname,
21714
21761
  specifierLine: reference.specifierLine,
21715
21762
  specifierColumn: reference.specifierColumn,
21716
21763
  specifierStart: reference.specifierStart,
@@ -24484,19 +24531,26 @@ const createBuildFilesService = ({ buildDirectoryUrl, buildMainFilePath }) => {
24484
24531
  resource: `/${buildMainFilePath}`,
24485
24532
  };
24486
24533
  }
24487
- return fetchFileSystem(
24488
- new URL(request.resource.slice(1), buildDirectoryUrl),
24489
- {
24490
- headers: request.headers,
24491
- cacheControl: urlIsVersioned
24492
- ? `private,max-age=${SECONDS_IN_30_DAYS},immutable`
24493
- : "private,max-age=0,must-revalidate",
24494
- etagEnabled: true,
24495
- compressionEnabled: !request.pathname.endsWith(".mp4"),
24496
- rootDirectoryUrl: buildDirectoryUrl,
24497
- canReadDirectory: true,
24534
+ const urlObject = new URL(request.resource.slice(1), buildDirectoryUrl);
24535
+ return fetchFileSystem(urlObject, {
24536
+ headers: request.headers,
24537
+ cacheControl: urlIsVersioned
24538
+ ? `private,max-age=${SECONDS_IN_30_DAYS},immutable`
24539
+ : "private,max-age=0,must-revalidate",
24540
+ etagEnabled: true,
24541
+ compressionEnabled: !request.pathname.endsWith(".mp4"),
24542
+ rootDirectoryUrl: buildDirectoryUrl,
24543
+ canReadDirectory: true,
24544
+ ENOENTFallback: () => {
24545
+ if (
24546
+ !urlToExtension$1(urlObject) &&
24547
+ !urlToPathname$1(urlObject).endsWith("/")
24548
+ ) {
24549
+ return new URL(buildMainFilePath, buildDirectoryUrl);
24550
+ }
24551
+ return null;
24498
24552
  },
24499
- );
24553
+ });
24500
24554
  };
24501
24555
  };
24502
24556
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "39.9.6",
3
+ "version": "39.10.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -69,22 +69,22 @@
69
69
  "dependencies": {
70
70
  "@financial-times/polyfill-useragent-normaliser": "1.10.2",
71
71
  "@jsenv/abort": "4.3.0",
72
- "@jsenv/ast": "6.4.3",
73
- "@jsenv/filesystem": "4.13.1",
72
+ "@jsenv/ast": "6.4.4",
73
+ "@jsenv/filesystem": "4.13.2",
74
74
  "@jsenv/humanize": "1.2.8",
75
75
  "@jsenv/importmap": "1.2.1",
76
76
  "@jsenv/integrity": "0.0.2",
77
- "@jsenv/js-module-fallback": "1.3.55",
77
+ "@jsenv/js-module-fallback": "1.3.56",
78
78
  "@jsenv/node-esm-resolution": "1.0.6",
79
- "@jsenv/plugin-bundling": "2.7.23",
79
+ "@jsenv/plugin-bundling": "2.7.24",
80
80
  "@jsenv/plugin-minification": "1.5.13",
81
- "@jsenv/plugin-supervisor": "1.6.2",
82
- "@jsenv/plugin-transpilation": "1.4.91",
81
+ "@jsenv/plugin-supervisor": "1.6.3",
82
+ "@jsenv/plugin-transpilation": "1.4.92",
83
83
  "@jsenv/runtime-compat": "1.3.1",
84
- "@jsenv/server": "15.3.3",
85
- "@jsenv/sourcemap": "1.2.29",
84
+ "@jsenv/server": "15.4.0",
85
+ "@jsenv/sourcemap": "1.2.30",
86
86
  "@jsenv/url-meta": "8.5.2",
87
- "@jsenv/urls": "2.5.4",
87
+ "@jsenv/urls": "2.6.0",
88
88
  "@jsenv/utils": "2.1.2",
89
89
  "string-width": "7.2.0"
90
90
  },
@@ -170,7 +170,7 @@ export const createBuildSpecifierManager = ({
170
170
  const url = new URL(reference.specifier, ownerRawUrl).href;
171
171
  return url;
172
172
  }
173
- if (reference.specifier[0] === "/") {
173
+ if (reference.specifierPathname[0] === "/") {
174
174
  const url = new URL(reference.specifier.slice(1), sourceDirectoryUrl)
175
175
  .href;
176
176
  return url;
@@ -295,6 +295,7 @@ export const createBuildSpecifierManager = ({
295
295
  type: reference.type,
296
296
  expectedType: reference.expectedType,
297
297
  specifier: reference.specifier,
298
+ specifierPathname: reference.specifierPathname,
298
299
  specifierLine: reference.specifierLine,
299
300
  specifierColumn: reference.specifierColumn,
300
301
  specifierStart: reference.specifierStart,
@@ -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
 
@@ -1,6 +1,7 @@
1
1
  import { generateUrlForInlineContent } from "@jsenv/ast";
2
2
  import { generateContentFrame } from "@jsenv/humanize";
3
3
  import {
4
+ asSpecifierWithoutSearch,
4
5
  getCallerPosition,
5
6
  stringifyUrlSite,
6
7
  urlToBasename,
@@ -315,6 +316,7 @@ const createReference = ({
315
316
  );
316
317
  }
317
318
  }
319
+
318
320
  const reference = {
319
321
  id: ++referenceId,
320
322
  ownerUrlInfo,
@@ -337,6 +339,9 @@ const createReference = ({
337
339
  integrity,
338
340
  crossorigin,
339
341
  specifier,
342
+ get specifierPathname() {
343
+ return asSpecifierWithoutSearch(reference.specifier);
344
+ },
340
345
  specifierStart,
341
346
  specifierEnd,
342
347
  specifierLine,
@@ -30,7 +30,7 @@ export const jsenvPluginDirectoryReferenceEffect = (
30
30
  reference.filenameHint = `${
31
31
  reference.ownerUrlInfo.filenameHint
32
32
  }${urlToFilename(reference.url)}/`;
33
- } else if (reference.specifier.endsWith("./")) {
33
+ } else if (reference.specifierPathname.endsWith("./")) {
34
34
  } else {
35
35
  reference.filenameHint = `${urlToFilename(reference.url)}/`;
36
36
  }
@@ -86,7 +86,7 @@ export const getCorePlugins = ({
86
86
  appliesDuring: "*",
87
87
  resolveReference: (reference) => {
88
88
  const { ownerUrlInfo } = reference;
89
- if (reference.specifier === "/") {
89
+ if (reference.specifierPathname === "/") {
90
90
  const { mainFilePath, rootDirectoryUrl } = ownerUrlInfo.context;
91
91
  const url = new URL(mainFilePath, rootDirectoryUrl);
92
92
  return url;
@@ -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)}`;
@@ -225,7 +233,7 @@ const generateDirectoryNav = (relativeUrl, rootDirectoryUrl) => {
225
233
  let i = 0;
226
234
  while (i < parts.length) {
227
235
  const part = parts[i];
228
- const href = i === 0 ? "/" : `/${parts.slice(1, i + 1).join("/")}/`;
236
+ const href = i === 0 ? "/..." : `/${parts.slice(1, i + 1).join("/")}/`;
229
237
  const text = part;
230
238
  const isLastPart = i === parts.length - 1;
231
239
  if (isLastPart) {
@@ -33,7 +33,7 @@ export const createNodeEsmResolver = ({
33
33
  return reference.specifier;
34
34
  }
35
35
  const { ownerUrlInfo } = reference;
36
- if (reference.specifier[0] === "/") {
36
+ if (reference.specifierPathname[0] === "/") {
37
37
  const url = new URL(
38
38
  reference.specifier.slice(1),
39
39
  ownerUrlInfo.context.rootDirectoryUrl,
@@ -4,7 +4,7 @@ export const jsenvPluginWebResolution = () => {
4
4
  appliesDuring: "*",
5
5
  resolveReference: (reference) => {
6
6
  const { ownerUrlInfo } = reference;
7
- if (reference.specifier[0] === "/") {
7
+ if (reference.specifierPathname[0] === "/") {
8
8
  const url = new URL(
9
9
  reference.specifier.slice(1),
10
10
  ownerUrlInfo.context.rootDirectoryUrl,