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