vercel/next.js · #100477
Turbopack: keep IIFEs with a PURE-annotated callee in production builds
.prettierignore2 + / 0 −
@@ -74,6 +74,8 @@ test/development/mcp-server/fixtures/compilation-errors-app/app/syntax-error/pag /turbopack/crates/turbopack-tests/tests/snapshot/css/css-parse-error/input/style.css /test/development/app-dir/css-parse-error-recovery/app/css-error/style.css /turbopack/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs+# prettier removes the parentheses that scope the PURE annotation to the inner call+/turbopack/crates/turbopack-tests/tests/execution/turbopack/minification/pure-annotated-callee*/input/index.js /turbopack/crates/turbopack-tests/tests/snapshot/source_maps/* /turbopack/crates/turbopack-tests/tests/**/output* /turbopack/crates/turbopack-tests/tests/**/staticturbopack/crates/turbopack-ecmascript/src/lib.rs16 + / 2 −
@@ -130,7 +130,7 @@ use crate::{ exports::compute_ecmascript_module_exports, }, side_effect_optimization::reference::EcmascriptModulePartReference,- swc_comments::{CowComments, ImmutableComments},+ swc_comments::{CowComments, ImmutableComments, ReadOnlyComments}, transform::{remove_directives, remove_shebang}, }; pub use crate::{@@ -2161,7 +2161,21 @@ async fn process_parse_result( ), ); }- program.visit_mut_with(&mut swc_core::ecma::transforms::base::fixer::fixer(None));+ // The fixer needs to see the comments: without them it drops the parentheses in+ // `(/* @__PURE__ */ f())()`, which moves the PURE annotation onto the outer call+ // when the code is printed (and re-parsed by the minifier), so the whole call+ // would be removed as side-effect free.+ {+ let parsed_comments: &ImmutableComments = match &comments {+ Either::Left(c) => c,+ Either::Right(c) => c,+ };+ let stores: [&dyn Comments; 2] = [parsed_comments, &extra_comments];+ let fixer_comments = ReadOnlyComments::new(&stores);+ program.visit_mut_with(&mut swc_core::ecma::transforms::base::fixer::fixer(+ Some(&fixer_comments),+ ));+ } // we need to remove any shebang before bundling as it's only valid as the first // line in a js file (not in a chunk item wrapped in the runtime)turbopack/crates/turbopack-ecmascript/src/swc_comments.rs122 + / 22 −
@@ -213,28 +213,7 @@ impl Comments for ImmutableComments { } fn has_flag(&self, pos: BytePos, flag: &str) -> bool {- self.with_leading(pos, |cmts| {- for c in cmts {- if c.kind == CommentKind::Block {- for line in c.text.lines() {- // jsdoc- let line = line.trim_start_matches(['*', ' ']);- let line = line.trim();-- //- if line.len() == (flag.len() + 5)- && (line.starts_with("#__") || line.starts_with("@__"))- && line.ends_with("__")- && flag == &line[3..line.len() - 2]- {- return true;- }- }- }- }-- false- })+ self.with_leading(pos, |cmts| comments_have_flag(cmts, flag)) } fn with_leading<F, Ret>(&self, pos: BytePos, f: F) -> Ret@@ -266,6 +245,127 @@ impl Comments for ImmutableComments { } } +/// Checks whether `comments` contain a `@__FLAG__` / `#__FLAG__` block comment, using the same+/// matching rules as SWC's default [Comments::has_flag], but without mutating anything.+fn comments_have_flag(comments: &[Comment], flag: &str) -> bool {+ for c in comments {+ if c.kind == CommentKind::Block {+ for line in c.text.lines() {+ // jsdoc+ let line = line.trim_start_matches(['*', ' ']);+ let line = line.trim();++ //+ if line.len() == (flag.len() + 5)+ && (line.starts_with("#__") || line.starts_with("@__"))+ && line.ends_with("__")+ && flag == &line[3..line.len() - 2]+ {+ return true;+ }+ }+ }+ }++ false+}++/// A read-only [Comments] view over several comment stores.+///+/// Some SWC transforms (e.g. the fixer) need to *read* comments to make correct decisions: the+/// fixer keeps the parentheses around a `/* @__PURE__ */`-annotated callee, which would otherwise+/// move the annotation onto the outer call. The comment stores at that point must not be changed+/// ([ImmutableComments] can't be), so this adapter:+///+/// - answers `has_*` / `get_*` / `has_flag` by looking at every store and concatenating the results+/// (`has_flag` is overridden because SWC's default implementation uses `take_leading` followed by+/// `add_trailing_comments`),+/// - ignores `move_*`: the fixer calls them for every pair of parentheses it removes, to move the+/// comments from the paren span to the inner expression. Ignoring them keeps the comments at+/// their original positions, which is what happened before the fixer saw any comments,+/// - panics on every other mutation (`add_*`, `take_*`, `add_pure_comment`) to make sure it is+/// never used where comments actually need to be changed.+pub struct ReadOnlyComments<'a> {+ stores: &'a [&'a dyn Comments],+}++impl<'a> ReadOnlyComments<'a> {+ pub fn new(stores: &'a [&'a dyn Comments]) -> Self {+ Self { stores }+ }++ fn collect(&self, f: impl Fn(&dyn Comments) -> Option<Vec<Comment>>) -> Option<Vec<Comment>> {+ let mut result: Option<Vec<Comment>> = None;+ for store in self.stores {+ if let Some(c) = f(*store) {+ result.get_or_insert_with(Vec::new).extend(c);+ }+ }+ result+ }+}++impl Comments for ReadOnlyComments<'_> {+ fn add_leading(&self, _pos: BytePos, _cmt: Comment) {+ panic!("ReadOnlyComments can't be mutated")+ }++ fn add_leading_comments(&self, _pos: BytePos, _comments: Vec<Comment>) {+ panic!("ReadOnlyComments can't be mutated")+ }++ fn has_leading(&self, pos: BytePos) -> bool {+ self.stores.iter().any(|s| s.has_leading(pos))+ }++ fn move_leading(&self, _from: BytePos, _to: BytePos) {+ // Intentionally ignored, see the type's documentation.+ }++ fn take_leading(&self, _pos: BytePos) -> Option<Vec<Comment>> {+ panic!("ReadOnlyComments can't be mutated")+ }++ fn get_leading(&self, pos: BytePos) -> Option<Vec<Comment>> {+ self.collect(|s| s.get_leading(pos))+ }++ fn add_trailing(&self, _pos: BytePos, _cmt: Comment) {+ panic!("ReadOnlyComments can't be mutated")+ }++ fn add_trailing_comments(&self, _pos: BytePos, _comments: Vec<Comment>) {+ panic!("ReadOnlyComments can't be mutated")+ }++ fn has_trailing(&self, pos: BytePos) -> bool {+ self.stores.iter().any(|s| s.has_trailing(pos))+ }++ fn move_trailing(&self, _from: BytePos, _to: BytePos) {+ // Intentionally ignored, see the type's documentation.+ }++ fn take_trailing(&self, _pos: BytePos) -> Option<Vec<Comment>> {+ panic!("ReadOnlyComments can't be mutated")+ }++ fn get_trailing(&self, pos: BytePos) -> Option<Vec<Comment>> {+ self.collect(|s| s.get_trailing(pos))+ }++ fn add_pure_comment(&self, _pos: BytePos) {+ panic!("ReadOnlyComments can't be mutated")+ }++ fn has_flag(&self, pos: BytePos, flag: &str) -> bool {+ self.stores.iter().any(|s| {+ s.get_leading(pos)+ .is_some_and(|cmts| comments_have_flag(&cmts, flag))+ })+ }+}+ pub struct CowComments<'a> { leading: RefCell<FxHashMap<BytePos, Cow<'a, [Comment]>>>, trailing: RefCell<FxHashMap<BytePos, Cow<'a, [Comment]>>>,turbopack/crates/turbopack-tests/tests/execution/turbopack/minification/pure-annotated-callee-no-scope-hoisting/input/index.jsadded22 + / 0 −
@@ -0,0 +1,22 @@+import { __name } from './name.js'++const calls = []++function make() {+ return {+ run() {+ calls.push('member')+ },+ }+}++// The PURE annotations below apply only to the inner, parenthesized calls. The+// outer calls have side effects and must not be removed by the minifier.+;(/* @__PURE__ */ __name(function annotated() {+ calls.push('iife')+}, 'annotated'))()+;(/* @__PURE__ */ make()).run()++it('should keep calls whose callee is a PURE-annotated call', () => {+ expect(calls).toEqual(['iife', 'member'])+})turbopack/crates/turbopack-tests/tests/execution/turbopack/minification/pure-annotated-callee-no-scope-hoisting/input/name.jsadded5 + / 0 −
@@ -0,0 +1,5 @@+// esbuild's `keepNames` helpers.+var __defProp = Object.defineProperty++export var __name = (target, value) =>+ __defProp(target, 'name', { value, configurable: true })turbopack/crates/turbopack-tests/tests/execution/turbopack/minification/pure-annotated-callee-no-scope-hoisting/options.jsonadded4 + / 0 −
@@ -0,0 +1,4 @@+{+ "minify": true,+ "scopeHoisting": false+}turbopack/crates/turbopack-tests/tests/execution/turbopack/minification/pure-annotated-callee/input/index.jsadded22 + / 0 −
@@ -0,0 +1,22 @@+import { __name } from './name.js'++const calls = []++function make() {+ return {+ run() {+ calls.push('member')+ },+ }+}++// The PURE annotations below apply only to the inner, parenthesized calls. The+// outer calls have side effects and must not be removed by the minifier.+;(/* @__PURE__ */ __name(function annotated() {+ calls.push('iife')+}, 'annotated'))()+;(/* @__PURE__ */ make()).run()++it('should keep calls whose callee is a PURE-annotated call', () => {+ expect(calls).toEqual(['iife', 'member'])+})turbopack/crates/turbopack-tests/tests/execution/turbopack/minification/pure-annotated-callee/input/name.jsadded5 + / 0 −
@@ -0,0 +1,5 @@+// esbuild's `keepNames` helpers.+var __defProp = Object.defineProperty++export var __name = (target, value) =>+ __defProp(target, 'name', { value, configurable: true })turbopack/crates/turbopack-tests/tests/execution/turbopack/minification/pure-annotated-callee/options.jsonadded3 + / 0 −
@@ -0,0 +1,3 @@+{+ "minify": true+}