[turbopack] Drop unused exports from a CJS module#95716
Conversation
Stats from current PR🟢 1 improvement
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📎 Tarball URLCommit: 5fb6e11 |
Failing CI jobsCommit: 5fb6e11 | About building and testing Next.js |
5255e39 to
eaaf647
Compare
eaaf647 to
5fb6e11
Compare
| /// expression) — sets `is_unsafe`. | ||
| /// | ||
| /// TODO (@sampoder): handle `Object.defineProperty(exports, "__esModule", { value: true })` | ||
| pub fn analyze_cjs_exports(program: &Program, unresolved_mark: Mark) -> CommonJsExportsAnalysis { |
There was a problem hiding this comment.
why does this need to be a separate AST pass?
There was a problem hiding this comment.
I can try moving it into a pre-existing one. Might be a little less clean but it will probably run a lot faster. I also was thinking we might want to put this behind a flag so this method would make that easier. Is there a pass you had in mind that I could combine it with?
There was a problem hiding this comment.
the natural place would be do it as part of the main Analyzer and then use the Effects system to report a droppable effect for every export
or really follow the pattern we have for ESM items: see add_esm_module_item
| /// shadow. Prevents `let exports = {}; exports.foo = 'a'` from being treated as | ||
| /// a write to the global `exports`. | ||
| fn is_global(identifier: &Ident, name: &str, unresolved_mark: Mark) -> bool { | ||
| pub(crate) fn is_global(identifier: &Ident, name: &str, unresolved_mark: Mark) -> bool { |
There was a problem hiding this comment.
these helpers should probably just move to a new module.... cjs_ast.rs?
| name: RcStr, | ||
| /// Source position (`Stmt::span().lo`) of the `exports.NAME = …` statement, | ||
| /// used to locate it in the module body at code-gen time. | ||
| span_lo: u32, |
There was a problem hiding this comment.
typically we capture an AST path, is that possible in your traversal
| if matches!(*export_usage_info, ModuleExportUsageInfo::All) { | ||
| return Ok(CodeGeneration::empty()); |
There was a problem hiding this comment.
maybe this is more efficient... but this would fall out of the is_export_used checks below so you could just drop this special case
| impl DropExportWrites { | ||
| fn drop_matching(&self, stmt: &mut Stmt) { | ||
| if self.unused.contains(&stmt.span().lo) { | ||
| *stmt = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); |
There was a problem hiding this comment.
i don't think we can drop the whole statement
exports.foo = bar() should become bar()
| /// Whether the module touches `exports` / `module` outside the recognized | ||
| /// safe forms. When set, `names` may be incomplete and the module must be | ||
| /// treated as opaque CommonJS (no exports dropped). | ||
| pub is_unsafe: bool, |
There was a problem hiding this comment.
has_dynamic_exports would be a better name
| // Only side-effect-free values are droppable: removing the statement | ||
| // removes the evaluation of its RHS, so an impure RHS must be kept. | ||
| if !assign.right.may_have_side_effects(expr_ctx) { |
There was a problem hiding this comment.
i don't think we need this condition as long as our rewrite preserves the LHS
we can let SWC decide the expression is deletable
| /// expression) — sets `is_unsafe`. | ||
| /// | ||
| /// TODO (@sampoder): handle `Object.defineProperty(exports, "__esModule", { value: true })` | ||
| pub fn analyze_cjs_exports(program: &Program, unresolved_mark: Mark) -> CommonJsExportsAnalysis { |
There was a problem hiding this comment.
the natural place would be do it as part of the main Analyzer and then use the Effects system to report a droppable effect for every export
or really follow the pattern we have for ESM items: see add_esm_module_item
| self.fn_depth -= 1; | ||
| } | ||
|
|
||
| fn visit_this_expr(&mut self, _: &swc_core::ecma::ast::ThisExpr) { |
There was a problem hiding this comment.
this should be a reference to a this that is not rebound by another function
This PR introduces two new things: a pass of the analyzer that recognises exports from CJS modules (in the form of
exports.foo, seeanalyze_cjs_exports) and then a CodeGen that readsexport_usage_infoto remove CJS exports that are not used.This PR does not add anything additionally to
export_usage_info, so this means that we only trim from modules that are imported withimport { foo } from "bar". The CodeGen depends on the output ofanalyze_cjs_exportsfor the locations of exports, whether it is safe to drop exports (its pretty conservative), and also whether__esModuleis present.