Skip to main content

turbopack_ecmascript/
json_source_transform.rs

1use anyhow::{Result, bail};
2use turbo_rcstr::rcstr;
3use turbo_tasks::Vc;
4use turbo_tasks_fs::{File, FileContent, FileJsonContent};
5use turbopack_core::{
6    asset::{Asset, AssetContent},
7    context::AssetContext,
8    issue::{IssueExt, IssueSeverity, IssueSource, StyledString, code_gen::CodeGenerationIssue},
9    source::Source,
10    source_transform::SourceTransform,
11    virtual_source::VirtualSource,
12};
13
14use crate::utils::inline_source_map_comment;
15
16/// A source transform that converts a JSON file into a JavaScript module.
17///
18/// Two modes are supported:
19///
20/// - **Spec-compliant mode** (`use_esm = true`): Generates an ESM module with only a default
21///   export, per the JSON Modules spec. Used for `import ... with { type: 'json' }`.
22///
23/// - **Webpack-compatible mode** (`use_esm = false`): Generates a CommonJS module via
24///   `module.exports`, which allows both default imports and named property imports (e.g., `import
25///   { a } from './data.json'`). This matches webpack's historical behavior.
26///
27/// For JSON files larger than 10KB, we use `JSON.parse()` for better performance.
28/// See: <https://v8.dev/blog/cost-of-javascript-2019#json>
29#[turbo_tasks::value]
30pub struct JsonSourceTransform {
31    /// If true, generate spec-compliant ESM with only an esm default export.
32    /// If false, generate CommonJS for webpack compatibility.
33    use_esm: bool,
34}
35
36#[turbo_tasks::value_impl]
37impl JsonSourceTransform {
38    /// Create a new JSON transform with webpack-compatible CommonJS output.
39    #[turbo_tasks::function]
40    pub fn new_cjs() -> Vc<Self> {
41        JsonSourceTransform { use_esm: false }.cell()
42    }
43
44    /// Create a new JSON transform with spec-compliant ESM output.
45    /// Use this for `import ... with { type: 'json' }` imports.
46    #[turbo_tasks::function]
47    pub fn new_esm() -> Vc<Self> {
48        JsonSourceTransform { use_esm: true }.cell()
49    }
50}
51
52#[turbo_tasks::value_impl]
53impl SourceTransform for JsonSourceTransform {
54    #[turbo_tasks::function]
55    async fn transform(
56        self: Vc<Self>,
57        source: Vc<Box<dyn Source>>,
58        _asset_context: Vc<Box<dyn AssetContext>>,
59    ) -> Result<Vc<Box<dyn Source>>> {
60        let this = self.await?;
61        let ident = source.ident().owned().await?;
62        let content = source.content().file_content();
63
64        // Parse the JSON to validate it and get the data
65        let data = content.parse_json().await?;
66        let (code, rename_pattern) = match &*data {
67            FileJsonContent::Content(data) => {
68                let data_str = data.to_string();
69
70                // The "use turbopack no side effects" directive marks this module as
71                // side-effect free for tree shaking
72                let mut code = String::with_capacity(
73                    data_str.len() + 100, /* estimate to account for our `use` comment, export
74                                           * overhead and sourcemap comment */
75                );
76                code.push_str("\"use turbopack no side effects\";\n");
77
78                let rename_pattern = if this.use_esm {
79                    // Spec-compliant ESM: only default export
80                    code.push_str("export default ");
81                    "*.[json].mjs"
82                } else {
83                    // Webpack-compatible CommonJS: allows named property imports
84                    code.push_str("module.exports = ");
85                    "*.[json].cjs"
86                };
87                // For large JSON files, wrap in JSON.parse for better performance
88                // https://v8.dev/blog/cost-of-javascript-2019#json
89                if data_str.len() > 10_000 {
90                    code.push_str("JSON.parse(");
91                    code.push_str(&serde_json::to_string(&data_str)?);
92                    code.push(')');
93                } else {
94                    code.push_str(&data_str);
95                }
96                code.push_str(";\n");
97                code.push_str(&inline_source_map_comment(&ident.path.path, &data_str));
98
99                (code, rename_pattern)
100            }
101            FileJsonContent::Unparsable(e) => {
102                let resolved_source = source.to_resolved().await?;
103                let issue_source = IssueSource::from_unparsable_json(resolved_source, e);
104
105                CodeGenerationIssue {
106                    severity: IssueSeverity::Error,
107                    path: ident.path.clone(),
108                    title: StyledString::Text(rcstr!("Unable to make a module from invalid JSON"))
109                        .resolved_cell(),
110                    message: StyledString::Text(e.message.clone()).resolved_cell(),
111                    source: Some(issue_source),
112                }
113                .resolved_cell()
114                .emit();
115
116                let js_error_message = serde_json::to_string(&format!(
117                    "Unable to make a module from invalid JSON: {}",
118                    e.message
119                ))?;
120                (
121                    format!("throw new Error({js_error_message});"),
122                    "*.[json].js",
123                )
124            }
125            FileJsonContent::NotFound => {
126                // This is basically impossible since we wouldn't be called if the module
127                // doesn't exist but some kind of eventual consistency situation is
128                // possible where we resolve the file and then it disappears, so bail is appropriate
129                bail!("JSON file not found: {:?}", ident.path);
130            }
131        };
132
133        let new_ident = ident.rename_as(rename_pattern).into_vc();
134
135        Ok(Vc::upcast(VirtualSource::new_with_ident(
136            new_ident,
137            AssetContent::file(FileContent::Content(File::from(code)).cell()),
138        )))
139    }
140}