turbopack_ecmascript_hmr_protocol/
lib.rs

1use std::{collections::BTreeMap, fmt::Display, path::PathBuf};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use turbo_rcstr::RcStr;
6use turbopack_cli_utils::issue::{LogOptions, format_issue};
7use turbopack_core::{
8    issue::{IssueSeverity, IssueStage, PlainIssue, StyledString},
9    source_pos::SourcePos,
10};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
13pub struct ResourceIdentifier {
14    pub path: RcStr,
15    pub headers: Option<BTreeMap<RcStr, RcStr>>,
16}
17
18impl Display for ResourceIdentifier {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "{}", self.path)?;
21        if let Some(headers) = &self.headers {
22            for (key, value) in headers.iter() {
23                write!(f, " [{key}: {value}]")?;
24            }
25        }
26        Ok(())
27    }
28}
29
30#[derive(Deserialize)]
31#[serde(tag = "type")]
32pub enum ClientMessage {
33    #[serde(rename = "turbopack-subscribe")]
34    Subscribe {
35        #[serde(flatten)]
36        resource: ResourceIdentifier,
37    },
38    #[serde(rename = "turbopack-unsubscribe")]
39    Unsubscribe {
40        #[serde(flatten)]
41        resource: ResourceIdentifier,
42    },
43}
44
45#[derive(Serialize)]
46#[serde(rename_all = "camelCase")]
47pub struct ClientUpdateInstruction<'a> {
48    pub resource: &'a ResourceIdentifier,
49    #[serde(flatten)]
50    pub ty: ClientUpdateInstructionType<'a>,
51    pub issues: &'a [Issue<'a>],
52}
53
54pub const EMPTY_ISSUES: &[Issue<'static>] = &[];
55
56impl<'a> ClientUpdateInstruction<'a> {
57    pub fn new(
58        resource: &'a ResourceIdentifier,
59        ty: ClientUpdateInstructionType<'a>,
60        issues: &'a [Issue<'a>],
61    ) -> Self {
62        Self {
63            resource,
64            ty,
65            issues,
66        }
67    }
68
69    pub fn restart(resource: &'a ResourceIdentifier, issues: &'a [Issue<'a>]) -> Self {
70        Self::new(resource, ClientUpdateInstructionType::Restart, issues)
71    }
72
73    /// Returns a [`ClientUpdateInstruction`] that indicates that the resource
74    /// was not found.
75    pub fn not_found(resource: &'a ResourceIdentifier) -> Self {
76        Self::new(resource, ClientUpdateInstructionType::NotFound, &[])
77    }
78
79    pub fn partial(
80        resource: &'a ResourceIdentifier,
81        instruction: &'a Value,
82        issues: &'a [Issue<'a>],
83    ) -> Self {
84        Self::new(
85            resource,
86            ClientUpdateInstructionType::Partial { instruction },
87            issues,
88        )
89    }
90
91    pub fn issues(resource: &'a ResourceIdentifier, issues: &'a [Issue<'a>]) -> Self {
92        Self::new(resource, ClientUpdateInstructionType::Issues, issues)
93    }
94
95    pub fn with_issues(self, issues: &'a [Issue<'a>]) -> Self {
96        Self {
97            resource: self.resource,
98            ty: self.ty,
99            issues,
100        }
101    }
102}
103
104#[derive(Serialize)]
105#[serde(tag = "type", rename_all = "camelCase")]
106pub enum ClientUpdateInstructionType<'a> {
107    Restart,
108    NotFound,
109    Partial { instruction: &'a Value },
110    Issues,
111}
112
113#[derive(Serialize)]
114#[serde(tag = "type", rename_all = "camelCase")]
115pub enum ServerError {
116    SSR(String),
117    Turbo(String),
118}
119
120#[derive(Serialize)]
121pub struct Asset<'a> {
122    pub path: &'a str,
123}
124
125#[derive(Serialize)]
126pub struct IssueSource<'a> {
127    pub asset: Asset<'a>,
128    pub range: Option<IssueSourceRange>,
129}
130
131#[derive(Serialize)]
132pub struct IssueSourceRange {
133    pub start: SourcePos,
134    pub end: SourcePos,
135}
136
137#[derive(Serialize)]
138pub struct Issue<'a> {
139    pub severity: IssueSeverity,
140    pub file_path: &'a str,
141    pub stage: &'a IssueStage,
142
143    pub title: &'a StyledString,
144    pub description: Option<&'a StyledString>,
145    pub detail: Option<&'a StyledString>,
146    pub documentation_link: &'a str,
147
148    pub source: Option<IssueSource<'a>>,
149
150    pub formatted: String,
151}
152
153impl<'a> From<&'a PlainIssue> for Issue<'a> {
154    fn from(plain: &'a PlainIssue) -> Self {
155        let source = plain.source.as_ref().map(|source| IssueSource {
156            asset: Asset {
157                path: &source.asset.ident,
158            },
159            range: source
160                .range
161                .map(|(start, end)| IssueSourceRange { start, end }),
162        });
163
164        Issue {
165            severity: plain.severity,
166            file_path: &plain.file_path,
167            stage: &plain.stage,
168            title: &plain.title,
169            description: plain.description.as_ref(),
170            documentation_link: &plain.documentation_link,
171            detail: plain.detail.as_ref(),
172            source,
173            // TODO(WEB-691) formatting the issue should be handled by the error overlay.
174            // The browser could handle error formatting in a better way than the text only
175            // formatting here
176            formatted: format_issue(
177                plain,
178                None,
179                &LogOptions {
180                    current_dir: PathBuf::new(),
181                    project_dir: PathBuf::new(),
182                    show_all: true,
183                    log_detail: true,
184                    log_level: IssueSeverity::Info,
185                },
186            ),
187        }
188    }
189}