@jsenv/core 39.12.0 → 39.13.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.
Files changed (37) 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 +1057 -793
  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/dev/start_dev_server.js +39 -49
  12. package/src/kitchen/kitchen.js +20 -4
  13. package/src/kitchen/out_directory_url.js +2 -1
  14. package/src/kitchen/url_graph/references.js +3 -1
  15. package/src/kitchen/url_graph/url_graph.js +1 -0
  16. package/src/kitchen/url_graph/url_info_transformations.js +37 -4
  17. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +10 -8
  18. package/src/plugins/plugin_controller.js +170 -114
  19. package/src/plugins/plugins.js +5 -4
  20. package/src/plugins/protocol_file/client/assets/home.svg +5 -5
  21. package/src/plugins/protocol_file/client/directory_listing.css +190 -0
  22. package/src/plugins/protocol_file/client/directory_listing.html +18 -0
  23. package/src/plugins/protocol_file/client/directory_listing.jsx +250 -0
  24. package/src/plugins/protocol_file/file_and_server_urls_converter.js +32 -0
  25. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +398 -0
  26. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +40 -369
  27. package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +3 -2
  28. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +7 -6
  29. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -3
  30. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +2 -18
  31. package/src/plugins/server_events/jsenv_plugin_server_events.js +100 -0
  32. package/dist/html/directory.html +0 -184
  33. package/dist/html/html_404_and_ancestor_dir.html +0 -222
  34. package/src/plugins/protocol_file/client/assets/directory.css +0 -150
  35. package/src/plugins/protocol_file/client/directory.html +0 -17
  36. package/src/plugins/protocol_file/client/html_404_and_ancestor_dir.html +0 -54
  37. 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
  });
