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