@jsenv/core 41.0.6 → 41.1.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.
@@ -6595,6 +6595,7 @@ const browserDefaultRuntimeCompat = {
6595
6595
  edge: "79",
6596
6596
  firefox: "67",
6597
6597
  ios: "12",
6598
+ ios_safari: "12",
6598
6599
  opera: "51",
6599
6600
  safari: "11.3",
6600
6601
  samsung: "9.2",
@@ -6696,6 +6697,7 @@ const featuresCompatMap = {
6696
6697
  safari: "10.1",
6697
6698
  opera: "48",
6698
6699
  ios: "10.3",
6700
+ ios_safari: "10.3",
6699
6701
  android: "61",
6700
6702
  samsung: "8.2",
6701
6703
  },
@@ -6715,6 +6717,7 @@ const featuresCompatMap = {
6715
6717
  edge: "79",
6716
6718
  firefox: "62",
6717
6719
  ios: "12",
6720
+ ios_safari: "12",
6718
6721
  opera: "51",
6719
6722
  safari: "11.1",
6720
6723
  samsung: "9.2",
@@ -6732,6 +6735,7 @@ const featuresCompatMap = {
6732
6735
  edge: "79",
6733
6736
  firefox: "67",
6734
6737
  ios: "11.3",
6738
+ ios_safari: "11.3",
6735
6739
  opera: "50",
6736
6740
  safari: "11.3",
6737
6741
  samsung: "8.0",
@@ -6745,6 +6749,7 @@ const featuresCompatMap = {
6745
6749
  safari: "15",
6746
6750
  samsung: "15",
6747
6751
  ios: "15",
6752
+ ios_safari: "15",
6748
6753
  node: "14.8",
6749
6754
  },
6750
6755
  // https://caniuse.com/import-maps
@@ -6780,6 +6785,7 @@ const featuresCompatMap = {
6780
6785
  opera: "11.5",
6781
6786
  safari: "4",
6782
6787
  ios: "5",
6788
+ ios_safari: "5",
6783
6789
  android: "4.4",
6784
6790
  },
6785
6791
  // https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#browser_compatibility
@@ -6797,6 +6803,7 @@ const featuresCompatMap = {
6797
6803
  safari: "11.1",
6798
6804
  opera: "27",
6799
6805
  ios: "11.3",
6806
+ ios_safari: "11.3",
6800
6807
  android: "12.12",
6801
6808
  },
6802
6809
  service_worker_type_module: {
@@ -6826,6 +6833,7 @@ const featuresCompatMap = {
6826
6833
  safari: "12.1",
6827
6834
  opera: "58",
6828
6835
  ios: "12.2",
6836
+ ios_safari: "12.2",
6829
6837
  android: "94",
6830
6838
  node: "12",
6831
6839
  },
@@ -6837,6 +6845,7 @@ const featuresCompatMap = {
6837
6845
  safari: "12",
6838
6846
  node: "10",
6839
6847
  ios: "12",
6848
+ ios_safari: "12",
6840
6849
  samsung: "8",
6841
6850
  electron: "3",
6842
6851
  },
@@ -6848,6 +6857,7 @@ const featuresCompatMap = {
6848
6857
  opera: "28",
6849
6858
  safari: "9",
6850
6859
  ios: "9",
6860
+ ios_safari: "9",
6851
6861
  android: "4",
6852
6862
  node: "4",
6853
6863
  },
@@ -6859,6 +6869,7 @@ const featuresCompatMap = {
6859
6869
  safari: "10",
6860
6870
  node: "6",
6861
6871
  ios: "10",
6872
+ ios_safari: "10",
6862
6873
  samsung: "5",
6863
6874
  electron: "0.36",
6864
6875
  },
@@ -6871,6 +6882,7 @@ const featuresCompatMap = {
6871
6882
  node: "4",
6872
6883
  ie: "11",
6873
6884
  ios: "10",
6885
+ ios_safari: "10",
6874
6886
  samsung: "3.4",
6875
6887
  electron: "0.22",
6876
6888
  },
@@ -6882,6 +6894,7 @@ const featuresCompatMap = {
6882
6894
  safari: "9",
6883
6895
  node: "4",
6884
6896
  ios: "9",
6897
+ ios_safari: "9",
6885
6898
  samsung: "4",
6886
6899
  electron: "0.28",
6887
6900
  },
@@ -6895,6 +6908,7 @@ const featuresCompatMap = {
6895
6908
  ie: "9",
6896
6909
  android: "4.4",
6897
6910
  ios: "6",
6911
+ ios_safari: "6",
6898
6912
  phantom: "2",
6899
6913
  samsung: "1",
6900
6914
  electron: "0.20",
@@ -6906,6 +6920,7 @@ const featuresCompatMap = {
6906
6920
  firefox: "36",
6907
6921
  safari: "9",
6908
6922
  ios: "9",
6923
+ ios_safari: "9",
6909
6924
  samsung: "4",
6910
6925
  node: "0.12",
6911
6926
  },
@@ -6992,7 +7007,7 @@ const inferRuntimeCompatFromClosestPackage = async (
6992
7007
  for (const browserNameAndVersion of browserslistConfig) {
6993
7008
  let [name, version] = browserNameAndVersion.split(" ");
6994
7009
  if (name === "ios_saf") {
6995
- name = "ios";
7010
+ name = "ios_safari";
6996
7011
  }
6997
7012
  if (Object.keys(browserDefaultRuntimeCompat).includes(name)) {
6998
7013
  runtimeCompat[name] = version;
@@ -5897,6 +5897,7 @@ const featuresCompatMap = {
5897
5897
  safari: "10.1",
5898
5898
  opera: "48",
5899
5899
  ios: "10.3",
5900
+ ios_safari: "10.3",
5900
5901
  android: "61",
5901
5902
  samsung: "8.2",
5902
5903
  },
@@ -5916,6 +5917,7 @@ const featuresCompatMap = {
5916
5917
  edge: "79",
5917
5918
  firefox: "62",
5918
5919
  ios: "12",
5920
+ ios_safari: "12",
5919
5921
  opera: "51",
5920
5922
  safari: "11.1",
5921
5923
  samsung: "9.2",
@@ -5933,6 +5935,7 @@ const featuresCompatMap = {
5933
5935
  edge: "79",
5934
5936
  firefox: "67",
5935
5937
  ios: "11.3",
5938
+ ios_safari: "11.3",
5936
5939
  opera: "50",
5937
5940
  safari: "11.3",
5938
5941
  samsung: "8.0",
@@ -5946,6 +5949,7 @@ const featuresCompatMap = {
5946
5949
  safari: "15",
5947
5950
  samsung: "15",
5948
5951
  ios: "15",
5952
+ ios_safari: "15",
5949
5953
  node: "14.8",
5950
5954
  },
5951
5955
  // https://caniuse.com/import-maps
@@ -5981,6 +5985,7 @@ const featuresCompatMap = {
5981
5985
  opera: "11.5",
5982
5986
  safari: "4",
5983
5987
  ios: "5",
5988
+ ios_safari: "5",
5984
5989
  android: "4.4",
5985
5990
  },
5986
5991
  // https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#browser_compatibility
@@ -5998,6 +6003,7 @@ const featuresCompatMap = {
5998
6003
  safari: "11.1",
5999
6004
  opera: "27",
6000
6005
  ios: "11.3",
6006
+ ios_safari: "11.3",
6001
6007
  android: "12.12",
6002
6008
  },
6003
6009
  service_worker_type_module: {
@@ -6027,6 +6033,7 @@ const featuresCompatMap = {
6027
6033
  safari: "12.1",
6028
6034
  opera: "58",
6029
6035
  ios: "12.2",
6036
+ ios_safari: "12.2",
6030
6037
  android: "94",
6031
6038
  node: "12",
6032
6039
  },
@@ -6038,6 +6045,7 @@ const featuresCompatMap = {
6038
6045
  safari: "12",
6039
6046
  node: "10",
6040
6047
  ios: "12",
6048
+ ios_safari: "12",
6041
6049
  samsung: "8",
6042
6050
  electron: "3",
6043
6051
  },
@@ -6049,6 +6057,7 @@ const featuresCompatMap = {
6049
6057
  opera: "28",
6050
6058
  safari: "9",
6051
6059
  ios: "9",
6060
+ ios_safari: "9",
6052
6061
  android: "4",
6053
6062
  node: "4",
6054
6063
  },
@@ -6060,6 +6069,7 @@ const featuresCompatMap = {
6060
6069
  safari: "10",
6061
6070
  node: "6",
6062
6071
  ios: "10",
6072
+ ios_safari: "10",
6063
6073
  samsung: "5",
6064
6074
  electron: "0.36",
6065
6075
  },
@@ -6072,6 +6082,7 @@ const featuresCompatMap = {
6072
6082
  node: "4",
6073
6083
  ie: "11",
6074
6084
  ios: "10",
6085
+ ios_safari: "10",
6075
6086
  samsung: "3.4",
6076
6087
  electron: "0.22",
6077
6088
  },
@@ -6083,6 +6094,7 @@ const featuresCompatMap = {
6083
6094
  safari: "9",
6084
6095
  node: "4",
6085
6096
  ios: "9",
6097
+ ios_safari: "9",
6086
6098
  samsung: "4",
6087
6099
  electron: "0.28",
6088
6100
  },
@@ -6096,6 +6108,7 @@ const featuresCompatMap = {
6096
6108
  ie: "9",
6097
6109
  android: "4.4",
6098
6110
  ios: "6",
6111
+ ios_safari: "6",
6099
6112
  phantom: "2",
6100
6113
  samsung: "1",
6101
6114
  electron: "0.20",
@@ -6107,6 +6120,7 @@ const featuresCompatMap = {
6107
6120
  firefox: "36",
6108
6121
  safari: "9",
6109
6122
  ios: "9",
6123
+ ios_safari: "9",
6110
6124
  samsung: "4",
6111
6125
  node: "0.12",
6112
6126
  },
@@ -6286,24 +6300,4 @@ const isResponseEligibleForIntegrityValidation = (response) => {
6286
6300
  return ["basic", "cors", "default"].includes(response.type);
6287
6301
  };
6288
6302
 
6289
- const memoizeByFirstArgument = (compute) => {
6290
- const urlCache = new Map();
6291
-
6292
- const fnWithMemoization = (url, ...args) => {
6293
- const valueFromCache = urlCache.get(url);
6294
- if (valueFromCache) {
6295
- return valueFromCache;
6296
- }
6297
- const value = compute(url, ...args);
6298
- urlCache.set(url, value);
6299
- return value;
6300
- };
6301
-
6302
- fnWithMemoization.forget = () => {
6303
- urlCache.clear();
6304
- };
6305
-
6306
- return fnWithMemoization;
6307
- };
6308
-
6309
- export { ANSI, CONTENT_TYPE, DATA_URL, JS_QUOTES, RUNTIME_COMPAT, URL_META, applyFileSystemMagicResolution, applyNodeEsmResolution, asSpecifierWithoutSearch, asUrlWithoutSearch, assertAndNormalizeDirectoryUrl, bufferToEtag, compareFileUrls, composeTwoImportMaps, createDetailedMessage$1 as createDetailedMessage, createLogger, createTaskLog, ensurePathnameTrailingSlash, ensureWindowsDriveLetter, errorToHTML, formatError, generateContentFrame, getCallerPosition, getExtensionsToTry, injectQueryParams, injectQueryParamsIntoSpecifier, isFileSystemPath, isSpecifierForNodeBuiltin, lookupPackageDirectory, memoizeByFirstArgument, moveUrl, normalizeImportMap, normalizeUrl, readCustomConditionsFromProcessArgs, readEntryStatSync, readPackageAtOrNull, registerDirectoryLifecycle, resolveImport, setUrlBasename, setUrlExtension, setUrlFilename, stringifyUrlSite, urlIsOrIsInsideOf, urlToBasename, urlToExtension$1 as urlToExtension, urlToFileSystemPath, urlToFilename$1 as urlToFilename, urlToPathname$1 as urlToPathname, urlToRelativeUrl, validateResponseIntegrity, writeFileSync };
6303
+ export { ANSI, CONTENT_TYPE, DATA_URL, JS_QUOTES, RUNTIME_COMPAT, URL_META, applyFileSystemMagicResolution, applyNodeEsmResolution, asSpecifierWithoutSearch, asUrlWithoutSearch, assertAndNormalizeDirectoryUrl, bufferToEtag, compareFileUrls, composeTwoImportMaps, createDetailedMessage$1 as createDetailedMessage, createLogger, createTaskLog, ensurePathnameTrailingSlash, ensureWindowsDriveLetter, errorToHTML, formatError, generateContentFrame, getCallerPosition, getExtensionsToTry, injectQueryParams, injectQueryParamsIntoSpecifier, isFileSystemPath, isSpecifierForNodeBuiltin, lookupPackageDirectory, moveUrl, normalizeImportMap, normalizeUrl, readCustomConditionsFromProcessArgs, readEntryStatSync, readPackageAtOrNull, registerDirectoryLifecycle, resolveImport, setUrlBasename, setUrlExtension, setUrlFilename, stringifyUrlSite, urlIsOrIsInsideOf, urlToBasename, urlToExtension$1 as urlToExtension, urlToFileSystemPath, urlToFilename$1 as urlToFilename, urlToPathname$1 as urlToPathname, urlToRelativeUrl, validateResponseIntegrity, writeFileSync };
@@ -1,6 +1,6 @@
1
1
  import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler, composeTwoResponses, fetchDirectory, serverPluginCORS, jsenvAccessControlAllowedHeaders, startServer } from "@jsenv/server";
2
2
  import { readFileSync, existsSync, readdirSync, lstatSync, realpathSync } from "node:fs";
3
- import { lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, urlToRelativeUrl, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, registerDirectoryLifecycle, asUrlWithoutSearch, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, urlToFilename, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, createDetailedMessage, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, memoizeByFirstArgument, formatError, assertAndNormalizeDirectoryUrl, createTaskLog } from "./jsenv_core_packages.js";
3
+ import { lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, urlToRelativeUrl, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, registerDirectoryLifecycle, asUrlWithoutSearch, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, urlToFilename, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, createDetailedMessage, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, formatError, assertAndNormalizeDirectoryUrl, createTaskLog } from "./jsenv_core_packages.js";
4
4
  import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
5
5
  import { parseHtml, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, stringifyHtmlAst, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, injectJsenvScript, applyBabelPlugins, analyzeLinkNode, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn } from "@jsenv/ast";
6
6
  import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
@@ -10,7 +10,6 @@ import { pathToFileURL } from "node:url";
10
10
  import { bundleJsModules } from "@jsenv/plugin-bundling";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/plugins/filesystem/filesystem_error_to_response.js";
13
- import { createRequire } from "node:module";
14
13
  import "./jsenv_core_node_modules.js";
15
14
  import "node:process";
16
15
  import "node:os";
@@ -9560,9 +9559,80 @@ const inferUrlInfoType = (urlInfo) => {
9560
9559
  return expectedType || "other";
9561
9560
  };
9562
9561
 
9563
- const requireFromJsenv = createRequire(import.meta.url);
9562
+ const runtimeBySecChUa = new Map();
9563
+ const runtimeByUserAgent = new Map();
9564
9564
 
9565
- const parseUserAgentHeader = memoizeByFirstArgument((userAgent) => {
9565
+ const getRuntimeFromRequest = (request) => {
9566
+ const secChUa = request.headers["sec-ch-ua"];
9567
+ if (secChUa) {
9568
+ const cached = runtimeBySecChUa.get(secChUa);
9569
+ if (cached) {
9570
+ return cached;
9571
+ }
9572
+ const result = parseSecChUaHeader(secChUa);
9573
+ if (result) {
9574
+ runtimeBySecChUa.set(secChUa, result);
9575
+ return result;
9576
+ }
9577
+ }
9578
+ const userAgent = request.headers["user-agent"] || "";
9579
+ const cached = runtimeByUserAgent.get(userAgent);
9580
+ if (cached) {
9581
+ return cached;
9582
+ }
9583
+ const result = parseUserAgentHeader(userAgent);
9584
+ runtimeByUserAgent.set(userAgent, result);
9585
+ return result;
9586
+ };
9587
+
9588
+ const parseSecChUaHeader = (secChUa) => {
9589
+ // sec-ch-ua format: "Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"
9590
+ const brands = [];
9591
+ const regex = /"([^"]+)";v="([^"]+)"/g;
9592
+ let match;
9593
+ while ((match = regex.exec(secChUa)) !== null) {
9594
+ const name = match[1];
9595
+ const version = match[2];
9596
+ // skip "Not X;Brand" noise entries
9597
+ if (!name.includes("Not") && !name.includes("Brand")) {
9598
+ brands.push({ name, version });
9599
+ }
9600
+ }
9601
+ if (brands.length === 0) {
9602
+ return null;
9603
+ }
9604
+ // Prefer the non-Chromium brand (e.g. "Google Chrome", "Microsoft Edge")
9605
+ // Fall back to "Chromium" if no specific brand found
9606
+ let brand = brands.find((b) => b.name !== "Chromium");
9607
+ if (!brand) {
9608
+ brand = brands[0];
9609
+ }
9610
+ const runtimeName = brandNameToRuntimeName(brand.name);
9611
+ const runtimeVersion = brand.version;
9612
+ return { runtimeName, runtimeVersion };
9613
+ };
9614
+
9615
+ const brandNameToRuntimeName = (brandName) => {
9616
+ const lower = brandName.toLowerCase();
9617
+ if (lower === "google chrome") {
9618
+ return "chrome";
9619
+ }
9620
+ if (lower === "microsoft edge") {
9621
+ return "edge";
9622
+ }
9623
+ if (lower === "opera") {
9624
+ return "opera";
9625
+ }
9626
+ if (lower === "samsung internet") {
9627
+ return "samsung";
9628
+ }
9629
+ if (lower === "chromium") {
9630
+ return "chrome";
9631
+ }
9632
+ return lower;
9633
+ };
9634
+
9635
+ const parseUserAgentHeader = (userAgent) => {
9566
9636
  if (userAgent.includes("node-fetch/")) {
9567
9637
  // it's not really node and conceptually we can't assume the node version
9568
9638
  // but good enough for now
@@ -9571,15 +9641,46 @@ const parseUserAgentHeader = memoizeByFirstArgument((userAgent) => {
9571
9641
  runtimeVersion: process.version.slice(1),
9572
9642
  };
9573
9643
  }
9574
- const UA = requireFromJsenv("@financial-times/polyfill-useragent-normaliser");
9575
- const { ua } = new UA(userAgent);
9576
- const { family, major, minor, patch } = ua;
9577
- return {
9578
- runtimeName: family.toLowerCase(),
9579
- runtimeVersion:
9580
- family === "Other" ? "unknown" : `${major}.${minor}${patch}`,
9581
- };
9582
- });
9644
+ // iOS Safari must be checked before Safari (UA contains both)
9645
+ if (userAgent.includes("Mobile") && userAgent.includes("Safari")) {
9646
+ const iosSafariMatch = userAgent.match(/\bOS (\d+)[._](\d+)(?:[._](\d+))?/);
9647
+ if (iosSafariMatch) {
9648
+ const major = iosSafariMatch[1];
9649
+ const minor = iosSafariMatch[2] || "0";
9650
+ const patch = iosSafariMatch[3] || "0";
9651
+ return {
9652
+ runtimeName: "ios_safari",
9653
+ runtimeVersion: `${major}.${minor}.${patch}`,
9654
+ };
9655
+ }
9656
+ }
9657
+ if (!userAgent.includes("Chrome") && userAgent.includes("Safari")) {
9658
+ const safariMatch = userAgent.match(/\bVersion\/(\d+)\.(\d+)(?:\.(\d+))?/);
9659
+ if (safariMatch) {
9660
+ const major = safariMatch[1];
9661
+ const minor = safariMatch[2] || "0";
9662
+ const patch = safariMatch[3] || "0";
9663
+ return {
9664
+ runtimeName: "safari",
9665
+ runtimeVersion: `${major}.${minor}.${patch}`,
9666
+ };
9667
+ }
9668
+ }
9669
+ const firefoxMatch = userAgent.match(/\bFirefox\/(\d+)\.(\d+)\b/);
9670
+ if (firefoxMatch) {
9671
+ const major = firefoxMatch[1];
9672
+ const minor = firefoxMatch[2] || "0";
9673
+ return { runtimeName: "firefox", runtimeVersion: `${major}.${minor}.0` };
9674
+ }
9675
+ // generic Chromium-based fallback (should normally be handled by sec-ch-ua)
9676
+ const chromeMatch = userAgent.match(/\bChrome\/(\d+)\.(\d+)\b/);
9677
+ if (chromeMatch) {
9678
+ const major = chromeMatch[1];
9679
+ const minor = chromeMatch[2] || "0";
9680
+ return { runtimeName: "chrome", runtimeVersion: `${major}.${minor}.0` };
9681
+ }
9682
+ return { runtimeName: "unknown", runtimeVersion: "unknown" };
9683
+ };
9583
9684
 
9584
9685
  const devServerPluginServeSourceFiles = ({
9585
9686
  packageDirectory,
@@ -9620,9 +9721,7 @@ const devServerPluginServeSourceFiles = ({
9620
9721
  serverStopCallbackSet.add(stopWatchingSourceFiles);
9621
9722
 
9622
9723
  const getOrCreateKitchen = async (request) => {
9623
- const { runtimeName, runtimeVersion } = parseUserAgentHeader(
9624
- request.headers["user-agent"] || "",
9625
- );
9724
+ const { runtimeName, runtimeVersion } = getRuntimeFromRequest(request);
9626
9725
  const runtimeId = `${runtimeName}@${runtimeVersion}`;
9627
9726
  const existing = kitchenCache.get(runtimeId);
9628
9727
  if (existing) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.0.6",
3
+ "version": "41.1.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -74,14 +74,13 @@
74
74
  "test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
75
75
  },
76
76
  "dependencies": {
77
- "@financial-times/polyfill-useragent-normaliser": "1.10.2",
78
77
  "@jsenv/ast": "6.7.21",
79
78
  "@jsenv/js-module-fallback": "1.4.29",
80
79
  "@jsenv/plugin-bundling": "2.10.9",
81
80
  "@jsenv/plugin-minification": "1.7.3",
82
81
  "@jsenv/plugin-supervisor": "1.8.0",
83
- "@jsenv/plugin-transpilation": "1.5.70",
84
- "@jsenv/server": "17.1.0",
82
+ "@jsenv/plugin-transpilation": "1.5.71",
83
+ "@jsenv/server": "17.1.1",
85
84
  "@jsenv/sourcemap": "1.3.17",
86
85
  "react-table": "7.8.0"
87
86
  },
@@ -8,8 +8,7 @@ import { watchSourceFiles } from "../../helpers/watch_source_files.js";
8
8
  import { WEB_URL_CONVERTER } from "../../helpers/web_url_converter.js";
9
9
  import { createKitchen } from "../../kitchen/kitchen.js";
10
10
  import { createJsenvPluginsController } from "../../plugins/jsenv_plugins_controller.js";
11
-
12
- import { parseUserAgentHeader } from "./user_agent.js";
11
+ import { getRuntimeFromRequest } from "./runtime_from_request.js";
13
12
 
14
13
  export const devServerPluginServeSourceFiles = ({
15
14
  packageDirectory,
@@ -50,9 +49,7 @@ export const devServerPluginServeSourceFiles = ({
50
49
  serverStopCallbackSet.add(stopWatchingSourceFiles);
51
50
 
52
51
  const getOrCreateKitchen = async (request) => {
53
- const { runtimeName, runtimeVersion } = parseUserAgentHeader(
54
- request.headers["user-agent"] || "",
55
- );
52
+ const { runtimeName, runtimeVersion } = getRuntimeFromRequest(request);
56
53
  const runtimeId = `${runtimeName}@${runtimeVersion}`;
57
54
  const existing = kitchenCache.get(runtimeId);
58
55
  if (existing) {
@@ -0,0 +1,122 @@
1
+ const runtimeBySecChUa = new Map();
2
+ const runtimeByUserAgent = new Map();
3
+
4
+ export const getRuntimeFromRequest = (request) => {
5
+ const secChUa = request.headers["sec-ch-ua"];
6
+ if (secChUa) {
7
+ const cached = runtimeBySecChUa.get(secChUa);
8
+ if (cached) {
9
+ return cached;
10
+ }
11
+ const result = parseSecChUaHeader(secChUa);
12
+ if (result) {
13
+ runtimeBySecChUa.set(secChUa, result);
14
+ return result;
15
+ }
16
+ }
17
+ const userAgent = request.headers["user-agent"] || "";
18
+ const cached = runtimeByUserAgent.get(userAgent);
19
+ if (cached) {
20
+ return cached;
21
+ }
22
+ const result = parseUserAgentHeader(userAgent);
23
+ runtimeByUserAgent.set(userAgent, result);
24
+ return result;
25
+ };
26
+
27
+ const parseSecChUaHeader = (secChUa) => {
28
+ // sec-ch-ua format: "Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"
29
+ const brands = [];
30
+ const regex = /"([^"]+)";v="([^"]+)"/g;
31
+ let match;
32
+ while ((match = regex.exec(secChUa)) !== null) {
33
+ const name = match[1];
34
+ const version = match[2];
35
+ // skip "Not X;Brand" noise entries
36
+ if (!name.includes("Not") && !name.includes("Brand")) {
37
+ brands.push({ name, version });
38
+ }
39
+ }
40
+ if (brands.length === 0) {
41
+ return null;
42
+ }
43
+ // Prefer the non-Chromium brand (e.g. "Google Chrome", "Microsoft Edge")
44
+ // Fall back to "Chromium" if no specific brand found
45
+ let brand = brands.find((b) => b.name !== "Chromium");
46
+ if (!brand) {
47
+ brand = brands[0];
48
+ }
49
+ const runtimeName = brandNameToRuntimeName(brand.name);
50
+ const runtimeVersion = brand.version;
51
+ return { runtimeName, runtimeVersion };
52
+ };
53
+
54
+ const brandNameToRuntimeName = (brandName) => {
55
+ const lower = brandName.toLowerCase();
56
+ if (lower === "google chrome") {
57
+ return "chrome";
58
+ }
59
+ if (lower === "microsoft edge") {
60
+ return "edge";
61
+ }
62
+ if (lower === "opera") {
63
+ return "opera";
64
+ }
65
+ if (lower === "samsung internet") {
66
+ return "samsung";
67
+ }
68
+ if (lower === "chromium") {
69
+ return "chrome";
70
+ }
71
+ return lower;
72
+ };
73
+
74
+ const parseUserAgentHeader = (userAgent) => {
75
+ if (userAgent.includes("node-fetch/")) {
76
+ // it's not really node and conceptually we can't assume the node version
77
+ // but good enough for now
78
+ return {
79
+ runtimeName: "node",
80
+ runtimeVersion: process.version.slice(1),
81
+ };
82
+ }
83
+ // iOS Safari must be checked before Safari (UA contains both)
84
+ if (userAgent.includes("Mobile") && userAgent.includes("Safari")) {
85
+ const iosSafariMatch = userAgent.match(/\bOS (\d+)[._](\d+)(?:[._](\d+))?/);
86
+ if (iosSafariMatch) {
87
+ const major = iosSafariMatch[1];
88
+ const minor = iosSafariMatch[2] || "0";
89
+ const patch = iosSafariMatch[3] || "0";
90
+ return {
91
+ runtimeName: "ios_safari",
92
+ runtimeVersion: `${major}.${minor}.${patch}`,
93
+ };
94
+ }
95
+ }
96
+ if (!userAgent.includes("Chrome") && userAgent.includes("Safari")) {
97
+ const safariMatch = userAgent.match(/\bVersion\/(\d+)\.(\d+)(?:\.(\d+))?/);
98
+ if (safariMatch) {
99
+ const major = safariMatch[1];
100
+ const minor = safariMatch[2] || "0";
101
+ const patch = safariMatch[3] || "0";
102
+ return {
103
+ runtimeName: "safari",
104
+ runtimeVersion: `${major}.${minor}.${patch}`,
105
+ };
106
+ }
107
+ }
108
+ const firefoxMatch = userAgent.match(/\bFirefox\/(\d+)\.(\d+)\b/);
109
+ if (firefoxMatch) {
110
+ const major = firefoxMatch[1];
111
+ const minor = firefoxMatch[2] || "0";
112
+ return { runtimeName: "firefox", runtimeVersion: `${major}.${minor}.0` };
113
+ }
114
+ // generic Chromium-based fallback (should normally be handled by sec-ch-ua)
115
+ const chromeMatch = userAgent.match(/\bChrome\/(\d+)\.(\d+)\b/);
116
+ if (chromeMatch) {
117
+ const major = chromeMatch[1];
118
+ const minor = chromeMatch[2] || "0";
119
+ return { runtimeName: "chrome", runtimeVersion: `${major}.${minor}.0` };
120
+ }
121
+ return { runtimeName: "unknown", runtimeVersion: "unknown" };
122
+ };
@@ -1,22 +0,0 @@
1
- import { memoizeByFirstArgument } from "@jsenv/utils/src/memoize/memoize_by_first_argument.js";
2
-
3
- import { requireFromJsenv } from "@jsenv/core/src/helpers/require_from_jsenv.js";
4
-
5
- export const parseUserAgentHeader = memoizeByFirstArgument((userAgent) => {
6
- if (userAgent.includes("node-fetch/")) {
7
- // it's not really node and conceptually we can't assume the node version
8
- // but good enough for now
9
- return {
10
- runtimeName: "node",
11
- runtimeVersion: process.version.slice(1),
12
- };
13
- }
14
- const UA = requireFromJsenv("@financial-times/polyfill-useragent-normaliser");
15
- const { ua } = new UA(userAgent);
16
- const { family, major, minor, patch } = ua;
17
- return {
18
- runtimeName: family.toLowerCase(),
19
- runtimeVersion:
20
- family === "Other" ? "unknown" : `${major}.${minor}${patch}`,
21
- };
22
- });