@stylexjs/postcss-plugin 0.10.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/package.json +16 -0
- package/src/builder.js +182 -0
- package/src/bundler.js +73 -0
- package/src/index.js +101 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @stylexjs/postcss-plugin
|
|
2
|
+
|
|
3
|
+
## Documentation Website
|
|
4
|
+
|
|
5
|
+
[https://stylexjs.com](https://stylexjs.com)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install the package by using:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --save-dev @stylexjs/postcss-plugin autoprefixer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
or with yarn:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
yarn add --dev @stylexjs/postcss-plugin autoprefixer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add the following to your `postcss.config.js`
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
// postcss.config.js
|
|
25
|
+
module.exports = {
|
|
26
|
+
plugins: {
|
|
27
|
+
'@stylexjs/postcss-plugin': {
|
|
28
|
+
include: ['src/**/*.{js,jsx,ts,tsx}'],
|
|
29
|
+
},
|
|
30
|
+
autoprefixer: {},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add the following to your `babel.config.js`
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
import styleXPlugin from '@stylexjs/babel-plugin';
|
|
39
|
+
|
|
40
|
+
const config = {
|
|
41
|
+
plugins: [
|
|
42
|
+
[
|
|
43
|
+
styleXPlugin,
|
|
44
|
+
{
|
|
45
|
+
// Required for this plugin
|
|
46
|
+
runtimeInjection: false,
|
|
47
|
+
dev: true,
|
|
48
|
+
// Set this to true for snapshot testing
|
|
49
|
+
// default: false
|
|
50
|
+
test: false,
|
|
51
|
+
// Required for CSS variable support
|
|
52
|
+
unstable_moduleResolution: {
|
|
53
|
+
// type: 'commonJS' | 'haste'
|
|
54
|
+
// default: 'commonJS'
|
|
55
|
+
type: 'commonJS',
|
|
56
|
+
// The absolute path to the root directory of your project
|
|
57
|
+
rootDir: __dirname,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default config;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add the following to `src/stylex.css`
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
/**
|
|
71
|
+
* The @stylex directive is used by the @stylexjs/postcss-plugin.
|
|
72
|
+
* It is automatically replaced with generated CSS during builds.
|
|
73
|
+
*/
|
|
74
|
+
@stylex;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then, import this file from your application entrypoint:
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
// src/index.js
|
|
81
|
+
import './stylex.css';
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Plugin Options
|
|
85
|
+
|
|
86
|
+
### include
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
include: string[] // Required
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Array of paths or glob patterns to compile.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### exclude
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
exclude: string[] // Default: []
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Array of paths or glob patterns to exclude from compilation. Paths in exclude
|
|
103
|
+
take precedence over include.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### cwd
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
cwd: string; // Default: process.cwd()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Working directory for the plugin; defaults to process.cwd().
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### babelConfig
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
babelConfig: object; // Default: {}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Options for Babel configuration. By default, the plugin reads from
|
|
124
|
+
babel.config.js in your project. For custom configurations, set babelrc: false
|
|
125
|
+
and specify desired options. Refer to
|
|
126
|
+
[Babel Config Options](https://babeljs.io/docs/options) for available options.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### useCSSLayers
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
useCSSLayers: boolean; // Default: false
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Enabling this option switches Stylex from using `:not(#\#)` to using `@layers`
|
|
137
|
+
for handling CSS specificity.
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stylexjs/postcss-plugin",
|
|
3
|
+
"version": "0.10.0-beta.2",
|
|
4
|
+
"description": "PostCSS plugin for StyleX",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"repository": "https://www.github.com/facebook/stylex",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@babel/core": "^7.25.8",
|
|
10
|
+
"@stylexjs/babel-plugin": "0.10.0-beta.2",
|
|
11
|
+
"postcss": "^8.4.49",
|
|
12
|
+
"fast-glob": "^3.3.2",
|
|
13
|
+
"glob-parent": "^6.0.2",
|
|
14
|
+
"is-glob": "^4.0.3"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/builder.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const { normalize, resolve } = require('path');
|
|
13
|
+
const { globSync } = require('fast-glob');
|
|
14
|
+
const isGlob = require('is-glob');
|
|
15
|
+
const globParent = require('glob-parent');
|
|
16
|
+
const createBundler = require('./bundler');
|
|
17
|
+
|
|
18
|
+
// Parses a glob pattern and extracts its base directory and pattern.
|
|
19
|
+
// Returns an object with `base` and `glob` properties.
|
|
20
|
+
function parseGlob(pattern) {
|
|
21
|
+
// License: MIT
|
|
22
|
+
// Based on:
|
|
23
|
+
// https://github.com/chakra-ui/panda/blob/6ab003795c0b076efe6879a2e6a2a548cb96580e/packages/node/src/parse-glob.ts
|
|
24
|
+
let glob = pattern;
|
|
25
|
+
const base = globParent(pattern);
|
|
26
|
+
|
|
27
|
+
if (base !== '.') {
|
|
28
|
+
glob = pattern.substring(base.length);
|
|
29
|
+
if (glob.charAt(0) === '/') {
|
|
30
|
+
glob = glob.substring(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (glob.substring(0, 2) === './') {
|
|
35
|
+
glob = glob.substring(2);
|
|
36
|
+
}
|
|
37
|
+
if (glob.charAt(0) === '/') {
|
|
38
|
+
glob = glob.substring(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { base, glob };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parses a file path or glob pattern into a PostCSS dependency message.
|
|
45
|
+
function parseDependency(fileOrGlob) {
|
|
46
|
+
// License: MIT
|
|
47
|
+
// Based on:
|
|
48
|
+
// https://github.com/chakra-ui/panda/blob/6ab003795c0b076efe6879a2e6a2a548cb96580e/packages/node/src/parse-dependency.ts
|
|
49
|
+
if (fileOrGlob.startsWith('!')) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let message = null;
|
|
54
|
+
|
|
55
|
+
if (isGlob(fileOrGlob)) {
|
|
56
|
+
const { base, glob } = parseGlob(fileOrGlob);
|
|
57
|
+
message = { type: 'dir-dependency', dir: normalize(resolve(base)), glob };
|
|
58
|
+
} else {
|
|
59
|
+
message = { type: 'dependency', file: normalize(resolve(fileOrGlob)) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return message;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Creates a builder for transforming files and bundling StyleX CSS.
|
|
66
|
+
function createBuilder() {
|
|
67
|
+
let config = null;
|
|
68
|
+
|
|
69
|
+
const bundler = createBundler();
|
|
70
|
+
|
|
71
|
+
const fileModifiedMap = new Map();
|
|
72
|
+
|
|
73
|
+
// Configures the builder with the provided options.
|
|
74
|
+
function configure(options) {
|
|
75
|
+
config = options;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Retrieves the current configuration.
|
|
79
|
+
function getConfig() {
|
|
80
|
+
if (config == null) {
|
|
81
|
+
throw new Error('Builder not configured');
|
|
82
|
+
}
|
|
83
|
+
return config;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Finds the `@stylex;` at-rule in the provided PostCSS root.
|
|
87
|
+
function findStyleXAtRule(root) {
|
|
88
|
+
let styleXAtRule = null;
|
|
89
|
+
root.walkAtRules((atRule) => {
|
|
90
|
+
if (atRule.name === 'stylex' && !atRule.params) {
|
|
91
|
+
styleXAtRule = atRule;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return styleXAtRule;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Retrieves all files that match the include and exclude patterns.
|
|
98
|
+
function getFiles() {
|
|
99
|
+
const { cwd, include, exclude } = getConfig();
|
|
100
|
+
return globSync(include, {
|
|
101
|
+
onlyFiles: true,
|
|
102
|
+
ignore: exclude,
|
|
103
|
+
cwd,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Transforms the included files, bundles the CSS, and returns the result.
|
|
108
|
+
async function build({ shouldSkipTransformError }) {
|
|
109
|
+
const { cwd, babelConfig, useCSSLayers, isDev } = getConfig();
|
|
110
|
+
|
|
111
|
+
const files = getFiles();
|
|
112
|
+
const filesToTransform = [];
|
|
113
|
+
|
|
114
|
+
// Remove deleted files since the last build
|
|
115
|
+
for (const file of fileModifiedMap.keys()) {
|
|
116
|
+
if (!files.includes(file)) {
|
|
117
|
+
fileModifiedMap.delete(file);
|
|
118
|
+
bundler.remove(file);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const filePath = path.resolve(cwd, file);
|
|
124
|
+
const mtimeMs = fs.existsSync(filePath)
|
|
125
|
+
? fs.statSync(filePath).mtimeMs
|
|
126
|
+
: -Infinity;
|
|
127
|
+
|
|
128
|
+
// Skip files that have not been modified since the last build
|
|
129
|
+
// On first run, all files will be transformed
|
|
130
|
+
const shouldSkip =
|
|
131
|
+
fileModifiedMap.has(file) && mtimeMs === fileModifiedMap.get(file);
|
|
132
|
+
|
|
133
|
+
if (shouldSkip) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fileModifiedMap.set(file, mtimeMs);
|
|
138
|
+
filesToTransform.push(file);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await Promise.all(
|
|
142
|
+
filesToTransform.map((file) => {
|
|
143
|
+
const filePath = path.resolve(cwd, file);
|
|
144
|
+
const contents = fs.readFileSync(filePath, 'utf-8');
|
|
145
|
+
if (!bundler.shouldTransform(contents)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
return bundler.transform(file, contents, babelConfig, {
|
|
149
|
+
isDev,
|
|
150
|
+
shouldSkipTransformError,
|
|
151
|
+
});
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const css = bundler.bundle({ useCSSLayers });
|
|
156
|
+
return css;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Retrieves the dependencies that PostCSS should watch.
|
|
160
|
+
function getDependencies() {
|
|
161
|
+
const { include } = getConfig();
|
|
162
|
+
const dependencies = [];
|
|
163
|
+
|
|
164
|
+
for (const fileOrGlob of include) {
|
|
165
|
+
const dependency = parseDependency(fileOrGlob);
|
|
166
|
+
if (dependency != null) {
|
|
167
|
+
dependencies.push(dependency);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return dependencies;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
findStyleXAtRule,
|
|
176
|
+
configure,
|
|
177
|
+
build,
|
|
178
|
+
getDependencies,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = createBuilder;
|
package/src/bundler.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const babel = require('@babel/core');
|
|
11
|
+
const stylexBabelPlugin = require('@stylexjs/babel-plugin');
|
|
12
|
+
|
|
13
|
+
// Creates a stateful bundler for processing StyleX rules using Babel.
|
|
14
|
+
module.exports = function createBundler() {
|
|
15
|
+
const styleXRulesMap = new Map();
|
|
16
|
+
|
|
17
|
+
// Determines if the source code should be transformed based on the presence of StyleX imports.
|
|
18
|
+
function shouldTransform(sourceCode) {
|
|
19
|
+
return sourceCode.includes('stylex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Transforms the source code using Babel, extracting StyleX rules and storing them.
|
|
23
|
+
async function transform(id, sourceCode, babelConfig, options) {
|
|
24
|
+
const { isDev, shouldSkipTransformError } = options;
|
|
25
|
+
const { code, map, metadata } = await babel
|
|
26
|
+
.transformAsync(sourceCode, {
|
|
27
|
+
filename: id,
|
|
28
|
+
caller: {
|
|
29
|
+
name: '@stylexjs/postcss-plugin',
|
|
30
|
+
isDev,
|
|
31
|
+
},
|
|
32
|
+
...babelConfig,
|
|
33
|
+
})
|
|
34
|
+
.catch((error) => {
|
|
35
|
+
if (shouldSkipTransformError) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`[@stylexjs/postcss-plugin] Failed to transform "${id}": ${error.message}`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return { code: sourceCode, map: null, metadata: {} };
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const stylex = metadata.stylex;
|
|
46
|
+
if (stylex != null && stylex.length > 0) {
|
|
47
|
+
styleXRulesMap.set(id, stylex);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { code, map, metadata };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Removes the stored StyleX rules for the specified file.
|
|
54
|
+
function remove(id) {
|
|
55
|
+
styleXRulesMap.delete(id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Bundles all collected StyleX rules into a single CSS string.
|
|
59
|
+
function bundle({ useCSSLayers }) {
|
|
60
|
+
const rules = Array.from(styleXRulesMap.values()).flat();
|
|
61
|
+
|
|
62
|
+
const css = stylexBabelPlugin.processStylexRules(rules, useCSSLayers);
|
|
63
|
+
|
|
64
|
+
return css;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
shouldTransform,
|
|
69
|
+
transform,
|
|
70
|
+
remove,
|
|
71
|
+
bundle,
|
|
72
|
+
};
|
|
73
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const postcss = require('postcss');
|
|
11
|
+
const createBuilder = require('./builder');
|
|
12
|
+
|
|
13
|
+
const PLUGIN_NAME = '@stylexjs/postcss-plugin';
|
|
14
|
+
|
|
15
|
+
const builder = createBuilder();
|
|
16
|
+
|
|
17
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
18
|
+
|
|
19
|
+
const plugin = ({
|
|
20
|
+
cwd = process.cwd(),
|
|
21
|
+
// By default reuses the Babel configuration from the project root.
|
|
22
|
+
// Use `babelrc: false` to disable this behavior.
|
|
23
|
+
babelConfig = {},
|
|
24
|
+
include,
|
|
25
|
+
exclude,
|
|
26
|
+
useCSSLayers = false,
|
|
27
|
+
}) => {
|
|
28
|
+
exclude = [
|
|
29
|
+
// Exclude type declaration files by default because it never contains any CSS rules.
|
|
30
|
+
'**/*.d.ts',
|
|
31
|
+
'**/*.flow',
|
|
32
|
+
...(exclude ?? []),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Whether to skip the error when transforming StyleX rules.
|
|
36
|
+
// Useful in watch mode where Fast Refresh can recover from errors.
|
|
37
|
+
// Initial transform will still throw errors in watch mode to surface issues early.
|
|
38
|
+
let shouldSkipTransformError = false;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
postcssPlugin: PLUGIN_NAME,
|
|
42
|
+
plugins: [
|
|
43
|
+
// Processes the PostCSS root node to find and transform StyleX at-rules.
|
|
44
|
+
async function (root, result) {
|
|
45
|
+
const fileName = result.opts.from;
|
|
46
|
+
|
|
47
|
+
// Configure the builder with the provided options
|
|
48
|
+
await builder.configure({
|
|
49
|
+
include,
|
|
50
|
+
exclude,
|
|
51
|
+
cwd,
|
|
52
|
+
babelConfig,
|
|
53
|
+
useCSSLayers,
|
|
54
|
+
isDev,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Find the "@stylex" at-rule
|
|
58
|
+
const styleXAtRule = builder.findStyleXAtRule(root);
|
|
59
|
+
if (styleXAtRule == null) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get dependencies to be watched for changes
|
|
64
|
+
const dependencies = builder.getDependencies();
|
|
65
|
+
|
|
66
|
+
// Add each dependency to the PostCSS result messages.
|
|
67
|
+
// This watches the entire "./src" directory for "./src/**/*.{ts,tsx}"
|
|
68
|
+
// to handle new files and deletions reliably in watch mode.
|
|
69
|
+
for (const dependency of dependencies) {
|
|
70
|
+
result.messages.push({
|
|
71
|
+
plugin: PLUGIN_NAME,
|
|
72
|
+
parent: fileName,
|
|
73
|
+
...dependency,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Build and parse the CSS from collected StyleX rules
|
|
78
|
+
const css = await builder.build({
|
|
79
|
+
shouldSkipTransformError,
|
|
80
|
+
});
|
|
81
|
+
const parsed = await postcss.parse(css, {
|
|
82
|
+
from: fileName,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Replace the "@stylex" rule with the generated CSS
|
|
86
|
+
styleXAtRule.replaceWith(parsed);
|
|
87
|
+
|
|
88
|
+
result.root = root;
|
|
89
|
+
|
|
90
|
+
if (!shouldSkipTransformError) {
|
|
91
|
+
// Build was successful, subsequent builds are for watch mode
|
|
92
|
+
shouldSkipTransformError = true;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
plugin.postcss = true;
|
|
100
|
+
|
|
101
|
+
module.exports = plugin;
|