mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
feat(minifier): merge import statements against the same module (#25534)
Merge compatible import declarations for the same module, including
declarations separated by imports from other modules:
```js
import { b } from "./res-SWvxTGCX.js";
import { g } from "./res-SWvxTGCX.js";
// becomes
import { b, g } from "./res-SWvxTGCX.js";
```
Preserve separate declarations when import attributes, phases,
type/value modes, or namespace specifiers prevent merging. Combine
multiple default bindings using named `default` imports. Keep
source-phase imports separate because their syntax permits only one
binding; a regression test checks that they remain parseable and
idempotent.
---------
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
1 parent
cc82598ffd
commit
adb1139e64
6 files changed
+208
-8
No files matched your search
@@ -133,6 +133,147 @@ impl<'a> PeepholeOptimizations {
|
||||
false
|
||||
}
|
||||
|
||||
/// Check whether two import declarations can be merged.
|
||||
fn can_merge_imports(first: &ImportDeclaration<'a>, second: &ImportDeclaration<'a>) -> bool {
|
||||
if first.source.value != second.source.value
|
||||
|| first.phase != second.phase
|
||||
|| first.phase == Some(ImportPhase::Source)
|
||||
|| first.import_kind != second.import_kind
|
||||
|| first.import_kind.is_type()
|
||||
|| first.with_clause.content_ne(&second.with_clause)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional default specifiers can be represented as named imports of
|
||||
// `default`. Namespace and named specifiers cannot coexist.
|
||||
let mut default_count = 0;
|
||||
let mut has_namespace = false;
|
||||
let mut has_named = false;
|
||||
for specifier in first
|
||||
.specifiers
|
||||
.iter()
|
||||
.flat_map(|specifiers| specifiers.iter())
|
||||
.chain(second.specifiers.iter().flat_map(|specifiers| specifiers.iter()))
|
||||
{
|
||||
match specifier {
|
||||
ImportDeclarationSpecifier::ImportSpecifier(_) => has_named = true,
|
||||
ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => default_count += 1,
|
||||
ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => {
|
||||
if has_namespace {
|
||||
return false;
|
||||
}
|
||||
has_namespace = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
!(has_namespace && (has_named || default_count > 1))
|
||||
}
|
||||
|
||||
/// Convert a default import into a named import of the module's `default`
|
||||
/// export. This allows multiple default bindings in one import clause.
|
||||
fn default_to_named_import(
|
||||
default_specifier: ImportDeclarationSpecifier<'a>,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> ImportDeclarationSpecifier<'a> {
|
||||
let ImportDeclarationSpecifier::ImportDefaultSpecifier(default_specifier) =
|
||||
default_specifier
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
let ImportDefaultSpecifier { span, local, .. } = default_specifier.unbox();
|
||||
ImportDeclarationSpecifier::new_import_specifier(
|
||||
span,
|
||||
ModuleExportName::IdentifierName(IdentifierName::new(span, "default", ctx)),
|
||||
local,
|
||||
ImportOrExportKind::Value,
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Merge import declarations that load the same module request.
|
||||
///
|
||||
/// ```js
|
||||
/// import { foo } from "module";
|
||||
/// import { bar } from "other";
|
||||
/// import { baz } from "module";
|
||||
/// // becomes
|
||||
/// import { foo, baz } from "module";
|
||||
/// import { bar } from "other";
|
||||
/// ```
|
||||
pub fn merge_imports(stmts: &mut ArenaVec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
|
||||
let mut import_cache: ArenaHashMap<'a, &'a str, ArenaVec<'a, usize>> =
|
||||
ArenaHashMap::new_in(ctx.allocator());
|
||||
let mut merges = ArenaVec::new_in(ctx);
|
||||
|
||||
// Merge into the target immediately so later declarations are checked
|
||||
// against the combined import clause. Keep every unmerged declaration
|
||||
// in a source group because import attributes, phases, and type/value
|
||||
// mode may differ between declarations with the same source string.
|
||||
for statement_index in 0..stmts.len() {
|
||||
let Statement::ImportDeclaration(import_decl) = stmts.get(statement_index).unwrap()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let candidates = import_cache
|
||||
.entry(import_decl.source.value.as_str())
|
||||
.or_insert_with(|| ArenaVec::new_in(ctx));
|
||||
let target_index = candidates.iter().find_map(|&target_index| {
|
||||
let Some(Statement::ImportDeclaration(target)) = stmts.get(target_index) else {
|
||||
return None;
|
||||
};
|
||||
Self::can_merge_imports(target, import_decl).then_some(target_index)
|
||||
});
|
||||
|
||||
let Some(target_index) = target_index else {
|
||||
candidates.push(statement_index);
|
||||
continue;
|
||||
};
|
||||
|
||||
{
|
||||
let source_stmt = stmts.get(statement_index).unwrap();
|
||||
ctx.drop_statement(source_stmt);
|
||||
}
|
||||
let Statement::ImportDeclaration(source_import) =
|
||||
stmts.get_mut(statement_index).unwrap()
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
let Some(mut source_specifiers) = source_import.specifiers.take() else {
|
||||
merges.push((target_index, statement_index));
|
||||
continue;
|
||||
};
|
||||
|
||||
let Statement::ImportDeclaration(target_import) = stmts.get_mut(target_index).unwrap()
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
let target_specifiers =
|
||||
target_import.specifiers.get_or_insert_with(|| ArenaVec::new_in(ctx));
|
||||
let target_has_default = target_specifiers.iter().any(|specifier| {
|
||||
matches!(specifier, ImportDeclarationSpecifier::ImportDefaultSpecifier(_))
|
||||
});
|
||||
if let Some(default_index) = source_specifiers.iter().position(|specifier| {
|
||||
matches!(specifier, ImportDeclarationSpecifier::ImportDefaultSpecifier(_))
|
||||
}) {
|
||||
let default_specifier = source_specifiers.remove(default_index);
|
||||
if target_has_default {
|
||||
target_specifiers.push(Self::default_to_named_import(default_specifier, ctx));
|
||||
} else {
|
||||
target_specifiers.insert(0, default_specifier);
|
||||
}
|
||||
}
|
||||
target_specifiers.append(&mut source_specifiers);
|
||||
merges.push((target_index, statement_index));
|
||||
}
|
||||
|
||||
// Remove imports in reverse order so removing a later import does not
|
||||
// shift the indices of any merge that is still waiting to be applied.
|
||||
for &(_, source_index) in merges.iter().rev() {
|
||||
stmts.remove(source_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether an import can be merged with a local named export.
|
||||
fn can_merge_import_export(
|
||||
import_decl: &ImportDeclaration<'a>,
|
||||
|
||||
@@ -389,6 +389,7 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||
}
|
||||
|
||||
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
Self::merge_imports(&mut program.body, ctx);
|
||||
Self::merge_import_export(&mut program.body, ctx);
|
||||
// Private member usage is collected only in full optimization mode.
|
||||
debug_assert!(ctx.is_tree_shake_only() || ctx.state.private_member_usage.is_at_root());
|
||||
|
||||
@@ -2,6 +2,10 @@ use crate::{test, test_same, test_target};
|
||||
|
||||
#[test]
|
||||
fn merge_import_and_export() {
|
||||
test(
|
||||
"import { foo }from 'foo'; import { bar } from 'bar'; import {foo2} from 'foo'; export {foo, foo2}",
|
||||
"export { foo, foo2 } from 'foo'; import { bar } from 'bar';",
|
||||
);
|
||||
test("import { foo } from 'somewhere'; export { foo };", "export { foo } from 'somewhere';");
|
||||
test(
|
||||
"import { foo as bar } from 'somewhere'; export { bar as baz };",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use crate::{test, test_same};
|
||||
|
||||
#[test]
|
||||
fn preserve_source_imports() {
|
||||
test_same("import source foo from 'foo'; import source bar from 'foo'; console.log(foo, bar);");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_imports() {
|
||||
test(
|
||||
"import { foo } from 'foo'; import { bar } from 'foo';",
|
||||
"import { foo, bar } from 'foo';",
|
||||
);
|
||||
test(
|
||||
"import { foo } from 'foo'; import { bar } from 'bar'; import { foo2 } from 'foo';",
|
||||
"import { foo, foo2 } from 'foo'; import { bar } from 'bar';",
|
||||
);
|
||||
test(
|
||||
"import { foo as foo1 } from 'foo'; import { bar as bar1 } from 'foo';",
|
||||
"import { foo as foo1, bar as bar1 } from 'foo';",
|
||||
);
|
||||
test(
|
||||
"import { foo } from 'foo'; import { bar } from 'bar'; import { foo2 } from 'foo'; import { bar2 } from 'bar';",
|
||||
"import { foo, foo2 } from 'foo'; import { bar, bar2 } from 'bar';",
|
||||
);
|
||||
test(
|
||||
"import { foo } from 'foo'; import { bar } from 'foo'; import { baz } from 'foo';",
|
||||
"import { foo, bar, baz } from 'foo';",
|
||||
);
|
||||
test("import foo from 'foo'; import { bar } from 'foo';", "import foo, { bar } from 'foo';");
|
||||
test("import { bar } from 'foo'; import foo from 'foo';", "import foo, { bar } from 'foo';");
|
||||
test("import foo from 'foo'; import * as ns from 'foo';", "import foo, * as ns from 'foo';");
|
||||
test("import * as ns from 'foo'; import foo from 'foo';", "import foo, * as ns from 'foo';");
|
||||
test(
|
||||
"import foo from 'foo'; import bar from 'foo';",
|
||||
"import foo, { default as bar } from 'foo';",
|
||||
);
|
||||
test(
|
||||
"import foo from 'foo'; import bar from 'foo'; import baz from 'foo';",
|
||||
"import foo, { default as bar, default as baz } from 'foo';",
|
||||
);
|
||||
|
||||
test_same("import { foo } from 'foo'; import * as ns from 'foo';");
|
||||
test(
|
||||
"import foo from 'foo'; import bar from 'foo'; import * as ns from 'foo';",
|
||||
"import foo, { default as bar } from 'foo'; import * as ns from 'foo';",
|
||||
);
|
||||
test_same("import * as ns1 from 'foo'; import * as ns2 from 'foo';");
|
||||
test_same("import { foo } from 'foo' with { type: 'json' }; import { bar } from 'foo';");
|
||||
test_same(
|
||||
"import { foo } from 'foo' with { type: 'json' }; import { bar } from 'foo' with { type: 'css' };",
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ mod inline_single_use_variable;
|
||||
mod manual_pure_functions;
|
||||
mod merge_assignments_to_declarations;
|
||||
mod merge_import_export;
|
||||
mod merge_imports;
|
||||
mod minimize_binary_expression;
|
||||
mod minimize_conditional_expression;
|
||||
mod minimize_conditions;
|
||||
|
||||
@@ -5,7 +5,7 @@ checker.ts:
|
||||
sys deallocs: 122
|
||||
sys alloc bytes: 14996287 # 15.00 MB
|
||||
sys peak growth: 10169560 # 10.17 MB
|
||||
arena allocs: 56217
|
||||
arena allocs: 56229
|
||||
arena reallocs: 5084
|
||||
arena size: 16369368 # 16.37 MB
|
||||
|
||||
@@ -16,9 +16,9 @@ App.tsx:
|
||||
sys deallocs: 61
|
||||
sys alloc bytes: 2128539 # 2.13 MB
|
||||
sys peak growth: 1612427 # 1.61 MB
|
||||
arena allocs: 7490
|
||||
arena allocs: 7727
|
||||
arena reallocs: 920
|
||||
arena size: 2733056 # 2.73 MB
|
||||
arena size: 2765960 # 2.77 MB
|
||||
|
||||
RadixUIAdoptionSection.jsx:
|
||||
file size: 2520 # 2.52 kB
|
||||
@@ -27,9 +27,9 @@ RadixUIAdoptionSection.jsx:
|
||||
sys deallocs: 21
|
||||
sys alloc bytes: 17400 # 17.40 kB
|
||||
sys peak growth: 11628 # 11.63 kB
|
||||
arena allocs: 21
|
||||
arena allocs: 35
|
||||
arena reallocs: 0
|
||||
arena size: 36112 # 36.11 kB
|
||||
arena size: 37216 # 37.22 kB
|
||||
|
||||
pdf.mjs:
|
||||
file size: 567296 # 567.30 kB
|
||||
@@ -60,7 +60,7 @@ binder.ts:
|
||||
sys deallocs: 33
|
||||
sys alloc bytes: 963008 # 963.01 kB
|
||||
sys peak growth: 657404 # 657.40 kB
|
||||
arena allocs: 2924
|
||||
arena allocs: 2930
|
||||
arena reallocs: 244
|
||||
arena size: 1083768 # 1.08 MB
|
||||
|
||||
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
|
||||
sys deallocs: 1854
|
||||
sys alloc bytes: 5304252 # 5.30 MB
|
||||
sys peak growth: 3707425 # 3.71 MB
|
||||
arena allocs: 31043
|
||||
arena allocs: 31108
|
||||
arena reallocs: 3012
|
||||
arena size: 8440280 # 8.44 MB
|
||||
arena size: 8444928 # 8.44 MB
|
||||
Reference in new issue
Block a user