mirror of
https://github.com/denoland/deno.git
synced 2025-09-27 04:39:10 +00:00
feat(bundle): support html entrypoint (#29856)
For instance `deno bundle --outdir dist index.html` It will find scripts referenced in the html, bundle them, and then update the paths in index.html for the bundled assets. Right now it doesn't handle other assets (from `link` elements), but it could
This commit is contained in:
parent
41ff38ae65
commit
4e4bbf2fcc
33 changed files with 1480 additions and 74 deletions
143
Cargo.lock
generated
143
Cargo.lock
generated
|
@ -871,9 +871,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
|
@ -1437,6 +1437,29 @@ version = "0.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25670139e591f1c2869eb8d0d977028f8d05e859132b4c874ecd02a00d3c9174"
|
||||
|
||||
[[package]]
|
||||
name = "cssparser"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa"
|
||||
dependencies = [
|
||||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa",
|
||||
"phf",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser-macros"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
|
@ -1585,6 +1608,7 @@ dependencies = [
|
|||
"bincode",
|
||||
"boxed_error",
|
||||
"bytes",
|
||||
"bytes-str",
|
||||
"capacity_builder",
|
||||
"chrono",
|
||||
"clap",
|
||||
|
@ -1650,6 +1674,7 @@ dependencies = [
|
|||
"libsui",
|
||||
"libz-sys",
|
||||
"log",
|
||||
"lol_html",
|
||||
"lsp-types",
|
||||
"malva",
|
||||
"markup_fmt",
|
||||
|
@ -1699,6 +1724,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"twox-hash 2.1.0",
|
||||
"typed-arena",
|
||||
"unicode-width 0.1.13",
|
||||
"uuid",
|
||||
|
@ -2338,7 +2364,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"deno_ast",
|
||||
"deno_semver",
|
||||
"derive_more",
|
||||
"derive_more 0.99.17",
|
||||
"if_chain",
|
||||
"log",
|
||||
"once_cell",
|
||||
|
@ -3391,6 +3417,26 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deunicode"
|
||||
version = "1.4.3"
|
||||
|
@ -3656,6 +3702,21 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa-short"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
|
@ -3867,14 +3928,13 @@ checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8"
|
|||
|
||||
[[package]]
|
||||
name = "esbuild_client"
|
||||
version = "0.6.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b3d661f00dac26314d27da56eb41e0e43f270251fe8c53686bb1e32205863"
|
||||
checksum = "97ec79be0f4b20864729b38953ad77c7f347e436a7532c5f332dce9736aa4b5c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"deno_unsync",
|
||||
"derive_builder",
|
||||
"indexmap 2.9.0",
|
||||
"log",
|
||||
"parking_lot",
|
||||
|
@ -4332,6 +4392,15 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.4"
|
||||
|
@ -5832,6 +5901,24 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lol_html"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63d49c99bfbf3400dd6450e516515b7014fcb49b5cb533f4b725a00c1462a36"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"cfg-if",
|
||||
"cssparser",
|
||||
"encoding_rs",
|
||||
"hashbrown 0.15.5",
|
||||
"memchr",
|
||||
"mime",
|
||||
"precomputed-hash",
|
||||
"selectors",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
|
@ -6841,6 +6928,16 @@ dependencies = [
|
|||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.2"
|
||||
|
@ -7002,6 +7099,12 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "prefix-trie"
|
||||
version = "0.7.0"
|
||||
|
@ -8014,6 +8117,25 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3df44ba8a7ca7a4d28c589e04f526266ed76b6cc556e33fe69fa25de31939a65"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"cssparser",
|
||||
"derive_more 2.0.1",
|
||||
"fxhash",
|
||||
"log",
|
||||
"new_debug_unreachable",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"precomputed-hash",
|
||||
"servo_arc",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.9.0"
|
||||
|
@ -8151,6 +8273,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "servo_arc"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
|
|
|
@ -103,6 +103,7 @@ base64.workspace = true
|
|||
bincode.workspace = true
|
||||
boxed_error.workspace = true
|
||||
bytes.workspace = true
|
||||
bytes-str = "0.2.5"
|
||||
capacity_builder.workspace = true
|
||||
chrono = { workspace = true, features = ["now"] }
|
||||
clap = { workspace = true, features = ["env", "string", "wrap_help", "error-context"] }
|
||||
|
@ -120,7 +121,7 @@ dprint-plugin-json.workspace = true
|
|||
dprint-plugin-jupyter.workspace = true
|
||||
dprint-plugin-markdown.workspace = true
|
||||
dprint-plugin-typescript.workspace = true
|
||||
esbuild_client = { version = "0.6.0", features = ["serde"] }
|
||||
esbuild_client = { version = "0.7.1", features = ["serde"] }
|
||||
fancy-regex.workspace = true
|
||||
faster-hex.workspace = true
|
||||
# If you disable the default __vendored_zlib_ng feature above, you _must_ be able to link against `-lz`.
|
||||
|
@ -139,6 +140,7 @@ lazy-regex.workspace = true
|
|||
libc.workspace = true
|
||||
libz-sys.workspace = true
|
||||
log = { workspace = true, features = ["serde"] }
|
||||
lol_html = "2.6.0"
|
||||
lsp-types.workspace = true
|
||||
malva.workspace = true
|
||||
markup_fmt.workspace = true
|
||||
|
@ -181,6 +183,7 @@ tower-lsp.workspace = true
|
|||
tracing = { workspace = true, features = ["log"], optional = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
|
||||
twox-hash.workspace = true
|
||||
typed-arena.workspace = true
|
||||
unicode-width.workspace = true
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
|
|
451
cli/tools/bundle/html.rs
Normal file
451
cli/tools/bundle/html.rs
Normal file
|
@ -0,0 +1,451 @@
|
|||
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use capacity_builder::StringBuilder;
|
||||
use deno_core::anyhow;
|
||||
use deno_core::error::AnyError;
|
||||
use lol_html::element;
|
||||
use lol_html::html_content::ContentType as LolContentType;
|
||||
|
||||
use crate::tools::bundle::OutputFile;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Script {
|
||||
pub src: Option<String>,
|
||||
pub is_async: bool,
|
||||
pub is_module: bool,
|
||||
pub resolved_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
struct Attr<'a> {
|
||||
name: Cow<'static, str>,
|
||||
value: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> Attr<'a> {
|
||||
fn new(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: Option<Cow<'a, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
value,
|
||||
}
|
||||
}
|
||||
fn write_out<'s>(&'s self, out: &mut StringBuilder<'s>)
|
||||
where
|
||||
'a: 's,
|
||||
{
|
||||
out.append(&self.name);
|
||||
if let Some(value) = &self.value {
|
||||
out.append("=\"");
|
||||
out.append(value);
|
||||
out.append('"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_attr_list<'a, 's>(attrs: &'s [Attr<'a>], out: &mut StringBuilder<'s>)
|
||||
where
|
||||
'a: 's,
|
||||
{
|
||||
if attrs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
out.append(' ');
|
||||
for item in attrs.iter().take(attrs.len() - 1) {
|
||||
item.write_out(out);
|
||||
out.append(' ');
|
||||
}
|
||||
|
||||
attrs.last().unwrap().write_out(out);
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn to_element_string(&self) -> String {
|
||||
let mut attrs = Vec::new();
|
||||
if let Some(src) = &self.src {
|
||||
attrs.push(Attr::new("src", Some(Cow::Borrowed(src))));
|
||||
}
|
||||
if self.is_async {
|
||||
attrs.push(Attr::new("async", None));
|
||||
}
|
||||
if self.is_module {
|
||||
attrs.push(Attr::new("type", Some("module".into())));
|
||||
}
|
||||
attrs.push(Attr::new("crossorigin", None));
|
||||
StringBuilder::build(|out| {
|
||||
out.append("<script");
|
||||
|
||||
write_attr_list(&attrs, out);
|
||||
|
||||
out.append("></script>");
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct NoOutput;
|
||||
|
||||
impl lol_html::OutputSink for NoOutput {
|
||||
fn handle_chunk(&mut self, _: &[u8]) {}
|
||||
}
|
||||
|
||||
fn collect_scripts(doc: &str) -> Result<Vec<Script>, AnyError> {
|
||||
let mut scripts = Vec::new();
|
||||
let mut rewriter = lol_html::HtmlRewriter::new(
|
||||
lol_html::Settings {
|
||||
element_content_handlers: vec![element!("script[src]", |el| {
|
||||
let is_ignored =
|
||||
el.has_attribute("deno-ignore") || el.has_attribute("vite-ignore");
|
||||
if is_ignored {
|
||||
return Ok(());
|
||||
}
|
||||
let typ = el.get_attribute("type");
|
||||
let (Some("module") | None) = typ.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let src = el.get_attribute("src").unwrap();
|
||||
let is_async = el.has_attribute("async");
|
||||
let is_module = matches!(typ.as_deref(), Some("module"));
|
||||
|
||||
scripts.push(Script {
|
||||
src: Some(src),
|
||||
is_async,
|
||||
is_module,
|
||||
resolved_path: None,
|
||||
});
|
||||
Ok(())
|
||||
})],
|
||||
..lol_html::Settings::new()
|
||||
},
|
||||
NoOutput,
|
||||
);
|
||||
rewriter.write(doc.as_bytes())?;
|
||||
rewriter.end()?;
|
||||
Ok(scripts)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HtmlEntrypoint {
|
||||
pub path: PathBuf,
|
||||
pub scripts: Vec<Script>,
|
||||
pub temp_module: String,
|
||||
pub contents: String,
|
||||
pub entry_name: String,
|
||||
|
||||
pub virtual_module_path: PathBuf,
|
||||
}
|
||||
|
||||
const VIRTUAL_ENTRY_SUFFIX: &str = ".deno-bundle-html.entry";
|
||||
|
||||
// Helper to create a filesystem-friendly name based on a path
|
||||
fn sanitize_entry_name(cwd: &Path, path: &Path) -> String {
|
||||
let rel =
|
||||
pathdiff::diff_paths(path, cwd).unwrap_or_else(|| path.to_path_buf());
|
||||
let stem = rel
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace(['\\', '/', ':'], "_");
|
||||
if stem.is_empty() {
|
||||
"entry".to_string()
|
||||
} else {
|
||||
stem
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_html_entrypoint(
|
||||
cwd: &Path,
|
||||
path: &Path,
|
||||
contents: String,
|
||||
) -> anyhow::Result<HtmlEntrypoint> {
|
||||
let mut scripts = collect_scripts(&contents)?;
|
||||
|
||||
let mut temp_module = String::new();
|
||||
for script in &mut scripts {
|
||||
if let Some(src) = &mut script.src {
|
||||
let src = src.trim_start_matches('/');
|
||||
let path = path.parent().unwrap_or(Path::new("")).join(src);
|
||||
|
||||
let url = deno_path_util::url_from_file_path(&path)?;
|
||||
|
||||
temp_module.push_str(&format!("import \"{}\";\n", url));
|
||||
script.resolved_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
let entry_name = sanitize_entry_name(cwd, path);
|
||||
let virtual_module_path = path
|
||||
.parent()
|
||||
.unwrap_or(Path::new(""))
|
||||
.join(format!("{}{}.js", entry_name, VIRTUAL_ENTRY_SUFFIX));
|
||||
|
||||
Ok(HtmlEntrypoint {
|
||||
path: path.to_path_buf(),
|
||||
scripts,
|
||||
temp_module,
|
||||
contents,
|
||||
entry_name,
|
||||
virtual_module_path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_html_entrypoint(
|
||||
cwd: &Path,
|
||||
path: &Path,
|
||||
) -> anyhow::Result<HtmlEntrypoint> {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
parse_html_entrypoint(cwd, path, contents)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedOutput {
|
||||
path: PathBuf,
|
||||
index: usize,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HtmlOutputFiles<'a, 'f> {
|
||||
output_files: &'f mut Vec<OutputFile<'a>>,
|
||||
index: HashMap<String, ParsedOutput>,
|
||||
}
|
||||
|
||||
impl<'a, 'f> HtmlOutputFiles<'a, 'f> {
|
||||
pub fn new(output_files: &'f mut Vec<OutputFile<'a>>) -> Self {
|
||||
let re =
|
||||
lazy_regex::regex!(r"(^.+\.deno-bundle-html.entry)-([^.]+)(\..+)$");
|
||||
let mut index = std::collections::HashMap::new();
|
||||
for (i, f) in output_files.iter().enumerate() {
|
||||
if let Some(name) = f.path.file_name().map(|s| s.to_string_lossy()) {
|
||||
let Some(captures) = re.captures(&name) else {
|
||||
continue;
|
||||
};
|
||||
let mut entry_name = captures.get(1).unwrap().as_str().to_string();
|
||||
let ext = captures.get(3).unwrap().as_str();
|
||||
entry_name.push_str(ext);
|
||||
|
||||
index.insert(
|
||||
entry_name,
|
||||
ParsedOutput {
|
||||
path: f.path.clone(),
|
||||
index: i,
|
||||
hash: captures.get(2).unwrap().as_str().to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
output_files,
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_and_update_path(
|
||||
&mut self,
|
||||
name: &str,
|
||||
f: impl FnOnce(PathBuf, &ParsedOutput) -> PathBuf,
|
||||
) -> Option<PathBuf> {
|
||||
let parsed_output = self.index.get_mut(name)?;
|
||||
let new_path = f(parsed_output.path.clone(), parsed_output);
|
||||
parsed_output.path = new_path.clone();
|
||||
self.output_files[parsed_output.index].path = new_path.clone();
|
||||
Some(new_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlEntrypoint {
|
||||
fn original_entry_name(&self) -> String {
|
||||
self.path.file_stem().unwrap().to_string_lossy().to_string()
|
||||
}
|
||||
pub fn patch_html_with_response<'a>(
|
||||
self,
|
||||
_cwd: &Path,
|
||||
outdir: &Path,
|
||||
html_output_files: &mut HtmlOutputFiles<'a, '_>,
|
||||
) -> anyhow::Result<()> {
|
||||
let original_entry_name = self.original_entry_name();
|
||||
|
||||
if self.scripts.is_empty() {
|
||||
let html_out_path =
|
||||
// TODO(nathanwhit): not really correct
|
||||
{ outdir.join(format!("{}.html", &original_entry_name)) };
|
||||
html_output_files.output_files.push(OutputFile {
|
||||
path: html_out_path,
|
||||
contents: Cow::Owned(self.contents.into_bytes()),
|
||||
hash: None,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let entry_name = format!("{}{}", self.entry_name, VIRTUAL_ENTRY_SUFFIX);
|
||||
let js_entry_name = format!("{}.js", entry_name);
|
||||
|
||||
let mut js_out_no_hash = None;
|
||||
let js_out = html_output_files
|
||||
.get_and_update_path(&js_entry_name, |p, f| {
|
||||
let p = p.to_string_lossy();
|
||||
js_out_no_hash = Some(
|
||||
p.replace(entry_name.as_str(), &original_entry_name)
|
||||
.replace(&format!("-{}", f.hash), "")
|
||||
.into(),
|
||||
);
|
||||
|
||||
p.replace(entry_name.as_str(), &original_entry_name).into()
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"failed to locate output for HTML entry '{}'; {js_entry_name}",
|
||||
self.entry_name
|
||||
)
|
||||
})?;
|
||||
let html_out_path = js_out_no_hash
|
||||
.unwrap_or_else(|| js_out.clone())
|
||||
.with_extension("html");
|
||||
|
||||
let css_entry_name = format!("{}.css", entry_name);
|
||||
let css_out =
|
||||
html_output_files.get_and_update_path(&css_entry_name, |p, _| {
|
||||
p.to_string_lossy()
|
||||
.replace(entry_name.as_str(), &original_entry_name)
|
||||
.into()
|
||||
});
|
||||
|
||||
let script_src = {
|
||||
let base = html_out_path.parent().unwrap_or(outdir);
|
||||
let mut rel = pathdiff::diff_paths(&js_out, base)
|
||||
.unwrap_or_else(|| js_out.clone())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if std::path::MAIN_SEPARATOR != '/' {
|
||||
rel = rel.replace('\\', "/");
|
||||
}
|
||||
rel
|
||||
};
|
||||
let any_async = self.scripts.iter().any(|s| s.is_async);
|
||||
let any_module = self.scripts.iter().any(|s| s.is_module);
|
||||
|
||||
let to_inject = Script {
|
||||
src: Some(
|
||||
if !script_src.starts_with(".") && !script_src.starts_with("/") {
|
||||
format!("./{}", script_src)
|
||||
} else {
|
||||
script_src
|
||||
},
|
||||
),
|
||||
is_async: any_async,
|
||||
is_module: any_module,
|
||||
resolved_path: None,
|
||||
};
|
||||
|
||||
let css_href = css_out.as_ref().map(|p| {
|
||||
let base = html_out_path.parent().unwrap_or(outdir);
|
||||
let mut rel = pathdiff::diff_paths(p, base)
|
||||
.unwrap_or_else(|| p.clone())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if std::path::MAIN_SEPARATOR != '/' {
|
||||
rel = rel.replace('\\', "/");
|
||||
}
|
||||
if !rel.starts_with(".") && !rel.starts_with("/") {
|
||||
rel = format!("./{}", rel);
|
||||
}
|
||||
rel
|
||||
});
|
||||
|
||||
let patched = inject_scripts_and_css(
|
||||
&self.contents,
|
||||
to_inject,
|
||||
&self.scripts,
|
||||
css_href,
|
||||
)?;
|
||||
|
||||
html_output_files.output_files.push(OutputFile {
|
||||
path: html_out_path,
|
||||
contents: Cow::Owned(patched.into_bytes()),
|
||||
hash: None,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_link_str(attrs: &[Attr]) -> String {
|
||||
StringBuilder::build(|out| {
|
||||
out.append("<link");
|
||||
write_attr_list(attrs, out);
|
||||
out.append(">");
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn stylesheet_str(path: &str) -> String {
|
||||
let attrs = &[
|
||||
Attr::new("rel", Some("stylesheet".into())),
|
||||
Attr::new("crossorigin", None),
|
||||
Attr::new("href", Some(Cow::Borrowed(path))),
|
||||
];
|
||||
make_link_str(attrs)
|
||||
}
|
||||
|
||||
fn inject_scripts_and_css(
|
||||
input: &str,
|
||||
to_inject: Script,
|
||||
to_remove: &[Script],
|
||||
css_to_inject_path: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let did_inject = Cell::new(false);
|
||||
let rewritten = lol_html::rewrite_str(
|
||||
input,
|
||||
lol_html::Settings {
|
||||
element_content_handlers: vec![
|
||||
element!("head", |el| {
|
||||
let already_done = did_inject.replace(true);
|
||||
if already_done {
|
||||
return Ok(());
|
||||
}
|
||||
el.append(&to_inject.to_element_string(), LolContentType::Html);
|
||||
|
||||
if let Some(css_to_inject_path) = &css_to_inject_path {
|
||||
let link = stylesheet_str(css_to_inject_path);
|
||||
el.append(&link, LolContentType::Html);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}),
|
||||
element!("script[src]", |el| {
|
||||
let src = el.get_attribute("src").unwrap();
|
||||
if to_remove
|
||||
.iter()
|
||||
.any(|script| script.src.as_deref() == Some(src.as_str()))
|
||||
{
|
||||
el.remove();
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
],
|
||||
document_content_handlers: vec![lol_html::end!(|end| {
|
||||
if !did_inject.replace(true) {
|
||||
let script = to_inject.to_element_string();
|
||||
let link = css_to_inject_path
|
||||
.as_ref()
|
||||
.map(|p| stylesheet_str(p))
|
||||
.unwrap_or_default();
|
||||
end.append(
|
||||
&format!("<head>{script}{link}</head>"),
|
||||
LolContentType::Html,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})],
|
||||
..lol_html::Settings::new()
|
||||
},
|
||||
)?;
|
||||
Ok(rewritten)
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
mod esbuild;
|
||||
mod externals;
|
||||
mod html;
|
||||
mod provider;
|
||||
mod transform;
|
||||
|
||||
|
@ -45,7 +46,6 @@ use deno_resolver::loader::RequestedModuleType;
|
|||
use deno_resolver::npm::managed::ResolvePkgFolderFromDenoModuleError;
|
||||
use deno_runtime::deno_permissions::PermissionsContainer;
|
||||
use deno_semver::npm::NpmPackageReqReference;
|
||||
use esbuild_client::EsbuildFlags;
|
||||
use esbuild_client::EsbuildFlagsBuilder;
|
||||
use esbuild_client::EsbuildService;
|
||||
use esbuild_client::protocol;
|
||||
|
@ -80,6 +80,105 @@ use crate::util::file_watcher::WatcherRestartMode;
|
|||
static DISABLE_HACK: LazyLock<bool> =
|
||||
LazyLock::new(|| std::env::var("NO_DENO_BUNDLE_HACK").is_err());
|
||||
|
||||
pub async fn prepare_inputs(
|
||||
resolver: &CliResolver,
|
||||
sys: CliSys,
|
||||
npm_resolver: &CliNpmResolver,
|
||||
node_resolver: &CliNodeResolver,
|
||||
init_cwd: &Path,
|
||||
bundle_flags: &BundleFlags,
|
||||
plugin_handler: &mut DenoPluginHandler,
|
||||
) -> Result<BundlerInput, AnyError> {
|
||||
let resolved_entrypoints =
|
||||
resolve_entrypoints(resolver, init_cwd, &bundle_flags.entrypoints)?;
|
||||
|
||||
// Partition into HTML and non-HTML entrypoints
|
||||
let mut html_paths = Vec::new();
|
||||
let mut script_entry_urls = Vec::new();
|
||||
for url in &resolved_entrypoints {
|
||||
if url.as_str().to_lowercase().ends_with(".html") {
|
||||
html_paths.push(url.to_file_path().unwrap());
|
||||
} else {
|
||||
script_entry_urls.push(url.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if html_paths.is_empty() {
|
||||
let _ = plugin_handler
|
||||
.prepare_module_load(&resolved_entrypoints)
|
||||
.await;
|
||||
|
||||
let roots =
|
||||
resolve_roots(resolved_entrypoints, sys, npm_resolver, node_resolver);
|
||||
let _ = plugin_handler.prepare_module_load(&roots).await;
|
||||
Ok(BundlerInput::Entrypoints(
|
||||
roots.into_iter().map(|e| ("".into(), e.into())).collect(),
|
||||
))
|
||||
} else {
|
||||
// require an outdir when any HTML is present
|
||||
if bundle_flags.output_dir.is_none() {
|
||||
return Err(deno_core::anyhow::anyhow!(
|
||||
"--outdir is required when bundling HTML entrypoints",
|
||||
));
|
||||
}
|
||||
if bundle_flags.output_path.is_some() {
|
||||
return Err(deno_core::anyhow::anyhow!(
|
||||
"--output is not supported with HTML entrypoints; use --outdir",
|
||||
));
|
||||
}
|
||||
|
||||
// Prepare HTML pages and temp entry modules
|
||||
let mut html_pages = Vec::new();
|
||||
let mut to_cache_urls = Vec::new();
|
||||
let mut entries: Vec<(String, String)> = Vec::new();
|
||||
let mut virtual_modules = VirtualModules::new();
|
||||
|
||||
for html_path in &html_paths {
|
||||
let entry = html::load_html_entrypoint(init_cwd, html_path)?;
|
||||
|
||||
let virtual_module_path =
|
||||
deno_path_util::url_from_file_path(&entry.virtual_module_path)?;
|
||||
let virtual_module_path = virtual_module_path.to_string();
|
||||
virtual_modules.insert(
|
||||
virtual_module_path.clone(),
|
||||
VirtualModule::new(
|
||||
entry.temp_module.as_bytes().to_vec(),
|
||||
esbuild_client::BuiltinLoader::Js,
|
||||
),
|
||||
);
|
||||
|
||||
for script in &entry.scripts {
|
||||
if let Some(path) = &script.resolved_path {
|
||||
let url = deno_path_util::url_from_file_path(path)?;
|
||||
to_cache_urls.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
entries.push(("".into(), virtual_module_path));
|
||||
html_pages.push(entry);
|
||||
}
|
||||
|
||||
plugin_handler.virtual_modules = Some(Arc::new(virtual_modules));
|
||||
|
||||
// Prepare non-HTML entries too
|
||||
let _ = plugin_handler.prepare_module_load(&script_entry_urls).await;
|
||||
let roots =
|
||||
resolve_roots(script_entry_urls, sys, npm_resolver, node_resolver);
|
||||
let _ = plugin_handler.prepare_module_load(&roots).await;
|
||||
for url in roots {
|
||||
entries.push(("".into(), url.into()));
|
||||
}
|
||||
|
||||
// Pre-cache modules referenced by HTML pages
|
||||
let _ = plugin_handler.prepare_module_load(&to_cache_urls).await;
|
||||
|
||||
Ok(BundlerInput::EntrypointsWithHtml {
|
||||
entries,
|
||||
html_pages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn bundle_init(
|
||||
mut flags: Arc<Flags>,
|
||||
bundle_flags: &BundleFlags,
|
||||
|
@ -106,7 +205,7 @@ pub async fn bundle_init(
|
|||
|
||||
let (on_end_tx, on_end_rx) = tokio::sync::mpsc::channel(10);
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let plugin_handler = Arc::new(DenoPluginHandler {
|
||||
let mut plugin_handler = Arc::new(DenoPluginHandler {
|
||||
file_fetcher: factory.file_fetcher()?.clone(),
|
||||
resolver: resolver.clone(),
|
||||
module_load_preparer,
|
||||
|
@ -123,17 +222,19 @@ pub async fn bundle_init(
|
|||
cjs_tracker: factory.cjs_tracker()?.clone(),
|
||||
emitter: factory.emitter()?.clone(),
|
||||
deferred_resolve_errors: Default::default(),
|
||||
virtual_modules: None,
|
||||
});
|
||||
|
||||
let resolved_entrypoints =
|
||||
resolve_entrypoints(&resolver, &init_cwd, &bundle_flags.entrypoints)?;
|
||||
let _ = plugin_handler
|
||||
.prepare_module_load(&resolved_entrypoints)
|
||||
.await;
|
||||
|
||||
let roots =
|
||||
resolve_roots(resolved_entrypoints, sys, npm_resolver, node_resolver);
|
||||
let _ = plugin_handler.prepare_module_load(&roots).await;
|
||||
let input = prepare_inputs(
|
||||
&resolver,
|
||||
sys,
|
||||
npm_resolver,
|
||||
node_resolver,
|
||||
&init_cwd,
|
||||
bundle_flags,
|
||||
Arc::get_mut(&mut plugin_handler).unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let esbuild = EsbuildService::new(
|
||||
esbuild_path,
|
||||
|
@ -149,9 +250,10 @@ pub async fn bundle_init(
|
|||
let res = esbuild.wait_for_exit().await;
|
||||
log::warn!("esbuild exited: {:?}", res);
|
||||
});
|
||||
|
||||
let esbuild_flags = configure_esbuild_flags(bundle_flags);
|
||||
let entries = roots.into_iter().map(|e| ("".into(), e.into())).collect();
|
||||
let esbuild_flags = configure_esbuild_flags(
|
||||
bundle_flags,
|
||||
matches!(input, BundlerInput::EntrypointsWithHtml { .. }),
|
||||
);
|
||||
let bundler = EsbuildBundler::new(
|
||||
client,
|
||||
plugin_handler.clone(),
|
||||
|
@ -162,7 +264,7 @@ pub async fn bundle_init(
|
|||
on_end_rx,
|
||||
init_cwd.clone(),
|
||||
esbuild_flags,
|
||||
entries,
|
||||
input.clone(),
|
||||
);
|
||||
|
||||
Ok(bundler)
|
||||
|
@ -199,6 +301,7 @@ pub async fn bundle(
|
|||
bundler,
|
||||
bundle_flags.minify,
|
||||
bundle_flags.platform,
|
||||
bundle_flags.output_dir.as_ref().map(Path::new),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
@ -216,6 +319,8 @@ pub async fn bundle(
|
|||
&init_cwd,
|
||||
should_replace_require_shim(bundle_flags.platform),
|
||||
bundle_flags.minify,
|
||||
bundler.input.clone(),
|
||||
bundle_flags.output_dir.as_ref().map(Path::new),
|
||||
)?;
|
||||
|
||||
if bundle_flags.output_dir.is_some() || bundle_flags.output_path.is_some() {
|
||||
|
@ -245,16 +350,41 @@ async fn bundle_watch(
|
|||
bundler: EsbuildBundler,
|
||||
minified: bool,
|
||||
platform: BundlePlatform,
|
||||
output_dir: Option<&Path>,
|
||||
) -> Result<(), AnyError> {
|
||||
let initial_roots = bundler
|
||||
.roots
|
||||
let (initial_roots, always_watch) = match &bundler.input {
|
||||
BundlerInput::Entrypoints(entries) => (
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|(_, root)| {
|
||||
let url = Url::parse(root).ok()?;
|
||||
deno_path_util::url_to_file_path(&url).ok()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
vec![],
|
||||
),
|
||||
BundlerInput::EntrypointsWithHtml {
|
||||
entries,
|
||||
html_pages,
|
||||
} => {
|
||||
let mut roots = entries
|
||||
.iter()
|
||||
.filter_map(|(_, root)| {
|
||||
let url = Url::parse(root).ok()?;
|
||||
deno_path_util::url_to_file_path(&url).ok()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let always = html_pages
|
||||
.iter()
|
||||
.map(|p| p.path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
roots.extend(always.iter().cloned());
|
||||
(roots, always)
|
||||
}
|
||||
};
|
||||
let always_watch = Rc::new(always_watch);
|
||||
let current_roots = Rc::new(RefCell::new(initial_roots.clone()));
|
||||
let input = bundler.input.clone();
|
||||
let bundler = Rc::new(tokio::sync::Mutex::new(bundler));
|
||||
let mut print_config =
|
||||
crate::util::file_watcher::PrintConfig::new_with_banner(
|
||||
|
@ -269,6 +399,8 @@ async fn bundle_watch(
|
|||
watcher_communicator.show_path_changed(changed_paths.clone());
|
||||
let bundler = Rc::clone(&bundler);
|
||||
let current_roots = current_roots.clone();
|
||||
let input = input.clone();
|
||||
let always_watch = always_watch.clone();
|
||||
Ok(async move {
|
||||
let mut bundler = bundler.lock().await;
|
||||
let start = std::time::Instant::now();
|
||||
|
@ -291,10 +423,13 @@ async fn bundle_watch(
|
|||
&bundler.cwd,
|
||||
should_replace_require_shim(platform),
|
||||
minified,
|
||||
input,
|
||||
output_dir,
|
||||
)?;
|
||||
print_finished_message(&metafile, &output_infos, start.elapsed())?;
|
||||
|
||||
let new_watched = get_input_paths_for_watch(&response);
|
||||
let mut new_watched = get_input_paths_for_watch(&response);
|
||||
new_watched.extend(always_watch.iter().cloned());
|
||||
*current_roots.borrow_mut() = new_watched.clone();
|
||||
let _ = watcher_communicator.watch_paths(new_watched);
|
||||
} else {
|
||||
|
@ -339,6 +474,17 @@ pub enum BundlingMode {
|
|||
Watch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BundlerInput {
|
||||
Entrypoints(Vec<(String, String)>),
|
||||
EntrypointsWithHtml {
|
||||
entries: Vec<(String, String)>,
|
||||
html_pages: Vec<html::HtmlEntrypoint>,
|
||||
},
|
||||
}
|
||||
|
||||
pub type EsbuildFlags = Vec<String>;
|
||||
|
||||
pub struct EsbuildBundler {
|
||||
client: esbuild_client::ProtocolClient,
|
||||
plugin_handler: Arc<DenoPluginHandler>,
|
||||
|
@ -346,7 +492,7 @@ pub struct EsbuildBundler {
|
|||
mode: BundlingMode,
|
||||
cwd: PathBuf,
|
||||
flags: EsbuildFlags,
|
||||
roots: Vec<(String, String)>,
|
||||
input: BundlerInput,
|
||||
}
|
||||
|
||||
impl EsbuildBundler {
|
||||
|
@ -357,7 +503,7 @@ impl EsbuildBundler {
|
|||
on_end_rx: tokio::sync::mpsc::Receiver<esbuild_client::OnEndArgs>,
|
||||
cwd: PathBuf,
|
||||
flags: EsbuildFlags,
|
||||
roots: Vec<(String, String)>,
|
||||
input: BundlerInput,
|
||||
) -> EsbuildBundler {
|
||||
EsbuildBundler {
|
||||
client,
|
||||
|
@ -366,7 +512,7 @@ impl EsbuildBundler {
|
|||
mode,
|
||||
cwd,
|
||||
flags,
|
||||
roots,
|
||||
input,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -376,10 +522,14 @@ impl EsbuildBundler {
|
|||
// doesn't actually do anything, it's just registering the args/flags
|
||||
// we're going to use for all of the rebuilds.
|
||||
fn make_build_request(&self) -> protocol::BuildRequest {
|
||||
let entries = match &self.input {
|
||||
BundlerInput::Entrypoints(entries) => entries.clone(),
|
||||
BundlerInput::EntrypointsWithHtml { entries, .. } => entries.clone(),
|
||||
};
|
||||
protocol::BuildRequest {
|
||||
entries: self.roots.clone(),
|
||||
entries,
|
||||
key: 0,
|
||||
flags: self.flags.to_flags(),
|
||||
flags: self.flags.clone(),
|
||||
write: false,
|
||||
stdin_contents: None.into(),
|
||||
stdin_resolve_dir: None.into(),
|
||||
|
@ -406,7 +556,7 @@ impl EsbuildBundler {
|
|||
}
|
||||
|
||||
async fn build(&self) -> Result<BuildResponse, AnyError> {
|
||||
let response = self
|
||||
let response: BuildResponse = self
|
||||
.client
|
||||
.send_build_request(self.make_build_request())
|
||||
.await
|
||||
|
@ -589,6 +739,41 @@ fn requested_type_from_map(
|
|||
}
|
||||
}
|
||||
|
||||
pub struct VirtualModule {
|
||||
contents: Vec<u8>,
|
||||
loader: esbuild_client::BuiltinLoader,
|
||||
}
|
||||
|
||||
impl VirtualModule {
|
||||
pub fn new(contents: Vec<u8>, loader: esbuild_client::BuiltinLoader) -> Self {
|
||||
Self { contents, loader }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualModules {
|
||||
modules: IndexMap<String, VirtualModule>,
|
||||
}
|
||||
|
||||
impl VirtualModules {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
modules: IndexMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, path: String, contents: VirtualModule) {
|
||||
self.modules.insert(path, contents);
|
||||
}
|
||||
|
||||
pub fn get(&self, path: &str) -> Option<&VirtualModule> {
|
||||
self.modules.get(path)
|
||||
}
|
||||
|
||||
pub fn contains(&self, path: &str) -> bool {
|
||||
self.modules.contains_key(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeferredResolveError {
|
||||
path: String,
|
||||
error: ResolveWithGraphError,
|
||||
|
@ -604,7 +789,7 @@ pub struct DenoPluginHandler {
|
|||
externals_matcher: Option<ExternalsMatcher>,
|
||||
on_end_tx: tokio::sync::mpsc::Sender<esbuild_client::OnEndArgs>,
|
||||
deferred_resolve_errors: Arc<Mutex<Vec<DeferredResolveError>>>,
|
||||
|
||||
virtual_modules: Option<Arc<VirtualModules>>,
|
||||
parsed_source_cache: Arc<ParsedSourceCache>,
|
||||
cjs_tracker: Arc<CliCjsTracker>,
|
||||
emitter: Arc<CliEmitter>,
|
||||
|
@ -673,6 +858,18 @@ impl esbuild_client::PluginHandler for DenoPluginHandler {
|
|||
args: esbuild_client::OnResolveArgs,
|
||||
) -> Result<Option<esbuild_client::OnResolveResult>, AnyError> {
|
||||
log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_resolve"));
|
||||
|
||||
if let Some(virtual_modules) = &self.virtual_modules
|
||||
&& virtual_modules.contains(&args.path)
|
||||
{
|
||||
return Ok(Some(esbuild_client::OnResolveResult {
|
||||
path: Some(args.path),
|
||||
plugin_name: Some("deno".to_string()),
|
||||
namespace: Some("deno".to_string()),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(matcher) = &self.externals_matcher
|
||||
&& matcher.is_pre_resolve_match(&args.path)
|
||||
{
|
||||
|
@ -743,6 +940,15 @@ impl esbuild_client::PluginHandler for DenoPluginHandler {
|
|||
args: esbuild_client::OnLoadArgs,
|
||||
) -> Result<Option<esbuild_client::OnLoadResult>, AnyError> {
|
||||
log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_load"));
|
||||
if let Some(virtual_modules) = &self.virtual_modules
|
||||
&& let Some(module) = virtual_modules.get(&args.path)
|
||||
{
|
||||
return Ok(Some(esbuild_client::OnLoadResult {
|
||||
contents: Some(module.contents.clone()),
|
||||
loader: Some(module.loader),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
let result = self
|
||||
.bundle_load(&args.path, &requested_type_from_map(&args.with))
|
||||
.await;
|
||||
|
@ -1497,14 +1703,17 @@ async fn ensure_esbuild_downloaded(
|
|||
Ok(esbuild_path)
|
||||
}
|
||||
|
||||
fn configure_esbuild_flags(bundle_flags: &BundleFlags) -> EsbuildFlags {
|
||||
fn configure_esbuild_flags(
|
||||
bundle_flags: &BundleFlags,
|
||||
is_html: bool,
|
||||
) -> Vec<String> {
|
||||
let mut builder = EsbuildFlagsBuilder::default();
|
||||
|
||||
builder
|
||||
.bundle(bundle_flags.inline_imports)
|
||||
.minify(bundle_flags.minify)
|
||||
.splitting(bundle_flags.code_splitting)
|
||||
.external(bundle_flags.external.clone())
|
||||
.externals(bundle_flags.external.clone())
|
||||
.tree_shaking(true)
|
||||
.format(match bundle_flags.format {
|
||||
BundleFormat::Esm => esbuild_client::Format::Esm,
|
||||
|
@ -1531,6 +1740,14 @@ fn configure_esbuild_flags(bundle_flags: &BundleFlags) -> EsbuildFlags {
|
|||
}
|
||||
builder.metafile(true);
|
||||
|
||||
if is_html {
|
||||
builder.platform(esbuild_client::Platform::Browser);
|
||||
builder.splitting(true);
|
||||
builder.entry_names("[dir]/[name]-[hash]");
|
||||
builder.chunk_names("[dir]/[name]-[hash]");
|
||||
builder.asset_names("[dir]/[name]-[hash]");
|
||||
builder.metafile(true);
|
||||
}
|
||||
match bundle_flags.platform {
|
||||
deno_bundle_runtime::BundlePlatform::Browser => {
|
||||
builder.platform(esbuild_client::Platform::Browser);
|
||||
|
@ -1538,7 +1755,7 @@ fn configure_esbuild_flags(bundle_flags: &BundleFlags) -> EsbuildFlags {
|
|||
deno_bundle_runtime::BundlePlatform::Deno => {}
|
||||
}
|
||||
|
||||
builder.build().unwrap()
|
||||
builder.build()
|
||||
}
|
||||
|
||||
// extract the path from a message like "Could not resolve "path/to/file.ts""
|
||||
|
@ -1588,17 +1805,6 @@ fn handle_esbuild_errors_and_warnings(
|
|||
}
|
||||
}
|
||||
|
||||
fn is_js(path: &Path) -> bool {
|
||||
if let Some(ext) = path.extension() {
|
||||
matches!(
|
||||
ext.to_string_lossy().as_ref(),
|
||||
"js" | "mjs" | "cjs" | "jsx" | "ts" | "tsx" | "mts" | "cts" | "dts"
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OutputFileInfo {
|
||||
relative_path: PathBuf,
|
||||
size: usize,
|
||||
|
@ -1610,19 +1816,30 @@ pub struct ProcessedContents {
|
|||
is_js: bool,
|
||||
}
|
||||
|
||||
fn is_js(path: &Path) -> bool {
|
||||
if let Some(ext) = path.extension() {
|
||||
matches!(
|
||||
ext.to_string_lossy().as_ref(),
|
||||
"js" | "mjs" | "cjs" | "jsx" | "ts" | "tsx" | "mts" | "cts" | "dts"
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_process_contents(
|
||||
file: &esbuild_client::protocol::BuildOutputFile,
|
||||
file: &OutputFile<'_>,
|
||||
should_replace_require_shim: bool,
|
||||
minified: bool,
|
||||
) -> Result<ProcessedContents, AnyError> {
|
||||
let path = Path::new(&file.path);
|
||||
let is_js = is_js(path) || file.path.ends_with("<stdout>");
|
||||
let path = &file.path;
|
||||
let is_js = is_js(path) || path.ends_with("<stdout>");
|
||||
if is_js {
|
||||
let string = String::from_utf8(file.contents.clone())?;
|
||||
let string = str::from_utf8(&file.contents)?;
|
||||
let string = if should_replace_require_shim {
|
||||
replace_require_shim(&string, minified)
|
||||
replace_require_shim(string, minified)
|
||||
} else {
|
||||
string
|
||||
string.to_string()
|
||||
};
|
||||
Ok(ProcessedContents {
|
||||
contents: Some(string.into_bytes()),
|
||||
|
@ -1635,18 +1852,101 @@ pub fn maybe_process_contents(
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OutputFile<'a> {
|
||||
pub path: PathBuf,
|
||||
pub contents: Cow<'a, [u8]>,
|
||||
pub hash: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for OutputFile<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OutputFile")
|
||||
.field("path", &self.path)
|
||||
.field("hash", &self.hash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a esbuild_client::protocol::BuildOutputFile>
|
||||
for OutputFile<'a>
|
||||
{
|
||||
fn from(file: &'a esbuild_client::protocol::BuildOutputFile) -> Self {
|
||||
OutputFile {
|
||||
path: PathBuf::from(&file.path),
|
||||
contents: Cow::Borrowed(&file.contents),
|
||||
hash: Some(file.hash.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<esbuild_client::protocol::BuildOutputFile> for OutputFile<'a> {
|
||||
fn from(file: esbuild_client::protocol::BuildOutputFile) -> Self {
|
||||
OutputFile {
|
||||
path: PathBuf::from(&file.path),
|
||||
contents: Cow::Owned(file.contents),
|
||||
hash: Some(file.hash),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_output_files<'a>(
|
||||
response_output_files: Option<&'a [protocol::BuildOutputFile]>,
|
||||
cwd: &Path,
|
||||
input: BundlerInput,
|
||||
outdir: Option<&Path>,
|
||||
) -> Result<Vec<OutputFile<'a>>, AnyError> {
|
||||
let outdir = if let Some(outdir) = outdir {
|
||||
if outdir.is_absolute() {
|
||||
Some(outdir.to_path_buf())
|
||||
} else {
|
||||
Some(cwd.join(outdir))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut output_files: Vec<OutputFile> = response_output_files
|
||||
.map(|fs| {
|
||||
fs.iter()
|
||||
.map(|f| OutputFile {
|
||||
path: PathBuf::from(&f.path),
|
||||
contents: Cow::Borrowed(&f.contents),
|
||||
hash: Some(f.hash.clone()),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if let BundlerInput::EntrypointsWithHtml {
|
||||
entries: _,
|
||||
html_pages,
|
||||
} = input
|
||||
{
|
||||
let outdir = outdir.ok_or_else(|| {
|
||||
deno_core::anyhow::anyhow!(
|
||||
"--outdir is required when bundling HTML entrypoints",
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut html_output_files = html::HtmlOutputFiles::new(&mut output_files);
|
||||
for page in html_pages {
|
||||
page.patch_html_with_response(cwd, &outdir, &mut html_output_files)?;
|
||||
}
|
||||
}
|
||||
Ok(output_files)
|
||||
}
|
||||
|
||||
pub fn process_result(
|
||||
response: &BuildResponse,
|
||||
cwd: &Path,
|
||||
should_replace_require_shim: bool,
|
||||
minified: bool,
|
||||
input: BundlerInput,
|
||||
outdir: Option<&Path>,
|
||||
) -> Result<Vec<OutputFileInfo>, AnyError> {
|
||||
let output_files =
|
||||
collect_output_files(response.output_files.as_deref(), cwd, input, outdir)?;
|
||||
let mut exists_cache = std::collections::HashSet::new();
|
||||
let output_files = response
|
||||
.output_files
|
||||
.as_ref()
|
||||
.map(Cow::Borrowed)
|
||||
.unwrap_or_default();
|
||||
let mut output_infos = Vec::new();
|
||||
for file in output_files.iter() {
|
||||
let processed_contents =
|
||||
|
@ -1655,13 +1955,13 @@ pub fn process_result(
|
|||
let relative_path =
|
||||
pathdiff::diff_paths(path, cwd).unwrap_or_else(|| path.to_path_buf());
|
||||
let is_js = processed_contents.is_js;
|
||||
let bytes = processed_contents
|
||||
let bytes: Cow<'_, [u8]> = processed_contents
|
||||
.contents
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| Cow::Borrowed(&file.contents));
|
||||
.unwrap_or_else(|| Cow::Borrowed(file.contents.as_ref()));
|
||||
|
||||
if file.path.ends_with("<stdout>") {
|
||||
crate::display::write_to_stdout_ignore_sigpipe(bytes.as_slice())?;
|
||||
crate::display::write_to_stdout_ignore_sigpipe(bytes.as_ref())?;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use deno_bundle_runtime as rt_bundle;
|
||||
|
@ -90,23 +91,46 @@ pub fn convert_build_response(
|
|||
}
|
||||
}
|
||||
|
||||
fn hash_contents(contents: &[u8]) -> String {
|
||||
use base64::prelude::*;
|
||||
let hash = twox_hash::XxHash64::oneshot(0, contents);
|
||||
let bytes = hash.to_le_bytes();
|
||||
base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes)
|
||||
}
|
||||
|
||||
fn process_output_files(
|
||||
bundle_flags: &crate::args::BundleFlags,
|
||||
response: &mut esbuild_client::protocol::BuildResponse,
|
||||
cwd: &Path,
|
||||
input: super::BundlerInput,
|
||||
) -> Result<(), AnyError> {
|
||||
if let Some(files) = &mut response.output_files {
|
||||
for file in files {
|
||||
if let Some(files) = std::mem::take(&mut response.output_files) {
|
||||
let output_files = super::collect_output_files(
|
||||
Some(&*files),
|
||||
cwd,
|
||||
input,
|
||||
bundle_flags.output_dir.as_ref().map(Path::new),
|
||||
)?;
|
||||
let mut new_files = Vec::new();
|
||||
|
||||
for output_file in output_files {
|
||||
let processed_contents = crate::tools::bundle::maybe_process_contents(
|
||||
file,
|
||||
&output_file,
|
||||
crate::tools::bundle::should_replace_require_shim(
|
||||
bundle_flags.platform,
|
||||
),
|
||||
bundle_flags.minify,
|
||||
)?;
|
||||
if let Some(contents) = processed_contents.contents {
|
||||
file.contents = contents;
|
||||
}
|
||||
let contents = processed_contents
|
||||
.contents
|
||||
.unwrap_or_else(|| output_file.contents.into_owned());
|
||||
new_files.push(esbuild_client::protocol::BuildOutputFile {
|
||||
path: output_file.path.to_string_lossy().into_owned(),
|
||||
hash: hash_contents(&contents),
|
||||
contents,
|
||||
});
|
||||
}
|
||||
response.output_files = Some(new_files);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -151,10 +175,17 @@ impl BundleProvider for CliBundleProvider {
|
|||
&bundler.cwd,
|
||||
true,
|
||||
bundle_flags.minify,
|
||||
bundler.input,
|
||||
bundle_flags.output_dir.as_ref().map(Path::new),
|
||||
)?;
|
||||
result.output_files = None;
|
||||
} else {
|
||||
process_output_files(&bundle_flags, &mut result)?;
|
||||
process_output_files(
|
||||
&bundle_flags,
|
||||
&mut result,
|
||||
&bundler.cwd,
|
||||
bundler.input,
|
||||
)?;
|
||||
}
|
||||
log::trace!("convert_build_response");
|
||||
let result = convert_build_response(result);
|
||||
|
|
65
tests/specs/bundle/html/__test__.jsonc
Normal file
65
tests/specs/bundle/html/__test__.jsonc
Normal file
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"tempDir": true,
|
||||
"tests": {
|
||||
"basic": {
|
||||
"steps": [
|
||||
{
|
||||
"args": "bundle --outdir ./dist index.html",
|
||||
"output": "basic-bundle.out"
|
||||
},
|
||||
{
|
||||
"args": "run -A basic-bundle.asserts.ts",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"multiple_scripts": {
|
||||
"steps": [
|
||||
{
|
||||
"args": "bundle --outdir ./dist multiple-scripts.html",
|
||||
"output": "multiple-scripts-bundle.out"
|
||||
},
|
||||
{
|
||||
"args": "run -A multiple-scripts-bundle.asserts.ts",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"multiple_html": {
|
||||
"steps": [
|
||||
{
|
||||
"args": "bundle --outdir ./dist index.html multiple-html.html",
|
||||
"output": "multiple-html-bundle.out"
|
||||
},
|
||||
{
|
||||
"args": "run -A multiple-html-bundle.asserts.ts",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"imports_css": {
|
||||
"steps": [
|
||||
{
|
||||
"args": "bundle --outdir ./dist imports-css.html",
|
||||
"output": "imports-css-bundle.out"
|
||||
},
|
||||
{
|
||||
"args": "run -A imports-css-bundle.asserts.ts",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"same_name_sub_folder": {
|
||||
"steps": [
|
||||
{
|
||||
"args": "bundle --outdir ./dist index.html sub/index.html",
|
||||
"output": "same-name-sub-folder-bundle.out"
|
||||
},
|
||||
{
|
||||
"args": "run -A same-name-sub-folder-bundle.asserts.ts",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
3
tests/specs/bundle/html/a.ts
Normal file
3
tests/specs/bundle/html/a.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const a = 1;
|
||||
document.body.insertAdjacentHTML("beforeend", "A");
|
||||
console.log("A", a);
|
32
tests/specs/bundle/html/assert-helpers.ts
Normal file
32
tests/specs/bundle/html/assert-helpers.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
export function assertFileContains(path: string, pattern: string | RegExp) {
|
||||
const contents = Deno.readTextFileSync(path);
|
||||
let matcher: (s: string) => boolean;
|
||||
if (typeof pattern === "string") {
|
||||
matcher = (s) => s.includes(pattern);
|
||||
} else {
|
||||
matcher = (s) => pattern.test(s);
|
||||
}
|
||||
if (!matcher(contents)) {
|
||||
let message = "";
|
||||
message += "file does not contain the pattern: " + path + "\n";
|
||||
message += "wanted: " + pattern + "\n";
|
||||
message += "found: " + contents + "\n";
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertFileDoesNotContain(
|
||||
path: string,
|
||||
pattern: string | RegExp,
|
||||
) {
|
||||
try {
|
||||
assertFileContains(path, pattern);
|
||||
} catch (_e) {
|
||||
return;
|
||||
}
|
||||
let message = "";
|
||||
message += "file contains the pattern: " + path + "\n";
|
||||
message += "did not want: " + pattern + "\n";
|
||||
message += "found: " + Deno.readTextFileSync(path) + "\n";
|
||||
throw new Error(message);
|
||||
}
|
3
tests/specs/bundle/html/b.ts
Normal file
3
tests/specs/bundle/html/b.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const b = 2;
|
||||
document.body.insertAdjacentHTML("beforeend", "B");
|
||||
console.log("B", b);
|
4
tests/specs/bundle/html/basic-bundle.asserts.ts
Normal file
4
tests/specs/bundle/html/basic-bundle.asserts.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { assertFileContains } from "./assert-helpers.ts";
|
||||
const re = /src="\.\/index-[^\.]+\.js"/;
|
||||
|
||||
assertFileContains("./dist/index.html", re);
|
5
tests/specs/bundle/html/basic-bundle.out
Normal file
5
tests/specs/bundle/html/basic-bundle.out
Normal file
|
@ -0,0 +1,5 @@
|
|||
[WILDCARD]
|
||||
Bundled 2 modules in [WILDLINE]
|
||||
dist[WILDCHAR]index-[WILDLINE].js [WILDLINE]
|
||||
dist[WILDCHAR]index.html [WILDLINE]
|
||||
|
8
tests/specs/bundle/html/deno.json
Normal file
8
tests/specs/bundle/html/deno.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"deno.window",
|
||||
"dom"
|
||||
]
|
||||
}
|
||||
}
|
26
tests/specs/bundle/html/file-has.ts
Normal file
26
tests/specs/bundle/html/file-has.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
const path = Deno.args[0];
|
||||
const pattern = Deno.args[1];
|
||||
|
||||
declare global {
|
||||
interface RegExpConstructor {
|
||||
escape(string: string): string;
|
||||
}
|
||||
}
|
||||
|
||||
const contents = Deno.readTextFileSync(path);
|
||||
|
||||
let matcher: RegExp | string = pattern;
|
||||
if (pattern.startsWith("regex:")) {
|
||||
matcher = pattern.slice(6);
|
||||
matcher = new RegExp(matcher);
|
||||
} else {
|
||||
matcher = new RegExp(RegExp.escape(pattern));
|
||||
}
|
||||
|
||||
if (matcher.test(contents)) {
|
||||
console.log("true");
|
||||
} else {
|
||||
console.log("false");
|
||||
console.log("wanted: ", matcher);
|
||||
console.log("found: ", contents);
|
||||
}
|
35
tests/specs/bundle/html/imports-css-bundle.asserts.ts
Normal file
35
tests/specs/bundle/html/imports-css-bundle.asserts.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { assertFileContains } from "./assert-helpers.ts";
|
||||
|
||||
assertFileContains(
|
||||
"./dist/imports-css.html",
|
||||
/src="\.\/imports-css-[^\.]+\.js"/,
|
||||
);
|
||||
assertFileContains(
|
||||
"./dist/imports-css.html",
|
||||
/href="\.\/imports-css-[^\.]+\.css"/,
|
||||
);
|
||||
|
||||
const jsCssFiles: string[] = [];
|
||||
Deno.readDirSync("./dist").forEach((entry) => {
|
||||
if (entry.name.endsWith(".js") || entry.name.endsWith(".css")) {
|
||||
jsCssFiles.push("./dist/" + entry.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (jsCssFiles.length === 0) {
|
||||
throw new Error("No .js files found");
|
||||
}
|
||||
|
||||
const importsCssJsFile = jsCssFiles.find((file) =>
|
||||
file.includes("imports-css") && file.endsWith(".js")
|
||||
);
|
||||
const cssFile = jsCssFiles.find((file) =>
|
||||
file.includes("imports-css") && file.endsWith(".css")
|
||||
);
|
||||
|
||||
if (!importsCssJsFile || !cssFile) {
|
||||
throw new Error("No imports-css.js or imports-css.css file found");
|
||||
}
|
||||
|
||||
assertFileContains(importsCssJsFile, "<h1>Hello, world!</h1>");
|
||||
assertFileContains(cssFile, "h1 {\n color: red;\n}");
|
8
tests/specs/bundle/html/imports-css-bundle.out
Normal file
8
tests/specs/bundle/html/imports-css-bundle.out
Normal file
|
@ -0,0 +1,8 @@
|
|||
[WILDCARD]
|
||||
Bundled 3 modules in [WILDLINE]
|
||||
[UNORDERED_START]
|
||||
dist[WILDCHAR]imports-css-[WILDLINE].js [WILDLINE]
|
||||
dist[WILDCHAR]imports-css-[WILDLINE].css [WILDLINE]
|
||||
dist[WILDCHAR]imports-css.html [WILDLINE]
|
||||
[UNORDERED_END]
|
||||
|
13
tests/specs/bundle/html/imports-css.html
Normal file
13
tests/specs/bundle/html/imports-css.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Deno bundle HTML</title>
|
||||
<script src="./imports-css.ts" type="module" ></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>This app requires JavaScript to run.</noscript>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
3
tests/specs/bundle/html/imports-css.ts
Normal file
3
tests/specs/bundle/html/imports-css.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import "./style.css";
|
||||
|
||||
document.body.insertAdjacentHTML("beforeend", "<h1>Hello, world!</h1>");
|
13
tests/specs/bundle/html/index.html
Normal file
13
tests/specs/bundle/html/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Deno bundle HTML</title>
|
||||
<script src="./index.ts" type="module" ></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>This app requires JavaScript to run.</noscript>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
1
tests/specs/bundle/html/index.ts
Normal file
1
tests/specs/bundle/html/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
document.body.innerHTML = "Hello, world!";
|
35
tests/specs/bundle/html/multiple-html-bundle.asserts.ts
Normal file
35
tests/specs/bundle/html/multiple-html-bundle.asserts.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import "./basic-bundle.asserts.ts";
|
||||
|
||||
import { assertFileContains } from "./assert-helpers.ts";
|
||||
|
||||
assertFileContains("./dist/index.html", /src="\.\/index-[^\.]+\.js"/);
|
||||
assertFileContains(
|
||||
"./dist/multiple-html.html",
|
||||
/src="\.\/multiple-html-[^\.]+\.js"/,
|
||||
);
|
||||
|
||||
const jsFiles: string[] = [];
|
||||
Deno.readDirSync("./dist").forEach((entry) => {
|
||||
if (entry.name.endsWith(".js")) {
|
||||
jsFiles.push("./dist/" + entry.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
throw new Error("No .js files found");
|
||||
}
|
||||
|
||||
const indexJsFile = jsFiles.find((file) => file.includes("index"));
|
||||
const multipleHtmlJsFile = jsFiles.find((file) =>
|
||||
file.includes("multiple-html")
|
||||
);
|
||||
|
||||
if (!indexJsFile || !multipleHtmlJsFile) {
|
||||
throw new Error("No index.js or multiple-html.js file found");
|
||||
}
|
||||
|
||||
assertFileContains(indexJsFile, "Hello, world!");
|
||||
assertFileContains(
|
||||
multipleHtmlJsFile,
|
||||
'document.body.insertAdjacentHTML("beforeend", "A");',
|
||||
);
|
7
tests/specs/bundle/html/multiple-html-bundle.out
Normal file
7
tests/specs/bundle/html/multiple-html-bundle.out
Normal file
|
@ -0,0 +1,7 @@
|
|||
[WILDCARD]
|
||||
Bundled 4 modules in [WILDLINE]
|
||||
dist[WILDCHAR]index-[WILDLINE].js [WILDLINE]
|
||||
dist[WILDCHAR]multiple-html-[WILDLINE].js [WILDLINE]
|
||||
dist[WILDCHAR]index.html [WILDLINE]
|
||||
dist[WILDCHAR]multiple-html.html [WILDLINE]
|
||||
|
13
tests/specs/bundle/html/multiple-html.html
Normal file
13
tests/specs/bundle/html/multiple-html.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Deno bundle HTML</title>
|
||||
<script src="./a.ts" type="module" ></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>This app requires JavaScript to run.</noscript>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
25
tests/specs/bundle/html/multiple-scripts-bundle.asserts.ts
Normal file
25
tests/specs/bundle/html/multiple-scripts-bundle.asserts.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { assertFileContains } from "./assert-helpers.ts";
|
||||
|
||||
let jsFile: string | undefined;
|
||||
Deno.readDirSync("./dist").forEach((entry) => {
|
||||
if (entry.name.endsWith(".js")) {
|
||||
jsFile = "./dist/" + entry.name;
|
||||
}
|
||||
});
|
||||
|
||||
if (!jsFile) {
|
||||
throw new Error("No .js file found");
|
||||
}
|
||||
|
||||
assertFileContains(
|
||||
"./dist/multiple-scripts.html",
|
||||
/src="\.\/multiple-scripts-[^\.]+\.js"/,
|
||||
);
|
||||
assertFileContains(
|
||||
jsFile,
|
||||
'insertAdjacentHTML("beforeend", "A")',
|
||||
);
|
||||
assertFileContains(
|
||||
jsFile,
|
||||
'insertAdjacentHTML("beforeend", "B")',
|
||||
);
|
5
tests/specs/bundle/html/multiple-scripts-bundle.out
Normal file
5
tests/specs/bundle/html/multiple-scripts-bundle.out
Normal file
|
@ -0,0 +1,5 @@
|
|||
[WILDCARD]
|
||||
Bundled 3 modules in [WILDLINE]
|
||||
dist[WILDCHAR]multiple-scripts-[WILDLINE].js [WILDLINE]
|
||||
dist[WILDCHAR]multiple-scripts.html [WILDLINE]
|
||||
|
13
tests/specs/bundle/html/multiple-scripts.html
Normal file
13
tests/specs/bundle/html/multiple-scripts.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Multi</title>
|
||||
<script src="./a.ts" type="module"></script>
|
||||
<script src="./b.ts" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
56
tests/specs/bundle/html/print-dir.ts
Normal file
56
tests/specs/bundle/html/print-dir.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
function printName(name: string, level = 0) {
|
||||
console.log("-".repeat(level + 1) + " " + name);
|
||||
}
|
||||
|
||||
function walk(
|
||||
dir: string,
|
||||
fn: (
|
||||
{ name, dir, level, kind }: {
|
||||
name: string;
|
||||
dir: string;
|
||||
level: number;
|
||||
kind: "file" | "dir" | "symlink";
|
||||
},
|
||||
) => void,
|
||||
{ maxLevel }: { maxLevel: number },
|
||||
) {
|
||||
const walkRecursive = (dir: string, level: number) => {
|
||||
if (maxLevel > 0 && level > maxLevel) {
|
||||
return;
|
||||
}
|
||||
const files = Deno.readDirSync(dir).toArray();
|
||||
files.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const file of files) {
|
||||
if (file.isDirectory) {
|
||||
fn({ name: file.name, dir, level, kind: "dir" });
|
||||
walkRecursive(dir + "/" + file.name, level + 1);
|
||||
} else if (file.isFile) {
|
||||
fn({ name: file.name, dir, level, kind: "file" });
|
||||
} else {
|
||||
fn({ name: file.name, dir, level, kind: "symlink" });
|
||||
}
|
||||
}
|
||||
};
|
||||
walkRecursive(dir, 0);
|
||||
}
|
||||
|
||||
function printDir(dir: string, maxLevel = 0) {
|
||||
walk(dir, ({ name, level, kind }) => {
|
||||
printName(name + (kind === "symlink" ? " (symlink)" : ""), level);
|
||||
}, { maxLevel });
|
||||
}
|
||||
let maxLevel = 0;
|
||||
for (let i = 0; i < Deno.args.length; i++) {
|
||||
const arg = Deno.args[i];
|
||||
if (arg.startsWith("--max-level=")) {
|
||||
maxLevel = parseInt(arg.split("=")[1]) - 1;
|
||||
maxLevel = Math.max(0, maxLevel);
|
||||
} else {
|
||||
console.log(arg);
|
||||
printDir(arg, maxLevel);
|
||||
if (i < Deno.args.length - 1) {
|
||||
console.log("");
|
||||
maxLevel = 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { assertFileContains } from "./assert-helpers.ts";
|
||||
|
||||
assertFileContains("./dist/index.html", /src="\.\/index-[^\.]+\.js"/);
|
||||
assertFileContains("./dist/sub/index.html", /src="\.\/index-[^\.]+\.js"/);
|
||||
|
||||
function walk(dir: string, fn: (entry: string) => void) {
|
||||
Deno.readDirSync(dir).forEach((entry) => {
|
||||
if (entry.isDirectory) {
|
||||
walk(dir + "/" + entry.name, fn);
|
||||
} else {
|
||||
fn(dir + "/" + entry.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const jsFiles: string[] = [];
|
||||
walk("./dist", (entry) => {
|
||||
if (entry.endsWith(".js")) {
|
||||
jsFiles.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
throw new Error("No .js files found");
|
||||
}
|
||||
|
||||
const subJsFile = jsFiles.find((file) => file.includes("sub"));
|
||||
|
||||
const indexJsFile = jsFiles.find((file) =>
|
||||
file.includes("index") && !file.includes("sub")
|
||||
);
|
||||
|
||||
if (!indexJsFile || !subJsFile) {
|
||||
throw new Error("No index.js or sub/index.js file found");
|
||||
}
|
||||
|
||||
assertFileContains(indexJsFile, "Hello, world!");
|
||||
assertFileContains(subJsFile, "Hello, world from sub!");
|
9
tests/specs/bundle/html/same-name-sub-folder-bundle.out
Normal file
9
tests/specs/bundle/html/same-name-sub-folder-bundle.out
Normal file
|
@ -0,0 +1,9 @@
|
|||
[WILDCARD]
|
||||
Bundled 4 modules in [WILDLINE]
|
||||
[UNORDERED_START]
|
||||
dist[WILDCHAR]index-[WILDLINE].js [WILDLINE]
|
||||
dist[WILDCHAR]sub[WILDCHAR]index-[WILDLINE].js [WILDLINE]
|
||||
dist[WILDCHAR]index.html [WILDLINE]
|
||||
dist[WILDCHAR]sub[WILDCHAR]index.html [WILDLINE]
|
||||
[UNORDERED_END]
|
||||
|
2
tests/specs/bundle/html/shared/mod.ts
Normal file
2
tests/specs/bundle/html/shared/mod.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const v = 1;
|
||||
document.body.textContent = "Nested";
|
3
tests/specs/bundle/html/style.css
Normal file
3
tests/specs/bundle/html/style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
h1 {
|
||||
color: red;
|
||||
}
|
12
tests/specs/bundle/html/sub/index.html
Normal file
12
tests/specs/bundle/html/sub/index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Nested</title>
|
||||
<script src="./index.ts" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
1
tests/specs/bundle/html/sub/index.ts
Normal file
1
tests/specs/bundle/html/sub/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
document.body.innerHTML = "Hello, world from sub!";
|
|
@ -8,7 +8,7 @@ import {
|
|||
unindent,
|
||||
} from "./test_util.ts";
|
||||
|
||||
import { join, toFileUrl } from "@std/path";
|
||||
import { basename, join, toFileUrl } from "@std/path";
|
||||
|
||||
class TempDir implements AsyncDisposable, Disposable {
|
||||
private path: string;
|
||||
|
@ -313,3 +313,55 @@ Deno.test("bundle: replaces require shim when platform is deno", async () => {
|
|||
const output = await evalEsmString(js.text());
|
||||
assertEquals(output.default, ["good", 1]);
|
||||
});
|
||||
|
||||
Deno.test("bundle: html works", async () => {
|
||||
using dir = new TempDir();
|
||||
const entry = dir.join("index.html");
|
||||
const input = unindent`
|
||||
<html>
|
||||
<body>
|
||||
<script src="./index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const script = dir.join("index.ts");
|
||||
const scriptInput = unindent`
|
||||
console.log("hello");
|
||||
document.body.innerHTML = "hello";
|
||||
`;
|
||||
const outDir = dir.join("dist");
|
||||
|
||||
await Deno.writeTextFile(entry, input);
|
||||
await Deno.writeTextFile(script, scriptInput);
|
||||
|
||||
const result = await Deno.bundle({
|
||||
entrypoints: [entry],
|
||||
outputDir: outDir,
|
||||
write: false,
|
||||
});
|
||||
|
||||
const js = result.outputFiles!.find((f) =>
|
||||
!!f.contents && f.path.endsWith(".js")
|
||||
);
|
||||
if (!js) {
|
||||
throw new Error("No JS file found");
|
||||
}
|
||||
|
||||
const html = result.outputFiles!.find((f) =>
|
||||
!!f.contents && f.path.endsWith(".html")
|
||||
)!;
|
||||
if (!html) {
|
||||
throw new Error("No HTML file found");
|
||||
}
|
||||
|
||||
assert(result.success);
|
||||
assertEquals(result.errors.length, 0);
|
||||
assertEquals(result.outputFiles!.length, 2);
|
||||
|
||||
const jsFileName = basename(js.path);
|
||||
|
||||
assertStringIncludes(html.text(), `src="./${jsFileName}`);
|
||||
|
||||
assertStringIncludes(js.text(), "innerHTML");
|
||||
assertStringIncludes(js.text(), "hello");
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue