diff --git a/apps/expo-app/src/app/index.js b/apps/expo-app/src/app/index.js index 6edc518e..105cc09f 100644 --- a/apps/expo-app/src/app/index.js +++ b/apps/expo-app/src/app/index.js @@ -6,7 +6,7 @@ */ // Required for CSS to work on Expo Web. -import './stylex.css'; +import './strict.css'; // Required for Fast Refresh to work on Expo Web import '@expo/metro-runtime'; diff --git a/apps/expo-app/src/app/stylex.css b/apps/expo-app/src/app/strict.css similarity index 95% rename from apps/expo-app/src/app/stylex.css rename to apps/expo-app/src/app/strict.css index 8121945e..1b9dec00 100644 --- a/apps/expo-app/src/app/stylex.css +++ b/apps/expo-app/src/app/strict.css @@ -22,4 +22,4 @@ body { flex-direction: column; } -@stylex; +@react-strict-dom; diff --git a/apps/nextjs-app/src/app/layout.tsx b/apps/nextjs-app/src/app/layout.tsx index d9f7b5d7..fc1603c6 100644 --- a/apps/nextjs-app/src/app/layout.tsx +++ b/apps/nextjs-app/src/app/layout.tsx @@ -6,7 +6,7 @@ */ import type { Metadata } from "next"; -import "@/stylex.css"; +import "@/strict.css"; export const metadata: Metadata = { title: "Create Next App", diff --git a/apps/nextjs-app/src/stylex.css b/apps/nextjs-app/src/strict.css similarity index 94% rename from apps/nextjs-app/src/stylex.css rename to apps/nextjs-app/src/strict.css index e41b23c9..762ebd05 100644 --- a/apps/nextjs-app/src/stylex.css +++ b/apps/nextjs-app/src/strict.css @@ -8,4 +8,4 @@ /* This directive is used by the react-strict-dom postcss plugin. */ /* It is automatically replaced with generated CSS during builds. */ -@stylex; +@react-strict-dom; diff --git a/apps/platform-tests/src/app/index.js b/apps/platform-tests/src/app/index.js index 6edc518e..105cc09f 100644 --- a/apps/platform-tests/src/app/index.js +++ b/apps/platform-tests/src/app/index.js @@ -6,7 +6,7 @@ */ // Required for CSS to work on Expo Web. -import './stylex.css'; +import './strict.css'; // Required for Fast Refresh to work on Expo Web import '@expo/metro-runtime'; diff --git a/apps/platform-tests/src/app/stylex.css b/apps/platform-tests/src/app/strict.css similarity index 76% rename from apps/platform-tests/src/app/stylex.css rename to apps/platform-tests/src/app/strict.css index f110a120..41d10ce4 100644 --- a/apps/platform-tests/src/app/stylex.css +++ b/apps/platform-tests/src/app/strict.css @@ -6,8 +6,8 @@ */ /** - * The @stylex directive is used by the react-strict-dom postcss plugin. + * The @-rule is used by the react-strict-dom postcss plugin. * It is automatically replaced with generated CSS during builds. */ - @stylex; + @react-strict-dom; diff --git a/package-lock.json b/package-lock.json index c25d86cb..774044bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8480,32 +8480,6 @@ "postcss-value-parser": "^4.1.0" } }, - "node_modules/@stylexjs/postcss-plugin": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@stylexjs/postcss-plugin/-/postcss-plugin-0.14.1.tgz", - "integrity": "sha512-qU9IGeZCQIiyZtB5zAIvuq89vbEcjPwMJ6R8R1NANJ0vfyGeLgltbzMCoeiYfUBkK/XxoaitAkHVUt7F7j7v/Q==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.8", - "@stylexjs/babel-plugin": "0.14.1", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "postcss": "^8.4.49" - } - }, - "node_modules/@stylexjs/postcss-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@stylexjs/stylex": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.14.1.tgz", @@ -27570,6 +27544,10 @@ "node": ">=4" } }, + "node_modules/postcss-react-strict-dom": { + "resolved": "packages/postcss-react-strict-dom", + "link": true + }, "node_modules/postcss-reduce-idents": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", @@ -33955,14 +33933,56 @@ "node": ">=20.11.0" } }, + "packages/postcss-plugin": { + "name": "postcss-react-strict-dom", + "version": "0.0.50", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.8", + "@stylexjs/babel-plugin": "^0.14.1", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3" + }, + "peerDependencies": { + "postcss": "^8.4.49" + } + }, + "packages/postcss-react-strict-dom": { + "version": "0.0.50", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.8", + "@stylexjs/babel-plugin": "^0.14.1", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3" + }, + "peerDependencies": { + "postcss": "^8.4.49" + } + }, + "packages/postcss-react-strict-dom/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "packages/react-strict-dom": { "version": "0.0.50", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.24.7", "@stylexjs/babel-plugin": "^0.14.1", - "@stylexjs/postcss-plugin": "^0.14.1", "@stylexjs/stylex": "^0.14.1", + "postcss-react-strict-dom": "0.0.50", "postcss-value-parser": "^4.1.0" }, "devDependencies": { diff --git a/packages/postcss-react-strict-dom/README.md b/packages/postcss-react-strict-dom/README.md new file mode 100644 index 00000000..2d6146ed --- /dev/null +++ b/packages/postcss-react-strict-dom/README.md @@ -0,0 +1,7 @@ +# postcss-react-strict-dom + +A PostCSS plugin for [React Strict DOM](https://facebook.github.io/react-strict-dom). + +This package does not need to be manually installed when using `react-strict-dom`. + +See the [React Strict DOM postcss documentation](https://facebook.github.io/react-strict-dom/learn/setup/#postcss-configuration) diff --git a/packages/postcss-react-strict-dom/__tests__/__fixtures__/.babelrc.js b/packages/postcss-react-strict-dom/__tests__/__fixtures__/.babelrc.js new file mode 100644 index 00000000..e03a694c --- /dev/null +++ b/packages/postcss-react-strict-dom/__tests__/__fixtures__/.babelrc.js @@ -0,0 +1,13 @@ +module.exports = { + plugins: [ + [ + '@stylexjs/babel-plugin', + { + dev: false, + runtimeInjection: false, + importSources: [{ from: 'react-strict-dom', as: 'css' }], + styleResolution: 'property-specificity' + } + ] + ] +}; diff --git a/packages/postcss-react-strict-dom/__tests__/__fixtures__/styles-second.js b/packages/postcss-react-strict-dom/__tests__/__fixtures__/styles-second.js new file mode 100644 index 00000000..ab4f0f27 --- /dev/null +++ b/packages/postcss-react-strict-dom/__tests__/__fixtures__/styles-second.js @@ -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({ + second: { + backgroundColor: 'green' + } +}); diff --git a/packages/postcss-react-strict-dom/__tests__/__fixtures__/styles.js b/packages/postcss-react-strict-dom/__tests__/__fixtures__/styles.js new file mode 100644 index 00000000..66c4c77e --- /dev/null +++ b/packages/postcss-react-strict-dom/__tests__/__fixtures__/styles.js @@ -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({ + container: { + backgroundColor: 'red' + } +}); diff --git a/packages/postcss-react-strict-dom/__tests__/index-test.js b/packages/postcss-react-strict-dom/__tests__/index-test.js new file mode 100644 index 00000000..99936520 --- /dev/null +++ b/packages/postcss-react-strict-dom/__tests__/index-test.js @@ -0,0 +1,179 @@ +/** + * 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('postcss-react-strict-dom', () => { + const fixturesDir = path.resolve(__dirname, '__fixtures__'); + + async function runPlugin(options = {}, inputCSS = '@react-strict-dom;') { + // Create a new instance for each test as the plugin is stateful + const postcssPlugin = createPlugin(); + + const plugin = postcssPlugin({ + 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 files', async () => { + const result = await runPlugin(); + + expect(result.css).toMatchInlineSnapshot(` +" +@layer priority1, priority2, priority3, priority4; +@layer priority1{ +.x1ghz6dp{margin:0} +.x1717udv{padding:0} +} +@layer priority2{ +.xng3xce{border-style:none} +.x1y0btm7{border-style:solid} +.xc342km{border-width:0} +.xmkeg23{border-width:1px} +.xe8uvvx{list-style:none} +.xysyzu8{overflow:auto} +.x1hl2dhg{text-decoration:none} +} +@layer priority3{ +.xuw900x{aspect-ratio:attr(width) / attr(height)} +.x42x0ya{background-color:black} +.x1u857p9{background-color:green} +.xrkmrrc{background-color:red} +.x9f619{box-sizing:border-box} +.x1lxnp44{font-family:monospace,"monospace"} +.xngnso2{font-size:1.5rem} +.xrv4cvt{font-size:1em} +.x117nqv4{font-weight:bold} +.x288g5{resize:vertical} +.x16tdsg8{text-align:inherit} +.x1vvkbs{word-wrap:break-word} +} +@layer priority4{ +.xjm9jq1{height:1px} +.xt7dq6l{height:auto} +.x193iq5w{max-width:100%} +}" +`); + + // 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 @-rule', async () => { + const result = await runPlugin({}, '/* No @-rule here */'); + + expect(result.css).toMatchInlineSnapshot('"/* No @-rule here */"'); + expect(result.messages.length).toBe(0); + }); + + test('handles exclude patterns', async () => { + const result = await runPlugin({ + exclude: ['**/styles-second.js'] + }); + + // Should not contain styles-second.js styles + expect(result.css).not.toContain('green'); + + expect(result.css).toMatchInlineSnapshot(` +" +@layer priority1, priority2, priority3, priority4; +@layer priority1{ +.x1ghz6dp{margin:0} +.x1717udv{padding:0} +} +@layer priority2{ +.xng3xce{border-style:none} +.x1y0btm7{border-style:solid} +.xc342km{border-width:0} +.xmkeg23{border-width:1px} +.xe8uvvx{list-style:none} +.xysyzu8{overflow:auto} +.x1hl2dhg{text-decoration:none} +} +@layer priority3{ +.xuw900x{aspect-ratio:attr(width) / attr(height)} +.x42x0ya{background-color:black} +.xrkmrrc{background-color:red} +.x9f619{box-sizing:border-box} +.x1lxnp44{font-family:monospace,"monospace"} +.xngnso2{font-size:1.5rem} +.xrv4cvt{font-size:1em} +.x117nqv4{font-weight:bold} +.x288g5{resize:vertical} +.x16tdsg8{text-align:inherit} +.x1vvkbs{word-wrap:break-word} +} +@layer priority4{ +.xjm9jq1{height:1px} +.xt7dq6l{height:auto} +.x193iq5w{max-width:100%} +}" +`); + }); + + test('skips files that do not match include/exclude patterns', async () => { + const result = await runPlugin({ + include: ['**/styles-second.js'] + }); + + // Should contain styles-second.js styles but not styles.js + expect(result.css).not.toContain('red'); + + expect(result.css).toMatchInlineSnapshot(` +" +@layer priority1, priority2, priority3, priority4; +@layer priority1{ +.x1ghz6dp{margin:0} +.x1717udv{padding:0} +} +@layer priority2{ +.xng3xce{border-style:none} +.x1y0btm7{border-style:solid} +.xc342km{border-width:0} +.xmkeg23{border-width:1px} +.xe8uvvx{list-style:none} +.xysyzu8{overflow:auto} +.x1hl2dhg{text-decoration:none} +} +@layer priority3{ +.xuw900x{aspect-ratio:attr(width) / attr(height)} +.x42x0ya{background-color:black} +.x1u857p9{background-color:green} +.x9f619{box-sizing:border-box} +.x1lxnp44{font-family:monospace,"monospace"} +.xngnso2{font-size:1.5rem} +.xrv4cvt{font-size:1em} +.x117nqv4{font-weight:bold} +.x288g5{resize:vertical} +.x16tdsg8{text-align:inherit} +.x1vvkbs{word-wrap:break-word} +} +@layer priority4{ +.xjm9jq1{height:1px} +.xt7dq6l{height:auto} +.x193iq5w{max-width:100%} +}" +`); + }); +}); diff --git a/packages/postcss-react-strict-dom/jest.config.js b/packages/postcss-react-strict-dom/jest.config.js new file mode 100644 index 00000000..8a069905 --- /dev/null +++ b/packages/postcss-react-strict-dom/jest.config.js @@ -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. + */ + +'use strict'; + +module.exports = { + prettierPath: null, + testPathIgnorePatterns: ['/__fixtures__/'], + testEnvironment: 'node' +}; diff --git a/packages/postcss-react-strict-dom/package.json b/packages/postcss-react-strict-dom/package.json new file mode 100644 index 00000000..b540de4c --- /dev/null +++ b/packages/postcss-react-strict-dom/package.json @@ -0,0 +1,25 @@ +{ + "name": "postcss-react-strict-dom", + "version": "0.0.50", + "description": "PostCSS plugin for React Strict DOM", + "main": "src/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/react-strict-dom.git" + }, + "license": "MIT", + "scripts": { + "jest": "jest", + "jest:report": "jest --collect-coverage" + }, + "dependencies": { + "@babel/core": "^7.26.8", + "@stylexjs/babel-plugin": "^0.14.1", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3" + }, + "peerDependencies": { + "postcss": "^8.4.49" + } +} diff --git a/packages/postcss-react-strict-dom/src/builder.js b/packages/postcss-react-strict-dom/src/builder.js new file mode 100644 index 00000000..02fcaf79 --- /dev/null +++ b/packages/postcss-react-strict-dom/src/builder.js @@ -0,0 +1,180 @@ +/** + * 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 path = require('node:path'); +const fs = require('node:fs'); +const { normalize, resolve } = require('path'); +const { globSync } = require('fast-glob'); +const isGlob = require('is-glob'); +const globParent = require('glob-parent'); +const createBundler = require('./bundler'); + +// Parses a glob pattern and extracts its base directory and pattern. +// Returns an object with `base` and `glob` properties. +function parseGlob(pattern) { + // License: MIT + // Based on: + // https://github.com/chakra-ui/panda/blob/6ab003795c0b076efe6879a2e6a2a548cb96580e/packages/node/src/parse-glob.ts + let glob = pattern; + const base = globParent(pattern); + + if (base !== '.') { + glob = pattern.substring(base.length); + if (glob.charAt(0) === '/') { + glob = glob.substring(1); + } + } + + if (glob.substring(0, 2) === './') { + glob = glob.substring(2); + } + if (glob.charAt(0) === '/') { + glob = glob.substring(1); + } + + return { base, glob }; +} + +// Parses a file path or glob pattern into a PostCSS dependency message. +function parseDependency(fileOrGlob) { + // License: MIT + // Based on: + // https://github.com/chakra-ui/panda/blob/6ab003795c0b076efe6879a2e6a2a548cb96580e/packages/node/src/parse-dependency.ts + if (fileOrGlob.startsWith('!')) { + return null; + } + + let message = null; + + if (isGlob(fileOrGlob)) { + const { base, glob } = parseGlob(fileOrGlob); + message = { type: 'dir-dependency', dir: normalize(resolve(base)), glob }; + } else { + message = { type: 'dependency', file: normalize(resolve(fileOrGlob)) }; + } + + return message; +} + +// Creates a builder for transforming files and bundling styles +function createBuilder() { + let config = null; + + const bundler = createBundler(); + + const fileModifiedMap = new Map(); + + // Configures the builder with the provided options. + function configure(options) { + config = options; + } + + /// Retrieves the current configuration. + function getConfig() { + if (config == null) { + throw new Error('Builder not configured'); + } + return config; + } + + // Finds the @-rule in the provided PostCSS root. + function findAtRule(root) { + let matchingAtRule = null; + root.walkAtRules((atRule) => { + if (atRule.name === 'react-strict-dom' && !atRule.params) { + matchingAtRule = atRule; + } + }); + return matchingAtRule; + } + + // Retrieves all files that match the include and exclude patterns. + function getFiles() { + const { cwd, include, exclude } = getConfig(); + return globSync(include, { + onlyFiles: true, + ignore: exclude, + cwd + }); + } + + // Transforms the included files, bundles the CSS, and returns the result. + async function build({ shouldSkipTransformError }) { + const { cwd, babelConfig, useCSSLayers, isDev } = getConfig(); + + const files = getFiles(); + const filesToTransform = []; + + // Remove deleted files since the last build + for (const file of fileModifiedMap.keys()) { + if (!files.includes(file)) { + fileModifiedMap.delete(file); + bundler.remove(file); + } + } + + for (const file of files) { + const filePath = path.resolve(cwd, file); + const mtimeMs = fs.existsSync(filePath) + ? fs.statSync(filePath).mtimeMs + : -Infinity; + + // Skip files that have not been modified since the last build + // On first run, all files will be transformed + const shouldSkip = + fileModifiedMap.has(file) && mtimeMs === fileModifiedMap.get(file); + + if (shouldSkip) { + continue; + } + + fileModifiedMap.set(file, mtimeMs); + filesToTransform.push(file); + } + + await Promise.all( + filesToTransform.map((file) => { + const filePath = path.resolve(cwd, file); + const contents = fs.readFileSync(filePath, 'utf-8'); + if (!bundler.shouldTransform(contents)) { + return; + } + return bundler.transform(filePath, contents, babelConfig, { + isDev, + shouldSkipTransformError + }); + }) + ); + + const css = bundler.bundle({ useCSSLayers }); + return css; + } + + // Retrieves the dependencies that PostCSS should watch. + function getDependencies() { + const { include } = getConfig(); + const dependencies = []; + + for (const fileOrGlob of include) { + const dependency = parseDependency(fileOrGlob); + if (dependency != null) { + dependencies.push(dependency); + } + } + + return dependencies; + } + + return { + findAtRule, + configure, + build, + getDependencies + }; +} + +module.exports = createBuilder; diff --git a/packages/postcss-react-strict-dom/src/bundler.js b/packages/postcss-react-strict-dom/src/bundler.js new file mode 100644 index 00000000..73ed1b1e --- /dev/null +++ b/packages/postcss-react-strict-dom/src/bundler.js @@ -0,0 +1,74 @@ +/** + * 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 babel = require('@babel/core'); +const stylexBabelPlugin = require('@stylexjs/babel-plugin'); + +// Creates a stateful bundler for processing styles using Babel. +module.exports = function createBundler() { + const styleXRulesMap = new Map(); + + // Determines if the source code should be transformed + function shouldTransform(sourceCode) { + return sourceCode.includes('react-strict-dom'); + } + + // Transforms the source code using Babel, extracting styles and storing them. + async function transform(id, sourceCode, babelConfig, options) { + const { isDev, shouldSkipTransformError } = options; + const { code, map, metadata } = await babel + .transformAsync(sourceCode, { + filename: id, + caller: { + name: 'postcss-react-strict-dom', + platform: 'web', + isDev + }, + ...babelConfig + }) + .catch((error) => { + if (shouldSkipTransformError) { + console.warn( + `[postcss-react-strict-dom] Failed to transform "${id}": ${error.message}` + ); + + return { code: sourceCode, map: null, metadata: {} }; + } + throw error; + }); + + const stylex = metadata.stylex; + if (stylex != null && stylex.length > 0) { + styleXRulesMap.set(id, stylex); + } + + return { code, map, metadata }; + } + + // Removes the stored styles for the specified file. + function remove(id) { + styleXRulesMap.delete(id); + } + + // Bundles all collected styles into a single CSS string. + function bundle({ useCSSLayers }) { + const rules = Array.from(styleXRulesMap.values()).flat(); + + const css = stylexBabelPlugin.processStylexRules(rules, { + useLayers: useCSSLayers + }); + + return css; + } + + return { + shouldTransform, + transform, + remove, + bundle + }; +}; diff --git a/packages/postcss-react-strict-dom/src/index.js b/packages/postcss-react-strict-dom/src/index.js new file mode 100644 index 00000000..c8edf91f --- /dev/null +++ b/packages/postcss-react-strict-dom/src/index.js @@ -0,0 +1,10 @@ +/** + * 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 createPlugin = require('./plugin'); + +module.exports = createPlugin(); diff --git a/packages/postcss-react-strict-dom/src/plugin.js b/packages/postcss-react-strict-dom/src/plugin.js new file mode 100644 index 00000000..fa1aea55 --- /dev/null +++ b/packages/postcss-react-strict-dom/src/plugin.js @@ -0,0 +1,107 @@ +/** + * 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 postcss = require('postcss'); +const createBuilder = require('./builder'); + +module.exports = function createPlugin() { + const PLUGIN_NAME = 'postcss-react-strict-dom'; + + 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 = true + }) => { + include = [ + // Include the React Strict DOM package's source files by default + require.resolve('react-strict-dom'), + require.resolve('react-strict-dom/runtime'), + ...(include ?? []) + ]; + + exclude = [ + // Exclude type declaration files by default because it never contains any styles. + '**/*.d.ts', + '**/*.flow', + ...(exclude ?? []) + ]; + + // Whether to skip the error when transforming styles. + // 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 @-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, + isDev + }); + + // Find the @-rule + const atRule = builder.findAtRule(root); + if (atRule == 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 styles + const css = await builder.build({ + shouldSkipTransformError + }); + const parsed = await postcss.parse(css, { + from: fileName + }); + + // Replace the @-rule with the generated CSS + atRule.replaceWith(parsed); + + result.root = root; + + if (!shouldSkipTransformError) { + // Build was successful, subsequent builds are for watch mode + shouldSkipTransformError = true; + } + } + ] + }; + }; + + plugin.postcss = true; + + return plugin; +}; diff --git a/packages/react-strict-dom/package.json b/packages/react-strict-dom/package.json index 9ef224bc..007f8475 100644 --- a/packages/react-strict-dom/package.json +++ b/packages/react-strict-dom/package.json @@ -38,8 +38,8 @@ "@babel/helper-module-imports": "^7.24.7", "@stylexjs/babel-plugin": "^0.14.1", "@stylexjs/stylex": "^0.14.1", - "@stylexjs/postcss-plugin": "^0.14.1", - "postcss-value-parser": "^4.1.0" + "postcss-value-parser": "^4.1.0", + "postcss-react-strict-dom": "0.0.50" }, "devDependencies": { "@rollup/plugin-alias": "^5.1.0", diff --git a/packages/react-strict-dom/postcss/plugin.js b/packages/react-strict-dom/postcss/plugin.js index 9975ef2f..8c00115b 100644 --- a/packages/react-strict-dom/postcss/plugin.js +++ b/packages/react-strict-dom/postcss/plugin.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -const styleXPlugin = require('@stylexjs/postcss-plugin'); +const postcssPlugin = require('postcss-react-strict-dom'); const plugin = ({ cwd = process.cwd(), @@ -15,24 +15,11 @@ const plugin = ({ include, exclude }) => { - include = [ - // Include the React Strict DOM package's source files by default - require.resolve('react-strict-dom'), - require.resolve('react-strict-dom/runtime'), - ...(include ?? []) - ]; - - return styleXPlugin({ + return postcssPlugin({ cwd, babelConfig, include, - exclude, - useCSSLayers: true, - importSources: [ - '@stylexjs/stylex', - 'stylex', - { from: 'react-strict-dom', as: 'css' } - ] + exclude }); }; diff --git a/packages/website/docs/learn/environment-setup/01-expo.md b/packages/website/docs/learn/environment-setup/01-expo.md index 5fdc0f6c..9e05e4e7 100644 --- a/packages/website/docs/learn/environment-setup/01-expo.md +++ b/packages/website/docs/learn/environment-setup/01-expo.md @@ -113,19 +113,19 @@ TypeScript-based projects should set the following TypeScript compiler options: ## App files -Your app needs to include a CSS file that contains a `@stylex` directive. This acts as a placeholder that is replaced by the generated CSS during builds. +Your app needs to include a CSS file that contains a `@react-strict-dom` directive. This acts as a placeholder that is replaced by the generated CSS during builds. -```css title="stylex.css" +```css title="strict.css" /* This directive is used by the react-strict-dom postcss plugin. */ /* It is automatically replaced with generated CSS during builds. */ -@stylex; +@react-strict-dom; ``` Next, import the CSS file in the entry file of your app. ```js title="index.js" // Required for CSS to work on Expo Web. -import './stylex.css'; +import './strict.css'; // Required for Fast Refresh to work on Expo Web import '@expo/metro-runtime'; diff --git a/packages/website/docs/learn/environment-setup/02-next.md b/packages/website/docs/learn/environment-setup/02-next.md index c67a583f..3318329c 100644 --- a/packages/website/docs/learn/environment-setup/02-next.md +++ b/packages/website/docs/learn/environment-setup/02-next.md @@ -110,19 +110,19 @@ export default nextConfig; ## App files -Your app needs to include a CSS file that contains a `@stylex` directive. This acts as a placeholder that is replaced by the generated CSS during builds. +Your app needs to include a CSS file that contains a `@react-strict-dom` directive. This acts as a placeholder that is replaced by the generated CSS during builds. -```css title="stylex.css" +```css title="strict.css" /* This directive is used by the react-strict-dom postcss plugin. */ /* It is automatically replaced with generated CSS during builds. */ -@stylex; +@react-strict-dom; ``` Next, import the CSS file in the `layout.tsx` file. ```js title="src/app/layout.tsx" // Required for CSS to work on Next.js -import './stylex.css'; +import './strict.css'; export default function RootLayout({ children,