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