diff --git a/Cargo.lock b/Cargo.lock index f5b2d6f52..e7714625d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "cpufeatures" version = "0.2.11" @@ -242,6 +252,7 @@ dependencies = [ "data-url", "deno_ast", "deno_semver", + "encoding_rs", "futures", "import_map", "indexmap 2.0.0", @@ -264,6 +275,7 @@ name = "deno_graph_wasm" version = "0.0.0" dependencies = [ "anyhow", + "console_error_panic_hook", "deno_graph", "futures", "js-sys", @@ -337,6 +349,15 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 2dbd8120f..39eea2184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ async-trait = "0.1.68" data-url = "0.3.0" deno_ast = { version = "0.32.0", features = ["dep_analysis", "module_specifier"] } deno_semver = "0.5.4" +encoding_rs = "0.8.33" futures = "0.3.26" import_map = "0.18.0" indexmap = { version = "2", features = ["serde"] } diff --git a/deno.json b/deno.json index dfe8a4bc4..9e652829b 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "build": "cp LICENSE js/LICENSE && deno run --unstable -A --no-check https://deno.land/x/wasmbuild@0.15.1/main.ts --no-default-features --project deno_graph_wasm --out js", + "build": "cp LICENSE js/LICENSE && deno run -A --no-check https://deno.land/x/wasmbuild@0.15.1/main.ts --no-default-features --project deno_graph_wasm --out js", "build:npm": "deno run -A _build_npm.ts", "test": "deno test --allow-read --allow-net" }, diff --git a/js/mod.ts b/js/mod.ts index 4435c166a..573dcd291 100644 --- a/js/mod.ts +++ b/js/mod.ts @@ -40,6 +40,8 @@ export type { TypesDependency, } from "./types.ts"; +const encoder = new TextEncoder(); + // note: keep this in line with deno_cache export type CacheSetting = "only" | "use" | "reload"; @@ -139,7 +141,22 @@ export async function createGraph( const { createGraph } = await wasm.instantiate(); return await createGraph( rootSpecifiers, - load, + async ( + specifier: string, + isDynamic: boolean, + cacheSetting: CacheSetting, + ) => { + const result = await load(specifier, isDynamic, cacheSetting); + if (result?.kind === "module") { + if (typeof result.content === "string") { + result.content = encoder.encode(result.content); + } + // need to convert to an array for serde_wasm_bindgen to work + // deno-lint-ignore no-explicit-any + (result as any).content = Array.from(result.content); + } + return result; + }, defaultJsxImportSource, jsxImportSourceModule, cacheInfo, @@ -192,7 +209,7 @@ export async function init(opts?: wasm.InstantiateOptions) { */ export function parseModule( specifier: string, - content: string, + content: Uint8Array, options: ParseModuleOptions = {}, ): ModuleJson { const { diff --git a/js/test.ts b/js/test.ts index 32b015277..acffdffae 100644 --- a/js/test.ts +++ b/js/test.ts @@ -538,13 +538,13 @@ Deno.test({ await init(); const module = parseModule( "file:///a/test01.js", - ` + new TextEncoder().encode(` /// import { a } from "./a.ts"; import * as b from "./b.ts"; export { c } from "./c.ts"; const d = await import("./d.ts"); - `, + `), ); assertEquals(module, { "specifier": "file:///a/test01.js", @@ -609,9 +609,9 @@ Deno.test({ await init(); const module = parseModule( `https://example.com/a`, - `declare interface A { + new TextEncoder().encode(`declare interface A { a: string; - }`, + }`), { headers: { "content-type": "application/typescript; charset=utf-8", @@ -628,10 +628,10 @@ Deno.test({ await init(); const module = parseModule( `file:///a/test01.tsx`, - `/* @jsxImportSource http://example.com/preact */ + new TextEncoder().encode(`/* @jsxImportSource http://example.com/preact */ export function A() {
Hello Deno
- }`, + }`), { jsxImportSourceModule: "jsx-dev-runtime", }, @@ -651,10 +651,10 @@ Deno.test({ await init(); const module = parseModule( `file:///a/test01.tsx`, - ` + new TextEncoder().encode(` export function A() {
Hello Deno
- }`, + }`), { defaultJsxImportSource: "http://example.com/preact", }, @@ -673,7 +673,10 @@ Deno.test({ await init(); assertThrows( () => { - parseModule("./bad.ts", `console.log("hello");`); + parseModule( + "./bad.ts", + new TextEncoder().encode(`console.log("hello");`), + ); }, Error, "relative URL without a base", @@ -687,7 +690,10 @@ Deno.test({ await init(); assertThrows( () => { - parseModule("file:///a/test.md", `# Some Markdown\n\n**bold**`); + parseModule( + "file:///a/test.md", + new TextEncoder().encode(`# Some Markdown\n\n**bold**`), + ); }, Error, "The module's source code could not be parsed", @@ -701,10 +707,10 @@ Deno.test({ await init(); const module = parseModule( "file:///a/test01.js", - ` + new TextEncoder().encode(` import a from "./a.json" with { type: "json" }; await import("./b.json", { with: { type: "json" } }); - `, + `), ); assertEquals(module, { "dependencies": [ @@ -758,10 +764,10 @@ Deno.test({ await init(); const module = parseModule( "file:///a/foo.ts", - ` + new TextEncoder().encode(` /// /// - `, + `), ); assertEquals(module, { "dependencies": [ diff --git a/js/types.ts b/js/types.ts index e54c6a8cd..567f93d5e 100644 --- a/js/types.ts +++ b/js/types.ts @@ -34,7 +34,7 @@ export interface LoadResponseModule { * have been normalized to be lower case values. */ headers?: Record; /** The string value of the loaded resources. */ - content: string; + content: string | Uint8Array; } export interface LoadResponseExternal { diff --git a/lib/Cargo.toml b/lib/Cargo.toml index e7ab26da8..59ec66097 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1.0.43" +console_error_panic_hook = "0.1.7" deno_graph = { path = "../" } futures = "0.3.17" js-sys = "0.3.63" diff --git a/lib/lib.rs b/lib/lib.rs index b9c903ed6..3742f285a 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -197,6 +197,7 @@ pub async fn js_create_graph( maybe_graph_kind: Option, maybe_imports: JsValue, ) -> Result { + console_error_panic_hook::set_once(); let roots_vec: Vec = serde_wasm_bindgen::from_value(roots) .map_err(|err| JsValue::from(js_sys::Error::new(&err.to_string())))?; let maybe_imports_map: Option>> = @@ -274,10 +275,11 @@ pub fn js_parse_module( maybe_headers: JsValue, maybe_default_jsx_import_source: Option, maybe_jsx_import_source_module: Option, - content: String, + content: Vec, maybe_resolve: Option, maybe_resolve_types: Option, ) -> Result { + console_error_panic_hook::set_once(); let maybe_headers: Option> = serde_wasm_bindgen::from_value(maybe_headers) .map_err(|err| js_sys::Error::new(&err.to_string()))?; diff --git a/src/fast_check/range_finder.rs b/src/fast_check/range_finder.rs index 9435dcba5..3c439351c 100644 --- a/src/fast_check/range_finder.rs +++ b/src/fast_check/range_finder.rs @@ -1025,7 +1025,7 @@ impl<'a> PublicRangeFinder<'a> { return true; // just analyze it }; match module { - crate::Module::Esm(m) => is_typed_media_type(m.media_type), + crate::Module::Js(m) => is_typed_media_type(m.media_type), crate::Module::Json(_) => true, crate::Module::Npm(_) | crate::Module::Node(_) diff --git a/src/graph.rs b/src/graph.rs index ca7ad8743..459eecfe7 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -693,7 +693,9 @@ pub struct WorkspaceMember { #[serde(rename_all = "camelCase")] #[serde(tag = "kind")] pub enum Module { - Esm(EsModule), + // todo(#239): remove this when updating the --json output for 2.0 + #[serde(rename = "esm")] + Js(JsModule), // todo(#239): remove this when updating the --json output for 2.0 #[serde(rename = "asserted")] Json(JsonModule), @@ -705,7 +707,7 @@ pub enum Module { impl Module { pub fn specifier(&self) -> &ModuleSpecifier { match self { - Module::Esm(module) => &module.specifier, + Module::Js(module) => &module.specifier, Module::Json(module) => &module.specifier, Module::Npm(module) => &module.specifier, Module::Node(module) => &module.specifier, @@ -721,8 +723,8 @@ impl Module { } } - pub fn esm(&self) -> Option<&EsModule> { - if let Module::Esm(module) = &self { + pub fn js(&self) -> Option<&JsModule> { + if let Module::Js(module) = &self { Some(module) } else { None @@ -817,7 +819,7 @@ pub struct FastCheckTypeModule { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct EsModule { +pub struct JsModule { #[serde( skip_serializing_if = "IndexMap::is_empty", serialize_with = "serialize_dependencies" @@ -836,7 +838,7 @@ pub struct EsModule { pub fast_check: Option, } -impl EsModule { +impl JsModule { fn new(specifier: ModuleSpecifier, source: Arc) -> Self { Self { dependencies: Default::default(), @@ -1095,7 +1097,7 @@ impl<'a> Iterator for ModuleEntryIterator<'a> { fn next(&mut self) -> Option { match self.previous_module.take() { Some(ModuleEntryRef::Module(module)) => match module { - Module::Esm(module) => { + Module::Js(module) => { let check_types = (self.check_js || !matches!( module.media_type, @@ -1201,7 +1203,7 @@ impl<'a> ModuleGraphErrorIterator<'a> { fn check_resolution( &self, - module: &EsModule, + module: &JsModule, mode: ResolutionMode, specifier_text: &str, resolution: &Resolution, @@ -1278,7 +1280,7 @@ impl<'a> Iterator for ModuleGraphErrorIterator<'a> { if let Some((_, module_entry)) = self.iterator.next() { match module_entry { - ModuleEntryRef::Module(Module::Esm(module)) => { + ModuleEntryRef::Module(Module::Js(module)) => { let check_types = (check_js || !matches!( module.media_type, @@ -1583,7 +1585,7 @@ impl ModuleGraph { prefer_types: bool, ) -> Option { match referring_module { - Module::Esm(referring_module) => { + Module::Js(referring_module) => { let dependency = referring_module.dependencies.get(specifier)?; self.resolve_dependency_from_dep(dependency, prefer_types) } @@ -1611,7 +1613,7 @@ impl ModuleGraph { // Even if we resolved the specifier, it doesn't mean the module is actually // there, so check in the module slots match self.module_slots.get(&resolved_specifier) { - Some(ModuleSlot::Module(Module::Esm(module))) if prefer_types => { + Some(ModuleSlot::Module(Module::Js(module))) if prefer_types => { // check for if this module has a types dependency if let Some(Resolution::Ok(resolved)) = module .maybe_types_dependency @@ -1676,7 +1678,7 @@ impl ModuleGraph { return Ok(None); }; - if let Some(specifier) = module.esm().and_then(|m| { + if let Some(specifier) = module.js().and_then(|m| { m.maybe_types_dependency .as_ref() .and_then(|d| d.dependency.ok()) @@ -1813,7 +1815,7 @@ pub(crate) fn parse_module( graph_kind: GraphKind, specifier: &ModuleSpecifier, maybe_headers: Option<&HashMap>, - content: Arc, + content: Arc<[u8]>, maybe_attribute_type: Option, maybe_referrer: Option, file_system: &dyn FileSystem, @@ -1823,8 +1825,8 @@ pub(crate) fn parse_module( is_dynamic_branch: bool, maybe_npm_resolver: Option<&dyn NpmResolver>, ) -> Result { - let media_type = - MediaType::from_specifier_and_headers(specifier, maybe_headers); + let (media_type, maybe_charset) = + resolve_media_type_and_charset_from_headers(specifier, maybe_headers); // here we check any media types that should have assertions made against them // if they aren't the root and add them to the graph, otherwise we continue @@ -1836,9 +1838,13 @@ pub(crate) fn parse_module( Some("json") )) { + let text = crate::source::decode_source(specifier, content, maybe_charset) + .map_err(|err| { + ModuleError::LoadingErr(specifier.clone(), None, Arc::new(err.into())) + })?; return Ok(Module::Json(JsonModule { maybe_cache_info: None, - source: content, + source: text, media_type: MediaType::Json, specifier: specifier.clone(), })); @@ -1862,7 +1868,7 @@ pub(crate) fn parse_module( } // Here we check for known ES Modules that we will analyze the dependencies of - match &media_type { + match media_type { MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs @@ -1874,16 +1880,18 @@ pub(crate) fn parse_module( | MediaType::Dts | MediaType::Dmts | MediaType::Dcts => { - match module_analyzer.analyze(specifier, content.clone(), media_type) { + let source = new_source_with_text(specifier, content, maybe_charset) + .map_err(|err| *err)?; + match module_analyzer.analyze(specifier, source.clone(), media_type) { Ok(module_info) => { // Return the module as a valid module - Ok(Module::Esm(parse_es_module_from_module_info( + Ok(Module::Js(parse_js_module_from_module_info( graph_kind, specifier, media_type, maybe_headers, module_info, - content, + source, file_system, maybe_resolver, maybe_npm_resolver, @@ -1895,20 +1903,22 @@ pub(crate) fn parse_module( } } MediaType::Unknown if is_root => { + let source = new_source_with_text(specifier, content, maybe_charset) + .map_err(|err| *err)?; match module_analyzer.analyze( specifier, - content.clone(), + source.clone(), MediaType::JavaScript, ) { Ok(module_info) => { // Return the module as a valid module - Ok(Module::Esm(parse_es_module_from_module_info( + Ok(Module::Js(parse_js_module_from_module_info( graph_kind, specifier, media_type, maybe_headers, module_info, - content, + source, file_system, maybe_resolver, maybe_npm_resolver, @@ -1919,7 +1929,11 @@ pub(crate) fn parse_module( } } } - _ => Err(ModuleError::UnsupportedMediaType( + MediaType::Json + | MediaType::Wasm + | MediaType::TsBuildInfo + | MediaType::SourceMap + | MediaType::Unknown => Err(ModuleError::UnsupportedMediaType( specifier.clone(), media_type, maybe_referrer, @@ -1928,7 +1942,7 @@ pub(crate) fn parse_module( } #[allow(clippy::too_many_arguments)] -pub(crate) fn parse_es_module_from_module_info( +pub(crate) fn parse_js_module_from_module_info( graph_kind: GraphKind, specifier: &ModuleSpecifier, media_type: MediaType, @@ -1938,8 +1952,8 @@ pub(crate) fn parse_es_module_from_module_info( file_system: &dyn FileSystem, maybe_resolver: Option<&dyn Resolver>, maybe_npm_resolver: Option<&dyn NpmResolver>, -) -> EsModule { - let mut module = EsModule::new(specifier.clone(), source); +) -> JsModule { + let mut module = JsModule::new(specifier.clone(), source); module.media_type = media_type; // Analyze the TypeScript triple-slash references @@ -2656,7 +2670,7 @@ struct PendingState { #[derive(Clone)] enum ContentOrModuleInfo { - Content(Arc), + Content(Arc<[u8]>), ModuleInfo(ModuleInfo), } @@ -3134,11 +3148,29 @@ impl<'a, 'graph> Builder<'a, 'graph> { match slot { ModuleSlot::Module(module) => { match module { - Module::Esm(module) => { - module.source = content; + Module::Js(module) => { + match new_source_with_text( + &module.specifier, + content, + None, // no charset for JSR + ) { + Ok(source) => { + module.source = source; + } + Err(err) => *slot = ModuleSlot::Err(*err), + } } Module::Json(module) => { - module.source = content; + match new_source_with_text( + &module.specifier, + content, + None, // no charset for JSR + ) { + Ok(source) => { + module.source = source; + } + Err(err) => *slot = ModuleSlot::Err(*err), + } } Module::Npm(_) | Module::Node(_) @@ -3201,7 +3233,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { module.maybe_cache_info = self.loader.get_cache_info(&module.specifier); } - Module::Esm(module) => { + Module::Js(module) => { module.maybe_cache_info = self.loader.get_cache_info(&module.specifier); } @@ -3622,7 +3654,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { match data { Some(LoadResponse::Module { content, .. }) => { let package_info: JsrPackageInfo = - serde_json::from_str(&content).map_err(|e| Arc::new(e.into()))?; + serde_json::from_slice(&content).map_err(|e| Arc::new(e.into()))?; Ok(Some(Arc::new(package_info))) } _ => Ok(None), @@ -3664,7 +3696,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { match data { Some(LoadResponse::Module { content, .. }) => { let version_info: JsrPackageVersionInfo = - serde_json::from_str(&content).map_err(|e| Arc::new(e.into()))?; + serde_json::from_slice(&content).map_err(|e| Arc::new(e.into()))?; Ok(Arc::new(version_info)) } _ => Err(Arc::new(anyhow!("Not found: {}", specifier))), @@ -3779,7 +3811,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { .boxed_local() }); ( - String::new().into(), + Arc::new([]) as Arc<[u8]>, Some(ProvidedModuleAnalyzer(RefCell::new(Some(info)))), ) } @@ -3806,7 +3838,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { Err(err) => ModuleSlot::Err(err), }; - if let ModuleSlot::Module(Module::Esm(module)) = module_slot.borrow_mut() { + if let ModuleSlot::Module(Module::Js(module)) = module_slot.borrow_mut() { if matches!(self.graph.graph_kind, GraphKind::All | GraphKind::CodeOnly) || module.maybe_types_dependency.is_none() { @@ -3937,7 +3969,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { let module_slot = self.graph.module_slots.get_mut(&specifier).unwrap(); let module = match module_slot { ModuleSlot::Module(m) => match m { - Module::Esm(m) => m, + Module::Js(m) => m, _ => continue, }, ModuleSlot::Err(_) | ModuleSlot::Pending => continue, @@ -4215,6 +4247,20 @@ impl<'a> NpmSpecifierResolver<'a> { } } +fn new_source_with_text( + specifier: &ModuleSpecifier, + text: Arc<[u8]>, + maybe_charset: Option<&str>, +) -> Result, Box> { + crate::source::decode_source(specifier, text, maybe_charset).map_err(|err| { + Box::new(ModuleError::LoadingErr( + specifier.clone(), + None, + Arc::new(err.into()), + )) + }) +} + impl Serialize for Resolution { fn serialize(&self, serializer: S) -> Result where @@ -4321,13 +4367,14 @@ mod tests { fn test_module_dependency_includes() { let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap(); let module_analyzer = DefaultModuleAnalyzer::default(); - let content = - "import * as b from \"./b.ts\";\nimport * as c from \"./b.ts\""; + let content: Arc<[u8]> = Arc::from( + b"import * as b from \"./b.ts\";\nimport * as c from \"./b.ts\"".to_vec(), + ); let module = parse_module( GraphKind::All, &specifier, None, - content.into(), + content, None, None, &NullFileSystem, @@ -4338,7 +4385,7 @@ mod tests { None, ) .unwrap(); - let module = module.esm().unwrap(); + let module = module.js().unwrap(); assert_eq!(module.dependencies.len(), 1); let dependency = module.dependencies.first().unwrap().1; assert_eq!( @@ -4424,7 +4471,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "await import('file:///bar.js')".into(), + content: b"await import('file:///bar.js')".to_vec().into(), })) }) } @@ -4435,7 +4482,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "import 'file:///baz.js'".into(), + content: b"import 'file:///baz.js'".to_vec().into(), })) }) } @@ -4446,7 +4493,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "console.log('Hello, world!')".into(), + content: b"console.log('Hello, world!')".to_vec().into(), })) }) } @@ -4490,7 +4537,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "await import('file:///bar.js')".into(), + content: b"await import('file:///bar.js')".to_vec().into(), })) }), "file:///bar.js" => Box::pin(async move { Ok(None) }), @@ -4577,14 +4624,14 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: Url::parse("file:///foo_actual.js").unwrap(), maybe_headers: None, - content: "import 'file:///bar.js'".into(), + content: b"import 'file:///bar.js'".to_vec().into(), })) }), "file:///bar.js" => Box::pin(async move { Ok(Some(LoadResponse::Module { specifier: Url::parse("file:///bar_actual.js").unwrap(), maybe_headers: None, - content: "(".into(), + content: b"(".to_vec().into(), })) }), _ => unreachable!(), @@ -4646,29 +4693,29 @@ mod tests { specifier: specifier.clone(), maybe_headers: None, content: - "import 'FILE:///baz.js'; import 'file:///bar.js'; import 'http://deno.land/foo.js';" - .into(), + b"import 'FILE:///baz.js'; import 'file:///bar.js'; import 'http://deno.land/foo.js';" + .to_vec().into(), })) }), "http://deno.land/foo.js" => Box::pin(async move { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "export {}".into(), + content: b"export {}".to_vec().into(), })) }), "file:///bar.js" => Box::pin(async move { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "console.log('Hello, world!')".into(), + content: b"console.log('Hello, world!')".to_vec().into(), })) }), "file:///baz.js" => Box::pin(async move { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "console.log('Hello, world 2!')".into(), + content: b"console.log('Hello, world 2!')".to_vec().into(), })) }), _ => unreachable!(), @@ -4771,7 +4818,9 @@ mod tests { specifier: specifier.clone(), maybe_headers: None, content: - "import 'file:///bar.js'; await import('file:///bar.js')".into(), + b"import 'file:///bar.js'; await import('file:///bar.js')" + .to_vec() + .into(), })) }), "file:///bar.js" => { @@ -4781,7 +4830,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "console.log('Hello, world!')".into(), + content: b"console.log('Hello, world!')".to_vec().into(), })) }) } @@ -4817,7 +4866,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: " + content: b" /// /// /* @jsxImportSource file:///bar.ts */ @@ -4828,6 +4877,7 @@ mod tests { import type {} from 'file:///bar.ts'; /** @typedef { import('file:///bar.ts') } bar */ " + .to_vec() .into(), })) }), @@ -4837,7 +4887,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "".into(), + content: b"".to_vec().into(), })) }) } @@ -4847,7 +4897,7 @@ mod tests { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "{}".into(), + content: b"{}".to_vec().into(), })) }) } @@ -4865,7 +4915,7 @@ mod tests { .await; graph.valid().unwrap(); let module = graph.get(&Url::parse("file:///foo.ts").unwrap()).unwrap(); - let module = module.esm().unwrap(); + let module = module.js().unwrap(); let dependency_a = module.dependencies.get("file:///bar.ts").unwrap(); let dependency_b = module.dependencies.get("file:///baz.json").unwrap(); assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index f01408ea2..21eb2d4a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,6 @@ use source::NpmResolver; use source::Resolver; use std::collections::HashMap; - use std::sync::Arc; pub use analyzer::analyze_deno_types; @@ -50,12 +49,12 @@ pub use graph::BuildDiagnostic; pub use graph::BuildOptions; pub use graph::Dependency; pub use graph::DiagnosticRange; -pub use graph::EsModule; pub use graph::ExternalModule; pub use graph::FastCheckTypeModule; pub use graph::FastCheckTypeModuleSlot; pub use graph::GraphImport; pub use graph::GraphKind; +pub use graph::JsModule; pub use graph::JsonModule; pub use graph::Module; pub use graph::ModuleEntryRef; @@ -92,7 +91,7 @@ pub struct ParseModuleOptions<'a> { pub graph_kind: GraphKind, pub specifier: &'a ModuleSpecifier, pub maybe_headers: Option<&'a HashMap>, - pub content: Arc, + pub content: Arc<[u8]>, pub file_system: &'a dyn FileSystem, pub maybe_resolver: Option<&'a dyn Resolver>, pub maybe_module_analyzer: Option<&'a dyn ModuleAnalyzer>, @@ -136,8 +135,8 @@ pub struct ParseModuleFromAstOptions<'a> { } /// Parse an individual module from an AST, returning the module. -pub fn parse_module_from_ast(options: ParseModuleFromAstOptions) -> EsModule { - graph::parse_es_module_from_module_info( +pub fn parse_module_from_ast(options: ParseModuleFromAstOptions) -> JsModule { + graph::parse_js_module_from_module_info( options.graph_kind, options.specifier, options.parsed_source.media_type(), @@ -220,7 +219,7 @@ mod tests { .unwrap() .module() .unwrap() - .esm() + .js() .unwrap(); assert_eq!(module.dependencies.len(), 1); let maybe_dependency = module.dependencies.get("./test02.ts"); @@ -1007,7 +1006,7 @@ console.log(a); .unwrap() .module() .unwrap() - .esm() + .js() .unwrap(); assert_eq!(module.media_type, MediaType::TypeScript); } @@ -1769,9 +1768,11 @@ export function a(a) { .await; assert_eq!(graph.module_slots.len(), 3); let data_specifier = ModuleSpecifier::parse("data:application/typescript,export%20*%20from%20%22https://example.com/c.ts%22;").unwrap(); - let module = graph.get(&data_specifier).unwrap().esm().unwrap(); - let source: &str = &module.source; - assert_eq!(source, r#"export * from "https://example.com/c.ts";"#,); + let module = graph.get(&data_specifier).unwrap().js().unwrap(); + assert_eq!( + module.source.as_ref(), + r#"export * from "https://example.com/c.ts";"#, + ); } #[tokio::test] @@ -1814,7 +1815,7 @@ export function a(a) { }, ) .await; - let module = graph.get(&graph.roots[0]).unwrap().esm().unwrap(); + let module = graph.get(&graph.roots[0]).unwrap().js().unwrap(); let maybe_dep = module.dependencies.get("b"); assert!(maybe_dep.is_some()); let dep = maybe_dep.unwrap(); @@ -1874,7 +1875,7 @@ export function a(a) { }, ) .await; - let module = graph.get(&graph.roots[0]).unwrap().esm().unwrap(); + let module = graph.get(&graph.roots[0]).unwrap().js().unwrap(); let types_dep = module.maybe_types_dependency.as_ref().unwrap(); assert_eq!(types_dep.specifier, "file:///a.js"); assert_eq!( @@ -3007,7 +3008,7 @@ export function a(a) { #[test] fn test_parse_module() { let specifier = ModuleSpecifier::parse("file:///a/test01.ts").unwrap(); - let code = r#" + let code = br#" /// import { a } from "./a.ts"; import * as b from "./b.ts"; @@ -3020,14 +3021,14 @@ export function a(a) { graph_kind: GraphKind::All, specifier: &specifier, maybe_headers: None, - content: code.into(), + content: code.to_vec().into(), file_system: &NullFileSystem, maybe_resolver: None, maybe_module_analyzer: None, maybe_npm_resolver: None, }) .unwrap(); - let actual = actual.esm().unwrap(); + let actual = actual.js().unwrap(); assert_eq!(actual.dependencies.len(), 7); assert_eq!(actual.specifier, specifier); assert_eq!(actual.media_type, MediaType::TypeScript); @@ -3037,14 +3038,14 @@ export function a(a) { graph_kind: GraphKind::CodeOnly, specifier: &specifier, maybe_headers: None, - content: code.into(), + content: code.to_vec().into(), file_system: &NullFileSystem, maybe_resolver: None, maybe_module_analyzer: None, maybe_npm_resolver: None, }) .unwrap(); - let actual = actual.esm().unwrap(); + let actual = actual.js().unwrap(); assert_eq!(actual.dependencies.len(), 4); } @@ -3055,10 +3056,11 @@ export function a(a) { graph_kind: GraphKind::All, specifier: &specifier, maybe_headers: None, - content: r#" + content: br#" import a from "./a.json" assert { type: "json" }; await import("./b.json", { assert: { type: "json" } }); "# + .to_vec() .into(), file_system: &NullFileSystem, maybe_resolver: None, @@ -3121,13 +3123,14 @@ export function a(a) { graph_kind: GraphKind::All, specifier: &specifier, maybe_headers: None, - content: r#" + content: br#" /** @jsxImportSource https://example.com/preact */ export function A() { return
Hello Deno
; } "# + .to_vec() .into(), file_system: &NullFileSystem, maybe_resolver: None, @@ -3135,7 +3138,7 @@ export function a(a) { maybe_npm_resolver: None, }) .unwrap(); - let actual = actual.esm().unwrap(); + let actual = actual.js().unwrap(); assert_eq!(actual.dependencies.len(), 1); let dep = actual .dependencies @@ -3165,11 +3168,12 @@ export function a(a) { graph_kind: GraphKind::All, specifier: &specifier, maybe_headers: None, - content: r#" + content: br#" export function A() { return
Hello Deno
; } "# + .to_vec() .into(), file_system: &NullFileSystem, maybe_resolver: Some(&R), @@ -3177,7 +3181,7 @@ export function a(a) { maybe_npm_resolver: None, }) .unwrap(); - let actual = actual.esm().unwrap(); + let actual = actual.js().unwrap(); assert_eq!(actual.dependencies.len(), 1); let dep = actual .dependencies @@ -3205,9 +3209,10 @@ export function a(a) { graph_kind: GraphKind::All, specifier: &specifier, maybe_headers, - content: r#"declare interface A { + content: br#"declare interface A { a: string; }"# + .to_vec() .into(), file_system: &NullFileSystem, maybe_resolver: None, @@ -3220,7 +3225,7 @@ export function a(a) { #[test] fn test_parse_module_with_jsdoc_imports() { let specifier = ModuleSpecifier::parse("file:///a/test.js").unwrap(); - let code = r#" + let code = br#" /** * Some js doc * @@ -3235,7 +3240,7 @@ export function a(a) { graph_kind: GraphKind::All, specifier: &specifier, maybe_headers: None, - content: code.into(), + content: code.to_vec().into(), file_system: &NullFileSystem, maybe_resolver: None, maybe_module_analyzer: None, @@ -3291,7 +3296,7 @@ export function a(a) { graph_kind: GraphKind::CodeOnly, specifier: &specifier, maybe_headers: None, - content: code.into(), + content: code.to_vec().into(), file_system: &NullFileSystem, maybe_resolver: None, maybe_module_analyzer: None, @@ -3316,7 +3321,7 @@ export function a(a) { graph_kind: GraphKind::All, specifier: &specifier, maybe_headers: None, - content: r#" + content: br#" /** * Some js doc * @@ -3327,6 +3332,7 @@ export function a(a: A): B { return; } "# + .to_vec() .into(), file_system: &NullFileSystem, maybe_resolver: None, diff --git a/src/source/mod.rs b/src/source/mod.rs index 01faf3beb..10fdc22a2 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -4,9 +4,10 @@ use crate::graph::Range; use crate::module_specifier::resolve_import; use crate::packages::JsrPackageInfo; use crate::packages::JsrPackageVersionInfo; -use crate::text_encoding::strip_bom_mut; +use crate::text_encoding; use crate::ModuleInfo; use crate::SpecifierError; +use deno_ast::MediaType; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; @@ -19,6 +20,7 @@ use futures::future::LocalBoxFuture; use once_cell::sync::Lazy; use serde::Deserialize; use serde::Serialize; +use std::borrow::Cow; use std::collections::HashMap; use std::fmt; use std::path::PathBuf; @@ -61,7 +63,7 @@ pub enum LoadResponse { /// A loaded module. Module { /// The content of the remote module. - content: Arc, + content: Arc<[u8]>, /// The final specifier of the module. specifier: ModuleSpecifier, /// If the module is a remote module, the headers should be returned as a @@ -142,7 +144,7 @@ pub trait Loader { fn cache_module_info( &mut self, _specifier: &ModuleSpecifier, - _source: &str, + _source: &Arc<[u8]>, _module_info: &ModuleInfo, ) { } @@ -311,12 +313,10 @@ pub fn load_data_url( .map_err(|_| anyhow!("Unable to decode data url."))?; let mut headers: HashMap = HashMap::with_capacity(1); headers.insert("content-type".to_string(), url.mime_type().to_string()); - let mut content = String::from_utf8(bytes)?; - strip_bom_mut(&mut content); Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: Some(headers), - content: content.into(), + content: Arc::from(bytes), })) } @@ -352,7 +352,7 @@ impl> Source { .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string())) .collect() }), - content: content.as_ref().into(), + content: Arc::from(content.as_ref().to_string().into_bytes()), }), Source::External(specifier) => Ok(LoadResponse::External { specifier: ModuleSpecifier::parse(specifier.as_ref()).unwrap(), @@ -480,6 +480,84 @@ pub trait Reporter: fmt::Debug { ); } +/// Resolve a media type and optionally the charset from a module specifier and +/// the value of a content type header. +pub fn resolve_media_type_and_charset_from_headers<'a>( + specifier: &ModuleSpecifier, + maybe_headers: Option<&'a HashMap>, +) -> (MediaType, Option<&'a str>) { + resolve_media_type_and_charset_from_content_type( + specifier, + maybe_headers.and_then(|h| h.get("content-type")), + ) +} + +/// Resolve a media type and optionally the charset from a module specifier and +/// the value of a content type header. +pub fn resolve_media_type_and_charset_from_content_type<'a>( + specifier: &ModuleSpecifier, + maybe_content_type: Option<&'a String>, +) -> (MediaType, Option<&'a str>) { + if let Some(content_type) = maybe_content_type { + let mut content_types = content_type.split(';'); + let content_type = content_types.next().unwrap(); + let media_type = MediaType::from_content_type(specifier, content_type); + let charset = content_types + .map(str::trim) + .find_map(|s| s.strip_prefix("charset=")); + + (media_type, charset) + } else { + (MediaType::from_specifier(specifier), None) + } +} + +/// Decodes the source bytes into a string handling any encoding rules +/// for local vs remote files and dealing with the charset. +pub fn decode_source( + specifier: &ModuleSpecifier, + bytes: Arc<[u8]>, + maybe_charset: Option<&str>, +) -> Result, std::io::Error> { + let charset = maybe_charset.unwrap_or_else(|| { + if specifier.scheme() == "file" { + text_encoding::detect_charset(bytes.as_ref()) + } else { + "utf-8" + } + }); + decode_with_charset(bytes, charset) +} + +fn decode_with_charset( + bytes: Arc<[u8]>, + charset: &str, +) -> Result, std::io::Error> { + let text = match text_encoding::convert_to_utf8(bytes.as_ref(), charset)? { + Cow::Borrowed(text) => { + if text.starts_with(text_encoding::BOM_CHAR) { + text[..text_encoding::BOM_CHAR.len_utf8()].to_string() + } else { + return Ok( + // SAFETY: we know it's a valid utf-8 string at this point + unsafe { + let raw_ptr = Arc::into_raw(bytes); + Arc::from_raw(std::mem::transmute::<*const [u8], *const str>( + raw_ptr, + )) + }, + ); + } + } + Cow::Owned(mut text) => { + text_encoding::strip_bom_mut(&mut text); + text + } + }; + let text: Arc = Arc::from(text); + Ok(text) +} + #[cfg(test)] pub mod tests { use super::*; @@ -562,4 +640,192 @@ pub mod tests { } ); } + + macro_rules! file_url { + ($path:expr) => { + if cfg!(target_os = "windows") { + concat!("file:///C:", $path) + } else { + concat!("file://", $path) + } + }; + } + + #[test] + fn test_resolve_media_type_and_charset_from_content_type() { + let fixtures = vec![ + // Extension only + (file_url!("/foo/bar.ts"), None, MediaType::TypeScript, None), + (file_url!("/foo/bar.tsx"), None, MediaType::Tsx, None), + (file_url!("/foo/bar.d.cts"), None, MediaType::Dcts, None), + (file_url!("/foo/bar.d.mts"), None, MediaType::Dmts, None), + (file_url!("/foo/bar.d.ts"), None, MediaType::Dts, None), + (file_url!("/foo/bar.js"), None, MediaType::JavaScript, None), + (file_url!("/foo/bar.jsx"), None, MediaType::Jsx, None), + (file_url!("/foo/bar.json"), None, MediaType::Json, None), + (file_url!("/foo/bar.wasm"), None, MediaType::Wasm, None), + (file_url!("/foo/bar.cjs"), None, MediaType::Cjs, None), + (file_url!("/foo/bar.mjs"), None, MediaType::Mjs, None), + (file_url!("/foo/bar.cts"), None, MediaType::Cts, None), + (file_url!("/foo/bar.mts"), None, MediaType::Mts, None), + (file_url!("/foo/bar"), None, MediaType::Unknown, None), + // Media type no extension + ( + "https://deno.land/x/mod", + Some("application/typescript".to_string()), + MediaType::TypeScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/typescript".to_string()), + MediaType::TypeScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("video/vnd.dlna.mpeg-tts".to_string()), + MediaType::TypeScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("video/mp2t".to_string()), + MediaType::TypeScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("application/x-typescript".to_string()), + MediaType::TypeScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("application/javascript".to_string()), + MediaType::JavaScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/javascript".to_string()), + MediaType::JavaScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("application/ecmascript".to_string()), + MediaType::JavaScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/ecmascript".to_string()), + MediaType::JavaScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("application/x-javascript".to_string()), + MediaType::JavaScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("application/node".to_string()), + MediaType::JavaScript, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/jsx".to_string()), + MediaType::Jsx, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/tsx".to_string()), + MediaType::Tsx, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/json".to_string()), + MediaType::Json, + None, + ), + ( + "https://deno.land/x/mod", + Some("text/json; charset=utf-8".to_string()), + MediaType::Json, + Some("utf-8".to_string()), + ), + // Extension with media type + ( + "https://deno.land/x/mod.ts", + Some("text/plain".to_string()), + MediaType::TypeScript, + None, + ), + ( + "https://deno.land/x/mod.ts", + Some("foo/bar".to_string()), + MediaType::Unknown, + None, + ), + ( + "https://deno.land/x/mod.tsx", + Some("application/typescript".to_string()), + MediaType::Tsx, + None, + ), + ( + "https://deno.land/x/mod.tsx", + Some("application/javascript".to_string()), + MediaType::Tsx, + None, + ), + ( + "https://deno.land/x/mod.jsx", + Some("application/javascript".to_string()), + MediaType::Jsx, + None, + ), + ( + "https://deno.land/x/mod.jsx", + Some("application/x-typescript".to_string()), + MediaType::Jsx, + None, + ), + ( + "https://deno.land/x/mod.d.ts", + Some("application/javascript".to_string()), + MediaType::Dts, + None, + ), + ( + "https://deno.land/x/mod.d.ts", + Some("text/plain".to_string()), + MediaType::Dts, + None, + ), + ( + "https://deno.land/x/mod.d.ts", + Some("application/x-typescript".to_string()), + MediaType::Dts, + None, + ), + ]; + + for (specifier, maybe_content_type, media_type, maybe_charset) in fixtures { + let specifier = ModuleSpecifier::parse(specifier).unwrap(); + assert_eq!( + resolve_media_type_and_charset_from_content_type( + &specifier, + maybe_content_type.as_ref() + ), + (media_type, maybe_charset.as_deref()) + ); + } + } } diff --git a/src/symbols/analyzer.rs b/src/symbols/analyzer.rs index ab3c665fc..43edf616d 100644 --- a/src/symbols/analyzer.rs +++ b/src/symbols/analyzer.rs @@ -6,7 +6,6 @@ use std::cell::Ref; use std::cell::RefCell; use std::hash::Hash; -use anyhow::Result; use deno_ast::swc::ast::*; use deno_ast::swc::utils::find_pat_ids; use deno_ast::ModuleSpecifier; @@ -17,7 +16,7 @@ use deno_ast::SourceTextInfo; use indexmap::IndexMap; use indexmap::IndexSet; -use crate::EsModule; +use crate::JsModule; use crate::JsonModule; use crate::ModuleGraph; use crate::ModuleParser; @@ -83,7 +82,7 @@ impl<'a> RootSymbol<'a> { }; match graph_module { - crate::Module::Esm(es_module) => self.analyze_es_module(es_module), + crate::Module::Js(js_module) => self.analyze_js_module(js_module), crate::Module::Json(json_module) => { Some(self.analyze_json_module(json_module)) } @@ -151,11 +150,14 @@ impl<'a> RootSymbol<'a> { ) } - fn analyze_es_module(&self, es_module: &EsModule) -> Option { - let Ok(source) = self.parsed_source(es_module) else { + fn analyze_js_module( + &self, + script_module: &JsModule, + ) -> Option { + let Ok(source) = self.parsed_source(script_module) else { return None; }; - let specifier = &es_module.specifier; + let specifier = &script_module.specifier; let module = source.module(); let module_id = ModuleId(self.ids_to_modules.len() as u32); @@ -240,7 +242,7 @@ impl<'a> RootSymbol<'a> { fn parsed_source( &self, - graph_module: &EsModule, + graph_module: &JsModule, ) -> Result { self.parser.parse_module(ParseOptions { specifier: &graph_module.specifier, diff --git a/src/text_encoding.rs b/src/text_encoding.rs index 221a3d17a..b1433c6a6 100644 --- a/src/text_encoding.rs +++ b/src/text_encoding.rs @@ -1,5 +1,44 @@ +use std::borrow::Cow; + pub const BOM_CHAR: char = '\u{FEFF}'; +/// Attempts to detect the character encoding of the provided bytes. +/// +/// Supports UTF-8, UTF-16 Little Endian and UTF-16 Big Endian. +pub fn detect_charset(bytes: &'_ [u8]) -> &'static str { + const UTF16_LE_BOM: &[u8] = b"\xFF\xFE"; + const UTF16_BE_BOM: &[u8] = b"\xFE\xFF"; + + if bytes.starts_with(UTF16_LE_BOM) { + "utf-16le" + } else if bytes.starts_with(UTF16_BE_BOM) { + "utf-16be" + } else { + // Assume everything else is utf-8 + "utf-8" + } +} + +/// Attempts to convert the provided bytes to a UTF-8 string. +/// +/// Supports all encodings supported by the encoding_rs crate, which includes +/// all encodings specified in the WHATWG Encoding Standard, and only those +/// encodings (see: ). +pub fn convert_to_utf8<'a>( + bytes: &'a [u8], + charset: &'_ str, +) -> Result, std::io::Error> { + match encoding_rs::Encoding::for_label(charset.as_bytes()) { + Some(encoding) => encoding + .decode_without_bom_handling_and_without_replacement(bytes) + .ok_or_else(|| std::io::ErrorKind::InvalidData.into()), + None => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Unsupported charset: {charset}"), + )), + } +} + /// Strips the byte order mark if it exists from the provided text. pub fn strip_bom_mut(text: &mut String) { if text.starts_with(BOM_CHAR) { @@ -11,6 +50,34 @@ pub fn strip_bom_mut(text: &mut String) { mod test { use super::*; + fn test_detection(test_data: &[u8], expected_charset: &str) { + let detected_charset = detect_charset(test_data); + assert_eq!( + expected_charset.to_lowercase(), + detected_charset.to_lowercase() + ); + } + + #[test] + fn test_detection_utf8_no_bom() { + let test_data = "Hello UTF-8 it is \u{23F0} for Deno!" + .to_owned() + .into_bytes(); + test_detection(&test_data, "utf-8"); + } + + #[test] + fn test_detection_utf16_little_endian() { + let test_data = b"\xFF\xFEHello UTF-16LE".to_owned().to_vec(); + test_detection(&test_data, "utf-16le"); + } + + #[test] + fn test_detection_utf16_big_endian() { + let test_data = b"\xFE\xFFHello UTF-16BE".to_owned().to_vec(); + test_detection(&test_data, "utf-16be"); + } + #[test] fn strip_bom_mut_with_bom() { let mut text = format!("{BOM_CHAR}text"); diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 89597a380..93b35bb13 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -89,7 +89,7 @@ async fn test_graph_specs() { } // now the fast check modules let fast_check_modules = result.graph.modules().filter_map(|module| { - let module = module.esm()?; + let module = module.js()?; let fast_check = module.fast_check.as_ref()?; Some((module, fast_check)) }); @@ -446,7 +446,7 @@ async fn test_jsr_version_not_found_then_found() { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "import 'jsr:@scope/a@1.2".into(), + content: b"import 'jsr:@scope/a@1.2".to_vec().into(), })) }), "https://jsr.io/@scope/a/meta.json" => { @@ -457,11 +457,11 @@ async fn test_jsr_version_not_found_then_found() { content: match cache_setting { CacheSetting::Only | CacheSetting::Use => { // first time it won't have the version - r#"{ "versions": { "1.0.0": {} } }"#.into() + br#"{ "versions": { "1.0.0": {} } }"#.to_vec().into() } CacheSetting::Reload => { // then on reload it will - r#"{ "versions": { "1.0.0": {}, "1.2.0": {} } }"#.into() + br#"{ "versions": { "1.0.0": {}, "1.2.0": {} } }"#.to_vec().into() } }, })) @@ -471,14 +471,14 @@ async fn test_jsr_version_not_found_then_found() { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: r#"{ "exports": { ".": "./mod.ts" } }"#.into(), + content: br#"{ "exports": { ".": "./mod.ts" } }"#.to_vec().into(), })) }), "https://jsr.io/@scope/a/1.2.0/mod.ts" => Box::pin(async move { Ok(Some(LoadResponse::Module { specifier: specifier.clone(), maybe_headers: None, - content: "console.log('Hello, world!')".into(), + content: b"console.log('Hello, world!')".to_vec().into(), })) }), _ => unreachable!(),