@stylexjs/postcss-plugin
Version:
PostCSS plugin for StyleX
182 lines (151 loc) • 4.82 kB
JavaScript
/**
* 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 StyleX CSS.
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 `@stylex;` at-rule in the provided PostCSS root.
function findStyleXAtRule(root) {
let styleXAtRule = null;
root.walkAtRules((atRule) => {
if (atRule.name === 'stylex' && !atRule.params) {
styleXAtRule = atRule;
}
});
return styleXAtRule;
}
// 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, importSources, 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, { importSources })) {
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 {
findStyleXAtRule,
configure,
build,
getDependencies,
};
}
module.exports = createBuilder;