1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00

feat(cli): add support for bundle --no-check (#8023)

Fixes #6686
This commit is contained in:
Kitson Kelly 2020-10-20 14:10:42 +11:00 committed by GitHub
parent 034ab48086
commit 57e95032c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 886 additions and 67 deletions

70
Cargo.lock generated
View file

@ -212,6 +212,12 @@ dependencies = [
"safemem", "safemem",
] ]
[[package]]
name = "build_const"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.4.0" version = "3.4.0"
@ -315,6 +321,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
[[package]]
name = "crc"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
dependencies = [
"build_const",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.2.0" version = "1.2.0"
@ -430,6 +445,7 @@ dependencies = [
"semver-parser 0.9.0", "semver-parser 0.9.0",
"serde", "serde",
"sourcemap", "sourcemap",
"swc_bundler",
"swc_common", "swc_common",
"swc_ecmascript", "swc_ecmascript",
"sys-info", "sys-info",
@ -661,6 +677,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.17" version = "1.0.17"
@ -1475,6 +1497,16 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "petgraph"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.8.0" version = "0.8.0"
@ -1638,6 +1670,12 @@ dependencies = [
"proc-macro2 1.0.21", "proc-macro2 1.0.21",
] ]
[[package]]
name = "radix_fmt"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.6.5" version = "0.6.5"
@ -1828,6 +1866,12 @@ version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
[[package]]
name = "relative-path"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65aff7c83039e88c1c0b4bedf8dfa93d6ec84d5fc2945b37c1fa4186f46c5f94"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@ -2225,6 +2269,32 @@ dependencies = [
"string_cache_codegen", "string_cache_codegen",
] ]
[[package]]
name = "swc_bundler"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dabc79c5bf47498e16a01134846fbb677dd9c7233c591710587d76bd30cbe02"
dependencies = [
"anyhow",
"crc",
"indexmap",
"is-macro",
"log",
"once_cell",
"petgraph",
"radix_fmt",
"relative-path",
"retain_mut",
"swc_atoms",
"swc_common",
"swc_ecma_ast",
"swc_ecma_codegen",
"swc_ecma_parser",
"swc_ecma_transforms",
"swc_ecma_utils",
"swc_ecma_visit",
]
[[package]] [[package]]
name = "swc_common" name = "swc_common"
version = "0.10.4" version = "0.10.4"

View file

@ -61,6 +61,7 @@ rustyline-derive = "0.3.1"
serde = { version = "1.0.116", features = ["derive"] } serde = { version = "1.0.116", features = ["derive"] }
sys-info = "0.7.0" sys-info = "0.7.0"
sourcemap = "6.0.1" sourcemap = "6.0.1"
swc_bundler = "=0.11.3"
swc_common = { version = "=0.10.4", features = ["sourcemap"] } swc_common = { version = "=0.10.4", features = ["sourcemap"] }
swc_ecmascript = { version = "=0.10.1", features = ["codegen", "dep_graph", "parser", "react", "transforms", "visit"] } swc_ecmascript = { version = "=0.10.1", features = ["codegen", "dep_graph", "parser", "react", "transforms", "visit"] }
tempfile = "3.1.0" tempfile = "3.1.0"

View file

