@jsenv/core 25.1.1 → 25.2.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 (58) hide show
  1. package/dist/browser_runtime/browser_runtime_91c5a3b8.js.map +2 -2
  2. package/dist/build_manifest.js +4 -4
  3. package/dist/compile_proxy/asset-manifest.json +2 -2
  4. package/dist/compile_proxy/{compile_proxy_e3b0c442_809f35f7.js.map → compile_proxy.html__inline__20_809f35f7.js.map} +0 -0
  5. package/dist/compile_proxy/{compile_proxy_7ad5faa6.html → compile_proxy_8dfaee51.html} +3 -4
  6. package/dist/redirector/asset-manifest.json +2 -2
  7. package/dist/redirector/{redirector_e3b0c442_e391410e.js.map → redirector.html__inline__15_e391410e.js.map} +0 -0
  8. package/dist/redirector/{redirector_eb92e8a7.html → redirector_3e9a97b9.html} +3 -4
  9. package/dist/toolbar/asset-manifest.json +1 -1
  10. package/dist/toolbar/{toolbar_f7b8a263.html → toolbar_361afb84.html} +2 -3
  11. package/dist/toolbar_injector/asset-manifest.json +2 -2
  12. package/dist/toolbar_injector/{toolbar_injector_49e4756e.js → toolbar_injector_fac1e995.js} +2 -2
  13. package/dist/toolbar_injector/{toolbar_injector_49e4756e.js.map → toolbar_injector_fac1e995.js.map} +2 -2
  14. package/package.json +7 -7
  15. package/readme.md +43 -49
  16. package/src/buildProject.js +21 -13
  17. package/src/commonJsToJavaScriptModule.js +8 -7
  18. package/src/execute.js +2 -0
  19. package/src/executeTestPlan.js +4 -1
  20. package/src/internal/building/buildUsingRollup.js +4 -2
  21. package/src/internal/building/build_stats.js +3 -0
  22. package/src/internal/building/build_url_generator.js +153 -0
  23. package/src/internal/building/css/parseCssRessource.js +32 -26
  24. package/src/internal/building/html/parseHtmlRessource.js +92 -68
  25. package/src/internal/building/js/parseJsRessource.js +4 -7
  26. package/src/internal/building/parseRessource.js +3 -0
  27. package/src/internal/building/ressource_builder.js +64 -62
  28. package/src/internal/building/ressource_builder_util.js +17 -5
  29. package/src/internal/building/rollup_plugin_jsenv.js +259 -189
  30. package/src/internal/building/url_fetcher.js +16 -7
  31. package/src/internal/building/url_loader.js +1 -5
  32. package/src/internal/building/url_versioning.js +0 -173
  33. package/src/internal/compiling/babel_plugin_import_metadata.js +7 -11
  34. package/src/internal/compiling/babel_plugin_proxy_external_imports.js +31 -0
  35. package/src/internal/compiling/compile-directory/compile-asset.js +8 -4
  36. package/src/internal/compiling/compile-directory/getOrGenerateCompiledFile.js +43 -8
  37. package/src/internal/compiling/compile-directory/updateMeta.js +2 -8
  38. package/src/internal/compiling/compile-directory/validateCache.js +1 -2
  39. package/src/internal/compiling/compileFile.js +22 -10
  40. package/src/internal/compiling/createCompiledFileService.js +22 -24
  41. package/src/internal/compiling/html_source_file_service.js +9 -9
  42. package/src/internal/compiling/js-compilation-service/jsenvTransform.js +14 -4
  43. package/src/internal/compiling/js-compilation-service/transformJs.js +9 -5
  44. package/src/internal/compiling/jsenvCompilerForHtml.js +221 -182
  45. package/src/internal/compiling/jsenvCompilerForJavaScript.js +15 -11
  46. package/src/internal/compiling/startCompileServer.js +79 -19
  47. package/src/internal/compiling/transformResultToCompilationResult.js +47 -25
  48. package/src/internal/executing/executePlan.js +2 -0
  49. package/src/internal/fetchUrl.js +3 -2
  50. package/src/internal/integrity/integrity_algorithms.js +26 -0
  51. package/src/internal/integrity/integrity_parsing.js +50 -0
  52. package/src/internal/integrity/integrity_update.js +23 -0
  53. package/src/internal/integrity/integrity_validation.js +49 -0
  54. package/src/internal/jsenv_remote_directory.js +156 -0
  55. package/src/internal/origin_directory_converter.js +62 -0
  56. package/src/internal/response_validation.js +11 -24
  57. package/src/internal/sourceMappingURLUtils.js +10 -0
  58. package/src/internal/url_conversion.js +1 -0
