Skip to content

Commit

Permalink
Fix jsx pragma breaking @compiled/babel-plugin-strip-runtime styleshe…
Browse files Browse the repository at this point in the history
…et extraction (#1557)
  • Loading branch information
dddlr authored Nov 14, 2023
1 parent a11a097 commit fbc17ed
Show file tree
Hide file tree
Showing 13 changed files with 348 additions and 34 deletions.
9 changes: 9 additions & 0 deletions .changeset/lovely-planes-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@compiled/babel-plugin-strip-runtime': minor
'@compiled/eslint-plugin': minor
'@compiled/utils': minor
---

- `@compiled/babel-plugin-strip-runtime`: Fix `css` function calls not being extracted when using classic JSX pragma syntax and `@babel/preset-react` is turned on. Now, when the classic JSX pragma syntax is used for Compiled and `@babel/preset-react` is turned on (assuming `@babel/preset-react` runs before `@compiled/babel-plugin-strip-runtime`), the JSX pragma and the `jsx` import will be completely removed in the output.
- `@compiled/eslint-plugin`: Change regex in `jsx-pragma` rule to match @babel/plugin-transform-react-jsx
- `@compiled/utils`: Change regex in `jsx-pragma` rule to match @babel/plugin-transform-react-jsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,194 @@ describe('babel-plugin-strip-runtime using source code', () => {
);
});
});

describe('with jsx pragma', () => {
it('work with classic jsx pragma', () => {
const codeWithPragma = `
/** @jsx myJsx */
import { css, jsx as myJsx } from '@compiled/react';
const Component = () => (
<div css={{ fontSize: 12, color: 'blue' }}>
hello world 2
</div>
);
const Component2 = () => (
<div css={css({ fontSize: 12, color: 'pink' })}>
hello world 2
</div>
);
`;

const actual = transform(codeWithPragma, {
run: 'both',
runtime: 'classic',
});

expect(actual).toMatchInlineSnapshot(`
"/* app.tsx generated by @compiled/babel-plugin v0.0.0 */
import * as React from 'react';
import { ax, ix } from '@compiled/react/runtime';
const Component = () =>
/*#__PURE__*/ React.createElement(
'div',
{
className: ax(['_1wyb1fwx _syaz13q2']),
},
'hello world 2'
);
const Component2 = () =>
/*#__PURE__*/ React.createElement(
'div',
{
className: ax(['_1wyb1fwx _syaz32ev']),
},
'hello world 2'
);
"
`);
});

it('work with automatic jsx pragma', () => {
const codeWithPragma = `
/** @jsxImportSource @compiled/react */
import { css } from '@compiled/react';
const Component = () => (
<div css={{ fontSize: 12, color: 'blue' }}>
hello world 2
</div>
);
const Component2 = () => (
<div css={css({ fontSize: 12, color: 'pink' })}>
hello world 2
</div>
);
`;

const actual = transform(codeWithPragma, {
run: 'both',
runtime: 'automatic',
});

expect(actual).toMatchInlineSnapshot(`
"/* app.tsx generated by @compiled/babel-plugin v0.0.0 */
import { ax, ix } from '@compiled/react/runtime';
import { jsxs as _jsxs } from '@compiled/react/jsx-runtime';
import { jsx as _jsx } from '@compiled/react/jsx-runtime';
/** @jsxImportSource @compiled/react */
const Component = () =>
_jsx('div', {
className: ax(['_1wyb1fwx _syaz13q2']),
children: 'hello world 2',
});
const Component2 = () =>
_jsx('div', {
className: ax(['_1wyb1fwx _syaz32ev']),
children: 'hello world 2',
});
"
`);
});

it('work with classic jsx pragma with extractStylesToDirectory', () => {
const codeWithPragma = `
/** @jsx myJsx */
import { css, jsx as myJsx } from '@compiled/react';
const Component = () => (
<div css={{ fontSize: 12, color: 'blue' }}>
hello world 2
</div>
);
const Component2 = () => (
<div css={css({ fontSize: 12, color: 'pink' })}>
hello world 2
</div>
);
`;

const actual = transform(codeWithPragma, {
run: 'both',
runtime: 'classic',
extractStylesToDirectory: { source: 'src/', dest: 'dist/' },
});

expect(actual).toMatchInlineSnapshot(`
"/* app.tsx generated by @compiled/babel-plugin v0.0.0 */
import './app.compiled.css';
import * as React from 'react';
import { ax, ix } from '@compiled/react/runtime';
const Component = () =>
/*#__PURE__*/ React.createElement(
'div',
{
className: ax(['_1wyb1fwx _syaz13q2']),
},
'hello world 2'
);
const Component2 = () =>
/*#__PURE__*/ React.createElement(
'div',
{
className: ax(['_1wyb1fwx _syaz32ev']),
},
'hello world 2'
);
"
`);
});

it('work with automatic jsx pragma with extractStylesToDirectory', () => {
const codeWithPragma = `
/** @jsxImportSource @compiled/react */
import { css } from '@compiled/react';
const Component = () => (
<div css={{ fontSize: 12, color: 'blue' }}>
hello world 2
</div>
);
const Component2 = () => (
<div css={css({ fontSize: 12, color: 'pink' })}>
hello world 2
</div>
);
`;

const actual = transform(codeWithPragma, {
run: 'both',
runtime: 'automatic',
extractStylesToDirectory: { source: 'src/', dest: 'dist/' },
});

expect(actual).toMatchInlineSnapshot(`
"/* app.tsx generated by @compiled/babel-plugin v0.0.0 */
import './app.compiled.css';
import { ax, ix } from '@compiled/react/runtime';
import { jsxs as _jsxs } from '@compiled/react/jsx-runtime';
import { jsx as _jsx } from '@compiled/react/jsx-runtime';
/** @jsxImportSource @compiled/react */
const Component = () =>
_jsx('div', {
className: ax(['_1wyb1fwx _syaz13q2']),
children: 'hello world 2',
});
const Component2 = () =>
_jsx('div', {
className: ax(['_1wyb1fwx _syaz32ev']),
children: 'hello world 2',
});
"
`);
});
});
});

