@jsenv/core 39.11.2 → 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 -757
  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 +6 -0
  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 -333
  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 -165
  33. package/dist/html/html_404_and_ancestor_dir.html +0 -203
  34. package/src/plugins/protocol_file/client/assets/directory.css +0 -133
  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,346 +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
20019
  let { fsStat } = firstReference;
19403
20020
  if (!fsStat) {
19404
20021
  fsStat = readEntryStatSync(urlInfo.url, { nullIfNotFound: true });
19405
20022
  }
19406
- const isDirectory = fsStat?.isDirectory();
19407
- const { rootDirectoryUrl, request } = urlInfo.context;
19408
- const serveFile = (url) => {
19409
- const contentType = CONTENT_TYPE.fromUrlExtension(url);
19410
- const fileBuffer = readFileSync(new URL(url));
19411
- const content = CONTENT_TYPE.isTextual(contentType)
19412
- ? String(fileBuffer)
19413
- : fileBuffer;
19414
- return {
19415
- content,
19416
- contentType,
19417
- contentLength: fileBuffer.length,
19418
- };
19419
- };
19420
-
19421
20023
  if (!fsStat) {
19422
- if (request && request.headers["sec-fetch-dest"] === "document") {
19423
- const directoryContentItems = generateDirectoryContentItems(
19424
- urlInfo.url,
19425
- rootDirectoryUrl,
19426
- );
19427
- const html = generateHtmlForENOENT(
19428
- urlInfo.url,
19429
- directoryContentItems,
19430
- directoryListingUrlMocks,
19431
- );
19432
- return {
19433
- status: 404,
19434
- contentType: "text/html",
19435
- content: html,
19436
- headers: {
19437
- "cache-control": "no-cache",
19438
- },
19439
- };
19440
- }
20024
+ return null;
19441
20025
  }
19442
- if (isDirectory) {
19443
- const directoryContentArray = readdirSync(new URL(urlInfo.url));
19444
- if (firstReference.type === "filesystem") {
19445
- const content = JSON.stringify(directoryContentArray, null, " ");
19446
- return {
19447
- type: "directory",
19448
- contentType: "application/json",
19449
- content,
19450
- };
19451
- }
19452
- const acceptsHtml = request
19453
- ? pickContentType(request, ["text/html"])
19454
- : false;
19455
- if (acceptsHtml) {
19456
- firstReference.expectedType = "html";
19457
- const directoryUrl = urlInfo.url;
19458
- const directoryContentItems = generateDirectoryContentItems(
19459
- directoryUrl,
19460
- rootDirectoryUrl,
19461
- );
19462
- const html = generateHtmlForDirectory(directoryContentItems);
19463
- return {
19464
- type: "html",
19465
- contentType: "text/html",
19466
- content: html,
19467
- };
19468
- }
19469
- return {
19470
- type: "directory",
19471
- contentType: "application/json",
19472
- content: JSON.stringify(directoryContentArray, null, " "),
19473
- };
20026
+ const isDirectory = fsStat.isDirectory();
20027
+ if (!isDirectory) {
20028
+ return null;
19474
20029
  }
19475
- 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
+ };
19476
20037
  },
19477
20038
  },
