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!(),