@jsenv/core 39.12.0 → 39.13.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.
Files changed (38) hide show
  1. package/dist/css/directory_listing.css +211 -0
  2. package/dist/html/directory_listing.html +18 -0
  3. package/dist/js/directory_listing.js +240 -0
  4. package/dist/jsenv_core.js +1035 -764
  5. package/dist/other/dir.png +0 -0
  6. package/dist/other/file.png +0 -0
  7. package/dist/other/home.svg +6 -0
  8. package/package.json +6 -6
  9. package/src/build/build.js +7 -7
  10. package/src/build/build_specifier_manager.js +0 -1
  11. package/src/build/build_urls_generator.js +0 -1
  12. package/src/dev/start_dev_server.js +66 -52
  13. package/src/kitchen/kitchen.js +1 -4
  14. package/src/kitchen/out_directory_url.js +2 -1
  15. package/src/kitchen/url_graph/references.js +1 -1
  16. package/src/kitchen/url_graph/url_graph.js +1 -0
  17. package/src/kitchen/url_graph/url_info_transformations.js +37 -4
  18. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +10 -8
  19. package/src/plugins/plugin_controller.js +170 -114
  20. package/src/plugins/plugins.js +10 -6
  21. package/src/plugins/protocol_file/client/assets/home.svg +5 -5
  22. package/src/plugins/protocol_file/client/directory_listing.css +190 -0
  23. package/src/plugins/protocol_file/client/directory_listing.html +18 -0
  24. package/src/plugins/protocol_file/client/directory_listing.jsx +250 -0
  25. package/src/plugins/protocol_file/file_and_server_urls_converter.js +32 -0
  26. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +398 -0
  27. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +43 -370
  28. package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +3 -2
  29. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +7 -6
  30. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -3
  31. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +2 -18
  32. package/src/plugins/server_events/jsenv_plugin_server_events.js +100 -0
  33. package/dist/html/directory.html +0 -184
  34. package/dist/html/html_404_and_ancestor_dir.html +0 -222
  35. package/src/plugins/protocol_file/client/assets/directory.css +0 -150
  36. package/src/plugins/protocol_file/client/directory.html +0 -17
  37. package/src/plugins/protocol_file/client/html_404_and_ancestor_dir.html +0 -54
  38. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +0 -37
@@ -1467,6 +1467,7 @@ const pathnameToExtension$1 = (pathname) => {
1467
1467
  };
1468
1468
 
1469
1469
  const asUrlWithoutSearch = (url) => {
1470
+ url = String(url);
1470
1471
  if (url.includes("?")) {
1471
1472
  const urlObject = new URL(url);
1472
1473
  urlObject.search = "";
@@ -1618,6 +1619,15 @@ const setUrlFilename = (url, filename) => {
1618
1619
  });
1619
1620
  };
1620
1621
 
