Skip to main content

turbopack_ecmascript/
bytes_source_transform.rs

1use std::io::Read;
2
3use anyhow::{Result, bail};
4use turbo_tasks::Vc;
5use turbo_tasks_fs::{File, FileContent};
6use turbopack_core::{
7    asset::{Asset, AssetContent},
8    context::AssetContext,
9    source::Source,
10    source_transform::SourceTransform,
11    virtual_source::VirtualSource,
12};
13
14use crate::utils::{StringifyJs, inline_source_map_comment};
15
16/// A source transform that converts any file into an ES module that exports
17/// the file's content as a default Uint8Array export.
18///
19/// This is used for `import bytes from './file.bin' with { type: 'bytes' }`.
20#[turbo_tasks::value]
21pub struct BytesSourceTransform;
22
23#[turbo_tasks::value_impl]
24impl BytesSourceTransform {
25    #[turbo_tasks::function]
26    pub fn new() -> Vc<Self> {
27        BytesSourceTransform.cell()
28    }
29}
30
31#[turbo_tasks::value_impl]
32impl SourceTransform for BytesSourceTransform {
33    #[turbo_tasks::function]
34    async fn transform(
35        self: Vc<Self>,
36        source: Vc<Box<dyn Source>>,
37        _asset_context: Vc<Box<dyn AssetContext>>,
38    ) -> Result<Vc<Box<dyn Source>>> {
39        let ident = source.ident();
40        let path = ident.path().await?;
41        let content = source.content().file_content().await?;
42        let bytes = match &*content {
43            FileContent::Content(data) => {
44                data.read().bytes().collect::<std::io::Result<Vec<u8>>>()?
45            }
46            FileContent::NotFound => {
47                bail!("File not found: {:?}", path);
48            }
49        };
50
51        let encoded = data_encoding::BASE64_NOPAD.encode(&bytes);
52
53        // Generate ES module that decodes base64 to Uint8Array with inline source map.
54        // Uses Uint8Array.fromBase64 (ES2024+) with atob fallback for older environments.
55        let code = format!(
56            r#"
57"use turbopack no side effects";
58
59const decode = Uint8Array.fromBase64 || function(base64) {{
60  const binaryString = atob(base64);
61  const buffer = new Uint8Array(binaryString.length);
62  for (let i = 0; i < binaryString.length; i++) {{
63    buffer[i] = binaryString.charCodeAt(i)
64  }}
65  return buffer
66}};
67
68export default decode({});
69{}"#,
70            StringifyJs(&encoded),
71            // For binary files, we use an empty string as sourcesContent since the
72            // original content isn't meaningful text.
73            inline_source_map_comment(&path.path, "")
74        );
75
76        // Rename to .mjs so module rules recognize it as ESM.
77        // The inline source map ensures debuggers show the original file.
78        let new_ident = ident.rename_as(format!("{}.[bytes].mjs", path.path).into());
79
80        Ok(Vc::upcast(VirtualSource::new_with_ident(
81            new_ident,
82            AssetContent::file(FileContent::Content(File::from(code)).cell()),
83        )))
84    }
85}