autodetect js module type - only bundle esm (#5515)
* wip: autodetect esm * finish fixes * thread minify through * add sweetalert2 * Fix default bundle types * ts-ignore * small nits * small more nits * shrink flamegraph
This commit is contained in:
1 parent
2f40780ce0
commit
218bb9d56d
24 files changed
+1598
-616
No files matched your search
@@ -276,6 +276,8 @@ fn legacy_asset_to_modern_asset(
|
||||
.with_minify(js.minified())
|
||||
.with_preload(js.preloaded())
|
||||
.with_static_head(js.static_head())
|
||||
// Legacy 0.7 manganis has no module concept; default to classic script.
|
||||
.with_module(false)
|
||||
.into_asset_options(),
|
||||
_ => AssetOptions::builder()
|
||||
.with_hash_suffix(add_hash)
|
||||
|
||||
@@ -1989,7 +1989,7 @@ impl AppBuilder {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let total_ms = time_taken as usize;
|
||||
let timeline_width = 96usize;
|
||||
let timeline_width = 48usize;
|
||||
let max_label_width = self
|
||||
.profile_spans
|
||||
.iter()
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
//! logo.png
|
||||
//! ```
|
||||
|
||||
use crate::{opt::AppManifest, BuildMode, BuildRequest};
|
||||
use crate::{
|
||||
opt::{js_is_module, AppManifest},
|
||||
BuildMode, BuildRequest,
|
||||
};
|
||||
use crate::{BuildContext, BundleFormat, Result, TraceSrc, WasmBindgen, WasmOptConfig};
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_config::format_base_path_meta_element;
|
||||
@@ -487,9 +490,15 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
);
|
||||
}
|
||||
if js_options.static_head() {
|
||||
let source = std::path::Path::new(asset.absolute_source_path());
|
||||
let module_attr = if js_is_module(js_options, source) {
|
||||
r#" type="module""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
_ = write!(
|
||||
head_resources,
|
||||
r#"<script src="/{{base_path}}/assets/{asset_path}"></script>"#
|
||||
r#"<script{module_attr} src="/{{base_path}}/assets/{asset_path}"></script>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ impl Bundle {
|
||||
|
||||
// Fill platform-specific defaults for package types when omitted.
|
||||
if self.package_types.is_none() {
|
||||
self.package_types = Self::default_package_types(client.bundle);
|
||||
self.package_types = Some(Self::default_package_types(client.bundle));
|
||||
}
|
||||
|
||||
Self::validate_package_types_for_bundle(client.bundle, self.package_types.as_deref())?;
|
||||
@@ -125,11 +125,9 @@ impl Bundle {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Bundled {} bundles:", bundles.len());
|
||||
for bundle_path in bundles.iter() {
|
||||
tracing::info!(
|
||||
"Bundled app at: {}",
|
||||
bundle_path.absolutize().unwrap().display()
|
||||
);
|
||||
println!("{}", bundle_path.absolutize().unwrap().display());
|
||||
}
|
||||
|
||||
let client = client_artifacts.into_structured_output();
|
||||
@@ -191,11 +189,14 @@ impl Bundle {
|
||||
Ok(bundles)
|
||||
}
|
||||
|
||||
fn default_package_types(bundle: BundleFormat) -> Option<Vec<PackageType>> {
|
||||
fn default_package_types(bundle: BundleFormat) -> Vec<PackageType> {
|
||||
match bundle {
|
||||
BundleFormat::Ios => Some(vec![crate::PackageType::Ipa]),
|
||||
BundleFormat::Android => Some(vec![crate::PackageType::Aab]),
|
||||
_ => None,
|
||||
BundleFormat::Web | BundleFormat::Server => Vec::new(),
|
||||
BundleFormat::MacOS => vec![PackageType::MacOsBundle, PackageType::Dmg],
|
||||
BundleFormat::Windows => vec![PackageType::Nsis],
|
||||
BundleFormat::Linux => vec![PackageType::AppImage],
|
||||
BundleFormat::Ios => vec![PackageType::Ipa],
|
||||
BundleFormat::Android => vec![PackageType::Aab],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,11 +268,55 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ios_bundle_defaults_to_ipa() {
|
||||
fn bundle_formats_have_default_package_types() {
|
||||
assert_eq!(
|
||||
Bundle::default_package_types(BundleFormat::Ios),
|
||||
Some(vec![PackageType::Ipa])
|
||||
vec![PackageType::Ipa]
|
||||
);
|
||||
assert_eq!(
|
||||
Bundle::default_package_types(BundleFormat::Android),
|
||||
vec![PackageType::Aab]
|
||||
);
|
||||
assert_eq!(
|
||||
Bundle::default_package_types(BundleFormat::MacOS),
|
||||
vec![PackageType::MacOsBundle, PackageType::Dmg]
|
||||
);
|
||||
assert_eq!(
|
||||
Bundle::default_package_types(BundleFormat::Windows),
|
||||
vec![PackageType::Nsis]
|
||||
);
|
||||
assert_eq!(
|
||||
Bundle::default_package_types(BundleFormat::Linux),
|
||||
vec![PackageType::AppImage]
|
||||
);
|
||||
assert_eq!(Bundle::default_package_types(BundleFormat::Web), vec![]);
|
||||
assert_eq!(Bundle::default_package_types(BundleFormat::Server), vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_package_types_are_valid_for_their_bundle_format() {
|
||||
for bundle_format in [
|
||||
BundleFormat::Web,
|
||||
BundleFormat::MacOS,
|
||||
BundleFormat::Windows,
|
||||
BundleFormat::Linux,
|
||||
BundleFormat::Server,
|
||||
BundleFormat::Ios,
|
||||
BundleFormat::Android,
|
||||
] {
|
||||
let package_types = Bundle::default_package_types(bundle_format);
|
||||
Bundle::validate_package_types_for_bundle(bundle_format, Some(&package_types)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_package_types_are_filled_with_defaults() {
|
||||
let mut package_types = None;
|
||||
if package_types.is_none() {
|
||||
package_types = Some(Bundle::default_package_types(BundleFormat::Linux));
|
||||
}
|
||||
|
||||
assert_eq!(package_types, Some(vec![PackageType::AppImage]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -16,17 +16,6 @@ pub(crate) fn process_file_to(
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
process_file_to_with_options(options, source, output_path, false, esbuild_path)
|
||||
}
|
||||
|
||||
/// Process a specific file asset with additional options
|
||||
pub(crate) fn process_file_to_with_options(
|
||||
options: &AssetOptions,
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
in_folder: bool,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
// If the file already exists and this is a hashed asset, then we must have a file
|
||||
// with the same hash already. The hash has the file contents and options, so if we
|
||||
@@ -63,7 +52,7 @@ pub(crate) fn process_file_to_with_options(
|
||||
process_scss(options, source, &temp_path)?;
|
||||
}
|
||||
ResolvedAssetType::Js(options) => {
|
||||
process_js(options, source, &temp_path, !in_folder, esbuild_path)?;
|
||||
process_js(options, source, &temp_path, esbuild_path)?;
|
||||
}
|
||||
ResolvedAssetType::Image(options) => {
|
||||
process_image(options, source, &temp_path)?;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::Path;
|
||||
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
|
||||
use crate::opt::file::process_file_to_with_options;
|
||||
use crate::opt::file::process_file_to;
|
||||
|
||||
/// Process a folder, optimizing and copying all assets into the output folder
|
||||
pub fn process_folder(
|
||||
@@ -40,11 +40,10 @@ fn process_file_minimal(
|
||||
output_path: &Path,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
process_file_to_with_options(
|
||||
process_file_to(
|
||||
&manganis_core::AssetOptions::builder().into_asset_options(),
|
||||
input_path,
|
||||
output_path,
|
||||
true,
|
||||
esbuild_path,
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
@@ -50,7 +50,7 @@ fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash>
|
||||
|
||||
// Hash the version of the CLI
|
||||
hash.write(crate::dx_build_info::PKG_VERSION.as_bytes());
|
||||
hash_file_with_options(options, source, &mut hash, false)?;
|
||||
hash_file_with_options(options, source, &mut hash)?;
|
||||
|
||||
let hash = hash.finish();
|
||||
Ok(AssetHash::new(hash))
|
||||
@@ -61,7 +61,6 @@ pub(crate) fn hash_file_with_options(
|
||||
options: &AssetOptions,
|
||||
source: &Path,
|
||||
hasher: &mut impl Hasher,
|
||||
in_folder: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let resolved_options = resolve_asset_options(source, options.variant());
|
||||
|
||||
@@ -73,7 +72,7 @@ pub(crate) fn hash_file_with_options(
|
||||
}
|
||||
|
||||
ResolvedAssetType::Js(options) => {
|
||||
hash_js(options, source, hasher, !in_folder)?;
|
||||
hash_js(options, source, hasher)?;
|
||||
}
|
||||
|
||||
// Otherwise, we can just hash the file contents
|
||||
@@ -100,7 +99,6 @@ pub(crate) fn hash_file_with_options(
|
||||
.into_asset_options(),
|
||||
&path,
|
||||
hasher,
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
+611
-17
@@ -4,20 +4,15 @@ use anyhow::Context;
|
||||
use manganis_core::JsAssetOptions;
|
||||
|
||||
use crate::opt::hash::hash_file_contents;
|
||||
|
||||
pub(crate) fn process_js(
|
||||
js_options: &JsAssetOptions,
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
bundle: bool,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
let minify = js_options.minified();
|
||||
let needs_esbuild = minify || bundle;
|
||||
|
||||
if needs_esbuild {
|
||||
if js_options.minified() || js_options.is_module() {
|
||||
if let Some(esbuild) = esbuild_path {
|
||||
match run_esbuild(esbuild, source, output_path, bundle, minify) {
|
||||
match run_esbuild(esbuild, source, output_path, js_options) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
@@ -30,7 +25,7 @@ pub(crate) fn process_js(
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: copy unprocessed
|
||||
// Fallback / no minification: copy unprocessed
|
||||
let mut source_file = std::fs::File::open(source)?;
|
||||
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
|
||||
std::io::copy(&mut source_file, &mut writer).with_context(|| {
|
||||
@@ -43,26 +38,42 @@ pub(crate) fn process_js(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run esbuild to bundle and/or minify a JavaScript file.
|
||||
/// Run esbuild to minify a JavaScript file in place.
|
||||
///
|
||||
/// When `is_module` is true, the file is treated as ES module input:
|
||||
/// `--bundle --format=esm` inlines local relative imports (notably the
|
||||
/// `snippets/` folder that wasm-bindgen emits for `#[wasm_bindgen(inline_js)]`
|
||||
/// / `module = "…"`) into a single ESM file. `http://` and `https://` imports
|
||||
/// are marked external so URL-based module loading (e.g. CDN-hosted ESM,
|
||||
/// firebase) is left for the browser to resolve at runtime. The consuming
|
||||
/// `<script>` tag is expected to be `type="module"`.
|
||||
///
|
||||
/// When `is_module` is false, only `--minify` is passed and esbuild preserves
|
||||
/// the input's format verbatim — a classic IIFE/UMD script stays a classic
|
||||
/// script with no wrapper added.
|
||||
fn run_esbuild(
|
||||
esbuild: &Path,
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
bundle: bool,
|
||||
minify: bool,
|
||||
js_options: &JsAssetOptions,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut cmd = std::process::Command::new(esbuild);
|
||||
cmd.arg(source);
|
||||
cmd.arg(format!("--outfile={}", output_path.display()));
|
||||
cmd.arg("--log-level=warning");
|
||||
|
||||
if bundle {
|
||||
cmd.arg("--bundle");
|
||||
cmd.arg("--format=esm");
|
||||
if js_options.minified() {
|
||||
cmd.arg("--minify");
|
||||
}
|
||||
|
||||
if minify {
|
||||
cmd.arg("--minify");
|
||||
if lexer::js_is_module(js_options, source) {
|
||||
cmd.arg("--bundle");
|
||||
cmd.arg("--format=esm");
|
||||
// Don't try to resolve URL-based imports at build time — let the
|
||||
// browser fetch them at runtime. Without these externals, esbuild
|
||||
// errors out on patterns like `import x from "https://cdn/lib.js"`.
|
||||
cmd.arg("--external:https://*");
|
||||
cmd.arg("--external:http://*");
|
||||
}
|
||||
|
||||
tracing::debug!("Running esbuild: {:?}", cmd);
|
||||
@@ -81,7 +92,590 @@ pub(crate) fn hash_js(
|
||||
_js_options: &JsAssetOptions,
|
||||
source: &Path,
|
||||
hasher: &mut impl std::hash::Hasher,
|
||||
_bundle: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
hash_file_contents(source, hasher)
|
||||
}
|
||||
|
||||
pub use lexer::*;
|
||||
mod lexer {
|
||||
//! Detect whether a JavaScript source file uses ES module syntax.
|
||||
//!
|
||||
//! This is a token-level scanner inspired by Guy Bedford's `es-module-lexer`
|
||||
//! (the same heuristic used by Vite, Rollup, and Node's `--experimental-detect-module`):
|
||||
//! skip strings, comments, template literals, and regex literals, then look for
|
||||
//! a top-level `import` declaration, a top-level `export` declaration, or an
|
||||
//! `import.meta` reference. Dynamic `import(...)` and `require(...)` do not
|
||||
//! count as module syntax — they are valid in classic scripts and CommonJS.
|
||||
//!
|
||||
//! This is intentionally a tokenizer, not a parser. We don't build an AST. The
|
||||
//! goal is a fast, dependency-free boolean decision for the asset pipeline.
|
||||
//!
|
||||
//! Edge cases on the regex/division ambiguity are biased toward false negatives
|
||||
//! (treating an ESM file as classic) rather than false positives (treating a
|
||||
//! classic file as ESM), so a misclassified file gets the "copy verbatim" path
|
||||
//! instead of the "wrap as module" path that broke the issue #5512 case.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use manganis_core::JsAssetOptions;
|
||||
|
||||
/// Resolve whether a JS asset should be treated as an ES module.
|
||||
///
|
||||
/// Resolution order:
|
||||
/// 1. Explicit `with_module(true)` always wins.
|
||||
/// 2. File extension fast-path: `.mjs` is always ESM, `.cjs` is always classic.
|
||||
/// 3. Otherwise, scan the source and look for top-level `import`/`export` or
|
||||
/// `import.meta`. If the file can't be read, treat it as classic.
|
||||
pub fn js_is_module(js_options: &JsAssetOptions, source: &Path) -> bool {
|
||||
if js_options.is_module() {
|
||||
return true;
|
||||
}
|
||||
match source.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("mjs") => return true,
|
||||
Some("cjs") => return false,
|
||||
_ => {}
|
||||
}
|
||||
match std::fs::read_to_string(source) {
|
||||
Ok(content) => has_module_syntax(&content),
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
"Failed to read JS source for module detection ({}): {err}. Treating as classic.",
|
||||
source.display()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect whether `source` uses ES module syntax.
|
||||
///
|
||||
/// Returns `true` if and only if the source contains at least one of:
|
||||
/// - a top-level `import` declaration (`import x from '…'`, `import '…'`, etc.)
|
||||
/// - a top-level `export` declaration (`export const`, `export default`, …)
|
||||
/// - an `import.meta` reference at any nesting level
|
||||
///
|
||||
/// Dynamic `import(…)`, `require(…)`, and `module.exports` are *not* module syntax.
|
||||
pub fn has_module_syntax(source: &str) -> bool {
|
||||
let bytes = source.as_bytes();
|
||||
let mut i = 0;
|
||||
let mut brace_depth: i32 = 0;
|
||||
// True when the next `/` should be parsed as the start of a regex literal
|
||||
// rather than the division operator. Initially true (start of file).
|
||||
let mut expect_expr = true;
|
||||
|
||||
while i < bytes.len() {
|
||||
let c = bytes[i];
|
||||
|
||||
match c {
|
||||
// Whitespace
|
||||
b' ' | b'\t' | b'\n' | b'\r' => {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Comments and the regex/division fork
|
||||
b'/' => match bytes.get(i + 1).copied() {
|
||||
Some(b'/') => {
|
||||
i = skip_line_comment(bytes, i);
|
||||
}
|
||||
Some(b'*') => {
|
||||
i = skip_block_comment(bytes, i);
|
||||
}
|
||||
_ => {
|
||||
if expect_expr {
|
||||
i = skip_regex(bytes, i);
|
||||
expect_expr = false;
|
||||
} else {
|
||||
i += 1;
|
||||
expect_expr = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
b'\'' | b'"' => {
|
||||
i = skip_string(bytes, i, c);
|
||||
expect_expr = false;
|
||||
}
|
||||
|
||||
b'`' => {
|
||||
i = skip_template(bytes, i);
|
||||
expect_expr = false;
|
||||
}
|
||||
|
||||
b'{' => {
|
||||
brace_depth += 1;
|
||||
i += 1;
|
||||
expect_expr = true;
|
||||
}
|
||||
|
||||
b'}' => {
|
||||
brace_depth = brace_depth.saturating_sub(1);
|
||||
i += 1;
|
||||
// After `}` we conservatively assume operator position. This
|
||||
// misclassifies regex literals at statement-start (rare) as
|
||||
// division, which is the safer direction per the false-negative
|
||||
// bias.
|
||||
expect_expr = false;
|
||||
}
|
||||
|
||||
// Open-grouping punctuators introduce an expression position.
|
||||
b'(' | b'[' => {
|
||||
i += 1;
|
||||
expect_expr = true;
|
||||
}
|
||||
|
||||
// Close-grouping punctuators end an expression.
|
||||
b')' | b']' => {
|
||||
i += 1;
|
||||
expect_expr = false;
|
||||
}
|
||||
|
||||
// Operators that take an operand on the right.
|
||||
b'=' | b',' | b';' | b'!' | b'~' | b'?' | b':' | b'<' | b'>' | b'&' | b'|'
|
||||
| b'^' | b'%' | b'*' | b'+' | b'-' => {
|
||||
i += 1;
|
||||
expect_expr = true;
|
||||
}
|
||||
|
||||
// Member access — next token is an identifier, not an expression.
|
||||
b'.' => {
|
||||
i += 1;
|
||||
expect_expr = false;
|
||||
}
|
||||
|
||||
c if is_identifier_start(c) => {
|
||||
let start = i;
|
||||
while i < bytes.len() && is_identifier_continue(bytes[i]) {
|
||||
i += 1;
|
||||
}
|
||||
let ident = &bytes[start..i];
|
||||
|
||||
match ident {
|
||||
b"import" => {
|
||||
let after = skip_ws_and_comments(bytes, i);
|
||||
match bytes.get(after).copied() {
|
||||
Some(b'(') => {
|
||||
// Dynamic import — not module syntax.
|
||||
expect_expr = false;
|
||||
}
|
||||
Some(b'.') => {
|
||||
// `import.meta` — module syntax at any depth.
|
||||
return true;
|
||||
}
|
||||
_ => {
|
||||
// Static `import` declaration. Only legal at
|
||||
// module top level; treat top-level occurrences
|
||||
// as the ESM signal and ignore anything inside
|
||||
// braces (where it'd be a parse error anyway).
|
||||
if brace_depth == 0 {
|
||||
return true;
|
||||
}
|
||||
expect_expr = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
b"export" => {
|
||||
if brace_depth == 0 {
|
||||
return true;
|
||||
}
|
||||
expect_expr = false;
|
||||
}
|
||||
// Keywords that introduce an expression.
|
||||
b"return" | b"throw" | b"typeof" | b"void" | b"delete" | b"new" | b"in"
|
||||
| b"instanceof" | b"await" | b"yield" | b"case" | b"else" | b"do" => {
|
||||
expect_expr = true;
|
||||
}
|
||||
_ => {
|
||||
expect_expr = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c if c.is_ascii_digit() => {
|
||||
i = skip_number(bytes, i);
|
||||
expect_expr = false;
|
||||
}
|
||||
|
||||
// Anything else: advance one byte. This handles non-ASCII identifiers
|
||||
// conservatively (treated as identifier-ish: expect_expr unchanged).
|
||||
_ => {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn skip_line_comment(bytes: &[u8], mut i: usize) -> usize {
|
||||
i += 2;
|
||||
while i < bytes.len() && bytes[i] != b'\n' && bytes[i] != b'\r' {
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
fn skip_block_comment(bytes: &[u8], mut i: usize) -> usize {
|
||||
i += 2;
|
||||
while i + 1 < bytes.len() {
|
||||
if bytes[i] == b'*' && bytes[i + 1] == b'/' {
|
||||
return i + 2;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
bytes.len()
|
||||
}
|
||||
|
||||
fn skip_string(bytes: &[u8], mut i: usize, quote: u8) -> usize {
|
||||
i += 1; // opening quote
|
||||
while i < bytes.len() {
|
||||
match bytes[i] {
|
||||
b'\\' if i + 1 < bytes.len() => i += 2,
|
||||
c if c == quote => return i + 1,
|
||||
// Strings can't span unescaped newlines; bail out at one for safety
|
||||
// even though that's technically a parse error.
|
||||
b'\n' => return i + 1,
|
||||
_ => i += 1,
|
||||
}
|
||||
}
|
||||
bytes.len()
|
||||
}
|
||||
|
||||
fn skip_template(bytes: &[u8], mut i: usize) -> usize {
|
||||
i += 1; // opening backtick
|
||||
while i < bytes.len() {
|
||||
match bytes[i] {
|
||||
b'`' => return i + 1,
|
||||
b'\\' if i + 1 < bytes.len() => i += 2,
|
||||
b'$' if bytes.get(i + 1) == Some(&b'{') => {
|
||||
i += 2;
|
||||
i = skip_template_expression(bytes, i);
|
||||
}
|
||||
_ => i += 1,
|
||||
}
|
||||
}
|
||||
bytes.len()
|
||||
}
|
||||
|
||||
/// Skip the expression inside a template's `${ … }`. Returns the position
|
||||
/// just past the closing `}`. Recurses into nested templates and skips
|
||||
/// strings/comments so that braces inside them don't affect the depth count.
|
||||
fn skip_template_expression(bytes: &[u8], mut i: usize) -> usize {
|
||||
let mut depth: i32 = 1;
|
||||
while i < bytes.len() && depth > 0 {
|
||||
match bytes[i] {
|
||||
b'{' => {
|
||||
depth += 1;
|
||||
i += 1;
|
||||
}
|
||||
b'}' => {
|
||||
depth -= 1;
|
||||
i += 1;
|
||||
}
|
||||
b'`' => {
|
||||
i = skip_template(bytes, i);
|
||||
}
|
||||
b'\'' | b'"' => {
|
||||
i = skip_string(bytes, i, bytes[i]);
|
||||
}
|
||||
b'/' => match bytes.get(i + 1).copied() {
|
||||
Some(b'/') => i = skip_line_comment(bytes, i),
|
||||
Some(b'*') => i = skip_block_comment(bytes, i),
|
||||
_ => i += 1,
|
||||
},
|
||||
_ => i += 1,
|
||||
}
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
fn skip_regex(bytes: &[u8], mut i: usize) -> usize {
|
||||
i += 1; // opening slash
|
||||
let mut in_class = false;
|
||||
while i < bytes.len() {
|
||||
match bytes[i] {
|
||||
b'\\' if i + 1 < bytes.len() => i += 2,
|
||||
b'[' => {
|
||||
in_class = true;
|
||||
i += 1;
|
||||
}
|
||||
b']' if in_class => {
|
||||
in_class = false;
|
||||
i += 1;
|
||||
}
|
||||
b'/' if !in_class => {
|
||||
i += 1;
|
||||
// Skip flags
|
||||
while i < bytes.len() && is_identifier_continue(bytes[i]) {
|
||||
i += 1;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
// Unterminated regex — bail.
|
||||
b'\n' => return i,
|
||||
_ => i += 1,
|
||||
}
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
fn skip_ws_and_comments(bytes: &[u8], mut i: usize) -> usize {
|
||||
loop {
|
||||
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t' | b'\n' | b'\r') {
|
||||
i += 1;
|
||||
}
|
||||
if i + 1 < bytes.len() && bytes[i] == b'/' {
|
||||
match bytes[i + 1] {
|
||||
b'/' => {
|
||||
i = skip_line_comment(bytes, i);
|
||||
continue;
|
||||
}
|
||||
b'*' => {
|
||||
i = skip_block_comment(bytes, i);
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_number(bytes: &[u8], start: usize) -> usize {
|
||||
// Crude: consume identifier-continue chars after the leading digit. This
|
||||
// covers integer/float/hex/binary/octal/bigint and exponent forms well
|
||||
// enough for the only purpose this scanner has — advancing past numeric
|
||||
// literals so we don't misread their characters as keywords.
|
||||
let mut i = start + 1;
|
||||
while i < bytes.len() {
|
||||
let c = bytes[i];
|
||||
if is_identifier_continue(c) || c == b'.' {
|
||||
i += 1;
|
||||
} else if (c == b'+' || c == b'-') && i > start && matches!(bytes[i - 1], b'e' | b'E') {
|
||||
i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
fn is_identifier_start(c: u8) -> bool {
|
||||
c.is_ascii_alphabetic() || c == b'_' || c == b'$'
|
||||
}
|
||||
|
||||
fn is_identifier_continue(c: u8) -> bool {
|
||||
c.is_ascii_alphanumeric() || c == b'_' || c == b'$'
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::has_module_syntax;
|
||||
|
||||
// --- Should detect ES module syntax ---
|
||||
|
||||
#[test]
|
||||
fn detects_static_import_side_effect() {
|
||||
assert!(has_module_syntax("import 'foo';"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_static_import_default() {
|
||||
assert!(has_module_syntax("import foo from 'bar';"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_static_import_named() {
|
||||
assert!(has_module_syntax("import { foo } from 'bar';"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_static_import_namespace() {
|
||||
assert!(has_module_syntax("import * as foo from 'bar';"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_default() {
|
||||
assert!(has_module_syntax("export default function () {}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_named() {
|
||||
assert!(has_module_syntax("export { foo };"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_const() {
|
||||
assert!(has_module_syntax("export const foo = 1;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_function() {
|
||||
assert!(has_module_syntax("export function foo() {}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_class() {
|
||||
assert!(has_module_syntax("export class Foo {}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_import_meta() {
|
||||
assert!(has_module_syntax("console.log(import.meta.url);"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_empty_export() {
|
||||
assert!(has_module_syntax("export {};"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_after_block_comment() {
|
||||
assert!(has_module_syntax(
|
||||
"/* leading comment */\nexport const x = 1;"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_after_line_comment() {
|
||||
assert!(has_module_syntax("// leading\nexport const x = 1;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_url_import() {
|
||||
// The firebase-style use case from issue #3748.
|
||||
assert!(has_module_syntax(
|
||||
r#"import { initializeApp } from "https://www.gstatic.com/firebasejs/11.1.0/firebase-app.js";"#
|
||||
));
|
||||
}
|
||||
|
||||
// --- Should NOT detect (classic / CJS / etc.) ---
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_string() {
|
||||
assert!(!has_module_syntax(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_dynamic_import_only() {
|
||||
assert!(!has_module_syntax("import('foo');"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_dynamic_import_in_assignment() {
|
||||
assert!(!has_module_syntax("var x = import('foo');"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_dynamic_import_inside_function() {
|
||||
assert!(!has_module_syntax("function foo() { import('x'); }"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_require() {
|
||||
assert!(!has_module_syntax("const foo = require('foo');"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_module_exports() {
|
||||
assert!(!has_module_syntax("module.exports = foo;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_classic_script() {
|
||||
assert!(!has_module_syntax(
|
||||
"var x = 1; window.foo = x; console.log(x);"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_import_inside_line_comment() {
|
||||
assert!(!has_module_syntax("// import foo from 'bar'\nvar x;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_export_inside_block_comment() {
|
||||
assert!(!has_module_syntax("/* export default foo */\nvar x;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_import_inside_string_double() {
|
||||
assert!(!has_module_syntax(r#"var x = "import foo from bar";"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_import_inside_string_single() {
|
||||
assert!(!has_module_syntax("var x = 'import foo from bar';"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_import_inside_template() {
|
||||
assert!(!has_module_syntax(
|
||||
"var x = `export default ${y}`; var z = 1;"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_import_inside_regex() {
|
||||
assert!(!has_module_syntax("var x = /import foo/.test('foo');"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_export_inside_regex_class() {
|
||||
assert!(!has_module_syntax("var x = /[export]/i.test('foo');"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_method_named_import() {
|
||||
// `obj.import('bar')` — `.import` is a property access, not a keyword.
|
||||
assert!(!has_module_syntax("foo.import('bar');"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_identifier_starting_with_import() {
|
||||
// `importFoo` is a regular identifier; the keyword check requires a
|
||||
// word boundary at the end as well.
|
||||
assert!(!has_module_syntax("var importFoo = 1; importFoo();"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_umd_wrapper() {
|
||||
// The SweetAlert2-shaped UMD bundle from issue #5512.
|
||||
let umd = r#"(function (root, factory) {
|
||||
if (typeof exports === 'object' && typeof module === 'object')
|
||||
module.exports = factory();
|
||||
else if (typeof define === 'function' && define.amd)
|
||||
define(factory);
|
||||
else
|
||||
root.Sweetalert2 = factory();
|
||||
})(this, function () { return {}; });"#;
|
||||
assert!(!has_module_syntax(umd));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_iife() {
|
||||
assert!(!has_module_syntax(
|
||||
"(function () { window.foo = 'bar'; })();"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_template_with_brace_in_string() {
|
||||
// Make sure the `}` inside the inner string doesn't end the template
|
||||
// expression early.
|
||||
assert!(!has_module_syntax("var x = `${ '}}}' }`; var y = 1;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_nested_template() {
|
||||
assert!(!has_module_syntax("var x = `outer ${`inner ${1}`} done`;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_export_after_iife() {
|
||||
// An ESM file that begins with an IIFE and then has an export.
|
||||
assert!(has_module_syntax(
|
||||
"(function () { var x = 1; })();\nexport const y = 2;"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ mod json;
|
||||
|
||||
pub(crate) use file::process_file_to;
|
||||
pub(crate) use hash::add_hash_to_asset;
|
||||
pub(crate) use js::js_is_module;
|
||||
|
||||
/// A manifest of all assets collected from dependencies. This is persisted to disk for users to be
|
||||
/// able to pick up the result of asset extraction.
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct JsAssetOptions {
|
||||
minify: bool,
|
||||
preload: bool,
|
||||
static_head: bool,
|
||||
module: bool,
|
||||
}
|
||||
|
||||
impl Default for JsAssetOptions {
|
||||
@@ -42,6 +43,7 @@ impl JsAssetOptions {
|
||||
preload: false,
|
||||
minify: true,
|
||||
static_head: false,
|
||||
module: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +61,11 @@ impl JsAssetOptions {
|
||||
pub const fn minified(&self) -> bool {
|
||||
self.minify
|
||||
}
|
||||
|
||||
/// Check whether the asset is declared as an ES module
|
||||
pub const fn is_module(&self) -> bool {
|
||||
self.module
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetOptions {
|
||||
@@ -116,6 +123,27 @@ impl AssetOptionsBuilder<JsAssetOptions> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark the asset as an ES module (default: false)
|
||||
///
|
||||
/// When true, the script tag emitted via `with_static_head(true)` is rendered as
|
||||
/// `<script type="module" ...>`, and the file is delivered with its module syntax
|
||||
/// (`import`/`export`) preserved so the browser can resolve imports natively at
|
||||
/// runtime. The default treats the file as a classic script: `<script>` is emitted
|
||||
/// without `type="module"`, and minification preserves classic-script semantics.
|
||||
///
|
||||
/// Note: this does not perform build-time bundling. If you need imports resolved
|
||||
/// into a single file, pre-bundle with your tool of choice and ship the result.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use manganis::{asset, Asset, AssetOptions};
|
||||
/// const _: Asset = asset!("/assets/script.js", AssetOptions::js().with_module(true));
|
||||
/// ```
|
||||
#[allow(unused)]
|
||||
pub const fn with_module(mut self, module: bool) -> Self {
|
||||
self.variant.module = module;
|
||||
self
|
||||
}
|
||||
|
||||
/// Convert the builder into asset options with the given variant
|
||||
pub const fn into_asset_options(self) -> AssetOptions {
|
||||
AssetOptions {
|
||||
|
||||
@@ -38,6 +38,48 @@ const REQUIRED: Asset = asset!("/assets/style.css");
|
||||
const OPTIONAL: Option<Asset> = option_asset!("/assets/missing.css");
|
||||
```
|
||||
|
||||
## JavaScript assets
|
||||
|
||||
The CLI auto-detects whether each `.js` asset is an ES module (top-level
|
||||
`import`/`export` or `import.meta`) or a classic script and processes it
|
||||
accordingly.
|
||||
|
||||
- **`with_minify(true)` (default)**: the file is minified in place. Classic
|
||||
scripts are minified verbatim with their format preserved (no IIFE
|
||||
re-wrapping). ES modules are minified and bundled — local relative imports
|
||||
are inlined into a single ESM file, while `http://` / `https://` imports
|
||||
remain as external `import` statements for the browser to resolve at runtime.
|
||||
- **`with_minify(false)`**: the file is copied byte-for-byte. Use this when
|
||||
shipping pre-built third-party libraries you do not want re-processed.
|
||||
- **`with_static_head(true)`**: appends a `<script>` tag to the document head
|
||||
pointing at the asset. The CLI emits `<script type="module" ...>` when the
|
||||
file is detected (or declared) as an ES module, and a classic `<script>`
|
||||
otherwise.
|
||||
- **`with_preload(true)`**: emits a `<link rel="preload" as="script">` for the asset.
|
||||
- **`with_module(true)`**: forces the file to be treated as an ES module even
|
||||
when auto-detection would say otherwise. Useful when you author a
|
||||
side-effect-only module without top-level `import`/`export` declarations.
|
||||
Files named `*.mjs` are always treated as modules; files named `*.cjs` are
|
||||
always treated as classic.
|
||||
|
||||
```rust
|
||||
use manganis::{asset, Asset, AssetOptions};
|
||||
|
||||
// Vendored UMD library: copy verbatim, emitted as a classic <script>.
|
||||
const SWEETALERT: Asset = asset!(
|
||||
"/assets/sweetalert2.all.min.js",
|
||||
AssetOptions::js().with_minify(false).with_static_head(true)
|
||||
);
|
||||
|
||||
// Authored ES module with local imports: auto-detected from the top-level
|
||||
// `import`/`export`, bundled and minified into a single ESM file, emitted as
|
||||
// `<script type="module">`.
|
||||
const APP: Asset = asset!(
|
||||
"/assets/app.js",
|
||||
AssetOptions::js().with_static_head(true)
|
||||
);
|
||||
```
|
||||
|
||||
## Native FFI Bindings
|
||||
|
||||
Manganis provides the `#[ffi]` attribute macro for generating direct FFI bindings between Rust and native platforms (Swift/Kotlin). See the `geolocation-native-plugin` example for usage.
|
||||
@@ -63,4 +63,108 @@ for (let { port, name } of test_variants) {
|
||||
);
|
||||
});
|
||||
|
||||
// Regression coverage for https://github.com/DioxusLabs/dioxus/issues/5512.
|
||||
// Both files are vanilla IIFE scripts injected via `with_static_head(true)`.
|
||||
// The 0.7.4–0.7.6 esbuild path wrapped them in a wrapper IIFE / `--format=esm`,
|
||||
// which dropped the global side-effect and produced `export` syntax under a
|
||||
// classic `<script>` tag. The asserted globals only get set if the file is
|
||||
// delivered as a classic script with its body intact.
|
||||
test(`classic js asset with_minify(false) is copied byte-for-byte in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__iife_classic_value);
|
||||
expect(value).toBe("ok-classic");
|
||||
});
|
||||
|
||||
test(`classic js asset with_minify(true) stays a classic script in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__iife_minify_value);
|
||||
expect(value).toBe("ok-minify");
|
||||
const classicScript = page.locator('script[src*="iife_minify"]');
|
||||
await expect(classicScript).toHaveCount(1);
|
||||
await expect(classicScript).not.toHaveAttribute("type", "module");
|
||||
});
|
||||
|
||||
test(`umd js asset is minified without being converted to a module in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__umd_minify_value);
|
||||
expect(value).toBe("ok-umd");
|
||||
const classicScript = page.locator('script[src*="umd_minify"]');
|
||||
await expect(classicScript).toHaveCount(1);
|
||||
await expect(classicScript).not.toHaveAttribute("type", "module");
|
||||
});
|
||||
|
||||
test(`cjs js asset is treated as a classic script in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__cjs_classic_value);
|
||||
expect(value).toBe("ok-cjs");
|
||||
const classicScript = page.locator('script[src*="cjs_classic"]');
|
||||
await expect(classicScript).toHaveCount(1);
|
||||
await expect(classicScript).not.toHaveAttribute("type", "module");
|
||||
});
|
||||
|
||||
test(`sweetalert2 UMD bundle from issue 5512 is copied and exported globally in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => ({
|
||||
// @ts-ignore
|
||||
hasGlobal: typeof window.Sweetalert2 === "function",
|
||||
// @ts-ignore
|
||||
hasFire: typeof window.Sweetalert2?.fire === "function",
|
||||
}));
|
||||
expect(value).toEqual({ hasGlobal: true, hasFire: true });
|
||||
const classicScript = page.locator('script[src*="sweetalert2.all.min"]');
|
||||
await expect(classicScript).toHaveCount(1);
|
||||
await expect(classicScript).not.toHaveAttribute("type", "module");
|
||||
});
|
||||
|
||||
// `with_module(true)` should emit `<script type="module">` and preserve
|
||||
// module syntax during minification. The fixture uses `import.meta.url`,
|
||||
// which only parses when the script tag has `type="module"`.
|
||||
test(`js asset with_module(true) is loaded as an ES module in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__esm_module_value);
|
||||
expect(value).toBe("ok-module");
|
||||
const moduleScript = page.locator('script[type="module"][src*="esm_module"]');
|
||||
await expect(moduleScript).toHaveCount(1);
|
||||
});
|
||||
|
||||
// Auto-detection: the CLI's has_module_syntax scan should recognise top-level
|
||||
// `export` and emit `<script type="module">` even without `with_module(true)`.
|
||||
test(`js asset with top-level export is auto-detected as ES module in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__esm_auto_value);
|
||||
expect(value).toBe("ok-auto");
|
||||
// import.meta only parses inside a real module; if the script were emitted
|
||||
// as classic the file would have errored at parse time.
|
||||
// @ts-ignore
|
||||
const metaType = await page.evaluate(() => window.__esm_auto_meta);
|
||||
expect(metaType).toBe("string");
|
||||
const moduleScript = page.locator('script[type="module"][src*="esm_auto"]');
|
||||
await expect(moduleScript).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`js asset with relative ESM import is bundled as an ES module in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__esm_import_value);
|
||||
expect(value).toBe("ok-esm-import");
|
||||
const moduleScript = page.locator('script[type="module"][src*="esm_import_entry"]');
|
||||
await expect(moduleScript).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`mjs extension is loaded as an ES module in ${name}`, async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
// @ts-ignore
|
||||
const value = await page.evaluate(() => window.__mjs_auto_value);
|
||||
expect(value).toBe("ok-mjs");
|
||||
const moduleScript = page.locator('script[type="module"][src*="mjs_auto"]');
|
||||
await expect(moduleScript).toHaveCount(1);
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
(function () {
|
||||
var module = { exports: {} };
|
||||
var exports = module.exports;
|
||||
exports.value = "ok-cjs";
|
||||
window.__cjs_classic_value = module.exports.value;
|
||||
})();
|
||||
@@ -0,0 +1,7 @@
|
||||
// No `with_module(true)` is set on the asset — the CLI must detect this file
|
||||
// as ESM from its source (top-level `export`) and emit `<script type="module">`
|
||||
// automatically. The `import.meta.url` reference would be a syntax error if the
|
||||
// script tag were a classic <script>.
|
||||
export const value = "ok-auto";
|
||||
window.__esm_auto_value = value;
|
||||
window.__esm_auto_meta = typeof import.meta.url;
|
||||
@@ -0,0 +1 @@
|
||||
export const importedValue = "ok-esm-import";
|
||||
@@ -0,0 +1,4 @@
|
||||
import { importedValue } from "./esm_import_dep.js";
|
||||
|
||||
window.__esm_import_value = importedValue;
|
||||
export const entryValue = importedValue;
|
||||
@@ -0,0 +1,6 @@
|
||||
// No top-level import/export — auto-detection alone would classify this as a
|
||||
// classic script. `with_module(true)` on the asset overrides that and forces
|
||||
// the CLI to emit `<script type="module">` and run esbuild with the ESM
|
||||
// pipeline. The playwright spec asserts both that the global is set and that
|
||||
// the script tag actually has `type="module"`.
|
||||
window.__esm_module_value = "ok-module";
|
||||
@@ -0,0 +1,3 @@
|
||||
(function () {
|
||||
window.__iife_classic_value = "ok-classic";
|
||||
})();
|
||||
@@ -0,0 +1,4 @@
|
||||
(function () {
|
||||
var value = "ok-minify";
|
||||
window.__iife_minify_value = value;
|
||||
})();
|
||||
@@ -0,0 +1 @@
|
||||
window.__mjs_auto_value = typeof import.meta.url === "string" ? "ok-mjs" : "bad-mjs";
|
||||
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* sweetalert2 v11.22.2
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Sweetalert2=t()}(this,function(){"use strict";function e(e,t,n){if("function"==typeof e?e===t:e.has(t))return arguments.length<3?t:n;throw new TypeError("Private element is not present on this object")}function t(t,n){return t.get(e(t,n))}function n(e,t,n){(function(e,t){if(t.has(e))throw new TypeError("Cannot initialize the same private elements twice on an object")})(e,t),t.set(e,n)}const o={},i=e=>new Promise(t=>{if(!e)return t();const n=window.scrollX,i=window.scrollY;o.restoreFocusTimeout=setTimeout(()=>{o.previousActiveElement instanceof HTMLElement?(o.previousActiveElement.focus(),o.previousActiveElement=null):document.body&&document.body.focus(),t()},100),window.scrollTo(n,i)}),s="swal2-",r=["container","shown","height-auto","iosfix","popup","modal","no-backdrop","no-transition","toast","toast-shown","show","hide","close","title","html-container","actions","confirm","deny","cancel","footer","icon","icon-content","image","input","file","range","select","radio","checkbox","label","textarea","inputerror","input-label","validation-message","progress-steps","active-progress-step","progress-step","progress-step-line","loader","loading","styled","top","top-start","top-end","top-left","top-right","center","center-start","center-end","center-left","center-right","bottom","bottom-start","bottom-end","bottom-left","bottom-right","grow-row","grow-column","grow-fullscreen","rtl","timer-progress-bar","timer-progress-bar-container","scrollbar-measure","icon-success","icon-warning","icon-info","icon-question","icon-error","draggable","dragging"].reduce((e,t)=>(e[t]=s+t,e),{}),a=["success","warning","info","question","error"].reduce((e,t)=>(e[t]=s+t,e),{}),l="SweetAlert2:",c=e=>e.charAt(0).toUpperCase()+e.slice(1),u=e=>{console.warn(`${l} ${"object"==typeof e?e.join(" "):e}`)},d=e=>{console.error(`${l} ${e}`)},p=[],m=(e,t=null)=>{var n;n=`"${e}" is deprecated and will be removed in the next major release.${t?` Use "${t}" instead.`:""}`,p.includes(n)||(p.push(n),u(n))},h=e=>"function"==typeof e?e():e,g=e=>e&&"function"==typeof e.toPromise,f=e=>g(e)?e.toPromise():Promise.resolve(e),b=e=>e&&Promise.resolve(e)===e,y=()=>document.body.querySelector(`.${r.container}`),v=e=>{const t=y();return t?t.querySelector(e):null},w=e=>v(`.${e}`),C=()=>w(r.popup),A=()=>w(r.icon),E=()=>w(r.title),k=()=>w(r["html-container"]),$=()=>w(r.image),B=()=>w(r["progress-steps"]),L=()=>w(r["validation-message"]),P=()=>v(`.${r.actions} .${r.confirm}`),x=()=>v(`.${r.actions} .${r.cancel}`),T=()=>v(`.${r.actions} .${r.deny}`),S=()=>v(`.${r.loader}`),O=()=>w(r.actions),M=()=>w(r.footer),j=()=>w(r["timer-progress-bar"]),H=()=>w(r.close),I=()=>{const e=C();if(!e)return[];const t=e.querySelectorAll('[tabindex]:not([tabindex="-1"]):not([tabindex="0"])'),n=Array.from(t).sort((e,t)=>{const n=parseInt(e.getAttribute("tabindex")||"0"),o=parseInt(t.getAttribute("tabindex")||"0");return n>o?1:n<o?-1:0}),o=e.querySelectorAll('\n a[href],\n area[href],\n input:not([disabled]),\n select:not([disabled]),\n textarea:not([disabled]),\n button:not([disabled]),\n iframe,\n object,\n embed,\n [tabindex="0"],\n [contenteditable],\n audio[controls],\n video[controls],\n summary\n'),i=Array.from(o).filter(e=>"-1"!==e.getAttribute("tabindex"));return[...new Set(n.concat(i))].filter(e=>ee(e))},D=()=>N(document.body,r.shown)&&!N(document.body,r["toast-shown"])&&!N(document.body,r["no-backdrop"]),V=()=>{const e=C();return!!e&&N(e,r.toast)},q=(e,t)=>{if(e.textContent="",t){const n=(new DOMParser).parseFromString(t,"text/html"),o=n.querySelector("head");o&&Array.from(o.childNodes).forEach(t=>{e.appendChild(t)});const i=n.querySelector("body");i&&Array.from(i.childNodes).forEach(t=>{t instanceof HTMLVideoElement||t instanceof HTMLAudioElement?e.appendChild(t.cloneNode(!0)):e.appendChild(t)})}},N=(e,t)=>{if(!t)return!1;const n=t.split(/\s+/);for(let t=0;t<n.length;t++)if(!e.classList.contains(n[t]))return!1;return!0},_=(e,t,n)=>{if(((e,t)=>{Array.from(e.classList).forEach(n=>{Object.values(r).includes(n)||Object.values(a).includes(n)||Object.values(t.showClass||{}).includes(n)||e.classList.remove(n)})})(e,t),!t.customClass)return;const o=t.customClass[n];o&&("string"==typeof o||o.forEach?z(e,o):u(`Invalid type of customClass.${n}! Expected string or iterable object, got "${typeof o}"`))},F=(e,t)=>{if(!t)return null;switch(t){case"select":case"textarea":case"file":return e.querySelector(`.${r.popup} > .${r[t]}`);case"checkbox":return e.querySelector(`.${r.popup} > .${r.checkbox} input`);case"radio":return e.querySelector(`.${r.popup} > .${r.radio} input:checked`)||e.querySelector(`.${r.popup} > .${r.radio} input:first-child`);case"range":return e.querySelector(`.${r.popup} > .${r.range} input`);default:return e.querySelector(`.${r.popup} > .${r.input}`)}},R=e=>{if(e.focus(),"filLine truncated
|
||||
"undefined"!=typeof document&&function(e,t){var n=e.createElement("style");if(e.getElementsByTagName("head")[0].appendChild(n),n.styleSheet)n.styleSheet.disabled||(n.styleSheet.cssText=t);else try{n.innerHTML=t}catch(e){n.innerText=t}}(document,":root{--swal2-outline: 0 0 0 3px rgba(100, 150, 200, 0.5);--swal2-container-padding: 0.625em;--swal2-backdrop: rgba(0, 0, 0, 0.4);--swal2-backdrop-transition: background-color 0.1s;--swal2-width: 32em;--swal2-padding: 0 0 1.25em;--swal2-border: none;--swal2-border-radius: 0.3125rem;--swal2-background: white;--swal2-color: #545454;--swal2-show-animation: swal2-show 0.3s;--swal2-hide-animation: swal2-hide 0.15s forwards;--swal2-icon-zoom: 1;--swal2-icon-animations: true;--swal2-title-padding: 0.8em 1em 0;--swal2-html-container-padding: 1em 1.6em 0.3em;--swal2-input-border: 1px solid #d9d9d9;--swal2-input-border-radius: 0.1875em;--swal2-input-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.06), 0 0 0 3px transparent;--swal2-input-background: transparent;--swal2-input-transition: border-color 0.2s, box-shadow 0.2s;--swal2-input-hover-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.06), 0 0 0 3px transparent;--swal2-input-focus-border: 1px solid #b4dbed;--swal2-input-focus-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.06), 0 0 0 3px $swal2-outline-color;--swal2-progress-step-background: #add8e6;--swal2-validation-message-background: #f0f0f0;--swal2-validation-message-color: #666;--swal2-footer-border-color: #eee;--swal2-footer-background: transparent;--swal2-footer-color: inherit;--swal2-close-button-position: initial;--swal2-close-button-inset: auto;--swal2-close-button-font-size: 2.5em;--swal2-close-button-color: #ccc;--swal2-close-button-transition: color 0.2s, box-shadow 0.2s;--swal2-close-button-outline: initial;--swal2-close-button-box-shadow: inset 0 0 0 3px transparent;--swal2-close-button-focus-box-shadow: inset var(--swal2-outline);--swal2-close-button-hover-transform: none;--swal2-actions-justify-content: center;--swal2-actions-width: auto;--swal2-actions-margin: 1.25em auto 0;--swal2-actions-padding: 0;--swal2-actions-border-radius: 0;--swal2-actions-background: transparent;--swal2-action-button-transition: background-color 0.2s, box-shadow 0.2s;--swal2-action-button-hover: black 10%;--swal2-action-button-active: black 10%;--swal2-confirm-button-box-shadow: none;--swal2-confirm-button-border-radius: 0.25em;--swal2-confirm-button-background-color: #7066e0;--swal2-confirm-button-color: #fff;--swal2-deny-button-box-shadow: none;--swal2-deny-button-border-radius: 0.25em;--swal2-deny-button-background-color: #dc3741;--swal2-deny-button-color: #fff;--swal2-cancel-button-box-shadow: none;--swal2-cancel-button-border-radius: 0.25em;--swal2-cancel-button-background-color: #6e7881;--swal2-cancel-button-color: #fff;--swal2-toast-show-animation: swal2-toast-show 0.5s;--swal2-toast-hide-animation: swal2-toast-hide 0.1s forwards;--swal2-toast-border: none;--swal2-toast-box-shadow: 0 0 1px hsl(0deg 0% 0% / 0.075), 0 1px 2px hsl(0deg 0% 0% / 0.075), 1px 2px 4px hsl(0deg 0% 0% / 0.075), 1px 3px 8px hsl(0deg 0% 0% / 0.075), 2px 4px 16px hsl(0deg 0% 0% / 0.075)}[data-swal2-theme=dark]{--swal2-dark-theme-black: #19191a;--swal2-dark-theme-white: #e1e1e1;--swal2-background: var(--swal2-dark-theme-black);--swal2-color: var(--swal2-dark-theme-white);--swal2-footer-border-color: #555;--swal2-input-background: color-mix(in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10%);--swal2-validation-message-background: color-mix( in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10% );--swal2-validation-message-color: var(--swal2-dark-theme-white)}@media(prefers-color-scheme: dark){[data-swal2-theme=auto]{--swal2-dark-theme-black: #19191a;--swal2-dark-theme-white: #e1e1e1;--swal2-background: var(--swal2-dark-theme-black);--swal2-color: var(--swal2-dark-theme-white);--swal2-footer-border-color: #555;--swal2-input-background: color-mix(in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10%);--swal2-validation-message-background: color-mix( in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10% );--swal2-validation-message-color: var(--swal2-dark-theme-white)}}body.swal2-shown:not(.swal2-no-backdrop,.swal2-toast-shown){overflow:hidden}body.swal2-height-auto{height:auto !important}body.swal2-no-backdrop .swal2-container{background-color:rgba(0,0,0,0) !important;pointer-events:none}body.swal2-no-backdrop .swal2-container .swal2-popup{pointer-events:all}body.swal2-no-backdrop .swal2-container .swal2-modal{box-shadow:0 0 10px var(--swal2-backdrop)}body.swal2-toast-shown .swal2-container{box-sizing:border-box;width:360px;max-width:100%;background-color:rgba(0,0,0,0);pointer-events:none}body.swal2-toast-shown .swal2-container.swal2-top{inset:0 auto auto 50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-top-end,body.swal2-toast-shown .swal2-container.swal2-top-right{inset:0 0 auto auto}body.swal2-toast-showLine truncated
|
||||
@@ -0,0 +1,14 @@
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object" && typeof module === "object") {
|
||||
root.__umd_minify_value = (module.exports = factory()).value;
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
define(function () {
|
||||
root.__umd_minify_value = factory().value;
|
||||
return factory();
|
||||
});
|
||||
} else {
|
||||
root.__umd_minify_value = factory().value;
|
||||
}
|
||||
})(this, function () {
|
||||
return { value: "ok-umd" };
|
||||
});
|
||||
@@ -17,6 +17,65 @@ static _ASSET: Asset = asset!(
|
||||
AssetOptions::builder().with_hash_suffix(false)
|
||||
);
|
||||
|
||||
// Regression coverage for https://github.com/DioxusLabs/dioxus/issues/5512:
|
||||
// `with_minify(false)` must copy the file byte-for-byte (no IIFE rewrap, no
|
||||
// ESM transform), and `with_minify(true)` must keep the script as a classic
|
||||
// script (not ESM) so the `<script>` tag without `type="module"` still runs.
|
||||
#[used]
|
||||
static _IIFE_CLASSIC_JS: Asset = asset!(
|
||||
"/assets/iife_classic.js",
|
||||
AssetOptions::js().with_minify(false).with_static_head(true)
|
||||
);
|
||||
#[used]
|
||||
static _IIFE_MINIFY_JS: Asset = asset!(
|
||||
"/assets/iife_minify.js",
|
||||
AssetOptions::js().with_minify(true).with_static_head(true)
|
||||
);
|
||||
#[used]
|
||||
static _UMD_MINIFY_JS: Asset = asset!(
|
||||
"/assets/umd_minify.js",
|
||||
AssetOptions::js().with_minify(true).with_static_head(true)
|
||||
);
|
||||
#[used]
|
||||
static _CJS_CLASSIC_JS: Asset = asset!(
|
||||
"/assets/cjs_classic.cjs",
|
||||
AssetOptions::js().with_minify(true).with_static_head(true)
|
||||
);
|
||||
#[used]
|
||||
static _SWEETALERT2_JS: Asset = asset!(
|
||||
"/assets/sweetalert2.all.min.js",
|
||||
AssetOptions::js().with_minify(false).with_static_head(true)
|
||||
);
|
||||
|
||||
// Coverage for `with_module(true)`: the script tag must be emitted as
|
||||
// `<script type="module">` and minification must preserve module syntax so
|
||||
// the browser can parse the file as ESM.
|
||||
#[used]
|
||||
static _ESM_MODULE_JS: Asset = asset!(
|
||||
"/assets/esm_module.js",
|
||||
AssetOptions::js().with_module(true).with_static_head(true)
|
||||
);
|
||||
|
||||
// Coverage for module auto-detection: this asset has no `with_module(true)`
|
||||
// override, but its source contains a top-level `export`, so the CLI's
|
||||
// has_module_syntax scan must classify it as ESM and emit the script tag with
|
||||
// `type="module"`. Confirms detection works without explicit user opt-in.
|
||||
#[used]
|
||||
static _ESM_AUTO_JS: Asset = asset!(
|
||||
"/assets/esm_auto.js",
|
||||
AssetOptions::js().with_static_head(true)
|
||||
);
|
||||
#[used]
|
||||
static _ESM_IMPORT_ENTRY_JS: Asset = asset!(
|
||||
"/assets/esm_import_entry.js",
|
||||
AssetOptions::js().with_static_head(true)
|
||||
);
|
||||
#[used]
|
||||
static _MJS_AUTO_JS: Asset = asset!(
|
||||
"/assets/mjs_auto.mjs",
|
||||
AssetOptions::js().with_static_head(true)
|
||||
);
|
||||
|
||||
pub fn main() {
|
||||
dioxus::launch(App);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user