-
Notifications
You must be signed in to change notification settings - Fork 382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(macro): macro to plugin #1867
refactor(macro): macro to plugin #1867
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
size-limit report 📦
|
58ee56f
to
2a276ad
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## next #1867 +/- ##
==========================================
+ Coverage 76.66% 77.23% +0.56%
==========================================
Files 81 83 +2
Lines 2083 2126 +43
Branches 532 549 +17
==========================================
+ Hits 1597 1642 +45
+ Misses 375 374 -1
+ Partials 111 110 -1 ☔ View full report in Codecov by Sentry. |
b0ec5f1
to
ffc5241
Compare
@andrii-bodnar rebased. PR is ready for review. |
@thekip the |
Well, I cover every separate issue by its own test in macro / extractor. So yeah, it's probably redundant. From the other side, this is a real test, not synthetic, which potentially could catch problems after updating esbuild, or other dependencies. |
Understand. As I can see, we already have some similar tests for the experimental extractor. Let's follow them and limit the new test to the minimum amount of code and files needed. It probably doesn't need so many strings to test, a language switcher, styles, utils, tsconfig, and package.json. |
they all needed unfortunately, Styles needed also nice to have, to check that esbuild don't try to parse them. All this is fixtures, we don't compare these files. We compare only resulting catalogs. |
I'm trying to avoid committing excessive code. Let's reduce this test to the minimum size needed - delete unnecessary files and make the rest files as small as possible (we probably don't need 14 messages, 50+ lines of styles, or a 100-line component for the test). |
This reverts commit 797fe4e.
i don't have time to checking how changes in the imports / files structure after "cleaning" will affect bundled output of esbuild and will it still reproduce the issue or not. I reverted it. |
@thekip thank you! |
@thekip are you planning to commit anything soon? I would like to make a new release if not |
@andrii-bodnar no you can go ahead. Make it as |
Tested on my project and on https://github.com/brave/ads-ui works well, catalogs did not changed after extracting with new version and no errors during build. |
Awesome! |
Description
There are few noticeable changes in this PR:
These changes make a transformation more robust, reliable and ready for more complicated cases which may appear for example after code bundling.
The original motivation was to avoid complications related to babel-macros architecture, but that it turned out that it will help with fixing few of the opened bugs (#1844 #1848 ) and fix broken experimental deps extractor
The problem with babel-macro-plugin for lingui macro:
Macro written for babel-macro-plugin works a little bit different way then usual babel plugins. Instead of defining a Visitor (object which enters each node of AST) it receives a list of references for nodes used from the macro package.
For this snippet babel-macro-plugin will invoke our macro function one time with an object:
Then our macro should take each node and somehow process it.
The problem here lays in the nested macro calls:
Which should be turned in a single i18n call:
We receive 2 nodes for this one expression one node in
t
key another inplural
, and we don't have any information about how these nodes are correlated to each other.The previous macro used quite simple approach, we collect all nodes from all keys into one single array
And then add a condition before we start processing each node which recursively goes and check do each node has parent declared in this array. Let's call this "parent matching method"
And this worked up to the point. The code inside transformation was written "imperatively", every possible case is handled manually by additional conditions in the flow. Although there are still cases which were not anticipated in the transformation code, such as:
But since it's a rear case that wasn't a problem for a long time.
However, things start biting again during implementation of
useLingui
hook:Only
useLingui
andplural
are imported from@lingui/macro
, and we have only theirs nodes in thereferences
, all usages oft
are in the "gray" zone for the macro.So when we have a nested macro call it get transformed by it's own, not as part of outer expression.
This is solvable, and was solved in the original PR, but this feels bad, its fragile, hard to understand and probably not very performant (i didn't test though).
When things start complicating
So far, i covered simple, human written code. But that's not all.
Imagine the case:
This a completely valid code which may be even written by a person. Due to how
babel-macro-plugin
works our macro function will be called 2 times (for each import statement) with references belonging to each import:If you followed all my writings you can already guess, that our "parent matching method" described above will not work, since each invocation would have it's own array of nodes and mixing and matching macros referenced from one import with another would be treat as standalone macro calls and wouldn't be turned in one runtime i18n statement.
However, this hasn't happened often for users, or at least they don't open tickets for that.
But that is the case for experimental deps extractor. Look to the code generated by esbuild:
Esbuild bundle
Due to how bundler works, there might be multiple import statements in one file and usages of macro could be mixed and matched from different imports.
Solution
I decided to stop fighting with
babel-macro-plugin
and just write a good-old babel plugin using macro just as hook to run my plugin.Due to Visitor and DFS tree traversal we always start from the root node and go deeper processing every nested macro it might have. Also even if we forgot to handle some case, thanks to Visitor pattern this node could be picked up and processed by plugin even if didn't manually cover this case.
Additionally, plugin heavily relies on babel built-in methods to work with scopes/bindings, and instead of "naive" matching lingui's macros by it's name, now plugin uses
referencesImport()
which consider all scope bindings in the file. Let's look on example:Now this is not the case and plugin correctly understand scopes and shadowing of bindings.
Clash of inserted variables and existing ones
Previous implementation was so dumb about scopes and bindings that it may broke the runtime code entirely in specific circumstances:
Now the plugin creates a unique identifier for every inserted binding and transformed code looks like:
What's next?
In next iterations, i'm going to make this plugin publically available and start promoting it as a recomended way to setup lingui. So users which have access to the babel config may not need to install an additional
babel-macro-plugin
dependency and could use this plugin directly.The macro hook would still be available for that users which doesn't have access to babel config, such as CRA or gatsby but would be less promoted.
Currently, the plugin is private and not exposed publically from the package.
Types of changes
Fixes # (issue)
Checklist