Skip to main content

intlayer_swc_plugin/
imports.rs

1//! Injection of the dictionary import declarations collected by the optimize
2//! transform.
3
4use crate::{
5    ast::{make_ident, make_str},
6    dictionary_imports::InjectedImports,
7    packages::NESTED_DICTIONARIES_SUBDIR,
8    paths::relative_import_path,
9};
10use std::{collections::HashSet, path::Path};
11use swc_core::{common::DUMMY_SP, ecma::ast::*};
12
13/// Absolute directories the generated dictionary modules live in.
14pub struct DictionaryDirs<'a> {
15    /// `<key>.json` compiled dictionaries (and their `nested/<key>.mjs` companions).
16    pub dictionaries_dir: &'a str,
17    /// `<key>.mjs` per-locale dynamic loaders.
18    pub dynamic_dictionaries_dir: &'a str,
19    /// `<key>.mjs` live/fetch loaders.
20    pub fetch_dictionaries_dir: &'a str,
21}
22
23/// Builds the `with { type: "json" }` import attribute.
24fn json_import_attributes() -> Box<ObjectLit> {
25    Box::new(ObjectLit {
26        span: DUMMY_SP,
27        props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
28            key: PropName::Ident(make_ident("type").into()),
29            value: Box::new(Expr::Lit(Lit::Str(make_str("json")))),
30        })))],
31    })
32}
33
34/// Builds `import <local> from "<specifier>"`, optionally carrying the JSON
35/// import attribute.
36fn default_import(local: Ident, specifier: String, with_json_attribute: bool) -> ModuleItem {
37    ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
38        span: DUMMY_SP,
39        specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier {
40            span: DUMMY_SP,
41            local,
42        })],
43        src: Box::new(Str::from(specifier)),
44        type_only: false,
45        with: with_json_attribute.then(json_import_attributes),
46        phase: ImportPhase::Evaluation,
47    }))
48}
49
50/// Returns the index at which injected imports must be inserted so the leading
51/// directive prologue (`'use client'`, `'use server'`, `'use strict'`, …) stays
52/// at the top of the module.
53///
54/// Every leading string-literal statement counts, mirroring the Babel pass:
55/// a bundler that lowers an `import` to a string statement is the one exception
56/// it makes, since inserting before that would reorder module evaluation.
57fn import_insert_position(body: &[ModuleItem]) -> usize {
58    let mut insert_position = 0;
59
60    for item in body {
61        let ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. })) = item else {
62            break;
63        };
64        let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr else {
65            break;
66        };
67        let Some(directive) = value.as_str() else {
68            break;
69        };
70        if directive.starts_with("import") || directive.starts_with("require") {
71            break;
72        }
73
74        insert_position += 1;
75    }
76
77    insert_position
78}
79
80/// Inserts the collected dictionary imports at the top of `program`.
81///
82/// Static imports point at the compiled JSON — or at the `nested/<key>.mjs`
83/// companion module for dictionaries holding `nest()` references, which
84/// re-exports the dictionary with its nest targets attached (an ES module, so
85/// no `with { type: "json" }` attribute). Dynamic imports point at the
86/// per-locale loader in either the dynamic or the fetch directory, chosen by
87/// the identifier suffix the optimize transform assigned.
88pub fn inject_dictionary_imports(
89    program: &mut Program,
90    injected_imports: &InjectedImports,
91    dirs: &DictionaryDirs<'_>,
92    nesting_dictionary_keys: &HashSet<&str>,
93    source_file_path: &str,
94) {
95    let Program::Module(Module { body, .. }) = program else {
96        return;
97    };
98
99    let file_dir_abs = Path::new(source_file_path)
100        .parent()
101        .unwrap_or_else(|| Path::new("/"));
102
103    let mut insert_position = import_insert_position(body);
104
105    for (key, ident) in injected_imports.static_imports.iter().rev() {
106        let has_nested_dictionaries = nesting_dictionary_keys.contains(key.as_str());
107        let dictionary_file_abs = if has_nested_dictionaries {
108            Path::new(dirs.dictionaries_dir)
109                .join(NESTED_DICTIONARIES_SUBDIR)
110                .join(format!("{}.mjs", key))
111        } else {
112            Path::new(dirs.dictionaries_dir).join(format!("{}.json", key))
113        };
114
115        body.insert(
116            insert_position,
117            default_import(
118                ident.clone(),
119                relative_import_path(&dictionary_file_abs, file_dir_abs),
120                !has_nested_dictionaries,
121            ),
122        );
123
124        insert_position += 1;
125    }
126
127    for (key, ident) in injected_imports.dynamic_imports.iter().rev() {
128        let is_fetch_loader = ident.sym.as_ref().ends_with("_fetch");
129        let target_dir = if is_fetch_loader {
130            dirs.fetch_dictionaries_dir
131        } else {
132            dirs.dynamic_dictionaries_dir
133        };
134        let dictionary_file_abs = Path::new(target_dir).join(format!("{}.mjs", key));
135
136        body.insert(
137            insert_position,
138            default_import(
139                ident.clone(),
140                relative_import_path(&dictionary_file_abs, file_dir_abs),
141                false,
142            ),
143        );
144
145        insert_position += 1;
146    }
147}