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    ident::AssetIdent,
22    module::Module,
23    module_graph::ModuleGraph,
24    source::Source,
25};
26use turbopack_ecmascript::{
27    chunk::{
28        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
29        EcmascriptChunkType, EcmascriptExports,
30    },
31    runtime_functions::TURBOPACK_EXPORT_VALUE,
32};
33
34#[turbo_tasks::value]
35pub struct JsonModuleAsset {
36    source: ResolvedVc<Box<dyn Source>>,
37}
38
39#[turbo_tasks::value_impl]
40impl JsonModuleAsset {
41    #[turbo_tasks::function]
42    pub fn new(source: ResolvedVc<Box<dyn Source>>) -> Vc<Self> {
43        Self::cell(JsonModuleAsset { source })
44    }
45}
46
47#[turbo_tasks::value_impl]
48impl Module for JsonModuleAsset {
49    #[turbo_tasks::function]
50    fn ident(&self) -> Vc<AssetIdent> {
51        self.source.ident().with_modifier(rcstr!("json"))
52    }
53}
54
55#[turbo_tasks::value_impl]
56impl Asset for JsonModuleAsset {
57    #[turbo_tasks::function]
58    fn content(&self) -> Vc<AssetContent> {
59        self.source.content()
60    }
61}
62
63#[turbo_tasks::value_impl]
64impl ChunkableModule for JsonModuleAsset {
65    #[turbo_tasks::function]
66    fn as_chunk_item(
67        self: ResolvedVc<Self>,
68        _module_graph: Vc<ModuleGraph>,
69        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
70    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
71        Vc::upcast(JsonChunkItem::cell(JsonChunkItem {
72            module: self,
73            chunking_context,
74        }))
75    }
76}
77
78#[turbo_tasks::value_impl]
79impl EcmascriptChunkPlaceable for JsonModuleAsset {
80    #[turbo_tasks::function]
81    fn get_exports(&self) -> Vc<EcmascriptExports> {
82        EcmascriptExports::Value.cell()
83    }
84
85    #[turbo_tasks::function]
86    fn is_marked_as_side_effect_free(&self, _side_effect_free_packages: Vc<Glob>) -> Vc<bool> {
87        Vc::cell(true)
88    }
89}
90
91#[turbo_tasks::value]
92struct JsonChunkItem {
93    module: ResolvedVc<JsonModuleAsset>,
94    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
95}
96
97#[turbo_tasks::value_impl]
98impl ChunkItem for JsonChunkItem {
99    #[turbo_tasks::function]
100    fn asset_ident(&self) -> Vc<AssetIdent> {
101        self.module.ident()
102    }
103
104    #[turbo_tasks::function]
105    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
106        Vc::upcast(*self.chunking_context)
107    }
108
109    #[turbo_tasks::function]
110    async fn ty(&self) -> Result<Vc<Box<dyn ChunkType>>> {
111        Ok(Vc::upcast(
112            Vc::<EcmascriptChunkType>::default().resolve().await?,
113        ))
114    }
115
116    #[turbo_tasks::function]
117    fn module(&self) -> Vc<Box<dyn Module>> {
118        Vc::upcast(*self.module)
119    }
120}
121
122#[turbo_tasks::value_impl]
123impl EcmascriptChunkItem for JsonChunkItem {
124    #[turbo_tasks::function]
125    async fn content(&self) -> Result<Vc<EcmascriptChunkItemContent>> {
126        // We parse to JSON and then stringify again to ensure that the
127        // JSON is valid.
128        let content = self.module.content().file_content();
129        let data = content.parse_json().await?;
130        match &*data {
131            FileJsonContent::Content(data) => {
132                let js_str_content = serde_json::to_string(&data.to_string())?;
133                let inner_code = format!("{TURBOPACK_EXPORT_VALUE}(JSON.parse({js_str_content}));");
134
135                Ok(EcmascriptChunkItemContent {
136                    inner_code: inner_code.into(),
137                    ..Default::default()
138                }
139                .into())
140            }
141            FileJsonContent::Unparsable(e) => {
142                let mut message = "Unable to make a module from invalid JSON: ".to_string();
143                if let FileContent::Content(content) = &*content.await? {
144                    let text = content.content().to_str()?;
145                    e.write_with_content(&mut message, text.as_ref())?;
146                } else {
147                    write!(message, "{e}")?;
148                }
149
150                Err(Error::msg(message))
151            }
152            FileJsonContent::NotFound => {
153                bail!(
154                    "JSON file not found: {}",
155                    self.module.ident().to_string().await?
156                );
157            }
158        }
159    }
160}
161
162pub fn register() {
163    turbo_tasks::register();
164    turbo_tasks_fs::register();
165    turbopack_core::register();
166    turbopack_ecmascript::register();
167    include!(concat!(env!("OUT_DIR"), "/register.rs"));
168}