turbopack_core/
environment.rs

1use std::{
2    process::{Command, Stdio},
3    str::FromStr,
4};
5
6use anyhow::{Context, Result, anyhow, bail};
7use browserslist::Distrib;
8use swc_core::ecma::preset_env::{Version, Versions};
9use turbo_rcstr::{RcStr, rcstr};
10use turbo_tasks::{ResolvedVc, TaskInput, Vc};
11use turbo_tasks_env::ProcessEnv;
12use turbo_tasks_fs::FileSystemPathOption;
13
14use crate::target::CompileTarget;
15
16static DEFAULT_NODEJS_VERSION: &str = "18.0.0";
17
18#[turbo_tasks::value]
19#[derive(Clone, Copy, Default, Hash, TaskInput, Debug)]
20pub enum Rendering {
21    #[default]
22    None,
23    Client,
24    Server,
25}
26
27impl Rendering {
28    pub fn is_none(&self) -> bool {
29        matches!(self, Rendering::None)
30    }
31}
32
33#[turbo_tasks::value]
34pub enum ChunkLoading {
35    Edge,
36    /// CommonJS in Node.js
37    NodeJs,
38    /// <script> and <link> tags in the browser
39    Dom,
40}
41
42#[turbo_tasks::value]
43pub struct Environment {
44    // members must be private to avoid leaking non-custom types
45    execution: ExecutionEnvironment,
46}
47
48#[turbo_tasks::value_impl]
49impl Environment {
50    #[turbo_tasks::function]
51    pub fn new(execution: ExecutionEnvironment) -> Vc<Self> {
52        Self::cell(Environment { execution })
53    }
54}
55
56#[turbo_tasks::value]
57#[derive(Debug, Hash, Clone, Copy, TaskInput)]
58pub enum ExecutionEnvironment {
59    NodeJsBuildTime(ResolvedVc<NodeJsEnvironment>),
60    NodeJsLambda(ResolvedVc<NodeJsEnvironment>),
61    EdgeWorker(ResolvedVc<EdgeWorkerEnvironment>),
62    Browser(ResolvedVc<BrowserEnvironment>),
63    // TODO allow custom trait here
64    Custom(u8),
65}
66
67async fn resolve_browserslist(browser_env: ResolvedVc<BrowserEnvironment>) -> Result<Vec<Distrib>> {
68    Ok(browserslist::resolve(
69        browser_env.await?.browserslist_query.split(','),
70        &browserslist::Opts {
71            ignore_unknown_versions: true,
72            ..Default::default()
73        },
74    )?)
75}
76
77#[turbo_tasks::value_impl]
78impl Environment {
79    #[turbo_tasks::function]
80    pub async fn compile_target(&self) -> Result<Vc<CompileTarget>> {
81        Ok(match self.execution {
82            ExecutionEnvironment::NodeJsBuildTime(node_env, ..)
83            | ExecutionEnvironment::NodeJsLambda(node_env) => *node_env.await?.compile_target,
84            ExecutionEnvironment::Browser(_) => CompileTarget::unknown(),
85            ExecutionEnvironment::EdgeWorker(_) => CompileTarget::unknown(),
86            ExecutionEnvironment::Custom(_) => todo!(),
87        })
88    }
89
90    #[turbo_tasks::function]
91    pub async fn runtime_versions(&self) -> Result<Vc<RuntimeVersions>> {
92        Ok(match self.execution {
93            ExecutionEnvironment::NodeJsBuildTime(node_env, ..)
94            | ExecutionEnvironment::NodeJsLambda(node_env) => node_env.runtime_versions(),
95            ExecutionEnvironment::Browser(browser_env) => {
96                let distribs = resolve_browserslist(browser_env).await?;
97                Vc::cell(Versions::parse_versions(distribs)?)
98            }
99            ExecutionEnvironment::EdgeWorker(edge_env) => edge_env.runtime_versions(),
100            ExecutionEnvironment::Custom(_) => todo!(),
101        })
102    }
103
104    #[turbo_tasks::function]
105    pub async fn browserslist_query(&self) -> Result<Vc<RcStr>> {
106        Ok(match self.execution {
107            ExecutionEnvironment::NodeJsBuildTime(_)
108            | ExecutionEnvironment::NodeJsLambda(_)
109            | ExecutionEnvironment::EdgeWorker(_) =>
110            // TODO: This is a hack, browserslist_query is only used by CSS processing for
111            // LightningCSS However, there is an issue where the CSS is not transitioned
112            // to the client which we still have to solve. It does apply the
113            // browserslist correctly because CSS Modules in client components is double-processed,
114            // once for server once for browser.
115            {
116                Vc::cell(rcstr!(""))
117            }
118            ExecutionEnvironment::Browser(browser_env) => {
119                Vc::cell(browser_env.await?.browserslist_query.clone())
120            }
121            ExecutionEnvironment::Custom(_) => todo!(),
122        })
123    }
124
125    #[turbo_tasks::function]
126    pub fn node_externals(&self) -> Vc<bool> {
127        match self.execution {
128            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
129                Vc::cell(true)
130            }
131            ExecutionEnvironment::Browser(_) => Vc::cell(false),
132            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(false),
133            ExecutionEnvironment::Custom(_) => todo!(),
134        }
135    }
136
137    #[turbo_tasks::function]
138    pub fn supports_esm_externals(&self) -> Vc<bool> {
139        match self.execution {
140            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
141                Vc::cell(true)
142            }
143            ExecutionEnvironment::Browser(_) => Vc::cell(false),
144            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(false),
145            ExecutionEnvironment::Custom(_) => todo!(),
146        }
147    }
148
149    #[turbo_tasks::function]
150    pub fn supports_commonjs_externals(&self) -> Vc<bool> {
151        match self.execution {
152            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
153                Vc::cell(true)
154            }
155            ExecutionEnvironment::Browser(_) => Vc::cell(false),
156            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(true),
157            ExecutionEnvironment::Custom(_) => todo!(),
158        }
159    }
160
161    #[turbo_tasks::function]
162    pub fn supports_wasm(&self) -> Vc<bool> {
163        match self.execution {
164            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
165                Vc::cell(true)
166            }
167            ExecutionEnvironment::Browser(_) => Vc::cell(false),
168            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(false),
169            ExecutionEnvironment::Custom(_) => todo!(),
170        }
171    }
172
173    #[turbo_tasks::function]
174    pub fn resolve_extensions(&self) -> Vc<Vec<RcStr>> {
175        let env = self;
176        match env.execution {
177            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
178                Vc::cell(vec![rcstr!(".js"), rcstr!(".node"), rcstr!(".json")])
179            }
180            ExecutionEnvironment::EdgeWorker(_) | ExecutionEnvironment::Browser(_) => {
181                Vc::<Vec<RcStr>>::default()
182            }
183            ExecutionEnvironment::Custom(_) => todo!(),
184        }
185    }
186
187    #[turbo_tasks::function]
188    pub fn resolve_node_modules(&self) -> Vc<bool> {
189        let env = self;
190        match env.execution {
191            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
192                Vc::cell(true)
193            }
194            ExecutionEnvironment::EdgeWorker(_) | ExecutionEnvironment::Browser(_) => {
195                Vc::cell(false)
196            }
197            ExecutionEnvironment::Custom(_) => todo!(),
198        }
199    }
200
201    #[turbo_tasks::function]
202    pub fn resolve_conditions(&self) -> Vc<Vec<RcStr>> {
203        let env = self;
204        match env.execution {
205            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
206                Vc::cell(vec![rcstr!("node")])
207            }
208            ExecutionEnvironment::Browser(_) => Vc::<Vec<RcStr>>::default(),
209            ExecutionEnvironment::EdgeWorker(_) => {
210                Vc::cell(vec![rcstr!("edge-light"), rcstr!("worker")])
211            }
212            ExecutionEnvironment::Custom(_) => todo!(),
213        }
214    }
215
216    #[turbo_tasks::function]
217    pub async fn cwd(&self) -> Result<Vc<FileSystemPathOption>> {
218        let env = self;
219        Ok(match env.execution {
220            ExecutionEnvironment::NodeJsBuildTime(env)
221            | ExecutionEnvironment::NodeJsLambda(env) => *env.await?.cwd,
222            _ => Vc::cell(None),
223        })
224    }
225
226    #[turbo_tasks::function]
227    pub fn rendering(&self) -> Vc<Rendering> {
228        let env = self;
229        match env.execution {
230            ExecutionEnvironment::NodeJsBuildTime(_) | ExecutionEnvironment::NodeJsLambda(_) => {
231                Rendering::Server.cell()
232            }
233            ExecutionEnvironment::EdgeWorker(_) => Rendering::Server.cell(),
234            ExecutionEnvironment::Browser(_) => Rendering::Client.cell(),
235            _ => Rendering::None.cell(),
236        }
237    }
238
239    #[turbo_tasks::function]
240    pub fn chunk_loading(&self) -> Vc<ChunkLoading> {
241        let env = self;
242        match env.execution {
243            ExecutionEnvironment::NodeJsBuildTime(_) | ExecutionEnvironment::NodeJsLambda(_) => {
244                ChunkLoading::NodeJs.cell()
245            }
246            ExecutionEnvironment::EdgeWorker(_) => ChunkLoading::Edge.cell(),
247            ExecutionEnvironment::Browser(_) => ChunkLoading::Dom.cell(),
248            ExecutionEnvironment::Custom(_) => todo!(),
249        }
250    }
251}
252
253pub enum NodeEnvironmentType {
254    Server,
255}
256
257#[turbo_tasks::value(shared)]
258pub struct NodeJsEnvironment {
259    pub compile_target: ResolvedVc<CompileTarget>,
260    pub node_version: ResolvedVc<NodeJsVersion>,
261    // user specified process.cwd
262    pub cwd: ResolvedVc<FileSystemPathOption>,
263}
264
265impl Default for NodeJsEnvironment {
266    fn default() -> Self {
267        NodeJsEnvironment {
268            compile_target: CompileTarget::current_raw().resolved_cell(),
269            node_version: NodeJsVersion::default().resolved_cell(),
270            cwd: ResolvedVc::cell(None),
271        }
272    }
273}
274
275#[turbo_tasks::value_impl]
276impl NodeJsEnvironment {
277    #[turbo_tasks::function]
278    pub async fn runtime_versions(&self) -> Result<Vc<RuntimeVersions>> {
279        let str = match *self.node_version.await? {
280            NodeJsVersion::Current(process_env) => get_current_nodejs_version(*process_env),
281            NodeJsVersion::Static(version) => *version,
282        }
283        .await?;
284
285        Ok(Vc::cell(Versions {
286            node: Some(
287                Version::from_str(&str)
288                    .map_err(|_| anyhow!("Failed to parse Node.js version: '{}'", str))?,
289            ),
290            ..Default::default()
291        }))
292    }
293
294    #[turbo_tasks::function]
295    pub async fn current(process_env: ResolvedVc<Box<dyn ProcessEnv>>) -> Result<Vc<Self>> {
296        Ok(Self::cell(NodeJsEnvironment {
297            compile_target: CompileTarget::current().to_resolved().await?,
298            node_version: NodeJsVersion::cell(NodeJsVersion::Current(process_env))
299                .to_resolved()
300                .await?,
301            cwd: ResolvedVc::cell(None),
302        }))
303    }
304}
305
306#[turbo_tasks::value(shared)]
307pub enum NodeJsVersion {
308    /// Use the version of Node.js that is available from the environment (via `node --version`)
309    Current(ResolvedVc<Box<dyn ProcessEnv>>),
310    /// Use the specified version of Node.js.
311    Static(ResolvedVc<RcStr>),
312}
313
314impl Default for NodeJsVersion {
315    fn default() -> Self {
316        NodeJsVersion::Static(ResolvedVc::cell(DEFAULT_NODEJS_VERSION.into()))
317    }
318}
319
320#[turbo_tasks::value(shared)]
321pub struct BrowserEnvironment {
322    pub dom: bool,
323    pub web_worker: bool,
324    pub service_worker: bool,
325    pub browserslist_query: RcStr,
326}
327
328#[turbo_tasks::value(shared)]
329pub struct EdgeWorkerEnvironment {
330    // This isn't actually the Edge's worker environment, but we have to use some kind of version
331    // for transpiling ECMAScript features. No tool supports Edge Workers as a separate
332    // environment.
333    pub node_version: ResolvedVc<NodeJsVersion>,
334}
335
336#[turbo_tasks::value_impl]
337impl EdgeWorkerEnvironment {
338    #[turbo_tasks::function]
339    pub async fn runtime_versions(&self) -> Result<Vc<RuntimeVersions>> {
340        let str = match *self.node_version.await? {
341            NodeJsVersion::Current(process_env) => get_current_nodejs_version(*process_env),
342            NodeJsVersion::Static(version) => *version,
343        }
344        .await?;
345
346        Ok(Vc::cell(Versions {
347            node: Some(
348                Version::from_str(&str).map_err(|_| anyhow!("Node.js version parse error"))?,
349            ),
350            ..Default::default()
351        }))
352    }
353}
354
355// TODO preset_env_base::Version implements Serialize/Deserialize incorrectly
356#[turbo_tasks::value(transparent, serialization = "none")]
357pub struct RuntimeVersions(#[turbo_tasks(trace_ignore)] pub Versions);
358
359#[turbo_tasks::function]
360pub async fn get_current_nodejs_version(env: Vc<Box<dyn ProcessEnv>>) -> Result<Vc<RcStr>> {
361    let path_read = env.read(rcstr!("PATH")).await?;
362    let path = path_read.as_ref().context("env must have PATH")?;
363    let mut cmd = Command::new("node");
364    cmd.arg("--version");
365    cmd.env_clear();
366    cmd.env("PATH", path);
367    cmd.stdin(Stdio::piped());
368    cmd.stdout(Stdio::piped());
369
370    let output = cmd.output()?;
371
372    if !output.status.success() {
373        bail!(
374            "'node --version' command failed{}{}",
375            output
376                .status
377                .code()
378                .map(|c| format!(" with exit code {c}"))
379                .unwrap_or_default(),
380            String::from_utf8(output.stderr)
381                .map(|stderr| format!(": {stderr}"))
382                .unwrap_or_default()
383        );
384    }
385
386    let version = String::from_utf8(output.stdout)
387        .context("failed to parse 'node --version' output as utf8")?;
388    if let Some(version_number) = version.strip_prefix("v") {
389        Ok(Vc::cell(version_number.trim().into()))
390    } else {
391        bail!(
392            "Expected 'node --version' to return a version starting with 'v', but received: '{}'",
393            version
394        )
395    }
396}