mirror of
https://github.com/denoland/deno.git
synced 2025-09-27 12:49: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]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg_aliases"
|
name = "cfg_aliases"
|
||||||
|
@ -1437,6 +1437,29 @@ version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25670139e591f1c2869eb8d0d977028f8d05e859132b4c874ecd02a00d3c9174"
|
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]]
|
[[package]]
|
||||||
name = "ctr"
|
name = "ctr"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
|
@ -1585,6 +1608,7 @@ dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"boxed_error",
|
"boxed_error",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"bytes-str",
|
||||||
"capacity_builder",
|
"capacity_builder",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
@ -1650,6 +1674,7 @@ dependencies = [
|
||||||
"libsui",
|
"libsui",
|
||||||
"libz-sys",
|
"libz-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"lol_html",
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
"malva",
|
"malva",
|
||||||
"markup_fmt",
|
"markup_fmt",
|
||||||
|
@ -1699,6 +1724,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-opentelemetry",
|
"tracing-opentelemetry",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"twox-hash 2.1.0",
|
||||||
"typed-arena",
|
"typed-arena",
|
||||||
"unicode-width 0.1.13",
|
"unicode-width 0.1.13",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
@ -2338,7 +2364,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"deno_ast",
|
"deno_ast",
|
||||||
"deno_semver",
|
"deno_semver",
|
||||||
"derive_more",
|
"derive_more 0.99.17",
|
||||||
"if_chain",
|
"if_chain",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
@ -3391,6 +3417,26 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"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]]
|
[[package]]
|
||||||
name = "deunicode"
|
name = "deunicode"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
|
@ -3656,6 +3702,21 @@ dependencies = [
|
||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "dunce"
|
name = "dunce"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
@ -3867,14 +3928,13 @@ checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esbuild_client"
|
name = "esbuild_client"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "456b3d661f00dac26314d27da56eb41e0e43f270251fe8c53686bb1e32205863"
|
checksum = "97ec79be0f4b20864729b38953ad77c7f347e436a7532c5f332dce9736aa4b5c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"deno_unsync",
|
"deno_unsync",
|
||||||
"derive_builder",
|
|
||||||
"indexmap 2.9.0",
|
"indexmap 2.9.0",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
@ -4332,6 +4392,15 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fxhash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generator"
|
name = "generator"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
@ -5832,6 +5901,24 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "loom"
|
name = "loom"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
@ -6841,6 +6928,16 @@ dependencies = [
|
||||||
"phf_shared",
|
"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]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
@ -7002,6 +7099,12 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "precomputed-hash"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prefix-trie"
|
name = "prefix-trie"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -8014,6 +8117,25 @@ dependencies = [
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -8151,6 +8273,15 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
|
|
@ -103,6 +103,7 @@ base64.workspace = true
|
||||||
bincode.workspace = true
|
bincode.workspace = true
|
||||||
boxed_error.workspace = true
|
boxed_error.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
|
bytes-str = "0.2.5"
|
||||||
capacity_builder.workspace = true
|
capacity_builder.workspace = true
|
||||||
chrono = { workspace = true, features = ["now"] }
|
chrono = { workspace = true, features = ["now"] }
|
||||||
clap = { workspace = true, features = ["env", "string", "wrap_help", "error-context"] }
|
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-jupyter.workspace = true
|
||||||
dprint-plugin-markdown.workspace = true
|
dprint-plugin-markdown.workspace = true
|
||||||
dprint-plugin-typescript.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
|
fancy-regex.workspace = true
|
||||||
faster-hex.workspace = true
|
faster-hex.workspace = true
|
||||||
# If you disable the default __vendored_zlib_ng feature above, you _must_ be able to link against `-lz`.
|
# 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
|
libc.workspace = true
|
||||||
libz-sys.workspace = true
|
libz-sys.workspace = true
|
||||||
log = { workspace = true, features = ["serde"] }
|
log = { workspace = true, features = ["serde"] }
|
||||||
|
lol_html = "2.6.0"
|
||||||
lsp-types.workspace = true
|
lsp-types.workspace = true
|
||||||
malva.workspace = true
|
malva.workspace = true
|
||||||
markup_fmt.workspace = true
|
markup_fmt.workspace = true
|
||||||
|
@ -181,6 +183,7 @@ tower-lsp.workspace = true
|
||||||
tracing = { workspace = true, features = ["log"], optional = true }
|
tracing = { workspace = true, features = ["log"], optional = true }
|
||||||
tracing-opentelemetry = { workspace = true, optional = true }
|
tracing-opentelemetry = { workspace = true, optional = true }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
|
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
|
||||||
|
twox-hash.workspace = true
|
||||||
typed-arena.workspace = true
|
typed-arena.workspace = true
|
||||||
unicode-width.workspace = true
|
unicode-width.workspace = true
|
||||||
uuid = { workspace = true, features = ["serde"] }
|
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 esbuild;
|
||||||
mod externals;
|
mod externals;
|
||||||
|
mod html;
|
||||||
mod provider;
|
mod provider;
|
||||||
mod transform;
|
mod transform;
|
||||||
|
|
||||||
|
@ -45,7 +46,6 @@ use deno_resolver::loader::RequestedModuleType;
|
||||||
use deno_resolver::npm::managed::ResolvePkgFolderFromDenoModuleError;
|
use deno_resolver::npm::managed::ResolvePkgFolderFromDenoModuleError;
|
||||||
use deno_runtime::deno_permissions::PermissionsContainer;
|
use deno_runtime::deno_permissions::PermissionsContainer;
|
||||||
use deno_semver::npm::NpmPackageReqReference;
|
use deno_semver::npm::NpmPackageReqReference;
|
||||||
use esbuild_client::EsbuildFlags;
|
|
||||||
use esbuild_client::EsbuildFlagsBuilder;
|
use esbuild_client::EsbuildFlagsBuilder;
|
||||||
use esbuild_client::EsbuildService;
|
use esbuild_client::EsbuildService;
|
||||||
use esbuild_client::protocol;
|
use esbuild_client::protocol;
|
||||||
|
@ -80,6 +80,105 @@ use crate::util::file_watcher::WatcherRestartMode;
|
||||||
static DISABLE_HACK: LazyLock<bool> =
|
static DISABLE_HACK: LazyLock<bool> =
|
||||||
LazyLock::new(|| std::env::var("NO_DENO_BUNDLE_HACK").is_err());
|
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(
|
pub async fn bundle_init(
|
||||||
mut flags: Arc<Flags>,
|
mut flags: Arc<Flags>,
|
||||||
bundle_flags: &BundleFlags,
|
bundle_flags: &BundleFlags,
|
||||||
|
@ -106,7 +205,7 @@ pub async fn bundle_init(
|
||||||
|
|
||||||
let (on_end_tx, on_end_rx) = tokio::sync::mpsc::channel(10);
|
let (on_end_tx, on_end_rx) = tokio::sync::mpsc::channel(10);
|
||||||
#[allow(clippy::arc_with_non_send_sync)]
|
#[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(),
|
file_fetcher: factory.file_fetcher()?.clone(),
|
||||||
resolver: resolver.clone(),
|
resolver: resolver.clone(),
|
||||||
module_load_preparer,
|
module_load_preparer,
|
||||||
|
@ -123,17 +222,19 @@ pub async fn bundle_init(
|
||||||
cjs_tracker: factory.cjs_tracker()?.clone(),
|
cjs_tracker: factory.cjs_tracker()?.clone(),
|
||||||
emitter: factory.emitter()?.clone(),
|
emitter: factory.emitter()?.clone(),
|
||||||
deferred_resolve_errors: Default::default(),
|
deferred_resolve_errors: Default::default(),
|
||||||
|
virtual_modules: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let resolved_entrypoints =
|
let input = prepare_inputs(
|
||||||
resolve_entrypoints(&resolver, &init_cwd, &bundle_flags.entrypoints)?;
|
&resolver,
|
||||||
let _ = plugin_handler
|
sys,
|
||||||
.prepare_module_load(&resolved_entrypoints)
|
npm_resolver,
|
||||||
.await;
|
node_resolver,
|
||||||
|
&init_cwd,
|
||||||
let roots =
|
bundle_flags,
|
||||||
resolve_roots(resolved_entrypoints, sys, npm_resolver, node_resolver);
|
Arc::get_mut(&mut plugin_handler).unwrap(),
|
||||||
let _ = plugin_handler.prepare_module_load(&roots).await;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let esbuild = EsbuildService::new(
|
let esbuild = EsbuildService::new(
|
||||||
esbuild_path,
|
esbuild_path,
|
||||||
|
@ -149,9 +250,10 @@ pub async fn bundle_init(
|
||||||
let res = esbuild.wait_for_exit().await;
|
let res = esbuild.wait_for_exit().await;
|
||||||
log::warn!("esbuild exited: {:?}", res);
|
log::warn!("esbuild exited: {:?}", res);
|
||||||
});
|
});
|
||||||
|
let esbuild_flags = configure_esbuild_flags(
|
||||||
let esbuild_flags = configure_esbuild_flags(bundle_flags);
|
bundle_flags,
|
||||||
let entries = roots.into_iter().map(|e| ("".into(), e.into())).collect();
|
matches!(input, BundlerInput::EntrypointsWithHtml { .. }),
|
||||||
|
);
|
||||||
let bundler = EsbuildBundler::new(
|
let bundler = EsbuildBundler::new(
|
||||||
client,
|
client,
|
||||||
plugin_handler.clone(),
|
plugin_handler.clone(),
|
||||||
|
@ -162,7 +264,7 @@ pub async fn bundle_init(
|
||||||
on_end_rx,
|
on_end_rx,
|
||||||
init_cwd.clone(),
|
init_cwd.clone(),
|
||||||
esbuild_flags,
|
esbuild_flags,
|
||||||
entries,
|
input.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(bundler)
|
Ok(bundler)
|
||||||
|
@ -199,6 +301,7 @@ pub async fn bundle(
|
||||||
bundler,
|
bundler,
|
||||||
bundle_flags.minify,
|
bundle_flags.minify,
|
||||||
bundle_flags.platform,
|
bundle_flags.platform,
|
||||||
|
bundle_flags.output_dir.as_ref().map(Path::new),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -216,6 +319,8 @@ pub async fn bundle(
|
||||||
&init_cwd,
|
&init_cwd,
|
||||||
should_replace_require_shim(bundle_flags.platform),
|
should_replace_require_shim(bundle_flags.platform),
|
||||||
bundle_flags.minify,
|
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() {
|
if bundle_flags.output_dir.is_some() || bundle_flags.output_path.is_some() {
|
||||||
|
@ -245,16 +350,41 @@ async fn bundle_watch(
|
||||||
bundler: EsbuildBundler,
|
bundler: EsbuildBundler,
|
||||||
minified: bool,
|
minified: bool,
|
||||||
platform: BundlePlatform,
|
platform: BundlePlatform,
|
||||||
|
output_dir: Option<&Path>,
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<(), AnyError> {
|
||||||
let initial_roots = bundler
|
let (initial_roots, always_watch) = match &bundler.input {
|
||||||
.roots
|
BundlerInput::Entrypoints(entries) => (
|
||||||
.iter()
|
entries
|
||||||
.filter_map(|(_, root)| {
|
.iter()
|
||||||
let url = Url::parse(root).ok()?;
|
.filter_map(|(_, root)| {
|
||||||
deno_path_util::url_to_file_path(&url).ok()
|
let url = Url::parse(root).ok()?;
|
||||||
})
|
deno_path_util::url_to_file_path(&url).ok()
|
||||||
.collect::<Vec<_>>();
|
})
|
||||||
|
.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 current_roots = Rc::new(RefCell::new(initial_roots.clone()));
|
||||||
|
let input = bundler.input.clone();
|
||||||
let bundler = Rc::new(tokio::sync::Mutex::new(bundler));
|
let bundler = Rc::new(tokio::sync::Mutex::new(bundler));
|
||||||
let mut print_config =
|
let mut print_config =
|
||||||
crate::util::file_watcher::PrintConfig::new_with_banner(
|
crate::util::file_watcher::PrintConfig::new_with_banner(
|
||||||
|
@ -269,6 +399,8 @@ async fn bundle_watch(
|
||||||
watcher_communicator.show_path_changed(changed_paths.clone());
|
watcher_communicator.show_path_changed(changed_paths.clone());
|
||||||
let bundler = Rc::clone(&bundler);
|
let bundler = Rc::clone(&bundler);
|
||||||
let current_roots = current_roots.clone();
|
let current_roots = current_roots.clone();
|
||||||
|
let input = input.clone();
|
||||||
|
let always_watch = always_watch.clone();
|
||||||
Ok(async move {
|
Ok(async move {
|
||||||
let mut bundler = bundler.lock().await;
|
let mut bundler = bundler.lock().await;
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
@ -291,10 +423,13 @@ async fn bundle_watch(
|
||||||
&bundler.cwd,
|
&bundler.cwd,
|
||||||
should_replace_require_shim(platform),
|
should_replace_require_shim(platform),
|
||||||
minified,
|
minified,
|
||||||
|
input,
|
||||||
|
output_dir,
|
||||||
)?;
|
)?;
|
||||||
print_finished_message(&metafile, &output_infos, start.elapsed())?;
|
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();
|
*current_roots.borrow_mut() = new_watched.clone();
|
||||||
let _ = watcher_communicator.watch_paths(new_watched);
|
let _ = watcher_communicator.watch_paths(new_watched);
|
||||||
} else {
|
} else {
|
||||||
|
@ -339,6 +474,17 @@ pub enum BundlingMode {
|
||||||
Watch,
|
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 {
|
pub struct EsbuildBundler {
|
||||||
client: esbuild_client::ProtocolClient,
|
client: esbuild_client::ProtocolClient,
|
||||||
plugin_handler: Arc<DenoPluginHandler>,
|
plugin_handler: Arc<DenoPluginHandler>,
|
||||||
|
@ -346,7 +492,7 @@ pub struct EsbuildBundler {
|
||||||
mode: BundlingMode,
|
mode: BundlingMode,
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
flags: EsbuildFlags,
|
flags: EsbuildFlags,
|
||||||
roots: Vec<(String, String)>,
|
input: BundlerInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EsbuildBundler {
|
impl EsbuildBundler {
|
||||||
|
@ -357,7 +503,7 @@ impl EsbuildBundler {
|
||||||
on_end_rx: tokio::sync::mpsc::Receiver<esbuild_client::OnEndArgs>,
|
on_end_rx: tokio::sync::mpsc::Receiver<esbuild_client::OnEndArgs>,
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
flags: EsbuildFlags,
|
flags: EsbuildFlags,
|
||||||
roots: Vec<(String, String)>,
|
input: BundlerInput,
|
||||||
) -> EsbuildBundler {
|
) -> EsbuildBundler {
|
||||||
EsbuildBundler {
|
EsbuildBundler {
|
||||||
client,
|
client,
|
||||||
|
@ -366,7 +512,7 @@ impl EsbuildBundler {
|
||||||
mode,
|
mode,
|
||||||
cwd,
|
cwd,
|
||||||
flags,
|
flags,
|
||||||
roots,
|
input,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,10 +522,14 @@ impl EsbuildBundler {
|
||||||
// doesn't actually do anything, it's just registering the args/flags
|
// doesn't actually do anything, it's just registering the args/flags
|
||||||
// we're going to use for all of the rebuilds.
|
// we're going to use for all of the rebuilds.
|
||||||
fn make_build_request(&self) -> protocol::BuildRequest {
|
fn make_build_request(&self) -> protocol::BuildRequest {
|
||||||
|
let entries = match &self.input {
|
||||||
|
BundlerInput::Entrypoints(entries) => entries.clone(),
|
||||||
|
BundlerInput::EntrypointsWithHtml { entries, .. } => entries.clone(),
|
||||||
|
};
|
||||||
protocol::BuildRequest {
|
protocol::BuildRequest {
|
||||||
entries: self.roots.clone(),
|
entries,
|
||||||
key: 0,
|
key: 0,
|
||||||
flags: self.flags.to_flags(),
|
flags: self.flags.clone(),
|
||||||
write: false,
|
write: false,
|
||||||
stdin_contents: None.into(),
|
stdin_contents: None.into(),
|
||||||
stdin_resolve_dir: None.into(),
|
stdin_resolve_dir: None.into(),
|
||||||
|
@ -406,7 +556,7 @@ impl EsbuildBundler {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build(&self) -> Result<BuildResponse, AnyError> {
|
async fn build(&self) -> Result<BuildResponse, AnyError> {
|
||||||
let response = self
|
let response: BuildResponse = self
|
||||||
.client
|
.client
|
||||||
.send_build_request(self.make_build_request())
|
.send_build_request(self.make_build_request())
|
||||||
.await
|
.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 {
|
pub struct DeferredResolveError {
|
||||||
path: String,
|
path: String,
|
||||||
error: ResolveWithGraphError,
|
error: ResolveWithGraphError,
|
||||||
|
@ -604,7 +789,7 @@ pub struct DenoPluginHandler {
|
||||||
externals_matcher: Option<ExternalsMatcher>,
|
externals_matcher: Option<ExternalsMatcher>,
|
||||||
on_end_tx: tokio::sync::mpsc::Sender<esbuild_client::OnEndArgs>,
|
on_end_tx: tokio::sync::mpsc::Sender<esbuild_client::OnEndArgs>,
|
||||||
deferred_resolve_errors: Arc<Mutex<Vec<DeferredResolveError>>>,
|
deferred_resolve_errors: Arc<Mutex<Vec<DeferredResolveError>>>,
|
||||||
|
virtual_modules: Option<Arc<VirtualModules>>,
|
||||||
parsed_source_cache: Arc<ParsedSourceCache>,
|
parsed_source_cache: Arc<ParsedSourceCache>,
|
||||||
cjs_tracker: Arc<CliCjsTracker>,
|
cjs_tracker: Arc<CliCjsTracker>,
|
||||||
emitter: Arc<CliEmitter>,
|
emitter: Arc<CliEmitter>,
|
||||||
|
@ -673,6 +858,18 @@ impl esbuild_client::PluginHandler for DenoPluginHandler {
|
||||||
args: esbuild_client::OnResolveArgs,
|
args: esbuild_client::OnResolveArgs,
|
||||||
) -> Result<Option<esbuild_client::OnResolveResult>, AnyError> {
|
) -> Result<Option<esbuild_client::OnResolveResult>, AnyError> {
|
||||||
log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_resolve"));
|
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
|
if let Some(matcher) = &self.externals_matcher
|
||||||
&& matcher.is_pre_resolve_match(&args.path)
|
&& matcher.is_pre_resolve_match(&args.path)
|
||||||
{
|
{
|
||||||
|
@ -743,6 +940,15 @@ impl esbuild_client::PluginHandler for DenoPluginHandler {
|
||||||
args: esbuild_client::OnLoadArgs,
|
args: esbuild_client::OnLoadArgs,
|
||||||
) -> Result<Option<esbuild_client::OnLoadResult>, AnyError> {
|
) -> Result<Option<esbuild_client::OnLoadResult>, AnyError> {
|
||||||
log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_load"));
|
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
|
let result = self
|
||||||
.bundle_load(&args.path, &requested_type_from_map(&args.with))
|
.bundle_load(&args.path, &requested_type_from_map(&args.with))
|
||||||
.await;
|
.await;
|
||||||
|
@ -1497,14 +1703,17 @@ async fn ensure_esbuild_downloaded(
|
||||||
Ok(esbuild_path)
|
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();
|
let mut builder = EsbuildFlagsBuilder::default();
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.bundle(bundle_flags.inline_imports)
|
.bundle(bundle_flags.inline_imports)
|
||||||
.minify(bundle_flags.minify)
|
.minify(bundle_flags.minify)
|
||||||
.splitting(bundle_flags.code_splitting)
|
.splitting(bundle_flags.code_splitting)
|
||||||
.external(bundle_flags.external.clone())
|
.externals(bundle_flags.external.clone())
|
||||||
.tree_shaking(true)
|
.tree_shaking(true)
|
||||||
.format(match bundle_flags.format {
|
.format(match bundle_flags.format {
|
||||||
BundleFormat::Esm => esbuild_client::Format::Esm,
|
BundleFormat::Esm => esbuild_client::Format::Esm,
|
||||||
|
@ -1531,6 +1740,14 @@ fn configure_esbuild_flags(bundle_flags: &BundleFlags) -> EsbuildFlags {
|
||||||
}
|
}
|
||||||
builder.metafile(true);
|
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 {
|
match bundle_flags.platform {
|
||||||
deno_bundle_runtime::BundlePlatform::Browser => {
|
deno_bundle_runtime::BundlePlatform::Browser => {
|
||||||
builder.platform(esbuild_client::Platform::Browser);
|
builder.platform(esbuild_client::Platform::Browser);
|
||||||
|
@ -1538,7 +1755,7 @@ fn configure_esbuild_flags(bundle_flags: &BundleFlags) -> EsbuildFlags {
|
||||||
deno_bundle_runtime::BundlePlatform::Deno => {}
|
deno_bundle_runtime::BundlePlatform::Deno => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build().unwrap()
|
builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the path from a message like "Could not resolve "path/to/file.ts""
|
// 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 {
|
pub struct OutputFileInfo {
|
||||||
relative_path: PathBuf,
|
relative_path: PathBuf,
|
||||||
size: usize,
|
size: usize,
|
||||||
|
@ -1610,19 +1816,30 @@ pub struct ProcessedContents {
|
||||||
is_js: bool,
|
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(
|
pub fn maybe_process_contents(
|
||||||
file: &esbuild_client::protocol::BuildOutputFile,
|
file: &OutputFile<'_>,
|
||||||
should_replace_require_shim: bool,
|
should_replace_require_shim: bool,
|
||||||
minified: bool,
|
minified: bool,
|
||||||
) -> Result<ProcessedContents, AnyError> {
|
) -> Result<ProcessedContents, AnyError> {
|
||||||
let path = Path::new(&file.path);
|
let path = &file.path;
|
||||||
let is_js = is_js(path) || file.path.ends_with("<stdout>");
|
let is_js = is_js(path) || path.ends_with("<stdout>");
|
||||||
if is_js {
|
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 {
|
let string = if should_replace_require_shim {
|
||||||
replace_require_shim(&string, minified)
|
replace_require_shim(string, minified)
|
||||||
} else {
|
} else {
|
||||||
string
|
string.to_string()
|
||||||
};
|
};
|
||||||
Ok(ProcessedContents {
|
Ok(ProcessedContents {
|
||||||
contents: Some(string.into_bytes()),
|
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(
|
pub fn process_result(
|
||||||
response: &BuildResponse,
|
response: &BuildResponse,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
should_replace_require_shim: bool,
|
should_replace_require_shim: bool,
|
||||||
minified: bool,
|
minified: bool,
|
||||||
|
input: BundlerInput,
|
||||||
|
outdir: Option<&Path>,
|
||||||
) -> Result<Vec<OutputFileInfo>, AnyError> {
|
) -> 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 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();
|
let mut output_infos = Vec::new();
|
||||||
for file in output_files.iter() {
|
for file in output_files.iter() {
|
||||||
let processed_contents =
|
let processed_contents =
|
||||||
|
@ -1655,13 +1955,13 @@ pub fn process_result(
|
||||||
let relative_path =
|
let relative_path =
|
||||||
pathdiff::diff_paths(path, cwd).unwrap_or_else(|| path.to_path_buf());
|
pathdiff::diff_paths(path, cwd).unwrap_or_else(|| path.to_path_buf());
|
||||||
let is_js = processed_contents.is_js;
|
let is_js = processed_contents.is_js;
|
||||||
let bytes = processed_contents
|
let bytes: Cow<'_, [u8]> = processed_contents
|
||||||
.contents
|
.contents
|
||||||
.map(Cow::Owned)
|
.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>") {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright 2018-2025 the Deno authors. MIT license.
|
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use deno_bundle_runtime as rt_bundle;
|
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(
|
fn process_output_files(
|
||||||
bundle_flags: &crate::args::BundleFlags,
|
bundle_flags: &crate::args::BundleFlags,
|
||||||
response: &mut esbuild_client::protocol::BuildResponse,
|
response: &mut esbuild_client::protocol::BuildResponse,
|
||||||
|
cwd: &Path,
|
||||||
|
input: super::BundlerInput,
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<(), AnyError> {
|
||||||
if let Some(files) = &mut response.output_files {
|
if let Some(files) = std::mem::take(&mut response.output_files) {
|
||||||
for file in 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(
|
let processed_contents = crate::tools::bundle::maybe_process_contents(
|
||||||
file,
|
&output_file,
|
||||||
crate::tools::bundle::should_replace_require_shim(
|
crate::tools::bundle::should_replace_require_shim(
|
||||||
bundle_flags.platform,
|
bundle_flags.platform,
|
||||||
),
|
),
|
||||||
bundle_flags.minify,
|
bundle_flags.minify,
|
||||||
)?;
|
)?;
|
||||||
if let Some(contents) = processed_contents.contents {
|
let contents = processed_contents
|
||||||
file.contents = 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -151,10 +175,17 @@ impl BundleProvider for CliBundleProvider {
|
||||||
&bundler.cwd,
|
&bundler.cwd,
|
||||||
true,
|
true,
|
||||||
bundle_flags.minify,
|
bundle_flags.minify,
|
||||||
|
bundler.input,
|
||||||
|
bundle_flags.output_dir.as_ref().map(Path::new),
|
||||||
)?;
|
)?;
|
||||||
result.output_files = None;
|
result.output_files = None;
|
||||||
} else {
|
} else {
|
||||||
process_output_files(&bundle_flags, &mut result)?;
|
process_output_files(
|
||||||
|
&bundle_flags,
|
||||||
|
&mut result,
|
||||||
|
&bundler.cwd,
|
||||||
|
bundler.input,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
log::trace!("convert_build_response");
|
log::trace!("convert_build_response");
|
||||||
let result = convert_build_response(result);
|
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,
|
unindent,
|
||||||
} from "./test_util.ts";
|
} from "./test_util.ts";
|
||||||
|
|
||||||
import { join, toFileUrl } from "@std/path";
|
import { basename, join, toFileUrl } from "@std/path";
|
||||||
|
|
||||||
class TempDir implements AsyncDisposable, Disposable {
|
class TempDir implements AsyncDisposable, Disposable {
|
||||||
private path: string;
|
private path: string;
|
||||||
|
@ -313,3 +313,55 @@ Deno.test("bundle: replaces require shim when platform is deno", async () => {
|
||||||
const output = await evalEsmString(js.text());
|
const output = await evalEsmString(js.text());
|
||||||
assertEquals(output.default, ["good", 1]);
|
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