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