@@ -1,12 +1,13 @@
1
+ import { generateSourcemapUrl } from "@jsenv/core/src/internal/sourceMappingURLUtils.js"
2
+
1
3
  import { transformJs } from "./js-compilation-service/transformJs.js"
2
4
  import { transformResultToCompilationResult } from "./transformResultToCompilationResult.js"
3
5
 
4
6
  export const compileJavascript = async ({
5
- code,
6
- map,
7
+ projectDirectoryUrl,
8
+ jsenvRemoteDirectory,
7
9
  url,
8
10
  compiledUrl,
9
- projectDirectoryUrl,
10
11
 
11
12
  babelPluginMap,
12
13
  workerUrls,
@@ -16,6 +17,8 @@ export const compileJavascript = async ({
16
17
  topLevelAwait,
17
18
  prependSystemJs,
18
19
 
20
+ code,
21
+ map,
19
22
  sourcemapExcludeSources,
20
23
  sourcemapMethod,
21
24
  }) => {
@@ -23,38 +26,39 @@ export const compileJavascript = async ({
23
26
  prependSystemJs =
24
27
  workerUrls.includes(url) || serviceWorkerUrls.includes(url)
25
28
  }
26
-
27
29
  const transformResult = await transformJs({
28
- code,
29
- map,
30
+ projectDirectoryUrl,
31
+ jsenvRemoteDirectory,
30
32
  url,
31
33
  compiledUrl,
32
- projectDirectoryUrl,
33
34
 
34
35
  babelPluginMap,
35
36
  moduleOutFormat,
36
37
  importMetaFormat,
37
38
  topLevelAwait,
38
39
  prependSystemJs,
39
- })
40
40
 
41
+ code,
42
+ map,
43
+ })
41
44
  return transformResultToCompilationResult(
42
45
  {
43
46
  contentType: "application/javascript",
47
+ metadata: transformResult.metadata,
44
48
  code: transformResult.code,
45
49
  map: transformResult.map,
46
- metadata: transformResult.metadata,
47
50
  },
48
51
  {
49
52
  projectDirectoryUrl,
50
- originalFileContent: code,
53
+ jsenvRemoteDirectory,
51
54
  originalFileUrl: url,
52
55
  compiledFileUrl: compiledUrl,
53
56
  // sourcemap are not inside the asset folder because
54
57
  // of https://github.com/microsoft/vscode-chrome-debug-core/issues/544
55
- sourcemapFileUrl: `${compiledUrl}.map`,
58
+ sourcemapFileUrl: generateSourcemapUrl(compiledUrl),
56
59
  sourcemapExcludeSources,
57
60
  sourcemapMethod,
61
+ originalFileContent: code,
58
62
  },
59
63
  )
60
64
  }
@@ -36,17 +36,19 @@ import {
36
36
  } from "@jsenv/core/dist/build_manifest.js"
37
37
  import { generateGroupMap } from "@jsenv/core/src/internal/generateGroupMap/generateGroupMap.js"
38
38
  import { isBrowserPartOfSupportedRuntimes } from "@jsenv/core/src/internal/generateGroupMap/runtime_support.js"
39
- import { loadBabelPluginMapFromFile } from "./load_babel_plugin_map_from_file.js"
40
- import { extractSyntaxBabelPluginMap } from "./babel_plugins.js"
39
+
40
+ import { createJsenvRemoteDirectory } from "../jsenv_remote_directory.js"
41
41
  import {
42
42
  sourcemapMainFileInfo,
43
43
  sourcemapMappingFileInfo,
44
44
  } from "../jsenvInternalFiles.js"
45
+ import { babelPluginReplaceExpressions } from "../babel_plugin_replace_expressions.js"
45
46
  import {
46
47
  jsenvCoreDirectoryUrl,
47
48
  jsenvDistDirectoryUrl,
48
49
  } from "../jsenvCoreDirectoryUrl.js"
49
- import { babelPluginReplaceExpressions } from "../babel_plugin_replace_expressions.js"
50
+ import { loadBabelPluginMapFromFile } from "./load_babel_plugin_map_from_file.js"
51
+ import { extractSyntaxBabelPluginMap } from "./babel_plugins.js"
50
52
  import { babelPluginGlobalThisAsJsenvImport } from "./babel_plugin_global_this_as_jsenv_import.js"
51
53
  import { babelPluginNewStylesheetAsJsenvImport } from "./babel_plugin_new_stylesheet_as_jsenv_import.js"
52
54
  import { babelPluginImportAssertions } from "./babel_plugin_import_assertions.js"
@@ -98,6 +100,7 @@ export const startCompileServer = async ({
98
100
  babelPluginMap,
99
101
  babelConfigFileUrl,
100
102
  customCompilers = {},
103
+ preservedUrls,
101
104
  workers = [],
102
105
  serviceWorkers = [],
103
106
  importMapInWebWorkers = false,
@@ -146,6 +149,30 @@ export const startCompileServer = async ({
146
149
  )
147
150
  const logger = createLogger({ logLevel })
148
151
 
152
+ preservedUrls = {
153
+ // Authorize jsenv to modify any file url
154
+ // because the goal is to build the files into chunks
155
+ "file://": false,
156
+ // Preserves http and https urls
157
+ // because if code specifiy a CDN url it's usually because code wants
158
+ // to keep the url intact and keep HTTP request to CDN (both in dev and prod)
159
+ "http://": true,
160
+ "https://": true,
161
+ /*
162
+ * It's possible to selectively overrides the behaviour above:
163
+ * 1. The CDN file needs to be transformed to be executable in dev, build or both
164
+ * preservedUrls: {"https://cdn.skypack.dev/preact@10.6.4": false}
165
+ * 2. No strong need to preserve the CDN dependency
166
+ * 3. Prevent concatenation of a file during build
167
+ * preservedUrls: {"./file.js": false}
168
+ */
169
+ ...preservedUrls,
170
+ }
171
+ const jsenvRemoteDirectory = createJsenvRemoteDirectory({
172
+ projectDirectoryUrl,
173
+ jsenvDirectoryRelativeUrl,
174
+ preservedUrls,
175
+ })
149
176
  const workerUrls = workers.map((worker) =>
150
177
  resolveUrl(worker, projectDirectoryUrl),
151
178
  )
@@ -290,11 +317,18 @@ export const startCompileServer = async ({
290
317
  projectDirectoryUrl,
291
318
  jsenvDirectoryRelativeUrl,
292
319
  outDirectoryRelativeUrl,
320
+ jsenvRemoteDirectory,
293
321
  importDefaultExtension,
322
+
323
+ preservedUrls,
324
+ workers,
325
+ serviceWorkers,
294
326
  compileServerGroupMap,
327
+ babelPluginMap,
328
+ replaceProcessEnvNodeEnv,
329
+ processEnvNodeEnv,
295
330
  env,
296
331
  inlineImportMapIntoHTML,
297
- babelPluginMap,
298
332
  customCompilers,
299
333
  jsenvToolbarInjection,
300
334
  sourcemapMethod,
@@ -328,7 +362,9 @@ export const startCompileServer = async ({
328
362
  logger,
329
363
 
330
364
  projectDirectoryUrl,
365
+ jsenvDirectoryRelativeUrl,
331
366
  outDirectoryRelativeUrl,
367
+ jsenvRemoteDirectory,
332
368
 
333
369
  importDefaultExtension,
334
370
 
@@ -369,6 +405,7 @@ export const startCompileServer = async ({
369
405
  : {}),
370
406
  "service:source file": createSourceFileService({
371
407
  projectDirectoryUrl,
408
+ jsenvRemoteDirectory,
372
409
  projectFileRequestedCallback,
373
410
  projectFileCacheStrategy,
374
411
  }),
@@ -424,6 +461,7 @@ export const startCompileServer = async ({
424
461
  ...compileServer,
425
462
  compileServerGroupMap,
426
463
  babelPluginMap,
464
+ preservedUrls,
427
465
  projectFileRequestedCallback,
428
466
  }
429
467
  }
@@ -939,30 +977,45 @@ const createSourceFileService = ({
939
977
  projectDirectoryUrl,
940
978
  projectFileRequestedCallback,
941
979
  projectFileCacheStrategy,
980
+ jsenvRemoteDirectory,
942
981
  }) => {
943
982
  return async (request) => {
944
983
  const relativeUrl = request.pathname.slice(1)
945
984
  projectFileRequestedCallback(relativeUrl, request)
946
-
947
985
  const fileUrl = new URL(request.ressource.slice(1), projectDirectoryUrl)
948
986
  .href
949
987
  const fileIsInsideJsenvDistDirectory = urlIsInsideOf(
950
988
  fileUrl,
951
989
  jsenvDistDirectoryUrl,
952
990
  )
953
-
954
- const responsePromise = fetchFileSystem(fileUrl, {
955
- headers: request.headers,
956
- etagEnabled: projectFileCacheStrategy === "etag",
957
- mtimeEnabled: projectFileCacheStrategy === "mtime",
958
- ...(fileIsInsideJsenvDistDirectory
959
- ? {
960
- cacheControl: `private,max-age=${60 * 60 * 24 * 30},immutable`,
961
- }
962
- : {}),
963
- })
964
-
965
- return responsePromise
991
+ const fromFileSystem = () =>
992
+ fetchFileSystem(fileUrl, {
993
+ headers: request.headers,
994
+ etagEnabled: projectFileCacheStrategy === "etag",
995
+ mtimeEnabled: projectFileCacheStrategy === "mtime",
996
+ ...(fileIsInsideJsenvDistDirectory
997
+ ? {
998
+ cacheControl: `private,max-age=${60 * 60 * 24 * 30},immutable`,
999
+ }
1000
+ : {}),
1001
+ })
1002
+ const filesystemResponse = await fromFileSystem()
1003
+ if (
1004
+ filesystemResponse.status === 404 &&
1005
+ jsenvRemoteDirectory.isFileUrlForRemoteUrl(fileUrl)
1006
+ ) {
1007
+ try {
1008
+ await jsenvRemoteDirectory.loadFileUrlFromRemote(fileUrl, request)
1009
+ // re-fetch filesystem instead to ensure response headers are correct
1010
+ return fromFileSystem()
1011
+ } catch (e) {
1012
+ if (e && e.asResponse) {
1013
+ return e.asResponse()
1014
+ }
1015
+ throw e
1016
+ }
1017
+ }
1018
+ return filesystemResponse
966
1019
  }
967
1020
  }
968
1021
 
@@ -971,6 +1024,10 @@ const createCompileServerMetaFileInfo = ({
971
1024
  jsenvDirectoryRelativeUrl,
972
1025
  outDirectoryRelativeUrl,
973
1026
  importDefaultExtension,
1027
+
1028
+ preservedUrls,
1029
+ workers,
1030
+ serviceWorkers,
974
1031
  compileServerGroupMap,
975
1032
  babelPluginMap,
976
1033
  replaceProcessEnvNodeEnv,
@@ -1016,6 +1073,9 @@ const createCompileServerMetaFileInfo = ({
1016
1073
  outDirectoryRelativeUrl,
1017
1074
  importDefaultExtension,
1018
1075
 
1076
+ preservedUrls,
1077
+ workers,
1078
+ serviceWorkers,
1019
1079
  babelPluginMap: babelPluginMapAsData(babelPluginMap),
1020
1080
  compileServerGroupMap,
1021
1081
  customCompilerPatterns,
@@ -1029,7 +1089,7 @@ const createCompileServerMetaFileInfo = ({
1029
1089
  sourcemapMappingFileRelativeUrl,
1030
1090
  errorStackRemapping: true,
1031
1091
 
1032
- // used to consider the logic generating files may have changed
1092
+ // used to consider logic generating files may have changed
1033
1093
  jsenvCorePackageVersion,
1034
1094
 
1035
1095
  // impact only HTML files
@@ -3,6 +3,7 @@ import {
3
3
  urlToRelativeUrl,
4
4
  readFile,
5
5
  ensureWindowsDriveLetter,
6
+ urlIsInsideOf,
6
7
  } from "@jsenv/filesystem"
7
8
 
8
9
  import {
@@ -15,16 +16,16 @@ import {
15
16
  setCssSourceMappingUrl,
16
17
  sourcemapToBase64Url,
17
18
  } from "../sourceMappingURLUtils.js"
18
- import { generateCompiledFileAssetUrl } from "./compile-directory/compile-asset.js"
19
+ import { generateCompilationAssetUrl } from "./compile-directory/compile-asset.js"
19
20
  import { testFilePresence } from "./compile-directory/fs-optimized-for-cache.js"
20
21
 
21
22
  const isWindows = process.platform === "win32"
22
23
 
23
24
  export const transformResultToCompilationResult = async (
24
- { contentType, code, map, metadata = {} },
25
+ { contentType, metadata = {}, code, map },
25
26
  {
26
27
  projectDirectoryUrl,
27
- originalFileContent,
28
+ jsenvRemoteDirectory,
28
29
  originalFileUrl,
29
30
  compiledFileUrl,
30
31
  sourcemapFileUrl,
@@ -33,9 +34,13 @@ export const transformResultToCompilationResult = async (
33
34
  // it also means client have to fetch source from server (additional http request)
34
35
  // some client ignore sourcesContent property such as vscode-chrome-debugger
35
36
  // Because it's the most complex scenario and we want to ensure client is always able
36
- // to find source from the sourcemap, we remove map.sourcesContent by default to test this.
37
- sourcemapExcludeSources = true,
37
+ // to find source from the sourcemap, it's a good idea
38
+ // to exclude sourcesContent from sourcemap.
39
+ // However some ressource are abstract and it means additional http request for the browser.
40
+ // For these reasons it's simpler to keep source content in sourcemap.
41
+ sourcemapExcludeSources = false,
38
42
  sourcemapMethod = "comment", // "comment", "inline"
43
+ originalFileContent,
39
44
  },
40
45
  ) => {
41
46
  if (typeof contentType !== "string") {
@@ -71,6 +76,14 @@ export const transformResultToCompilationResult = async (
71
76
  const sourcesContent = []
72
77
  const assets = []
73
78
  const assetsContent = []
79
+ const addSource = ({ url, content }) => {
80
+ sources.push(url)
81
+ sourcesContent.push(content)
82
+ }
83
+ const addAsset = ({ url, content }) => {
84
+ assets.push(url)
85
+ assetsContent.push(content)
86
+ }
74
87
 
75
88
  let output = code
76
89
  if (sourcemapEnabled && map) {
@@ -78,8 +91,10 @@ export const transformResultToCompilationResult = async (
78
91
  // may happen in some cases where babel returns a wrong sourcemap
79
92
  // there is at least one case where it happens
80
93
  // a file with only import './whatever.js' inside
81
- sources.push(originalFileUrl)
82
- sourcesContent.push(originalFileContent)
94
+ addSource({
95
+ url: originalFileUrl,
96
+ content: originalFileContent,
97
+ })
83
98
  } else {
84
99
  map.sources.forEach((source, index) => {
85
100
  const sourceFileUrl = resolveSourceFile({
@@ -90,19 +105,25 @@ export const transformResultToCompilationResult = async (
90
105
  projectDirectoryUrl,
91
106
  })
92
107
  if (sourceFileUrl) {
93
- map.sources[index] = urlToRelativeUrl(sourceFileUrl, sourcemapFileUrl)
108
+ // In case the file comes from a remote url
109
+ // we prefer to consider remote url as the real source for this code
110
+ map.sources[index] =
111
+ jsenvRemoteDirectory &&
112
+ jsenvRemoteDirectory.isFileUrlForRemoteUrl(sourceFileUrl)
113
+ ? jsenvRemoteDirectory.remoteUrlFromFileUrl(sourceFileUrl)
114
+ : urlToRelativeUrl(sourceFileUrl, sourcemapFileUrl)
94
115
  sources[index] = sourceFileUrl
95
116
  }
96
117
  })
97
-
98
118
  if (sources.length === 0) {
99
119
  // happens when sourcemap is generated by webpack and looks like
100
120
  // webpack://Package./src/file.js
101
121
  // in that case we'll don't know how to find the source file
102
- sources.push(originalFileUrl)
103
- sourcesContent.push(originalFileContent)
122
+ addSource({
123
+ url: originalFileUrl,
124
+ content: originalFileContent,
125
+ })
104
126
  }
105
-
106
127
  await Promise.all(
107
128
  sources.map(async (sourceUrl, index) => {
108
129
  const contentFromSourcemap = map.sourcesContent
@@ -121,7 +142,6 @@ export const transformResultToCompilationResult = async (
121
142
  if (sourcemapExcludeSources) {
122
143
  delete map.sourcesContent
123
144
  }
124
-
125
145
  // we don't need sourceRoot because our path are relative or absolute to the current location
126
146
  // we could comment this line because it is not set by babel because not passed during transform
127
147
  delete map.sourceRoot
@@ -139,22 +159,28 @@ export const transformResultToCompilationResult = async (
139
159
  compiledFileUrl,
140
160
  )
141
161
  output = setSourceMappingUrl(output, sourcemapFileRelativePathForModule)
142
- assets.push(sourcemapFileUrl)
143
- assetsContent.push(stringifyMap(map))
162
+ addAsset({
163
+ url: sourcemapFileUrl,
164
+ content: stringifyMap(map),
165
+ })
144
166
  }
145
167
  } else {
146
- sources.push(originalFileUrl)
147
- sourcesContent.push(originalFileContent)
168
+ addSource({
169
+ url: originalFileUrl,
170
+ content: originalFileContent,
171
+ })
148
172
  }
149
173
 
150
174
  const { coverage } = metadata
151
175
  if (coverage) {
152
- const coverageAssetFileUrl = generateCompiledFileAssetUrl(
176
+ const coverageAssetFileUrl = generateCompilationAssetUrl(
153
177
  compiledFileUrl,
154
178
  "coverage.json",
155
179
  )
156
- assets.push(coverageAssetFileUrl)
157
- assetsContent.push(stringifyCoverage(coverage))
180
+ addAsset({
181
+ url: coverageAssetFileUrl,
182
+ content: stringifyCoverage(coverage),
183
+ })
158
184
  }
159
185
 
160
186
  const { dependencies = [] } = metadata
@@ -179,18 +205,15 @@ const resolveSourceFile = ({
179
205
  projectDirectoryUrl,
180
206
  }) => {
181
207
  const sourceFileUrl = resolveSourceUrl({ source, sourcemapFileUrl })
182
-
183
- if (!sourceFileUrl.startsWith(projectDirectoryUrl)) {
208
+ if (!urlIsInsideOf(sourceFileUrl, projectDirectoryUrl)) {
184
209
  // do not track dependency outside project
185
210
  // it means cache stays valid for those external sources
186
211
  return null
187
212
  }
188
-
189
213
  const fileFound = testFilePresence(sourceFileUrl)
190
214
  if (fileFound) {
191
215
  return sourceFileUrl
192
216
  }
193
-
194
217
  // prefer original source file
195
218
  const relativeUrl = urlToRelativeUrl(sourceFileUrl, compiledFileUrl)
196
219
  const originalSourceUrl = resolveUrl(relativeUrl, originalFileUrl)
@@ -211,7 +234,6 @@ const resolveSourceUrl = ({ source, sourcemapFileUrl }) => {
211
234
  )
212
235
  return ensureWindowsDriveLetter(url, sourcemapFileUrl)
213
236
  }
214
-
215
237
  return resolveUrl(source, sourcemapFileUrl)
216
238
  }
217
239
 
@@ -51,6 +51,7 @@ export const executePlan = async (
51
51
  compileServerCanWriteOnFilesystem,
52
52
  babelPluginMap,
53
53
  babelConfigFileUrl,
54
+ preservedUrls,
54
55
  workers,
55
56
  serviceWorkers,
56
57
  importMapInWebWorkers,
@@ -126,6 +127,7 @@ export const executePlan = async (
126
127
  keepProcessAlive: true, // to be sure it stays alive
127
128
  babelPluginMap,
128
129
  babelConfigFileUrl,
130
+ preservedUrls,
129
131
  workers,
130
132
  serviceWorkers,
131
133
  importMapInWebWorkers,
@@ -13,9 +13,9 @@ export const fetchUrl = async (
13
13
  ignoreHttpsError,
14
14
  ...rest,
15
15
  })
16
-
17
- return {
16
+ const responseObject = {
18
17
  url: response.url,
18
+ type: "default",
19
19
  status: response.status,
20
20
  statusText: response.statusText,
21
21
  headers: headersToObject(response.headers),
@@ -24,4 +24,5 @@ export const fetchUrl = async (
24
24
  blob: response.blob.bind(response),
25
25
  arrayBuffer: response.arrayBuffer.bind(response),
26
26
  }
27
+ return responseObject
27
28
  }
@@ -0,0 +1,26 @@
1
+ import crypto from "node:crypto"
2
+
3
+ export const isSupportedAlgorithm = (algo) => {
4
+ return SUPPORTED_ALGORITHMS.includes(algo)
5
+ }
6
+
7
+ // https://www.w3.org/TR/SRI/#priority
8
+ export const getPrioritizedHashFunction = (firstAlgo, secondAlgo) => {
9
+ const firstIndex = SUPPORTED_ALGORITHMS.indexOf(firstAlgo)
10
+ const secondIndex = SUPPORTED_ALGORITHMS.indexOf(secondAlgo)
11
+ if (firstIndex === secondIndex) {
12
+ return ""
13
+ }
14
+ if (firstIndex < secondIndex) {
15
+ return secondAlgo
16
+ }
17
+ return firstAlgo
18
+ }
19
+
20
+ export const applyAlgoToRepresentationData = (algo, data) => {
21
+ const base64Value = crypto.createHash(algo).update(data).digest("base64")
22
+ return base64Value
23
+ }
24
+
25
+ // keep this ordered by collision resistance as it is also used by "getPrioritizedHashFunction"
26
+ const SUPPORTED_ALGORITHMS = ["sha256", "sha384", "sha512"]
@@ -0,0 +1,50 @@
1
+ import { isSupportedAlgorithm } from "./integrity_algorithms.js"
2
+
3
+ // see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
4
+ export const parseIntegrity = (string) => {
5
+ const integrityMetadata = {}
6
+ string
7
+ .trim()
8
+ .split(/\s+/)
9
+ .forEach((token) => {
10
+ const { isValid, algo, base64Value, optionExpression } =
11
+ parseAsHashWithOptions(token)
12
+ if (!isValid) {
13
+ return
14
+ }
15
+ if (!isSupportedAlgorithm(algo)) {
16
+ return
17
+ }
18
+ const metadataList = integrityMetadata[algo]
19
+ const metadata = { base64Value, optionExpression }
20
+ integrityMetadata[algo] = metadataList
21
+ ? [...metadataList, metadata]
22
+ : [metadata]
23
+ })
24
+ return integrityMetadata
25
+ }
26
+
27
+ // see https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute
28
+ const parseAsHashWithOptions = (token) => {
29
+ const dashIndex = token.indexOf("-")
30
+ if (dashIndex === -1) {
31
+ return { isValid: false }
32
+ }
33
+ const beforeDash = token.slice(0, dashIndex)
34
+ const afterDash = token.slice(dashIndex + 1)
35
+ const questionIndex = afterDash.indexOf("?")
36
+ const algo = beforeDash
37
+ if (questionIndex === -1) {
38
+ const base64Value = afterDash
39
+ const isValid = BASE64_REGEX.test(afterDash)
40
+ return { isValid, algo, base64Value }
41
+ }
42
+ const base64Value = afterDash.slice(0, questionIndex)
43
+ const optionExpression = afterDash.slice(questionIndex + 1)
44
+ const isValid =
45
+ BASE64_REGEX.test(afterDash) && VCHAR_REGEX.test(optionExpression)
46
+ return { isValid, algo, base64Value, optionExpression }
47
+ }
48
+
49
+ const BASE64_REGEX = /^[A-Za-z0-9+\/=+]+$/
50
+ const VCHAR_REGEX = /^[\x21-\x7E]+$/
@@ -0,0 +1,23 @@
1
+ import { parseIntegrity } from "./integrity_parsing.js"
2
+ import {
3
+ getPrioritizedHashFunction,
4
+ applyAlgoToRepresentationData,
5
+ } from "./integrity_algorithms.js"
6
+
7
+ export const updateIntegrity = (integrity, representationData) => {
8
+ const integrityMetadata = parseIntegrity(integrity)
9
+ const algos = Object.keys(integrityMetadata)
10
+ if (algos.length === 0) {
11
+ return ""
12
+ }
13
+ let strongestAlgo = algos[0]
14
+ algos.slice(1).forEach((algoCandidate) => {
15
+ strongestAlgo =
16
+ getPrioritizedHashFunction(strongestAlgo, algoCandidate) || strongestAlgo
17
+ })
18
+ const base64Value = applyAlgoToRepresentationData(
19
+ strongestAlgo,
20
+ representationData,
21
+ )
22
+ return `${strongestAlgo}-${base64Value}`
23
+ }
@@ -0,0 +1,49 @@
1
+ import { parseIntegrity } from "./integrity_parsing.js"
2
+ import {
3
+ getPrioritizedHashFunction,
4
+ applyAlgoToRepresentationData,
5
+ } from "./integrity_algorithms.js"
6
+
7
+ // https://www.w3.org/TR/SRI/#does-response-match-metadatalist
8
+ export const validateResponseIntegrity = (
9
+ { url, type, dataRepresentation },
10
+ integrity,
11
+ ) => {
12
+ if (!isResponseEligibleForIntegrityValidation({ type })) {
13
+ return false
14
+ }
15
+ const integrityMetadata = parseIntegrity(integrity)
16
+ const algos = Object.keys(integrityMetadata)
17
+ if (algos.length === 0) {
18
+ return true
19
+ }
20
+ let strongestAlgo = algos[0]
21
+ algos.slice(1).forEach((algoCandidate) => {
22
+ strongestAlgo =
23
+ getPrioritizedHashFunction(strongestAlgo, algoCandidate) || strongestAlgo
24
+ })
25
+ const metadataList = integrityMetadata[strongestAlgo]
26
+ const actualBase64Value = applyAlgoToRepresentationData(
27
+ strongestAlgo,
28
+ dataRepresentation,
29
+ )
30
+ const acceptedBase64Values = metadataList.map(
31
+ (metadata) => metadata.base64Value,
32
+ )
33
+ const someIsMatching = acceptedBase64Values.includes(actualBase64Value)
34
+ if (someIsMatching) {
35
+ return true
36
+ }
37
+ const error = new Error(
38
+ `Integrity validation failed for ressource "${url}". The integrity found for this ressource is "${strongestAlgo}-${actualBase64Value}"`,
39
+ )
40
+ error.code = "EINTEGRITY"
41
+ error.algorithm = strongestAlgo
42
+ error.found = actualBase64Value
43
+ throw error
44
+ }
45
+
46
+ // https://www.w3.org/TR/SRI/#is-response-eligible-for-integrity-validation
47
+ const isResponseEligibleForIntegrityValidation = (response) => {
48
+ return ["basic", "cors", "default"].includes(response.type)
49
+ }