1.2.0 • Published 6 months ago

rollup-plugin-jsx-if-for v1.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

rollup-plugin-jsx-if-for

This is a plugin for the Rollup bundler which rewrites JSX content (whether or not React is used) so certain "pseudo-component" tags are converted to corresponding Javascript expressions:

  • <$if test={expr}>...</$if> becomes {(expr) ? <>...</> : null}
  • <$for var="id" of={expr}>...</$for> becomes {(expr).map((id) => <> ... </>)}
  • <$let var="id" value={expr}>...</$let> becomes {((id) => <> ... </>)(expr)}

Note that var in <$for> and <$let> may be a variable name or a destructuring pattern, and of and value may be any Javascript expression:

<$for var="{x, y}" of {[{x: 1, y: 2}, {x: 3, y: 4}]}>
  <div>x is {x}, y is {y}</div>
</$for>

Why?

In most contexts you can and should write the {...} equivalent directly.

However, MDX (Markdown with JSX support) only allows Markdown content inside component tags, not inside Javascript curly braces. (See these discussions.)

So, while this plugin isn't technically MDX-specific, it exists mostly to deal with this MDX quirk and let you write conditions, loops, and local variable bindings around Markdown content. ("Traditional" template languages often use tag-based conditionals and loops in this way.)

Usage

Add this module:

npm i rollup-plugin-jsx-if-for

Configure Rollup to use it, in rollup.config.js or equivalent:

import rollupJsxIfFor from "rollup-plugin-jsx-if-for";
...
export default {
  ...
  plugins: [
    ...
    rollupJsxIfFor({ ... }),
  ],
};

This plugin's constructor takes conventional include and exclude options to subselect from the files Rollup is processing.

rollupJsxIfFor({
  include = ["**/*.mdx", "**/*.jsx"],
  exclude = [],
})

!NOTE List this plugin AFTER plugins which convert other formats into JS/JSX (eg. rollup-plugin-postcss), or in fact @mdx-js/rollup. Otherwise this plugin will fail trying to parse CSS or raw MDX or something.

Using this plugin with MDX

If you're using this plugin, you're probably also using @mdx-js/rollup.

Configure both MDX and Rollup so JSX-to-JS conversion happens in the Rollup core, not the MDX plugin:

import rollupJsxIfFor from "rollup-plugin-jsx-if-for";
import rollupMdx from "@mdx-js/rollup";
...
export default {
  ...
  jsx: { mode: "automatic", jsxImportSource: ... },
  ...
  plugins: [
    ...
    rollupMdx({ jsx: true }),  // output JSX tags in MDX output
    rollupJsxIfFor({ ... }),
    ...
  ],
};

Minimal example

Use this rollup.config.mjs

import fg from "fast-glob";
import rollupAutomountDom from "rollup-plugin-automount-dom";
import rollupHtml from "@rollup/plugin-html";
import rollupJsxIfFor from "rollup-plugin-jsx-if-for";
import rollupMdx from "@mdx-js/rollup";
import { nodeResolve as rollupNodeResolve } from "@rollup/plugin-node-resolve";

export default fg.sync("*.mdx").map((input) => ({
  input,
  jsx: { mode: "automatic", jsxImportSource: "jsx-dom" },
  output: { directory: "dist", format: "iife" },
  plugins: [
    rollupAutomountDom(),
    rollupHtml({ fileName: input.replace(/mdx$/, "html"), title: "" }),
    rollupJsxIfFor(),
    rollupMdx({ jsx: true }),
    rollupNodeResolve(),
  ],
}));

And this beer.mdx

<$let var="countdown" value={[...Array(100).keys()].reverse()}>
  <$for var="n" of={countdown}>
    <$if test={n > 0}>
      ## {n} bottles of beer on the wall, {n} bottles of beer
      Take one down and pass it around,
      <$if test={n > 1}>{n - 1}</$if> <$if test={n <= 1}>no more</$if>
      bottles of beer on the wall.
    </$if>
    <$if test={n == 0}>
      ## No more bottles of beer on the wall, no more bottles of beer
      Go to the store and buy some more, 99 bottles of beer on the wall
    </$if>
  </$for>
</$let>

And then run

npm i fast-glob jsx-dom @mdx-js/rollup rollup rollup-plugin-automount-dom @rollup/plugin-html rollup-plugin-jsx-if-for @rollup/plugin-node-resolve
npx rollup -c

And finally load dist/beer.html in your browser and you should see something like this

image

Tada! 🍺

Tips and Pitfalls

⚠️ Don't use MDX export inside tag scopes (use <$let> instead)

In MDX content, <$if>, <$for>, and <$let> tags will wrap Markdown/JSX, BUT ALL export directives are executed globally first. This will NOT work:

<$for var="i" of={[1, 2, 3]}>
  export const j = i * 2;  // WILL FAIL, is evaluated BEFORE and OUTSIDE the loop
  ## {i} times 2 is {j}   {/* WILL NOT WORK */}
</$for>

Instead, use <$let> for local bindings, like this:

<$for var="i" of={[1, 2, 3]}>
  <$let var="j" value={i * 2}>
    ## {i} times 2 is {j}
  </$let>
</$for>

ℹ️ Ways to avoid nested <$let> towers

If you find yourself with towers of annoyingly nested dependent <$let> tags:

<$let var="x" value={3.14159}>
  <$let var="y" value={x * x}>
    <$let var="z" value={y / (x + 1)}>
      ## x={x} y={y} z={z}
    </$let>
  </$let>
</$let>

Consider instead building an object in an immediately invoked function:

<$let var="{x, y, z}" value={(() =>
  const x = 3.14159;
  const y = x * x;
  const z = y / (x + 1);
  return {x, y, z};
)()}>
  ## x={x} y={y} z={z}
</$let>

You could also use a named function with MDX export (if the function can run in global scope):

export function getXYZ() {
  const x = 3.14159;
  const y = x * x;
  const z = y / (x + 1);
  return {x, y, z};
}

...
<$let var="{x, y, z}" value={getXYZ()}>
  ## x={x} y={y} z={z}
</$let>

You could even import the function from another module entirely, if that makes sense for you.

1.2.0

6 months ago

1.1.2

6 months ago

1.1.1

6 months ago

1.1.0

6 months ago

1.0.2

6 months ago

1.0.1

6 months ago

1.0.0

6 months ago