turbopack_ecmascript/webpack/
parse.rs1use 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 #[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 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}