@@ -13073,6 +13158,7 @@ const createReference = ({
13073
13158
  specifierColumn,
13074
13159
  baseUrl,
13075
13160
  isOriginalPosition,
13161
+ isDirectRequest = false,
13076
13162
  isEntryPoint = false,
13077
13163
  isResourceHint = false,
13078
13164
  // implicit references are not real references
@@ -13147,6 +13233,7 @@ const createReference = ({
13147
13233
  specifierColumn,
13148
13234
  isOriginalPosition,
13149
13235
  baseUrl,
13236
+ isDirectRequest,
13150
13237
  isEntryPoint,
13151
13238
  isResourceHint,
13152
13239
  isImplicit,
@@ -13982,6 +14069,7 @@ const createUrlInfo = (url, context) => {
13982
14069
  writable: false,
13983
14070
  value: url,
13984
14071
  });
14072
+ urlInfo.pathname = new URL(url).pathname;
13985
14073
  urlInfo.searchParams = new URL(url).searchParams;
13986
14074
 
13987
14075
  urlInfo.dependencies = createDependencies(urlInfo);
@@ -14570,7 +14658,15 @@ const createUrlInfoTransformer = ({
14570
14658
  contentIsInlined = false;
14571
14659
  }
14572
14660
  if (!contentIsInlined) {
14573
- writeFileSync(new URL(generatedUrl), urlInfo.content, { force: true });
14661
+ const generatedUrlObject = new URL(generatedUrl);
14662
+ let baseName = urlToBasename(generatedUrlObject);
14663
+ for (const [key, value] of generatedUrlObject.searchParams) {
14664
+ baseName += `7${encodeFilePathComponent(key)}=${encodeFilePathComponent(value)}`;
14665
+ }
14666
+ const outFileUrl = setUrlBasename(generatedUrlObject, baseName);
14667
+ let outFilePath = urlToFileSystemPath(outFileUrl);
14668
+ outFilePath = truncate(outFilePath, 2055); // for windows
14669
+ writeFileSync(outFilePath, urlInfo.content, { force: true });
14574
14670
  }
14575
14671
  const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
14576
14672
  if (sourcemapGeneratedUrl && sourcemapReference) {
@@ -14689,6 +14785,26 @@ const createUrlInfoTransformer = ({
14689
14785
  };
14690
14786
  };
14691
14787
 
14788
+ // https://gist.github.com/barbietunnie/7bc6d48a424446c44ff4
14789
+ const illegalRe = /[/?<>\\:*|"]/g;
14790
+ // eslint-disable-next-line no-control-regex
14791
+ const controlRe = /[\x00-\x1f\x80-\x9f]/g;
14792
+ const reservedRe = /^\.+$/;
14793
+ const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
14794
+ const encodeFilePathComponent = (input, replacement = "") => {
14795
+ const encoded = input
14796
+ .replace(illegalRe, replacement)
14797
+ .replace(controlRe, replacement)
14798
+ .replace(reservedRe, replacement)
14799
+ .replace(windowsReservedRe, replacement);
14800
+ return encoded;
14801
+ };
14802
+ const truncate = (sanitized, length) => {
14803
+ const uint8Array = new TextEncoder().encode(sanitized);
14804
+ const truncated = uint8Array.slice(0, length);
14805
+ return new TextDecoder().decode(truncated);
14806
+ };
14807
+
14692
14808
  const shouldUpdateSourcemapComment = (urlInfo, sourcemaps) => {
14693
14809
  if (urlInfo.context.buildStep === "shape") {
14694
14810
  return false;
@@ -14698,7 +14814,6 @@ const shouldUpdateSourcemapComment = (urlInfo, sourcemaps) => {
14698
14814
  }
14699
14815
  return false;
14700
14816
  };
14701
-
14702
14817
  const mayHaveSourcemap = (urlInfo) => {
14703
14818
  if (urlInfo.url.startsWith("data:")) {
14704
14819
  return false;
@@ -14708,7 +14823,6 @@ const mayHaveSourcemap = (urlInfo) => {
14708
14823
  }
14709
14824
  return true;
14710
14825
  };
14711
-
14712
14826
  const shouldHandleSourcemap = (urlInfo) => {
14713
14827
  const { sourcemaps } = urlInfo.context;
14714
14828
  if (
@@ -14792,10 +14906,7 @@ const createKitchen = ({
14792
14906
  initialPluginsMeta,
14793
14907
  );
14794
14908
  kitchen.pluginController = pluginController;
14795
- pluginController.pushPlugin(jsenvPluginHtmlSyntaxErrorFallback());
14796
- plugins.forEach((pluginEntry) => {
14797
- pluginController.pushPlugin(pluginEntry);
14798
- });
14909
+ pluginController.pushPlugin(jsenvPluginHtmlSyntaxErrorFallback(), ...plugins);
14799
14910
 
14800
14911
  const urlInfoTransformer = createUrlInfoTransformer({
14801
14912
  logger,
@@ -14901,6 +15012,25 @@ ${ANSI.color(reference.url, ANSI.YELLOW)}
14901
15012
  `);
14902
15013
  }
14903
15014
  }
15015
+ const request = kitchen.context.request;
15016
+ if (request) {
15017
+ let requestResource = request.resource;
15018
+ let requestedUrl;
15019
+ if (requestResource.startsWith("/@fs/")) {
15020
+ const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
15021
+ requestedUrl = `file:///${fsRootRelativeUrl}`;
15022
+ } else {
15023
+ const requestedUrlObject = new URL(
15024
+ requestResource === "/" ? mainFilePath : requestResource.slice(1),
15025
+ rootDirectoryUrl,
15026
+ );
15027
+ requestedUrlObject.searchParams.delete("hot");
15028
+ requestedUrl = requestedUrlObject.href;
15029
+ }
15030
+ if (requestedUrl === reference.url) {
15031
+ reference.isDirectRequest = true;
15032
+ }
15033
+ }
14904
15034
  redirect: {
14905
15035
  if (reference.isImplicit && reference.isWeak) {
14906
15036
  // not needed for implicit references that are not rendered anywhere
@@ -15808,10 +15938,11 @@ const jsenvPluginInliningIntoHtml = () => {
15808
15938
  const { line, column, isOriginal } = getHtmlNodePosition(linkNode, {
15809
15939
  preferOriginal: true,
15810
15940
  });
15811
- const linkInlineUrl = getUrlForContentInsideHtml(linkNode, {
15812
- htmlUrl: urlInfo.url,
15813
- url: linkReference.url,
15814
- });
15941
+ const linkInlineUrl = getUrlForContentInsideHtml(
15942
+ linkNode,
15943
+ urlInfo,
15944
+ linkReference,
15945
+ );
15815
15946
  const linkReferenceInlined = linkReference.inline({
15816
15947
  line,
15817
15948
  column,
@@ -15860,10 +15991,11 @@ const jsenvPluginInliningIntoHtml = () => {
15860
15991
  const { line, column, isOriginal } = getHtmlNodePosition(scriptNode, {
15861
15992
  preferOriginal: true,
15862
15993
  });
15863
- const scriptInlineUrl = getUrlForContentInsideHtml(scriptNode, {
15864
- htmlUrl: urlInfo.url,
15865
- url: scriptReference.url,
15866
- });
15994
+ const scriptInlineUrl = getUrlForContentInsideHtml(
15995
+ scriptNode,
15996
+ urlInfo,
15997
+ scriptReference,
15998
+ );
15867
15999
  const scriptReferenceInlined = scriptReference.inline({
15868
16000
  line,
15869
16001
  column,
@@ -17052,9 +17184,11 @@ const jsenvPluginHtmlReferenceAnalysis = ({
17052
17184
  const { line, column, isOriginal } = getHtmlNodePosition(node, {
17053
17185
  preferOriginal: true,
17054
17186
  });
17055
- const inlineContentUrl = getUrlForContentInsideHtml(node, {
17056
- htmlUrl: urlInfo.url,
17057
- });
17187
+ const inlineContentUrl = getUrlForContentInsideHtml(
17188
+ node,
17189
+ urlInfo,
17190
+ null,
17191
+ );
17058
17192
  const debug =
17059
17193
  getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
17060
17194
  const inlineReference = urlInfo.dependencies.foundInline({
@@ -17195,9 +17329,8 @@ const jsenvPluginHtmlReferenceAnalysis = ({
17195
17329
  );
17196
17330
  const importmapInlineUrl = getUrlForContentInsideHtml(
17197
17331
  scriptNode,
17198
- {
17199
- htmlUrl: urlInfo.url,
17200
- },
17332
+ urlInfo,
17333
+ importmapReference,
17201
17334
  );
17202
17335
  const importmapReferenceInlined = importmapReference.inline({
17203
17336
  line,
@@ -17481,9 +17614,7 @@ const parseAndTransformJsReferences = async (
17481
17614
  Object.keys(urlInfo.context.runtimeCompat).toString() === "node";
17482
17615
 
17483
17616
  const onInlineReference = (inlineReferenceInfo) => {
17484
- const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, {
17485
- url: urlInfo.url,
17486
- });
17617
+ const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, urlInfo);
17487
17618
  let { quote } = inlineReferenceInfo;
17488
17619
  if (quote === "`" && !canUseTemplateLiterals) {
17489
17620
  // if quote is "`" and template literals are not supported
@@ -17726,23 +17857,7 @@ const jsenvPluginInlineContentFetcher = () => {
17726
17857
  if (!urlInfo.isInline) {
17727
17858
  return null;
17728
17859
  }
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
- }
17860
+ const { isDirectRequest } = urlInfo.lastReference;
17746
17861
  /*
17747
17862
  * We want to find inline content but it's not straightforward
17748
17863
  *
@@ -17771,7 +17886,7 @@ const jsenvPluginInlineContentFetcher = () => {
17771
17886
  originalContent = reference.content;
17772
17887
  }
17773
17888
  lastInlineReference = reference;
17774
- if (isDirectRequestToFile) {
17889
+ if (isDirectRequest) {
17775
17890
  break;
17776
17891
  }
17777
17892
  }
@@ -19184,71 +19299,581 @@ const jsenvPluginVersionSearchParam = () => {
19184
19299
  };
19185
19300
  };
19186
19301
 
19187
- const jsenvPluginFsRedirection = ({
19188
- directoryContentMagicName,
19189
- magicExtensions = ["inherit", ".js"],
19190
- magicDirectoryIndex = true,
19191
- preserveSymlinks = false,
19192
- }) => {
19302
+ const FILE_AND_SERVER_URLS_CONVERTER = {
19303
+ asServerUrl: (fileUrl, serverRootDirectoryUrl) => {
19304
+ if (fileUrl === serverRootDirectoryUrl) {
19305
+ return "/";
19306
+ }
19307
+ if (urlIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
19308
+ const urlRelativeToServer = urlToRelativeUrl(
19309
+ fileUrl,
19310
+ serverRootDirectoryUrl,
19311
+ );
19312
+ return `/${urlRelativeToServer}`;
19313
+ }
19314
+ const urlRelativeToFilesystemRoot = String(fileUrl).slice(
19315
+ "file:///".length,
19316
+ );
19317
+ return `/@fs/${urlRelativeToFilesystemRoot}`;
19318
+ },
19319
+ asFileUrl: (urlRelativeToServer, serverRootDirectoryUrl) => {
19320
+ if (urlRelativeToServer.startsWith("/@fs/")) {
19321
+ const urlRelativeToFilesystemRoot = urlRelativeToServer.slice(
19322
+ "/@fs/".length,
19323
+ );
19324
+ return `file:///${urlRelativeToFilesystemRoot}`;
19325
+ }
19326
+ if (urlRelativeToServer[0] === "/") {
19327
+ return new URL(urlRelativeToServer.slice(1), serverRootDirectoryUrl).href;
19328
+ }
19329
+ return new URL(urlRelativeToServer, serverRootDirectoryUrl).href;
19330
+ },
19331
+ };
19332
+
19333
+ const jsenvPluginInjections = (rawAssociations) => {
19334
+ let resolvedAssociations;
19335
+
19193
19336
  return {
19194
- name: "jsenv:fs_redirection",
19337
+ name: "jsenv:injections",
19195
19338
  appliesDuring: "*",
19196
- redirectReference: (reference) => {
19197
- // http, https, data, about, ...
19198
- if (!reference.url.startsWith("file:")) {
19199
- return null;
19200
- }
19201
- if (reference.isInline) {
19339
+ init: (context) => {
19340
+ resolvedAssociations = URL_META.resolveAssociations(
19341
+ { injectionsGetter: rawAssociations },
19342
+ context.rootDirectoryUrl,
19343
+ );
19344
+ },
19345
+ transformUrlContent: async (urlInfo) => {
19346
+ const { injectionsGetter } = URL_META.applyAssociations({
19347
+ url: asUrlWithoutSearch(urlInfo.url),
19348
+ associations: resolvedAssociations,
19349
+ });
19350
+ if (!injectionsGetter) {
19202
19351
  return null;
19203
19352
  }
19204
- if (reference.url === "file:///" || reference.url === "file://") {
19205
- return `ignore:file:///`;
19353
+ if (typeof injectionsGetter !== "function") {
19354
+ throw new TypeError("injectionsGetter must be a function");
19206
19355
  }
19207
- // ignore all new URL second arg
19208
- if (reference.subtype === "new_url_second_arg") {
19209
- return `ignore:${reference.url}`;
19356
+ const injections = await injectionsGetter(urlInfo);
19357
+ if (!injections) {
19358
+ return null;
19210
19359
  }
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;
19360
+ const keys = Object.keys(injections);
19361
+ if (keys.length === 0) {
19362
+ return null;
19222
19363
  }
19223
- // ignore "./" on new URL("./")
19224
- // if (
19225
- // reference.subtype === "new_url_first_arg" &&
19226
- // reference.specifier === "./"
19227
- // ) {
19228
- // return `ignore:${reference.url}`;
19229
- // }
19230
- const urlObject = new URL(reference.url);
19231
- let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
19232
- reference.fsStat = fsStat;
19233
- const { search, hash } = urlObject;
19234
- urlObject.search = "";
19235
- urlObject.hash = "";
19236
- applyFsStatEffectsOnUrlObject(urlObject, fsStat);
19237
- const shouldApplyFilesystemMagicResolution =
19238
- reference.type === "js_import";
19239
- if (shouldApplyFilesystemMagicResolution) {
19240
- const filesystemResolution = applyFileSystemMagicResolution(
19241
- urlObject.href,
19242
- {
19243
- fileStat: fsStat,
19244
- magicDirectoryIndex,
19245
- magicExtensions: getExtensionsToTry(
19246
- magicExtensions,
19247
- reference.ownerUrlInfo.url,
19248
- ),
19249
- },
19250
- );
19251
- if (filesystemResolution.stat) {
19364
+ return replacePlaceholders(urlInfo.content, injections, urlInfo);
19365
+ },
19366
+ };
19367
+ };
19368
+
19369
+ const injectionSymbol = Symbol.for("jsenv_injection");
19370
+ const INJECTIONS = {
19371
+ optional: (value) => {
19372
+ return { [injectionSymbol]: "optional", value };
19373
+ },
19374
+ };
19375
+
19376
+ // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
19377
+ const replacePlaceholders = (content, replacements, urlInfo) => {
19378
+ const magicSource = createMagicSource(content);
19379
+ for (const key of Object.keys(replacements)) {
19380
+ let index = content.indexOf(key);
19381
+ const replacement = replacements[key];
19382
+ let isOptional;
19383
+ let value;
19384
+ if (replacement && replacement[injectionSymbol]) {
19385
+ const valueBehindSymbol = replacement[injectionSymbol];
19386
+ isOptional = valueBehindSymbol === "optional";
19387
+ value = replacement.value;
19388
+ } else {
19389
+ value = replacement;
19390
+ }
19391
+ if (index === -1) {
19392
+ if (!isOptional) {
19393
+ urlInfo.context.logger.warn(
19394
+ `placeholder "${key}" not found in ${urlInfo.url}.
19395
+ --- suggestion a ---
19396
+ Add "${key}" in that file.
19397
+ --- suggestion b ---
19398
+ Fix eventual typo in "${key}"?
19399
+ --- suggestion c ---
19400
+ Mark injection as optional using INJECTIONS.optional():
19401
+ import { INJECTIONS } from "@jsenv/core";
19402
+
19403
+ return {
19404
+ "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
19405
+ };`,
19406
+ );
19407
+ }
19408
+ continue;
19409
+ }
19410
+
19411
+ while (index !== -1) {
19412
+ const start = index;
19413
+ const end = index + key.length;
19414
+ magicSource.replace({
19415
+ start,
19416
+ end,
19417
+ replacement:
19418
+ urlInfo.type === "js_classic" ||
19419
+ urlInfo.type === "js_module" ||
19420
+ urlInfo.type === "html"
19421
+ ? JSON.stringify(value, null, " ")
19422
+ : value,
19423
+ });
19424
+ index = content.indexOf(key, end);
19425
+ }
19426
+ }
19427
+ return magicSource.toContentAndSourcemap();
19428
+ };
19429
+
19430
+ /*
19431
+ * NICE TO HAVE:
19432
+ *
19433
+ * - when visiting urls outside server root directory the UI is messed up
19434
+ *
19435
+ * Let's say I visit file outside the server root directory that is in 404
19436
+ * We must update the enoent message and maybe other things to take into account
19437
+ * that url is no longer /something but "@fs/project_root/something" in the browser url bar
19438
+ *
19439
+ * - watching directory might result into things that are not properly handled:
19440
+ * 1. the existing directory is deleted
19441
+ * -> we should update the whole page to use a new "firstExistingDirectoryUrl"
19442
+ * 2. the enoent is impacted
19443
+ * -> we should update the ENOENT message
19444
+ * It means the websocket should contain more data and we can't assume firstExistingDirectoryUrl won't change
19445
+ *
19446
+
19447
+ */
19448
+
19449
+
19450
+ const htmlFileUrlForDirectory = new URL(
19451
+ "./html/directory_listing.html",
19452
+ import.meta.url,
19453
+ );
19454
+
19455
+ const jsenvPluginDirectoryListing = ({
19456
+ directoryContentMagicName,
19457
+ directoryListingUrlMocks,
19458
+ autoreload = true,
19459
+ }) => {
19460
+ return {
19461
+ name: "jsenv:directory_listing",
19462
+ appliesDuring: "dev",
19463
+ redirectReference: (reference) => {
19464
+ if (reference.isInline) {
19465
+ return null;
19466
+ }
19467
+ const url = reference.url;
19468
+ if (!url.startsWith("file:")) {
19469
+ return null;
19470
+ }
19471
+ let { fsStat } = reference;
19472
+ if (!fsStat) {
19473
+ fsStat = readEntryStatSync(url, { nullIfNotFound: true });
19474
+ reference.fsStat = fsStat;
19475
+ }
19476
+ const { request } = reference.ownerUrlInfo.context;
19477
+ if (!fsStat) {
19478
+ if (
19479
+ reference.isDirectRequest &&
19480
+ request &&
19481
+ request.headers["sec-fetch-dest"] === "document"
19482
+ ) {
19483
+ return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}&enoent`;
19484
+ }
19485
+ return null;
19486
+ }
19487
+ const isDirectory = fsStat?.isDirectory();
19488
+ if (!isDirectory) {
19489
+ return null;
19490
+ }
19491
+ if (reference.type === "filesystem") {
19492
+ // TODO: we should redirect to something like /...json
19493
+ // and any file name ...json is a special file serving directory content as json
19494
+ return null;
19495
+ }
19496
+ const acceptsHtml = request
19497
+ ? pickContentType(request, ["text/html"])
19498
+ : false;
19499
+ if (!acceptsHtml) {
19500
+ return null;
19501
+ }
19502
+ reference.fsStat = null; // reset fsStat, now it's not a directory anyor
19503
+ return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}`;
19504
+ },
19505
+ transformUrlContent: {
19506
+ html: (urlInfo) => {
19507
+ const urlWithoutSearch = asUrlWithoutSearch(urlInfo.url);
19508
+ if (urlWithoutSearch !== String(htmlFileUrlForDirectory)) {
19509
+ return null;
19510
+ }
19511
+ const requestedUrl = urlInfo.searchParams.get("url");
19512
+ if (!requestedUrl) {
19513
+ return null;
19514
+ }
19515
+ urlInfo.headers["cache-control"] = "no-cache";
19516
+ const enoent = urlInfo.searchParams.has("enoent");
19517
+ if (enoent) {
19518
+ urlInfo.status = 404;
19519
+ urlInfo.headers["cache-control"] = "no-cache";
19520
+ }
19521
+ const request = urlInfo.context.request;
19522
+ const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
19523
+ return replacePlaceholders(
19524
+ urlInfo.content,
19525
+ {
19526
+ ...generateDirectoryListingInjection(requestedUrl, {
19527
+ autoreload,
19528
+ request,
19529
+ directoryListingUrlMocks,
19530
+ directoryContentMagicName,
19531
+ rootDirectoryUrl,
19532
+ mainFilePath,
19533
+ enoent,
19534
+ }),
19535
+ },
19536
+ urlInfo,
19537
+ );
19538
+ },
19539
+ },
19540
+ serveWebsocket: ({ websocket, request, context }) => {
19541
+ if (!autoreload) {
19542
+ return false;
19543
+ }
19544
+ const secProtocol = request.headers["sec-websocket-protocol"];
19545
+ if (secProtocol !== "watch-directory") {
19546
+ return false;
19547
+ }
19548
+ const { rootDirectoryUrl, mainFilePath } = context;
19549
+ const requestedUrl = FILE_AND_SERVER_URLS_CONVERTER.asFileUrl(
19550
+ request.pathname,
19551
+ rootDirectoryUrl,
19552
+ );
19553
+ const closestDirectoryUrl = getFirstExistingDirectoryUrl(requestedUrl);
19554
+ const sendMessage = (message) => {
19555
+ websocket.send(JSON.stringify(message));
19556
+ };
19557
+ const generateItems = () => {
19558
+ const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
19559
+ requestedUrl,
19560
+ rootDirectoryUrl,
19561
+ );
19562
+ const items = getDirectoryContentItems({
19563
+ serverRootDirectoryUrl: rootDirectoryUrl,
19564
+ mainFilePath,
19565
+ requestedUrl,
19566
+ firstExistingDirectoryUrl,
19567
+ });
19568
+ return items;
19569
+ };
19570
+
19571
+ const unwatch = registerDirectoryLifecycle(closestDirectoryUrl, {
19572
+ added: ({ relativeUrl }) => {
19573
+ sendMessage({
19574
+ type: "change",
19575
+ reason: `${relativeUrl} added`,
19576
+ items: generateItems(),
19577
+ });
19578
+ },
19579
+ updated: ({ relativeUrl }) => {
19580
+ sendMessage({
19581
+ type: "change",
19582
+ reason: `${relativeUrl} updated`,
19583
+ items: generateItems(),
19584
+ });
19585
+ },
19586
+ removed: ({ relativeUrl }) => {
19587
+ sendMessage({
19588
+ type: "change",
19589
+ reason: `${relativeUrl} removed`,
19590
+ items: generateItems(),
19591
+ });
19592
+ },
19593
+ });
19594
+ websocket.signal.addEventListener("abort", () => {
19595
+ unwatch();
19596
+ });
19597
+ return true;
19598
+ },
19599
+ };
19600
+ };
19601
+
19602
+ const generateDirectoryListingInjection = (
19603
+ requestedUrl,
19604
+ {
19605
+ rootDirectoryUrl,
19606
+ mainFilePath,
19607
+ request,
19608
+ directoryListingUrlMocks,
19609
+ directoryContentMagicName,
19610
+ autoreload,
19611
+ enoent,
19612
+ },
19613
+ ) => {
19614
+ let serverRootDirectoryUrl = rootDirectoryUrl;
19615
+ const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
19616
+ requestedUrl,
19617
+ serverRootDirectoryUrl,
19618
+ );
19619
+ const directoryContentItems = getDirectoryContentItems({
19620
+ serverRootDirectoryUrl,
19621
+ mainFilePath,
19622
+ requestedUrl,
19623
+ firstExistingDirectoryUrl,
19624
+ });
19625
+ package_workspaces: {
19626
+ const packageDirectoryUrl = lookupPackageDirectory(serverRootDirectoryUrl);
19627
+ if (!packageDirectoryUrl) {
19628
+ break package_workspaces;
19629
+ }
19630
+ if (String(packageDirectoryUrl) === String(serverRootDirectoryUrl)) {
19631
+ break package_workspaces;
19632
+ }
19633
+ rootDirectoryUrl = packageDirectoryUrl;
19634
+ // if (String(firstExistingDirectoryUrl) === String(serverRootDirectoryUrl)) {
19635
+ // let packageContent;
19636
+ // try {
19637
+ // packageContent = JSON.parse(
19638
+ // readFileSync(new URL("package.json", packageDirectoryUrl), "utf8"),
19639
+ // );
19640
+ // } catch {
19641
+ // break package_workspaces;
19642
+ // }
19643
+ // const { workspaces } = packageContent;
19644
+ // if (Array.isArray(workspaces)) {
19645
+ // for (const workspace of workspaces) {
19646
+ // const workspaceUrlObject = new URL(workspace, packageDirectoryUrl);
19647
+ // const workspaceUrl = workspaceUrlObject.href;
19648
+ // if (workspaceUrl.endsWith("*")) {
19649
+ // const directoryUrl = ensurePathnameTrailingSlash(
19650
+ // workspaceUrl.slice(0, -1),
19651
+ // );
19652
+ // fileUrls.push(new URL(directoryUrl));
19653
+ // } else {
19654
+ // fileUrls.push(ensurePathnameTrailingSlash(workspaceUrlObject));
19655
+ // }
19656
+ // }
19657
+ // }
19658
+ // }
19659
+ }
19660
+ const directoryUrlRelativeToServer =
19661
+ FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
19662
+ firstExistingDirectoryUrl,
19663
+ serverRootDirectoryUrl,
19664
+ );
19665
+ const websocketScheme = request.protocol === "https" ? "wss" : "ws";
19666
+ const { host } = new URL(request.url);
19667
+ const websocketUrl = `${websocketScheme}://${host}${directoryUrlRelativeToServer}`;
19668
+
19669
+ const navItems = [];
19670
+ {
19671
+ const lastItemUrl = firstExistingDirectoryUrl;
19672
+ const lastItemRelativeUrl = urlToRelativeUrl(lastItemUrl, rootDirectoryUrl);
19673
+ const rootDirectoryUrlName = urlToFilename$1(rootDirectoryUrl);
19674
+ let parts;
19675
+ if (lastItemRelativeUrl) {
19676
+ parts = `${rootDirectoryUrlName}/${lastItemRelativeUrl}`.split("/");
19677
+ } else {
19678
+ parts = [rootDirectoryUrlName];
19679
+ }
19680
+
19681
+ let i = 0;
19682
+ while (i < parts.length) {
19683
+ const part = parts[i];
19684
+ const isLastPart = i === parts.length - 1;
19685
+ if (isLastPart && part === "") {
19686
+ // ignore trailing slash
19687
+ break;
19688
+ }
19689
+ let navItemRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
19690
+ let navItemUrl =
19691
+ navItemRelativeUrl === ""
19692
+ ? rootDirectoryUrl
19693
+ : new URL(navItemRelativeUrl, rootDirectoryUrl).href;
19694
+ if (!isLastPart) {
19695
+ navItemUrl = ensurePathnameTrailingSlash(navItemUrl);
19696
+ }
19697
+ let urlRelativeToServer = FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
19698
+ navItemUrl,
19699
+ serverRootDirectoryUrl,
19700
+ );
19701
+ let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, requestedUrl);
19702
+ const isServerRootDirectory = navItemUrl === serverRootDirectoryUrl;
19703
+ if (isServerRootDirectory) {
19704
+ urlRelativeToServer = `/${directoryContentMagicName}`;
19705
+ urlRelativeToDocument = `/${directoryContentMagicName}`;
19706
+ }
19707
+ const name = part;
19708
+ const isCurrent = navItemUrl === String(firstExistingDirectoryUrl);
19709
+ navItems.push({
19710
+ url: navItemUrl,
19711
+ urlRelativeToServer,
19712
+ urlRelativeToDocument,
19713
+ isServerRootDirectory,
19714
+ isCurrent,
19715
+ name,
19716
+ });
19717
+ i++;
19718
+ }
19719
+ }
19720
+
19721
+ let enoentDetails = null;
19722
+ if (enoent) {
19723
+ const fileRelativeUrl = urlToRelativeUrl(
19724
+ requestedUrl,
19725
+ serverRootDirectoryUrl,
19726
+ );
19727
+ let filePathExisting;
19728
+ let filePathNotFound;
19729
+ const existingIndex = String(firstExistingDirectoryUrl).length;
19730
+ filePathExisting = urlToRelativeUrl(
19731
+ firstExistingDirectoryUrl,
19732
+ serverRootDirectoryUrl,
19733
+ );
19734
+ filePathNotFound = requestedUrl.slice(existingIndex);
19735
+ enoentDetails = {
19736
+ fileUrl: requestedUrl,
19737
+ fileRelativeUrl,
19738
+ filePathExisting: `/${filePathExisting}`,
19739
+ filePathNotFound,
19740
+ };
19741
+ }
19742
+
19743
+ return {
19744
+ __DIRECTORY_LISTING__: {
19745
+ enoentDetails,
19746
+ navItems,
19747
+ directoryListingUrlMocks,
19748
+ directoryContentMagicName,
19749
+ directoryUrl: firstExistingDirectoryUrl,
19750
+ serverRootDirectoryUrl,
19751
+ rootDirectoryUrl,
19752
+ mainFilePath,
19753
+ directoryContentItems,
19754
+ websocketUrl,
19755
+ autoreload,
19756
+ },
19757
+ };
19758
+ };
19759
+ const getFirstExistingDirectoryUrl = (requestedUrl, serverRootDirectoryUrl) => {
19760
+ let firstExistingDirectoryUrl = new URL("./", requestedUrl);
19761
+ while (!existsSync(firstExistingDirectoryUrl)) {
19762
+ firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
19763
+ if (!urlIsInsideOf(firstExistingDirectoryUrl, serverRootDirectoryUrl)) {
19764
+ firstExistingDirectoryUrl = new URL(serverRootDirectoryUrl);
19765
+ break;
19766
+ }
19767
+ }
19768
+ return firstExistingDirectoryUrl;
19769
+ };
19770
+ const getDirectoryContentItems = ({
19771
+ serverRootDirectoryUrl,
19772
+ mainFilePath,
19773
+ firstExistingDirectoryUrl,
19774
+ }) => {
19775
+ const directoryContentArray = readdirSync(new URL(firstExistingDirectoryUrl));
19776
+ const fileUrls = [];
19777
+ for (const filename of directoryContentArray) {
19778
+ const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
19779
+ if (lstatSync(fileUrlObject).isDirectory()) {
19780
+ fileUrls.push(ensurePathnameTrailingSlash(fileUrlObject));
19781
+ } else {
19782
+ fileUrls.push(fileUrlObject);
19783
+ }
19784
+ }
19785
+ fileUrls.sort((a, b) => {
19786
+ return comparePathnames(a.pathname, b.pathname);
19787
+ });
19788
+ const items = [];
19789
+ for (const fileUrl of fileUrls) {
19790
+ const urlRelativeToCurrentDirectory = urlToRelativeUrl(
19791
+ fileUrl,
19792
+ firstExistingDirectoryUrl,
19793
+ );
19794
+ const urlRelativeToServer = FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
19795
+ fileUrl,
19796
+ serverRootDirectoryUrl,
19797
+ );
19798
+ const url = String(fileUrl);
19799
+ const mainFileUrl = new URL(mainFilePath, serverRootDirectoryUrl).href;
19800
+ const isMainFile = url === mainFileUrl;
19801
+
19802
+ items.push({
19803
+ url,
19804
+ urlRelativeToCurrentDirectory,
19805
+ urlRelativeToServer,
19806
+ isMainFile,
19807
+ });
19808
+ }
19809
+ return items;
19810
+ };
19811
+
19812
+ const jsenvPluginFsRedirection = ({
19813
+ directoryContentMagicName,
19814
+ magicExtensions = ["inherit", ".js"],
19815
+ magicDirectoryIndex = true,
19816
+ preserveSymlinks = false,
19817
+ }) => {
19818
+ return {
19819
+ name: "jsenv:fs_redirection",
19820
+ appliesDuring: "*",
19821
+ redirectReference: (reference) => {
19822
+ // http, https, data, about, ...
19823
+ if (!reference.url.startsWith("file:")) {
19824
+ return null;
19825
+ }
19826
+ if (reference.isInline) {
19827
+ return null;
19828
+ }
19829
+ if (reference.url === "file:///" || reference.url === "file://") {
19830
+ return `ignore:file:///`;
19831
+ }
19832
+ // ignore all new URL second arg
19833
+ if (reference.subtype === "new_url_second_arg") {
19834
+ return `ignore:${reference.url}`;
19835
+ }
19836
+ if (
19837
+ reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
19838
+ ) {
19839
+ const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19840
+ const directoryUrl = new URL(
19841
+ reference.specifierPathname
19842
+ .replace(`/${directoryContentMagicName}`, "/")
19843
+ .slice(1),
19844
+ rootDirectoryUrl,
19845
+ ).href;
19846
+ return directoryUrl;
19847
+ }
19848
+ // ignore "./" on new URL("./")
19849
+ // if (
19850
+ // reference.subtype === "new_url_first_arg" &&
19851
+ // reference.specifier === "./"
19852
+ // ) {
19853
+ // return `ignore:${reference.url}`;
19854
+ // }
19855
+ const urlObject = new URL(reference.url);
19856
+ let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
19857
+ reference.fsStat = fsStat;
19858
+ const { search, hash } = urlObject;
19859
+ urlObject.search = "";
19860
+ urlObject.hash = "";
19861
+ applyFsStatEffectsOnUrlObject(urlObject, fsStat);
19862
+ const shouldApplyFilesystemMagicResolution =
19863
+ reference.type === "js_import";
19864
+ if (shouldApplyFilesystemMagicResolution) {
19865
+ const filesystemResolution = applyFileSystemMagicResolution(
19866
+ urlObject.href,
19867
+ {
19868
+ fileStat: fsStat,
19869
+ magicDirectoryIndex,
19870
+ magicExtensions: getExtensionsToTry(
19871
+ magicExtensions,
19872
+ reference.ownerUrlInfo.url,
19873
+ ),
19874
+ },
19875
+ );
19876
+ if (filesystemResolution.stat) {
19252
19877
  fsStat = filesystemResolution.stat;
19253
19878
  reference.fsStat = fsStat;
19254
19879
  urlObject.href = filesystemResolution.url;
@@ -19318,17 +19943,10 @@ const resolveSymlink = (fileUrl) => {
19318
19943
  return realUrlObject.href;
19319
19944
  };
19320
19945
 
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
19946
  const directoryContentMagicName = "...";
19330
19947
 
19331
19948
  const jsenvPluginProtocolFile = ({
19949
+ supervisorEnabled,
19332
19950
  magicExtensions,
19333
19951
  magicDirectoryIndex,
19334
19952
  preserveSymlinks,
@@ -19363,8 +19981,7 @@ const jsenvPluginProtocolFile = ({
19363
19981
  appliesDuring: "dev",
19364
19982
  resolveReference: (reference) => {
19365
19983
  if (reference.specifier.startsWith("/@fs/")) {
19366
- const fsRootRelativeUrl = reference.specifier.slice("/@fs/".length);
19367
- return `file:///${fsRootRelativeUrl}`;
19984
+ return FILE_AND_SERVER_URLS_CONVERTER.asFileUrl(reference.specifier);
19368
19985
  }
19369
19986
  return null;
19370
19987
  },
@@ -19383,382 +20000,71 @@ const jsenvPluginProtocolFile = ({
19383
20000
  }
19384
20001
  }
19385
20002
  const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
19386
- if (urlIsInsideOf(generatedUrl, rootDirectoryUrl)) {
19387
- const result = `/${urlToRelativeUrl(generatedUrl, rootDirectoryUrl)}`;
19388
- return result;
19389
- }
19390
- const result = `/@fs/${generatedUrl.slice("file:///".length)}`;
19391
- return result;
20003
+ return FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
20004
+ generatedUrl,
20005
+ rootDirectoryUrl,
20006
+ );
19392
20007
  },
19393
20008
  },
20009
+ jsenvPluginDirectoryListing({
20010
+ supervisorEnabled,
20011
+ directoryContentMagicName,
20012
+ directoryListingUrlMocks,
20013
+ }),
19394
20014
  {
19395
- name: "jsenv:file_url_fetching",
20015
+ name: "jsenv:directory_as_json",
19396
20016
  appliesDuring: "*",
19397
20017
  fetchUrlContent: (urlInfo) => {
19398
- if (!urlInfo.url.startsWith("file:")) {
19399
- return null;
19400
- }
19401
20018
  const { firstReference } = urlInfo;
19402
- const { mainFilePath } = urlInfo.context;
19403
20019
  let { fsStat } = firstReference;
19404
20020
  if (!fsStat) {
19405
20021
  fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
19406
20022
  }
19407
- const isDirectory = fsStat?.isDirectory();
19408
- const { rootDirectoryUrl, request } = urlInfo.context;
19409
- const serveFile = (url) => {
19410
- const contentType = CONTENT_TYPE.fromUrlExtension(url);
19411
- const fileBuffer = readFileSync(new URL(url));
19412
- const content = CONTENT_TYPE.isTextual(contentType)
19413
- ? String(fileBuffer)
19414
- : fileBuffer;
19415
- return {
19416
- content,
19417
- contentType,
19418
- contentLength: fileBuffer.length,
19419
- };
19420
- };
19421
-
19422
20023
  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
- }
20024
+ return null;
19443
20025
  }
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
- };
20026
+ const isDirectory = fsStat.isDirectory();
20027
+ if (!isDirectory) {
20028
+ return null;
19478
20029
  }
19479
- return serveFile(urlInfo.url);
20030
+ const directoryContentArray = readdirSync(new URL(urlInfo.url));
20031
+ const content = JSON.stringify(directoryContentArray, null, " ");
20032
+ return {
20033
+ type: "directory",
20034
+ contentType: "application/json",
20035
+ content,
20036
+ };
19480
20037
  },
19481
20038
  },
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
- });
20039
+ {
20040
+ name: "jsenv:file_url_fetching",
20041
+ appliesDuring: "*",
20042
+ fetchUrlContent: (urlInfo) => {
20043
+ if (!urlInfo.url.startsWith("file:")) {
20044
+ return null;
20045
+ }
20046
+ const { firstReference } = urlInfo;
20047
+ let { fsStat } = firstReference;
20048
+ if (!fsStat) {
20049
+ fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
20050
+ }
20051
+ const serveFile = (url) => {
20052
+ const contentType = CONTENT_TYPE.fromUrlExtension(url);
20053
+ const fileBuffer = readFileSync(new URL(url));
20054
+ const content = CONTENT_TYPE.isTextual(contentType)
20055
+ ? String(fileBuffer)
20056
+ : fileBuffer;
20057
+ return {
20058
+ content,
20059
+ contentType,
20060
+ contentLength: fileBuffer.length,
20061
+ };
20062
+ };
20063
+
20064
+ return serveFile(urlInfo.url);
20065
+ },
20066
+ },
20067
+ ];
19762
20068
  };
19763
20069
 
19764
20070
  const jsenvPluginProtocolHttp = ({ include }) => {
@@ -19815,10 +20121,11 @@ const jsenvPluginProtocolHttp = ({ include }) => {
19815
20121
  return fileUrl;
19816
20122
  },
19817
20123
  fetchUrlContent: async (urlInfo) => {
19818
- if (!urlInfo.originalUrl.startsWith("http")) {
20124
+ const originalUrl = urlInfo.originalUrl;
20125
+ if (!originalUrl.startsWith("http")) {
19819
20126
  return null;
19820
20127
  }
19821
- const response = await fetch(urlInfo.originalUrl);
20128
+ const response = await fetch(originalUrl);
19822
20129
  const responseStatus = response.status;
19823
20130
  if (responseStatus < 200 || responseStatus > 299) {
19824
20131
  throw new Error(`unexpected response status ${responseStatus}`);
@@ -19851,103 +20158,6 @@ const asValidFilename = (string) => {
19851
20158
  return string;
19852
20159
  };
19853
20160
 
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
20161
  /*
19952
20162
  * Some code uses globals specific to Node.js in code meant to run in browsers...
19953
20163
  * This plugin will replace some node globals to things compatible with web:
@@ -21269,8 +21479,8 @@ const getCorePlugins = ({
21269
21479
  jsenvPluginReferenceAnalysis(referenceAnalysis),
21270
21480
  ...(injections ? [jsenvPluginInjections(injections)] : []),
21271
21481
  jsenvPluginTranspilation(transpilation),
21482
+ // "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
21272
21483
  ...(inlining ? [jsenvPluginInlining()] : []),
21273
- ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []), // after inline as it needs inline script to be cooked
21274
21484
 
21275
21485
  /* When resolving references the following applies by default:
21276
21486
  - http urls are resolved by jsenvPluginHttpUrls
@@ -21284,7 +21494,6 @@ const getCorePlugins = ({
21284
21494
  magicDirectoryIndex,
21285
21495
  directoryListingUrlMocks,
21286
21496
  }),
21287
-
21288
21497
  {
21289
21498
  name: "jsenv:resolve_root_as_main",
21290
21499
  appliesDuring: "*",
@@ -21303,12 +21512,14 @@ const getCorePlugins = ({
21303
21512
  : []),
21304
21513
  jsenvPluginWebResolution(),
21305
21514
  jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect),
21306
-
21307
21515
  jsenvPluginVersionSearchParam(),
21516
+
21517
+ // "jsenvPluginSupervisor" MUST be after "jsenvPluginInlining" as it needs inline script to be cooked
21518
+ ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []),
21519
+
21308
21520
  jsenvPluginCommonJsGlobals(),
21309
21521
  jsenvPluginImportMetaScenarios(),
21310
21522
  ...(scenarioPlaceholders ? [jsenvPluginGlobalScenarios()] : []),
21311
-
21312
21523
  jsenvPluginNodeRuntime({ runtimeCompat }),
21313
21524
 
21314
21525
  jsenvPluginImportMetaHot(),
@@ -21868,7 +22079,6 @@ const createBuildSpecifierManager = ({
21868
22079
  type: reference.type,
21869
22080
  expectedType: reference.expectedType,
21870
22081
  specifier: reference.specifier,
21871
- specifierPathname: reference.specifierPathname,
21872
22082
  specifierLine: reference.specifierLine,
21873
22083
  specifierColumn: reference.specifierColumn,
21874
22084
  specifierStart: reference.specifierStart,
@@ -23317,33 +23527,33 @@ build ${entryPointKeys.length} entry points`);
23317
23527
 
23318
23528
  const bundlers = {};
23319
23529
  {
23320
- rawKitchen.pluginController.plugins.forEach((plugin) => {
23530
+ for (const plugin of rawKitchen.pluginController.activePlugins) {
23321
23531
  const bundle = plugin.bundle;
23322
23532
  if (!bundle) {
23323
- return;
23533
+ continue;
23324
23534
  }
23325
23535
  if (typeof bundle !== "object") {
23326
23536
  throw new Error(
23327
23537
  `bundle must be an object, found "${bundle}" on plugin named "${plugin.name}"`,
23328
23538
  );
23329
23539
  }
23330
- Object.keys(bundle).forEach((type) => {
23540
+ for (const type of Object.keys(bundle)) {
23331
23541
  const bundleFunction = bundle[type];
23332
23542
  if (!bundleFunction) {
23333
- return;
23543
+ continue;
23334
23544
  }
23335
23545
  const bundlerForThatType = bundlers[type];
23336
23546
  if (bundlerForThatType) {
23337
23547
  // first plugin to define a bundle hook wins
23338
- return;
23548
+ continue;
23339
23549
  }
23340
23550
  bundlers[type] = {
23341
23551
  plugin,
23342
23552
  bundleFunction: bundle[type],
23343
23553
  urlInfoMap: new Map(),
23344
23554
  };
23345
- });
23346
- });
23555
+ }
23556
+ }
23347
23557
  const addToBundlerIfAny = (rawUrlInfo) => {
23348
23558
  const bundler = bundlers[rawUrlInfo.type];
23349
23559
  if (bundler) {
@@ -23663,43 +23873,6 @@ const WEB_URL_CONVERTER = {
23663
23873
  },
23664
23874
  };
23665
23875
 
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
23876
  const createServerEventsDispatcher = () => {
23704
23877
  const clients = [];
23705
23878
  const MAX_CLIENTS = 100;
@@ -23795,6 +23968,105 @@ const createServerEventsDispatcher = () => {
23795
23968
  };
23796
23969
  };
23797
23970
 
23971
+ /*
23972
+ * This plugin is very special because it is here
23973
+ * to provide "serverEvents" used by other plugins
23974
+ */
23975
+
23976
+
23977
+ const serverEventsClientFileUrl = new URL(
23978
+ "./js/server_events_client.js",
23979
+ import.meta.url,
23980
+ ).href;
23981
+
23982
+ const jsenvPluginServerEvents = ({ clientAutoreload }) => {
23983
+ let serverEventsDispatcher;
23984
+
23985
+ const { clientServerEventsConfig } = clientAutoreload;
23986
+ const { logs = true } = clientServerEventsConfig;
23987
+
23988
+ return {
23989
+ name: "jsenv:server_events",
23990
+ appliesDuring: "dev",
23991
+ effect: ({ kitchenContext, otherPlugins }) => {
23992
+ const allServerEvents = {};
23993
+ for (const otherPlugin of otherPlugins) {
23994
+ const { serverEvents } = otherPlugin;
23995
+ if (!serverEvents) {
23996
+ continue;
23997
+ }
23998
+ for (const serverEventName of Object.keys(serverEvents)) {
23999
+ // we could throw on serverEvent name conflict
24000
+ // we could throw if serverEvents[serverEventName] is not a function
24001
+ allServerEvents[serverEventName] = serverEvents[serverEventName];
24002
+ }
24003
+ }
24004
+ const serverEventNames = Object.keys(allServerEvents);
24005
+ if (serverEventNames.length === 0) {
24006
+ return false;
24007
+ }
24008
+ serverEventsDispatcher = createServerEventsDispatcher();
24009
+ const onabort = () => {
24010
+ serverEventsDispatcher.destroy();
24011
+ };
24012
+ kitchenContext.signal.addEventListener("abort", onabort);
24013
+ for (const serverEventName of Object.keys(allServerEvents)) {
24014
+ const serverEventInfo = {
24015
+ ...kitchenContext,
24016
+ // serverEventsDispatcher variable is safe, we can disable esling warning
24017
+ // eslint-disable-next-line no-loop-func
24018
+ sendServerEvent: (data) => {
24019
+ if (!serverEventsDispatcher) {
24020
+ // this can happen if a plugin wants to send a server event but
24021
+ // server is closing or the plugin got destroyed but still wants to do things
24022
+ // if plugin code is correctly written it is never supposed to happen
24023
+ // because it means a plugin is still trying to do stuff after being destroyed
24024
+ return;
24025
+ }
24026
+ serverEventsDispatcher.dispatch({
24027
+ type: serverEventName,
24028
+ data,
24029
+ });
24030
+ },
24031
+ };
24032
+ const serverEventInit = allServerEvents[serverEventName];
24033
+ serverEventInit(serverEventInfo);
24034
+ }
24035
+ return () => {
24036
+ kitchenContext.signal.removeEventListener("abort", onabort);
24037
+ serverEventsDispatcher.destroy();
24038
+ serverEventsDispatcher = undefined;
24039
+ };
24040
+ },
24041
+ serveWebsocket: async ({ websocket, request }) => {
24042
+ if (request.headers["sec-websocket-protocol"] !== "jsenv") {
24043
+ return false;
24044
+ }
24045
+ serverEventsDispatcher.addWebsocket(websocket, request);
24046
+ return true;
24047
+ },
24048
+ transformUrlContent: {
24049
+ html: (urlInfo) => {
24050
+ const htmlAst = parseHtml({
24051
+ html: urlInfo.content,
24052
+ url: urlInfo.url,
24053
+ });
24054
+ injectJsenvScript(htmlAst, {
24055
+ src: serverEventsClientFileUrl,
24056
+ initCall: {
24057
+ callee: "window.__server_events__.setup",
24058
+ params: {
24059
+ logs,
24060
+ },
24061
+ },
24062
+ pluginName: "jsenv:server_events",
24063
+ });
24064
+ return stringifyHtmlAst(htmlAst);
24065
+ },
24066
+ },
24067
+ };
24068
+ };
24069
+
23798
24070
  const memoizeByFirstArgument = (compute) => {
23799
24071
  const urlCache = new Map();
23800
24072
 
@@ -23952,10 +24224,11 @@ const startDevServer = async ({
23952
24224
  });
23953
24225
 
23954
24226
  const serverStopCallbackSet = new Set();
23955
- const serverEventsDispatcher = createServerEventsDispatcher();
24227
+ const serverStopAbortController = new AbortController();
23956
24228
  serverStopCallbackSet.add(() => {
23957
- serverEventsDispatcher.destroy();
24229
+ serverStopAbortController.abort();
23958
24230
  });
24231
+ const serverStopAbortSignal = serverStopAbortController.signal;
23959
24232
  const kitchenCache = new Map();
23960
24233
 
23961
24234
  const finalServices = [];
@@ -24058,7 +24331,7 @@ const startDevServer = async ({
24058
24331
 
24059
24332
  kitchen = createKitchen({
24060
24333
  name: runtimeId,
24061
- signal,
24334
+ signal: serverStopAbortSignal,
24062
24335
  logLevel,
24063
24336
  rootDirectoryUrl: sourceDirectoryUrl,
24064
24337
  mainFilePath: sourceMainFilePath,
@@ -24067,6 +24340,7 @@ const startDevServer = async ({
24067
24340
  runtimeCompat,
24068
24341
  clientRuntimeCompat,
24069
24342
  plugins: [
24343
+ jsenvPluginServerEvents({ clientAutoreload }),
24070
24344
  ...plugins,
24071
24345
  ...getCorePlugins({
24072
24346
  rootDirectoryUrl: sourceDirectoryUrl,
@@ -24142,7 +24416,17 @@ const startDevServer = async ({
24142
24416
  for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
24143
24417
  const implicitUrlInfo =
24144
24418
  urlInfoCreated.graph.getUrlInfo(implicitUrl);
24145
- if (implicitUrlInfo && !implicitUrlInfo.isValid()) {
24419
+ if (!implicitUrlInfo) {
24420
+ continue;
24421
+ }
24422
+ if (implicitUrlInfo.content === undefined) {
24423
+ // happens when we explicitely load an url with a search param
24424
+ // - it creates an implicit url info to the url without params
24425
+ // - we never explicitely request the url without search param so it has no content
24426
+ // in that case the underlying urlInfo cannot be invalidate by the implicit
24427
+ continue;
24428
+ }
24429
+ if (!implicitUrlInfo.isValid()) {
24146
24430
  return false;
24147
24431
  }
24148
24432
  }
@@ -24161,41 +24445,6 @@ const startDevServer = async ({
24161
24445
  serverStopCallbackSet.add(() => {
24162
24446
  kitchen.pluginController.callHooks("destroy", kitchen.context);
24163
24447
  });
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
24448
  kitchenCache.set(runtimeId, kitchen);
24200
24449
  onKitchenCreated(kitchen);
24201
24450
  return kitchen;
@@ -24280,9 +24529,10 @@ const startDevServer = async ({
24280
24529
  // If they match jsenv bypass cooking and returns 304
24281
24530
  // This must not happen when a plugin uses "no-store" or "no-cache" as it means
24282
24531
  // 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
- ? {}
24532
+ ...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
24533
+ ? {
24534
+ "cache-control": "no-store", // for inline file we force no-store when parent is no-store
24535
+ }
24286
24536
  : {
24287
24537
  "cache-control": `private,max-age=0,must-revalidate`,
24288
24538
  // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
@@ -24383,13 +24633,20 @@ ${error.trace?.message}`);
24383
24633
  };
24384
24634
  }
24385
24635
  },
24386
- handleWebsocket: (websocket, { request }) => {
24636
+ handleWebsocket: async (websocket, { request }) => {
24387
24637
  // if (true || logLevel === "debug") {
24388
24638
  // console.log("handleWebsocket", websocket, request.headers);
24389
24639
  // }
24390
- if (request.headers["sec-websocket-protocol"] === "jsenv") {
24391
- serverEventsDispatcher.addWebsocket(websocket, request);
24392
- }
24640
+ const kitchen = getOrCreateKitchen(request);
24641
+ const serveWebsocketHookInfo = {
24642
+ request,
24643
+ websocket,
24644
+ context: kitchen.context,
24645
+ };
24646
+ await kitchen.pluginController.callAsyncHooksUntil(
24647
+ "serveWebsocket",
24648
+ serveWebsocketHookInfo,
24649
+ );
24393
24650
  },
24394
24651
  });
24395
24652
  }
@@ -24483,6 +24740,13 @@ ${error.trace?.message}`);
24483
24740
  };
24484
24741
  };
24485
24742
 
24743
+ const cacheIsDisabledInResponseHeader = (urlInfo) => {
24744
+ return (
24745
+ urlInfo.headers["cache-control"] === "no-store" ||
24746
+ urlInfo.headers["cache-control"] === "no-cache"
24747
+ );
24748
+ };
24749
+
24486
24750
  /*
24487
24751
  * startBuildServer is mean to interact with the build files;
24488
24752
  * files that will be deployed to production server(s).