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 NodeJs,
38 Dom,
40}
41
42#[turbo_tasks::value]
43pub struct Environment {
44 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 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 {
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 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 Current(ResolvedVc<Box<dyn ProcessEnv>>),
310 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 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#[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}