Repository: babel/babel (TypeScript)

Issue:
[Bug]: Fragments no longer work in mithril
### 💻

- [ ] Would you like to work on a fix?

### How are you using Babel?

babel-loader (webpack)

### Input code

unfortunately I can't provide a repo as it's from my company's gitlab which is private

### Configuration file name

babel.config.js

### Configuration

```
module.exports = {
	presets: ["@babel/preset-env"],
	plugins: [
		"@babel/plugin-proposal-object-rest-spread",
		["babel-plugin-root-import", { rootPathSuffix: "src" }],
		[
			"@babel/plugin-transform-react-jsx",
			{
				pragma: "m",
				pragmaFrag: "'['"
			}
		]
	],
	sourceMaps: true,
	retainLines: true,
	env: {
		test: {
			plugins: ["@babel/plugin-transform-modules-commonjs"]
		}
	}
};
```

### Current and expected behavior

when upgrading from 7.19.0 to 7.20.7 my mithril application stops working.  I have followed the steps outlined on their page and it was workign previously https://mithril.js.org/jsx.html.  Note that I am using babel.config.js though, not .babelrc as they recommend.  The issue seems to be with how 7.20.7 is recognizing my fragments.  When I change them to divs the application no longer fails but doesn't render properly because I would have to modify all the CSS to handle all the new nested divs.

I can't seem to find a list of what was changed from 7.19.0 ti 7.20.7 or the steps involved to upgrade my application

### Environment

  System:
    OS: Windows 10 10.0.19045
  Binaries:
    Node: 16.13.1 - C:\Program Files\nodejs\node.EXE
    npm: 8.14.0 - C:\Program Files\nodejs\npm.CMD
  npmPackages:
    @babel/plugin-proposal-object-rest-spread: ^7.20.7 => 7.20.7 
    @babel/plugin-transform-react-jsx: ^7.20.7 => 7.20.7         
    @babel/preset-env: ^7.20.2 => 7.20.2 
    babel-eslint: ^10.1.0 => 10.1.0 
    babel-jest: ^29.3.1 => 29.3.1
    babel-loader: ^9.1.2 => 9.1.2
    babel-plugin-root-import: ^6.6.0 => 6.6.0
    jest: ^29.3.1 => 29.3.1
    webpack: ^5.75.0 => 5.75.0

### Possible solution

_No response_

### Additional context

_No response_


