Explaining some quirks and design decisions:
I really didn't like designing this. Variables are hard! It makes sense to support them but without running in a real JS runtime concepts like declaration, scope, import/export, nested objects, and updates all don't make sense anymore.
Variables were orignally set in JS like const/let/var x = decl`...`
where
macro decl
had to be a TagTemplateExpression
(to enforce that ${...}
used
only other decl
statements). Then the entire variable declaration was straight
up removed from the code (!). This is bad because it treats decl
like a
variable when it's really not... People can easily export the variable or mutate
it and their editor will say it's fine.
The next version didn't use VariableDeclaration
to get the variable name.
Instead you treat decl
as an object and write to it like decl.[...] = '...'
.
This enters a huge mess. Now variables are global. In --watch
(and not) the
last write wins and must update all references in the project. However, as
learned in the below section on classnames, it's not possible to use Babel to
modify already written JS files - so I can't support exporting decl
to a
string to be used in JS code. I can only touch the CSS output. This is bad
because I'm treating decl
like a variable that can be set but not read unless
it's in a css
or injectGlobal
block, and that changes update CSS but not JS.
Also people will want to be able to export into JS-land. That's important.
The last/current version sets variables in the Babel config as JSON. No
assignment in JS! This also means it's intuitive that changes to variables will
require a full reload (not just a --watch
save). That's also great because it
doesn't let people get too deep into variable tricks - it's JSON.
This macro supports the babel --watch
command to update the takeout on each
save in your editor. Unfortunately there's no way to know when Babel is done
compiling and files are processed one at a time in isolation. I managed to
get around this by adding a process.on('exit', ...)
listener, since Babel
must be done if the process is exiting. This works for any tools that are Node
processes like Webpack/Rollup, too. However, for --watch
the process hangs
forever (that's the point)...there is a console.log
message on each hot-update
though: "Successfully compiled N files with Babel (XXXms)". In Node the stdout
stream write-only, but the function process.stdout.write
can be replaced - so
I wrapped it and look for "Successfully compiled". You can change the string
with "stdoutSearchString" or turn off the patch with "stdoutPatch".
I didn't want to use a hash or random number for the classname because in my
experience they're meaningless at a glance - .sc-JwXcy
doesn't tell me
anything. The idea was to use the filename only, and if there was a collision,
reconcile by adjusting each of the conflicting names to use their parent
directory (as many times as needed).
For example, a conflicted Grouper.jsx
component would have conflicts resolve
into Dropdown/Grouper.jsx
and List/Grouper.jsx
.
Originally I collected all filenames and did conflict resolution just before writing the takeout (on process exit, likely). This didn't work. I imagine that the AST had already been serialized by then, so changes weren't observed.
Next I tried to update on the fly - this work is at the HEAD of unique-paths
for reference - which should work but I think Babel must be serializing and
writing the AST for each file as it's encountered. Once the macro is working
on another file, references to the previous file's AST don't work. Ugh.
Even if it did work though, it adds a lot of code complexity. 1/3 of the code was filepath reconcilation...
Settled on an auto-incrementing number +N
since it at least gives you the
filename at a glance, which is more meaningful than a hash. The mapping of N to
the full filepath is written alongside the CSS takeout for when you need to find
the source.