Skip to content

Commit

Permalink
Merge pull request #127 from lingui/next
Browse files Browse the repository at this point in the history
chore: sync `next` to `main`
  • Loading branch information
andrii-bodnar authored Nov 29, 2024
2 parents d02b493 + b6189ca commit 571234e
Show file tree
Hide file tree
Showing 22 changed files with 1,097 additions and 648 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ module.exports = nextConfig;
It's important to build a plugin with the same Rust version used to build SWC itself.

This project uses `rust-toolchain` file in the root of project to define rust version.

To update Rust, put new version into `rust-toolchain` and call `rustup update` command
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "lingui_macro_plugin"
version = "4.1.0"
version = "5.0.0"
edition = "2021"

[lib]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lingui/swc-plugin",
"version": "4.1.0",
"version": "5.0.0",
"description": "A SWC Plugin for LinguiJS",
"author": {
"name": "Timofei Iatsenko",
Expand Down
29 changes: 25 additions & 4 deletions src/ast_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ pub fn get_jsx_attr<'a>(el: &'a JSXOpeningElement, name: &str) -> Option<&'a JSX
return None;
}

// get_local_ident_from_object_pat_prop(prop, "t")
// const {t} = useLingui() // => Ident("t")
// const {t: _} = useLingui() // => Ident("_")
pub fn get_local_ident_from_object_pat_prop(prop: &ObjectPatProp, imported_symbol: &str) -> Option<BindingIdent> {
return match prop {
ObjectPatProp::KeyValue(key_value)
if key_value.key.as_ident().is_some_and(|ident| ident.sym == imported_symbol.to_string()) =>
{
Some(key_value.value.as_ident().unwrap().clone())
}
ObjectPatProp::Assign(assign)
if assign.key.sym == imported_symbol.to_string() =>
{
Some(assign.key.clone())
}
_ => {
None
}
}
}

pub fn get_jsx_attr_value_as_string(val: &JSXAttrValue) -> Option<String> {
match val {
// offset="5"
Expand Down Expand Up @@ -142,14 +163,15 @@ pub fn create_key_value_prop(key: &str, value: Box<Expr>) -> PropOrSpread {
)));
}

pub fn create_import(source: JsWord, specifier: Ident) -> ModuleItem {
pub fn create_import(source: JsWord, imported: IdentName, local: IdentName) -> ModuleItem {
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
phase: ImportPhase::default(),
specifiers: vec![
ImportSpecifier::Named(ImportNamedSpecifier {
span: DUMMY_SP,
local: specifier,
imported: None,
local: local.into(),
imported: Some(ModuleExportName::Ident(imported.into())),
is_type_only: false,
})
],
Expand All @@ -160,6 +182,5 @@ pub fn create_import(source: JsWord, specifier: Ident) -> ModuleItem {
}),
with: None,
type_only: false,
phase: Default::default(),
}))
}
16 changes: 4 additions & 12 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ use swc_core::{
},
};

use crate::{
normalize_witespaces_js::normalize_whitespaces_js,
normalize_witespaces_jsx::normalize_whitespaces_jsx,
};
use crate::tokens::{IcuChoice, CaseOrOffset, MsgToken};