@ -1,13 +1,14 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::media_type::MediaType; use crate::media_type::MediaType;
use crate::tsc_config;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use std::rc::Rc; use std::rc::Rc;
use std::result;
use std::sync::Arc; use std::sync::Arc;
use std::sync::RwLock; use std::sync::RwLock;
use swc_common::chain; use swc_common::chain;
@ -21,6 +22,7 @@ use swc_common::errors::HandlerFlags;
use swc_common::FileName; use swc_common::FileName;
use swc_common::Globals; use swc_common::Globals;
use swc_common::Loc; use swc_common::Loc;
use swc_common::SourceFile;
use swc_common::SourceMap; use swc_common::SourceMap;
use swc_common::Span; use swc_common::Span;
use swc_ecmascript::ast::Module; use swc_ecmascript::ast::Module;
@ -38,13 +40,11 @@ use swc_ecmascript::parser::TsConfig;
use swc_ecmascript::transforms::fixer; use swc_ecmascript::transforms::fixer;
use swc_ecmascript::transforms::helpers; use swc_ecmascript::transforms::helpers;
use swc_ecmascript::transforms::pass::Optional; use swc_ecmascript::transforms::pass::Optional;
use swc_ecmascript::transforms::proposals::decorators; use swc_ecmascript::transforms::proposals;
use swc_ecmascript::transforms::react; use swc_ecmascript::transforms::react;
use swc_ecmascript::transforms::typescript; use swc_ecmascript::transforms::typescript;
use swc_ecmascript::visit::FoldWith; use swc_ecmascript::visit::FoldWith;
type Result<V> = result::Result<V, AnyError>;
static TARGET: JscTarget = JscTarget::Es2020; static TARGET: JscTarget = JscTarget::Es2020;
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
@ -173,7 +173,10 @@ pub fn get_syntax(media_type: &MediaType) -> Syntax {
/// Options which can be adjusted when transpiling a module. /// Options which can be adjusted when transpiling a module.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TranspileOptions { pub struct EmitOptions {
/// Indicate if JavaScript is being checked/transformed as well, or if it is
/// only TypeScript.
pub check_js: bool,
/// When emitting a legacy decorator, also emit experimental decorator meta /// When emitting a legacy decorator, also emit experimental decorator meta
/// data. Defaults to `false`. /// data. Defaults to `false`.
pub emit_metadata: bool, pub emit_metadata: bool,
@ -190,9 +193,10 @@ pub struct TranspileOptions {
pub transform_jsx: bool, pub transform_jsx: bool,
} }
impl Default for TranspileOptions { impl Default for EmitOptions {
fn default() -> Self { fn default() -> Self {
TranspileOptions { EmitOptions {
check_js: false,
emit_metadata: false, emit_metadata: false,
inline_source_map: true, inline_source_map: true,
jsx_factory: "React.createElement".into(), jsx_factory: "React.createElement".into(),
@ -202,6 +206,21 @@ impl Default for TranspileOptions {
} }
} }
impl From<tsc_config::TsConfig> for EmitOptions {
fn from(config: tsc_config::TsConfig) -> Self {
let options: tsc_config::EmitConfigOptions =
serde_json::from_value(config.0).unwrap();
EmitOptions {
check_js: options.check_js,
emit_metadata: options.emit_decorator_metadata,
inline_source_map: true,
jsx_factory: options.jsx_factory,
jsx_fragment_factory: options.jsx_fragment_factory,
transform_jsx: options.jsx == "react",
}
}
}
/// A logical structure to hold the value of a parsed module for further /// A logical structure to hold the value of a parsed module for further
/// processing. /// processing.
#[derive(Clone)] #[derive(Clone)]
@ -245,8 +264,8 @@ impl ParsedModule {
/// The result is a tuple of the code and optional source map as strings. /// The result is a tuple of the code and optional source map as strings.
pub fn transpile( pub fn transpile(
self, self,
options: &TranspileOptions, options: &EmitOptions,
) -> Result<(String, Option<String>)> { ) -> Result<(String, Option<String>), AnyError> {
let program = Program::Module(self.module); let program = Program::Module(self.module);
let jsx_pass = react::react( let jsx_pass = react::react(
@ -263,7 +282,7 @@ impl ParsedModule {
); );
let mut passes = chain!( let mut passes = chain!(
Optional::new(jsx_pass, options.transform_jsx), Optional::new(jsx_pass, options.transform_jsx),
decorators::decorators(decorators::Config { proposals::decorators::decorators(proposals::decorators::Config {
legacy: true, legacy: true,
emit_metadata: options.emit_metadata emit_metadata: options.emit_metadata
}), }),
@ -329,7 +348,7 @@ pub fn parse(
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
source: &str, source: &str,
media_type: &MediaType, media_type: &MediaType,
) -> Result<ParsedModule> { ) -> Result<ParsedModule, AnyError> {
let source_map = SourceMap::default(); let source_map = SourceMap::default();
let source_file = source_map.new_source_file( let source_file = source_map.new_source_file(
FileName::Custom(specifier.to_string()), FileName::Custom(specifier.to_string()),
@ -372,6 +391,95 @@ pub fn parse(
}) })
} }
/// A low level function which transpiles a source module into an swc
/// SourceFile.
pub fn transpile_module(
filename: &str,
src: &str,
media_type: &MediaType,
emit_options: &EmitOptions,
cm: Rc<SourceMap>,
) -> Result<(Rc<SourceFile>, Module), AnyError> {
// TODO(@kitsonk) DRY-up with ::parse()
let error_buffer = ErrorBuffer::new();
let handler = Handler::with_emitter_and_flags(
Box::new(error_buffer.clone()),
HandlerFlags {
can_emit_warnings: true,
dont_buffer_diagnostics: true,
..HandlerFlags::default()
},
);
let comments = SingleThreadedComments::default();
let syntax = get_syntax(media_type);
let source_file =
cm.new_source_file(FileName::Custom(filename.to_string()), src.to_string());
let lexer = Lexer::new(
syntax,
TARGET,
StringInput::from(&*source_file),
Some(&comments),
);
let mut parser = swc_ecmascript::parser::Parser::new_from(lexer);
let sm = cm.clone();
let module = parser.parse_module().map_err(move |err| {
let mut diagnostic = err.into_diagnostic(&handler);
diagnostic.emit();
DiagnosticBuffer::from_error_buffer(error_buffer, |span| {
sm.lookup_char_pos(span.lo)
})
})?;
// TODO(@kitsonk) DRY-up with ::transpile()
let jsx_pass = react::react(
cm,
Some(&comments),
react::Options {
pragma: emit_options.jsx_factory.clone(),
pragma_frag: emit_options.jsx_fragment_factory.clone(),
// this will use `Object.assign()` instead of the `_extends` helper
// when spreading props.
use_builtins: true,
..Default::default()
},
);
let mut passes = chain!(
Optional::new(jsx_pass, emit_options.transform_jsx),
proposals::decorators::decorators(proposals::decorators::Config {
legacy: true,
emit_metadata: emit_options.emit_metadata
}),
typescript::strip(),
fixer(Some(&comments)),
);
let module = module.fold_with(&mut passes);
Ok((source_file, module))
}
pub struct BundleHook;
impl swc_bundler::Hook for BundleHook {
fn get_import_meta_url(
&self,
span: swc_common::Span,
file: &swc_common::FileName,
) -> Result<Option<swc_ecmascript::ast::Expr>, AnyError> {
// we use custom file names, and swc "wraps" these in `<` and `>` so, we
// want to strip those back out.
let mut value = file.to_string();
value.pop();
value.remove(0);
Ok(Some(swc_ecmascript::ast::Expr::Lit(
swc_ecmascript::ast::Lit::Str(swc_ecmascript::ast::Str {
span,
value: value.into(),
has_escape: false,
}),
)))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -436,7 +544,7 @@ mod tests {
let module = parse(&specifier, source, &MediaType::TypeScript) let module = parse(&specifier, source, &MediaType::TypeScript)
.expect("could not parse module"); .expect("could not parse module");
let (code, maybe_map) = module let (code, maybe_map) = module
.transpile(&TranspileOptions::default()) .transpile(&EmitOptions::default())
.expect("could not strip types"); .expect("could not strip types");
assert!(code.starts_with("var D;\n(function(D) {\n")); assert!(code.starts_with("var D;\n(function(D) {\n"));
assert!( assert!(
@ -460,7 +568,7 @@ mod tests {
let module = parse(&specifier, source, &MediaType::TSX) let module = parse(&specifier, source, &MediaType::TSX)
.expect("could not parse module"); .expect("could not parse module");
let (code, _) = module let (code, _) = module
.transpile(&TranspileOptions::default()) .transpile(&EmitOptions::default())
.expect("could not strip types"); .expect("could not strip types");
assert!(code.contains("React.createElement(\"div\", null")); assert!(code.contains("React.createElement(\"div\", null"));
} }
@ -491,7 +599,7 @@ mod tests {
let module = parse(&specifier, source, &MediaType::TypeScript) let module = parse(&specifier, source, &MediaType::TypeScript)
.expect("could not parse module"); .expect("could not parse module");
let (code, _) = module let (code, _) = module
.transpile(&TranspileOptions::default()) .transpile(&EmitOptions::default())
.expect("could not strip types"); .expect("could not strip types");
assert!(code.contains("_applyDecoratedDescriptor(")); assert!(code.contains("_applyDecoratedDescriptor("));
} }

View file

@ -402,14 +402,7 @@ fn install_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
} }
fn bundle_parse(flags: &mut Flags, matches: &clap::ArgMatches) { fn bundle_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
// TODO(nayeemrmn): Replace the next couple lines with `compile_args_parse()` compile_args_parse(flags, matches);
// once `deno bundle --no-check` is supported.
importmap_arg_parse(flags, matches);
no_remote_arg_parse(flags, matches);
config_arg_parse(flags, matches);
reload_arg_parse(flags, matches);
lock_args_parse(flags, matches);
ca_file_arg_parse(flags, matches);
let source_file = matches.value_of("source_file").unwrap().to_string(); let source_file = matches.value_of("source_file").unwrap().to_string();
@ -776,16 +769,7 @@ These must be added to the path manually if required.")
} }
fn bundle_subcommand<'a, 'b>() -> App<'a, 'b> { fn bundle_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("bundle") compile_args(SubCommand::with_name("bundle"))
// TODO(nayeemrmn): Replace the next couple lines with `compile_args()` once
// `deno bundle --no-check` is supported.
.arg(importmap_arg())
.arg(no_remote_arg())
.arg(config_arg())
.arg(reload_arg())
.arg(lock_arg())
.arg(lock_write_arg())
.arg(ca_file_arg())
.arg( .arg(
Arg::with_name("source_file") Arg::with_name("source_file")
.takes_value(true) .takes_value(true)
@ -2353,6 +2337,24 @@ mod tests {
); );
} }
#[test]
fn bundle_nocheck() {
let r =
flags_from_vec_safe(svec!["deno", "bundle", "--no-check", "script.ts"])
.unwrap();
assert_eq!(
r,
Flags {
subcommand: DenoSubcommand::Bundle {
source_file: "script.ts".to_string(),
out_file: None,
},
no_check: true,
..Flags::default()
}
);
}
#[test] #[test]
fn run_importmap() { fn run_importmap() {
let r = flags_from_vec_safe(svec![ let r = flags_from_vec_safe(svec![

View file

@ -65,6 +65,7 @@ use crate::fs as deno_fs;
use crate::media_type::MediaType; use crate::media_type::MediaType;
use crate::permissions::Permissions; use crate::permissions::Permissions;
use crate::program_state::ProgramState; use crate::program_state::ProgramState;
use crate::specifier_handler::FetchHandler;
use crate::worker::MainWorker; use crate::worker::MainWorker;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::futures::future::FutureExt; use deno_core::futures::future::FutureExt;
@ -304,7 +305,7 @@ async fn bundle_command(
let module_specifier = ModuleSpecifier::resolve_url_or_path(&source_file)?; let module_specifier = ModuleSpecifier::resolve_url_or_path(&source_file)?;
debug!(">>>>> bundle START"); debug!(">>>>> bundle START");
let program_state = ProgramState::new(flags)?; let program_state = ProgramState::new(flags.clone())?;
info!( info!(
"{} {}", "{} {}",
@ -312,10 +313,36 @@ async fn bundle_command(
module_specifier.to_string() module_specifier.to_string()
); );
let output = program_state let output = if flags.no_check {
.ts_compiler let handler = Rc::new(RefCell::new(FetchHandler::new(
.bundle(&program_state, module_specifier) &program_state,
.await?; Permissions::allow_all(),
)?));
let mut builder = module_graph2::GraphBuilder2::new(
handler,
program_state.maybe_import_map.clone(),
);
builder.insert(&module_specifier).await?;
let graph = builder.get_graph(&program_state.lockfile)?;
let (s, stats, maybe_ignored_options) =
graph.bundle(module_graph2::BundleOptions {
debug: flags.log_level == Some(Level::Debug),
maybe_config_path: flags.config_path,
})?;
if let Some(ignored_options) = maybe_ignored_options {
eprintln!("{}", ignored_options);
}
debug!("{}", stats);
s
} else {
program_state
.ts_compiler
.bundle(&program_state, module_specifier)
.await?
};
debug!(">>>>> bundle END"); debug!(">>>>> bundle END");

View file

@ -1,7 +1,9 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::ast;
use crate::ast::parse; use crate::ast::parse;
use crate::ast::transpile_module;
use crate::ast::BundleHook;
use crate::ast::EmitOptions;
use crate::ast::Location; use crate::ast::Location;
use crate::ast::ParsedModule; use crate::ast::ParsedModule;
use crate::import_map::ImportMap; use crate::import_map::ImportMap;
@ -21,6 +23,7 @@ use crate::tsc_config::TsConfig;
use crate::version; use crate::version;
use crate::AnyError; use crate::AnyError;
use deno_core::error::Context;
use deno_core::futures::stream::FuturesUnordered; use deno_core::futures::stream::FuturesUnordered;
use deno_core::futures::stream::StreamExt; use deno_core::futures::stream::StreamExt;
use deno_core::serde_json::json; use deno_core::serde_json::json;
@ -38,7 +41,6 @@ use std::rc::Rc;
use std::result; use std::result;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::Instant; use std::time::Instant;
use swc_ecmascript::dep_graph::DependencyKind;
lazy_static! { lazy_static! {
/// Matched the `@deno-types` pragma. /// Matched the `@deno-types` pragma.
@ -108,6 +110,59 @@ impl fmt::Display for GraphError {
impl Error for GraphError {} impl Error for GraphError {}
/// A structure for handling bundle loading, which is implemented here, to
/// avoid a circular dependency with `ast`.
struct BundleLoader<'a> {
cm: Rc<swc_common::SourceMap>,
graph: &'a Graph2,
emit_options: &'a EmitOptions,
}
impl<'a> BundleLoader<'a> {
pub fn new(
graph: &'a Graph2,
emit_options: &'a EmitOptions,
cm: Rc<swc_common::SourceMap>,
) -> Self {
BundleLoader {
cm,
graph,
emit_options,
}
}
}
impl swc_bundler::Load for BundleLoader<'_> {
fn load(
&self,
file: &swc_common::FileName,
) -> Result<(Rc<swc_common::SourceFile>, swc_ecmascript::ast::Module), AnyError>
{
match file {
swc_common::FileName::Custom(filename) => {
let specifier = ModuleSpecifier::resolve_url_or_path(filename)
.context("Failed to convert swc FileName to ModuleSpecifier.")?;
if let Some(src) = self.graph.get_source(&specifier) {
let media_type = self
.graph
.get_media_type(&specifier)
.context("Looking up media type during bundling.")?;
transpile_module(
filename,
&src,
&media_type,
self.emit_options,
self.cm.clone(),
)
} else {
Err(MissingDependency(specifier, "<bundle>".to_string()).into())
}
}
_ => unreachable!("Received request for unsupported filename {:?}", file),
}
}
}
/// An enum which represents the parsed out values of references in source code. /// An enum which represents the parsed out values of references in source code.
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
enum TypeScriptReference { enum TypeScriptReference {
@ -273,10 +328,9 @@ impl Module {
// Parse out all the syntactical dependencies for a module // Parse out all the syntactical dependencies for a module
let dependencies = parsed_module.analyze_dependencies(); let dependencies = parsed_module.analyze_dependencies();
for desc in dependencies for desc in dependencies.iter().filter(|desc| {
.iter() desc.kind != swc_ecmascript::dep_graph::DependencyKind::Require
.filter(|desc| desc.kind != DependencyKind::Require) }) {
{
let location = Location { let location = Location {
filename: self.specifier.to_string(), filename: self.specifier.to_string(),
col: desc.col, col: desc.col,
@ -301,8 +355,8 @@ impl Module {
.dependencies .dependencies
.entry(desc.specifier.to_string()) .entry(desc.specifier.to_string())
.or_default(); .or_default();
if desc.kind == DependencyKind::ExportType if desc.kind == swc_ecmascript::dep_graph::DependencyKind::ExportType
|| desc.kind == DependencyKind::ImportType || desc.kind == swc_ecmascript::dep_graph::DependencyKind::ImportType
{ {
dep.maybe_type = Some(specifier); dep.maybe_type = Some(specifier);
} else { } else {
@ -392,6 +446,12 @@ impl fmt::Display for Stats {
} }
} }
#[derive(Debug, Default)]
pub struct BundleOptions {
pub debug: bool,
pub maybe_config_path: Option<String>,
}
/// A structure which provides options when transpiling modules. /// A structure which provides options when transpiling modules.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct TranspileOptions { pub struct TranspileOptions {
@ -431,6 +491,76 @@ impl Graph2 {
} }
} }
/// Transform the module graph into a single JavaScript module which is
/// returned as a `String` in the result.
pub fn bundle(
&self,
options: BundleOptions,
) -> Result<(String, Stats, Option<IgnoredCompilerOptions>), AnyError> {
if self.roots.is_empty() || self.roots.len() > 1 {
return Err(NotSupported(format!("Bundling is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into());
}
let start = Instant::now();
let root_specifier = self.roots[0].clone();
let mut ts_config = TsConfig::new(json!({
"checkJs": false,
"emitDecoratorMetadata": false,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
}));
let maybe_ignored_options =
ts_config.merge_user_config(options.maybe_config_path)?;
let emit_options: EmitOptions = ts_config.into();
let cm = Rc::new(swc_common::SourceMap::new(
swc_common::FilePathMapping::empty(),
));
let loader = BundleLoader::new(self, &emit_options, cm.clone());
let hook = Box::new(BundleHook);
let globals = swc_common::Globals::new();
let bundler = swc_bundler::Bundler::new(
&globals,
cm.clone(),
loader,
self,
swc_bundler::Config::default(),
hook,
);
let mut entries = HashMap::new();
entries.insert(
"bundle".to_string(),
swc_common::FileName::Custom(root_specifier.to_string()),
);
let output = bundler
.bundle(entries)
.context("Unable to output bundle during Graph2::bundle().")?;
let mut buf = Vec::new();
{
let mut emitter = swc_ecmascript::codegen::Emitter {
cfg: swc_ecmascript::codegen::Config { minify: false },
cm: cm.clone(),
comments: None,
wr: Box::new(swc_ecmascript::codegen::text_writer::JsWriter::new(
cm, "\n", &mut buf, None,
)),
};
emitter
.emit_module(&output[0].module)
.context("Unable to emit bundle during Graph2::bundle().")?;
}
let s = String::from_utf8(buf)
.context("Emitted bundle is an invalid utf-8 string.")?;
let stats = Stats(vec![
("Files".to_string(), self.modules.len() as u128),
("Total time".to_string(), start.elapsed().as_millis()),
]);
Ok((s, stats, maybe_ignored_options))
}
fn contains_module(&self, specifier: &ModuleSpecifier) -> bool { fn contains_module(&self, specifier: &ModuleSpecifier) -> bool {
let s = self.resolve_specifier(specifier); let s = self.resolve_specifier(specifier);
self.modules.contains_key(s) self.modules.contains_key(s)
@ -725,16 +855,7 @@ impl Graph2 {
let maybe_ignored_options = let maybe_ignored_options =
ts_config.merge_user_config(options.maybe_config_path)?; ts_config.merge_user_config(options.maybe_config_path)?;
let compiler_options = ts_config.as_transpile_config()?; let emit_options: EmitOptions = ts_config.clone().into();
let check_js = compiler_options.check_js;
let transform_jsx = compiler_options.jsx == "react";
let emit_options = ast::TranspileOptions {
emit_metadata: compiler_options.emit_decorator_metadata,
inline_source_map: true,
jsx_factory: compiler_options.jsx_factory,
jsx_fragment_factory: compiler_options.jsx_fragment_factory,
transform_jsx,
};
let mut emit_count: u128 = 0; let mut emit_count: u128 = 0;
for (_, module) in self.modules.iter_mut() { for (_, module) in self.modules.iter_mut() {
@ -748,7 +869,7 @@ impl Graph2 {
} }
// if we don't have check_js enabled, we won't touch non TypeScript // if we don't have check_js enabled, we won't touch non TypeScript
// modules // modules
if !(check_js if !(emit_options.check_js
|| module.media_type == MediaType::TSX || module.media_type == MediaType::TSX
|| module.media_type == MediaType::TypeScript) || module.media_type == MediaType::TypeScript)
{ {
@ -781,6 +902,27 @@ impl Graph2 {
} }
} }
impl swc_bundler::Resolve for Graph2 {
fn resolve(
&self,
referrer: &swc_common::FileName,
specifier: &str,
) -> Result<swc_common::FileName, AnyError> {
let referrer = if let swc_common::FileName::Custom(referrer) = referrer {
ModuleSpecifier::resolve_url_or_path(referrer)
.context("Cannot resolve swc FileName to a module specifier")?
} else {
unreachable!(
"An unexpected referrer was passed when bundling: {:?}",
referrer
)
};
let specifier = self.resolve(specifier, &referrer)?;
Ok(swc_common::FileName::Custom(specifier.to_string()))
}
}
/// A structure for building a dependency graph of modules. /// A structure for building a dependency graph of modules.
pub struct GraphBuilder2 { pub struct GraphBuilder2 {
fetched: HashSet<ModuleSpecifier>, fetched: HashSet<ModuleSpecifier>,
@ -1094,6 +1236,50 @@ pub mod tests {
assert_eq!(module.maybe_version, expected); assert_eq!(module.maybe_version, expected);
} }
#[tokio::test]
async fn test_graph_bundle() {
let tests = vec![
("file:///tests/fixture01.ts", "fixture01.out"),
("file:///tests/fixture02.ts", "fixture02.out"),
("file:///tests/fixture03.ts", "fixture03.out"),
("file:///tests/fixture04.ts", "fixture04.out"),
("file:///tests/fixture05.ts", "fixture05.out"),
("file:///tests/fixture06.ts", "fixture06.out"),
("file:///tests/fixture07.ts", "fixture07.out"),
("file:///tests/fixture08.ts", "fixture08.out"),
("file:///tests/fixture09.ts", "fixture09.out"),
("file:///tests/fixture10.ts", "fixture10.out"),
("file:///tests/fixture11.ts", "fixture11.out"),
("file:///tests/fixture12.ts", "fixture12.out"),
("file:///tests/fixture13.ts", "fixture13.out"),
("file:///tests/fixture14.ts", "fixture14.out"),
];
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let fixtures = c.join("tests/bundle");
for (specifier, expected_str) in tests {
let specifier = ModuleSpecifier::resolve_url_or_path(specifier).unwrap();
let handler = Rc::new(RefCell::new(MockSpecifierHandler {
fixtures: fixtures.clone(),
..MockSpecifierHandler::default()
}));
let mut builder = GraphBuilder2::new(handler.clone(), None);
builder
.insert(&specifier)
.await
.expect("module not inserted");
let graph = builder.get_graph(&None).expect("could not get graph");
let (actual, stats, maybe_ignored_options) = graph
.bundle(BundleOptions::default())
.expect("could not bundle");
assert_eq!(stats.0.len(), 2);
assert_eq!(maybe_ignored_options, None);
let expected_path = fixtures.join(expected_str);
let expected = fs::read_to_string(expected_path).unwrap();
assert_eq!(actual, expected, "fixture: {}", specifier);
}
}
#[tokio::test] #[tokio::test]
async fn test_graph_info() { async fn test_graph_info() {
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());

View file

@ -0,0 +1,3 @@
import * as a from "./subdir/a.ts";
console.log(a);

View file

@ -0,0 +1,4 @@
import * as b from "./subdir/b.ts";
console.log(b.b); // "b"
console.log(b.c); // { c: "c", default: class C }

View file

@ -0,0 +1,3 @@
import { d } from "./subdir/d.ts";
console.log(d);

View file

@ -0,0 +1,3 @@
const a = await import("./subdir/a.ts");
console.log(a);

View file

@ -0,0 +1,3 @@
import { a } from "./subdir/e.ts";
console.log(a);

View file

@ -0,0 +1,4 @@
import { isMain, modUrl } from "./subdir/f.ts";
console.log(isMain, modUrl);
console.log(import.meta.main, import.meta.url);

View file

@ -0,0 +1,4 @@
import { G } from "./subdir/g.ts";
import { H } from "./subdir/h.ts";
console.log(new G(true), new H(true));

View file

@ -0,0 +1 @@
export * as a from "./subdir/a.ts";

View file

@ -0,0 +1 @@
export { a } from "./subdir/k.ts";

View file

@ -0,0 +1,7 @@
import { a as defaultA } from "./subdir/l.ts";
const o: { a?: string } = {};
const { a = defaultA } = o;
console.log(a);

View file

@ -0,0 +1,32 @@
import { a as defaultA, O } from "./subdir/m.ts";
export { O } from "./subdir/m.ts";
interface AOptions {
a?(): void;
c?: O;
}
class A {
#a: () => void;
#c?: O;
constructor(o: AOptions = {}) {
const {
a = defaultA,
c,
} = o;
this.#a = a;
this.#c = c;
}
a() {
this.#a();
}
c() {
console.log(this.#c);
}
}
const a = new A();
a.a();
a.c();

View file

@ -0,0 +1,7 @@
import { a } from "./subdir/p.ts";
function b() {
a();
}
b();

View file

@ -0,0 +1,11 @@
import { D, d } from "./subdir/q.ts";
class A {
private s: D = d();
a() {
this.s.resolve();
}
}
new A();

View file

@ -0,0 +1,4 @@
// @deno-types="https://deno.land/x/lib/mod.d.ts"
import * as lib from "https://deno.land/x/lib/mod.js";
console.log(lib);

View file

@ -0,0 +1 @@
export const a = "a";

View file

@ -0,0 +1,3 @@
export * as c from "./c.ts";
export const b = "b";

View file

@ -0,0 +1,2 @@
export const c = "c";
export default class C {}

View file

@ -0,0 +1,3 @@
import { a } from "./a.ts";
export const d = { a };

View file

@ -0,0 +1 @@
export * from "./a.ts";

View file

@ -0,0 +1,2 @@
export const isMain = import.meta.main;
export const modUrl = import.meta.url;

View file

@ -0,0 +1,12 @@
const g: number[] = [];
export class G {
#g!: number[];
constructor(shared: boolean) {
if (shared) {
this.#g = g;
} else {
this.#g = [];
}
}
}

View file

@ -0,0 +1,12 @@
const g: number[] = [];
export class H {
#g!: number[];
constructor(shared: boolean) {
if (shared) {
this.#g = g;
} else {
this.#g = [];
}
}
}

View file

@ -0,0 +1,3 @@
export function a(...d: string[]): string {
return d.join(" ");
}

View file

@ -0,0 +1,3 @@
export function a(...d: string[]): string {
return d.join("/");
}

View file

@ -0,0 +1,11 @@
import * as _i from "./i.ts";
import * as _j from "./j.ts";
const k = globalThis.value ? _i : _j;
export const i = _i;
export const j = _j;
export const {
a,
} = k;

View file

@ -0,0 +1 @@
export { a } from "./a.ts";

View file

@ -0,0 +1,2 @@
export { a } from "./n.ts";
export { O } from "./o.ts";

View file

@ -0,0 +1,3 @@
export function a() {
console.log("a");
}

View file

@ -0,0 +1,5 @@
export enum O {
A,
B,
C,
}

View file

@ -0,0 +1 @@
export * from "./i.ts";

View file

@ -0,0 +1,13 @@
/* eslint-disable */
export interface D {
resolve: any;
reject: any;
}
export function d(): D {
let methods;
const promise = new Promise((resolve, reject) => {
methods = { resolve, reject };
});
return Object.assign(promise, methods);
}

View file

@ -0,0 +1,7 @@
const a = function() {
const a = "a";
return {
a
};
}();
console.log(a);

View file

@ -0,0 +1,11 @@
const c = function() {
const c1 = "c";
class C {
}
return {
c: c1,
default: C
};
}();
console.log("b");
console.log(c);

View file

@ -0,0 +1,4 @@
const d = {
a: "a"
};
console.log(d);

View file

@ -0,0 +1,2 @@
const a = await import("./subdir/a.ts");
console.log(a);

View file

@ -0,0 +1 @@
console.log("a");

View file

@ -0,0 +1,2 @@
console.log(false, "file:///tests/subdir/f.ts");
console.log(import.meta.main, "file:///tests/fixture06.ts");

View file

@ -0,0 +1,23 @@
const g = [];
class G {
#g;
constructor(shared){
if (shared) {
this.#g = g;
} else {
this.#g = [];
}
}
}
const g1 = [];
class H {
#g;
constructor(shared1){
if (shared1) {
this.#g = g1;
} else {
this.#g = [];
}
}
}
console.log(new G(true), new H(true));

View file

@ -0,0 +1,7 @@
const a = function() {
const a1 = "a";
return {
a: a1
};
}();
export { a };

View file

@ -0,0 +1,19 @@
const _i = function() {
function a(...d) {
return d.join(" ");
}
return {
a
};
}();
const _j = function() {
function a(...d) {
return d.join("/");
}
return {
a
};
}();
const k = globalThis.value ? _i : _j;
const { a , } = k;
export { a };

View file

@ -0,0 +1,4 @@
const o = {
};
const { a ="a" } = o;
console.log(a);

View file

@ -0,0 +1,31 @@
function a() {
console.log("a");
}
var O;
(function(O1) {
O1[O1["A"] = 0] = "A";
O1[O1["B"] = 1] = "B";
O1[O1["C"] = 2] = "C";
})((void 0) || (O = {
}));
const O1 = void 0;
export { O1 as O };
class A {
#a;
#c;
constructor(o = {
}){
const { a: a1 = a , c , } = o;
this.#a = a1;
this.#c = c;
}
a() {
this.#a();
}
c() {
console.log(this.#c);
}
}
const a2 = new A();
a2.a();
a2.c();

View file

@ -0,0 +1,7 @@
function a(...d) {
return d.join(" ");
}
function b() {
a();
}
b();

View file

@ -0,0 +1,17 @@
function d() {
let methods;
const promise = new Promise((resolve, reject)=>{
methods = {
resolve,
reject
};
});
return Object.assign(promise, methods);
}
class A {
s = d();
a() {
this.s.resolve();
}
}
new A();

View file

@ -0,0 +1,25 @@
const lib = function() {
const a = function() {
const a1 = [];
return {
a: a1
};
}();
const b = function() {
const b1 = [];
return {
b: b1
};
}();
const c = function() {
const c1;
return {
c: c1
};
}();
const mod;
return {
mod
};
}();
console.log(lib);

View file

@ -0,0 +1 @@
export const a: string[] = [];

View file

@ -0,0 +1 @@
export const b = [];

View file

@ -0,0 +1 @@
export const c: string[];

View file

@ -0,0 +1,3 @@
/// <reference types="./c.d.ts" />
export const c = [];

View file

@ -0,0 +1,9 @@
export * as a from "./a.ts";
export * as b from "./b.js";
export * as c from "./c.js";
export interface A {
a: string;
}
export const mod: A[];

View file

@ -0,0 +1,5 @@
export * as a from "./a.ts";
export * as b from "./b.js";
export * as c from "./c.js";
export const mod = [];

View file

@ -838,6 +838,49 @@ fn bundle_exports() {
assert_eq!(output.stderr, b""); assert_eq!(output.stderr, b"");
} }
#[test]
fn bundle_exports_no_check() {
// First we have to generate a bundle of some module that has exports.
let mod1 = util::root_path().join("cli/tests/subdir/mod1.ts");
assert!(mod1.is_file());
let t = TempDir::new().expect("tempdir fail");
let bundle = t.path().join("mod1.bundle.js");
let mut deno = util::deno_cmd()
.current_dir(util::root_path())
.arg("bundle")
.arg("--no-check")
.arg(mod1)
.arg(&bundle)
.spawn()
.expect("failed to spawn script");
let status = deno.wait().expect("failed to wait for the child process");
assert!(status.success());
assert!(bundle.is_file());
// Now we try to use that bundle from another module.
let test = t.path().join("test.js");
std::fs::write(
&test,
"
import { printHello3 } from \"./mod1.bundle.js\";
printHello3(); ",
)
.expect("error writing file");
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("run")
.arg(&test)
.output()
.expect("failed to spawn script");
// check the output of the test.ts program.
assert!(std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
.ends_with("Hello"));
assert_eq!(output.stderr, b"");
}
#[test] #[test]
fn bundle_circular() { fn bundle_circular() {
// First we have to generate a bundle of some module that has exports. // First we have to generate a bundle of some module that has exports.
@ -1051,6 +1094,52 @@ fn bundle_import_map() {
assert_eq!(output.stderr, b""); assert_eq!(output.stderr, b"");
} }
#[test]
fn bundle_import_map_no_check() {
let import = util::root_path().join("cli/tests/bundle_im.ts");
let import_map_path = util::root_path().join("cli/tests/bundle_im.json");
assert!(import.is_file());
let t = TempDir::new().expect("tempdir fail");
let bundle = t.path().join("import_map.bundle.js");
let mut deno = util::deno_cmd()
.current_dir(util::root_path())
.arg("bundle")
.arg("--no-check")
.arg("--importmap")
.arg(import_map_path)
.arg("--unstable")
.arg(import)
.arg(&bundle)
.spawn()
.expect("failed to spawn script");
let status = deno.wait().expect("failed to wait for the child process");
assert!(status.success());
assert!(bundle.is_file());
// Now we try to use that bundle from another module.
let test = t.path().join("test.js");
std::fs::write(
&test,
"
import { printHello3 } from \"./import_map.bundle.js\";
printHello3(); ",
)
.expect("error writing file");
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("run")
.arg(&test)
.output()
.expect("failed to spawn script");
// check the output of the test.ts program.
assert!(std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
.ends_with("Hello"));
assert_eq!(output.stderr, b"");
}
#[test] #[test]
fn info_with_compiled_source() { fn info_with_compiled_source() {
let _g = util::http_server(); let _g = util::http_server();

View file

@ -17,7 +17,7 @@ use std::str::FromStr;
/// file, that we want to deserialize out of the final config for a transpile. /// file, that we want to deserialize out of the final config for a transpile.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TranspileConfigOptions { pub struct EmitConfigOptions {
pub check_js: bool, pub check_js: bool,
pub emit_decorator_metadata: bool, pub emit_decorator_metadata: bool,
pub jsx: String, pub jsx: String,
@ -202,7 +202,7 @@ pub fn parse_config(
/// A structure for managing the configuration of TypeScript /// A structure for managing the configuration of TypeScript
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TsConfig(Value); pub struct TsConfig(pub Value);
impl TsConfig { impl TsConfig {
/// Create a new `TsConfig` with the base being the `value` supplied. /// Create a new `TsConfig` with the base being the `value` supplied.
@ -247,15 +247,6 @@ impl TsConfig {
Ok(None) Ok(None)
} }
} }
/// Return the current configuration as a `TranspileConfigOptions` structure.
pub fn as_transpile_config(
&self,
) -> Result<TranspileConfigOptions, AnyError> {
let options: TranspileConfigOptions =
serde_json::from_value(self.0.clone())?;
Ok(options)
}
} }
impl Serialize for TsConfig { impl Serialize for TsConfig {