diff --git a/packages/babel-plugin/__tests__/stylex-transform-pre-plugin-test.js b/packages/babel-plugin/__tests__/stylex-transform-pre-plugin-test.js new file mode 100644 index 00000000..8898b7a7 --- /dev/null +++ b/packages/babel-plugin/__tests__/stylex-transform-pre-plugin-test.js @@ -0,0 +1,149 @@ +/** + * 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. + * + * + */ + +'use strict'; + +jest.autoMockOff(); + +import * as t from '@babel/types'; +const { transformSync } = require('@babel/core'); +const stylexPlugin = require('../src/index'); + +// A simple babel plugin that looks for inline() calls +// hoists them out to stylex.create calls +// and replaces them with references. +function inlineDemoPlugin() { + const collectedStyles = []; + let stylesIdentifier; + return { + visitor: { + Program: { + enter(path, _state) { + // get unique identifier + stylesIdentifier = path.scope.generateUidIdentifier('styles'); + path.traverse({ + CallExpression(path, _state) { + if ( + path.node.callee.type === 'Identifier' && + path.node.callee.name === 'inline' + ) { + const args = path.node.arguments; + if (args.length !== 1) { + return; + } + const index = collectedStyles.length; + collectedStyles.push(args[0]); + path.replaceWith( + t.memberExpression( + stylesIdentifier, + t.identifier(`$${index}`), + ), + ); + } + }, + }); + path.node.body.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + stylesIdentifier, + t.callExpression( + t.memberExpression( + t.identifier('stylex'), + t.identifier('create'), + ), + [ + t.objectExpression( + collectedStyles.map((style, index) => + t.objectProperty(t.identifier(`$${index}`), style), + ), + ), + ], + ), + ), + ]), + ); + }, + }, + }, + }; +} + +function transform(source, opts = {}) { + return transformSync(source, { + filename: opts.filename, + parserOpts: { + flow: 'all', + }, + babelrc: false, + plugins: [ + ['babel-plugin-syntax-hermes-parser', { flow: 'detect' }], + inlineDemoPlugin, + [ + stylexPlugin, + { + runtimeInjection: true, + unstable_moduleResolution: { type: 'haste' }, + ...opts, + }, + ], + ], + }).code; +} + +describe('[transform] stylex.create()', () => { + test('transforms style object', () => { + expect( + transform(` + import stylex from 'stylex'; + + function Demo() { + return ( +
+ +
+ ); + } + + const styles = stylex.create({ + default: { + appearance: 'none', + borderWidth: '0', + borderStyle: 'none', + } + }); + `), + ).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + function Demo() { + return
+ +
; + } + _inject2(".xjyslct{appearance:none}", 3000); + _inject2(".xc342km{border-width:0}", 2000); + _inject2(".xng3xce{border-style:none}", 2000); + _inject2(".x6tqnqi{background-color:pink}", 3000); + _inject2(".x1awj2ng{color:white}", 3000);" + `); + }); +}); diff --git a/packages/babel-plugin/src/index.js b/packages/babel-plugin/src/index.js index 32bbc631..106ffc1a 100644 --- a/packages/babel-plugin/src/index.js +++ b/packages/babel-plugin/src/index.js @@ -69,28 +69,48 @@ function styleXTransform(): PluginObj<> { } } } - - path.traverse({ - // Look for stylex-related function calls and transform them. - // e.g. - // stylex.create(...) - // stylex.keyframes(...) - CallExpression(path: NodePath) { - if (pathUtils.isVariableDeclarator(path.parentPath)) { - // # Look for `stylex.keyframes` calls - // Needs to be handled *before* `stylex.create` as the `create` call - // may use the generated animation name. - transformStyleXKeyframes(path.parentPath, state); - } - transformStyleXDefineVars(path, state); - transformStyleXCreateTheme(path, state); - transformStyleXCreate(path, state); - }, - }); }, // After all other visitors are done, we can remove `styles=stylex.create(...)` // variables entirely if they're not needed. exit: (path: NodePath) => { + path.traverse({ + Identifier(path: NodePath) { + // Look for variables bound to `stylex.create` calls that are used + // outside of `stylex(...)` calls + if (pathUtils.isReferencedIdentifier(path)) { + const { name } = path.node; + if (state.styleMap.has(name)) { + const parentPath = path.parentPath; + if (pathUtils.isMemberExpression(parentPath)) { + const { property, computed } = parentPath.node; + if (property.type === 'Identifier' && !computed) { + state.markComposedNamespace([name, property.name, true]); + } else if (property.type === 'StringLiteral' && computed) { + state.markComposedNamespace([name, property.value, true]); + } else if (property.type === 'NumericLiteral' && computed) { + state.markComposedNamespace([ + name, + String(property.value), + true, + ]); + } else { + state.markComposedNamespace([name, true, true]); + } + } else { + state.markComposedNamespace([name, true, true]); + } + } + } + }, + CallExpression(path: NodePath) { + // Don't traverse the children of `stylex(...)` calls. + // This is important for detecting which `stylex.create()` calls + // should be kept. + skipStylexMergeChildren(path, state); + skipStylexPropsChildren(path, state); + skipStylexAttrsChildren(path, state); + }, + }); path.traverse({ CallExpression(path: NodePath) { transformStylexCall(path, state); @@ -217,41 +237,15 @@ function styleXTransform(): PluginObj<> { }, CallExpression(path: NodePath) { - // Don't traverse the children of `stylex(...)` calls. - // This is important for detecting which `stylex.create()` calls - // should be kept. - skipStylexMergeChildren(path, state); - skipStylexPropsChildren(path, state); - skipStylexAttrsChildren(path, state); - }, - - Identifier(path: NodePath) { - // Look for variables bound to `stylex.create` calls that are used - // outside of `stylex(...)` calls - if (pathUtils.isReferencedIdentifier(path)) { - const { name } = path.node; - if (state.styleMap.has(name)) { - const parentPath = path.parentPath; - if (pathUtils.isMemberExpression(parentPath)) { - const { property, computed } = parentPath.node; - if (property.type === 'Identifier' && !computed) { - state.markComposedNamespace([name, property.name, true]); - } else if (property.type === 'StringLiteral' && computed) { - state.markComposedNamespace([name, property.value, true]); - } else if (property.type === 'NumericLiteral' && computed) { - state.markComposedNamespace([ - name, - String(property.value), - true, - ]); - } else { - state.markComposedNamespace([name, true, true]); - } - } else { - state.markComposedNamespace([name, true, true]); - } - } + if (pathUtils.isVariableDeclarator(path.parentPath)) { + // # Look for `stylex.keyframes` calls + // Needs to be handled *before* `stylex.create` as the `create` call + // may use the generated animation name. + transformStyleXKeyframes(path.parentPath, state); } + transformStyleXDefineVars(path, state); + transformStyleXCreateTheme(path, state); + transformStyleXCreate(path, state); }, }, };