1622
+ const setUrlBasename = (url, basename) => {
1623
+ return setUrlFilename(url, (filename) => {
1624
+ if (typeof basename === "function") {
1625
+ basename = basename(filenameToBasename(filename));
1626
+ }
1627
+ return `${basename}${urlToExtension$1(url)}`;
1628
+ });
1629
+ };
1630
+
1621
1631
  const transformUrlPathname = (url, transformer) => {
1622
1632
  if (typeof url === "string") {
1623
1633
  const urlObject = new URL(url);
@@ -5003,6 +5013,17 @@ const fromNodeRequest = (
5003
5013
  nodeRequest,
5004
5014
  { serverOrigin, signal, requestBodyLifetime },
5005
5015
  ) => {
5016
+ const handleRequestOperation = Abort.startOperation();
5017
+ if (signal) {
5018
+ handleRequestOperation.addAbortSignal(signal);
5019
+ }
5020
+ handleRequestOperation.addAbortSource((abort) => {
5021
+ nodeRequest.once("close", abort);
5022
+ return () => {
5023
+ nodeRequest.removeListener("close", abort);
5024
+ };
5025
+ });
5026
+
5006
5027
  const headers = headersFromObject(nodeRequest.headers);
5007
5028
  const body = observableFromNodeStream(nodeRequest, {
5008
5029
  readableStreamLifetime: requestBodyLifetime,
@@ -5024,7 +5045,7 @@ const fromNodeRequest = (
5024
5045
  }
5025
5046
 
5026
5047
  return Object.freeze({
5027
- signal,
5048
+ signal: handleRequestOperation.signal,
5028
5049
  http2: Boolean(nodeRequest.stream),
5029
5050
  origin: requestOrigin,
5030
5051
  ...getPropertiesFromResource({
@@ -6924,6 +6945,15 @@ const startServer = async ({
6924
6945
  status = "stopped";
6925
6946
  stoppedResolve(reason);
6926
6947
  });
6948
+ let stopAbortSignal;
6949
+ {
6950
+ let stopAbortController = new AbortController();
6951
+ stopCallbackSet.add(() => {
6952
+ stopAbortController.abort();
6953
+ stopAbortController = undefined;
6954
+ });
6955
+ stopAbortSignal = stopAbortController.signal;
6956
+ }
6927
6957
 
6928
6958
  const cancelProcessTeardownRace = raceProcessTeardownEvents(
6929
6959
  processTeardownEvents,
@@ -6991,6 +7021,9 @@ const startServer = async ({
6991
7021
  }
6992
7022
 
6993
7023
  const receiveRequestOperation = Abort.startOperation();
7024
+ receiveRequestOperation.addAbortSignal(stopAbortSignal);
7025
+ const sendResponseOperation = Abort.startOperation();
7026
+ sendResponseOperation.addAbortSignal(stopAbortSignal);
6994
7027
  receiveRequestOperation.addAbortSource((abort) => {
6995
7028
  const closeEventCallback = () => {
6996
7029
  if (nodeRequest.complete) {
@@ -7005,19 +7038,11 @@ const startServer = async ({
7005
7038
  nodeRequest.removeListener("close", closeEventCallback);
7006
7039
  };
7007
7040
  });
7008
- receiveRequestOperation.addAbortSource((abort) => {
7009
- return stopCallbackSet.add(abort);
7010
- });
7011
-
7012
- const sendResponseOperation = Abort.startOperation();
7013
7041
  sendResponseOperation.addAbortSignal(receiveRequestOperation.signal);
7014
- sendResponseOperation.addAbortSource((abort) => {
7015
- return stopCallbackSet.add(abort);
7016
- });
7017
7042
 
7018
7043
  const request = fromNodeRequest(nodeRequest, {
7044
+ signal: stopAbortSignal,
7019
7045
  serverOrigin,
7020
- signal: receiveRequestOperation.signal,
7021
7046
  });
7022
7047
 
7023
7048
  // Handling request is asynchronous, we buffer logs for that request
@@ -7591,13 +7616,16 @@ const startServer = async ({
7591
7616
  socket,
7592
7617
  head,
7593
7618
  async (websocket) => {
7619
+ const websocketAbortController = new AbortController();
7594
7620
  websocketClients.add(websocket);
7621
+ websocket.signal = websocketAbortController.signal;
7595
7622
  websocket.once("close", () => {
7596
7623
  websocketClients.delete(websocket);
7624
+ websocketAbortController.abort();
7597
7625
  });
7598
7626
  const request = fromNodeRequest(nodeRequest, {
7627
+ signal: stopAbortSignal,
7599
7628
  serverOrigin: websocketOrigin,
7600
- signal: new AbortController().signal,
7601
7629
  requestBodyLifetime,
7602
7630
  });
7603
7631
  serviceController.callAsyncHooksUntil(
@@ -11711,7 +11739,7 @@ const generateHtmlForSyntaxError = (
11711
11739
  errorLinkText: `${htmlRelativeUrl}:${line}:${column}`,
11712
11740
  syntaxError: escapeHtml(htmlErrorContentFrame),
11713
11741
  };
11714
- const html = replacePlaceholders$2(htmlForSyntaxError, replacers);
11742
+ const html = replacePlaceholders$1(htmlForSyntaxError, replacers);
11715
11743
  return html;
11716
11744
  };
11717
11745
  const escapeHtml = (string) => {
@@ -11722,7 +11750,7 @@ const escapeHtml = (string) => {
11722
11750
  .replace(/"/g, """)
11723
11751
  .replace(/'/g, "'");
11724
11752
  };
11725
- const replacePlaceholders$2 = (html, replacers) => {
11753
+ const replacePlaceholders$1 = (html, replacers) => {
11726
11754
  return html.replace(/\$\{(\w+)\}/g, (match, name) => {
11727
11755
  const replacer = replacers[name];
11728
11756
  if (replacer === undefined) {
@@ -11738,6 +11766,7 @@ const replacePlaceholders$2 = (html, replacers) => {
11738
11766
  const HOOK_NAMES = [
11739
11767
  "init",
11740
11768
  "serve", // is called only during dev/tests
11769
+ "serveWebsocket",
11741
11770
  "resolveReference",
11742
11771
  "redirectReference",
11743
11772
  "transformReferenceSearchParams",
@@ -11750,6 +11779,7 @@ const HOOK_NAMES = [
11750
11779
  "cooked",
11751
11780
  "augmentResponse", // is called only during dev/tests
11752
11781
  "destroy",
11782
+ "effect",
11753
11783
  ];
11754
11784
 
11755
11785
  const createPluginController = (
@@ -11763,19 +11793,18 @@ const createPluginController = (
11763
11793
  return value;
11764
11794
  };
11765
11795
 
11766
- const plugins = [];
11767
- // precompute a list of hooks per hookName for one major reason:
11768
- // - When debugging, there is less iteration
11769
- // also it should increase perf as there is less work to do
11770
- const hookGroups = {};
11771
- const addPlugin = (plugin, { position = "end" }) => {
11796
+ const pluginCandidates = [];
11797
+ const activeEffectSet = new Set();
11798
+ const activePlugins = [];
11799
+ // precompute a list of hooks per hookName because:
11800
+ // 1. [MAJOR REASON] when debugging, there is less iteration (so much better)
11801
+ // 2. [MINOR REASON] it should increase perf as there is less work to do
11802
+ const hookSetMap = new Map();
11803
+ const addPlugin = (plugin, options) => {
11772
11804
  if (Array.isArray(plugin)) {
11773
- if (position === "start") {
11774
- plugin = plugin.slice().reverse();
11805
+ for (const value of plugin) {
11806
+ addPlugin(value);
11775
11807
  }
11776
- plugin.forEach((plugin) => {
11777
- addPlugin(plugin, { position });
11778
- });
11779
11808
  return;
11780
11809
  }
11781
11810
  if (plugin === null || typeof plugin !== "object") {
@@ -11785,65 +11814,10 @@ const createPluginController = (
11785
11814
  plugin.name = "anonymous";
11786
11815
  }
11787
11816
  if (!testAppliesDuring(plugin) || !initPlugin(plugin)) {
11788
- if (plugin.destroy) {
11789
- plugin.destroy();
11790
- }
11817
+ plugin.destroy?.();
11791
11818
  return;
11792
11819
  }
11793
- plugins.push(plugin);
11794
- for (const key of Object.keys(plugin)) {
11795
- if (key === "meta") {
11796
- const value = plugin[key];
11797
- if (typeof value !== "object" || value === null) {
11798
- console.warn(`plugin.meta must be an object, got ${value}`);
11799
- continue;
11800
- }
11801
- Object.assign(pluginsMeta, value);
11802
- // any extension/modification on plugin.meta
11803
- // won't be taken into account so we freeze object
11804
- // to throw in case it happen
11805
- Object.freeze(value);
11806
- continue;
11807
- }
11808
-
11809
- if (
11810
- key === "name" ||
11811
- key === "appliesDuring" ||
11812
- key === "init" ||
11813
- key === "serverEvents" ||
11814
- key === "mustStayFirst"
11815
- ) {
11816
- continue;
11817
- }
11818
- const isHook = HOOK_NAMES.includes(key);
11819
- if (!isHook) {
11820
- console.warn(`Unexpected "${key}" property on "${plugin.name}" plugin`);
11821
- continue;
11822
- }
11823
- const hookName = key;
11824
- const hookValue = plugin[hookName];
11825
- if (hookValue) {
11826
- const group = hookGroups[hookName] || (hookGroups[hookName] = []);
11827
- const hook = {
11828
- plugin,
11829
- name: hookName,
11830
- value: hookValue,
11831
- };
11832
- if (position === "start") {
11833
- let i = 0;
11834
- while (i < group.length) {
11835
- const before = group[i];
11836
- if (!before.plugin.mustStayFirst) {
11837
- break;
11838
- }
11839
- i++;
11840
- }
11841
- group.splice(i, 0, hook);
11842
- } else {
11843
- group.push(hook);
11844
- }
11845
- }
11846
- }
11820
+ pluginCandidates.push(plugin);
11847
11821
  };
11848
11822
  const testAppliesDuring = (plugin) => {
11849
11823
  const { appliesDuring } = plugin;
@@ -11882,22 +11856,131 @@ const createPluginController = (
11882
11856
  );
11883
11857
  };
11884
11858
  const initPlugin = (plugin) => {
11885
- if (plugin.init) {
11886
- const initReturnValue = plugin.init(kitchenContext, plugin);
11887
- if (initReturnValue === false) {
11888
- return false;
11889
- }
11890
- if (typeof initReturnValue === "function" && !plugin.destroy) {
11891
- plugin.destroy = initReturnValue;
11892
- }
11859
+ const { init } = plugin;
11860
+ if (!init) {
11861
+ return true;
11862
+ }
11863
+ const initReturnValue = init(kitchenContext, { plugin });
11864
+ if (initReturnValue === false) {
11865
+ return false;
11866
+ }
11867
+ if (typeof initReturnValue === "function" && !plugin.destroy) {
11868
+ plugin.destroy = initReturnValue;
11893
11869
  }
11894
11870
  return true;
11895
11871
  };
11896
- const pushPlugin = (plugin) => {
11897
- addPlugin(plugin, { position: "end" });
11872
+ const pushPlugin = (...args) => {
11873
+ for (const arg of args) {
11874
+ addPlugin(arg);
11875
+ }
11876
+ updateActivePlugins();
11898
11877
  };
11899
- const unshiftPlugin = (plugin) => {
11900
- addPlugin(plugin, { position: "start" });
11878
+ const updateActivePlugins = () => {
11879
+ // construct activePlugins and hooks according
11880
+ // to the one present in candidates and their effects
11881
+ // 1. active plugins is an empty array
11882
+ // 2. all active effects are cleaned-up
11883
+ // 3. all effects are re-activated if still relevant
11884
+ // 4. hooks are precomputed according to plugin order
11885
+
11886
+ // 1.
11887
+ activePlugins.length = 0;
11888
+ // 2.
11889
+ for (const { cleanup } of activeEffectSet) {
11890
+ cleanup();
11891
+ }
11892
+ activeEffectSet.clear();
11893
+ for (const pluginCandidate of pluginCandidates) {
11894
+ const effect = pluginCandidate.effect;
11895
+ if (!effect) {
11896
+ activePlugins.push(pluginCandidate);
11897
+ continue;
11898
+ }
11899
+ }
11900
+ // 3.
11901
+ for (const pluginCandidate of pluginCandidates) {
11902
+ const effect = pluginCandidate.effect;
11903
+ if (!effect) {
11904
+ continue;
11905
+ }
11906
+ const returnValue = effect({
11907
+ kitchenContext,
11908
+ otherPlugins: activePlugins,
11909
+ });
11910
+ if (!returnValue) {
11911
+ continue;
11912
+ }
11913
+ activePlugins.push(pluginCandidate);
11914
+ activeEffectSet.add({
11915
+ plugin: pluginCandidate,
11916
+ cleanup: typeof returnValue === "function" ? returnValue : () => {},
11917
+ });
11918
+ }
11919
+ // 4.
11920
+ activePlugins.sort((a, b) => {
11921
+ return pluginCandidates.indexOf(a) - pluginCandidates.indexOf(b);
11922
+ });
11923
+ hookSetMap.clear();
11924
+ for (const activePlugin of activePlugins) {
11925
+ for (const key of Object.keys(activePlugin)) {
11926
+ if (key === "meta") {
11927
+ const value = activePlugin[key];
11928
+ if (typeof value !== "object" || value === null) {
11929
+ console.warn(`plugin.meta must be an object, got ${value}`);
11930
+ continue;
11931
+ }
11932
+ Object.assign(pluginsMeta, value);
11933
+ // any extension/modification on plugin.meta
11934
+ // won't be taken into account so we freeze object
11935
+ // to throw in case it happen
11936
+ Object.freeze(value);
11937
+ continue;
11938
+ }
11939
+ if (
11940
+ key === "name" ||
11941
+ key === "appliesDuring" ||
11942
+ key === "init" ||
11943
+ key === "serverEvents" ||
11944
+ key === "mustStayFirst" ||
11945
+ key === "effect"
11946
+ ) {
11947
+ continue;
11948
+ }
11949
+ const isHook = HOOK_NAMES.includes(key);
11950
+ if (!isHook) {
11951
+ console.warn(
11952
+ `Unexpected "${key}" property on "${activePlugin.name}" plugin`,
11953
+ );
11954
+ continue;
11955
+ }
11956
+ const hookName = key;
11957
+ const hookValue = activePlugin[hookName];
11958
+ if (hookValue) {
11959
+ let hookSet = hookSetMap.get(hookName);
11960
+ if (!hookSet) {
11961
+ hookSet = new Set();
11962
+ hookSetMap.set(hookName, hookSet);
11963
+ }
11964
+ const hook = {
11965
+ plugin: activePlugin,
11966
+ name: hookName,
11967
+ value: hookValue,
11968
+ };
11969
+ // if (position === "start") {
11970
+ // let i = 0;
11971
+ // while (i < group.length) {
11972
+ // const before = group[i];
11973
+ // if (!before.plugin.mustStayFirst) {
11974
+ // break;
11975
+ // }
11976
+ // i++;
11977
+ // }
11978
+ // group.splice(i, 0, hook);
11979
+ // } else {
11980
+ hookSet.add(hook);
11981
+ }
11982
+ }
11983
+ }
11901
11984
  };
11902
11985
 
11903
11986
  let lastPluginUsed = null;
@@ -11950,64 +12033,66 @@ const createPluginController = (
11950
12033
  };
11951
12034
 
11952
12035
  const callHooks = (hookName, info, callback) => {
11953
- const hooks = hookGroups[hookName];
11954
- if (hooks) {
11955
- const setHookParams = (firstArg = info) => {
11956
- info = firstArg;
11957
- };
11958
- for (const hook of hooks) {
11959
- const returnValue = callHook(hook, info);
11960
- if (returnValue && callback) {
11961
- callback(returnValue, hook.plugin, setHookParams);
11962
- }
12036
+ const hookSet = hookSetMap.get(hookName);
12037
+ if (!hookSet) {
12038
+ return;
12039
+ }
12040
+ const setHookParams = (firstArg = info) => {
12041
+ info = firstArg;
12042
+ };
12043
+ for (const hook of hookSet) {
12044
+ const returnValue = callHook(hook, info);
12045
+ if (returnValue && callback) {
12046
+ callback(returnValue, hook.plugin, setHookParams);
11963
12047
  }
11964
12048
  }
11965
12049
  };
11966
12050
  const callAsyncHooks = async (hookName, info, callback, options) => {
11967
- const hooks = hookGroups[hookName];
11968
- if (hooks) {
11969
- for (const hook of hooks) {
11970
- const returnValue = await callAsyncHook(hook, info);
11971
- if (returnValue && callback) {
11972
- await callback(returnValue, hook.plugin);
11973
- }
12051
+ const hookSet = hookSetMap.get(hookName);
12052
+ if (!hookSet) {
12053
+ return;
12054
+ }
12055
+ for (const hook of hookSet) {
12056
+ const returnValue = await callAsyncHook(hook, info);
12057
+ if (returnValue && callback) {
12058
+ await callback(returnValue, hook.plugin);
11974
12059
  }
11975
12060
  }
11976
12061
  };
11977
12062
 
11978
12063
  const callHooksUntil = (hookName, info) => {
11979
- const hooks = hookGroups[hookName];
11980
- if (hooks) {
11981
- for (const hook of hooks) {
11982
- const returnValue = callHook(hook, info);
11983
- if (returnValue) {
11984
- return returnValue;
11985
- }
12064
+ const hookSet = hookSetMap.get(hookName);
12065
+ if (!hookSet) {
12066
+ return null;
12067
+ }
12068
+ for (const hook of hookSet) {
12069
+ const returnValue = callHook(hook, info);
12070
+ if (returnValue) {
12071
+ return returnValue;
11986
12072
  }
11987
12073
  }
11988
12074
  return null;
11989
12075
  };
11990
12076
  const callAsyncHooksUntil = async (hookName, info, options) => {
11991
- const hooks = hookGroups[hookName];
11992
- if (!hooks) {
12077
+ const hookSet = hookSetMap.get(hookName);
12078
+ if (!hookSet) {
11993
12079
  return null;
11994
12080
  }
11995
- if (hooks.length === 0) {
12081
+ if (hookSet.size === 0) {
11996
12082
  return null;
11997
12083
  }
12084
+ const iterator = hookSet.values()[Symbol.iterator]();
11998
12085
  let result;
11999
- let index = 0;
12000
12086
  const visit = async () => {
12001
- if (index >= hooks.length) {
12087
+ const { done, value: hook } = iterator.next();
12088
+ if (done) {
12002
12089
  return;
12003
12090
  }
12004
- const hook = hooks[index];
12005
12091
  const returnValue = await callAsyncHook(hook, info);
12006
12092
  if (returnValue) {
12007
12093
  result = returnValue;
12008
12094
  return;
12009
12095
  }
12010
- index++;
12011
12096
  await visit();
12012
12097
  };
12013
12098
  await visit();
@@ -12016,9 +12101,8 @@ const createPluginController = (
12016
12101
 
12017
12102
  return {
12018
12103
  pluginsMeta,
12019
- plugins,
12104
+ activePlugins,
12020
12105
  pushPlugin,
12021
- unshiftPlugin,
12022
12106
  getHookFunction,
12023
12107
  callHook,
12024
12108
  callAsyncHook,
@@ -12614,11 +12698,12 @@ const determineFileUrlForOutDirectory = (urlInfo) => {
12614
12698
  if (filenameHint) {
12615
12699
  url = setUrlFilename(url, filenameHint);
12616
12700
  }
12617
- return moveUrl({
12701
+ const outUrl = moveUrl({
12618
12702
  url,
12619
12703
  from: rootDirectoryUrl,
12620
12704
  to: outDirectoryUrl,
12621
12705
  });
12706
+ return outUrl;
12622
12707
  };
12623
12708
 
12624
12709
  const determineSourcemapFileUrl = (urlInfo) => {
@@ -12910,7 +12995,7 @@ const createDependencies = (ownerUrlInfo) => {
12910
12995
  const injectAsBannerCodeBeforeFinalize = (urlInfoReceiver) => {
12911
12996
  const basename = urlToBasename(sideEffectFileUrl);
12912
12997
  const inlineUrl = generateUrlForInlineContent({
12913
- url: urlInfoReceiver.url,
12998
+ url: urlInfoReceiver.originalUrl || urlInfoReceiver.url,
12914
12999
  basename,
12915
13000
  extension: urlToExtension$1(sideEffectFileUrl),
12916
13001
  });
@@ -13982,6 +14067,7 @@ const createUrlInfo = (url, context) => {
13982
14067
  writable: false,
13983
14068
  value: url,
13984
14069
  });
14070
+ urlInfo.pathname = new URL(url).pathname;
13985
14071
  urlInfo.searchParams = new URL(url).searchParams;
13986
14072
 
13987
14073
  urlInfo.dependencies = createDependencies(urlInfo);
@@ -14570,7 +14656,15 @@ const createUrlInfoTransformer = ({
14570
14656
  contentIsInlined = false;
14571
14657
  }
14572
14658
  if (!contentIsInlined) {
14573
- writeFileSync(new URL(generatedUrl), urlInfo.content, { force: true });
14659
+ const generatedUrlObject = new URL(generatedUrl);
14660
+ let baseName = urlToBasename(generatedUrlObject);
14661
+ for (const [key, value] of generatedUrlObject.searchParams) {
14662
+ baseName += `7${encodeFilePathComponent(key)}=${encodeFilePathComponent(value)}`;
14663
+ }
14664
+ const outFileUrl = setUrlBasename(generatedUrlObject, baseName);
14665
+ let outFilePath = urlToFileSystemPath(outFileUrl);
14666
+ outFilePath = truncate(outFilePath, 2055); // for windows
14667
+ writeFileSync(outFilePath, urlInfo.content, { force: true });
14574
14668
  }
14575
14669
  const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
14576
14670
  if (sourcemapGeneratedUrl && sourcemapReference) {
@@ -14689,6 +14783,26 @@ const createUrlInfoTransformer = ({
14689
14783
  };
14690
14784
  };
14691
14785
 
14786
+ // https://gist.github.com/barbietunnie/7bc6d48a424446c44ff4
14787
+ const illegalRe = /[/?<>\\:*|"]/g;
14788
+ // eslint-disable-next-line no-control-regex
14789
+ const controlRe = /[\x00-\x1f\x80-\x9f]/g;
14790
+ const reservedRe = /^\.+$/;
14791
+ const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
14792
+ const encodeFilePathComponent = (input, replacement = "") => {
14793
+ const encoded = input
14794
+ .replace(illegalRe, replacement)
14795
+ .replace(controlRe, replacement)
14796
+ .replace(reservedRe, replacement)
14797
+ .replace(windowsReservedRe, replacement);
14798
+ return encoded;
14799
+ };
14800
+ const truncate = (sanitized, length) => {
14801
+ const uint8Array = new TextEncoder().encode(sanitized);
14802
+ const truncated = uint8Array.slice(0, length);
14803
+ return new TextDecoder().decode(truncated);
14804
+ };
14805
+
14692
14806
  const shouldUpdateSourcemapComment = (urlInfo, sourcemaps) => {
14693
14807
  if (urlInfo.context.buildStep === "shape") {
14694
14808
  return false;
@@ -14698,7 +14812,6 @@ const shouldUpdateSourcemapComment = (urlInfo, sourcemaps) => {
14698
14812
  }
14699
14813
  return false;
14700
14814
  };
14701
-
14702
14815
  const mayHaveSourcemap = (urlInfo) => {
14703
14816
  if (urlInfo.url.startsWith("data:")) {
14704
14817
  return false;
@@ -14708,7 +14821,6 @@ const mayHaveSourcemap = (urlInfo) => {
14708
14821
  }
14709
14822
  return true;
14710
14823
  };
14711
-
14712
14824
  const shouldHandleSourcemap = (urlInfo) => {
14713
14825
  const { sourcemaps } = urlInfo.context;
14714
14826
  if (
@@ -14792,10 +14904,7 @@ const createKitchen = ({
14792
14904
  initialPluginsMeta,
14793
14905
  );
14794
14906
  kitchen.pluginController = pluginController;
14795
- pluginController.pushPlugin(jsenvPluginHtmlSyntaxErrorFallback());
14796
- plugins.forEach((pluginEntry) => {
14797
- pluginController.pushPlugin(pluginEntry);
14798
- });
14907
+ pluginController.pushPlugin(jsenvPluginHtmlSyntaxErrorFallback(), ...plugins);
14799
14908
 
14800
14909
  const urlInfoTransformer = createUrlInfoTransformer({
14801
14910
  logger,
@@ -15808,10 +15917,11 @@ const jsenvPluginInliningIntoHtml = () => {
15808
15917
  const { line, column, isOriginal } = getHtmlNodePosition(linkNode, {
15809
15918
  preferOriginal: true,
15810
15919
  });
15811
- const linkInlineUrl = getUrlForContentInsideHtml(linkNode, {
15812
- htmlUrl: urlInfo.url,
15813
- url: linkReference.url,
15814
- });
15920
+ const linkInlineUrl = getUrlForContentInsideHtml(
15921
+ linkNode,
15922
+ urlInfo,
15923
+ linkReference,
15924
+ );
15815
15925
  const linkReferenceInlined = linkReference.inline({
15816
15926
  line,
15817
15927
  column,
@@ -15860,10 +15970,11 @@ const jsenvPluginInliningIntoHtml = () => {
15860
15970
  const { line, column, isOriginal } = getHtmlNodePosition(scriptNode, {
15861
15971
  preferOriginal: true,
15862
15972
  });
15863
- const scriptInlineUrl = getUrlForContentInsideHtml(scriptNode, {
15864
- htmlUrl: urlInfo.url,
15865
- url: scriptReference.url,
15866
- });
15973
+ const scriptInlineUrl = getUrlForContentInsideHtml(
15974
+ scriptNode,
15975
+ urlInfo,
15976
+ scriptReference,
15977
+ );
15867
15978
  const scriptReferenceInlined = scriptReference.inline({
15868
15979
  line,
15869
15980
  column,
@@ -17052,9 +17163,11 @@ const jsenvPluginHtmlReferenceAnalysis = ({
17052
17163
  const { line, column, isOriginal } = getHtmlNodePosition(node, {
17053
17164
  preferOriginal: true,
17054
17165
  });
17055
- const inlineContentUrl = getUrlForContentInsideHtml(node, {
17056
- htmlUrl: urlInfo.url,
17057
- });
17166
+ const inlineContentUrl = getUrlForContentInsideHtml(
17167
+ node,
17168
+ urlInfo,
17169
+ null,
17170
+ );
17058
17171
  const debug =
17059
17172
  getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
17060
17173
  const inlineReference = urlInfo.dependencies.foundInline({
@@ -17195,9 +17308,8 @@ const jsenvPluginHtmlReferenceAnalysis = ({
17195
17308
  );
17196
17309
  const importmapInlineUrl = getUrlForContentInsideHtml(
17197
17310
  scriptNode,
17198
- {
17199
- htmlUrl: urlInfo.url,
17200
- },
17311
+ urlInfo,
17312
+ importmapReference,
17201
17313
  );
17202
17314
  const importmapReferenceInlined = importmapReference.inline({
17203
17315
  line,
@@ -17481,9 +17593,7 @@ const parseAndTransformJsReferences = async (
17481
17593
  Object.keys(urlInfo.context.runtimeCompat).toString() === "node";
17482
17594
 
17483
17595
  const onInlineReference = (inlineReferenceInfo) => {
17484
- const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, {
17485
- url: urlInfo.url,
17486
- });
17596
+ const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, urlInfo);
17487
17597
  let { quote } = inlineReferenceInfo;
17488
17598
  if (quote === "`" && !canUseTemplateLiterals) {
17489
17599
  // if quote is "`" and template literals are not supported
@@ -17726,23 +17836,7 @@ const jsenvPluginInlineContentFetcher = () => {
17726
17836
  if (!urlInfo.isInline) {
17727
17837
  return null;
17728
17838
  }
17729
- let isDirectRequestToFile;
17730
- if (urlInfo.context.request) {
17731
- let requestResource = urlInfo.context.request.resource;
17732
- let requestedUrl;
17733
- if (requestResource.startsWith("/@fs/")) {
17734
- const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
17735
- requestedUrl = `file:///${fsRootRelativeUrl}`;
17736
- } else {
17737
- const requestedUrlObject = new URL(
17738
- requestResource.slice(1),
17739
- urlInfo.context.rootDirectoryUrl,
17740
- );
17741
- requestedUrlObject.searchParams.delete("hot");
17742
- requestedUrl = requestedUrlObject.href;
17743
- }
17744
- isDirectRequestToFile = requestedUrl === urlInfo.url;
17745
- }
17839
+ const isDirectRequest = urlInfo.context.requestedUrl === urlInfo.url;
17746
17840
  /*
17747
17841
  * We want to find inline content but it's not straightforward
17748
17842
  *
@@ -17771,7 +17865,7 @@ const jsenvPluginInlineContentFetcher = () => {
17771
17865
  originalContent = reference.content;
17772
17866
  }
17773
17867
  lastInlineReference = reference;
17774
- if (isDirectRequestToFile) {
17868
+ if (isDirectRequest) {
17775
17869
  break;
17776
17870
  }
17777
17871
  }
@@ -19184,41 +19278,551 @@ const jsenvPluginVersionSearchParam = () => {
19184
19278
  };
19185
19279
  };
19186
19280
 
19187
- const jsenvPluginFsRedirection = ({
19188
- directoryContentMagicName,
19189
- magicExtensions = ["inherit", ".js"],
19190
- magicDirectoryIndex = true,
19191
- preserveSymlinks = false,
19192
- }) => {
19281
+ const FILE_AND_SERVER_URLS_CONVERTER = {
19282
+ asServerUrl: (fileUrl, serverRootDirectoryUrl) => {
19283
+ if (fileUrl === serverRootDirectoryUrl) {
19284
+ return "/";
19285
+ }
19286
+ if (urlIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
19287
+ const urlRelativeToServer = urlToRelativeUrl(
19288
+ fileUrl,
19289
+ serverRootDirectoryUrl,
19290
+ );
19291
+ return `/${urlRelativeToServer}`;
19292
+ }
19293
+ const urlRelativeToFilesystemRoot = String(fileUrl).slice(
19294
+ "file:///".length,
19295
+ );
19296
+ return `/@fs/${urlRelativeToFilesystemRoot}`;
19297
+ },
19298
+ asFileUrl: (urlRelativeToServer, serverRootDirectoryUrl) => {
19299
+ if (urlRelativeToServer.startsWith("/@fs/")) {
19300
+ const urlRelativeToFilesystemRoot = urlRelativeToServer.slice(
19301
+ "/@fs/".length,
19302
+ );
19303
+ return `file:///${urlRelativeToFilesystemRoot}`;
19304
+ }
19305
+ if (urlRelativeToServer[0] === "/") {
19306
+ return new URL(urlRelativeToServer.slice(1), serverRootDirectoryUrl).href;
19307
+ }
19308
+ return new URL(urlRelativeToServer, serverRootDirectoryUrl).href;
19309
+ },
19310
+ };
19311
+
19312
+ const jsenvPluginInjections = (rawAssociations) => {
19313
+ let resolvedAssociations;
19314
+
19193
19315
  return {
19194
- name: "jsenv:fs_redirection",
19316
+ name: "jsenv:injections",
19195
19317
  appliesDuring: "*",
19196
- redirectReference: (reference) => {
19197
- // http, https, data, about, ...
19198
- if (!reference.url.startsWith("file:")) {
19318
+ init: (context) => {
19319
+ resolvedAssociations = URL_META.resolveAssociations(
19320
+ { injectionsGetter: rawAssociations },
19321
+ context.rootDirectoryUrl,
19322
+ );
19323
+ },
19324
+ transformUrlContent: async (urlInfo) => {
19325
+ const { injectionsGetter } = URL_META.applyAssociations({
19326
+ url: asUrlWithoutSearch(urlInfo.url),
19327
+ associations: resolvedAssociations,
19328
+ });
19329
+ if (!injectionsGetter) {
19199
19330
  return null;
19200
19331
  }
19201
- if (reference.isInline) {
19202
- return null;
19332
+ if (typeof injectionsGetter !== "function") {
19333
+ throw new TypeError("injectionsGetter must be a function");
19203
19334
  }
19204
- if (reference.url === "file:///" || reference.url === "file://") {
19205
- return `ignore:file:///`;
19335
+ const injections = await injectionsGetter(urlInfo);
19336
+ if (!injections) {
19337
+ return null;
19206
19338
  }
19207
- // ignore all new URL second arg
19208
- if (reference.subtype === "new_url_second_arg") {
19209
- return `ignore:${reference.url}`;
19339
+ const keys = Object.keys(injections);
19340
+ if (keys.length === 0) {
19341
+ return null;
19210
19342
  }
19211
- if (
19212
- reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
19213
- ) {
19214
- const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19215
- const directoryUrl = new URL(
19216
- reference.specifierPathname
19217
- .replace(`/${directoryContentMagicName}`, "/")
19218
- .slice(1),
19219
- rootDirectoryUrl,
19220
- ).href;
19221
- return directoryUrl;
19343
+ return replacePlaceholders(urlInfo.content, injections, urlInfo);
19344
+ },
19345
+ };
19346
+ };
19347
+
19348
+ const injectionSymbol = Symbol.for("jsenv_injection");
19349
+ const INJECTIONS = {
19350
+ optional: (value) => {
19351
+ return { [injectionSymbol]: "optional", value };
19352
+ },
19353
+ };
19354
+
19355
+ // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
19356
+ const replacePlaceholders = (content, replacements, urlInfo) => {
19357
+ const magicSource = createMagicSource(content);
19358
+ for (const key of Object.keys(replacements)) {
19359
+ let index = content.indexOf(key);
19360
+ const replacement = replacements[key];
19361
+ let isOptional;
19362
+ let value;
19363
+ if (replacement && replacement[injectionSymbol]) {
19364
+ const valueBehindSymbol = replacement[injectionSymbol];
19365
+ isOptional = valueBehindSymbol === "optional";
19366
+ value = replacement.value;
19367
+ } else {
19368
+ value = replacement;
19369
+ }
19370
+ if (index === -1) {
19371
+ if (!isOptional) {
19372
+ urlInfo.context.logger.warn(
19373
+ `placeholder "${key}" not found in ${urlInfo.url}.
19374
+ --- suggestion a ---
19375
+ Add "${key}" in that file.
19376
+ --- suggestion b ---
19377
+ Fix eventual typo in "${key}"?
19378
+ --- suggestion c ---
19379
+ Mark injection as optional using INJECTIONS.optional():
19380
+ import { INJECTIONS } from "@jsenv/core";
19381
+
19382
+ return {
19383
+ "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
19384
+ };`,
19385
+ );
19386
+ }
19387
+ continue;
19388
+ }
19389
+
19390
+ while (index !== -1) {
19391
+ const start = index;
19392
+ const end = index + key.length;
19393
+ magicSource.replace({
19394
+ start,
19395
+ end,
19396
+ replacement:
19397
+ urlInfo.type === "js_classic" ||
19398
+ urlInfo.type === "js_module" ||
19399
+ urlInfo.type === "html"
19400
+ ? JSON.stringify(value, null, " ")
19401
+ : value,
19402
+ });
19403
+ index = content.indexOf(key, end);
19404
+ }
19405
+ }
19406
+ return magicSource.toContentAndSourcemap();
19407
+ };
19408
+
19409
+ /*
19410
+ * NICE TO HAVE:
19411
+ *
19412
+ * - when visiting urls outside server root directory the UI is messed up
19413
+ *
19414
+ * Let's say I visit file outside the server root directory that is in 404
19415
+ * We must update the enoent message and maybe other things to take into account
19416
+ * that url is no longer /something but "@fs/project_root/something" in the browser url bar
19417
+ *
19418
+ * - watching directory might result into things that are not properly handled:
19419
+ * 1. the existing directory is deleted
19420
+ * -> we should update the whole page to use a new "firstExistingDirectoryUrl"
19421
+ * 2. the enoent is impacted
19422
+ * -> we should update the ENOENT message
19423
+ * It means the websocket should contain more data and we can't assume firstExistingDirectoryUrl won't change
19424
+ *
19425
+
19426
+ */
19427
+
19428
+
19429
+ const htmlFileUrlForDirectory = new URL(
19430
+ "./html/directory_listing.html",
19431
+ import.meta.url,
19432
+ );
19433
+
19434
+ const jsenvPluginDirectoryListing = ({
19435
+ urlMocks = false,
19436
+ autoreload = true,
19437
+ directoryContentMagicName,
19438
+ }) => {
19439
+ return {
19440
+ name: "jsenv:directory_listing",
19441
+ appliesDuring: "dev",
19442
+ redirectReference: (reference) => {
19443
+ if (reference.isInline) {
19444
+ return null;
19445
+ }
19446
+ const url = reference.url;
19447
+ if (!url.startsWith("file:")) {
19448
+ return null;
19449
+ }
19450
+ let { fsStat } = reference;
19451
+ if (!fsStat) {
19452
+ fsStat = readEntryStatSync(url, { nullIfNotFound: true });
19453
+ reference.fsStat = fsStat;
19454
+ }
19455
+ const { request, requestedUrl } = reference.ownerUrlInfo.context;
19456
+ if (!fsStat) {
19457
+ if (
19458
+ requestedUrl === url &&
19459
+ request &&
19460
+ request.headers["sec-fetch-dest"] === "document"
19461
+ ) {
19462
+ return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}&enoent`;
19463
+ }
19464
+ return null;
19465
+ }
19466
+ const isDirectory = fsStat?.isDirectory();
19467
+ if (!isDirectory) {
19468
+ return null;
19469
+ }
19470
+ if (reference.type === "filesystem") {
19471
+ // TODO: we should redirect to something like /...json
19472
+ // and any file name ...json is a special file serving directory content as json
19473
+ return null;
19474
+ }
19475
+ const acceptsHtml = request
19476
+ ? pickContentType(request, ["text/html"])
19477
+ : false;
19478
+ if (!acceptsHtml) {
19479
+ return null;
19480
+ }
19481
+ reference.fsStat = null; // reset fsStat, now it's not a directory anyor
19482
+ return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}`;
19483
+ },
19484
+ transformUrlContent: {
19485
+ html: (urlInfo) => {
19486
+ const urlWithoutSearch = asUrlWithoutSearch(urlInfo.url);
19487
+ if (urlWithoutSearch !== String(htmlFileUrlForDirectory)) {
19488
+ return null;
19489
+ }
19490
+ const requestedUrl = urlInfo.searchParams.get("url");
19491
+ if (!requestedUrl) {
19492
+ return null;
19493
+ }
19494
+ urlInfo.headers["cache-control"] = "no-cache";
19495
+ const enoent = urlInfo.searchParams.has("enoent");
19496
+ if (enoent) {
19497
+ urlInfo.status = 404;
19498
+ urlInfo.headers["cache-control"] = "no-cache";
19499
+ }
19500
+ const request = urlInfo.context.request;
19501
+ const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
19502
+ return replacePlaceholders(
19503
+ urlInfo.content,
19504
+ {
19505
+ ...generateDirectoryListingInjection(requestedUrl, {
19506
+ autoreload,
19507
+ request,
19508
+ urlMocks,
19509
+ directoryContentMagicName,
19510
+ rootDirectoryUrl,
19511
+ mainFilePath,
19512
+ enoent,
19513
+ }),
19514
+ },
19515
+ urlInfo,
19516
+ );
19517
+ },
19518
+ },
19519
+ serveWebsocket: ({ websocket, request, context }) => {
19520
+ if (!autoreload) {
19521
+ return false;
19522
+ }
19523
+ const secProtocol = request.headers["sec-websocket-protocol"];
19524
+ if (secProtocol !== "watch-directory") {
19525
+ return false;
19526
+ }
19527
+ const { rootDirectoryUrl, mainFilePath } = context;
19528
+ const requestedUrl = FILE_AND_SERVER_URLS_CONVERTER.asFileUrl(
19529
+ request.pathname,
19530
+ rootDirectoryUrl,
19531
+ );
19532
+ const closestDirectoryUrl = getFirstExistingDirectoryUrl(requestedUrl);
19533
+ const sendMessage = (message) => {
19534
+ websocket.send(JSON.stringify(message));
19535
+ };
19536
+ const generateItems = () => {
19537
+ const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
19538
+ requestedUrl,
19539
+ rootDirectoryUrl,
19540
+ );
19541
+ const items = getDirectoryContentItems({
19542
+ serverRootDirectoryUrl: rootDirectoryUrl,
19543
+ mainFilePath,
19544
+ requestedUrl,
19545
+ firstExistingDirectoryUrl,
19546
+ });
19547
+ return items;
19548
+ };
19549
+
19550
+ const unwatch = registerDirectoryLifecycle(closestDirectoryUrl, {
19551
+ added: ({ relativeUrl }) => {
19552
+ sendMessage({
19553
+ type: "change",
19554
+ reason: `${relativeUrl} added`,
19555
+ items: generateItems(),
19556
+ });
19557
+ },
19558
+ updated: ({ relativeUrl }) => {
19559
+ sendMessage({
19560
+ type: "change",
19561
+ reason: `${relativeUrl} updated`,
19562
+ items: generateItems(),
19563
+ });
19564
+ },
19565
+ removed: ({ relativeUrl }) => {
19566
+ sendMessage({
19567
+ type: "change",
19568
+ reason: `${relativeUrl} removed`,
19569
+ items: generateItems(),
19570
+ });
19571
+ },
19572
+ });
19573
+ websocket.signal.addEventListener("abort", () => {
19574
+ unwatch();
19575
+ });
19576
+ return true;
19577
+ },
19578
+ };
19579
+ };
19580
+
19581
+ const generateDirectoryListingInjection = (
19582
+ requestedUrl,
19583
+ {
19584
+ rootDirectoryUrl,
19585
+ mainFilePath,
19586
+ request,
19587
+ urlMocks,
19588
+ directoryContentMagicName,
19589
+ autoreload,
19590
+ enoent,
19591
+ },
19592
+ ) => {
19593
+ let serverRootDirectoryUrl = rootDirectoryUrl;
19594
+ const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
19595
+ requestedUrl,
19596
+ serverRootDirectoryUrl,
19597
+ );
19598
+ const directoryContentItems = getDirectoryContentItems({
19599
+ serverRootDirectoryUrl,
19600
+ mainFilePath,
19601
+ requestedUrl,
19602
+ firstExistingDirectoryUrl,
19603
+ });
19604
+ package_workspaces: {
19605
+ const packageDirectoryUrl = lookupPackageDirectory(serverRootDirectoryUrl);
19606
+ if (!packageDirectoryUrl) {
19607
+ break package_workspaces;
19608
+ }
19609
+ if (String(packageDirectoryUrl) === String(serverRootDirectoryUrl)) {
19610
+ break package_workspaces;
19611
+ }
19612
+ rootDirectoryUrl = packageDirectoryUrl;
19613
+ // if (String(firstExistingDirectoryUrl) === String(serverRootDirectoryUrl)) {
19614
+ // let packageContent;
19615
+ // try {
19616
+ // packageContent = JSON.parse(
19617
+ // readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
19618
+ // );
19619
+ // } catch {
19620
+ // break package_workspaces;
19621
+ // }
19622
+ // const { workspaces } = packageContent;
19623
+ // if (Array.isArray(workspaces)) {
19624
+ // for (const workspace of workspaces) {
19625
+ // const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
19626
+ // const workspaceUrl = workspaceUrlObject.href;
19627
+ // if (workspaceUrl.endsWith("*")) {
19628
+ // const directoryUrl = ensurePathnameTrailingSlash(
19629
+ // workspaceUrl.slice(0, -1),
19630
+ // );
19631
+ // fileUrls.push(new URL(directoryUrl));
19632
+ // } else {
19633
+ // fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
19634
+ // }
19635
+ // }
19636
+ // }
19637
+ // }
19638
+ }
19639
+ const directoryUrlRelativeToServer =
19640
+ FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
19641
+ firstExistingDirectoryUrl,
19642
+ serverRootDirectoryUrl,
19643
+ );
19644
+ const websocketScheme = request.protocol === "https" ? "wss" : "ws";
19645
+ const { host } = new URL(request.url);
19646
+ const websocketUrl = `${websocketScheme}://${host}${directoryUrlRelativeToServer}`;
19647
+
19648
+ const navItems = [];
19649
+ {
19650
+ const lastItemUrl = firstExistingDirectoryUrl;
19651
+ const lastItemRelativeUrl = urlToRelativeUrl(lastItemUrl, rootDirectoryUrl);
19652
+ const rootDirectoryUrlName = urlToFilename$1(rootDirectoryUrl);
19653
+ let parts;
19654
+ if (lastItemRelativeUrl) {
19655
+ parts = `${rootDirectoryUrlName}/${lastItemRelativeUrl}`.split("/");
19656
+ } else {
19657
+ parts = [rootDirectoryUrlName];
19658
+ }
19659
+
19660
+ let i = 0;
19661
+ while (i < parts.length) {
19662
+ const part = parts[i];
19663
+ const isLastPart = i === parts.length - 1;
19664
+ if (isLastPart && part === "") {
19665
+ // ignore trailing slash
19666
+ break;
19667
+ }
19668
+ let navItemRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
19669
+ let navItemUrl =
19670
+ navItemRelativeUrl === ""
19671
+ ? rootDirectoryUrl
19672
+ : new URL(navItemRelativeUrl, rootDirectoryUrl).href;
19673
+ if (!isLastPart) {
19674
+ navItemUrl = ensurePathnameTrailingSlash(navItemUrl);
19675
+ }
19676
+ let urlRelativeToServer = FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
19677
+ navItemUrl,
19678
+ serverRootDirectoryUrl,
19679
+ );
19680
+ let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, requestedUrl);
19681
+ const isServerRootDirectory = navItemUrl === serverRootDirectoryUrl;
19682
+ if (isServerRootDirectory) {
19683
+ urlRelativeToServer = `/${directoryContentMagicName}`;
19684
+ urlRelativeToDocument = `/${directoryContentMagicName}`;
19685
+ }
19686
+ const name = part;
19687
+ const isCurrent = navItemUrl === String(firstExistingDirectoryUrl);
19688
+ navItems.push({
19689
+ url: navItemUrl,
19690
+ urlRelativeToServer,
19691
+ urlRelativeToDocument,
19692
+ isServerRootDirectory,
19693
+ isCurrent,
19694
+ name,
19695
+ });
19696
+ i++;
19697
+ }
19698
+ }
19699
+
19700
+ let enoentDetails = null;
19701
+ if (enoent) {
19702
+ const fileRelativeUrl = urlToRelativeUrl(
19703
+ requestedUrl,
19704
+ serverRootDirectoryUrl,
19705
+ );
19706
+ let filePathExisting;
19707
+ let filePathNotFound;
19708
+ const existingIndex = String(firstExistingDirectoryUrl).length;
19709
+ filePathExisting = urlToRelativeUrl(
19710
+ firstExistingDirectoryUrl,
19711
+ serverRootDirectoryUrl,
19712
+ );
19713
+ filePathNotFound = requestedUrl.slice(existingIndex);
19714
+ enoentDetails = {
19715
+ fileUrl: requestedUrl,
19716
+ fileRelativeUrl,
19717
+ filePathExisting: `/${filePathExisting}`,
19718
+ filePathNotFound,
19719
+ };
19720
+ }
19721
+
19722
+ return {
19723
+ __DIRECTORY_LISTING__: {
19724
+ enoentDetails,
19725
+ navItems,
19726
+ urlMocks,
19727
+ directoryContentMagicName,
19728
+ directoryUrl: firstExistingDirectoryUrl,
19729
+ serverRootDirectoryUrl,
19730
+ rootDirectoryUrl,
19731
+ mainFilePath,
19732
+ directoryContentItems,
19733
+ websocketUrl,
19734
+ autoreload,
19735
+ },
19736
+ };
19737
+ };
19738
+ const getFirstExistingDirectoryUrl = (requestedUrl, serverRootDirectoryUrl) => {
19739
+ let firstExistingDirectoryUrl = new URL("./", requestedUrl);
19740
+ while (!existsSync(firstExistingDirectoryUrl)) {
19741
+ firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
19742
+ if (!urlIsInsideOf(firstExistingDirectoryUrl, serverRootDirectoryUrl)) {
19743
+ firstExistingDirectoryUrl = new URL(serverRootDirectoryUrl);
19744
+ break;
19745
+ }
19746
+ }
19747
+ return firstExistingDirectoryUrl;
19748
+ };
19749
+ const getDirectoryContentItems = ({
19750
+ serverRootDirectoryUrl,
19751
+ mainFilePath,
19752
+ firstExistingDirectoryUrl,
19753
+ }) => {
19754
+ const directoryContentArray = readdirSync(new URL(firstExistingDirectoryUrl));
19755
+ const fileUrls = [];
19756
+ for (const filename of directoryContentArray) {
19757
+ const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
19758
+ if (lstatSync(fileUrlObject).isDirectory()) {
19759
+ fileUrls.push(ensurePathnameTrailingSlash(fileUrlObject));
19760
+ } else {
19761
+ fileUrls.push(fileUrlObject);
19762
+ }
19763
+ }
19764
+ fileUrls.sort((a, b) => {
19765
+ return comparePathnames(a.pathname, b.pathname);
19766
+ });
19767
+ const items = [];
19768
+ for (const fileUrl of fileUrls) {
19769
+ const urlRelativeToCurrentDirectory = urlToRelativeUrl(
19770
+ fileUrl,
19771
+ firstExistingDirectoryUrl,
19772
+ );
19773
+ const urlRelativeToServer = FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
19774
+ fileUrl,
19775
+ serverRootDirectoryUrl,
19776
+ );
19777
+ const url = String(fileUrl);
19778
+ const mainFileUrl = new URL(mainFilePath, serverRootDirectoryUrl).href;
19779
+ const isMainFile = url === mainFileUrl;
19780
+
19781
+ items.push({
19782
+ url,
19783
+ urlRelativeToCurrentDirectory,
19784
+ urlRelativeToServer,
19785
+ isMainFile,
19786
+ });
19787
+ }
19788
+ return items;
19789
+ };
19790
+
19791
+ const jsenvPluginFsRedirection = ({
19792
+ directoryContentMagicName,
19793
+ magicExtensions = ["inherit", ".js"],
19794
+ magicDirectoryIndex = true,
19795
+ preserveSymlinks = false,
19796
+ }) => {
19797
+ return {
19798
+ name: "jsenv:fs_redirection",
19799
+ appliesDuring: "*",
19800
+ redirectReference: (reference) => {
19801
+ // http, https, data, about, ...
19802
+ if (!reference.url.startsWith("file:")) {
19803
+ return null;
19804
+ }
19805
+ if (reference.isInline) {
19806
+ return null;
19807
+ }
19808
+ if (reference.url === "file:///" || reference.url === "file://") {
19809
+ return `ignore:file:///`;
19810
+ }
19811
+ // ignore all new URL second arg
19812
+ if (reference.subtype === "new_url_second_arg") {
19813
+ return `ignore:${reference.url}`;
19814
+ }
19815
+ if (
19816
+ reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
19817
+ ) {
19818
+ const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19819
+ const directoryUrl = new URL(
19820
+ reference.specifierPathname
19821
+ .replace(`/${directoryContentMagicName}`, "/")
19822
+ .slice(1),
19823
+ rootDirectoryUrl,
19824
+ ).href;
19825
+ return directoryUrl;
19222
19826
  }
19223
19827
  // ignore "./" on new URL("./")
19224
19828
  // if (
@@ -19318,21 +19922,13 @@ const resolveSymlink = (fileUrl) => {
19318
19922
  return realUrlObject.href;
19319
19923
  };
19320
19924
 
19321
- const html404AndAncestorDirFileUrl = new URL(
19322
- "./html/html_404_and_ancestor_dir.html",
19323
- import.meta.url,
19324
- );
19325
- const htmlFileUrlForDirectory = new URL(
19326
- "./html/directory.html",
19327
- import.meta.url,
19328
- );
19329
19925
  const directoryContentMagicName = "...";
19330
19926
 
19331
19927
  const jsenvPluginProtocolFile = ({
19332
19928
  magicExtensions,
19333
19929
  magicDirectoryIndex,
19334
19930
  preserveSymlinks,
19335
- directoryListingUrlMocks,
19931
+ directoryListing,
19336
19932
  }) => {
19337
19933
  return [
19338
19934
  jsenvPluginFsRedirection({
@@ -19363,8 +19959,7 @@ const jsenvPluginProtocolFile = ({
19363
19959
  appliesDuring: "dev",
19364
19960
  resolveReference: (reference) => {
19365
19961
  if (reference.specifier.startsWith("/@fs/")) {
19366
- const fsRootRelativeUrl = reference.specifier.slice("/@fs/".length);
19367
- return `file:///${fsRootRelativeUrl}`;
19962
+ return FILE_AND_SERVER_URLS_CONVERTER.asFileUrl(reference.specifier);
19368
19963
  }
19369
19964
  return null;
19370
19965
  },
@@ -19383,12 +19978,43 @@ const jsenvPluginProtocolFile = ({
19383
19978
  }
19384
19979
  }
19385
19980
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19386
- if (urlIsInsideOf(generatedUrl, rootDirectoryUrl)) {
19387
- const result = `/${urlToRelativeUrl(generatedUrl, rootDirectoryUrl)}`;
19388
- return result;
19981
+ return FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
19982
+ generatedUrl,
19983
+ rootDirectoryUrl,
19984
+ );
19985
+ },
19986
+ },
19987
+ ...(directoryListing
19988
+ ? [
19989
+ jsenvPluginDirectoryListing({
19990
+ ...directoryListing,
19991
+ directoryContentMagicName,
19992
+ }),
19993
+ ]
19994
+ : []),
19995
+ {
19996
+ name: "jsenv:directory_as_json",
19997
+ appliesDuring: "*",
19998
+ fetchUrlContent: (urlInfo) => {
19999
+ const { firstReference } = urlInfo;
20000
+ let { fsStat } = firstReference;
20001
+ if (!fsStat) {
20002
+ fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
19389
20003
  }
19390
- const result = `/@fs/${generatedUrl.slice("file:///".length)}`;
19391
- return result;
20004
+ if (!fsStat) {
20005
+ return null;
20006
+ }
20007
+ const isDirectory = fsStat.isDirectory();
20008
+ if (!isDirectory) {
20009
+ return null;
20010
+ }
20011
+ const directoryContentArray = readdirSync(new URL(urlInfo.url));
20012
+ const content = JSON.stringify(directoryContentArray, null, " ");
20013
+ return {
20014
+ type: "directory",
20015
+ contentType: "application/json",
20016
+ content,
20017
+ };
19392
20018
  },
19393
20019
  },
19394
20020
  {
@@ -19399,366 +20025,27 @@ const jsenvPluginProtocolFile = ({
19399
20025
  return null;
19400
20026
  }
19401
20027
  const { firstReference } = urlInfo;
19402
- const { mainFilePath } = urlInfo.context;
19403
20028
  let { fsStat } = firstReference;
19404
20029
  if (!fsStat) {
19405
20030
  fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
19406
20031
  }
19407
- const isDirectory = fsStat?.isDirectory();
19408
- const { rootDirectoryUrl, request } = urlInfo.context;
19409
20032
  const serveFile = (url) => {
19410
20033
  const contentType = CONTENT_TYPE.fromUrlExtension(url);
19411
20034
  const fileBuffer = readFileSync(new URL(url));
19412
20035
  const content = CONTENT_TYPE.isTextual(contentType)
19413
20036
  ? String(fileBuffer)
19414
20037
  : fileBuffer;
19415
- return {
19416
- content,
19417
- contentType,
19418
- contentLength: fileBuffer.length,
19419
- };
19420
- };
19421
-
19422
- if (!fsStat) {
19423
- if (request && request.headers["sec-fetch-dest"] === "document") {
19424
- const directoryContentItems = generateDirectoryContentItems(
19425
- urlInfo.url,
19426
- rootDirectoryUrl,
19427
- );
19428
- const html = generateHtmlForENOENT(
19429
- urlInfo.url,
19430
- directoryContentItems,
19431
- directoryListingUrlMocks,
19432
- { mainFilePath },
19433
- );
19434
- return {
19435
- status: 404,
19436
- contentType: "text/html",
19437
- content: html,
19438
- headers: {
19439
- "cache-control": "no-cache",
19440
- },
19441
- };
19442
- }
19443
- }
19444
- if (isDirectory) {
19445
- const directoryContentArray = readdirSync(new URL(urlInfo.url));
19446
- if (firstReference.type === "filesystem") {
19447
- const content = JSON.stringify(directoryContentArray, null, " ");
19448
- return {
19449
- type: "directory",
19450
- contentType: "application/json",
19451
- content,
19452
- };
19453
- }
19454
- const acceptsHtml = request
19455
- ? pickContentType(request, ["text/html"])
19456
- : false;
19457
- if (acceptsHtml) {
19458
- firstReference.expectedType = "html";
19459
- const directoryUrl = urlInfo.url;
19460
- const directoryContentItems = generateDirectoryContentItems(
19461
- directoryUrl,
19462
- rootDirectoryUrl,
19463
- );
19464
- const html = generateHtmlForDirectory(directoryContentItems, {
19465
- mainFilePath,
19466
- });
19467
- return {
19468
- type: "html",
19469
- contentType: "text/html",
19470
- content: html,
19471
- };
19472
- }
19473
- return {
19474
- type: "directory",
19475
- contentType: "application/json",
19476
- content: JSON.stringify(directoryContentArray, null, " "),
19477
- };
19478
- }
19479
- return serveFile(urlInfo.url);
19480
- },
19481
- },
19482
- ];
19483
- };
19484
-
19485
- const generateHtmlForDirectory = (directoryContentItems, { mainFilePath }) => {
19486
- let directoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19487
- const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19488
- directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
19489
-
19490
- const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
19491
- const replacers = {
19492
- directoryUrl,
19493
- directoryNav: () =>
19494
- generateDirectoryNav(directoryUrl, {
19495
- rootDirectoryUrl,
19496
- rootDirectoryUrlForServer:
19497
- directoryContentItems.rootDirectoryUrlForServer,
19498
- mainFilePath,
19499
- }),
19500
- directoryContent: () =>
19501
- generateDirectoryContent(directoryContentItems, { mainFilePath }),
19502
- };
19503
- const html = replacePlaceholders$1(htmlForDirectory, replacers);
19504
- return html;
19505
- };
19506
- const generateHtmlForENOENT = (
19507
- url,
19508
- directoryContentItems,
19509
- directoryListingUrlMocks,
19510
- { mainFilePath },
19511
- ) => {
19512
- const ancestorDirectoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19513
- const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19514
-
19515
- const htmlFor404AndAncestorDir = String(
19516
- readFileSync(html404AndAncestorDirFileUrl),
19517
- );
19518
- const fileRelativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
19519
- const ancestorDirectoryRelativeUrl = urlToRelativeUrl(
19520
- ancestorDirectoryUrl,
19521
- rootDirectoryUrl,
19522
- );
19523
- const replacers = {
19524
- fileUrl: directoryListingUrlMocks
19525
- ? `@jsenv/core/${urlToRelativeUrl(url, jsenvCoreDirectoryUrl)}`
19526
- : url,
19527
- fileRelativeUrl,
19528
- ancestorDirectoryUrl,
19529
- ancestorDirectoryRelativeUrl,
19530
- ancestorDirectoryNav: () =>
19531
- generateDirectoryNav(ancestorDirectoryUrl, {
19532
- rootDirectoryUrl,
19533
- rootDirectoryUrlForServer:
19534
- directoryContentItems.rootDirectoryUrlForServer,
19535
- mainFilePath,
19536
- }),
19537
- ancestorDirectoryContent: () =>
19538
- generateDirectoryContent(directoryContentItems, { mainFilePath }),
19539
- };
19540
- const html = replacePlaceholders$1(htmlFor404AndAncestorDir, replacers);
19541
- return html;
19542
- };
19543
- const generateDirectoryNav = (
19544
- entryDirectoryUrl,
19545
- { rootDirectoryUrl, rootDirectoryUrlForServer, mainFilePath },
19546
- ) => {
19547
- const entryDirectoryRelativeUrl = urlToRelativeUrl(
19548
- entryDirectoryUrl,
19549
- rootDirectoryUrl,
19550
- );
19551
- const isDir =
19552
- entryDirectoryRelativeUrl === "" || entryDirectoryRelativeUrl.endsWith("/");
19553
- const rootDirectoryUrlName = urlToFilename$1(rootDirectoryUrl);
19554
- const items = [];
19555
- let dirPartsHtml = "";
19556
- const parts = entryDirectoryRelativeUrl
19557
- ? `${rootDirectoryUrlName}/${entryDirectoryRelativeUrl.slice(0, -1)}`.split(
19558
- "/",
19559
- )
19560
- : [rootDirectoryUrlName];
19561
- let i = 0;
19562
- while (i < parts.length) {
19563
- const part = parts[i];
19564
- const directoryRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
19565
- const directoryUrl =
19566
- directoryRelativeUrl === ""
19567
- ? rootDirectoryUrl
19568
- : new URL(`${directoryRelativeUrl}/`, rootDirectoryUrl).href;
19569
- let href =
19570
- directoryUrl === rootDirectoryUrlForServer ||
19571
- urlIsInsideOf(directoryUrl, rootDirectoryUrlForServer)
19572
- ? urlToRelativeUrl(directoryUrl, rootDirectoryUrlForServer)
19573
- : directoryUrl;
19574
- if (href === "") {
19575
- href = `/${directoryContentMagicName}`;
19576
- } else {
19577
- href = `/${href}`;
19578
- }
19579
- const text = part;
19580
- items.push({
19581
- href,
19582
- text,
19583
- });
19584
- i++;
19585
- }
19586
- i = 0;
19587
-
19588
- const renderDirNavItem = ({ isCurrent, href, text }) => {
19589
- const isServerRootDir = href === `/${directoryContentMagicName}`;
19590
- if (isServerRootDir) {
19591
- if (isCurrent) {
19592
- return `
19593
- <span class="directory_nav_item" data-current>
19594
- <a class="directory_root_for_server" hot-decline href="/${mainFilePath}"></a>
19595
- <span class="directory_name">${text}</span>
19596
- </span>`;
19597
- }
19598
- return `
19599
- <span class="directory_nav_item">
19600
- <a class="directory_root_for_server" hot-decline href="/${mainFilePath}"></a>
19601
- <a class="directory_name" hot-decline href="${href}">${text}</a>
19602
- </span>`;
19603
- }
19604
- if (isCurrent) {
19605
- return `
19606
- <span class="directory_nav_item" data-current>
19607
- <span class="directory_text">${text}</span>
19608
- </span>`;
19609
- }
19610
- return `
19611
- <span class="directory_nav_item">
19612
- <a class="directory_text" hot-decline href="${href}">${text}</a>
19613
- </span>`;
19614
- };
19615
-
19616
- for (const { href, text } of items) {
19617
- const isLastPart = i === items.length - 1;
19618
- dirPartsHtml += renderDirNavItem({
19619
- isCurrent: isLastPart,
19620
- href,
19621
- text,
19622
- });
19623
- if (isLastPart) {
19624
- break;
19625
- }
19626
- dirPartsHtml += `
19627
- <span class="directory_separator">/</span>`;
19628
- i++;
19629
- }
19630
- if (isDir) {
19631
- dirPartsHtml += `
19632
- <span class="directory_separator">/</span>`;
19633
- }
19634
- return dirPartsHtml;
19635
- };
19636
- const generateDirectoryContentItems = (
19637
- directoryUrl,
19638
- rootDirectoryUrlForServer,
19639
- ) => {
19640
- let firstExistingDirectoryUrl = new URL("./", directoryUrl);
19641
- while (!existsSync(firstExistingDirectoryUrl)) {
19642
- firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
19643
- if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrlForServer)) {
19644
- firstExistingDirectoryUrl = new URL(rootDirectoryUrlForServer);
19645
- break;
19646
- }
19647
- }
19648
- const directoryContentArray = readdirSync(firstExistingDirectoryUrl);
19649
- const fileUrls = [];
19650
- for (const filename of directoryContentArray) {
19651
- const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
19652
- fileUrls.push(fileUrlObject);
19653
- }
19654
- let rootDirectoryUrl = rootDirectoryUrlForServer;
19655
- package_workspaces: {
19656
- const packageDirectoryUrl = lookupPackageDirectory(
19657
- rootDirectoryUrlForServer,
19658
- );
19659
- if (!packageDirectoryUrl) {
19660
- break package_workspaces;
19661
- }
19662
- if (String(packageDirectoryUrl) === String(rootDirectoryUrlForServer)) {
19663
- break package_workspaces;
19664
- }
19665
- rootDirectoryUrl = packageDirectoryUrl;
19666
- if (
19667
- String(firstExistingDirectoryUrl) === String(rootDirectoryUrlForServer)
19668
- ) {
19669
- let packageContent;
19670
- try {
19671
- packageContent = JSON.parse(
19672
- readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
19673
- );
19674
- } catch {
19675
- break package_workspaces;
19676
- }
19677
- const { workspaces } = packageContent;
19678
- if (Array.isArray(workspaces)) {
19679
- for (const workspace of workspaces) {
19680
- const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
19681
- const workspaceUrl = workspaceUrlObject.href;
19682
- if (workspaceUrl.endsWith("*")) {
19683
- const directoryUrl = ensurePathnameTrailingSlash(
19684
- workspaceUrl.slice(0, -1),
19685
- );
19686
- fileUrls.push(new URL(directoryUrl));
19687
- } else {
19688
- fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
19689
- }
19690
- }
19691
- }
19692
- }
19693
- }
19694
-
19695
- const sortedUrls = [];
19696
- for (let fileUrl of fileUrls) {
19697
- if (lstatSync(fileUrl).isDirectory()) {
19698
- sortedUrls.push(ensurePathnameTrailingSlash(fileUrl));
19699
- } else {
19700
- sortedUrls.push(fileUrl);
19701
- }
19702
- }
19703
- sortedUrls.sort((a, b) => {
19704
- return comparePathnames(a.pathname, b.pathname);
19705
- });
19706
-
19707
- const items = [];
19708
- for (const sortedUrl of sortedUrls) {
19709
- const fileUrlRelativeToParent = urlToRelativeUrl(
19710
- sortedUrl,
19711
- firstExistingDirectoryUrl,
19712
- );
19713
- const fileUrlRelativeToServer = urlToRelativeUrl(
19714
- sortedUrl,
19715
- rootDirectoryUrlForServer,
19716
- );
19717
- const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
19718
- items.push({
19719
- type,
19720
- fileUrlRelativeToParent,
19721
- fileUrlRelativeToServer,
19722
- });
19723
- }
19724
- items.rootDirectoryUrlForServer = rootDirectoryUrlForServer;
19725
- items.rootDirectoryUrl = rootDirectoryUrl;
19726
- items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
19727
- return items;
19728
- };
19729
- const generateDirectoryContent = (directoryContentItems, { mainFilePath }) => {
19730
- if (directoryContentItems.length === 0) {
19731
- return `<p class="directory_empty_message">Directory is empty</p>`;
19732
- }
19733
- let html = `<ul class="directory_content">`;
19734
- for (const directoryContentItem of directoryContentItems) {
19735
- const { type, fileUrlRelativeToParent, fileUrlRelativeToServer } =
19736
- directoryContentItem;
19737
- let href = fileUrlRelativeToServer;
19738
- if (href === "") {
19739
- href = `${directoryContentMagicName}`;
19740
- }
19741
- const isMainFile = href === mainFilePath;
19742
- const mainFileAttr = isMainFile ? ` data-main-file` : "";
19743
- html += `
19744
- <li class="directory_child" data-type="${type}"${mainFileAttr}>
19745
- <a href="/${href}" hot-decline>${fileUrlRelativeToParent}</a>
19746
- </li>`;
19747
- }
19748
- html += `\n </ul>`;
19749
- return html;
19750
- };
19751
- const replacePlaceholders$1 = (html, replacers) => {
19752
- return html.replace(/\$\{(\w+)\}/g, (match, name) => {
19753
- const replacer = replacers[name];
19754
- if (replacer === undefined) {
19755
- return match;
19756
- }
19757
- if (typeof replacer === "function") {
19758
- return replacer();
19759
- }
19760
- return replacer;
19761
- });
20038
+ return {
20039
+ content,
20040
+ contentType,
20041
+ contentLength: fileBuffer.length,
20042
+ };
20043
+ };
20044
+
20045
+ return serveFile(urlInfo.url);
20046
+ },
20047
+ },
20048
+ ];
19762
20049
  };
19763
20050
 
19764
20051
  const jsenvPluginProtocolHttp = ({ include }) => {
@@ -19815,10 +20102,11 @@ const jsenvPluginProtocolHttp = ({ include }) => {
19815
20102
  return fileUrl;
19816
20103
  },
19817
20104
  fetchUrlContent: async (urlInfo) => {
19818
- if (!urlInfo.originalUrl.startsWith("http")) {
20105
+ const originalUrl = urlInfo.originalUrl;
20106
+ if (!originalUrl.startsWith("http")) {
19819
20107
  return null;
19820
20108
  }
19821
- const response = await fetch(urlInfo.originalUrl);
20109
+ const response = await fetch(originalUrl);
19822
20110
  const responseStatus = response.status;
19823
20111
  if (responseStatus < 200 || responseStatus > 299) {
19824
20112
  throw new Error(`unexpected response status ${responseStatus}`);
@@ -19851,103 +20139,6 @@ const asValidFilename = (string) => {
19851
20139
  return string;
19852
20140
  };
19853
20141
 
19854
- const jsenvPluginInjections = (rawAssociations) => {
19855
- let resolvedAssociations;
19856
-
19857
- return {
19858
- name: "jsenv:injections",
19859
- appliesDuring: "*",
19860
- init: (context) => {
19861
- resolvedAssociations = URL_META.resolveAssociations(
19862
- { injectionsGetter: rawAssociations },
19863
- context.rootDirectoryUrl,
19864
- );
19865
- },
19866
- transformUrlContent: async (urlInfo) => {
19867
- const { injectionsGetter } = URL_META.applyAssociations({
19868
- url: asUrlWithoutSearch(urlInfo.url),
19869
- associations: resolvedAssociations,
19870
- });
19871
- if (!injectionsGetter) {
19872
- return null;
19873
- }
19874
- if (typeof injectionsGetter !== "function") {
19875
- throw new TypeError("injectionsGetter must be a function");
19876
- }
19877
- const injections = await injectionsGetter(urlInfo);
19878
- if (!injections) {
19879
- return null;
19880
- }
19881
- const keys = Object.keys(injections);
19882
- if (keys.length === 0) {
19883
- return null;
19884
- }
19885
- return replacePlaceholders(urlInfo.content, injections, urlInfo);
19886
- },
19887
- };
19888
- };
19889
-
19890
- const injectionSymbol = Symbol.for("jsenv_injection");
19891
- const INJECTIONS = {
19892
- optional: (value) => {
19893
- return { [injectionSymbol]: "optional", value };
19894
- },
19895
- };
19896
-
19897
- // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
19898
- const replacePlaceholders = (content, replacements, urlInfo) => {
19899
- const magicSource = createMagicSource(content);
19900
- for (const key of Object.keys(replacements)) {
19901
- let index = content.indexOf(key);
19902
- const replacement = replacements[key];
19903
- let isOptional;
19904
- let value;
19905
- if (replacement && replacement[injectionSymbol]) {
19906
- const valueBehindSymbol = replacement[injectionSymbol];
19907
- isOptional = valueBehindSymbol === "optional";
19908
- value = replacement.value;
19909
- } else {
19910
- value = replacement;
19911
- }
19912
- if (index === -1) {
19913
- if (!isOptional) {
19914
- urlInfo.context.logger.warn(
19915
- `placeholder "${key}" not found in ${urlInfo.url}.
19916
- --- suggestion a ---
19917
- Add "${key}" in that file.
19918
- --- suggestion b ---
19919
- Fix eventual typo in "${key}"?
19920
- --- suggestion c ---
19921
- Mark injection as optional using INJECTIONS.optional():
19922
- import { INJECTIONS } from "@jsenv/core";
19923
-
19924
- return {
19925
- "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
19926
- };`,
19927
- );
19928
- }
19929
- continue;
19930
- }
19931
-
19932
- while (index !== -1) {
19933
- const start = index;
19934
- const end = index + key.length;
19935
- magicSource.replace({
19936
- start,
19937
- end,
19938
- replacement:
19939
- urlInfo.type === "js_classic" ||
19940
- urlInfo.type === "js_module" ||
19941
- urlInfo.type === "html"
19942
- ? JSON.stringify(value, null, " ")
19943
- : value,
19944
- });
19945
- index = content.indexOf(key, end);
19946
- }
19947
- }
19948
- return magicSource.toContentAndSourcemap();
19949
- };
19950
-
19951
20142
  /*
19952
20143
  * Some code uses globals specific to Node.js in code meant to run in browsers...
19953
20144
  * This plugin will replace some node globals to things compatible with web:
@@ -21236,7 +21427,7 @@ const getCorePlugins = ({
21236
21427
  nodeEsmResolution = {},
21237
21428
  magicExtensions,
21238
21429
  magicDirectoryIndex,
21239
- directoryListingUrlMocks,
21430
+ directoryListing = true,
21240
21431
  directoryReferenceEffect,
21241
21432
  supervisor,
21242
21433
  injections,
@@ -21264,13 +21455,16 @@ const getCorePlugins = ({
21264
21455
  if (http === false) {
21265
21456
  http = { include: false };
21266
21457
  }
21458
+ if (directoryListing === true) {
21459
+ directoryListing = {};
21460
+ }
21267
21461
 
21268
21462
  return [
21269
21463
  jsenvPluginReferenceAnalysis(referenceAnalysis),
21270
21464
  ...(injections ? [jsenvPluginInjections(injections)] : []),
21271
21465
  jsenvPluginTranspilation(transpilation),
21466
+ // "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
21272
21467
  ...(inlining ? [jsenvPluginInlining()] : []),
21273
- ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []), // after inline as it needs inline script to be cooked
21274
21468
 
21275
21469
  /* When resolving references the following applies by default:
21276
21470
  - http urls are resolved by jsenvPluginHttpUrls
@@ -21282,9 +21476,8 @@ const getCorePlugins = ({
21282
21476
  jsenvPluginProtocolFile({
21283
21477
  magicExtensions,
21284
21478
  magicDirectoryIndex,
21285
- directoryListingUrlMocks,
21479
+ directoryListing,
21286
21480
  }),
21287
-
21288
21481
  {
21289
21482
  name: "jsenv:resolve_root_as_main",
21290
21483
  appliesDuring: "*",
@@ -21303,12 +21496,14 @@ const getCorePlugins = ({
21303
21496
  : []),
21304
21497
  jsenvPluginWebResolution(),
21305
21498
  jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect),
21306
-
21307
21499
  jsenvPluginVersionSearchParam(),
21500
+
21501
+ // "jsenvPluginSupervisor" MUST be after "jsenvPluginInlining" as it needs inline script to be cooked
21502
+ ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []),
21503
+
21308
21504
  jsenvPluginCommonJsGlobals(),
21309
21505
  jsenvPluginImportMetaScenarios(),
21310
21506
  ...(scenarioPlaceholders ? [jsenvPluginGlobalScenarios()] : []),
21311
-
21312
21507
  jsenvPluginNodeRuntime({ runtimeCompat }),
21313
21508
 
21314
21509
  jsenvPluginImportMetaHot(),
@@ -21442,7 +21637,6 @@ ${ANSI.color(buildUrl, ANSI.MAGENTA)}
21442
21637
  integer++;
21443
21638
  nameCandidate = `${basename}${integer}${extension}`;
21444
21639
  }
21445
- hash = "";
21446
21640
  const buildUrl = `${buildDirectoryUrl}${directoryPath}${nameCandidate}${search}${hash}`;
21447
21641
  associateBuildUrl(url, buildUrl);
21448
21642
  return buildUrl;
@@ -21868,7 +22062,6 @@ const createBuildSpecifierManager = ({
21868
22062
  type: reference.type,
21869
22063
  expectedType: reference.expectedType,
21870
22064
  specifier: reference.specifier,
21871
- specifierPathname: reference.specifierPathname,
21872
22065
  specifierLine: reference.specifierLine,
21873
22066
  specifierColumn: reference.specifierColumn,
21874
22067
  specifierStart: reference.specifierStart,
@@ -23317,33 +23510,33 @@ build ${entryPointKeys.length} entry points`);
23317
23510
 
23318
23511
  const bundlers = {};
23319
23512
  {
23320
- rawKitchen.pluginController.plugins.forEach((plugin) => {
23513
+ for (const plugin of rawKitchen.pluginController.activePlugins) {
23321
23514
  const bundle = plugin.bundle;
23322
23515
  if (!bundle) {
23323
- return;
23516
+ continue;
23324
23517
  }
23325
23518
  if (typeof bundle !== "object") {
23326
23519
  throw new Error(
23327
23520
  `bundle must be an object, found "${bundle}" on plugin named "${plugin.name}"`,
23328
23521
  );
23329
23522
  }
23330
- Object.keys(bundle).forEach((type) => {
23523
+ for (const type of Object.keys(bundle)) {
23331
23524
  const bundleFunction = bundle[type];
23332
23525
  if (!bundleFunction) {
23333
- return;
23526
+ continue;
23334
23527
  }
23335
23528
  const bundlerForThatType = bundlers[type];
23336
23529
  if (bundlerForThatType) {
23337
23530
  // first plugin to define a bundle hook wins
23338
- return;
23531
+ continue;
23339
23532
  }
23340
23533
  bundlers[type] = {
23341
23534
  plugin,
23342
23535
  bundleFunction: bundle[type],
23343
23536
  urlInfoMap: new Map(),
23344
23537
  };
23345
- });
23346
- });
23538
+ }
23539
+ }
23347
23540
  const addToBundlerIfAny = (rawUrlInfo) => {
23348
23541
  const bundler = bundlers[rawUrlInfo.type];
23349
23542
  if (bundler) {
@@ -23663,43 +23856,6 @@ const WEB_URL_CONVERTER = {
23663
23856
  },
23664
23857
  };
23665
23858
 
23666
- /*
23667
- * This plugin is very special because it is here
23668
- * to provide "serverEvents" used by other plugins
23669
- */
23670
-
23671
-
23672
- const serverEventsClientFileUrl = new URL(
23673
- "./js/server_events_client.js",
23674
- import.meta.url,
23675
- ).href;
23676
-
23677
- const jsenvPluginServerEventsClientInjection = ({ logs = true }) => {
23678
- return {
23679
- name: "jsenv:server_events_client_injection",
23680
- appliesDuring: "*",
23681
- transformUrlContent: {
23682
- html: (urlInfo) => {
23683
- const htmlAst = parseHtml({
23684
- html: urlInfo.content,
23685
- url: urlInfo.url,
23686
- });
23687
- injectJsenvScript(htmlAst, {
23688
- src: serverEventsClientFileUrl,
23689
- initCall: {
23690
- callee: "window.__server_events__.setup",
23691
- params: {
23692
- logs,
23693
- },
23694
- },
23695
- pluginName: "jsenv:server_events_client_injection",
23696
- });
23697
- return stringifyHtmlAst(htmlAst);
23698
- },
23699
- },
23700
- };
23701
- };
23702
-
23703
23859
  const createServerEventsDispatcher = () => {
23704
23860
  const clients = [];
23705
23861
  const MAX_CLIENTS = 100;
@@ -23795,6 +23951,105 @@ const createServerEventsDispatcher = () => {
23795
23951
  };
23796
23952
  };
23797
23953
 
23954
+ /*
23955
+ * This plugin is very special because it is here
23956
+ * to provide "serverEvents" used by other plugins
23957
+ */
23958
+
23959
+
23960
+ const serverEventsClientFileUrl = new URL(
23961
+ "./js/server_events_client.js",
23962
+ import.meta.url,
23963
+ ).href;
23964
+
23965
+ const jsenvPluginServerEvents = ({ clientAutoreload }) => {
23966
+ let serverEventsDispatcher;
23967
+
23968
+ const { clientServerEventsConfig } = clientAutoreload;
23969
+ const { logs = true } = clientServerEventsConfig;
23970
+
23971
+ return {
23972
+ name: "jsenv:server_events",
23973
+ appliesDuring: "dev",
23974
+ effect: ({ kitchenContext, otherPlugins }) => {
23975
+ const allServerEvents = {};
23976
+ for (const otherPlugin of otherPlugins) {
23977
+ const { serverEvents } = otherPlugin;
23978
+ if (!serverEvents) {
23979
+ continue;
23980
+ }
23981
+ for (const serverEventName of Object.keys(serverEvents)) {
23982
+ // we could throw on serverEvent name conflict
23983
+ // we could throw if serverEvents[serverEventName] is not a function
23984
+ allServerEvents[serverEventName] = serverEvents[serverEventName];
23985
+ }
23986
+ }
23987
+ const serverEventNames = Object.keys(allServerEvents);
23988
+ if (serverEventNames.length === 0) {
23989
+ return false;
23990
+ }
23991
+ serverEventsDispatcher = createServerEventsDispatcher();
23992
+ const onabort = () => {
23993
+ serverEventsDispatcher.destroy();
23994
+ };
23995
+ kitchenContext.signal.addEventListener("abort", onabort);
23996
+ for (const serverEventName of Object.keys(allServerEvents)) {
23997
+ const serverEventInfo = {
23998
+ ...kitchenContext,
23999
+ // serverEventsDispatcher variable is safe, we can disable esling warning
24000
+ // eslint-disable-next-line no-loop-func
24001
+ sendServerEvent: (data) => {
24002
+ if (!serverEventsDispatcher) {
24003
+ // this can happen if a plugin wants to send a server event but
24004
+ // server is closing or the plugin got destroyed but still wants to do things
24005
+ // if plugin code is correctly written it is never supposed to happen
24006
+ // because it means a plugin is still trying to do stuff after being destroyed
24007
+ return;
24008
+ }
24009
+ serverEventsDispatcher.dispatch({
24010
+ type: serverEventName,
24011
+ data,
24012
+ });
24013
+ },
24014
+ };
24015
+ const serverEventInit = allServerEvents[serverEventName];
24016
+ serverEventInit(serverEventInfo);
24017
+ }
24018
+ return () => {
24019
+ kitchenContext.signal.removeEventListener("abort", onabort);
24020
+ serverEventsDispatcher.destroy();
24021
+ serverEventsDispatcher = undefined;
24022
+ };
24023
+ },
24024
+ serveWebsocket: async ({ websocket, request }) => {
24025
+ if (request.headers["sec-websocket-protocol"] !== "jsenv") {
24026
+ return false;
24027
+ }
24028
+ serverEventsDispatcher.addWebsocket(websocket, request);
24029
+ return true;
24030
+ },
24031
+ transformUrlContent: {
24032
+ html: (urlInfo) => {
24033
+ const htmlAst = parseHtml({
24034
+ html: urlInfo.content,
24035
+ url: urlInfo.url,
24036
+ });
24037
+ injectJsenvScript(htmlAst, {
24038
+ src: serverEventsClientFileUrl,
24039
+ initCall: {
24040
+ callee: "window.__server_events__.setup",
24041
+ params: {
24042
+ logs,
24043
+ },
24044
+ },
24045
+ pluginName: "jsenv:server_events",
24046
+ });
24047
+ return stringifyHtmlAst(htmlAst);
24048
+ },
24049
+ },
24050
+ };
24051
+ };
24052
+
23798
24053
  const memoizeByFirstArgument = (compute) => {
23799
24054
  const urlCache = new Map();
23800
24055
 
@@ -23879,7 +24134,7 @@ const startDevServer = async ({
23879
24134
  supervisor = true,
23880
24135
  magicExtensions,
23881
24136
  magicDirectoryIndex,
23882
- directoryListingUrlMocks,
24137
+ directoryListing,
23883
24138
  injections,
23884
24139
  transpilation,
23885
24140
  cacheControl = true,
@@ -23952,10 +24207,11 @@ const startDevServer = async ({
23952
24207
  });
23953
24208
 
23954
24209
  const serverStopCallbackSet = new Set();
23955
- const serverEventsDispatcher = createServerEventsDispatcher();
24210
+ const serverStopAbortController = new AbortController();
23956
24211
  serverStopCallbackSet.add(() => {
23957
- serverEventsDispatcher.destroy();
24212
+ serverStopAbortController.abort();
23958
24213
  });
24214
+ const serverStopAbortSignal = serverStopAbortController.signal;
23959
24215
  const kitchenCache = new Map();
23960
24216
 
23961
24217
  const finalServices = [];
@@ -24058,7 +24314,7 @@ const startDevServer = async ({
24058
24314
 
24059
24315
  kitchen = createKitchen({
24060
24316
  name: runtimeId,
24061
- signal,
24317
+ signal: serverStopAbortSignal,
24062
24318
  logLevel,
24063
24319
  rootDirectoryUrl: sourceDirectoryUrl,
24064
24320
  mainFilePath: sourceMainFilePath,
@@ -24067,6 +24323,7 @@ const startDevServer = async ({
24067
24323
  runtimeCompat,
24068
24324
  clientRuntimeCompat,
24069
24325
  plugins: [
24326
+ jsenvPluginServerEvents({ clientAutoreload }),
24070
24327
  ...plugins,
24071
24328
  ...getCorePlugins({
24072
24329
  rootDirectoryUrl: sourceDirectoryUrl,
@@ -24076,7 +24333,7 @@ const startDevServer = async ({
24076
24333
  nodeEsmResolution,
24077
24334
  magicExtensions,
24078
24335
  magicDirectoryIndex,
24079
- directoryListingUrlMocks,
24336
+ directoryListing,
24080
24337
  supervisor,
24081
24338
  injections,
24082
24339
  transpilation,
@@ -24142,7 +24399,22 @@ const startDevServer = async ({
24142
24399
  for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
24143
24400
  const implicitUrlInfo =
24144
24401
  urlInfoCreated.graph.getUrlInfo(implicitUrl);
24145
- if (implicitUrlInfo && !implicitUrlInfo.isValid()) {
24402
+ if (!implicitUrlInfo) {
24403
+ continue;
24404
+ }
24405
+ if (implicitUrlInfo.content === undefined) {
24406
+ // happens when we explicitely load an url with a search param
24407
+ // - it creates an implicit url info to the url without params
24408
+ // - we never explicitely request the url without search param so it has no content
24409
+ // in that case the underlying urlInfo cannot be invalidate by the implicit
24410
+ // we use modifiedTimestamp to detect if the url was loaded once
24411
+ // or is just here to be used later
24412
+ if (implicitUrlInfo.modifiedTimestamp) {
24413
+ return false;
24414
+ }
24415
+ continue;
24416
+ }
24417
+ if (!implicitUrlInfo.isValid()) {
24146
24418
  return false;
24147
24419
  }
24148
24420
  }
@@ -24161,41 +24433,6 @@ const startDevServer = async ({
24161
24433
  serverStopCallbackSet.add(() => {
24162
24434
  kitchen.pluginController.callHooks("destroy", kitchen.context);
24163
24435
  });
24164
- {
24165
- const allServerEvents = {};
24166
- kitchen.pluginController.plugins.forEach((plugin) => {
24167
- const { serverEvents } = plugin;
24168
- if (serverEvents) {
24169
- Object.keys(serverEvents).forEach((serverEventName) => {
24170
- // we could throw on serverEvent name conflict
24171
- // we could throw if serverEvents[serverEventName] is not a function
24172
- allServerEvents[serverEventName] = serverEvents[serverEventName];
24173
- });
24174
- }
24175
- });
24176
- const serverEventNames = Object.keys(allServerEvents);
24177
- if (serverEventNames.length > 0) {
24178
- Object.keys(allServerEvents).forEach((serverEventName) => {
24179
- const serverEventInfo = {
24180
- ...kitchen.context,
24181
- sendServerEvent: (data) => {
24182
- serverEventsDispatcher.dispatch({
24183
- type: serverEventName,
24184
- data,
24185
- });
24186
- },
24187
- };
24188
- const serverEventInit = allServerEvents[serverEventName];
24189
- serverEventInit(serverEventInfo);
24190
- });
24191
- kitchen.pluginController.unshiftPlugin(
24192
- jsenvPluginServerEventsClientInjection(
24193
- clientAutoreload.clientServerEventsConfig,
24194
- ),
24195
- );
24196
- }
24197
- }
24198
-
24199
24436
  kitchenCache.set(runtimeId, kitchen);
24200
24437
  onKitchenCreated(kitchen);
24201
24438
  return kitchen;
@@ -24217,6 +24454,20 @@ const startDevServer = async ({
24217
24454
  if (responseFromPlugin) {
24218
24455
  return responseFromPlugin;
24219
24456
  }
24457
+ const { rootDirectoryUrl, mainFilePath } = kitchen.context;
24458
+ let requestResource = request.resource;
24459
+ let requestedUrl;
24460
+ if (requestResource.startsWith("/@fs/")) {
24461
+ const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
24462
+ requestedUrl = `file:///${fsRootRelativeUrl}`;
24463
+ } else {
24464
+ const requestedUrlObject = new URL(
24465
+ requestResource === "/" ? mainFilePath : requestResource.slice(1),
24466
+ rootDirectoryUrl,
24467
+ );
24468
+ requestedUrlObject.searchParams.delete("hot");
24469
+ requestedUrl = requestedUrlObject.href;
24470
+ }
24220
24471
  const { referer } = request.headers;
24221
24472
  const parentUrl = referer
24222
24473
  ? WEB_URL_CONVERTER.asFileUrl(referer, {
@@ -24228,15 +24479,20 @@ const startDevServer = async ({
24228
24479
  request.resource,
24229
24480
  parentUrl,
24230
24481
  );
24231
- if (!reference) {
24482
+ if (reference) {
24483
+ reference.urlInfo.context.request = request;
24484
+ reference.urlInfo.context.requestedUrl = requestedUrl;
24485
+ } else {
24232
24486
  const rootUrlInfo = kitchen.graph.rootUrlInfo;
24233
24487
  rootUrlInfo.context.request = request;
24488
+ rootUrlInfo.context.requestedUrl = requestedUrl;
24234
24489
  reference = rootUrlInfo.dependencies.createResolveAndFinalize({
24235
24490
  trace: { message: parentUrl },
24236
24491
  type: "http_request",
24237
24492
  specifier: request.resource,
24238
24493
  });
24239
24494
  rootUrlInfo.context.request = null;
24495
+ rootUrlInfo.context.requestedUrl = null;
24240
24496
  }
24241
24497
  const urlInfo = reference.urlInfo;
24242
24498
  const ifNoneMatch = request.headers["if-none-match"];
@@ -24280,9 +24536,10 @@ const startDevServer = async ({
24280
24536
  // If they match jsenv bypass cooking and returns 304
24281
24537
  // This must not happen when a plugin uses "no-store" or "no-cache" as it means
24282
24538
  // plugin logic wants to happens for every request to this url
24283
- ...(urlInfo.headers["cache-control"] === "no-store" ||
24284
- urlInfo.headers["cache-control"] === "no-cache"
24285
- ? {}
24539
+ ...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
24540
+ ? {
24541
+ "cache-control": "no-store", // for inline file we force no-store when parent is no-store
24542
+ }
24286
24543
  : {
24287
24544
  "cache-control": `private,max-age=0,must-revalidate`,
24288
24545
  // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
@@ -24383,13 +24640,20 @@ ${error.trace?.message}`);
24383
24640
  };
24384
24641
  }
24385
24642
  },
24386
- handleWebsocket: (websocket, { request }) => {
24643
+ handleWebsocket: async (websocket, { request }) => {
24387
24644
  // if (true || logLevel === "debug") {
24388
24645
  // console.log("handleWebsocket", websocket, request.headers);
24389
24646
  // }
24390
- if (request.headers["sec-websocket-protocol"] === "jsenv") {
24391
- serverEventsDispatcher.addWebsocket(websocket, request);
24392
- }
24647
+ const kitchen = getOrCreateKitchen(request);
24648
+ const serveWebsocketHookInfo = {
24649
+ request,
24650
+ websocket,
24651
+ context: kitchen.context,
24652
+ };
24653
+ await kitchen.pluginController.callAsyncHooksUntil(
24654
+ "serveWebsocket",
24655
+ serveWebsocketHookInfo,
24656
+ );
24393
24657
  },
24394
24658
  });
24395
24659
  }
@@ -24483,6 +24747,13 @@ ${error.trace?.message}`);
24483
24747
  };
24484
24748
  };
24485
24749
 
24750
+ const cacheIsDisabledInResponseHeader = (urlInfo) => {
24751
+ return (
24752
+ urlInfo.headers["cache-control"] === "no-store" ||
24753
+ urlInfo.headers["cache-control"] === "no-cache"
24754
+ );
24755
+ };
24756
+
24486
24757
  /*
24487
24758
  * startBuildServer is mean to interact with the build files;
24488
24759
  * files that will be deployed to production server(s).