19478
- ];
19479
- };
19480
-
19481
- const generateHtmlForDirectory = (directoryContentItems) => {
19482
- let directoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19483
- const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19484
- directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
19485
-
19486
- const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
19487
- const replacers = {
19488
- directoryUrl,
19489
- directoryNav: () =>
19490
- generateDirectoryNav(directoryUrl, {
19491
- rootDirectoryUrl,
19492
- rootDirectoryUrlForServer:
19493
- directoryContentItems.rootDirectoryUrlForServer,
19494
- }),
19495
- directoryContent: () => generateDirectoryContent(directoryContentItems),
19496
- };
19497
- const html = replacePlaceholders$1(htmlForDirectory, replacers);
19498
- return html;
19499
- };
19500
- const generateHtmlForENOENT = (
19501
- url,
19502
- directoryContentItems,
19503
- directoryListingUrlMocks,
19504
- ) => {
19505
- const ancestorDirectoryUrl = directoryContentItems.firstExistingDirectoryUrl;
19506
- const rootDirectoryUrl = directoryContentItems.rootDirectoryUrl;
19507
-
19508
- const htmlFor404AndAncestorDir = String(
19509
- readFileSync(html404AndAncestorDirFileUrl),
19510
- );
19511
- const fileRelativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
19512
- const ancestorDirectoryRelativeUrl = urlToRelativeUrl(
19513
- ancestorDirectoryUrl,
19514
- rootDirectoryUrl,
19515
- );
19516
- const replacers = {
19517
- fileUrl: directoryListingUrlMocks
19518
- ? `@jsenv/core/${urlToRelativeUrl(url, jsenvCoreDirectoryUrl)}`
19519
- : url,
19520
- fileRelativeUrl,
19521
- ancestorDirectoryUrl,
19522
- ancestorDirectoryRelativeUrl,
19523
- ancestorDirectoryNav: () =>
19524
- generateDirectoryNav(ancestorDirectoryUrl, {
19525
- rootDirectoryUrl,
19526
- rootDirectoryUrlForServer:
19527
- directoryContentItems.rootDirectoryUrlForServer,
19528
- }),
19529
- ancestorDirectoryContent: () =>
19530
- generateDirectoryContent(directoryContentItems),
19531
- };
19532
- const html = replacePlaceholders$1(htmlFor404AndAncestorDir, replacers);
19533
- return html;
19534
- };
19535
- const generateDirectoryNav = (
19536
- entryDirectoryUrl,
19537
- { rootDirectoryUrl, rootDirectoryUrlForServer },
19538
- ) => {
19539
- const entryDirectoryRelativeUrl = urlToRelativeUrl(
19540
- entryDirectoryUrl,
19541
- rootDirectoryUrl,
19542
- );
19543
- const isDir =
19544
- entryDirectoryRelativeUrl === "" || entryDirectoryRelativeUrl.endsWith("/");
19545
- const rootDirectoryUrlName = urlToFilename$1(rootDirectoryUrl);
19546
- const items = [];
19547
- let dirPartsHtml = "";
19548
- const parts = entryDirectoryRelativeUrl
19549
- ? `${rootDirectoryUrlName}/${entryDirectoryRelativeUrl.slice(0, -1)}`.split(
19550
- "/",
19551
- )
19552
- : [rootDirectoryUrlName];
19553
- let i = 0;
19554
- while (i < parts.length) {
19555
- const part = parts[i];
19556
- const directoryRelativeUrl = `${parts.slice(1, i + 1).join("/")}`;
19557
- const directoryUrl =
19558
- directoryRelativeUrl === ""
19559
- ? rootDirectoryUrl
19560
- : new URL(`${directoryRelativeUrl}/`, rootDirectoryUrl).href;
19561
- let href =
19562
- directoryUrl === rootDirectoryUrlForServer ||
19563
- urlIsInsideOf(directoryUrl, rootDirectoryUrlForServer)
19564
- ? urlToRelativeUrl(directoryUrl, rootDirectoryUrlForServer)
19565
- : directoryUrl;
19566
- if (href === "") {
19567
- href = `/${directoryContentMagicName}`;
19568
- } else {
19569
- href = `/${href}`;
19570
- }
19571
- const text = part;
19572
- items.push({
19573
- href,
19574
- text,
19575
- });
19576
- i++;
19577
- }
19578
- i = 0;
19579
- for (const { href, text } of items) {
19580
- const isLastPart = i === items.length - 1;
19581
- if (isLastPart) {
19582
- dirPartsHtml += `
19583
- <span class="directory_nav_item" data-current>
19584
- ${text}
19585
- </span>`;
19586
- break;
19587
- }
19588
- dirPartsHtml += `
19589
- <a class="directory_nav_item" href="${href}">
19590
- ${text}
19591
- </a>`;
19592
- dirPartsHtml += `
19593
- <span class="directory_separator">/</span>`;
19594
- i++;
19595
- }
19596
- if (isDir) {
19597
- dirPartsHtml += `
19598
- <span class="directory_separator">/</span>`;
19599
- }
19600
- return dirPartsHtml;
19601
- };
19602
- const generateDirectoryContentItems = (
19603
- directoryUrl,
19604
- rootDirectoryUrlForServer,
19605
- ) => {
19606
- let firstExistingDirectoryUrl = new URL("./", directoryUrl);
19607
- while (!existsSync(firstExistingDirectoryUrl)) {
19608
- firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
19609
- if (!urlIsInsideOf(firstExistingDirectoryUrl, rootDirectoryUrlForServer)) {
19610
- firstExistingDirectoryUrl = new URL(rootDirectoryUrlForServer);
19611
- break;
19612
- }
19613
- }
19614
- const directoryContentArray = readdirSync(firstExistingDirectoryUrl);
19615
- const fileUrls = [];
19616
- for (const filename of directoryContentArray) {
19617
- const fileUrlObject = new URL(filename, firstExistingDirectoryUrl);
19618
- fileUrls.push(fileUrlObject);
19619
- }
19620
- let rootDirectoryUrl = rootDirectoryUrlForServer;
19621
- package_workspaces: {
19622
- const packageDirectoryUrl = lookupPackageDirectory(
19623
- rootDirectoryUrlForServer,
19624
- );
19625
- if (!packageDirectoryUrl) {
19626
- break package_workspaces;
19627
- }
19628
- if (String(packageDirectoryUrl) === String(rootDirectoryUrlForServer)) {
19629
- break package_workspaces;
19630
- }
19631
- rootDirectoryUrl = packageDirectoryUrl;
19632
- if (
19633
- String(firstExistingDirectoryUrl) === String(rootDirectoryUrlForServer)
19634
- ) {
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
-
19661
- const sortedUrls = [];
19662
- for (let fileUrl of fileUrls) {
19663
- if (lstatSync(fileUrl).isDirectory()) {
19664
- sortedUrls.push(ensurePathnameTrailingSlash(fileUrl));
19665
- } else {
19666
- sortedUrls.push(fileUrl);
19667
- }
19668
- }
19669
- sortedUrls.sort((a, b) => {
19670
- return comparePathnames(a.pathname, b.pathname);
19671
- });
19672
-
19673
- const items = [];
19674
- for (const sortedUrl of sortedUrls) {
19675
- const fileUrlRelativeToParent = urlToRelativeUrl(
19676
- sortedUrl,
19677
- firstExistingDirectoryUrl,
19678
- );
19679
- const fileUrlRelativeToServer = urlToRelativeUrl(
19680
- sortedUrl,
19681
- rootDirectoryUrlForServer,
19682
- );
19683
- const type = fileUrlRelativeToParent.endsWith("/") ? "dir" : "file";
19684
- items.push({
19685
- type,
19686
- fileUrlRelativeToParent,
19687
- fileUrlRelativeToServer,
19688
- });
19689
- }
19690
- items.rootDirectoryUrlForServer = rootDirectoryUrlForServer;
19691
- items.rootDirectoryUrl = rootDirectoryUrl;
19692
- items.firstExistingDirectoryUrl = firstExistingDirectoryUrl;
19693
- return items;
19694
- };
19695
- const generateDirectoryContent = (directoryContentItems) => {
19696
- if (directoryContentItems.length === 0) {
19697
- return `<p class="directory_empty_message">Directory is empty</p>`;
19698
- }
19699
- let html = `<ul class="directory_content">`;
19700
- for (const directoryContentItem of directoryContentItems) {
19701
- const { type, fileUrlRelativeToParent, fileUrlRelativeToServer } =
19702
- directoryContentItem;
19703
- let href = fileUrlRelativeToServer;
19704
- if (href === "") {
19705
- href = `${directoryContentMagicName}`;
19706
- }
19707
- html += `
19708
- <li class="directory_child" data-type="${type}">
19709
- <a href="/${href}">${fileUrlRelativeToParent}</a>
19710
- </li>`;
19711
- }
19712
- html += `\n </ul>`;
19713
- return html;
19714
- };
19715
- const replacePlaceholders$1 = (html, replacers) => {
19716
- return html.replace(/\$\{(\w+)\}/g, (match, name) => {
19717
- const replacer = replacers[name];
19718
- if (replacer === undefined) {
19719
- return match;
19720
- }
19721
- if (typeof replacer === "function") {
19722
- return replacer();
19723
- }
19724
- return replacer;
19725
- });
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
+ ];
19726
20068
  };
19727
20069
 
19728
20070
  const jsenvPluginProtocolHttp = ({ include }) => {
@@ -19779,10 +20121,11 @@ const jsenvPluginProtocolHttp = ({ include }) => {
19779
20121
  return fileUrl;
19780
20122
  },
19781
20123
  fetchUrlContent: async (urlInfo) => {
19782
- if (!urlInfo.originalUrl.startsWith("http")) {
20124
+ const originalUrl = urlInfo.originalUrl;
20125
+ if (!originalUrl.startsWith("http")) {
19783
20126
  return null;
19784
20127
  }
19785
- const response = await fetch(urlInfo.originalUrl);
20128
+ const response = await fetch(originalUrl);
19786
20129
  const responseStatus = response.status;
19787
20130
  if (responseStatus < 200 || responseStatus > 299) {
19788
20131
  throw new Error(`unexpected response status ${responseStatus}`);
@@ -19815,103 +20158,6 @@ const asValidFilename = (string) => {
19815
20158
  return string;
19816
20159
  };
19817
20160
 
19818
- const jsenvPluginInjections = (rawAssociations) => {
19819
- let resolvedAssociations;
19820
-
19821
- return {
19822
- name: "jsenv:injections",
19823
- appliesDuring: "*",
19824
- init: (context) => {
19825
- resolvedAssociations = URL_META.resolveAssociations(
19826
- { injectionsGetter: rawAssociations },
19827
- context.rootDirectoryUrl,
19828
- );
19829
- },
19830
- transformUrlContent: async (urlInfo) => {
19831
- const { injectionsGetter } = URL_META.applyAssociations({
19832
- url: asUrlWithoutSearch(urlInfo.url),
19833
- associations: resolvedAssociations,
19834
- });
19835
- if (!injectionsGetter) {
19836
- return null;
19837
- }
19838
- if (typeof injectionsGetter !== "function") {
19839
- throw new TypeError("injectionsGetter must be a function");
19840
- }
19841
- const injections = await injectionsGetter(urlInfo);
19842
- if (!injections) {
19843
- return null;
19844
- }
19845
- const keys = Object.keys(injections);
19846
- if (keys.length === 0) {
19847
- return null;
19848
- }
19849
- return replacePlaceholders(urlInfo.content, injections, urlInfo);
19850
- },
19851
- };
19852
- };
19853
-
19854
- const injectionSymbol = Symbol.for("jsenv_injection");
19855
- const INJECTIONS = {
19856
- optional: (value) => {
19857
- return { [injectionSymbol]: "optional", value };
19858
- },
19859
- };
19860
-
19861
- // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
19862
- const replacePlaceholders = (content, replacements, urlInfo) => {
19863
- const magicSource = createMagicSource(content);
19864
- for (const key of Object.keys(replacements)) {
19865
- let index = content.indexOf(key);
19866
- const replacement = replacements[key];
19867
- let isOptional;
19868
- let value;
19869
- if (replacement && replacement[injectionSymbol]) {
19870
- const valueBehindSymbol = replacement[injectionSymbol];
19871
- isOptional = valueBehindSymbol === "optional";
19872
- value = replacement.value;
19873
- } else {
19874
- value = replacement;
19875
- }
19876
- if (index === -1) {
19877
- if (!isOptional) {
19878
- urlInfo.context.logger.warn(
19879
- `placeholder "${key}" not found in ${urlInfo.url}.
19880
- --- suggestion a ---
19881
- Add "${key}" in that file.
19882
- --- suggestion b ---
19883
- Fix eventual typo in "${key}"?
19884
- --- suggestion c ---
19885
- Mark injection as optional using INJECTIONS.optional():
19886
- import { INJECTIONS } from "@jsenv/core";
19887
-
19888
- return {
19889
- "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
19890
- };`,
19891
- );
19892
- }
19893
- continue;
19894
- }
19895
-
19896
- while (index !== -1) {
19897
- const start = index;
19898
- const end = index + key.length;
19899
- magicSource.replace({
19900
- start,
19901
- end,
19902
- replacement:
19903
- urlInfo.type === "js_classic" ||
19904
- urlInfo.type === "js_module" ||
19905
- urlInfo.type === "html"
19906
- ? JSON.stringify(value, null, " ")
19907
- : value,
19908
- });
19909
- index = content.indexOf(key, end);
19910
- }
19911
- }
19912
- return magicSource.toContentAndSourcemap();
19913
- };
19914
-
19915
20161
  /*
19916
20162
  * Some code uses globals specific to Node.js in code meant to run in browsers...
19917
20163
  * This plugin will replace some node globals to things compatible with web:
@@ -21233,8 +21479,8 @@ const getCorePlugins = ({
21233
21479
  jsenvPluginReferenceAnalysis(referenceAnalysis),
21234
21480
  ...(injections ? [jsenvPluginInjections(injections)] : []),
21235
21481
  jsenvPluginTranspilation(transpilation),
21482
+ // "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
21236
21483
  ...(inlining ? [jsenvPluginInlining()] : []),
21237
- ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []), // after inline as it needs inline script to be cooked
21238
21484
 
21239
21485
  /* When resolving references the following applies by default:
21240
21486
  - http urls are resolved by jsenvPluginHttpUrls
@@ -21248,7 +21494,6 @@ const getCorePlugins = ({
21248
21494
  magicDirectoryIndex,
21249
21495
  directoryListingUrlMocks,
21250
21496
  }),
21251
-
21252
21497
  {
21253
21498
  name: "jsenv:resolve_root_as_main",
21254
21499
  appliesDuring: "*",
@@ -21267,12 +21512,14 @@ const getCorePlugins = ({
21267
21512
  : []),
21268
21513
  jsenvPluginWebResolution(),
21269
21514
  jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect),
21270
-
21271
21515
  jsenvPluginVersionSearchParam(),
21516
+
21517
+ // "jsenvPluginSupervisor" MUST be after "jsenvPluginInlining" as it needs inline script to be cooked
21518
+ ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []),
21519
+
21272
21520
  jsenvPluginCommonJsGlobals(),
21273
21521
  jsenvPluginImportMetaScenarios(),
21274
21522
  ...(scenarioPlaceholders ? [jsenvPluginGlobalScenarios()] : []),
21275
-
21276
21523
  jsenvPluginNodeRuntime({ runtimeCompat }),
21277
21524
 
21278
21525
  jsenvPluginImportMetaHot(),
@@ -21832,7 +22079,6 @@ const createBuildSpecifierManager = ({
21832
22079
  type: reference.type,
21833
22080
  expectedType: reference.expectedType,
21834
22081
  specifier: reference.specifier,
21835
- specifierPathname: reference.specifierPathname,
21836
22082
  specifierLine: reference.specifierLine,
21837
22083
  specifierColumn: reference.specifierColumn,
21838
22084
  specifierStart: reference.specifierStart,
@@ -23281,33 +23527,33 @@ build ${entryPointKeys.length} entry points`);
23281
23527
 
23282
23528
  const bundlers = {};
23283
23529
  {
23284
- rawKitchen.pluginController.plugins.forEach((plugin) => {
23530
+ for (const plugin of rawKitchen.pluginController.activePlugins) {
23285
23531
  const bundle = plugin.bundle;
23286
23532
  if (!bundle) {
23287
- return;
23533
+ continue;
23288
23534
  }
23289
23535
  if (typeof bundle !== "object") {
23290
23536
  throw new Error(
23291
23537
  `bundle must be an object, found "${bundle}" on plugin named "${plugin.name}"`,
23292
23538
  );
23293
23539
  }
23294
- Object.keys(bundle).forEach((type) => {
23540
+ for (const type of Object.keys(bundle)) {
23295
23541
  const bundleFunction = bundle[type];
23296
23542
  if (!bundleFunction) {
23297
- return;
23543
+ continue;
23298
23544
  }
23299
23545
  const bundlerForThatType = bundlers[type];
23300
23546
  if (bundlerForThatType) {
23301
23547
  // first plugin to define a bundle hook wins
23302
- return;
23548
+ continue;
23303
23549
  }
23304
23550
  bundlers[type] = {
23305
23551
  plugin,
23306
23552
  bundleFunction: bundle[type],
23307
23553
  urlInfoMap: new Map(),
23308
23554
  };
23309
- });
23310
- });
23555
+ }
23556
+ }
23311
23557
  const addToBundlerIfAny = (rawUrlInfo) => {
23312
23558
  const bundler = bundlers[rawUrlInfo.type];
23313
23559
  if (bundler) {
@@ -23627,43 +23873,6 @@ const WEB_URL_CONVERTER = {
23627
23873
  },
23628
23874
  };
23629
23875
 
23630
- /*
23631
- * This plugin is very special because it is here
23632
- * to provide "serverEvents" used by other plugins
23633
- */
23634
-
23635
-
23636
- const serverEventsClientFileUrl = new URL(
23637
- "./js/server_events_client.js",
23638
- import.meta.url,
23639
- ).href;
23640
-
23641
- const jsenvPluginServerEventsClientInjection = ({ logs = true }) => {
23642
- return {
23643
- name: "jsenv:server_events_client_injection",
23644
- appliesDuring: "*",
23645
- transformUrlContent: {
23646
- html: (urlInfo) => {
23647
- const htmlAst = parseHtml({
23648
- html: urlInfo.content,
23649
- url: urlInfo.url,
23650
- });
23651
- injectJsenvScript(htmlAst, {
23652
- src: serverEventsClientFileUrl,
23653
- initCall: {
23654
- callee: "window.__server_events__.setup",
23655
- params: {
23656
- logs,
23657
- },
23658
- },
23659
- pluginName: "jsenv:server_events_client_injection",
23660
- });
23661
- return stringifyHtmlAst(htmlAst);
23662
- },
23663
- },
23664
- };
23665
- };
23666
-
23667
23876
  const createServerEventsDispatcher = () => {
23668
23877
  const clients = [];
23669
23878
  const MAX_CLIENTS = 100;
@@ -23759,6 +23968,105 @@ const createServerEventsDispatcher = () => {
23759
23968
  };
23760
23969
  };
23761
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
+
23762
24070
  const memoizeByFirstArgument = (compute) => {
23763
24071
  const urlCache = new Map();
23764
24072
 
@@ -23916,10 +24224,11 @@ const startDevServer = async ({
23916
24224
  });
23917
24225
 
23918
24226
  const serverStopCallbackSet = new Set();
23919
- const serverEventsDispatcher = createServerEventsDispatcher();
24227
+ const serverStopAbortController = new AbortController();
23920
24228
  serverStopCallbackSet.add(() => {
23921
- serverEventsDispatcher.destroy();
24229
+ serverStopAbortController.abort();
23922
24230
  });
24231
+ const serverStopAbortSignal = serverStopAbortController.signal;
23923
24232
  const kitchenCache = new Map();
23924
24233
 
23925
24234
  const finalServices = [];
@@ -24022,7 +24331,7 @@ const startDevServer = async ({
24022
24331
 
24023
24332
  kitchen = createKitchen({
24024
24333
  name: runtimeId,
24025
- signal,
24334
+ signal: serverStopAbortSignal,
24026
24335
  logLevel,
24027
24336
  rootDirectoryUrl: sourceDirectoryUrl,
24028
24337
  mainFilePath: sourceMainFilePath,
@@ -24031,6 +24340,7 @@ const startDevServer = async ({
24031
24340
  runtimeCompat,
24032
24341
  clientRuntimeCompat,
24033
24342
  plugins: [
24343
+ jsenvPluginServerEvents({ clientAutoreload }),
24034
24344
  ...plugins,
24035
24345
  ...getCorePlugins({
24036
24346
  rootDirectoryUrl: sourceDirectoryUrl,
@@ -24106,7 +24416,17 @@ const startDevServer = async ({
24106
24416
  for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
24107
24417
  const implicitUrlInfo =
24108
24418
  urlInfoCreated.graph.getUrlInfo(implicitUrl);
24109
- 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()) {
24110
24430
  return false;
24111
24431
  }
24112
24432
  }
@@ -24125,41 +24445,6 @@ const startDevServer = async ({
24125
24445
  serverStopCallbackSet.add(() => {
24126
24446
  kitchen.pluginController.callHooks("destroy", kitchen.context);
24127
24447
  });
24128
- {
24129
- const allServerEvents = {};
24130
- kitchen.pluginController.plugins.forEach((plugin) => {
24131
- const { serverEvents } = plugin;
24132
- if (serverEvents) {
24133
- Object.keys(serverEvents).forEach((serverEventName) => {
24134
- // we could throw on serverEvent name conflict
24135
- // we could throw if serverEvents[serverEventName] is not a function
24136
- allServerEvents[serverEventName] = serverEvents[serverEventName];
24137
- });
24138
- }
24139
- });
24140
- const serverEventNames = Object.keys(allServerEvents);
24141
- if (serverEventNames.length > 0) {
24142
- Object.keys(allServerEvents).forEach((serverEventName) => {
24143
- const serverEventInfo = {
24144
- ...kitchen.context,
24145
- sendServerEvent: (data) => {
24146
- serverEventsDispatcher.dispatch({
24147
- type: serverEventName,
24148
- data,
24149
- });
24150
- },
24151
- };
24152
- const serverEventInit = allServerEvents[serverEventName];
24153
- serverEventInit(serverEventInfo);
24154
- });
24155
- kitchen.pluginController.unshiftPlugin(
24156
- jsenvPluginServerEventsClientInjection(
24157
- clientAutoreload.clientServerEventsConfig,
24158
- ),
24159
- );
24160
- }
24161
- }
24162
-
24163
24448
  kitchenCache.set(runtimeId, kitchen);
24164
24449
  onKitchenCreated(kitchen);
24165
24450
  return kitchen;
@@ -24244,9 +24529,10 @@ const startDevServer = async ({
24244
24529
  // If they match jsenv bypass cooking and returns 304
24245
24530
  // This must not happen when a plugin uses "no-store" or "no-cache" as it means
24246
24531
  // plugin logic wants to happens for every request to this url
24247
- ...(urlInfo.headers["cache-control"] === "no-store" ||
24248
- urlInfo.headers["cache-control"] === "no-cache"
24249
- ? {}
24532
+ ...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
24533
+ ? {
24534
+ "cache-control": "no-store", // for inline file we force no-store when parent is no-store
24535
+ }
24250
24536
  : {
24251
24537
  "cache-control": `private,max-age=0,must-revalidate`,
24252
24538
  // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
@@ -24347,13 +24633,20 @@ ${error.trace?.message}`);
24347
24633
  };
24348
24634
  }
24349
24635
  },
24350
- handleWebsocket: (websocket, { request }) => {
24636
+ handleWebsocket: async (websocket, { request }) => {
24351
24637
  // if (true || logLevel === "debug") {
24352
24638
  // console.log("handleWebsocket", websocket, request.headers);
24353
24639
  // }
24354
- if (request.headers["sec-websocket-protocol"] === "jsenv") {
24355
- serverEventsDispatcher.addWebsocket(websocket, request);
24356
- }
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
+ );
24357
24650
  },
24358
24651
  });
24359
24652
  }
@@ -24447,6 +24740,13 @@ ${error.trace?.message}`);
24447
24740
  };
24448
24741
  };
24449
24742
 
24743
+ const cacheIsDisabledInResponseHeader = (urlInfo) => {
24744
+ return (
24745
+ urlInfo.headers["cache-control"] === "no-store" ||
24746
+ urlInfo.headers["cache-control"] === "no-cache"
24747
+ );
24748
+ };
24749
+
24450
24750
  /*
24451
24751
  * startBuildServer is mean to interact with the build files;
24452
24752
  * files that will be deployed to production server(s).