turbopack_ecmascript/references/esm/
meta.rs

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