describe('when run in subsequent steps', () => {
Expand All @@ -257,14 +445,13 @@ describe('babel-plugin-strip-runtime using source code', () => {
const baked = transform(code, { run: 'bake', runtime });
const actual = transform(baked, { run: 'extract', runtime });

// TODO: This is missing the PURE pragma in the Component return. Fix this.
expect(actual).toMatchInlineSnapshot(`
"/* app.tsx generated by @compiled/babel-plugin v0.0.0 */
import { ax, ix } from '@compiled/react/runtime';
import { jsxs as _jsxs } from 'react/jsx-runtime';
import { jsx as _jsx } from 'react/jsx-runtime';
const Component = () =>
_jsx('div', {
/*#__PURE__*/ _jsx('div', {
className: ax(['_1wyb1fwx _syaz13q2']),
children: 'hello world',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('babel-plugin-strip-runtime using transpiled code', () => {

expect(actual.split('var Component = ')[1]).toMatchInlineSnapshot(`
"() =>
(0, _jsxRuntime.jsx)('div', {
/*#__PURE__*/ (0, _jsxRuntime.jsx)('div', {
className: (0, _runtime.ax)(['_1wyb1fwx _syaz13q2']),
children: 'hello world',
});
Expand All @@ -114,7 +114,7 @@ describe('babel-plugin-strip-runtime using transpiled code', () => {
import { jsxs as _jsxs } from 'react/jsx-runtime';
import { jsx as _jsx } from 'react/jsx-runtime';
var Component = () =>
_jsx('div', {
/*#__PURE__*/ _jsx('div', {
className: ax(['_1wyb1fwx _syaz13q2']),
children: 'hello world',
});
Expand Down Expand Up @@ -150,7 +150,7 @@ describe('babel-plugin-strip-runtime using transpiled code', () => {

expect(actual.split('var Component = ')[1]).toMatchInlineSnapshot(`
"() =>
React.createElement(
/*#__PURE__*/ React.createElement(
'div',
{
className: (0, _runtime.ax)(['_1wyb1fwx _syaz13q2']),
Expand All @@ -169,7 +169,7 @@ describe('babel-plugin-strip-runtime using transpiled code', () => {
import * as React from 'react';
import { ax, ix } from '@compiled/react/runtime';
var Component = () =>
React.createElement(
/*#__PURE__*/ React.createElement(
'div',
{
className: ax(['_1wyb1fwx _syaz13q2']),
Expand Down
66 changes: 63 additions & 3 deletions packages/babel-plugin-strip-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,43 @@ import { dirname, join, parse } from 'path';

import { declare } from '@babel/helper-plugin-utils';
import template from '@babel/template';
import type { NodePath } from '@babel/traverse';
import type { NodePath, Visitor } from '@babel/traverse';
import * as t from '@babel/types';
import { sort } from '@compiled/css';
import { preserveLeadingComments } from '@compiled/utils';

import type { PluginPass, PluginOptions, BabelFileMetadata } from './types';
import { getClassicJsxPragma } from './utils/get-jsx-pragma';
import { isAutomaticRuntime } from './utils/is-automatic-runtime';
import { isCCComponent } from './utils/is-cc-component';
import { isCreateElement } from './utils/is-create-element';
import { removeStyleDeclarations } from './utils/remove-style-declarations';
import { toURIComponent } from './utils/to-uri-component';

const FindJsxPragmaImport: Visitor<PluginPass> = {
ImportSpecifier(path, state) {
const specifier = path.node;

t.assertImportDeclaration(path.parent);
// We don't care about other libraries
if (path.parent.source.value !== '@compiled/react') return;

if (
(specifier.imported.type === 'StringLiteral' && specifier.imported.value === 'jsx') ||
(specifier.imported.type === 'Identifier' && specifier.imported.name === 'jsx')
) {
// Hurrah, we know that the jsx function in the JSX pragma refers to the
// jsx function from Compiled.
state.jsxPragmaIsCompiled = true;

// Remove the jsx import; the assumption is that we removed the classic JSX pragma, so
// Babel shouldn't convert React.createElement to the jsx function anymore.
path.remove();
return;
}
},
};

export default declare<PluginPass>((api) => {
api.assertVersion(7);

Expand All @@ -25,7 +50,39 @@ export default declare<PluginPass>((api) => {
},
visitor: {
Program: {
enter(path, { file }) {
const classicJsxPragma = getClassicJsxPragma(file.ast.comments);
this.classicJsxPragmaName = classicJsxPragma?.name;
if (!this.classicJsxPragmaName) return;

// Delete comment so that @babel/preset-react doesn't see it and convert all of the
// React.createElement function calls to jsx function calls
if (classicJsxPragma?.comment) {
file.ast.comments = file.ast.comments?.filter(
(comment) => comment !== classicJsxPragma.comment
);

// Babel provides no way for us to traverse comments >:(
//
// So the best we can do is guess that the JSX pragma is probably at the start of
// the file.
if (path.node.body[0].leadingComments) {
path.node.body[0].leadingComments = path.node.body[0].leadingComments.filter(
(comment) => comment !== classicJsxPragma.comment
);
}
}

path.traverse<PluginPass>(FindJsxPragmaImport, this);
},

exit(path, { file, filename }) {
if (!filename) {
throw new Error(
`@compiled/babel-plugin-strip-runtime expected the filename not to be empty, but actually got '${filename}'.`
);
}

if (this.opts.compiledRequireExclude) {
// Rather than inserting styleRules to the code, inserting them to metadata in the case like SSR
if (!file.metadata?.styleRules) file.metadata.styleRules = [];
Expand Down Expand Up @@ -90,11 +147,13 @@ export default declare<PluginPass>((api) => {
}
},
},

ImportSpecifier(path) {
if (t.isIdentifier(path.node.imported) && ['CC', 'CS'].includes(path.node.imported.name)) {
path.remove();
}
},

JSXElement(path, pass) {
if (!t.isJSXIdentifier(path.node.openingElement.name)) {
return;
Expand Down Expand Up @@ -122,6 +181,7 @@ export default declare<PluginPass>((api) => {
path.node.leadingComments = null;
return;
},

CallExpression(path, pass) {
const callee = path.node.callee;
if (isCreateElement(callee)) {
Expand All @@ -139,8 +199,8 @@ export default declare<PluginPass>((api) => {
removeStyleDeclarations(compiledStyles.node, path, pass);

// All done! Let's replace this node with the user land child.
path.replaceWith(nodeToReplace);
path.node.leadingComments = null;
path.replaceWith(nodeToReplace);
return;
}

Expand Down Expand Up @@ -177,8 +237,8 @@ export default declare<PluginPass>((api) => {
removeStyleDeclarations(compiledStyles, path, pass);

// All done! Let's replace this node with the user land child.
path.replaceWith(nodeToReplace);
path.node.leadingComments = null;
path.replaceWith(nodeToReplace);
return;
}
},
Expand Down
Loading

0 comments on commit fbc17ed

Please sign in to comment.