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