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();
62        let path = ident.path().await?;
63        let content = source.content().file_content();
64
65        // Parse the JSON to validate it and get the data
66        let data = content.parse_json().await?;
67        let (code, extension) = match &*data {
68            FileJsonContent::Content(data) => {
69                let data_str = data.to_string();
70
71                // The "use turbopack no side effects" directive marks this module as
72                // side-effect free for tree shaking
73                let mut code = String::with_capacity(
74                    data_str.len() + 100, /* estimate to account for our `use` comment, export
75                                           * overhead and sourcemap comment */
76                );
77                code.push_str("\"use turbopack no side effects\";\n");
78
79                let extension = if this.use_esm {
80                    // Spec-compliant ESM: only default export
81                    code.push_str("export default ");
82                    "mjs"
83                } else {
84                    // Webpack-compatible CommonJS: allows named property imports
85                    code.push_str("module.exports = ");
86                    "cjs"
87                };
88                // For large JSON files, wrap in JSON.parse for better performance
89                // https://v8.dev/blog/cost-of-javascript-2019#json
90                if data_str.len() > 10_000 {
91                    code.push_str("JSON.parse(");
92                    code.push_str(&serde_json::to_string(&data_str)?);
93                    code.push(')');
94                } else {
95                    code.push_str(&data_str);
96                }
97                code.push_str(";\n");
98                code.push_str(&inline_source_map_comment(&path.path, &data_str));
99
100                (code, extension)
101            }
102            FileJsonContent::Unparsable(e) => {
103                let resolved_source = source.to_resolved().await?;
104                let issue_source = IssueSource::from_unparsable_json(resolved_source, e);
105
106                CodeGenerationIssue {
107                    severity: IssueSeverity::Error,
108                    path: ident.path().owned().await?,
109                    title: StyledString::Text(rcstr!("Unable to make a module from invalid JSON"))
110                        .resolved_cell(),
111                    message: StyledString::Text(e.message.clone()).resolved_cell(),
112                    source: Some(issue_source),
113                }
114                .resolved_cell()
115                .emit();
116
117                let js_error_message = serde_json::to_string(&format!(
118                    "Unable to make a module from invalid JSON: {}",
119                    e.message
120                ))?;
121                (format!("throw new Error({js_error_message});"), "js")
122            }
123            FileJsonContent::NotFound => {
124                // This is basically impossible since we wouldn't be called if the module
125                // doesn't exist but some kind of eventual consistency situation is
126                // possible where we resolve the file and then it disappears, so bail is appropriate
127                bail!("JSON file not found: {:?}", path);
128            }
129        };
130
131        let new_ident = ident.rename_as(format!("{}.[json].{}", path.path, extension).into());
132
133        Ok(Vc::upcast(VirtualSource::new_with_ident(
134            new_ident,
135            AssetContent::file(FileContent::Content(File::from(code)).cell()),
136        )))
137    }
138}