Skip to main content

turbopack_ecmascript/chunk/
factory_group.rs

1use std::io::Write;
2
3use anyhow::Result;
4use turbopack_core::code_builder::CodeBuilder;
5
6use crate::{chunk::CodeModuleIdAndPath, utils::StringifyJs};
7
8const MIXED_ARROW_PREFIX: &str = "\n(()=>{\"use strict\";return[";
9const MIXED_FUNCTION_PREFIX: &str = "\n(function(){\"use strict\";return[";
10const MIXED_SUFFIX: &str = "\n]})(),";
11const ALL_STRICT_ARROW_PREFIX: &str = "(()=>{\"use strict\";";
12const ALL_STRICT_FUNCTION_PREFIX: &str = "(function(){\"use strict\";";
13const ALL_STRICT_SUFFIX: &str = "})()";
14
15/// Minimum number of strict factories that makes hoisting the directive worth its wrapper.
16/// Derived from the minified wrapper sizes and pinned by `strict_thresholds_match_wrapper_sizes`.
17/// The mixed wrappers differ in length but land on the same minimum; the all-strict ones do not,
18/// which is why arrow support still selects between two constants here.
19const MIXED_STRICT_THRESHOLD: usize = 3;
20const ALL_STRICT_ARROW_THRESHOLD: usize = 2;
21const ALL_STRICT_FUNCTION_THRESHOLD: usize = 3;
22
23#[derive(Clone, Copy, PartialEq, Eq)]
24pub enum StrictFactoryMode {
25    Flat,
26    Mixed,
27    AllStrict,
28}
29
30/// Selects the smallest expected minified representation for this chunk's mix of module factories.
31pub fn strict_factory_mode(
32    chunk_items: &[CodeModuleIdAndPath],
33    supports_arrow_functions: bool,
34) -> StrictFactoryMode {
35    let mut strict = 0;
36    let mut non_strict = 0;
37    for item in chunk_items {
38        if item.strict {
39            strict += 1;
40        } else {
41            non_strict += 1;
42        }
43    }
44    if strict == 0 {
45        return StrictFactoryMode::Flat;
46    }
47
48    let mode = if non_strict == 0 {
49        StrictFactoryMode::AllStrict
50    } else {
51        StrictFactoryMode::Mixed
52    };
53    let threshold = match (mode, supports_arrow_functions) {
54        (StrictFactoryMode::Mixed, _) => MIXED_STRICT_THRESHOLD,
55        (StrictFactoryMode::AllStrict, true) => ALL_STRICT_ARROW_THRESHOLD,
56        (StrictFactoryMode::AllStrict, false) => ALL_STRICT_FUNCTION_THRESHOLD,
57        (StrictFactoryMode::Flat, _) => unreachable!(),
58    };
59    if strict >= threshold {
60        mode
61    } else {
62        StrictFactoryMode::Flat
63    }
64}
65
66/// Code written around the whole chunk in the all-strict case.
67pub fn strict_chunk_wrapper(
68    mode: StrictFactoryMode,
69    supports_arrow_functions: bool,
70) -> Option<(&'static str, &'static str)> {
71    (mode == StrictFactoryMode::AllStrict).then(|| {
72        (
73            all_strict_prefix(supports_arrow_functions),
74            ALL_STRICT_SUFFIX,
75        )
76    })
77}
78
79const fn mixed_prefix(supports_arrow_functions: bool) -> &'static str {
80    if supports_arrow_functions {
81        MIXED_ARROW_PREFIX
82    } else {
83        MIXED_FUNCTION_PREFIX
84    }
85}
86
87const fn all_strict_prefix(supports_arrow_functions: bool) -> &'static str {
88    if supports_arrow_functions {
89        ALL_STRICT_ARROW_PREFIX
90    } else {
91        ALL_STRICT_FUNCTION_PREFIX
92    }
93}
94
95/// Writes flat factories, with a strict IIFE prepended to the non-strict factories in mixed chunks.
96pub fn write_module_factories(
97    code: &mut CodeBuilder,
98    chunk_items: &[CodeModuleIdAndPath],
99    mode: StrictFactoryMode,
100    supports_arrow_functions: bool,
101) -> Result<()> {
102    if mode == StrictFactoryMode::Mixed {
103        *code += mixed_prefix(supports_arrow_functions);
104        write_factories(code, chunk_items, |strict| strict)?;
105        *code += MIXED_SUFFIX;
106        write_factories(code, chunk_items, |strict| !strict)?;
107    } else {
108        write_factories(code, chunk_items, |_| true)?;
109    }
110    Ok(())
111}
112
113fn write_factories(
114    code: &mut CodeBuilder,
115    chunk_items: &[CodeModuleIdAndPath],
116    include: impl Fn(bool) -> bool,
117) -> Result<()> {
118    for item in chunk_items {
119        if include(item.strict) {
120            write!(code, "\n{}, ", StringifyJs(&item.id))?;
121            code.push_code(&item.code);
122            write!(code, ",")?;
123        }
124    }
125    Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    /// Minified length of the `"use strict";` directive that
133    /// `EcmascriptChunkItemContent::module_factory` writes into every strict factory, and that
134    /// the minimizer removes again inside a strict wrapper. The trailing newlines the factory
135    /// writes do not survive minification, so they are not counted. Kept here because only these
136    /// assertions need it.
137    const STRICT_MODE_DIRECTIVE_LEN: usize = "\"use strict\";".len();
138
139    fn minified_len(wrapper: &str) -> usize {
140        // Formatting whitespace is only added at line boundaries. Trim each line rather than all
141        // whitespace so the space inside the `"use strict"` string literal is preserved.
142        wrapper.lines().map(str::trim).map(str::len).sum()
143    }
144
145    /// `threshold` must be the smallest factory count worth grouping: one below it the directives
146    /// do not outweigh the wrapper, at the threshold they do.
147    fn assert_threshold(threshold: usize, wrapper: &str) {
148        let wrapper_len = minified_len(wrapper);
149        assert!((threshold - 1) * STRICT_MODE_DIRECTIVE_LEN <= wrapper_len);
150        assert!(threshold * STRICT_MODE_DIRECTIVE_LEN > wrapper_len);
151    }
152
153    #[test]
154    fn strict_thresholds_match_wrapper_sizes() {
155        assert_threshold(
156            MIXED_STRICT_THRESHOLD,
157            &format!("{MIXED_ARROW_PREFIX}{MIXED_SUFFIX}"),
158        );
159        assert_threshold(
160            MIXED_STRICT_THRESHOLD,
161            &format!("{MIXED_FUNCTION_PREFIX}{MIXED_SUFFIX}"),
162        );
163        assert_threshold(
164            ALL_STRICT_ARROW_THRESHOLD,
165            &format!("{ALL_STRICT_ARROW_PREFIX}{ALL_STRICT_SUFFIX}"),
166        );
167        assert_threshold(
168            ALL_STRICT_FUNCTION_THRESHOLD,
169            &format!("{ALL_STRICT_FUNCTION_PREFIX}{ALL_STRICT_SUFFIX}"),
170        );
171    }
172}