feat: Import maps (#2360)

This commit is contained in:
Bartek Iwańczuk 2019-06-09 15:08:20 +02:00 committed by Ryan Dahl
parent 8ec5276d30
commit a115340288
24 changed files with 2406 additions and 36 deletions

2
Cargo.lock generated
View File

@ -221,6 +221,7 @@ dependencies = [
"http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.12.29 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-rustls 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)",
"indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"integer-atomics 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
@ -950,6 +951,7 @@ name = "serde_json"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -1301,11 +1301,15 @@ rust_proc_macro("serde_derive") {
rust_rlib("serde_json") {
edition = "2015"
source_root = "$cargo_home/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.39/src/lib.rs"
features = [ "default" ]
features = [
"default",
"preserve_order",
]
extern_rlib = [
"itoa",
"ryu",
"serde",
"indexmap",
]
args = [
"--cap-lints",

View File

@ -29,6 +29,7 @@ main_extern_rlib = [
"http",
"hyper",
"hyper_rustls",
"indexmap",
"lazy_static",
"libc",
"log",

View File

@ -27,6 +27,7 @@ futures = "0.1.27"
http = "0.1.17"
hyper = "0.12.29"
hyper-rustls = "0.16.1"
indexmap = "1.0.2"
integer-atomics = "1.0.2"
lazy_static = "1.3.0"
libc = "0.2.55"
@ -38,7 +39,7 @@ ring = "0.14.6"
rustyline = "4.1.0"
serde = "1.0.91"
serde_derive = "1.0.91"
serde_json = "1.0.39"
serde_json = { version = "1.0.39", features = [ "preserve_order" ] }
source-map-mappings = "0.5.0"
tempfile = "3.0.8"
tokio = "0.1.20"

View File

@ -1,4 +1,5 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
use crate::import_map::ImportMapError;
use crate::js_errors::JSErrorColor;
pub use crate::msg::ErrorKind;
use crate::resolve_addr::ResolveAddrError;
@ -24,6 +25,7 @@ enum Repr {
IoErr(io::Error),
UrlErr(url::ParseError),
HyperErr(hyper::Error),
ImportMapErr(ImportMapError),
}
pub fn new(kind: ErrorKind, msg: String) -> DenoError {
@ -92,6 +94,7 @@ impl DenoError {
ErrorKind::HttpOther
}
}
Repr::ImportMapErr(ref _err) => ErrorKind::ImportMapError,
}
}
}
@ -103,6 +106,7 @@ impl fmt::Display for DenoError {
Repr::IoErr(ref err) => err.fmt(f),
Repr::UrlErr(ref err) => err.fmt(f),
Repr::HyperErr(ref err) => err.fmt(f),
Repr::ImportMapErr(ref err) => f.pad(&err.msg),
}
}
}
@ -114,6 +118,7 @@ impl std::error::Error for DenoError {
Repr::IoErr(ref err) => err.description(),
Repr::UrlErr(ref err) => err.description(),
Repr::HyperErr(ref err) => err.description(),
Repr::ImportMapErr(ref err) => &err.msg,
}
}
@ -123,6 +128,7 @@ impl std::error::Error for DenoError {
Repr::IoErr(ref err) => Some(err),
Repr::UrlErr(ref err) => Some(err),
Repr::HyperErr(ref err) => Some(err),
Repr::ImportMapErr(ref _err) => None,
}
}
}
@ -202,6 +208,14 @@ impl From<UnixError> for DenoError {
}
}
impl From<ImportMapError> for DenoError {
fn from(err: ImportMapError) -> Self {
Self {
repr: Repr::ImportMapErr(err),
}
}
}
pub fn bad_resource() -> DenoError {
new(ErrorKind::BadResource, String::from("bad resource id"))
}

View File

