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.