Skip to main content

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, 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, 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(shared)]
34pub enum ChunkLoading {
35    Edge,
36    /// CommonJS in Node.js
37    NodeJs,
38    /// `<script>` and `<link>` tags in the browser
39    Dom,
40    /// Everything inlined into one entry chunk
41    SingleChunk,
42}
43
44impl ChunkLoading {
45    pub fn can_split_async(&self) -> bool {
46        matches!(self, ChunkLoading::NodeJs | ChunkLoading::Dom)
47    }
48}
49
50#[turbo_tasks::value]
51pub struct Environment {
52    // members must be private to avoid leaking non-custom types
53    execution: ExecutionEnvironment,
54}
55
56#[turbo_tasks::value_impl]
57impl Environment {
58    #[turbo_tasks::function]
59    pub fn new(execution: ExecutionEnvironment) -> Vc<Self> {
60        Self::cell(Environment { execution })
61    }
62}
63
64#[turbo_tasks::value(task_input)]
65#[derive(Debug, Hash, Clone, Copy)]
66pub enum ExecutionEnvironment {
67    NodeJsBuildTime(ResolvedVc<NodeJsEnvironment>),
68    NodeJsLambda(ResolvedVc<NodeJsEnvironment>),
69    EdgeWorker(ResolvedVc<EdgeWorkerEnvironment>),
70    Browser(ResolvedVc<BrowserEnvironment>),
71    // TODO allow custom trait here
72    Custom(u8),
73}
74
75async fn resolve_browserslist(browser_env: ResolvedVc<BrowserEnvironment>) -> Result<Vec<Distrib>> {
76    Ok(browserslist::resolve(
77        browser_env.await?.browserslist_query.split(','),
78        &browserslist::Opts {
79            ignore_unknown_versions: true,
80            ..Default::default()
81        },
82    )?)
83}
84
85#[turbo_tasks::value_impl]
86impl Environment {
87    #[turbo_tasks::function]
88    pub async fn compile_target(&self) -> Result<Vc<CompileTarget>> {
89        Ok(match self.execution {
90            ExecutionEnvironment::NodeJsBuildTime(node_env, ..)
91            | ExecutionEnvironment::NodeJsLambda(node_env) => *node_env.await?.compile_target,
92            ExecutionEnvironment::Browser(_) => CompileTarget::unknown(),
93            ExecutionEnvironment::EdgeWorker(_) => CompileTarget::unknown(),
94            ExecutionEnvironment::Custom(_) => todo!(),
95        })
96    }
97
98    #[turbo_tasks::function]
99    pub async fn runtime_versions(&self) -> Result<Vc<RuntimeVersions>> {
100        Ok(match self.execution {
101            ExecutionEnvironment::NodeJsBuildTime(node_env, ..)
102            | ExecutionEnvironment::NodeJsLambda(node_env) => node_env.runtime_versions(),
103            ExecutionEnvironment::Browser(browser_env) => {
104                let distribs = resolve_browserslist(browser_env).await?;
105                Vc::cell(Versions::parse_versions(distribs)?)
106            }
107            ExecutionEnvironment::EdgeWorker(edge_env) => edge_env.runtime_versions(),
108            ExecutionEnvironment::Custom(_) => todo!(),
109        })
110    }
111
112    #[turbo_tasks::function]
113    pub async fn browserslist_query(&self) -> Result<Vc<RcStr>> {
114        Ok(match self.execution {
115            ExecutionEnvironment::NodeJsBuildTime(_)
116            | ExecutionEnvironment::NodeJsLambda(_)
117            | ExecutionEnvironment::EdgeWorker(_) =>
118            // TODO: This is a hack, browserslist_query is only used by CSS processing for
119            // LightningCSS However, there is an issue where the CSS is not transitioned
120            // to the client which we still have to solve. It does apply the
121            // browserslist correctly because CSS Modules in client components is double-processed,
122            // once for server once for browser.
123            {
124                Vc::default()
125            }
126            ExecutionEnvironment::Browser(browser_env) => {
127                Vc::cell(browser_env.await?.browserslist_query.clone())
128            }
129            ExecutionEnvironment::Custom(_) => todo!(),
130        })
131    }
132
133    #[turbo_tasks::function]
134    pub fn node_externals(&self) -> Vc<bool> {
135        match self.execution {
136            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
137                Vc::cell(true)
138            }
139            ExecutionEnvironment::Browser(_) => Vc::cell(false),
140            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(false),
141            ExecutionEnvironment::Custom(_) => todo!(),
142        }
143    }
144
145    #[turbo_tasks::function]
146    pub fn supports_esm_externals(&self) -> Vc<bool> {
147        match self.execution {
148            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
149                Vc::cell(true)
150            }
151            ExecutionEnvironment::Browser(_) => Vc::cell(false),
152            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(false),
153            ExecutionEnvironment::Custom(_) => todo!(),
154        }
155    }
156
157    #[turbo_tasks::function]
158    pub fn supports_commonjs_externals(&self) -> Vc<bool> {
159        match self.execution {
160            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
161                Vc::cell(true)
162            }
163            ExecutionEnvironment::Browser(_) => Vc::cell(false),
164            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(true),
165            ExecutionEnvironment::Custom(_) => todo!(),
166        }
167    }
168
169    #[turbo_tasks::function]
170    pub fn supports_wasm(&self) -> Vc<bool> {
171        match self.execution {
172            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
173                Vc::cell(true)
174            }
175            ExecutionEnvironment::Browser(_) => Vc::cell(false),
176            ExecutionEnvironment::EdgeWorker(_) => Vc::cell(false),
177            ExecutionEnvironment::Custom(_) => todo!(),
178        }
179    }
180
181    #[turbo_tasks::function]
182    pub fn resolve_extensions(&self) -> Vc<Vec<RcStr>> {
183        let env = self;
184        match env.execution {
185            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
186                Vc::cell(vec![rcstr!(".js"), rcstr!(".node"), rcstr!(".json")])
187            }
188            ExecutionEnvironment::EdgeWorker(_) | ExecutionEnvironment::Browser(_) => {
189                Vc::<Vec<RcStr>>::default()
190            }
191            ExecutionEnvironment::Custom(_) => todo!(),
192        }
193    }
194
195    #[turbo_tasks::function]
196    pub fn resolve_node_modules(&self) -> Vc<bool> {
197        let env = self;
198        match env.execution {
199            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
200                Vc::cell(true)
201            }
202            ExecutionEnvironment::EdgeWorker(_) | ExecutionEnvironment::Browser(_) => {
203                Vc::cell(false)
204            }
205            ExecutionEnvironment::Custom(_) => todo!(),
206        }
207    }
208
209    #[turbo_tasks::function]
210    pub fn resolve_conditions(&self) -> Vc<Vec<RcStr>> {
211        let env = self;
212        match env.execution {
213            ExecutionEnvironment::NodeJsBuildTime(..) | ExecutionEnvironment::NodeJsLambda(_) => {
214                Vc::cell(vec![rcstr!("node")])
215            }
216            ExecutionEnvironment::Browser(_) => Vc::<Vec<RcStr>>::default(),
217            ExecutionEnvironment::EdgeWorker(_) => {
218                Vc::cell(vec![rcstr!("edge-light"), rcstr!("worker")])
219            }
220            ExecutionEnvironment::Custom(_) => todo!(),
221        }
222    }
223
224    #[turbo_tasks::function]
225    pub async fn cwd(&self) -> Result<Vc<FileSystemPathOption>> {
226        let env = self;
227        Ok(match env.execution {
228            ExecutionEnvironment::NodeJsBuildTime(env)
229            | ExecutionEnvironment::NodeJsLambda(env) => *env.await?.cwd,
230            _ => Vc::cell(None),
231        })
232    }
233
234    #[turbo_tasks::function]
235    pub fn rendering(&self) -> Vc<Rendering> {
236        let env = self;
237        match env.execution {
238            ExecutionEnvironment::NodeJsBuildTime(_) | ExecutionEnvironment::NodeJsLambda(_) => {
239                Rendering::Server.cell()
240            }
241            ExecutionEnvironment::EdgeWorker(_) => Rendering::Server.cell(),
242            ExecutionEnvironment::Browser(_) => Rendering::Client.cell(),
243            _ => Rendering::None.cell(),
244        }
245    }
246
247    #[turbo_tasks::function]
248    pub fn chunk_loading(&self) -> Vc<ChunkLoading> {
249        let env = self;
250        match env.execution {
251            ExecutionEnvironment::NodeJsBuildTime(_) | ExecutionEnvironment::NodeJsLambda(_) => {
252                ChunkLoading::NodeJs.cell()
253            }
254            ExecutionEnvironment::EdgeWorker(_) => ChunkLoading::Edge.cell(),
255            ExecutionEnvironment::Browser(_) => ChunkLoading::Dom.cell(),
256            ExecutionEnvironment::Custom(_) => todo!(),
257        }
258    }
259}
260
261pub enum NodeEnvironmentType {
262    Server,
263}
264
265#[turbo_tasks::value(shared)]
266pub struct NodeJsEnvironment {
267    pub compile_target: ResolvedVc<CompileTarget>,
268    pub node_version: ResolvedVc<NodeJsVersion>,
269    // user specified process.cwd
270    pub cwd: ResolvedVc<FileSystemPathOption>,
271}
272
273impl Default for NodeJsEnvironment {
274    fn default() -> Self {
275        NodeJsEnvironment {
276            compile_target: CompileTarget::current_raw().resolved_cell(),
277            node_version: NodeJsVersion::default().resolved_cell(),
278            cwd: ResolvedVc::cell(None),
279        }
280    }
281}
282
283#[turbo_tasks::value_impl]
284impl NodeJsEnvironment {
285    #[turbo_tasks::function]
286    pub async fn runtime_versions(&self) -> Result<Vc<RuntimeVersions>> {
287        let str = match *self.node_version.await? {
288            NodeJsVersion::Current(process_env) => get_current_nodejs_version(*process_env),
289            NodeJsVersion::Static(version) => *version,
290        }
291        .await?;
292
293        Ok(Vc::cell(Versions {
294            node: Some(
295                Version::from_str(&str)
296                    .map_err(|_| anyhow!("Failed to parse Node.js version: '{}'", str))?,
297            ),
298            ..Default::default()
299        }))
300    }
301
302    #[turbo_tasks::function]
303    pub async fn current(process_env: ResolvedVc<Box<dyn ProcessEnv>>) -> Result<Vc<Self>> {
304        Ok(Self::cell(NodeJsEnvironment {
305            compile_target: CompileTarget::current().to_resolved().await?,
306            node_version: NodeJsVersion::cell(NodeJsVersion::Current(process_env))
307                .to_resolved()
308                .await?,
309            cwd: ResolvedVc::cell(None),
310        }))
311    }
312}
313
314#[turbo_tasks::value(shared)]
315pub enum NodeJsVersion {
316    /// Use the version of Node.js that is available from the environment (via `node --version`)
317    Current(ResolvedVc<Box<dyn ProcessEnv>>),
318    /// Use the specified version of Node.js.
319    Static(ResolvedVc<RcStr>),
320}
321
322impl Default for NodeJsVersion {
323    fn default() -> Self {
324        NodeJsVersion::Static(ResolvedVc::cell(DEFAULT_NODEJS_VERSION.into()))
325    }
326}
327
328#[turbo_tasks::value(shared)]
329pub struct BrowserEnvironment {
330    pub dom: bool,
331    pub web_worker: bool,
332    pub service_worker: bool,
333    pub browserslist_query: RcStr,
334}
335
336#[turbo_tasks::value(shared)]
337pub struct EdgeWorkerEnvironment {
338    // This isn't actually the Edge's worker environment, but we have to use some kind of version
339    // for transpiling ECMAScript features. No tool supports Edge Workers as a separate
340    // environment.
341    pub node_version: ResolvedVc<NodeJsVersion>,
342}
343
344#[turbo_tasks::value_impl]
345impl EdgeWorkerEnvironment {
346    #[turbo_tasks::function]
347    pub async fn runtime_versions(&self) -> Result<Vc<RuntimeVersions>> {
348        let str = match *self.node_version.await? {
349            NodeJsVersion::Current(process_env) => get_current_nodejs_version(*process_env),
350            NodeJsVersion::Static(version) => *version,
351        }
352        .await?;
353
354        Ok(Vc::cell(Versions {
355            node: Some(
356                Version::from_str(&str).map_err(|_| anyhow!("Node.js version parse error"))?,
357            ),
358            ..Default::default()
359        }))
360    }
361}
362
363// TODO preset_env_base::Version implements Serialize/Deserialize incorrectly
364#[derive(Debug)]
365#[turbo_tasks::value(transparent, serialization = "skip")]
366pub struct RuntimeVersions(#[turbo_tasks(trace_ignore)] pub Versions);
367
368#[turbo_tasks::value_impl]
369impl RuntimeVersions {
370    /// Whether the environment supports arrow functions.
371    #[turbo_tasks::function]
372    pub fn supports_arrow_functions(&self) -> Vc<bool> {
373        // https://github.com/babel/babel/blob/b0e3517dc566880e76b5f1f4dcf7fcecba58337d/packages/babel-compat-data/data/plugins.json#L363-L376
374        // "chrome": "47",
375        // "opera": "34",
376        // "edge": "13",
377        // "firefox": "43",
378        // "safari": "10",
379        // "node": "6",
380        // "deno": "1",
381        // "ios": "10",
382        // "samsung": "5",
383        // "rhino": "1.7.13",
384        // "opera_mobile": "34",
385        // "electron": "0.36"
386        let data = &self.0;
387        let supported = data.chrome.is_none_or(|v| v.major >= 47)
388            && data.opera.is_none_or(|v| v.major >= 34)
389            && data.edge.is_none_or(|v| v.major >= 13)
390            && data.firefox.is_none_or(|v| v.major >= 43)
391            && data.safari.is_none_or(|v| v.major >= 10)
392            && data.node.is_none_or(|v| v.major >= 6)
393            && data.deno.is_none_or(|v| v.major >= 1)
394            && data.ios.is_none_or(|v| v.major >= 10)
395            && data.samsung.is_none_or(|v| v.major >= 5)
396            && data.rhino.is_none_or(|v| {
397                v.major > 1
398                    || (v.major == 1 && v.minor > 7)
399                    || (v.major == 1 && v.minor == 7 && v.patch >= 13)
400            })
401            && data.opera_mobile.is_none_or(|v| v.major >= 34)
402            && data.electron.is_none_or(|v| v.major > 0 || v.minor >= 36);
403
404        Vc::cell(supported)
405    }
406
407    /// Whether the environment supports block scoping (let/const).
408    #[turbo_tasks::function]
409    pub fn supports_block_scoping(&self) -> Vc<bool> {
410        // https://github.com/babel/babel/blob/b0e3517dc566880e76b5f1f4dcf7fcecba58337d/packages/babel-compat-data/data/plugins.json#L538
411        // "chrome": "50",
412        // "opera": "37",
413        // "edge": "14",
414        // "firefox": "53",
415        // "safari": "11",
416        // "node": "6",
417        // "deno": "1",
418        // "ios": "11",
419        // "samsung": "5",
420        // "opera_mobile": "37",
421        // "electron": "1.1"
422        let data = &self.0;
423        let supported = data.chrome.is_none_or(|v| v.major >= 50)
424            && data.opera.is_none_or(|v| v.major >= 37)
425            && data.edge.is_none_or(|v| v.major >= 14)
426            && data.firefox.is_none_or(|v| v.major >= 53)
427            && data.safari.is_none_or(|v| v.major >= 11)
428            && data.node.is_none_or(|v| v.major >= 6)
429            && data.deno.is_none_or(|v| v.major >= 1)
430            && data.ios.is_none_or(|v| v.major >= 11)
431            && data.samsung.is_none_or(|v| v.major >= 5)
432            && data.opera_mobile.is_none_or(|v| v.major >= 37)
433            && data
434                .electron
435                .is_none_or(|v| v.major > 1 || (v.major == 1 && v.minor >= 1));
436
437        Vc::cell(supported)
438    }
439}
440
441#[turbo_tasks::function]
442pub async fn get_current_nodejs_version(env: Vc<Box<dyn ProcessEnv>>) -> Result<Vc<RcStr>> {
443    let path_read = env.read(rcstr!("PATH")).await?;
444    let path = path_read.as_ref().context("env must have PATH")?;
445    let mut cmd = Command::new("node");
446    cmd.arg("--version");
447    cmd.env_clear();
448    cmd.env("PATH", path);
449    cmd.stdin(Stdio::piped());
450    cmd.stdout(Stdio::piped());
451
452    let output = cmd.output()?;
453
454    if !output.status.success() {
455        bail!(
456            "'node --version' command failed{}{}",
457            output
458                .status
459                .code()
460                .map(|c| format!(" with exit code {c}"))
461                .unwrap_or_default(),
462            String::from_utf8(output.stderr)
463                .map(|stderr| format!(": {stderr}"))
464                .unwrap_or_default()
465        );
466    }
467
468    let version = String::from_utf8(output.stdout)
469        .context("failed to parse 'node --version' output as utf8")?;
470    if let Some(version_number) = version.strip_prefix("v") {
471        Ok(Vc::cell(version_number.trim().into()))
472    } else {
473        bail!(
474            "Expected 'node --version' to return a version starting with 'v', but received: '{}'",
475            version
476        )
477    }
478}