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:
Jonathan Kelley authored and GitHub committed 2026-04-30 17:50:37 -07:00
1 parent 2f40780ce0
commit 218bb9d56d
24 files changed
+1598 -616

No files matched your search

+2
View File
@@ -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)
+1 -1
View File
@@ -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()
+11 -2
View File
@@ -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>"#
);
}
}
+56 -11
View File
@@ -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]
+1 -12
View File
@@ -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 -3
View File
@@ -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(())
+2 -4
View File
@@ -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
View File
@@ -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;"
));
}
}
}
+1
View File
@@ -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.
+28
View File
@@ -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 {
+42
View File
@@ -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);
}