Hints:
Hey @pdfabbro! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant [Slack community](https://babeljs.slack.com) that typically always has someone willing to help. You can sign-up [here](https://slack.babeljs.io/) for an invite.
Can you provide a repo to reproduce it?
Hi @liuxingbaoyu per my comment, unfortunately the repo is a company repo and I can't share it.
Sorry for not noticing this.
https://github.com/babel/babel/pull/15047
A recent relevant change might be this one.（v7.20.7）
However, since there is no reproducible example, it is not sure whether the cause can be found.
No problem, thanks for the responses!  Do you see anything in my config file that maybe needs to be changed to handle this?
No, I don't know much about jsx/react. Perhaps other members will have suggestions.
> No problem, thanks for the responses! Do you see anything in my config file that maybe needs to be changed to handle this?

As a workaround, please specify `pragmaFrag: "["` (without single quotes) when using Babel >= 7.20.7. Presumably, `pragmaFrag` should be something like `React.Fragment` (as a member expression) or `Fragment` (as an identifier). Passing a string literal `"'['"` is sort of uncharted territory. Anyway the breaking change here is never intended and I will try to restore it to the pre-7.20.7 behaviour.

```jsx
// Input
/* @jsxFrag '[' */
/* @jsx m */
<>Test</>;

// Output
// Before 7.20.7
m('[', null, "Test");
// After 7.20.7
m("'['", null, "Test");
```


Thank you!  I will try this tomorrow and let you know if it works
your suggestion of changing babel config to not have the quotes worked.  thank-you so much!

```
module.exports = {
	presets: ["@babel/preset-env"],
	plugins: [
		"@babel/plugin-proposal-object-rest-spread",
		["babel-plugin-root-import", { rootPathSuffix: "src" }],
		[
			"@babel/plugin-transform-react-jsx",
			{
				pragma: "m",
				pragmaFrag: "["
			}
		]
	],
	sourceMaps: true,
	retainLines: true,
	env: {
		test: {
			plugins: ["@babel/plugin-transform-modules-commonjs"]
		}
	}
};
```
@JLHwung thanks for taking a look at this.

For context, canonical fragment representation in user code in Mithril is either just an array of vnodes, or a `m.fragment(hooks?, ...children)` call (the default syntax for Mithril is hyperscript, but we also support JSX). Both syntaxes predate `React.Fragment` and the shorthand `<>` JSX support.

Our internal representation for fragments was a vnode with the string `'['` as a tag name, and for JSX compatibility, we added it to the public API (such that `m('[', ...)` works as intended after the Babel pass).

It was working with previous Babel versions, we didn't know it was by accident :-)
funny, I said to a colleague that I have a feeling that the mithril documentation was always wrong but it just happened to work previously by accident
The thing is, I don't understand the semantics of the workaround.

What I understood of the pragma is that it would pass whatever expression it was given to`m()`, hence the quotes within the quotes.

The workaround syntax looks like it should produce a syntax error (e.g. Since `@jsx m` and `@jsxFrag foo` gives `m(foo, ...)`, I'd expect a bare `[` to produce `m([,...)`).

@JLHwung is the workaround guaranteed to work in the future?
@pygy @pdfabbro The workaround will work before the next patch release. We will restore the behaviour so `pragmaFrag: "'['"` will work in the future Babel 7 release.

> the pragma is that it would pass whatever expression it was given to `m()`

This is an implementation detail and thus never an intended design goal. Currently, Babel treats `pragmaFrag:" React.Fragment"` as a member expression and generates `m(React.Fragment, ...)`, if `pragmaFrag` does not contain a `.`, Babel assumes it is a valid identifier name and create an identifier AST node from the given `pragmaFrag`. So internally `'['` is treated as an identifier and eventually printed as if it were an identifier, therefore you will see `'['` in the output. However, this approach will eventually _break_ in Babel 8 once we begin to verify the identifier names are actually valid.

In Babel 7.20.7 we first transform `<>...</>` to an JSXElement `<pragmaFrag>...</pragmaFrag>` and then re-use the JSX element transform. This approach works as long as `pragmaFrag` is a member expression or an identifier name, but apparently it will fail for `<'['>...</'['>`.

> Our internal representation for fragments was a vnode with the string '[' as a tag name, and for JSX compatibility, we added it to the public API (such that m('[', ...) works as intended after the Babel pass).

Would `mithril` consider migrating to the `automatic` runtime? Once finished, the new Babel config will be:

```js
[
  "@babel/plugin-transform-react-jsx", {
    importSource: "mithril/jsx-runtime",
    runtime: "automatic" // <-- This will be the Babel 8 default, users can remove it from their config
}]
```

For the example input
```jsx
<><div /></>;
```

The generated output will be
```js
import { jsx as _jsx, Fragment as _Fragment } from "mithril/jsx-runtime"
var x = _jsx(_Fragment, {
  children: _jsx("div", {})
});
// jsx can be a wrapper of `m` and Fragment can be the magic string `"["`
```

Pros here is that you don't have to leak internal implementation detail (`pragmaFrag` and `pragma`) to end users. All they need to know is there is a jsx-runtime handling every JSX concept. And this approach will work with Babel 7.9+ and Babel 8.
Once we migrate to esm (Mithril still uses cjs right now), I'll add support for the new automatic runtime. As a stopgap, we can define `m.jsxFrag = '['`.

On your end, for backwards and forward compatibility, could you consider, for `@jsx foo` and `@jsxFrag bar`, to parse `foo` and `bar` as js expressions, and have the main JSX transform create a `foo(bar)` expression in one pass rather than having an dedicated fragment pass?

The documentation is terse, just gives an example of the transform, and nowhere states that the `jsxFrag` pragma is expected to be a valid JSX tag name. We understood it as being a JS expression, we may not be the only ones...
@JLHwung What is the story for keyed fragments in `automatic` mode? could `< key={theKey}></>` become a thing?

Patch:
diff --git a/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts b/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts
--- a/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts
+++ b/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts
@@ -12,6 +12,7 @@ import type {
   Identifier,
   JSXAttribute,
   JSXElement,
+  JSXFragment,
   JSXOpeningElement,
   JSXSpreadAttribute,
   MemberExpression,
@@ -71,6 +72,9 @@ export default function createPlugin({
 
       throwIfNamespace = true,
 
+      // TODO (Babel 8): It should throw if this option is used with the automatic runtime
+      filter,
+
       runtime: RUNTIME_DEFAULT = process.env.BABEL_8_BREAKING
         ? "automatic"
         : development
@@ -276,24 +280,17 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
           // },
         },
 
-        JSXFragment(path, file) {
-          // <>...</>  ->  <React.Fragment>...</React.Fragment>
-
-          const frag = memberExpressionToJSX(get(file, "id/fragment")());
+        JSXFragment: {
+          exit(path, file) {
+            let callExpr;
+            if (get(file, "runtime") === "classic") {
+              callExpr = buildCreateElementFragmentCall(path, file);
+            } else {
+              callExpr = buildJSXFragmentCall(path, file);
+            }
 
-          path.replaceWith(
-            t.inherits(
-              t.jsxElement(
-                t.inherits(
-                  t.jsxOpeningElement(frag, []),
-                  path.node.openingFragment,
-                ),
-                t.jsxClosingElement(t.cloneNode(frag)),
-                path.node.children,
-              ),
-              path.node,
-            ),
-          );
+            path.replaceWith(t.inherits(callExpr, path.node));
+          },
         },
 
         JSXElement: {
@@ -603,6 +600,56 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
       return t.objectExpression(props);
     }
 
+    // Builds JSX Fragment <></> into
+    // Production: React.jsx(type, arguments)
+    // Development: React.jsxDEV(type, { children })
+    function buildJSXFragmentCall(
+      path: NodePath<JSXFragment>,
+      file: PluginPass,
+    ) {
+      const args = [get(file, "id/fragment")()];
+
+      const children = t.react.buildChildren(path.node);
+
+      args.push(
+        t.objectExpression(
+          children.length > 0
+            ? [
+                buildChildrenProperty(
+                  //@ts-expect-error The children here contains JSXSpreadChild,
+                  // which will be thrown later
+                  children,
+                ),
+              ]
+            : [],
+        ),
+      );
+
+      if (development) {
+        args.push(
+          path.scope.buildUndefinedNode(),
+          t.booleanLiteral(children.length > 1),
+        );
+      }
+
+      return call(file, children.length > 1 ? "jsxs" : "jsx", args);
+    }
+
+    // Builds JSX Fragment <></> into
+    // React.createElement(React.Fragment, null, ...children)
+    function buildCreateElementFragmentCall(
+      path: NodePath<JSXFragment>,
+      file: PluginPass,
+    ) {
+      if (filter && !filter(path.node, file)) return;
+
+      return call(file, "createElement", [
+        get(file, "id/fragment")(),
+        t.nullLiteral(),
+        ...t.react.buildChildren(path.node),
+      ]);
+    }
+
     // Builds JSX into:
     // Production: React.createElement(type, arguments, children)
     // Development: React.createElement(type, arguments, children, source, self)
@@ -806,22 +853,6 @@ function toMemberExpression(id: string): Identifier | MemberExpression {
   );
 }
 
-function memberExpressionToJSX(
-  expr: t.Node,
-): t.JSXMemberExpression | t.JSXIdentifier {
-  switch (expr.type) {
-    case "Identifier":
-      return t.jsxIdentifier(expr.name);
-    case "MemberExpression":
-      return t.jsxMemberExpression(
-        memberExpressionToJSX(expr.object),
-        memberExpressionToJSX(expr.property) as t.JSXIdentifier,
-      );
-    default:
-      throw new Error("Internal error: unknown member expression type");
-  }
-}
-
 function makeSource(path: NodePath, state: PluginPass) {
   const location = path.node.loc;
   if (!location) {


Test patch:
diff --git a/packages/babel-plugin-transform-react-inline-elements/test/fixtures/inline-elements/fragment/output.js b/packages/babel-plugin-transform-react-inline-elements/test/fixtures/inline-elements/fragment/output.js
--- a/packages/babel-plugin-transform-react-inline-elements/test/fixtures/inline-elements/fragment/output.js
+++ b/packages/babel-plugin-transform-react-inline-elements/test/fixtures/inline-elements/fragment/output.js
@@ -1 +1 @@
-/*#__PURE__*/babelHelpers.jsx(React.Fragment, {}, void 0, /*#__PURE__*/babelHelpers.jsx("span", {}), /*#__PURE__*/babelHelpers.jsx("div", {}));
+/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/babelHelpers.jsx("span", {}), /*#__PURE__*/babelHelpers.jsx("div", {}));
diff --git a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/linux/auto-import-dev/output.mjs b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/linux/auto-import-dev/output.mjs
--- a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/linux/auto-import-dev/output.mjs
+++ b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/linux/auto-import-dev/output.mjs
@@ -1,7 +1,7 @@
 var _jsxFileName = "<CWD>/packages/babel-plugin-transform-react-jsx-development/test/fixtures/linux/auto-import-dev/input.js";
-import { Fragment as _Fragment } from "react/jsx-dev-runtime";
 import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
 import { createElement as _createElement } from "react";
+import { Fragment as _Fragment } from "react/jsx-dev-runtime";
 var x = /*#__PURE__*/_jsxDEV(_Fragment, {
   children: /*#__PURE__*/_jsxDEV("div", {
     children: [/*#__PURE__*/_jsxDEV("div", {}, "1", false, {
diff --git a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/windows/auto-import-dev-windows/output.mjs b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/windows/auto-import-dev-windows/output.mjs
--- a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/windows/auto-import-dev-windows/output.mjs
+++ b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/windows/auto-import-dev-windows/output.mjs
@@ -1,7 +1,7 @@
 var _jsxFileName = "<CWD>\\packages\\babel-plugin-transform-react-jsx-development\\test\\fixtures\\windows\\auto-import-dev-windows\\input.js";
-import { Fragment as _Fragment } from "react/jsx-dev-runtime";
 import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
 import { createElement as _createElement } from "react";
+import { Fragment as _Fragment } from "react/jsx-dev-runtime";
 var x = /*#__PURE__*/_jsxDEV(_Fragment, {
   children: /*#__PURE__*/_jsxDEV("div", {
     children: [/*#__PURE__*/_jsxDEV("div", {}, "1", false, {
diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/autoImport/auto-import-react-source-type-module/output.mjs b/packages/babel-plugin-transform-react-jsx/test/fixtures/autoImport/auto-import-react-source-type-module/output.mjs
--- a/packages/babel-plugin-transform-react-jsx/test/fixtures/autoImport/auto-import-react-source-type-module/output.mjs
+++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/autoImport/auto-import-react-source-type-module/output.mjs
@@ -1,7 +1,7 @@
-import { Fragment as _Fragment } from "react/jsx-runtime";
 import { jsx as _jsx } from "react/jsx-runtime";
 import { createElement as _createElement } from "react";
 import { jsxs as _jsxs } from "react/jsx-runtime";
+import { Fragment as _Fragment } from "react/jsx-runtime";
 var x = /*#__PURE__*/_jsx(_Fragment, {
   children: /*#__PURE__*/_jsxs("div", {
     children: [/*#__PURE__*/_jsx("div", {}, "1"), /*#__PURE__*/_jsx("div", {
diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/does-not-add-source-self-automatic/output.mjs b/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/does-not-add-source-self-automatic/output.mjs
--- a/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/does-not-add-source-self-automatic/output.mjs
+++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/does-not-add-source-self-automatic/output.mjs
@@ -1,7 +1,7 @@
-import { Fragment as _Fragment } from "react/jsx-runtime";
 import { jsx as _jsx } from "react/jsx-runtime";
 import { createElement as _createElement } from "react";
 import { jsxs as _jsxs } from "react/jsx-runtime";
+import { Fragment as _Fragment } from "react/jsx-runtime";
 var x = /*#__PURE__*/_jsx(_Fragment, {
   children: /*#__PURE__*/_jsxs("div", {
     children: [/*#__PURE__*/_jsx("div", {}, "1"), /*#__PURE__*/_jsx("div", {
diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/handle-fragments/output.mjs b/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/handle-fragments/output.mjs
--- a/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/handle-fragments/output.mjs
+++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/handle-fragments/output.mjs
@@ -1,5 +1,5 @@
-import { Fragment as _Fragment } from "react/jsx-runtime";
 import { jsx as _jsx } from "react/jsx-runtime";
+import { Fragment as _Fragment } from "react/jsx-runtime";
 var x = /*#__PURE__*/_jsx(_Fragment, {
   children: /*#__PURE__*/_jsx("div", {})
 });
diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/should-allow-nested-fragments/output.mjs b/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/should-allow-nested-fragments/output.mjs
--- a/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/should-allow-nested-fragments/output.mjs
+++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/react-automatic/should-allow-nested-fragments/output.mjs
@@ -1,5 +1,5 @@
-import { Fragment as _Fragment } from "react/jsx-runtime";
 import { jsx as _jsx } from "react/jsx-runtime";
+import { Fragment as _Fragment } from "react/jsx-runtime";
 import { jsxs as _jsxs } from "react/jsx-runtime";
 /*#__PURE__*/_jsx("div", {
   children: /*#__PURE__*/_jsxs(_Fragment, {
diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/input.js b/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/input.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/input.js
@@ -0,0 +1 @@
+<>Test</>;
diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/options.json b/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/options.json
new file mode 100644
--- /dev/null
+++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/options.json
@@ -0,0 +1,9 @@
+{
+  "BABEL_8_BREAKING": false,
+  "plugins": [
+    [
+      "transform-react-jsx",
+      { "pragma": "m", "pragmaFrag": "'['", "runtime": "classic" }
+    ]
+  ]
+}
diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/output.js b/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/output.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/regression/issue-15353-classic/output.js
@@ -0,0 +1 @@
+m('[', null, "Test");