turbopack_json/
lib.rs

1//! JSON asset support for turbopack.
2//!
3//! JSON assets are parsed to ensure they contain valid JSON.
4//!
5//! When imported from ES modules, they produce a module that exports the
6//! JSON value as an object.
7
8#![feature(min_specialization)]
9#![feature(arbitrary_self_types)]
10#![feature(arbitrary_self_types_pointers)]
11
12use std::fmt::Write;
13
14use anyhow::{Error, Result, bail};
15use turbo_rcstr::rcstr;
16use turbo_tasks::{ResolvedVc, ValueToString, Vc};
17use turbo_tasks_fs::{FileContent, FileJsonContent, glob::Glob};
18use turbopack_core::{
19    asset::{Asset, AssetContent},
20    chunk::{ChunkItem, ChunkType, ChunkableModule, ChunkingContext},
21    code_builder::CodeBuilder,
22    ident::AssetIdent,
23    module::Module,
24    module_graph::ModuleGraph,
25    source::Source,
26};
27use turbopack_ecmascript::{
28    chunk::{
29        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
30        EcmascriptChunkType, EcmascriptExports,
31    },
32    runtime_functions::TURBOPACK_EXPORT_VALUE,
33};
34
35#[turbo_tasks::value]
36pub struct JsonModuleAsset {
37    source: ResolvedVc<Box<dyn Source>>,
38}
39
40#[turbo_tasks::value_impl]
41impl JsonModuleAsset {
42    #[turbo_tasks::function]
43    pub fn new(source: ResolvedVc<Box<dyn Source>>) -> Vc<Self> {
44        Self::cell(JsonModuleAsset { source })
45    }
46}
47
48#[turbo_tasks::value_impl]
49impl Module for JsonModuleAsset {
50    #[turbo_tasks::function]
51    fn ident(&self) -> Vc<AssetIdent> {
52        self.source.ident().with_modifier(rcstr!("json"))
53    }
54}
55
56#[turbo_tasks::value_impl]
57impl Asset for JsonModuleAsset {
58    #[turbo_tasks::function]
59    fn content(&self) -> Vc<AssetContent> {
60        self.source.content()
61    }
62}
63
64#[turbo_tasks::value_impl]
65impl ChunkableModule for JsonModuleAsset {
66    #[turbo_tasks::function]
67    fn as_chunk_item(
68        self: ResolvedVc<Self>,
69        _module_graph: Vc<ModuleGraph>,
70        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
71    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
72        Vc::upcast(JsonChunkItem::cell(JsonChunkItem {
73            module: self,
74            chunking_context,
75        }))
76    }
77}
78
79#[turbo_tasks::value_impl]
80impl EcmascriptChunkPlaceable for JsonModuleAsset {
81    #[turbo_tasks::function]
82    fn get_exports(&self) -> Vc<EcmascriptExports> {
83        EcmascriptExports::Value.cell()
84    }
85
86    #[turbo_tasks::function]
87    fn is_marked_as_side_effect_free(&self, _side_effect_free_packages: Vc<Glob>) -> Vc<bool> {
88        Vc::cell(true)
89    }
90}
91
92#[turbo_tasks::value]
93struct JsonChunkItem {
94    module: ResolvedVc<JsonModuleAsset>,
95    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
96}
97
98#[turbo_tasks::value_impl]
99impl ChunkItem for JsonChunkItem {
100    #[turbo_tasks::function]
101    fn asset_ident(&self) -> Vc<AssetIdent> {
102        self.module.ident()
103    }
104
105    #[turbo_tasks::function]
106    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
107        *self.chunking_context
108    }
109
110    #[turbo_tasks::function]
111    async fn ty(&self) -> Result<Vc<Box<dyn ChunkType>>> {
112        Ok(Vc::upcast(
113            Vc::<EcmascriptChunkType>::default().resolve().await?,
114        ))
115    }
116
117    #[turbo_tasks::function]
118    fn module(&self) -> Vc<Box<dyn Module>> {
119        Vc::upcast(*self.module)
120    }
121}
122
123#[turbo_tasks::value_impl]
124impl EcmascriptChunkItem for JsonChunkItem {
125    #[turbo_tasks::function]
126    async fn content(&self) -> Result<Vc<EcmascriptChunkItemContent>> {
127        // We parse to JSON and then stringify again to ensure that the
128        // JSON is valid.
129        let content = self.module.content().file_content();
130        let data = content.parse_json().await?;
131        match &*data {
132            FileJsonContent::Content(data) => {
133                let data_str = data.to_string();
134
135                let mut code = CodeBuilder::default();
136
137                let source_code = if data_str.len() > 10_000 {
138                    // Only use JSON.parse if the content is larger than 10kb
139                    // https://v8.dev/blog/cost-of-javascript-2019#json
140                    let js_str_content = serde_json::to_string(&data_str)?;
141                    format!("{TURBOPACK_EXPORT_VALUE}(JSON.parse({js_str_content}));")
142                } else {
143                    format!("{TURBOPACK_EXPORT_VALUE}({data_str});")
144                };
145
146                let source_code = source_code.into();
147                let source_map = serde_json::json!({
148                    "version": 3,
149                    // TODO: Encode using `urlencoding`, so that these
150                    // are valid URLs. However, `project_trace_source_operation` (and
151                    // `uri_from_file`) need to handle percent encoding correctly first.
152                    //
153                    // See turbopack/crates/turbopack-core/src/source_map/utils.rs as well
154                    "sources": [format!("turbopack:///{}", self.module.ident().path().to_string().await?)],
155                    "sourcesContent": [&data_str],
156                    "names": [],
157                    // Maps 0:0 in the output code to 0:0 in the `source_code`. Sufficient for
158                    // bundle analyzers to attribute the bytes in the output chunks
159                    "mappings": "AAAA",
160                })
161                .to_string()
162                .into();
163                code.push_source(&source_code, Some(source_map));
164
165                let code = code.build();
166                Ok(EcmascriptChunkItemContent {
167                    source_map: Some(code.generate_source_map_ref(None)),
168                    inner_code: code.into_source_code(),
169                    ..Default::default()
170                }
171                .into())
172            }
173            FileJsonContent::Unparsable(e) => {
174                let mut message = "Unable to make a module from invalid JSON: ".to_string();
175                if let FileContent::Content(content) = &*content.await? {
176                    let text = content.content().to_str()?;
177                    e.write_with_content(&mut message, text.as_ref())?;
178                } else {
179                    write!(message, "{e}")?;
180                }
181
182                Err(Error::msg(message))
183            }
184            FileJsonContent::NotFound => {
185                bail!(
186                    "JSON file not found: {}",
187                    self.module.ident().to_string().await?
188                );
189            }
190        }
191    }
192}