kentcdodds/babel-plugin-macros

Feature request: support macros importing macros

Janpot opened this issue · 13 comments

I'm trying to write a macro that is to be used inside another macro but that doesn't seem to work.
Would it be possible to recursively call the plugin on imported macros?
Another use case would be that I could define a macro in my project that itself is transpiled with the same configuration as my project.

Hi @Janpot.

Hmmm... Could you show me some example code of what you'd want to be able to do?

I'm trying out making macros to help babel ast creation. Like:

const { createMacro } = require('babel-plugin-macros');
const { quote, unquote, evaluate } = require('ast-tools/macro');
module.exports = createMacro(({ babel, state, references: { default: paths } }) => {
  paths.forEach(({ parentPath }) => {
    const toAdd = 20;
    parentPath.replaceWith(quote(10 + unquote(toAdd)));
  });
});

where quote(expr) would be replaced with the ast of expr, unquote would escape values in there, evaluate would be like preval-ing ast's within a certain environment, etc...

quote(10 + unquote(toAdd))

would be replaced with an object that would be like the result of

babel.transform(`(10 + ${toAdd})`).ast

I'm basically just experimenting with meta-programming in javascript in general and trying to wrap my head around a few concepts I borrowed from previous experiences. But this is just me hitting a wall in the plugin during experimenting. If it succeeds, I could see this being useful in ast creation around babel tooling in general. It'd be a bit like a babel-template on steroids, let's say.

Another way I could see this being useful is when you want to create a macro that is living inside your project, as one of the files. You'd likely want it transpiled just like the rest of your code so that you can use the same features, like ES6 modules and any other feature you'd use babel for in the first place.

I understand these examples come over as a bit contrived but I'm just a bit in a learning/experimenting mode right now and I'm not even sure I'm on a right track anyway.

Interesting! I don't think I want to add this right now. Why don't you go ahead and experiment with it yourself. Once you have a solid use case then feel free to come back. But honestly I don't see a reason to use macros to help build a macro. Just make utility functions that run at Babel transpilation runtime...

@kentcdodds btw, Just doing some quick tests and it seems that all it needs is changing this line to

  const register = require('babel-register');
  register({ cache: false });
  const macro = require(requirePath);
  register.revert();

with babel-register@next.

This shouldn't influence the current supported functionality at all since node_modules are ignored by babel-register anyway.

Now babel-plugin-macros is able to import any local file as a macro while respecting the surrounding babel configuration at the same time.

And as far as a solid use-case goes, I have some project around that does some compile time interpolation of i18n messages in a webpack loader, that loader is built local to my project. It's not general enough to publish to npm. Were I have to do this in the future, I'd be able to use create-react-app without ejecting and use a macro local to the project.

Were I have to do this in the future, I'd be able to use create-react-app without ejecting and use a macro local to the project.

I think you misunderstand. You can actually do that with CRA v2 (alpha) already. You don't need any changes to babel-plugin-macros! It already works! Have you given it a try?

@kentcdodds I'm not contesting that this works:

// ./hello.macro.js
const { createMacro } = require('babel-plugin-macros');
module.exports = createMacro(({ babel, state, references }) => {
  references.default.forEach(({ parentPath }) => {
    parentPath.replaceWith(babel.types.stringLiteral('hello'));
  });
});

But this fails with a SyntaxError: Unexpected token import:

// ./world.macro.js
import { createMacro } from 'babel-plugin-macros';

export default createMacro(({ babel, state, references }) => {
  references.default.forEach(({ parentPath }) => {
    parentPath.replaceWith(babel.types.stringLiteral('world'));
  });
});

I think a user would expect that he can write a macro in CRA in the same style as the rest of the project.

OOoooh, I see what you're suggesting. So you don't care that a macro can be used inside a macro, you're more concerned that your macro be transpiled on the fly so you can use the same syntax in the macro that you use in your source.

Yes, I'm in favor of this change. Feel free to open a pull request to make that happen 👍

Thanks for your patience!

Well, My point is also that being able to use a macro inside a macro automatically follows from this functionality since the macro would be transpiled with a babel configuration that has the macro plugin enabled.
The priniciple works in a test repo of mine but I haven't been able to replicate it quite yet in a CRA repo because of clashing babel versions. In order to get this to work I need to either copy part of babel-register or make babel-register use the same version of babel that comes in babel-plugin-macros.
I'll see if I can make a PR for that.

Sorry, I'm not interested in copying part of babel-register. I'd like to hear more about why it didn't just work for you...

I don't want that either.
It didn't wok because this version of babel seems to be different from the one in CRA. And it gave me errors that it didn't understand certain (newer) AST types like JSXFragment.
I think it's a matter of making babel injectable in babel-register so that I can transpile the macro with the same babel that is used in your plugin.

Hmm... Thanks for looking into that. What if you use babel-register in your macro entry file, then require another file?

Do you mean as in

// ./world.macro.js
require('babel-register');
require('./worldMacro.js');

with:

// ./worldMacro.js
import { createMacro } from 'babel-plugin-macros';

export default createMacro(({ babel, state, references }) => {
  references.default.forEach(({ parentPath }) => {
    parentPath.replaceWith(babel.types.stringLiteral('world'));
  });
});

?
That will probably work. But I think the real solution would be to try and get babel-register patched.

I agree with you. But honestly the only reason that I think it'd be worth putting any work into making this work well would be to support ESModules and that's not reason enough for me.

If you want to put in work for a pull request to make it a good experience then you're welcome to do so, as long as the end result is as simple as what you showed before:

const register = require('babel-register');
register({ cache: false });
const macro = require(requirePath);
register.revert();

I'm going to go ahead and close this because it's not a priority of the project. But feel free to open a pull request if you like and if it's that simple then I'll merge it 👍