@stylable/core
Version:
CSS for Components
196 lines (184 loc) • 7.02 kB
text/typescript
import { isAbsolute } from 'path';
import * as postcss from 'postcss';
import { createDiagnosticReporter, Diagnostics } from './diagnostics';
import type { ImportSymbol, StylableSymbol } from './features';
import { isChildOfAtRule, stMixinMarker, isStMixinMarker } from './helpers/rule';
import { scopeNestedSelector, parseSelectorWithCache } from './helpers/selector';
export function isValidDeclaration(decl: postcss.Declaration) {
return typeof decl.value === 'string';
}
export const utilDiagnostics = {
INVALID_MERGE_OF: createDiagnosticReporter(
'14001',
'error',
(mergeValue: string) => `invalid merge of: \n"${mergeValue}"`
),
INVALID_RECURSIVE_MIXIN: createDiagnosticReporter(
'10010',
'error',
() => `invalid recursive mixin`
),
};
// ToDo: move to helpers/mixin
export function mergeRules(
mixinAst: postcss.Root,
rule: postcss.Rule,
mixinDecl: postcss.Declaration,
report: Diagnostics,
useNestingAsAnchor: boolean
) {
let mixinRoot: postcss.Rule | null | 'NoRoot' = null;
const nestedInKeyframes = isChildOfAtRule(rule, `keyframes`);
const anchorSelector = useNestingAsAnchor ? '&' : '[' + stMixinMarker + ']';
const anchorNodeCheck = useNestingAsAnchor ? undefined : isStMixinMarker;
mixinAst.walkRules((mixinRule: postcss.Rule) => {
if (isChildOfAtRule(mixinRule, 'keyframes')) {
return;
}
if (mixinRule.selector === anchorSelector && !mixinRoot) {
if (mixinRule.parent === mixinAst) {
mixinRoot = mixinRule;
} else {
const { selector } = scopeNestedSelector(
parseSelectorWithCache(rule.selector),
parseSelectorWithCache(mixinRule.selector),
false,
anchorNodeCheck
);
mixinRoot = 'NoRoot';
mixinRule.selector = selector;
}
} else if (!isChildOfMixinRoot(mixinRule, mixinRoot)) {
// scope to mixin target if not already scoped by parent
const { selector } = scopeNestedSelector(
parseSelectorWithCache(rule.selector),
parseSelectorWithCache(mixinRule.selector),
false,
anchorNodeCheck
);
mixinRule.selector = selector;
} else if (mixinRule.selector.includes(anchorSelector)) {
// report invalid nested mixin
mixinRule.selector = mixinRule.selector.split(anchorSelector).join('&');
report?.report(utilDiagnostics.INVALID_RECURSIVE_MIXIN(), {
node: rule,
});
}
});
if (mixinAst.nodes) {
let nextRule: postcss.Rule | postcss.AtRule = rule;
// TODO: handle rules before and after decl on entry
const inlineMixin = !hasNonDeclsBeforeDecl(mixinDecl);
const mixInto = inlineMixin ? rule : postcss.rule({ selector: '&' });
const mixIntoRule = (node: postcss.AnyNode) => {
// mix node into rule
if (inlineMixin) {
mixInto.insertBefore(mixinDecl, node);
} else {
// indent first level - doesn't change deep nested
node.raws.before = (node.raws.before || '') + ' ';
mixInto.append(node);
}
// mark following decls for nesting
if (!nestFollowingDecls && node.type !== 'decl' && hasAnyDeclsAfter(mixinDecl)) {
nestFollowingDecls = true;
}
};
let nestFollowingDecls = false;
mixinAst.nodes.slice().forEach((node) => {
if (node === mixinRoot) {
for (const nested of [...node.nodes]) {
mixIntoRule(nested);
}
} else if (node.type === 'decl') {
// stand alone decl - most likely from js mixin
mixIntoRule(node);
} else if (node.type === 'rule' || node.type === 'atrule') {
const valid = !nestedInKeyframes;
if (valid) {
if (rule.parent!.last === nextRule) {
rule.parent!.append(node);
} else {
rule.parent!.insertAfter(nextRule, node);
}
nextRule = node;
} else {
report?.report(utilDiagnostics.INVALID_MERGE_OF(node.toString()), {
node: rule,
});
}
}
});
// add nested mixin to rule body
if (mixInto !== rule && mixInto.nodes.length) {
mixinDecl.before(mixInto);
}
// nest following decls if needed
if (nestFollowingDecls) {
const nestFollowingDecls = postcss.rule({ selector: '&' });
while (mixinDecl.next()) {
const nextNode = mixinDecl.next()!;
nextNode.raws.before = (nextNode.raws.before || '') + ' ';
nestFollowingDecls.append(nextNode);
}
mixinDecl.after(nestFollowingDecls);
}
}
return rule;
}
function hasNonDeclsBeforeDecl(decl: postcss.Declaration) {
let current: postcss.AnyNode | undefined = decl.prev();
while (current) {
if (current.type !== 'decl' && current.type !== 'comment') {
return true;
}
current = current.prev();
}
return false;
}
function hasAnyDeclsAfter(decl: postcss.Declaration) {
let current: postcss.AnyNode | undefined = decl.next();
while (current) {
if (current.type === 'decl') {
return true;
}
current = current.prev();
}
return false;
}
const isChildOfMixinRoot = (rule: postcss.Rule, mixinRoot: postcss.Rule | null | 'NoRoot') => {
let current: postcss.Container | postcss.Document | undefined = rule.parent;
while (current) {
if (current === mixinRoot) {
return true;
}
current = current.parent;
}
return false;
};
export const sourcePathDiagnostics = {
MISSING_SOURCE_FILENAME: createDiagnosticReporter(
'17001',
'error',
() => 'missing source filename'
),
};
export function getSourcePath(root: postcss.Root, diagnostics: Diagnostics) {
const source = (root.source && root.source.input.file) || '';
if (!source) {
diagnostics.report(sourcePathDiagnostics.MISSING_SOURCE_FILENAME(), {
node: root,
});
} else if (!isAbsolute(source)) {
throw new Error('source filename is not absolute path: "' + source + '"');
}
return source;
}
export function getAlias(symbol: StylableSymbol): ImportSymbol | undefined {
if (symbol._kind === 'class' || symbol._kind === 'element') {
if (!symbol[`-st-extends`]) {
return symbol.alias;
}
}
return undefined;
}