@ -15,6 +15,9 @@ pub struct DenoFlags {
/// When the `--config`/`-c` flag is used to pass the name, this will be set
/// the path passed on the command line, otherwise `None`.
pub config_path: Option<String>,
/// When the `--importmap` flag is used to pass the name, this will be set
/// the path passed on the command line, otherwise `None`.
pub import_map_path: Option<String>,
pub allow_read: bool,
pub read_whitelist: Vec<String>,
pub allow_write: bool,
@ -82,6 +85,16 @@ fn add_run_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
Arg::with_name("no-prompt")
.long("no-prompt")
.help("Do not use prompts"),
).arg(
Arg::with_name("importmap")
.long("importmap")
.value_name("FILE")
.help("Load import map file")
.long_help(
"Load import map file
Specification: https://wicg.github.io/import-maps/
Examples: https://github.com/WICG/import-maps#the-import-map",
).takes_value(true),
)
}
@ -367,10 +380,10 @@ pub fn parse_flags(matches: &ArgMatches) -> DenoFlags {
flags.v8_flags = Some(v8_flags);
}
flags = parse_permission_args(flags, matches);
flags = parse_run_args(flags, matches);
// flags specific to "run" subcommand
if let Some(run_matches) = matches.subcommand_matches("run") {
flags = parse_permission_args(flags.clone(), run_matches);
flags = parse_run_args(flags.clone(), run_matches);
}
flags
@ -378,10 +391,7 @@ pub fn parse_flags(matches: &ArgMatches) -> DenoFlags {
/// Parse permission specific matches Args and assign to DenoFlags.
/// This method is required because multiple subcommands use permission args.
fn parse_permission_args(
mut flags: DenoFlags,
matches: &ArgMatches,
) -> DenoFlags {
fn parse_run_args(mut flags: DenoFlags, matches: &ArgMatches) -> DenoFlags {
if matches.is_present("allow-read") {
if matches.value_of("allow-read").is_some() {
let read_wl = matches.values_of("allow-read").unwrap();
@ -435,6 +445,7 @@ fn parse_permission_args(
if matches.is_present("no-prompt") {
flags.no_prompts = true;
}
flags.import_map_path = matches.value_of("importmap").map(ToOwned::to_owned);
flags
}
@ -912,6 +923,7 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Xeval);
assert_eq!(argv, svec!["deno", "console.log(val)"]);
}
#[test]
fn test_flags_from_vec_19() {
use tempfile::TempDir;
@ -936,6 +948,7 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Run);
assert_eq!(argv, svec!["deno", "script.ts"]);
}
#[test]
fn test_flags_from_vec_20() {
use tempfile::TempDir;
@ -960,6 +973,7 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Run);
assert_eq!(argv, svec!["deno", "script.ts"]);
}
#[test]
fn test_flags_from_vec_21() {
let (flags, subcommand, argv) = flags_from_vec(svec![
@ -1067,4 +1081,35 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Bundle);
assert_eq!(argv, svec!["deno", "source.ts", "bundle.js"])
}
#[test]
fn test_flags_from_vec_27() {
let (flags, subcommand, argv) = flags_from_vec(svec![
"deno",
"run",
"--importmap=importmap.json",
"script.ts"
]);
assert_eq!(
flags,
DenoFlags {
import_map_path: Some("importmap.json".to_owned()),
..DenoFlags::default()
}
);
assert_eq!(subcommand, DenoSubcommand::Run);
assert_eq!(argv, svec!["deno", "script.ts"]);
let (flags, subcommand, argv) =
flags_from_vec(svec!["deno", "--importmap=importmap.json", "script.ts"]);
assert_eq!(
flags,
DenoFlags {
import_map_path: Some("importmap.json".to_owned()),
..DenoFlags::default()
}
);
assert_eq!(subcommand, DenoSubcommand::Run);
assert_eq!(argv, svec!["deno", "script.ts"]);
}
}

2133
cli/import_map.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ extern crate futures;
extern crate serde_json;
extern crate clap;
extern crate deno;
extern crate indexmap;
#[cfg(unix)]
extern crate nix;
extern crate rand;
@ -24,6 +25,7 @@ mod fs;
mod global_timer;
mod http_body;
mod http_util;
mod import_map;
pub mod js_errors;
pub mod msg;
pub mod msg_util;

View File

@ -136,6 +136,7 @@ enum ErrorKind: byte {
OpNotAvaiable,
WorkerInitFailed,
UnixError,
ImportMapError,
}
table Cwd {}

View File

@ -29,6 +29,7 @@ use crate::worker::Worker;
use deno::js_check;
use deno::Buf;
use deno::JSError;
//use deno::Loader;
use deno::Op;
use deno::PinnedBuf;
use flatbuffers::FlatBufferBuilder;
@ -499,10 +500,30 @@ fn op_fetch_module_meta_data(
let use_cache = !state.flags.reload;
let no_fetch = state.flags.no_fetch;
// TODO(bartlomieju): I feel this is wrong - specifier is only resolved if there's an
// import map - why it is not always resolved? Eg. "bad-module.ts" will return NotFound
// error whilst it should return RelativeUrlWithCannotBeABaseBase error
let resolved_specifier = match &state.import_map {
Some(import_map) => {
match import_map.resolve(specifier, referrer) {
Ok(result) => match result {
Some(url) => url.clone(),
None => specifier.to_string(),
},
Err(err) => panic!("error resolving using import map: {:?}", err), // TODO: this should be coerced to DenoError
}
}
None => specifier.to_string(),
};
let fut = state
.dir
.fetch_module_meta_data_async(specifier, referrer, use_cache, no_fetch)
.and_then(move |out| {
.fetch_module_meta_data_async(
&resolved_specifier,
referrer,
use_cache,
no_fetch,
).and_then(move |out| {
let builder = &mut FlatBufferBuilder::new();
let data_off = builder.create_vector(out.source_code.as_slice());
let msg_args = msg::FetchModuleMetaDataResArgs {

View File

@ -6,6 +6,7 @@ use crate::errors::DenoError;
use crate::errors::DenoResult;
use crate::flags;
use crate::global_timer::GlobalTimer;
use crate::import_map::ImportMap;
use crate::msg;
use crate::ops;
use crate::permissions::DenoPermissions;
@ -57,6 +58,7 @@ pub struct ThreadSafeState(Arc<State>);
#[cfg_attr(feature = "cargo-clippy", allow(stutter))]
pub struct State {
pub main_module: Option<String>,
pub dir: deno_dir::DenoDir,
pub argv: Vec<String>,
pub permissions: DenoPermissions,
@ -67,6 +69,9 @@ pub struct State {
/// When flags contains a `.config_path` option, the fully qualified path
/// name of the passed path will be resolved and set.
pub config_path: Option<String>,
/// When flags contains a `.import_map_path` option, the content of the
/// import map file will be resolved and set.
pub import_map: Option<ImportMap>,
pub metrics: Metrics,
pub worker_channels: Mutex<WorkerChannels>,
pub global_timer: Mutex<GlobalTimer>,
@ -111,9 +116,10 @@ pub fn fetch_module_meta_data_and_maybe_compile_async(
let state_ = state.clone();
let specifier = specifier.to_string();
let referrer = referrer.to_string();
let is_root = referrer == ".";
let f =
futures::future::result(ThreadSafeState::resolve(&specifier, &referrer));
futures::future::result(state.resolve(&specifier, &referrer, is_root));
f.and_then(move |module_id| {
let use_cache = !state_.flags.reload || state_.has_compiled(&module_id);
let no_fetch = state_.flags.no_fetch;
@ -157,7 +163,28 @@ pub fn fetch_module_meta_data_and_maybe_compile(
impl Loader for ThreadSafeState {
type Error = DenoError;
fn resolve(specifier: &str, referrer: &str) -> Result<String, Self::Error> {
fn resolve(
&self,
specifier: &str,
referrer: &str,
is_root: bool,
) -> Result<String, Self::Error> {
if !is_root {
if let Some(import_map) = &self.import_map {
match import_map.resolve(specifier, referrer) {
Ok(result) => {
if result.is_some() {
return Ok(result.unwrap());
}
}
Err(err) => {
// TODO(bartlomieju): this should be coerced to DenoError
panic!("error resolving using import map: {:?}", err);
}
}
}
}
resolve_module_spec(specifier, referrer).map_err(DenoError::from)
}
@ -233,14 +260,50 @@ impl ThreadSafeState {
_ => None,
};
let dir =
deno_dir::DenoDir::new(custom_root, &config, progress.clone()).unwrap();
let main_module: Option<String> = if argv_rest.len() <= 1 {
None
} else {
let specifier = argv_rest[1].clone();
let referrer = ".";
// TODO: does this really have to be resolved by DenoDir?
// Maybe we can call `resolve_module_spec`
match dir.resolve_module_url(&specifier, referrer) {
Ok(url) => Some(url.to_string()),
Err(e) => {
debug!("Potentially swallowed error {}", e);
None
}
}
};
let mut import_map = None;
if let Some(file_name) = &flags.import_map_path {
let base_url = match &main_module {
Some(url) => url,
None => unreachable!(),
};
match ImportMap::load(base_url, file_name) {
Ok(map) => import_map = Some(map),
Err(err) => {
println!("{:?}", err);
panic!("Error parsing import map");
}
}
}
ThreadSafeState(Arc::new(State {
dir: deno_dir::DenoDir::new(custom_root, &config, progress.clone())
.unwrap(),
main_module,
dir,
argv: argv_rest,
permissions: DenoPermissions::from_flags(&flags),
flags,
config,
config_path,
import_map,
metrics: Metrics::default(),
worker_channels: Mutex::new(internal_channels),
global_timer: Mutex::new(GlobalTimer::new()),
@ -255,18 +318,9 @@ impl ThreadSafeState {
/// Read main module from argv
pub fn main_module(&self) -> Option<String> {
if self.argv.len() <= 1 {
None
} else {
let specifier = self.argv[1].clone();
let referrer = ".";
match self.dir.resolve_module_url(&specifier, referrer) {
Ok(url) => Some(url.to_string()),
Err(e) => {
debug!("Potentially swallowed error {}", e);
None
}
}
match &self.main_module {
Some(url) => Some(url.to_string()),
None => None,
}
}

View File

@ -43,7 +43,12 @@ pub trait Loader: Send + Sync {
/// When implementing an spec-complaint VM, this should be exactly the
/// algorithm described here:
/// https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier
fn resolve(specifier: &str, referrer: &str) -> Result<String, Self::Error>;
fn resolve(
&self,
specifier: &str,
referrer: &str,
is_root: bool,
) -> Result<String, Self::Error>;
/// Given an absolute url, load its source code.
fn load(&self, url: &str) -> Box<SourceCodeInfoFuture<Self::Error>>;
@ -98,17 +103,15 @@ impl<L: Loader> RecursiveLoad<L> {
referrer: &str,
parent_id: Option<deno_mod>,
) -> Result<String, L::Error> {
let url = L::resolve(specifier, referrer)?;
let is_root = parent_id.is_none();
let url = self.loader.resolve(specifier, referrer, is_root)?;
let is_root = if let Some(parent_id) = parent_id {
if !is_root {
{
let mut m = self.modules.lock().unwrap();
m.add_child(parent_id, &url);
m.add_child(parent_id.unwrap(), &url);
}
false
} else {
true
};
}
{
// #B We only add modules that have not yet been resolved for RecursiveLoad.
@ -251,7 +254,9 @@ impl<L: Loader> Future for RecursiveLoad<L> {
|specifier: &str, referrer_id: deno_mod| -> deno_mod {
let modules = self.modules.lock().unwrap();
let referrer = modules.get_name(referrer_id).unwrap();
match L::resolve(specifier, &referrer) {
// TODO(bartlomieju): there must be a better way
let is_root = referrer == ".";
match self.loader.resolve(specifier, &referrer, is_root) {
Ok(url) => match modules.get_id(&url) {
Some(id) => id,
None => 0,
@ -619,7 +624,12 @@ mod tests {
impl Loader for MockLoader {
type Error = MockError;
fn resolve(specifier: &str, referrer: &str) -> Result<String, Self::Error> {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_is_root: bool,
) -> Result<String, Self::Error> {
eprintln!(">> RESOLVING, S: {}, R: {}", specifier, referrer);
let output_specifier =
if specifier.starts_with("./") && referrer.starts_with("./") {

7
tests/033_import_map.out Normal file
View File

@ -0,0 +1,7 @@
Hello from remapped moment!
Hello from remapped moment dir!
Hello from remapped lodash!
Hello from remapped lodash dir!
Hello from remapped Vue!
Hello from scoped moment!
Hello from scoped!

View File

@ -0,0 +1,2 @@
args: run --reload --importmap=tests/importmaps/import_map.json tests/importmaps/test.ts
output: tests/033_import_map.out

View File

@ -0,0 +1,14 @@
{
"imports": {
"moment": "./moment/moment.ts",
"moment/": "./moment/",
"lodash": "./lodash/lodash.ts",
"lodash/": "./lodash/",
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js": "./vue.ts"
},
"scopes": {
"scope/": {
"moment": "./scoped_moment.ts"
}
}
}

View File

@ -0,0 +1 @@
console.log("Hello from remapped lodash!");

View File

@ -0,0 +1 @@
console.log("Hello from remapped lodash dir!");

View File

@ -0,0 +1 @@
console.log("Hello from remapped moment!");

View File

@ -0,0 +1 @@
console.log("Hello from remapped moment dir!");

View File

@ -0,0 +1,2 @@
import "moment";
console.log("Hello from scoped!");

View File

@ -0,0 +1 @@
console.log("Hello from scoped moment!");

6
tests/importmaps/test.ts Normal file
View File

@ -0,0 +1,6 @@
import "moment";
import "moment/other_file.ts";
import "lodash";
import "lodash/other_file.ts";
import "https://www.unpkg.com/vue/dist/vue.runtime.esm.js";
import "./scope/scoped.ts";

1
tests/importmaps/vue.ts Normal file
View File

@ -0,0 +1 @@
console.log("Hello from remapped Vue!");

View File

@ -634,6 +634,7 @@ OPTIONS:
--allow-read=<allow-read> Allow file system read access
--allow-write=<allow-write> Allow file system write access
-c, --config <FILE> Load compiler configuration file
--importmap <FILE> Load import map file
--v8-flags=<v8-flags> Set V8 command line options
SUBCOMMANDS:
@ -676,6 +677,50 @@ Particularly useful ones:
--async-stack-trace
```
## Import maps
Deno supports [import maps](https://github.com/WICG/import-maps).
One can use import map with `--importmap=<FILE>` CLI flag.
Current limitations:
- single import map
- no fallback URLs
- Deno does not support `std:` namespace
- Does supports only `file:`, `http:` and `https:` schemes
Example:
```js
// import_map.json
{
"imports": {
"http/": "https://deno.land/std/http/"
}
}
```
```ts
// hello_server.ts
import { serve } from "http/server.ts";
async function main() {
const body = new TextEncoder().encode("Hello World\n");
for await (const req of serve(":8000")) {
req.respond({ body });
}
}
main();
```
```bash
$ deno run --importmap=import_map.json hello_server.ts
```
## Internal details
### Deno and Linux analogy