Skip to main content

turbopack_ecmascript/references/esm/
meta.rs

1use std::borrow::Cow;
2
3use anyhow::Result;
4use bincode::{Decode, Encode};
5use swc_core::{
6    common::DUMMY_SP,
7    ecma::ast::{Expr, Ident},
8    quote,
9};
10use turbo_rcstr::{RcStr, rcstr};
11use turbo_tasks::{NonLocalValue, Vc, debug::ValueDebugFormat};
12use turbo_tasks_fs::FileSystemPath;
13use turbopack_core::chunk::ChunkingContext;
14
15use crate::{
16    ast_path_trie::{AstPathId, AstPathTrie},
17    code_gen::{CodeGen, CodeGeneration},
18    create_visitor, magic_identifier,
19    runtime_functions::{TURBOPACK_MODULE, TURBOPACK_RESOLVE_FILE_URL},
20};
21
22/// Responsible for initializing the `import.meta` object binding, so that it
23/// may be referenced in th the file.
24///
25/// There can be many references to import.meta, and they appear at any nesting
26/// in the file. But we must only initialize the binding a single time.
27///
28/// This singleton behavior must be enforced by the caller!
29#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Debug, Hash, Encode, Decode)]
30pub struct ImportMetaBinding {
31    path: FileSystemPath,
32    hmr_enabled: bool,
33    mode: RcStr,
34    base_url: RcStr,
35    is_ssr: bool,
36}
37
38impl ImportMetaBinding {
39    pub fn new(
40        path: FileSystemPath,
41        hmr_enabled: bool,
42        mode: RcStr,
43        base_url: RcStr,
44        is_ssr: bool,
45    ) -> Self {
46        ImportMetaBinding {
47            path,
48            hmr_enabled,
49            mode,
50            base_url,
51            is_ssr,
52        }
53    }
54
55    pub async fn code_generation(
56        &self,
57        _trie: &AstPathTrie,
58        chunking_context: Vc<Box<dyn ChunkingContext>>,
59    ) -> Result<CodeGeneration> {
60        let rel_path = chunking_context
61            .root_path()
62            .await?
63            .get_relative_path_to(&self.path);
64        let path = rel_path.map_or_else(
65            || {
66                quote!(
67                    "(() => { throw new Error('could not convert import.meta.url to filepath') })()"
68                        as Expr
69                )
70            },
71            |path| {
72                // `encode_path` only escapes characters that would break the JS string literal
73                // we embed `formatted` into. The runtime helper (`TURBOPACK_RESOLVE_FILE_URL`)
74                // is responsible for producing the final, properly URL-encoded `file://` URI.
75                let formatted = encode_path(path.trim_start_matches("./")).to_string();
76                quote!(
77                    "$turbopack_resolve_file_url($formatted)" as Expr,
78                    turbopack_resolve_file_url: Expr = TURBOPACK_RESOLVE_FILE_URL.into(),
79                    formatted: Expr = formatted.into()
80                )
81            },
82        );
83
84        let hmr_enabled = self.hmr_enabled;
85        let mode: Expr = self.mode.as_str().into();
86        let is_prod: Expr = (self.mode == "production").into();
87        let is_dev: Expr = (self.mode != "production").into();
88        let is_ssr: Expr = self.is_ssr.into();
89        let base_url: Expr = self.base_url.as_str().into();
90
91        // [NOTE] url property is lazy-evaluated, as it should be computed once
92        // turbopack_runtime injects a function to calculate an absolute path.
93        let stmt = if hmr_enabled {
94            // turbopackHot exposes the HMR API (equivalent to module.hot in CJS).
95            let turbopack_module: Expr = TURBOPACK_MODULE.into();
96            quote!(
97                "var $name = { get url() { return $path }, env: { DEV: $is_dev, PROD: \
98                 $is_prod, MODE: $mode, BASE_URL: $base_url, SSR: $is_ssr }, get turbopackHot() { \
99                 return $m.hot } };" as Stmt,
100                name = meta_ident(),
101                path: Expr = path,
102                is_dev: Expr = is_dev,
103                is_prod: Expr = is_prod,
104                mode: Expr = mode,
105                base_url: Expr = base_url,
106                is_ssr: Expr = is_ssr,
107                m: Expr = turbopack_module,
108            )
109        } else {
110            quote!(
111                "var $name = { get url() { return $path }, env: { DEV: $is_dev, PROD: \
112                 $is_prod, MODE: $mode, BASE_URL: $base_url, SSR: $is_ssr } };" as Stmt,
113                name = meta_ident(),
114                path: Expr = path,
115                is_dev: Expr = is_dev,
116                is_prod: Expr = is_prod,
117                mode: Expr = mode,
118                base_url: Expr = base_url,
119                is_ssr: Expr = is_ssr,
120            )
121        };
122
123        Ok(CodeGeneration::hoisted_stmt(rcstr!("import.meta"), stmt))
124    }
125}
126
127impl From<ImportMetaBinding> for CodeGen {
128    fn from(val: ImportMetaBinding) -> Self {
129        CodeGen::ImportMetaBinding(val)
130    }
131}
132
133/// Handles rewriting `import.meta` references into the injected binding created
134/// by ImportMetaBinding.
135///
136/// There can be many references to import.meta, and they appear at any nesting
137/// in the file. But all references refer to the same mutable object.
138#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
139pub struct ImportMetaRef {
140    ast_path: AstPathId,
141}
142
143impl ImportMetaRef {
144    pub fn new(ast_path: AstPathId) -> Self {
145        ImportMetaRef { ast_path }
146    }
147
148    pub async fn code_generation(
149        &self,
150        trie: &AstPathTrie,
151        _chunking_context: Vc<Box<dyn ChunkingContext>>,
152    ) -> Result<CodeGeneration> {
153        let visitor = create_visitor!(trie, self.ast_path, visit_mut_expr, |expr: &mut Expr| {
154            *expr = Expr::Ident(meta_ident());
155        });
156
157        Ok(CodeGeneration::visitors(vec![visitor]))
158    }
159}
160
161impl From<ImportMetaRef> for CodeGen {
162    fn from(val: ImportMetaRef) -> Self {
163        CodeGen::ImportMetaRef(val)
164    }
165}
166
167/// URL encodes special chars that would appear in the "pathname" portion.
168/// https://github.com/nodejs/node/blob/3bed5f11e039153eff5cbfd9513b8f55fd53fc43/lib/internal/url.js#L1513-L1526
169fn encode_path(path: &'_ str) -> Cow<'_, str> {
170    let mut encoded = String::new();
171    let mut start = 0;
172    for (i, c) in path.char_indices() {
173        let mapping = match c {
174            '%' => "%25",
175            '\\' => "%5C",
176            '\n' => "%0A",
177            '\r' => "%0D",
178            '\t' => "%09",
179            _ => continue,
180        };
181
182        if encoded.is_empty() {
183            encoded.reserve(path.len());
184        }
185
186        encoded += &path[start..i];
187        encoded += mapping;
188        start = i + 1;
189    }
190
191    if encoded.is_empty() {
192        return Cow::Borrowed(path);
193    }
194    encoded += &path[start..];
195    Cow::Owned(encoded)
196}
197
198fn meta_ident() -> Ident {
199    Ident::new(
200        magic_identifier::mangle("import.meta").into(),
201        DUMMY_SP,
202        Default::default(),
203    )
204}
205
206#[cfg(test)]
207mod test {
208    use super::encode_path;
209
210    #[test]
211    fn test_encode_path_regular() {
212        let input = "abc";
213        assert_eq!(encode_path(input), "abc");
214    }
215
216    #[test]
217    fn test_encode_path_special_chars() {
218        let input = "abc%def\\ghi\njkl\rmno\tpqr";
219        assert_eq!(encode_path(input), "abc%25def%5Cghi%0Ajkl%0Dmno%09pqr");
220    }
221
222    #[test]
223    fn test_encode_path_special_char_start() {
224        let input = "%abc";
225        assert_eq!(encode_path(input), "%25abc");
226    }
227
228    #[test]
229    fn test_encode_path_special_char_end() {
230        let input = "abc%";
231        assert_eq!(encode_path(input), "abc%25");
232    }
233
234    #[test]
235    fn test_encode_path_special_char_contiguous() {
236        let input = "%%%";
237        assert_eq!(encode_path(input), "%25%25%25");
238    }
239}