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 NodeJs,
38 Dom,
40 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 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 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 {
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 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 Current(ResolvedVc<Box<dyn ProcessEnv>>),
318 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 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#[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 #[turbo_tasks::function]
372 pub fn supports_arrow_functions(&self) -> Vc<bool> {
373 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 #[turbo_tasks::function]
409 pub fn supports_block_scoping(&self) -> Vc<bool> {
410 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}