Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
['@stylexjs/babel-plugin', { dev: false, runtimeInjection: false }],
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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.
*/

import { css } from 'react-strict-dom';

export const styles = css.create({
object: {
backgroundColor: 'yellow',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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.
*/

import * as custom from 'custom';

export const styles = custom.create({
string: {
backgroundColor: 'blue',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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.
*/

import * as stylex from '@stylexjs/stylex';

export const styles = stylex.create({
second: {
backgroundColor: 'green',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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.
*/

import * as stylex from '@stylexjs/stylex';

export const styles = stylex.create({
container: {
backgroundColor: 'red',
},
});
153 changes: 153 additions & 0 deletions packages/@stylexjs/postcss-plugin/__tests__/index-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* 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';

const path = require('path');
const postcss = require('postcss');
const createPlugin = require('../src/plugin');

describe('@stylexjs/postcss-plugin', () => {
const fixturesDir = path.resolve(__dirname, '__fixtures__');

async function runStylexPostcss(options = {}, inputCSS = '@stylex;') {
// Create a new instance for each test as the plugin is stateful
const stylexPostcssPlugin = createPlugin();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we re-create the plugin every time and reset internal state


const plugin = stylexPostcssPlugin({
cwd: fixturesDir,
include: ['**/*.js'],
babelConfig: {
configFile: path.join(fixturesDir, '.babelrc.js'),
},
...options,
});

const processor = postcss([plugin]);
const result = await processor.process(inputCSS, {
from: path.join(fixturesDir, 'input.css'),
});

return result;
}

test('extracts CSS from StyleX files', async () => {
const result = await runStylexPostcss();

expect(result.css).toMatchInlineSnapshot(`
".x1u857p9{background-color:green}
.xrkmrrc{background-color:red}"
`);

// Check that messages contain dependency information
expect(result.messages.length).toBeGreaterThan(0);
expect(result.messages.some((m) => m.type === 'dir-dependency')).toBe(true);
});

test('handles empty CSS input without @stylex rule', async () => {
const result = await runStylexPostcss({}, '/* No stylex rule here */');

expect(result.css).toMatchInlineSnapshot('"/* No stylex rule here */"');
expect(result.messages.length).toBe(0);
});

test('supports CSS layers', async () => {
const result = await runStylexPostcss({ useCSSLayers: true });

expect(result.css).toContain('@layer');
expect(result.css).toMatchInlineSnapshot(`
"
@layer priority1;
@layer priority1{
.x1u857p9{background-color:green}
.xrkmrrc{background-color:red}
}"
`);
});

test('handles exclude patterns', async () => {
const result = await runStylexPostcss({
exclude: ['**/styles-second.js'],
});

// Should not contain styles-second.js styles
expect(result.css).not.toContain('green');

expect(result.css).toMatchInlineSnapshot(
'".xrkmrrc{background-color:red}"',
);
});

test('respects string syntax for importSources', async () => {
// Default importSources should not process any files
const defaultResult = await runStylexPostcss({
include: ['**/import-sources-*.js'],
});

expect(defaultResult.css).toBe('');

// Custom importSources should process only import-sources-string.js
const customResult = await runStylexPostcss({
include: ['**/import-sources-*.js'],
importSources: ['custom'],
babelConfig: {
babelrc: false,
plugins: [
[
'@stylexjs/babel-plugin',
{
dev: false,
runtimeInjection: false,
importSources: ['custom'],
},
],
],
},
});

expect(customResult.css).toMatchInlineSnapshot(
'".x1t391ir{background-color:blue}"',
);
});

test('supports object syntax for importSources', async () => {
const result = await runStylexPostcss({
include: ['**/import-sources-object.js'],
importSources: [{ as: 'css', from: 'react-strict-dom' }],
babelConfig: {
babelrc: false,
plugins: [
[
'@stylexjs/babel-plugin',
{
dev: false,
runtimeInjection: false,
importSources: [{ as: 'css', from: 'react-strict-dom' }],
},
],
],
},
});

expect(result.css).toMatchInlineSnapshot(
'".x1cu41gw{background-color:yellow}"',
);
});

test('skips files that do not match include/exclude patterns', async () => {
const result = await runStylexPostcss({
include: ['**/styles-second.js'],
});

// Should contain styles-second.js styles but not styles.js
expect(result.css).not.toContain('red');

expect(result.css).toMatchInlineSnapshot(
'".x1u857p9{background-color:green}"',
);
});
});
13 changes: 13 additions & 0 deletions packages/@stylexjs/postcss-plugin/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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';

module.exports = {
testPathIgnorePatterns: ['/__fixtures__/'],
testEnvironment: 'node',
};
3 changes: 3 additions & 0 deletions packages/@stylexjs/postcss-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"url": "git+https://github.com/facebook/stylex.git"
},
"license": "MIT",
"scripts": {
"test": "jest"
},
"dependencies": {
"@babel/core": "^7.26.8",
"@stylexjs/babel-plugin": "0.12.0",
Expand Down
95 changes: 2 additions & 93 deletions packages/@stylexjs/postcss-plugin/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/

const postcss = require('postcss');
const createBuilder = require('./builder');
const createPlugin = require('./plugin');

const PLUGIN_NAME = '@stylexjs/postcss-plugin';

const builder = createBuilder();

const isDev = process.env.NODE_ENV === 'development';

const plugin = ({
cwd = process.cwd(),
// By default reuses the Babel configuration from the project root.
// Use `babelrc: false` to disable this behavior.
babelConfig = {},
include,
exclude,
useCSSLayers = false,
importSources = ['@stylexjs/stylex', 'stylex'],
}) => {
exclude = [
// Exclude type declaration files by default because it never contains any CSS rules.
'**/*.d.ts',
'**/*.flow',
...(exclude ?? []),
];

// Whether to skip the error when transforming StyleX rules.
// Useful in watch mode where Fast Refresh can recover from errors.
// Initial transform will still throw errors in watch mode to surface issues early.
let shouldSkipTransformError = false;

return {
postcssPlugin: PLUGIN_NAME,
plugins: [
// Processes the PostCSS root node to find and transform StyleX at-rules.
async function (root, result) {
const fileName = result.opts.from;

// Configure the builder with the provided options
await builder.configure({
include,
exclude,
cwd,
babelConfig,
useCSSLayers,
importSources,
isDev,
});

// Find the "@stylex" at-rule
const styleXAtRule = builder.findStyleXAtRule(root);
if (styleXAtRule == null) {
return;
}

// Get dependencies to be watched for changes
const dependencies = builder.getDependencies();

// Add each dependency to the PostCSS result messages.
// This watches the entire "./src" directory for "./src/**/*.{ts,tsx}"
// to handle new files and deletions reliably in watch mode.
for (const dependency of dependencies) {
result.messages.push({
plugin: PLUGIN_NAME,
parent: fileName,
...dependency,
});
}

// Build and parse the CSS from collected StyleX rules
const css = await builder.build({
shouldSkipTransformError,
});
const parsed = await postcss.parse(css, {
from: fileName,
});

// Replace the "@stylex" rule with the generated CSS
styleXAtRule.replaceWith(parsed);

result.root = root;

if (!shouldSkipTransformError) {
// Build was successful, subsequent builds are for watch mode
shouldSkipTransformError = true;
}
},
],
};
};

plugin.postcss = true;

module.exports = plugin;
module.exports = createPlugin();
Loading