Skip to main content

turbopack_ecmascript/webpack/
parse.rs

1use std::borrow::Cow;
2
3use anyhow::Result;
4use swc_core::{
5    common::GLOBALS,
6    ecma::{
7        ast::{
8            ArrowExpr, AssignOp, AssignTarget, BinExpr, BinaryOp, CallExpr, Callee, Expr,
9            ExprOrSpread, ExprStmt, FnExpr, Lit, Module, ModuleItem, Program, Script,
10            SimpleAssignTarget, Stmt,
11        },
12        visit::{Visit, VisitWith},
13    },
14};
15use turbo_rcstr::rcstr;
16use turbo_tasks::Vc;
17use turbo_tasks_fs::FileSystemPath;
18use turbopack_core::{compile_time_info::CompileTimeInfo, source::Source};
19
20use crate::{
21    EcmascriptInputTransforms, EcmascriptModuleAssetType,
22    analyzer::{JsValue, graph::EvalContext},
23    parse::{ParseResult, parse},
24    utils::unparen,
25};
26
27#[turbo_tasks::value(shared, serialization = "skip")]
28#[derive(Debug)]
29pub enum WebpackRuntime {
30    Webpack5 {
31        /// There is a [JsValue]::FreeVar("chunkId") that need to be replaced
32        /// before converting to string
33        #[turbo_tasks(trace_ignore)]
34        chunk_request_expr: JsValue,
35        context_path: FileSystemPath,
36    },
37    None,
38}
39
40fn iife(stmt: &Stmt) -> Option<&Vec<Stmt>> {
41    if let Stmt::Expr(ExprStmt { expr, .. }) = &stmt
42        && let Expr::Call(CallExpr {
43            callee: Callee::Expr(callee),
44            args,
45            ..
46        }) = unparen(expr)
47    {
48        if !args.is_empty() {
49            return None;
50        }
51        return get_fn_body(callee);
52    }
53    None
54}
55
56fn program_iife(program: &Program) -> Option<&Vec<Stmt>> {
57    match program {
58        Program::Module(Module { body, .. }) => {
59            if let [ModuleItem::Stmt(stmt)] = &body[..] {
60                return iife(stmt);
61            }
62        }
63        Program::Script(Script { body, .. }) => {
64            if let [stmt] = &body[..] {
65                return iife(stmt);
66            }
67        }
68    }
69    None
70}
71
72fn is_webpack_require_decl(stmt: &Stmt) -> bool {
73    if let Some(decl) = stmt.as_decl()
74        && let Some(fn_decl) = decl.as_fn_decl()
75    {
76        return &*fn_decl.ident.sym == "__webpack_require__";
77    }
78    false
79}
80
81fn get_assign_target_identifier(expr: &AssignTarget) -> Option<Cow<'_, str>> {
82    match expr.as_simple()? {
83        SimpleAssignTarget::Ident(ident) => Some(Cow::Borrowed(&*ident.sym)),
84        SimpleAssignTarget::Member(member) => {
85            if let Some(obj_name) = get_expr_identifier(&member.obj)
86                && let Some(ident) = member.prop.as_ident()
87            {
88                return Some(Cow::Owned(obj_name.into_owned() + "." + &*ident.sym));
89            }
90            None
91        }
92        SimpleAssignTarget::Paren(p) => get_expr_identifier(&p.expr),
93
94        _ => None,
95    }
96}
97
98fn get_expr_identifier(expr: &Expr) -> Option<Cow<'_, str>> {
99    let expr = unparen(expr);
100    if let Some(ident) = expr.as_ident() {
101        return Some(Cow::Borrowed(&*ident.sym));
102    }
103    if let Some(member) = expr.as_member()
104        && let Some(ident) = member.prop.as_ident()
105        && let Some(obj_name) = get_expr_identifier(&member.obj)
106    {
107        return Some(Cow::Owned(obj_name.into_owned() + "." + &*ident.sym));
108    }
109    None
110}
111
112fn get_assignment<'a>(stmts: &'a Vec<Stmt>, property: &str) -> Option<&'a Expr> {
113    for stmt in stmts {
114        if let Some(stmts) = iife(stmt)
115            && let Some(result) = get_assignment(stmts, property)
116        {
117            return Some(result);
118        }
119        if let Some(expr_stmt) = stmt.as_expr()
120            && let Some(assign) = unparen(&expr_stmt.expr).as_assign()
121            && assign.op == AssignOp::Assign
122            && let Some(name) = get_assign_target_identifier(&assign.left)
123            && name == property
124        {
125            return Some(unparen(&assign.right));
126        }
127    }
128    None
129}
130
131fn get_fn_body(expr: &Expr) -> Option<&Vec<Stmt>> {
132    let expr = unparen(expr);
133    if let Some(FnExpr { function, .. }) = expr.as_fn_expr()
134        && let Some(body) = &function.body
135    {
136        return Some(&body.stmts);
137    }
138    if let Some(ArrowExpr { body, .. }) = expr.as_arrow()
139        && let Some(block) = body.as_block_stmt()
140    {
141        return Some(&block.stmts);
142    }
143    None
144}
145
146fn get_javascript_chunk_filename(stmts: &Vec<Stmt>, eval_context: &EvalContext) -> Option<JsValue> {
147    if let Some(expr) = get_assignment(stmts, "__webpack_require__.u")
148        && let Some(stmts) = get_fn_body(expr)
149        && let Some(ret) = stmts.iter().find_map(|stmt| stmt.as_return_stmt())
150        && let Some(expr) = &ret.arg
151    {
152        return Some(eval_context.eval(expr));
153    }
154    None
155}
156
157struct RequirePrefixVisitor {
158    result: Option<Lit>,
159}
160
161impl Visit for RequirePrefixVisitor {
162    fn visit_call_expr(&mut self, call: &CallExpr) {
163        if let Some(expr) = call.callee.as_expr()
164            && let Some(name) = get_expr_identifier(expr)
165            && name == "require"
166            && let [ExprOrSpread { spread: None, expr }] = &call.args[..]
167            && let Some(BinExpr {
168                op: BinaryOp::Add,
169                left,
170                ..
171            }) = expr.as_bin()
172        {
173            self.result = left.as_lit().cloned();
174            return;
175        }
176        call.visit_children_with(self);
177    }
178}
179
180fn get_require_prefix(stmts: &Vec<Stmt>) -> Option<Lit> {
181    if let Some(expr) = get_assignment(stmts, "__webpack_require__.f.require") {
182        let mut visitor = RequirePrefixVisitor { result: None };
183        expr.visit_children_with(&mut visitor);
184        return visitor.result;
185    }
186    None
187}
188
189#[turbo_tasks::function]
190pub async fn webpack_runtime(
191    source: Vc<Box<dyn Source>>,
192    transforms: Vc<EcmascriptInputTransforms>,
193    compile_time_info: Vc<CompileTimeInfo>,
194) -> Result<Vc<WebpackRuntime>> {
195    let node_env = compile_time_info
196        .await?
197        .defines
198        .read_process_env(rcstr!("NODE_ENV"))
199        .owned()
200        .await?
201        .unwrap_or_else(|| rcstr!("development"));
202    let parsed = parse(
203        source,
204        EcmascriptModuleAssetType::Ecmascript,
205        transforms,
206        node_env,
207        false,
208        false,
209    )
210    .await?;
211    match &*parsed {
212        ParseResult::Ok {
213            program,
214            eval_context,
215            globals,
216            ..
217        } => {
218            if let Some(stmts) = program_iife(program)
219                && stmts.iter().any(is_webpack_require_decl)
220            {
221                // extract webpack/runtime/get javascript chunk filename
222                let chunk_filename = GLOBALS.set(globals, || {
223                    get_javascript_chunk_filename(stmts, eval_context)
224                });
225
226                let prefix_path = get_require_prefix(stmts);
227
228                if let (Some(chunk_filename), Some(prefix_path)) = (chunk_filename, prefix_path) {
229                    let value = JsValue::concat(vec![
230                        JsValue::Constant(prefix_path.into()),
231                        chunk_filename,
232                    ]);
233
234                    return Ok(WebpackRuntime::Webpack5 {
235                        chunk_request_expr: value,
236                        context_path: source.ident().await?.path.parent(),
237                    }
238                    .cell());
239                }
240            }
241        }
242        ParseResult::Unparsable { .. } | ParseResult::NotFound => {}
243    }
244    Ok(WebpackRuntime::None.cell())
245}