feat: Add support for import assertions and JSON modules (#12866)

This commit adds proper support for import assertions and JSON modules.

Implementation of "core/modules.rs" was changed to account for multiple possible
module types, instead of always assuming that the code is an "ES module". In
effect "ModuleMap" now has knowledge about each modules' type (stored via
"ModuleType" enum). Module loading pipeline now stores information about
expected module type for each request and validates that expected type matches
discovered module type based on file's "MediaType".

Relevant tests were added to "core/modules.rs" and integration tests,
additionally multiple WPT tests were enabled.

There are still some rough edges in the implementation and not all WPT were
enabled, due to:
a) unclear BOM handling in source code by "FileFetcher"
b) design limitation of Deno's "FileFetcher" that doesn't download the same
module multiple times in a single run

Co-authored-by: Kitson Kelly <me@kitsonkelly.com>
This commit is contained in:
Bartek Iwańczuk 2021-12-15 19:22:36 +01:00 committed by GitHub
parent ec7d90666f
commit a1f0796fcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 916 additions and 228 deletions

4
Cargo.lock generated
View file

@ -836,9 +836,9 @@ dependencies = [
[[package]]
name = "deno_graph"
version = "0.14.0"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72e7615cd00e7c9b93d804fad6c2a25c9c40a647bf3b5cf857f91aa4c107792"
checksum = "8f3280596f5b5825b0363142b72fe2786163905c61dfeb18bd5db1c390a94093"
dependencies = [
"anyhow",
"cfg-if 1.0.0",

View file

@ -104,6 +104,7 @@ pub const IGNORED_COMPILER_OPTIONS: &[&str] = &[
"paths",
"preserveConstEnums",
"reactNamespace",
"resolveJsonModule",
"rootDir",
"rootDirs",
"skipLibCheck",

View file

@ -153,6 +153,7 @@ pub(crate) fn get_ts_config(
"isolatedModules": true,
"lib": lib,
"module": "esnext",
"resolveJsonModule": true,
"strict": true,
"target": "esnext",
"tsBuildInfoFile": "deno:///.tsbuildinfo",
@ -186,6 +187,7 @@ pub(crate) fn get_ts_config(
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
"resolveJsonModule": true,
})),
ConfigType::RuntimeEmit { tsc_emit } => {
let mut ts_config = TsConfig::new(json!({
@ -403,14 +405,15 @@ pub(crate) fn check_and_maybe_emit(
log::debug!("module missing, skipping emit for {}", specifier);
continue;
};
// Sometimes if `tsc` sees a CommonJS file it will _helpfully_ output it
// to ESM, which we don't really want to do unless someone has enabled
// check_js.
if !check_js
&& matches!(
media_type,
MediaType::JavaScript | MediaType::Cjs | MediaType::Mjs
)
// Sometimes if `tsc` sees a CommonJS file or a JSON module, it will
// _helpfully_ output it, which we don't really want to do unless
// someone has enabled check_js.
if matches!(media_type, MediaType::Json)
|| (!check_js
&& matches!(
media_type,
MediaType::JavaScript | MediaType::Cjs | MediaType::Mjs
))
{
log::debug!("skipping emit for {}", specifier);
continue;
@ -429,7 +432,10 @@ pub(crate) fn check_and_maybe_emit(
MediaType::Dts | MediaType::Dcts | MediaType::Dmts => {
cache.set(CacheType::Declaration, &specifier, emit.data)?;
}
_ => unreachable!(),
_ => unreachable!(
"unexpected media_type {} {}",
emit.media_type, specifier
),
}
}
}

View file

@ -569,6 +569,7 @@ impl Inner {
"lib": ["deno.ns", "deno.window"],
"module": "esnext",
"noEmit": true,
"resolveJsonModule": true,
"strict": true,
"target": "esnext",
"useDefineForClassFields": true,

View file

@ -31,6 +31,7 @@ use deno_core::url::Url;
use deno_core::CompiledWasmModuleStore;
use deno_core::ModuleSource;
use deno_core::ModuleSpecifier;
use deno_core::ModuleType;
use deno_core::SharedArrayBufferStore;
use deno_graph::create_graph;
use deno_graph::Dependency;
@ -68,6 +69,7 @@ pub struct ProcState(Arc<Inner>);
enum ModuleEntry {
Module {
code: String,
media_type: MediaType,
dependencies: BTreeMap<String, Dependency>,
},
Error(ModuleGraphError),
@ -584,6 +586,7 @@ impl ProcState {
| MediaType::Unknown
| MediaType::Cjs
| MediaType::Mjs
| MediaType::Json
) {
module.maybe_source().unwrap_or("").to_string()
// The emit may also be missing when a declaration file is in the
@ -602,7 +605,11 @@ impl ProcState {
module.maybe_dependencies().cloned().unwrap_or_default();
graph_data.modules.insert(
specifier.clone(),
ModuleEntry::Module { code, dependencies },
ModuleEntry::Module {
code,
dependencies,
media_type: *media_type,
},
);
if let Some(dependencies) = module.maybe_dependencies() {
for dep in dependencies.values() {
@ -724,10 +731,16 @@ impl ProcState {
_ => &specifier,
};
match graph_data.modules.get(found_specifier) {
Some(ModuleEntry::Module { code, .. }) => Ok(ModuleSource {
Some(ModuleEntry::Module {
code, media_type, ..
}) => Ok(ModuleSource {
code: code.clone(),
module_url_specified: specifier.to_string(),
module_url_found: found_specifier.to_string(),
module_type: match media_type {
MediaType::Json => ModuleType::Json,
_ => ModuleType::JavaScript,
},
}),
_ => Err(anyhow!(
"Loading unprepared module: {}",

View file

@ -164,6 +164,7 @@ impl ModuleLoader for EmbeddedModuleLoader {
Ok(deno_core::ModuleSource {
code,
module_type: deno_core::ModuleType::JavaScript,
module_url_specified: module_specifier.to_string(),
module_url_found: module_specifier.to_string(),
})

View file

@ -2407,3 +2407,36 @@ fn issue12807() {
.unwrap();
assert!(status.success());
}
itest!(import_assertions_static_import {
args: "run --allow-read import_assertions/static_import.ts",
output: "import_assertions/static_import.out",
});
itest!(import_assertions_static_export {
args: "run --allow-read import_assertions/static_export.ts",
output: "import_assertions/static_export.out",
});
itest!(import_assertions_static_error {
args: "run --allow-read import_assertions/static_error.ts",
output: "import_assertions/static_error.out",
exit_code: 1,
});
itest!(import_assertions_dynamic_import {
args: "run --allow-read import_assertions/dynamic_import.ts",
output: "import_assertions/dynamic_import.out",
});
itest!(import_assertions_dynamic_error {
args: "run --allow-read import_assertions/dynamic_error.ts",
output: "import_assertions/dynamic_error.out",
exit_code: 1,
});
itest!(import_assertions_type_check {
args: "run --allow-read import_assertions/type_check.ts",
output: "import_assertions/type_check.out",
exit_code: 1,
});

View file

@ -0,0 +1,6 @@
{
"a": "b",
"c": {
"d": 10
}
}

View file

@ -0,0 +1,5 @@
[WILDCARD]
error: Uncaught (in promise) TypeError: Expected a "JavaScript" module but loaded a "JSON" module.
const data = await import("./data.json");
^
at async [WILDCARD]dynamic_error.ts:1:14

View file

@ -0,0 +1,3 @@
const data = await import("./data.json");
console.log(data);

View file

@ -0,0 +1,2 @@
[WILDCARD]
Module { default: { a: "b", c: { d: 10 } } }

View file

@ -0,0 +1,3 @@
const data = await import("./data.json", { assert: { type: "json" } });
console.log(data);

View file

@ -0,0 +1,5 @@
[WILDCARD]
error: An unsupported media type was attempted to be imported as a module.
Specifier: [WILDCARD]data.json
MediaType: Json
at [WILDCARD]static_error.ts:1:18

View file

@ -0,0 +1,3 @@
import data from "./data.json";
console.log(data);

View file

@ -0,0 +1,2 @@
[WILDCARD]
{ a: "b", c: { d: 10 } }

View file

@ -0,0 +1,3 @@
import data from "./static_reexport.ts";
console.log(data);

View file

@ -0,0 +1,2 @@
[WILDCARD]
{ a: "b", c: { d: 10 } }

View file

@ -0,0 +1,3 @@
import data from "./data.json" assert { type: "json" };
console.log(data);

View file

@ -0,0 +1 @@
export { default } from "./data.json" assert { type: "json" };

View file

@ -0,0 +1,5 @@
[WILDCARD]
error: TS2339 [ERROR]: Property 'foo' does not exist on type '{ a: string; c: { d: number; }; }'.
console.log(data.foo);
~~~
at [WILDCARD]type_check.ts:3:18

View file

@ -0,0 +1,3 @@
import data from "./data.json" assert { type: "json" };
console.log(data.foo);

View file

@ -352,6 +352,24 @@ struct LoadArgs {
specifier: String,
}
fn as_ts_script_kind(media_type: &MediaType) -> i32 {
match media_type {
MediaType::JavaScript => 1,
MediaType::Jsx => 2,
MediaType::Mjs => 1,
MediaType::Cjs => 1,
MediaType::TypeScript => 3,
MediaType::Mts => 3,
MediaType::Cts => 3,
MediaType::Dts => 3,
MediaType::Dmts => 3,
MediaType::Dcts => 3,
MediaType::Tsx => 4,
MediaType::Json => 6,
_ => 0,
}
}
fn op_load(state: &mut State, args: Value) -> Result<Value, AnyError> {
let v: LoadArgs = serde_json::from_value(args)
.context("Invalid request from JavaScript for \"op_load\".")?;
@ -395,7 +413,7 @@ fn op_load(state: &mut State, args: Value) -> Result<Value, AnyError> {
};
Ok(
json!({ "data": data, "hash": hash, "scriptKind": media_type.as_ts_script_kind() }),
json!({ "data": data, "hash": hash, "scriptKind": as_ts_script_kind(&media_type) }),
)
}

View file

@ -1,6 +1,10 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use crate::error::is_instance_of_error;
use crate::modules::get_module_type_from_assertions;
use crate::modules::parse_import_assertions;
use crate::modules::validate_import_assertions;
use crate::modules::ImportAssertionsKind;
use crate::modules::ModuleMap;
use crate::resolve_url_or_path;
use crate::JsRuntime;
@ -243,7 +247,7 @@ pub extern "C" fn host_import_module_dynamically_callback(
context: v8::Local<v8::Context>,
referrer: v8::Local<v8::ScriptOrModule>,
specifier: v8::Local<v8::String>,
_import_assertions: v8::Local<v8::FixedArray>,
import_assertions: v8::Local<v8::FixedArray>,
) -> *mut v8::Promise {
let scope = &mut unsafe { v8::CallbackScope::new(context) };
@ -267,6 +271,22 @@ pub extern "C" fn host_import_module_dynamically_callback(
let resolver = v8::PromiseResolver::new(scope).unwrap();
let promise = resolver.get_promise(scope);
let assertions = parse_import_assertions(
scope,
import_assertions,
ImportAssertionsKind::DynamicImport,
);
{
let tc_scope = &mut v8::TryCatch::new(scope);
validate_import_assertions(tc_scope, &assertions);
if tc_scope.has_caught() {
let e = tc_scope.exception().unwrap();
resolver.reject(tc_scope, e);
}
}
let module_type = get_module_type_from_assertions(&assertions);
let resolver_handle = v8::Global::new(scope, resolver);
{
let state_rc = JsRuntime::state(scope);
@ -280,6 +300,7 @@ pub extern "C" fn host_import_module_dynamically_callback(
module_map_rc,
&specifier_str,
&referrer_name_str,
module_type,
resolver_handle,
);
state_rc.borrow_mut().notify_new_dynamic_import();
@ -294,13 +315,29 @@ pub extern "C" fn host_import_module_dynamically_callback(
_rv: v8::ReturnValue| {
let arg = args.get(0);
if is_instance_of_error(scope, arg) {
let e: crate::error::NativeJsError =
serde_v8::from_v8(scope, arg).unwrap();
let name = e.name.unwrap_or_else(|| "Error".to_string());
let message = v8::Exception::create_message(scope, arg);
if message.get_stack_trace(scope).unwrap().get_frame_count() == 0 {
let arg: v8::Local<v8::Object> = arg.try_into().unwrap();
let message_key = v8::String::new(scope, "message").unwrap();
let message = arg.get(scope, message_key.into()).unwrap();
let exception =
v8::Exception::type_error(scope, message.try_into().unwrap());
let exception = match name.as_str() {
"RangeError" => {
v8::Exception::range_error(scope, message.try_into().unwrap())
}
"TypeError" => {
v8::Exception::type_error(scope, message.try_into().unwrap())
}
"SyntaxError" => {
v8::Exception::syntax_error(scope, message.try_into().unwrap())
}
"ReferenceError" => {
v8::Exception::reference_error(scope, message.try_into().unwrap())
}
_ => v8::Exception::error(scope, message.try_into().unwrap()),
};
let code_key = v8::String::new(scope, "code").unwrap();
let code_value =
v8::String::new(scope, "ERR_MODULE_NOT_FOUND").unwrap();
@ -1311,7 +1348,7 @@ fn create_host_object(
pub fn module_resolve_callback<'s>(
context: v8::Local<'s, v8::Context>,
specifier: v8::Local<'s, v8::String>,
_import_assertions: v8::Local<'s, v8::FixedArray>,
import_assertions: v8::Local<'s, v8::FixedArray>,
referrer: v8::Local<'s, v8::Module>,
) -> Option<v8::Local<'s, v8::Module>> {
let scope = &mut unsafe { v8::CallbackScope::new(context) };
@ -1328,8 +1365,17 @@ pub fn module_resolve_callback<'s>(
let specifier_str = specifier.to_rust_string_lossy(scope);
let maybe_module =
module_map.resolve_callback(scope, &specifier_str, &referrer_name);
let assertions = parse_import_assertions(
scope,
import_assertions,
ImportAssertionsKind::StaticImport,
);
let maybe_module = module_map.resolve_callback(
scope,
&specifier_str,
&referrer_name,
assertions,
);
if let Some(module) = maybe_module {
return Some(module);
}

View file

@ -158,9 +158,9 @@ fn get_property<'a>(
}
#[derive(serde::Deserialize)]
struct NativeJsError {
name: Option<String>,
message: Option<String>,
pub(crate) struct NativeJsError {
pub name: Option<String>,
pub message: Option<String>,
// Warning! .stack is special so handled by itself
// stack: Option<String>,
}

View file

@ -61,6 +61,7 @@ pub use crate::modules::ModuleLoadId;
pub use crate::modules::ModuleLoader;
pub use crate::modules::ModuleSource;
pub use crate::modules::ModuleSourceFuture;
pub use crate::modules::ModuleType;
pub use crate::modules::NoopModuleLoader;
pub use crate::runtime::CompiledWasmModuleStore;
pub use crate::runtime::SharedArrayBufferStore;

File diff suppressed because it is too large Load diff

View file

@ -1259,12 +1259,15 @@ impl JsRuntime {
if let Some(load_stream_result) = maybe_result {
match load_stream_result {
Ok(info) => {
Ok((request, info)) => {
// A module (not necessarily the one dynamically imported) has been
// fetched. Create and register it, and if successful, poll for the
// next recursive-load event related to this dynamic import.
let register_result =
load.register_and_recurse(&mut self.handle_scope(), &info);
let register_result = load.register_and_recurse(
&mut self.handle_scope(),
&request,
&info,
);
match register_result {
Ok(()) => {
@ -1417,7 +1420,7 @@ impl JsRuntime {
) -> Result<ModuleId, Error> {
let module_map_rc = Self::module_map(self.v8_isolate());
if let Some(code) = code {
module_map_rc.borrow_mut().new_module(
module_map_rc.borrow_mut().new_es_module(
&mut self.handle_scope(),
// main module
true,
@ -1429,10 +1432,10 @@ impl JsRuntime {
let mut load =
ModuleMap::load_main(module_map_rc.clone(), specifier.as_str()).await?;
while let Some(info_result) = load.next().await {
let info = info_result?;
while let Some(load_result) = load.next().await {
let (request, info) = load_result?;
let scope = &mut self.handle_scope();
load.register_and_recurse(scope, &info)?;
load.register_and_recurse(scope, &request, &info)?;
}
let root_id = load.root_module_id.expect("Root module should be loaded");
@ -1454,7 +1457,7 @@ impl JsRuntime {
) -> Result<ModuleId, Error> {
let module_map_rc = Self::module_map(self.v8_isolate());
if let Some(code) = code {
module_map_rc.borrow_mut().new_module(
module_map_rc.borrow_mut().new_es_module(
&mut self.handle_scope(),
// not main module
false,
@ -1466,10 +1469,10 @@ impl JsRuntime {
let mut load =
ModuleMap::load_side(module_map_rc.clone(), specifier.as_str()).await?;
while let Some(info_result) = load.next().await {
let info = info_result?;
while let Some(load_result) = load.next().await {
let (request, info) = load_result?;
let scope = &mut self.handle_scope();
load.register_and_recurse(scope, &info)?;
load.register_and_recurse(scope, &request, &info)?;
}
let root_id = load.root_module_id.expect("Root module should be loaded");
@ -1630,6 +1633,7 @@ pub mod tests {
use crate::error::custom_error;
use crate::modules::ModuleSource;
use crate::modules::ModuleSourceFuture;
use crate::modules::ModuleType;
use crate::op_async;
use crate::op_sync;
use crate::ZeroCopyBuf;
@ -2642,6 +2646,7 @@ assertEquals(1, notify_return_value);
code: "console.log('hello world');".to_string(),
module_url_specified: "file:///main.js".to_string(),
module_url_found: "file:///main.js".to_string(),
module_type: ModuleType::JavaScript,
})
}
.boxed_local()

View file

@ -6352,52 +6352,25 @@
"semantics": {
"scripting-1": {
"the-script-element": {
"import-assertions": {
"dynamic-import-with-assertion-argument.any.html": [
"Dynamic import with an unsupported type assertion should fail"
],
"dynamic-import-with-assertion-argument.any.worker.html": [
"Dynamic import with an unsupported type assertion should fail"
]
},
"import-assertions": true,
"json-module": {
"charset-bom.any.html": [
"UTF-8 BOM should be stripped when decoding JSON module script",
"UTF-16BE BOM should result in parse error in JSON module script",
"UTF-16LE BOM should result in parse error in JSON module script"
],
"charset-bom.any.worker.html": [
"UTF-8 BOM should be stripped when decoding JSON module script",
"UTF-16BE BOM should result in parse error in JSON module script",
"UTF-16LE BOM should result in parse error in JSON module script"
],
"invalid-content-type.any.html": true,
"invalid-content-type.any.worker.html": true,
"non-object.any.html": [
"Non-object: null",
"Non-object: true",
"Non-object: false",
"Non-object: string",
"Non-object: array"
],
"non-object.any.worker.html": [
"Non-object: null",
"Non-object: true",
"Non-object: false",
"Non-object: string",
"Non-object: array"
],
"non-object.any.html": true,
"non-object.any.worker.html": true,
"repeated-imports.any.html": [
"Importing a specifier that previously failed due to an incorrect type assertion can succeed if the correct assertion is later given",
"Importing a specifier that previously succeeded with the correct type assertion should fail if the incorrect assertion is later given",
"Two modules of different type with the same specifier can load if the server changes its responses",
"If an import previously succeeded for a given specifier/type assertion pair, future uses of that pair should yield the same result"
"Two modules of different type with the same specifier can load if the server changes its responses"
],
"repeated-imports.any.worker.html": [
"Importing a specifier that previously failed due to an incorrect type assertion can succeed if the correct assertion is later given",
"Importing a specifier that previously succeeded with the correct type assertion should fail if the incorrect assertion is later given",
"Two modules of different type with the same specifier can load if the server changes its responses",
"If an import previously succeeded for a given specifier/type assertion pair, future uses of that pair should yield the same result"
"Two modules of different type with the same specifier can load if the server changes its responses"
]
},
"microtasks": {