fn dedup_values(mut v: Vec<ValueWithPlaceholder>) -> Vec<ValueWithPlaceholder> {
Expand Down Expand Up @@ -55,7 +51,7 @@ pub struct MessageBuilder {
}

impl MessageBuilder {
pub fn parse(tokens: Vec<MsgToken>, jsx: bool) -> MessageBuilderResult {
pub fn parse(tokens: Vec<MsgToken>) -> MessageBuilderResult {
let mut builder = MessageBuilder {
message: String::new(),
components_stack: Vec::new(),
Expand All @@ -65,15 +61,11 @@ impl MessageBuilder {
};

builder.from_tokens(tokens);
builder.to_args(jsx)
builder.to_args()
}

pub fn to_args(mut self, jsx: bool) -> MessageBuilderResult {
let message_str = if jsx {
normalize_whitespaces_jsx(&self.message)
} else {
normalize_whitespaces_js(&self.message)
};
pub fn to_args(mut self) -> MessageBuilderResult {
let message_str = self.message;

let message = Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
Expand Down
9 changes: 4 additions & 5 deletions src/js_macro_folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl<'a> JsMacroFolder<'a> {
}

fn create_message_descriptor_from_tokens(&mut self, tokens: Vec<MsgToken>) -> Expr {
let parsed = MessageBuilder::parse(tokens, false);
let parsed = MessageBuilder::parse(tokens);

let mut props: Vec<PropOrSpread> = vec![
create_key_value_prop("id", generate_message_id(&parsed.message_str, "").into()),
Expand Down Expand Up @@ -64,9 +64,8 @@ impl<'a> JsMacroFolder<'a> {
span: DUMMY_SP,
obj: callee_obj.unwrap_or_else(|| {
self.ctx.should_add_18n_import = true;
let (_, i18n_export) = &self.ctx.options.runtime_modules.i18n;

return Box::new(IdentName::new(i18n_export.clone().into(), DUMMY_SP).into());
return Box::new(self.ctx.runtime_idents.i18n.clone().into());
}),
prop: MemberProp::Ident(IdentName::new("_".into(), DUMMY_SP)),
}).as_callee(),
Expand Down Expand Up @@ -100,7 +99,7 @@ impl<'a> JsMacroFolder<'a> {
if let Some(prop) = message_prop {
let tokens = self.ctx.try_tokenize_expr(&prop.value).unwrap_or_else(|| Vec::new());

let parsed = MessageBuilder::parse(tokens, false);
let parsed = MessageBuilder::parse(tokens);

if !id_prop.is_some() {
new_props.push(
Expand Down Expand Up @@ -200,6 +199,6 @@ impl<'a> Fold for JsMacroFolder<'a> {
);
}

expr
expr.fold_children_with(self)
}
}
137 changes: 82 additions & 55 deletions src/jsx_visitor.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use swc_core::ecma::{
visit::{Visit, VisitWith},
};
use swc_core::ecma::ast::{*};
use swc_core::common::DUMMY_SP;
use crate::ast_utils::{get_jsx_attr, get_jsx_attr_value_as_string};
use crate::tokens::{IcuChoice, ChoiceCase, CaseOrOffset, MsgToken, TagOpening};
use regex::{Regex};
use crate::macro_utils::MacroCtx;
use crate::tokens::{CaseOrOffset, ChoiceCase, IcuChoice, MsgToken, TagOpening};
use once_cell::sync::Lazy;
use regex::Regex;
use swc_core::common::DUMMY_SP;
use swc_core::ecma::ast::*;
use swc_core::ecma::atoms::JsWord;
use swc_core::ecma::visit::{Visit, VisitWith};
use swc_core::plugin::errors::HANDLER;
use crate::macro_utils::{ MacroCtx};

pub struct TransJSXVisitor<'a> {
pub tokens: Vec<MsgToken>,
Expand All @@ -20,18 +18,65 @@ impl<'a> TransJSXVisitor<'a> {
pub fn new(ctx: &'a MacroCtx) -> TransJSXVisitor<'a> {
TransJSXVisitor {
tokens: Vec::new(),
ctx
ctx,
}
}
}

static PLURAL_OPTIONS_WHITELIST: Lazy<Regex> = Lazy::new(|| Regex::new(r"(_[\d\w]+|zero|one|two|few|many|other)").unwrap());
static PLURAL_OPTIONS_WHITELIST: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(_[\d\w]+|zero|one|two|few|many|other)").unwrap());
static NUM_OPTION: Lazy<Regex> = Lazy::new(|| Regex::new(r"_(\d+)").unwrap());
static WORD_OPTION: Lazy<Regex> = Lazy::new(|| Regex::new(r"_(\w+)").unwrap());

// const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/
// const jsx2icuExactChoice = (value: string) => value.replace(/_(\d+)/, "=$1").replace(/_(\w+)/, "$1")

static TRIM_START: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[ ]+").unwrap());
static TRIM_END: Lazy<Regex> = Lazy::new(|| Regex::new(r"[ ]+$").unwrap());

// taken from babel repo -> packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
fn clean_jsx_element_literal_child(value: &str) -> String {
let lines: Vec<&str> = value.split('\n').collect();
let mut last_non_empty_line = 0;

for (i, line) in lines.iter().enumerate() {
if line.trim().len() > 0 {
last_non_empty_line = i;
}
}

let mut result = String::new();

for (i, line) in lines.iter().enumerate() {
let is_first_line = i == 0;
let is_last_line = i == lines.len() - 1;
let is_last_non_empty_line = i == last_non_empty_line;

// replace rendered whitespace tabs with spaces
let mut trimmed_line = line.replace("\t", " ");

// trim whitespace touching a newline
if !is_first_line {
trimmed_line = TRIM_START.replace(&trimmed_line, "").to_string();
}

// trim whitespace touching an endline
if !is_last_line {
trimmed_line = TRIM_END.replace(&trimmed_line, "").to_string();
}

if !trimmed_line.is_empty() {
if !is_last_non_empty_line {
trimmed_line.push(' ');
}

result.push_str(&trimmed_line);
}
}

result
}

fn is_allowed_plural_option(key: &str) -> Option<JsWord> {
if PLURAL_OPTIONS_WHITELIST.is_match(key) {
let key = NUM_OPTION.replace(key, "=$1");
Expand Down Expand Up @@ -68,12 +113,14 @@ impl<'a> TransJSXVisitor<'a> {
tokens.push(MsgToken::String(string));
}

JSXAttrValue::JSXExprContainer(JSXExprContainer { expr: JSXExpr::Expr(exp), .. }) => {
JSXAttrValue::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::Expr(exp),
..
}) => {
match exp.as_ref() {
// some={"# books"}
Expr::Lit(Lit::Str(str)) => {
tokens.push(MsgToken::String(str.value.clone().to_string()))
}
Expr::Lit(Lit::Str(str)) => tokens
.push(MsgToken::String(str.value.clone().to_string())),
// some={`# books ${name}`}
Expr::Tpl(tpl) => {
tokens.extend(self.ctx.tokenize_tpl(tpl));
Expand All @@ -86,9 +133,7 @@ impl<'a> TransJSXVisitor<'a> {
tokens.extend(visitor.tokens)
}

_ => {
tokens.push(MsgToken::Expression(exp.clone()))
}
_ => tokens.push(MsgToken::Expression(exp.clone())),
}
}

Expand All @@ -97,11 +142,7 @@ impl<'a> TransJSXVisitor<'a> {
}
}

choices.push(CaseOrOffset::Case(
ChoiceCase {
tokens,
key,
}))
choices.push(CaseOrOffset::Case(ChoiceCase { tokens, key }))
}
}
}
Expand All @@ -128,21 +169,18 @@ impl<'a> Visit for TransJSXVisitor<'a> {

if self.ctx.is_lingui_jsx_choice_cmp(&ident) {
let value = match get_jsx_attr(&el, "value").and_then(|attr| attr.value.as_ref()) {
Some(
JSXAttrValue::JSXExprContainer(
JSXExprContainer { expr: JSXExpr::Expr(exp), .. }
)
) => {
exp.clone()
}
_ => {
Box::new(Expr::Lit(Lit::Null(Null {
span: DUMMY_SP
})))
}
Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::Expr(exp),
..
})) => exp.clone(),
_ => Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))),
};

let icu_method = self.ctx.get_ident_export_name(ident).unwrap().to_lowercase();
let icu_method = self
.ctx
.get_ident_export_name(ident)
.unwrap()
.to_lowercase();
let choices = self.visit_icu_macro(el, &icu_method);

self.tokens.push(MsgToken::IcuChoice(IcuChoice {
Expand All @@ -168,24 +206,21 @@ impl<'a> Visit for TransJSXVisitor<'a> {
}

fn visit_jsx_closing_element(&mut self, _el: &JSXClosingElement) {
self.tokens.push(
MsgToken::TagClosing
);
self.tokens.push(MsgToken::TagClosing);
}

fn visit_jsx_text(&mut self, el: &JSXText) {
self.tokens.push(
MsgToken::String(el.value.to_string())
);
self.tokens
.push(MsgToken::String(clean_jsx_element_literal_child(
&el.value.to_string(),
)));
}

fn visit_jsx_expr_container(&mut self, cont: &JSXExprContainer) {
if let JSXExpr::Expr(exp) = &cont.expr {
match exp.as_ref() {
Expr::Lit(Lit::Str(str)) => {
self.tokens.push(
MsgToken::String(str.value.to_string())
);
self.tokens.push(MsgToken::String(str.value.to_string()));
}

// todo write tests and validate
Expand All @@ -194,9 +229,7 @@ impl<'a> Visit for TransJSXVisitor<'a> {
if let Some(tokens) = self.ctx.try_tokenize_call_expr_as_choice_cmp(call) {
self.tokens.extend(tokens);
} else {
self.tokens.push(
MsgToken::Expression(exp.clone())
);
self.tokens.push(MsgToken::Expression(exp.clone()));
}
}

Expand All @@ -205,18 +238,12 @@ impl<'a> Visit for TransJSXVisitor<'a> {
}

Expr::Tpl(tpl) => {
self.tokens.extend(
self.ctx.tokenize_tpl(tpl)
);
self.tokens.extend(self.ctx.tokenize_tpl(tpl));
}
_ => {
self.tokens.push(
MsgToken::Expression(exp.clone())
);
self.tokens.push(MsgToken::Expression(exp.clone()));
}
}
}
}
}


Loading

0 comments on commit 571234e

Please sign in to comment.