Skip to content

Commit

Permalink
feat: add postcss-atomizer package (#496)
Browse files Browse the repository at this point in the history
  • Loading branch information
redonkulus authored Sep 7, 2022
1 parent cdba74f commit bb3138f
Show file tree
Hide file tree
Showing 20 changed files with 571 additions and 237 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-walls-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'atomizer': minor
---

feat(atomizer): refactor binary to use build utils
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ vendor/bundle
snippets.json
*.vsix

# webpack-atomizer-loader
# example generated files
**/example/*.css
**/example/bundle.js
# postcss requires this file for the demo
!packages/postcss-atomizer/example/main.css
2 changes: 1 addition & 1 deletion examples/atomizer.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
content: ['./examples'],
content: ['./examples/*.html'],
// 'custom' maps custom suffixes to values and it is specially useful for theming
// or things that you need to change globally in many different atomic classes.
// these key/value pairs map to the custom suffixes in 'classNames'.
Expand Down
227 changes: 48 additions & 179 deletions packages/atomizer/bin/atomizer
Original file line number Diff line number Diff line change
Expand Up @@ -13,132 +13,67 @@ process.title = 'atomizer';
var chalk = require('chalk');
var chokidar = require('chokidar');
var fs = require('fs');
var glob = require('glob');
var minimatch = require('minimatch');
var path = require('path');
var program = require('commander');
var empty = require('lodash/isEmpty');
var some = require('lodash/some');
var union = require('lodash/union');
var Atomizer = require('../src/atomizer');

var content = '';
var config = {};
var classnames = [];
var cwd = process.cwd();
var defaultConfigPath = path.resolve(cwd, 'atomizer.config.js');
var { buildAtomicCss, getConfig, findFiles } = require('../src/build');

function collect(val, memo) {
memo.push(val);
return memo;
}

program
.version(JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8')).version)
.usage('[options] [path]')
.option('-R, --recursive', 'process all files recursively in the path')
.option('-c, --config [file]', 'source config file')
.option('-r, --rules [file]', 'custom rules file (argument may be passed multiple times)', collect, [])
.option('-o, --outfile [file]', 'destination config file')
.option('-n, --namespace [namespace]', 'adds the given namespace to all generated Atomic CSS selectors')
.option('-H, --helpersNamespace [namespace]', 'adds the given namespace to all helper selectors')
.option('-w, --watch [target]', 'rebuilds when changes are detected in the file, directory, or glob (argument may be passed multiple times and are parsed for Atomic CSS classes)', collect, [])
.option('--exclude [pattern]', 'excluded file pattern', collect, [])
.option('--rtl', 'swaps `start` and `end` keyword replacements with `right` and `left`')
.option('--bump-mq', 'increases specificity of media queries a small amount')
.option('--ie', '[deprecated] no longer used')
.option('--verbose', 'show additional log info (warnings)')
.option('--quiet', 'hide processing info')
.parse(process.argv);

function warn() {
if (!programOpts.quiet) console.warn.apply(console, arguments);
}

function parseFiles (files, recursive, dir) {
var classNames = [];
.version(JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8')).version)
.usage('[options] [path]')
.option('-R, --recursive', 'process all files recursively in the path')
.option('-c, --config [file]', 'source config file')
.option('-r, --rules [file]', 'custom rules file (argument may be passed multiple times)', collect, [])
.option('-o, --outfile [file]', 'destination config file')
.option('-n, --namespace [namespace]', 'adds the given namespace to all generated Atomic CSS selectors')
.option('-H, --helpersNamespace [namespace]', 'adds the given namespace to all helper selectors')
.option(
'-w, --watch [target]',
'rebuilds when changes are detected in the file, directory, or glob (argument may be passed multiple times and are parsed for Atomic CSS classes)',
collect,
[]
)
.option('--exclude [pattern]', 'excluded file pattern', collect, [])
.option('--rtl', 'swaps `start` and `end` keyword replacements with `right` and `left`')
.option('--bump-mq', 'increases specificity of media queries a small amount')
.option('--ie', '[deprecated] no longer used')
.option('--verbose', 'show additional log info (warnings)')
.option('--quiet', 'hide processing info')
.parse(process.argv);

// Gather cli arguments and options
var programOpts = program.opts();

for (var i=0, iLen=files.length; i < iLen; i++) {
classNames = union(classNames, parseFile(files[i], recursive, dir));
}
// Load the atomizer config file
var config = getConfig(programOpts.config);

return classNames;
// Print help menu if no config is loaded and no arguments are passed in
if (empty(config) && process.argv.length <= 2) {
program.outputHelp();
return;
}

function parseFile (file, recursive, dir) {
var classNames = [],
fileContents,
filepath,
relative,
stat;

if (file) {
filepath = dir ? path.resolve(dir, file) : path.resolve(file);
relative = path.relative(cwd, filepath);
stat = fs.statSync(filepath);
// Collect files to parse from cli args
var filesToParse = program.args || [];

if (stat.isFile()) {
var isExcluded = false;
if (programOpts.exclude && programOpts.exclude.length) {
isExcluded = some(programOpts.exclude, function excludeFile(value) {
return minimatch(filepath, value, {matchBase: true});
});
}
if (!isExcluded) {
warn('Parsing file ' + chalk.cyan(relative) + ' for Atomic CSS classes');
fileContents = fs.readFileSync(filepath, {encoding: 'utf-8'});
classNames = atomizer.findClassNames(fileContents);
} else {
warn('Excluding file ' + chalk.cyan(relative) + ' for Atomic CSS classes');
}
} else if (stat.isDirectory()) {
if (!dir || dir && recursive) {
warn('Inspecting directory ' + chalk.cyan(path.relative(cwd, filepath)));
classNames = parseFiles(fs.readdirSync(filepath), recursive, filepath);
}
// Add more src files from "content" property of config
filesToParse = filesToParse.concat(findFiles(config.content));

// Run atomizer build, output to stdout if css returned
function runAtomizer(files, config = {}, options = {}, done) {
buildAtomicCss(files, config, options, function (err, css) {
done(err, css);
if (css) {
process.stdout.write(`\n${css}`);
}
}
return classNames;
}

function buildAtomicCss(additionalFiles, done) {
// Add additional files, if any, from the watcher
var finalFilesToParse = additionalFiles.concat(filesToParse);
if (finalFilesToParse.length) {
classnames = parseFiles(finalFilesToParse, !!programOpts.recursive);
}

// Finalize the config
config = atomizer.getConfig(classnames, config);

// Create the CSS
content = atomizer.getCss(config, options);

// Output the CSS
var outfile = programOpts.outfile;
if (outfile) {
fs.readFile(outfile, {encoding: 'utf-8'}, function(err, data) {
if (data === content) {
console.log('Content of ' + chalk.cyan(outfile) + ' has not changed.');
done();
} else {
fs.mkdir(path.dirname(outfile), function (err) {
// Fail silently
fs.writeFile(path.resolve(outfile), content, function (err) {
if (!err) {
console.log('File ' + chalk.cyan(outfile) + ' created.');
}
done(err);
});
});
}
});
} else {
process.stdout.write("\n" + content);
done();
}
});
}

// Used by the watcher to watch additional files
function triggerBuild(state) {
// Ensure only one build happens at a time.
if (state.building) {
Expand All @@ -148,7 +83,7 @@ function triggerBuild(state) {
}

state.building = true;
buildAtomicCss(Object.keys(state.files), function (err) {
runAtomizer(Object.keys(state.files), config, programOpts, function (err) {
if (err) {
throw err;
}
Expand All @@ -160,71 +95,6 @@ function triggerBuild(state) {
});
}

// Setup Atomizer instance
var programOpts = program.opts();
var atomizer = new Atomizer({ verbose: !!programOpts.verbose });

// Attempt to load config from `--config` option first, otherwise
// check for a default config file in the current working directory
var configFile = programOpts.config;
if (configFile) {
if (!fs.existsSync(configFile)) {
console.error('Configuration file ' + chalk.cyan(configFile) + ' not found.');
return;
}
config = require(path.resolve(configFile));
} else if (fs.existsSync(defaultConfigPath)) {
config = require(defaultConfigPath);
}

// Print help menu if no config is loaded and no arguments are passed in
if (empty(config) && process.argv.length <= 2) {
program.outputHelp();
return;
}

// Collect files to parse from cli args
var filesToParse = program.args || [];

// Add more src files from "content" property of config
if (config.content && Array.isArray(config.content)) {
filesToParse = config.content
.reduce(function (files, pattern) {
var found = glob.sync(pattern, { matchBase: true });
return files.concat(found);
}, [])
.concat(filesToParse);
}

// Custom rulesets
var rulesFiles = programOpts.rules;
if (rulesFiles) {
rulesFiles.forEach(function (rulesFile) {
if (!fs.existsSync(rulesFile)) {
done(new Error('Rule file ' + chalk.cyan(rulesFile) + ' not found.'));
return false;
}
warn('Adding rules from ' + chalk.cyan(rulesFile) + '.');
atomizer.addRules(require(path.resolve(rulesFile)));
});
}

// Options
var options = {
rtl: programOpts.rtl,
bumpMQ: programOpts.bumpMq
};

// Options: Namespace
if (typeof programOpts.namespace !== 'undefined') {
options.namespace = programOpts.namespace;
}

// Options: Helpers Namespace
if (typeof programOpts.helpersNamespace !== 'undefined') {
options.helpersNamespace = programOpts.helpersNamespace;
}

// Setup Watcher
if (programOpts.watch === true || programOpts.watch.length) {
// initially watch all parsed files
Expand All @@ -234,11 +104,11 @@ if (programOpts.watch === true || programOpts.watch.length) {
if (Array.isArray(programOpts.watch) && programOpts.watch.length) {
filesToWatch = filesToWatch.concat(programOpts.watch);
}

// used for testing --watch input
if (typeof process.env.TEST !== 'undefined') {
console.log('Watching ' + chalk.cyan(filesToWatch.join(', ')) + ' for changes.');
buildAtomicCss([], function () {});
runAtomizer(filesToParse, config, programOpts, function () {});
} else {
var buildTriggerState = { files: {} };
var watcher = chokidar.watch(filesToWatch);
Expand All @@ -256,6 +126,5 @@ if (programOpts.watch === true || programOpts.watch.length) {
});
}
} else {
buildAtomicCss([], function () {});
runAtomizer(filesToParse, config, programOpts, function () {});
}

43 changes: 35 additions & 8 deletions packages/atomizer/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
declare module 'atomizer' {
export interface Options {
/** Increases specificity of media queries a small amount */
bumpMq?: boolean;
/** Source config file, defaults to "atomizer.config.js" */
config?: string;
/** Excluded file pattern */
exclude?: string;
/** Adds the given namespace to all helper selectors */
helpersNamespace?: string;
/** Adds the given namespace to all generated Atomic CSS selectors */
namespace?: string;
/** Destination of CSS output file */
outfile?: string;
/** Hide processing info */
quiet?: boolean;
/** Process all files recursively in the path */
recursive?: boolean;
/** Swaps `start` and `end` keyword replacements with `right` and `left` */
rtl?: boolean;
/** Path to custom rules file */
rules?: string;
/** Show additional log info (warnings) */
verbose?: boolean;
}

export interface AtomizerConfig {
custom?: Record<string, string | Record<string, string>>;
/** Custom media queries */
breakPoints?: Record<string, string>;
/** List of allow listed Atomizer class names */
classNames?: string[];
/** List of glob patterns paths to find parseable content */
content?: string[];
/** Custom CSS variables */
custom?: Record<string, string | Record<string, string>>;
}

export interface AtomizerOptions {
/** Show additional log info (warnings) */
verbose?: boolean;
}

export interface AtomizerRule {
[key: string]: any;
}

export interface CSSOptions {
export type CSSOptions = Pick<Options, 'bumpMq' | 'helpersNamespace' | 'namespace' | 'rtl'> & {
/** Adds a comment to the top of the generated Atomizer style sheet */
banner?: string;
bumpMQ?: boolean;
helpersNamespace?: string;
ie?: boolean;
namespace?: string;
rtl?: boolean;
}
};

export default class {
public static escapeSelector: (str: string) => string;
Expand Down
Loading

0 comments on commit bb3138f

Please sign in to comment.