0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-02-01 12:16:11 -05:00
This commit is contained in:
Marvin Hagemeister 2024-11-25 20:42:03 +01:00
parent 5a2c94af2c
commit 32d7856189
3 changed files with 350 additions and 52 deletions

View file

@ -368,9 +368,17 @@ pub enum BundlePlatform {
Browser, Browser,
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BundleSourceMap {
Inline,
External,
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct BundleFlags { pub struct BundleFlags {
pub files: FileFlags, pub files: FileFlags,
pub minify: bool,
pub source_map: Option<BundleSourceMap>,
pub platform: BundlePlatform, pub platform: BundlePlatform,
pub out_dir: String, pub out_dir: String,
pub watch: Option<WatchFlags>, pub watch: Option<WatchFlags>,
@ -387,6 +395,8 @@ impl BundleFlags {
files, files,
platform, platform,
out_dir, out_dir,
minify: false,
source_map: None,
watch: None, watch: None,
} }
} }
@ -1860,6 +1870,18 @@ fn bundle_subcommand() -> Command {
.help("Target platform. Must be one of: 'deno' or 'browser' (Default: 'deno')") .help("Target platform. Must be one of: 'deno' or 'browser' (Default: 'deno')")
.action(ArgAction::Set) .action(ArgAction::Set)
) )
.arg(
Arg::new("minify")
.long("minify")
.help("Minify bundled code")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("source-map")
.long("source-map")
.help("Minify bundled code")
.action(ArgAction::Set)
)
.arg( .arg(
Arg::new("files") Arg::new("files")
.num_args(1..) .num_args(1..)
@ -4631,6 +4653,20 @@ fn bundle_parse(
None => Ok(BundlePlatform::Deno), None => Ok(BundlePlatform::Deno),
}?; }?;
let minify = matches.get_flag("minify");
let source_map =
if let Some(value) = matches.remove_one::<String>("source-map") {
match value.as_str() {
"inline" => Ok(Some(BundleSourceMap::Inline)),
"external" => Ok(Some(BundleSourceMap::External)),
_ => Err(clap::error::Error::new(
clap::error::ErrorKind::InvalidValue,
)),
}
} else {
Ok(None)
}?;
flags.subcommand = DenoSubcommand::Bundle(BundleFlags { flags.subcommand = DenoSubcommand::Bundle(BundleFlags {
files: FileFlags { files: FileFlags {
include: files, include: files,
@ -4638,6 +4674,8 @@ fn bundle_parse(
}, },
platform, platform,
out_dir, out_dir,
minify,
source_map,
watch: watch_arg_parse(matches)?, watch: watch_arg_parse(matches)?,
}); });

View file

@ -1,21 +1,54 @@
use std::{ use std::{
cmp::Ordering,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fs, fs,
path::Path, io::Write,
path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use deno_core::{error::AnyError, url::Url}; use deno_core::{anyhow::Context, error::AnyError, url::Url};
use deno_graph::{GraphKind, Module, ModuleGraph}; use deno_graph::{GraphKind, Module, ModuleGraph, NpmModule, Resolution};
use deno_runtime::colors; use deno_runtime::{colors, deno_node::NodeResolver};
use deno_semver::package;
use flate2::{write::ZlibEncoder, Compression};
use indexmap::IndexSet;
use node_resolver::{NodeModuleKind, NodeResolutionMode};
use crate::{ use crate::{
args::{BundleFlags, BundlePlatform, Flags}, args::{BundleFlags, BundlePlatform, Flags},
factory::CliFactory, factory::CliFactory,
graph_util::CreateGraphOptions, graph_util::CreateGraphOptions,
npm::CliNpmResolver,
resolver::CjsTracker,
util::{fs::collect_specifiers, path::matches_pattern_or_exact_path}, util::{fs::collect_specifiers, path::matches_pattern_or_exact_path},
}; };
#[derive(Debug)]
struct BundleChunkStat {
name: PathBuf,
size: usize,
gzip: usize,
brotli: usize,
}
#[derive(Debug)]
enum BundleGraphModKind {
Asset(String),
Js,
Json,
Wasm,
Node,
}
#[derive(Debug)]
struct BundleGraphMod {
id: usize,
kind: BundleGraphModKind,
used_count: usize,
has_side_effects: bool,
}
pub async fn bundle( pub async fn bundle(
flags: Arc<Flags>, flags: Arc<Flags>,
bundle_flags: BundleFlags, bundle_flags: BundleFlags,
@ -23,8 +56,9 @@ pub async fn bundle(
// FIXME: Permissions // FIXME: Permissions
let factory = CliFactory::from_flags(flags); let factory = CliFactory::from_flags(flags);
let cli_options = factory.cli_options()?; let cli_options = factory.cli_options()?;
let deno_dir = factory.deno_dir()?; let npm_resolver = factory.npm_resolver().await?;
let http_client = factory.http_client_provider(); let node_resolver = factory.node_resolver().await?;
let cjs_tracker = factory.cjs_tracker()?;
// TODO: Ensure that dependencies are installed // TODO: Ensure that dependencies are installed
@ -53,12 +87,110 @@ pub async fn bundle(
graph.valid()?; graph.valid()?;
let bundle_graph: HashMap<Url, BundleGraphMod> = HashMap::new();
let mut id = 0;
let mut all_modules: HashMap<Url, Module> = HashMap::new();
let mut module_to_id: HashMap<Url, usize> = HashMap::new();
let mut id_to_module: HashMap<usize, Url> = HashMap::new();
let mut npm_modules: IndexSet<Url> = IndexSet::new();
let mut seen: IndexSet<Url> = IndexSet::new();
fn resolve_npm_module(
module: &NpmModule,
npm_resolver: &Arc<dyn CliNpmResolver>,
node_resolver: &Arc<NodeResolver>,
) -> Url {
let nv = module.nv_reference.nv();
let managed = npm_resolver.as_managed().unwrap();
let package_folder =
managed.resolve_pkg_folder_from_deno_module(nv).unwrap();
let resolved = node_resolver
.resolve_package_subpath_from_deno_module(
&package_folder,
module.nv_reference.sub_path(),
None, // FIXME
NodeModuleKind::Esm, // FIXME
NodeResolutionMode::Execution,
)
.with_context(|| format!("Could not resolve '{}'.", module.nv_reference))
.unwrap();
resolved
}
// Hack: Create sub graphs for every npm module we encounter and
// expand npm specifiers to the actual file
for module in graph.modules() {
let url = module.specifier();
seen.insert(url.clone());
let current_id = id;
module_to_id.insert(url.clone(), current_id);
id_to_module.insert(current_id, url.clone());
id += 1;
match module {
Module::Npm(module) => {
let resolved = resolve_npm_module(&module, npm_resolver, node_resolver);
npm_modules.insert(resolved.clone());
}
_ => {
all_modules.insert(url.clone(), module.clone());
}
}
}
let npm_modules_vec =
npm_modules.iter().map(|u| u.clone()).collect::<Vec<_>>();
eprintln!("npm vec: {:#?}", npm_modules_vec);
while let Some(url) = npm_modules.pop() {
let npm_graph = module_graph_creator
.create_graph_with_options(CreateGraphOptions {
graph_kind: GraphKind::CodeOnly,
roots: vec![url.clone()],
is_dynamic: false,
loader: None,
})
.await?;
for module in npm_graph.modules() {
if seen.contains(module.specifier()) {
continue;
}
match module {
Module::Npm(module) => {
let resolved =
resolve_npm_module(&module, npm_resolver, node_resolver);
npm_modules.insert(resolved.clone());
}
_ => {
all_modules.insert(url.clone(), module.clone());
}
}
}
eprintln!("RES {:#?}", all_modules);
}
let mut chunk_graph = ChunkGraph::new(); let mut chunk_graph = ChunkGraph::new();
for file in files { for file in files {
let chunk_id = chunk_graph.new_chunk(None); assign_chunks(
if let Some(module) = graph.get(&file) { &bundle_flags,
assign_chunks(&bundle_flags, &mut chunk_graph, &graph, module, chunk_id); &mut chunk_graph,
} &all_modules,
npm_resolver,
node_resolver,
cjs_tracker,
&file,
None,
true,
);
} }
// Hoist shared modules into common parent chunk that is not a root chunk // Hoist shared modules into common parent chunk that is not a root chunk
@ -68,15 +200,18 @@ pub async fn bundle(
let out_dir = Path::new(&bundle_flags.out_dir); let out_dir = Path::new(&bundle_flags.out_dir);
fs::create_dir_all(out_dir)?; fs::create_dir_all(out_dir)?;
let mut stats: Vec<BundleChunkStat> = vec![];
let mut cols = (8, 4, 4, 6);
// Write out chunks // Write out chunks
// TODO: Walk topo for chunk hashes // TODO: Walk topo for chunk hashes
for (_id, chunk) in &chunk_graph.chunks { for (_id, chunk) in &chunk_graph.chunks {
//chunk //chunk
let mut source = String::new(); let mut source = String::new();
for spec in &chunk.specifiers { for spec in chunk.specifiers.iter().rev() {
if let Some(module) = graph.get(&spec) { if let Some(module) = graph.get(&spec) {
// // FIXME: don't print module urls by default
source.push_str(&format!("// {}\n", spec.to_string())); source.push_str(&format!("// {}\n", spec.to_string()));
if let Some(contents) = &module.source() { if let Some(contents) = &module.source() {
source.push_str(contents); source.push_str(contents);
@ -84,58 +219,123 @@ pub async fn bundle(
} }
} }
let out_path = out_dir.join("out.js"); let out_path = out_dir.join(chunk.name.to_string());
fs::write(&out_path, source).unwrap(); fs::write(&out_path, &source).unwrap();
log::log!(
log::Level::Info, let out_len = out_path.to_string_lossy().len();
"{} {}", if out_len > cols.0 {
colors::green("Filename"), cols.0 = out_len;
colors::green("Size")
);
log::log!(
log::Level::Info,
" {} {}",
out_path.to_string_lossy(),
colors::cyan(0)
);
log::log!(log::Level::Info, "");
} }
let mut gzip_writer = ZlibEncoder::new(vec![], Compression::default());
gzip_writer.write_all(source.as_bytes())?;
let gzip_compressed = gzip_writer.finish()?;
stats.push(BundleChunkStat {
name: out_path.clone(),
size: source.len(),
gzip: gzip_compressed.len(),
brotli: 0,
});
}
// Sort to show biggest files first
stats.sort_by(|a, b| {
if a.gzip > b.gzip {
Ordering::Greater
} else if a.gzip < b.gzip {
Ordering::Less
} else {
Ordering::Equal
}
});
log::log!(
log::Level::Info,
"{} {} {} {}",
colors::green(&format!("{:<width$}", "Filename", width = cols.0 + 2)),
colors::green("Size"),
colors::green("Gzip"),
colors::green("Brotli")
);
for stat in stats {
log::log!(
log::Level::Info,
" {} {} {} {}",
format!("{:<width$}", stat.name.to_string_lossy(), width = cols.0),
colors::cyan(&format!("{:>width$}", stat.size, width = cols.1)),
colors::cyan(&format!("{:>width$}", stat.gzip, width = cols.2)),
colors::cyan(&format!("{:>width$}", stat.brotli, width = cols.3))
);
}
log::log!(log::Level::Info, "");
// eprintln!("chunk {:#?}", chunk_graph);
Ok(()) Ok(())
} }
fn assign_chunks( fn assign_chunks(
bundle_flags: &BundleFlags, bundle_flags: &BundleFlags,
chunk_graph: &mut ChunkGraph, chunk_graph: &mut ChunkGraph,
graph: &ModuleGraph, graph: &HashMap<Url, Module>,
module: &Module, npm_resolver: &Arc<dyn CliNpmResolver>,
chunk_id: usize, node_resolver: &Arc<NodeResolver>,
cjs_tracker: &Arc<CjsTracker>,
url: &Url,
parent_chunk_id: Option<usize>,
is_dynamic: bool,
) { ) {
let module = graph.get(url).unwrap();
match module { match module {
Module::Js(js_module) => { Module::Js(js_module) => {
chunk_graph.assign_specifier(js_module.specifier.clone(), chunk_id); let chunk_id = chunk_graph.assign_specifier_to_chunk(
url,
parent_chunk_id,
ChunkKind::Js,
is_dynamic,
);
for (value, dep) in &js_module.dependencies { for (_, dep) in &js_module.dependencies {
let url = Url::parse(value).unwrap(); match &dep.maybe_code {
let chunk_id = if dep.is_dynamic { Resolution::None => todo!(),
chunk_graph.new_chunk(Some(chunk_id)) Resolution::Ok(resolution_resolved) => {
} else { assign_chunks(
chunk_id bundle_flags,
}; chunk_graph,
graph,
if let Some(module) = graph.get(&url) { npm_resolver,
assign_chunks(bundle_flags, chunk_graph, graph, module, chunk_id); node_resolver,
cjs_tracker,
&resolution_resolved.specifier,
Some(chunk_id),
dep.is_dynamic,
);
}
Resolution::Err(resolution_error) => todo!(),
} }
} }
} }
Module::Json(json_module) => { Module::Json(json_module) => {
chunk_graph.assign_specifier(json_module.specifier.clone(), chunk_id); chunk_graph.assign_specifier_to_chunk(
url,
parent_chunk_id,
ChunkKind::Js,
is_dynamic,
);
} }
Module::Wasm(wasm_module) => { Module::Wasm(wasm_module) => {
let chunk_id = chunk_graph.new_chunk(Some(chunk_id)); chunk_graph.assign_specifier_to_chunk(
chunk_graph.assign_specifier(wasm_module.specifier.clone(), chunk_id); url,
parent_chunk_id,
ChunkKind::Asset("wasm".to_string()),
true,
);
}
Module::Npm(_) => {
unreachable!()
} }
Module::Npm(npm_module) => todo!(),
Module::Node(built_in_node_module) => { Module::Node(built_in_node_module) => {
if let BundlePlatform::Browser = bundle_flags.platform { if let BundlePlatform::Browser = bundle_flags.platform {
// TODO: Show where it was imported from // TODO: Show where it was imported from
@ -150,12 +350,20 @@ fn assign_chunks(
} }
} }
#[derive(Debug, Eq, PartialEq)]
enum ChunkKind {
Asset(String),
Js,
}
#[derive(Debug)] #[derive(Debug)]
struct Chunk { struct Chunk {
id: usize, id: usize,
name: String,
pub kind: ChunkKind,
parent_ids: HashSet<usize>, parent_ids: HashSet<usize>,
children: Vec<usize>, // TODO: IndexSet? children: Vec<usize>, // TODO: IndexSet?
specifiers: HashSet<Url>, specifiers: IndexSet<Url>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -176,7 +384,49 @@ impl ChunkGraph {
} }
} }
fn assign_specifier(&mut self, url: Url, chunk_id: usize) { fn get_or_create_chunk(
&mut self,
url: &Url,
parent_chunk_id: Option<usize>,
kind: ChunkKind,
is_dynamic: bool,
) -> usize {
if let Some(parent_chunk_id) = parent_chunk_id {
if !is_dynamic {
return parent_chunk_id;
}
}
let name = if let Ok(f) = url.to_file_path() {
if let Some(name) = f.file_stem() {
name.to_string_lossy().to_string()
} else {
format!("chunk_{}", self.id)
}
} else {
format!("chunk_{}", self.id)
};
let ext = match &kind {
ChunkKind::Asset(ext) => ext,
ChunkKind::Js => "js",
};
let full_name = format!("{}.{}", name, ext);
self.new_chunk(full_name, parent_chunk_id, kind)
}
fn assign_specifier_to_chunk(
&mut self,
url: &Url,
parent_chunk_id: Option<usize>,
kind: ChunkKind,
is_dynamic: bool,
) -> usize {
let chunk_id =
self.get_or_create_chunk(&url, parent_chunk_id, kind, is_dynamic);
if let Some(value) = self.specifier_to_chunks.get_mut(&url) { if let Some(value) = self.specifier_to_chunks.get_mut(&url) {
value.push(chunk_id) value.push(chunk_id)
} else { } else {
@ -185,11 +435,18 @@ impl ChunkGraph {
} }
if let Some(chunk) = self.chunks.get_mut(&chunk_id) { if let Some(chunk) = self.chunks.get_mut(&chunk_id) {
chunk.specifiers.insert(url); chunk.specifiers.insert(url.clone());
}
} }
fn new_chunk(&mut self, parent_id: Option<usize>) -> usize { chunk_id
}
fn new_chunk(
&mut self,
name: String,
parent_id: Option<usize>,
kind: ChunkKind,
) -> usize {
let id = self.id; let id = self.id;
self.id += 1; self.id += 1;
@ -202,9 +459,11 @@ impl ChunkGraph {
let chunk = Chunk { let chunk = Chunk {
id, id,
name,
kind,
parent_ids, parent_ids,
children: vec![], children: vec![],
specifiers: HashSet::new(), specifiers: IndexSet::new(),
}; };
self.chunks.insert(id, chunk); self.chunks.insert(id, chunk);

View file

@ -46,8 +46,9 @@ use crate::PackageJsonResolverRc;
use crate::PathClean; use crate::PathClean;
use deno_package_json::PackageJson; use deno_package_json::PackageJson;
pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; // FIXME: Wire this through
pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; pub static DEFAULT_CONDITIONS: &[&str] = &["browser", "deno", "node", "import"];
pub static REQUIRE_CONDITIONS: &[&str] = &["browser", "require", "node"];
static TYPES_ONLY_CONDITIONS: &[&str] = &["types"]; static TYPES_ONLY_CONDITIONS: &[&str] = &["types"];
fn conditions_from_module_kind( fn conditions_from_module_kind(