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