packages/plugin-apply/src/index.ts
/* eslint-disable no-param-reassign */
// TODO: Documentation: #apply as decl with value of rule selectors to inline
// ↳ Selectors can be separated by comma and/or whitespace (including new
// lines), both OK
// ↳ Add note about why we use decl with "#"; the way stylis (+ brief note
// about what stylis is) parser creates AST nodes and how stringify works
// ↳ Selectors with a ":" must be wrapped in commas
// ↳ Selector refs are minified, the same as what the output CSS would be; so
// for '.a > .b {}' the ref would be '.a>.b' + show examples
// TODO: Documentation: plugin only applies the rules from selectors you give
// it. It will not create or modify other rule sets, i.e., it does not
// automagically also apply pseudo-classes, pseudo-elements, at-rules,
// attributes, etc. It's an intentional design choice to keep the code simple
// and have better performance. Although the consumer maintenance overhead can
// be higher it also has the bonus of better visibility into what's going on
// and no unexpected results (which often leads to logically incorrect or hugely
// bloated code in SASS for example.)
// ↳ Give examples how to use in common scenarios + link to addon/native.xcss
// TODO: Document build performance impact (about 10% extra time, but do
// benchmarks to verify)
// ↳ Impact of including this plugin and impact of actual #apply use
import {
type Element,
type Middleware,
ctx,
onAfterBuild,
onBeforeBuild,
} from 'ekscss';
import * as stylis from 'stylis';
type ApplyRefs = Record<string, Element[] | undefined>;
onBeforeBuild(() => {
ctx.applyRefs = {};
});
onAfterBuild(() => {
ctx.applyRefs = undefined;
});
/**
* XCSS plugin to inline the properties of referenced rules.
*/
export const applyPlugin: Middleware = (
element,
_index,
_children,
callback,
): void => {
if (element.type === stylis.RULESET) {
for (const selector of element.props) {
((ctx.applyRefs as ApplyRefs)[selector] ??= []).push(element);
}
return;
}
if (element.type === stylis.DECLARATION && element.props === '#apply') {
// TODO: Remove type cast; stylis types don't differentiate by element.type
const targets = (element.children as string)
.split(',')
.map((x) => x.trim().replace(/^["']/, '').replace(/["']$/, ''));
const decls: Element[] = [];
for (const target of targets) {
const refs = (ctx.applyRefs as ApplyRefs)[target];
if (refs) {
for (const ref of refs) {
// TODO: Remove type cast; stylis types don't differentiate by element.type
decls.push(...(ref.children as Element[]));
}
} else {
ctx.warnings.push({
code: 'apply-no-match',
message: `Unable to #apply "${target}", no matching rule`,
file: ctx.from,
line: element.line,
column: element.column,
});
}
}
element.return = stylis.serialize(decls, callback);
if (element.return === '') {
// Set empty value so declaration is removed in stringify
element.value = '';
ctx.warnings.push({
code: 'apply-empty',
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message: `#apply "${targets}" result empty`,
file: ctx.from,
line: element.line,
column: element.column,
});
}
}
};
export default applyPlugin;