ImportMap.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /*
  2. * Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
  3. * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include <LibJS/Console.h>
  8. #include <LibJS/Runtime/ConsoleObject.h>
  9. #include <LibWeb/DOMURL/DOMURL.h>
  10. #include <LibWeb/HTML/Scripting/Fetching.h>
  11. #include <LibWeb/HTML/Scripting/ImportMap.h>
  12. #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
  13. #include <LibWeb/HTML/Window.h>
  14. #include <LibWeb/Infra/JSON.h>
  15. #include <LibWeb/Infra/Strings.h>
  16. namespace Web::HTML {
  17. // https://html.spec.whatwg.org/multipage/webappapis.html#parse-an-import-map-string
  18. WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteString const& input, URL::URL base_url)
  19. {
  20. HTML::TemporaryExecutionContext execution_context { realm };
  21. // 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
  22. auto parsed = TRY(Infra::parse_json_string_to_javascript_value(realm, input));
  23. // 2. If parsed is not an ordered map, then throw a TypeError indicating that the top-level value needs to be a JSON object.
  24. if (!parsed.is_object())
  25. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "The top-level value of an importmap needs to be a JSON object."_string };
  26. auto& parsed_object = parsed.as_object();
  27. // 3. Let sortedAndNormalizedImports be an empty ordered map.
  28. ModuleSpecifierMap sorted_and_normalised_imports;
  29. // 4. If parsed["imports"] exists, then:
  30. if (TRY(parsed_object.has_property("imports"))) {
  31. auto imports = TRY(parsed_object.get("imports"));
  32. // If parsed["imports"] is not an ordered map, then throw a TypeError indicating that the value for the "imports" top-level key needs to be a JSON object.
  33. if (!imports.is_object())
  34. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "The 'imports' top-level value of an importmap needs to be a JSON object."_string };
  35. // Set sortedAndNormalizedImports to the result of sorting and normalizing a module specifier map given parsed["imports"] and baseURL.
  36. sorted_and_normalised_imports = TRY(sort_and_normalise_module_specifier_map(realm, imports.as_object(), base_url));
  37. }
  38. // 5. Let sortedAndNormalizedScopes be an empty ordered map.
  39. HashMap<URL::URL, ModuleSpecifierMap> sorted_and_normalised_scopes;
  40. // 6. If parsed["scopes"] exists, then:
  41. if (TRY(parsed_object.has_property("scopes"))) {
  42. auto scopes = TRY(parsed_object.get("scopes"));
  43. // If parsed["scopes"] is not an ordered map, then throw a TypeError indicating that the value for the "scopes" top-level key needs to be a JSON object.
  44. if (!scopes.is_object())
  45. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "The 'scopes' top-level value of an importmap needs to be a JSON object."_string };
  46. // Set sortedAndNormalizedScopes to the result of sorting and normalizing scopes given parsed["scopes"] and baseURL.
  47. sorted_and_normalised_scopes = TRY(sort_and_normalise_scopes(realm, scopes.as_object(), base_url));
  48. }
  49. // 7. Let normalizedIntegrity be an empty ordered map.
  50. ModuleIntegrityMap normalised_integrity;
  51. // 8. If parsed["integrity"] exists, then:
  52. if (TRY(parsed_object.has_property("integrity"))) {
  53. auto integrity = TRY(parsed_object.get("integrity"));
  54. // 1. If parsed["integrity"] is not an ordered map, then throw a TypeError indicating that the value for the "integrity" top-level key needs to be a JSON object.
  55. if (!integrity.is_object())
  56. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "The 'integrity' top-level value of an importmap needs to be a JSON object."_string };
  57. // 2. Set normalizedIntegrity to the result of normalizing a module integrity map given parsed["integrity"] and baseURL.
  58. normalised_integrity = TRY(normalize_module_integrity_map(realm, integrity.as_object(), base_url));
  59. }
  60. // 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity", then the user agent should report a warning to the console indicating that an invalid top-level key was present in the import map.
  61. for (auto& key : parsed_object.shape().property_table().keys()) {
  62. if (key.as_string().is_one_of("imports", "scopes", "integrity"))
  63. continue;
  64. auto& console = realm.intrinsics().console_object()->console();
  65. console.output_debug_message(JS::Console::LogLevel::Warn, MUST(String::formatted("An invalid top-level key ({}) was present in the import map", key.as_string())));
  66. }
  67. // 10. Return an import map whose imports are sortedAndNormalizedImports, whose scopes are sortedAndNormalizedScopes, and whose integrity are normalizedIntegrity.
  68. ImportMap import_map;
  69. import_map.set_imports(sorted_and_normalised_imports);
  70. import_map.set_scopes(sorted_and_normalised_scopes);
  71. import_map.set_integrity(normalised_integrity);
  72. return import_map;
  73. }
  74. // https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-specifier-key
  75. Optional<DeprecatedFlyString> normalise_specifier_key(JS::Realm& realm, DeprecatedFlyString specifier_key, URL::URL base_url)
  76. {
  77. // 1. If specifierKey is the empty string, then:
  78. if (specifier_key.is_empty()) {
  79. // 1. The user agent may report a warning to the console indicating that specifier keys may not be the empty string.
  80. auto& console = realm.intrinsics().console_object()->console();
  81. console.output_debug_message(JS::Console::LogLevel::Warn, "Specifier keys may not be empty"sv);
  82. // 2. Return null.
  83. return Optional<DeprecatedFlyString> {};
  84. }
  85. // 2. Let url be the result of resolving a URL-like module specifier, given specifierKey and baseURL.
  86. auto url = resolve_url_like_module_specifier(specifier_key, base_url);
  87. // 3. If url is not null, then return the serialization of url.
  88. if (url.has_value())
  89. return url->serialize().to_byte_string();
  90. // 4. Return specifierKey.
  91. return specifier_key;
  92. }
  93. // https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-a-module-specifier-map
  94. WebIDL::ExceptionOr<ModuleSpecifierMap> sort_and_normalise_module_specifier_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
  95. {
  96. // 1. Let normalized be an empty ordered map.
  97. ModuleSpecifierMap normalised;
  98. // 2. For each specifierKey → value of originalMap:
  99. for (auto& specifier_key : original_map.shape().property_table().keys()) {
  100. auto value = TRY(original_map.get(specifier_key.as_string()));
  101. // 1. Let normalizedSpecifierKey be the result of normalizing a specifier key given specifierKey and baseURL.
  102. auto normalised_specifier_key = normalise_specifier_key(realm, specifier_key.as_string(), base_url);
  103. // 2. If normalizedSpecifierKey is null, then continue.
  104. if (!normalised_specifier_key.has_value())
  105. continue;
  106. // 3. If value is not a string, then:
  107. if (!value.is_string()) {
  108. // 1. The user agent may report a warning to the console indicating that addresses need to be strings.
  109. auto& console = realm.intrinsics().console_object()->console();
  110. console.output_debug_message(JS::Console::LogLevel::Warn, "Addresses need to be strings"sv);
  111. // 2. Set normalized[normalizedSpecifierKey] to null.
  112. normalised.set(normalised_specifier_key.value(), {});
  113. // 3. Continue.
  114. continue;
  115. }
  116. // 4. Let addressURL be the result of resolving a URL-like module specifier given value and baseURL.
  117. auto address_url = resolve_url_like_module_specifier(value.as_string().byte_string(), base_url);
  118. // 5. If addressURL is null, then:
  119. if (!address_url.has_value()) {
  120. // 1. The user agent may report a warning to the console indicating that the address was invalid.
  121. auto& console = realm.intrinsics().console_object()->console();
  122. console.output_debug_message(JS::Console::LogLevel::Warn, "Address was invalid"sv);
  123. // 2. Set normalized[normalizedSpecifierKey] to null.
  124. normalised.set(normalised_specifier_key.value(), {});
  125. // 3. Continue.
  126. continue;
  127. }
  128. // 6. If specifierKey ends with U+002F (/), and the serialization of addressURL does not end with U+002F (/), then:
  129. if (specifier_key.as_string().ends_with("/"sv) && !address_url->serialize().ends_with('/')) {
  130. // 1. The user agent may report a warning to the console indicating that an invalid address was given for the specifier key specifierKey; since specifierKey ends with a slash, the address needs to as well.
  131. auto& console = realm.intrinsics().console_object()->console();
  132. console.output_debug_message(JS::Console::LogLevel::Warn,
  133. MUST(String::formatted("An invalid address was given for the specifier key ({}); since specifierKey ends with a slash, the address needs to as well", specifier_key.as_string())));
  134. // 2. Set normalized[normalizedSpecifierKey] to null.
  135. normalised.set(normalised_specifier_key.value(), {});
  136. // 3. Continue.
  137. continue;
  138. }
  139. // 7. Set normalized[normalizedSpecifierKey] to addressURL.
  140. normalised.set(normalised_specifier_key.value(), address_url.value());
  141. }
  142. // 3. Return the result of sorting in descending order normalized, with an entry a being less than an entry b if a's key is code unit less than b's key.
  143. return normalised;
  144. }
  145. // https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-scopes
  146. WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_scopes(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
  147. {
  148. // 1. Let normalized be an empty ordered map.
  149. HashMap<URL::URL, ModuleSpecifierMap> normalised;
  150. // 2. For each scopePrefix → potentialSpecifierMap of originalMap:
  151. for (auto& scope_prefix : original_map.shape().property_table().keys()) {
  152. auto potential_specifier_map = TRY(original_map.get(scope_prefix.as_string()));
  153. // 1. If potentialSpecifierMap is not an ordered map, then throw a TypeError indicating that the value of the scope with prefix scopePrefix needs to be a JSON object.
  154. if (!potential_specifier_map.is_object())
  155. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The value of the scope with the prefix '{}' needs to be a JSON object.", scope_prefix.as_string()).release_value_but_fixme_should_propagate_errors() };
  156. // 2. Let scopePrefixURL be the result of URL parsing scopePrefix with baseURL.
  157. auto scope_prefix_url = DOMURL::parse(scope_prefix.as_string(), base_url);
  158. // 3. If scopePrefixURL is failure, then:
  159. if (!scope_prefix_url.is_valid()) {
  160. // 1. The user agent may report a warning to the console that the scope prefix URL was not parseable.
  161. auto& console = realm.intrinsics().console_object()->console();
  162. console.output_debug_message(JS::Console::LogLevel::Warn,
  163. MUST(String::formatted("The scope prefix URL ({}) was not parseable", scope_prefix.as_string())));
  164. // 2. Continue.
  165. continue;
  166. }
  167. // 4. Let normalizedScopePrefix be the serialization of scopePrefixURL.
  168. auto normalised_scope_prefix = scope_prefix_url.serialize();
  169. // 5. Set normalized[normalizedScopePrefix] to the result of sorting and normalizing a module specifier map given potentialSpecifierMap and baseURL.
  170. normalised.set(normalised_scope_prefix, TRY(sort_and_normalise_module_specifier_map(realm, potential_specifier_map.as_object(), base_url)));
  171. }
  172. // 3. Return the result of sorting in descending order normalized, with an entry a being less than an entry b if a's key is code unit less than b's key.
  173. return normalised;
  174. }
  175. // https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-module-integrity-map
  176. WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
  177. {
  178. // 1. Let normalized be an empty ordered map.
  179. ModuleIntegrityMap normalised;
  180. // 2. For each key → value of originalMap:
  181. for (auto& key : original_map.shape().property_table().keys()) {
  182. auto value = TRY(original_map.get(key.as_string()));
  183. // 1. Let resolvedURL be the result of resolving a URL-like module specifier given key and baseURL.
  184. auto resolved_url = resolve_url_like_module_specifier(key.as_string(), base_url);
  185. // 2. If resolvedURL is null, then:
  186. if (!resolved_url.has_value()) {
  187. // 1. The user agent may report a warning to the console indicating that the key failed to resolve.
  188. auto& console = realm.intrinsics().console_object()->console();
  189. console.output_debug_message(JS::Console::LogLevel::Warn,
  190. MUST(String::formatted("Failed to resolve key ({})", key.as_string())));
  191. // 2. Continue.
  192. continue;
  193. }
  194. // 3. If value is not a string, then:
  195. if (!value.is_string()) {
  196. // 1. The user agent may report a warning to the console indicating that integrity metadata values need to be strings.
  197. auto& console = realm.intrinsics().console_object()->console();
  198. console.output_debug_message(JS::Console::LogLevel::Warn,
  199. MUST(String::formatted("Integrity metadata value for '{}' needs to be a string", key.as_string())));
  200. // 2. Continue.
  201. continue;
  202. }
  203. // 4. Set normalized[resolvedURL] to value.
  204. normalised.set(resolved_url.release_value(), value.as_string().byte_string());
  205. }
  206. // 3. Return normalized.
  207. return normalised;
  208. }
  209. // https://html.spec.whatwg.org/multipage/webappapis.html#merge-module-specifier-maps
  210. static ModuleSpecifierMap merge_module_specifier_maps(JS::Realm& realm, ModuleSpecifierMap const& new_map, ModuleSpecifierMap const& old_map)
  211. {
  212. // 1. Let mergedMap be a deep copy of oldMap.
  213. ModuleSpecifierMap merged_map = old_map;
  214. // 2. For each specifier → url of newMap:
  215. for (auto const& [specifier, url] : new_map) {
  216. // 1. If specifier exists in oldMap, then:
  217. if (old_map.contains(specifier)) {
  218. // 1. The user agent may report a warning to the console indicating the ignored rule. They may choose to
  219. // avoid reporting if the rule is identical to an existing one.
  220. auto& console = realm.intrinsics().console_object()->console();
  221. console.output_debug_message(JS::Console::LogLevel::Warn,
  222. MUST(String::formatted("An import map rule for specifier '{}' was ignored as one was already present in the existing import map", specifier)));
  223. // 2. Continue.
  224. continue;
  225. }
  226. // 2. Set mergedMap[specifier] to url.
  227. merged_map.set(specifier, url);
  228. }
  229. // 3. Return mergedMap.
  230. return merged_map;
  231. }
  232. // https://html.spec.whatwg.org/multipage/webappapis.html#merge-existing-and-new-import-maps
  233. void merge_existing_and_new_import_maps(Window& global, ImportMap& new_import_map)
  234. {
  235. auto& realm = global.realm();
  236. // 1. Let newImportMapScopes be a deep copy of newImportMap's scopes.
  237. auto new_import_map_scopes = new_import_map.scopes();
  238. // Spec-Note: We're mutating these copies and removing items from them when they are used to ignore scope-specific
  239. // rules. This is true for newImportMapScopes, as well as to newImportMapImports below.
  240. // 2. Let oldImportMap be global's import map.
  241. auto& old_import_map = global.import_map();
  242. // 3. Let newImportMapImports be a deep copy of newImportMap's imports.
  243. auto new_import_map_imports = new_import_map.imports();
  244. // 4. For each scopePrefix → scopeImports of newImportMapScopes:
  245. for (auto& [scope_prefix, scope_imports] : new_import_map_scopes) {
  246. // 1. For each record of global's resolved module set:
  247. for (auto const& record : global.resolved_module_set()) {
  248. // 1. If scopePrefix is record's serialized base URL, or if scopePrefix ends with U+002F (/) and scopePrefix is a code unit prefix of record's serialized base URL, then:
  249. if (scope_prefix == record.serialized_base_url || (scope_prefix.to_string().ends_with('/') && record.serialized_base_url.has_value() && Infra::is_code_unit_prefix(scope_prefix.to_string(), *record.serialized_base_url))) {
  250. // 1. For each specifierKey → resolutionResult of scopeImports:
  251. scope_imports.remove_all_matching([&](ByteString const& specifier_key, Optional<URL::URL> const&) {
  252. // 1. If specifierKey is record's specifier, or if all of the following conditions are true:
  253. // * specifierKey ends with U+002F (/);
  254. // * specifierKey is a code unit prefix of record's specifier;
  255. // * either record's specifier as a URL is null or is special,
  256. // then:
  257. if (specifier_key.view() == record.specifier
  258. || (specifier_key.ends_with('/')
  259. && Infra::is_code_unit_prefix(specifier_key, record.specifier)
  260. && record.specifier_is_null_or_url_like_that_is_special)) {
  261. // 1. The user agent may report a warning to the console indicating the ignored rule. They
  262. // may choose to avoid reporting if the rule is identical to an existing one.
  263. auto& console = realm.intrinsics().console_object()->console();
  264. console.output_debug_message(JS::Console::LogLevel::Warn,
  265. MUST(String::formatted("An import map rule for specifier '{}' was ignored as one was already present in the existing import map", specifier_key)));
  266. // 2. Remove scopeImports[specifierKey].
  267. return true;
  268. }
  269. return false;
  270. });
  271. }
  272. }
  273. // 2. If scopePrefix exists in oldImportMap's scopes, then set oldImportMap's scopes[scopePrefix] to the result
  274. // of merging module specifier maps, given scopeImports and oldImportMap's scopes[scopePrefix].
  275. if (auto it = old_import_map.scopes().find(scope_prefix); it != old_import_map.scopes().end()) {
  276. it->value = merge_module_specifier_maps(realm, scope_imports, it->value);
  277. }
  278. // 3. Otherwise, set oldImportMap's scopes[scopePrefix] to scopeImports.
  279. else {
  280. old_import_map.scopes().set(scope_prefix, scope_imports);
  281. }
  282. }
  283. // 5. For each url → integrity of newImportMap's integrity:
  284. for (auto const& [url, integrity] : new_import_map.integrity()) {
  285. // 1. If url exists in oldImportMap's integrity, then:
  286. if (old_import_map.integrity().contains(url)) {
  287. // 1. The user agent may report a warning to the console indicating the ignored rule. They may choose to
  288. // avoid reporting if the rule is identical to an existing one.
  289. auto& console = realm.intrinsics().console_object()->console();
  290. console.output_debug_message(JS::Console::LogLevel::Warn,
  291. MUST(String::formatted("An import map integrity rule for url '{}' was ignored as one was already present in the existing import map", url)));
  292. // 2. Continue.
  293. continue;
  294. }
  295. // 2. Set oldImportMap's integrity[url] to integrity.
  296. old_import_map.integrity().set(url, integrity);
  297. }
  298. // 6. For each record of global's resolved module set:
  299. for (auto const& record : global.resolved_module_set()) {
  300. // 1. For each specifier → url of newImportMapImports:
  301. new_import_map_imports.remove_all_matching([&](ByteString const& specifier, Optional<URL::URL> const&) {
  302. // 1. If specifier starts with record's specifier, then:
  303. if (specifier.starts_with(record.specifier)) {
  304. // 1. The user agent may report a warning to the console indicating the ignored rule. They may choose to
  305. // avoid reporting if the rule is identical to an existing one.
  306. auto& console = realm.intrinsics().console_object()->console();
  307. console.output_debug_message(JS::Console::LogLevel::Warn,
  308. MUST(String::formatted("An import map rule for specifier '{}' was ignored as one was already present in the existing import map", specifier)));
  309. // 2. Remove newImportMapImports[specifier].
  310. return true;
  311. }
  312. return false;
  313. });
  314. }
  315. // 7. Set oldImportMap's imports to the result of merge module specifier maps, given newImportMapImports and oldImportMap's imports.
  316. old_import_map.set_imports(merge_module_specifier_maps(realm, new_import_map_imports, old_import_map.imports()));
  317. }
  318. }