Skip to content

Commit

Permalink
Add babel plugin to optimize web output
Browse files Browse the repository at this point in the history
The babel plugin can be used by web builds to inline the component
wrapper and remove the runtime overhead.

Fix #23
  • Loading branch information
necolas committed Feb 26, 2024
1 parent 18bcf5f commit 75f19ec
Show file tree
Hide file tree
Showing 14 changed files with 688 additions and 193 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ jobs:
with:
name: "runtime library"
build-script: "build"
pattern: "./packages/react-strict-dom/dist/{dom.js,native.js}"
pattern: "./packages/react-strict-dom/dist/{dom.js,native.js,runtime.js}"
repo-token: "${{ secrets.GITHUB_TOKEN }}"
2 changes: 2 additions & 0 deletions apps/examples/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

const stylexPlugin = require('@stylexjs/babel-plugin');
const rsdPlugin = require('react-strict-dom/babel');

function getPlatform(caller) {
return caller && caller.platform;
Expand All @@ -29,6 +30,7 @@ module.exports = function (api) {
const plugins = [];

if (platform === 'web') {
plugins.push(rsdPlugin);
plugins.push([
stylexPlugin,
{
Expand Down
6 changes: 4 additions & 2 deletions apps/examples/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ export default function App(): React.MixedElement {
<ScrollView>
<html.div style={egStyles.div}>
<ExampleBlock title="HTML elements">
<html.div data-testid="testid">div</html.div>
<html.div data-testid="testid" role="none">
div
</html.div>
<html.span suppressHydrationWarning={true}>span</html.span>
<html.p>paragraph</html.p>

Expand Down Expand Up @@ -147,7 +149,7 @@ export default function App(): React.MixedElement {
/>

<html.div />
<html.label>label</html.label>
<html.label for="id">label</html.label>
<html.div />
<html.button>button</html.button>
<html.div />
Expand Down
3 changes: 3 additions & 0 deletions configs/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ module.exports = {
'<rootDir>/packages/react-strict-dom/tests/*-test.js',
'<rootDir>/packages/react-strict-dom/tests/*-test.native.js'
],
testPathIgnorePatterns: [
'<rootDir>/packages/react-strict-dom/tests/babel-test.js'
],
transform: {
'\\.[jt]sx?$': ['babel-jest', babelConfig()]
}
Expand Down
13 changes: 13 additions & 0 deletions configs/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ const webConfigs = [
},
plugins: [...ossPlugins]
},
// Runtime
{
external: ['react', 'react-dom', '@stylexjs/stylex'],
input: require.resolve('../packages/react-strict-dom/src/dom/runtime.js'),
output: {
file: path.join(
__dirname,
'../packages/react-strict-dom/dist/runtime.js'
),
format: 'es'
},
plugins: [...ossPlugins]
},
// www build
{
external: ['react', 'react-dom', '@stylexjs/stylex'],
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@babel/cli": "^7.22.0",
"@babel/core": "^7.22.0",
"@babel/eslint-parser": "^7.22.0",
"@babel/helper-module-imports": "^7.22.15",
"@babel/plugin-transform-flow-comments": "^7.22.0",
"@babel/plugin-transform-runtime": "^7.22.0",
"@babel/preset-env": "^7.22.0",
Expand Down
45 changes: 39 additions & 6 deletions packages/react-strict-dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react-strict-dom/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react-strict-dom.svg?style=flat)](https://www.npmjs.com/package/react-strict-dom)

![web](https://img.badgesize.io/https:/www.unpkg.com/react-strict-dom@latest/dist/dom.js?label=web&compression=brotli)
![web (prod)](https://img.badgesize.io/https:/www.unpkg.com/react-strict-dom@latest/dist/runtime.js?label=web%20(prod)&compression=brotli)
![web (dev)](https://img.badgesize.io/https:/www.unpkg.com/react-strict-dom@latest/dist/dom.js?label=web%20(dev)&compression=brotli)
![native](https://img.badgesize.io/https:/www.unpkg.com/react-strict-dom@latest/dist/native.js?label=native&compression=brotli)

**React Strict DOM** (RSD) is an experimental integration of [React DOM](https://react.dev/) and [StyleX](https://stylexjs.com/) that aims to improve and standardize the development of styled React components for web and native. The goal of RSD is to improve the speed and efficiency of React development without compromising on performance, reliability, or quality. Building with RSD is helping teams at Meta ship features faster, to more platforms, with fewer engineers.
Expand Down Expand Up @@ -31,11 +32,43 @@ npm install --dev @stylexjs/babel-plugin
Configure the `importSources` option for the StyleX Babel plugin or equivalent bundler integration.

```js
styleXBabelPlugin({
importSources: [
{ from: 'react-strict-dom', as: 'css '}
]
})
// babel.config.dom.js

import styleXBabelPlugin from '@stylexjs/babel-plugin';

module.exports = function () {
return {
plugins: [
styleXBabelPlugin({
importSources: [
{ from: 'react-strict-dom', as: 'css '}
]
})
]
}
};
```

Optionally use the RSD optimizing Babel plugin for improved runtime performance.

```js
// babel.config.dom.js

import rsdPlugin from 'react-strict-dom/babel';
import styleXBabelPlugin from '@stylexjs/babel-plugin';

module.exports = function () {
return {
plugins: [
rsdPlugin,
styleXBabelPlugin({
importSources: [
{ from: 'react-strict-dom', as: 'css '}
]
})
]
}
};
```

**For native**
Expand Down
168 changes: 168 additions & 0 deletions packages/react-strict-dom/babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const { addNamed, addNamespace } = require('@babel/helper-module-imports');

module.exports = function ({ types: t }) {
const packageName = 'react-strict-dom';
const packageRuntime = 'react-strict-dom/dist/runtime';
const findImportDeclaration = (body, sourceValue) =>
body.filter(
(node) =>
node.type === 'ImportDeclaration' && node.source.value === sourceValue
);
const findHtmlSpecifier = (specifiers) =>
specifiers
? specifiers.filter((specifier) => specifier.imported.name === 'html')
: [];
let defaultStylesImportIdentifier;
let stylexImportIdentifier;

return {
visitor: {
Program: {
enter(path) {
const importDeclarations = findImportDeclaration(
path.node.body,
packageName
);
if (importDeclarations.length > 0) {
defaultStylesImportIdentifier = addNamed(
path,
'defaultStyles',
packageRuntime
);
stylexImportIdentifier = addNamespace(path, '@stylexjs/stylex', {
nameHint: 'stylex'
});
path.scope.rename(
'defaultStyles',
defaultStylesImportIdentifier.name
);
path.scope.rename('stylex', stylexImportIdentifier.name);
}
}
},
JSXMemberExpression(path, state) {
const importDeclarations = findImportDeclaration(
state.file.ast.program.body,
packageName
);
if (!importDeclarations) return;
const htmlSpecifiers = importDeclarations.flatMap((declaration) =>
findHtmlSpecifier(declaration.specifiers)
);
if (
htmlSpecifiers.some(
(specifier) => path.node.object.name === specifier.local.name
)
) {
path.replaceWith(t.jsxIdentifier(path.node.property.name));
}
},
JSXOpeningElement(path, state) {
const importDeclarations = findImportDeclaration(
state.file.ast.program.body,
packageName
);
if (!importDeclarations) return;
const htmlSpecifiers = importDeclarations.flatMap((declaration) =>
findHtmlSpecifier(declaration.specifiers)
);
if (
htmlSpecifiers.some(
(specifier) =>
t.isJSXMemberExpression(path.node.name) &&
path.node.name.object.name === specifier.local.name
)
) {
let styleAttributeExists = false;
let typeAttributeExists = false;
let dirAttributeExists = false;

path.node.attributes.forEach((attribute, index) => {
if (t.isJSXAttribute(attribute) && attribute.name.name === 'for') {
attribute.name.name = 'htmlFor';
}
if (
t.isJSXAttribute(attribute) &&
attribute.name.name === 'role' &&
attribute.value.value === 'none'
) {
attribute.value.value = 'presentation';
}
if (
t.isJSXAttribute(attribute) &&
attribute.name.name === 'style'
) {
styleAttributeExists = true;
const styleValue = attribute.value.expression;
const elementName = path.node.name.property.name;
const defaultStyles = t.memberExpression(
t.identifier(defaultStylesImportIdentifier.name),
t.identifier(elementName)
);
path.node.attributes[index] = t.jsxSpreadAttribute(
t.callExpression(
t.memberExpression(
t.identifier(stylexImportIdentifier.name),
t.identifier('props')
),
[defaultStyles].concat(
Array.isArray(styleValue.elements)
? styleValue.elements
: [styleValue]
)
)
);
}
if (t.isJSXAttribute(attribute) && attribute.name.name === 'type') {
typeAttributeExists = true;
}
if (t.isJSXAttribute(attribute) && attribute.name.name === 'dir') {
dirAttributeExists = true;
}
});

const elementName = path.node.name.property.name;
if (elementName === 'button' && !typeAttributeExists) {
path.node.attributes.push(
t.jsxAttribute(t.jsxIdentifier('type'), t.stringLiteral('button'))
);
}

if (
(elementName === 'input' || elementName === 'textarea') &&
!dirAttributeExists
) {
path.node.attributes.push(
t.jsxAttribute(t.jsxIdentifier('dir'), t.stringLiteral('auto'))
);
}

if (!styleAttributeExists) {
const elementName = path.node.name.property.name;
const defaultStyles = t.memberExpression(
t.identifier(defaultStylesImportIdentifier.name),
t.identifier(elementName)
);
path.node.attributes.push(
t.jsxSpreadAttribute(
t.callExpression(
t.memberExpression(
t.identifier(stylexImportIdentifier.name),
t.identifier('props')
),
[defaultStyles]
)
)
);
}
}
}
}
};
};
Loading

0 comments on commit 75f19ec

Please sign in to comment.