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