Skip to content

Commit

Permalink
fix: mixin with native CSS nesting (#2898)
Browse files Browse the repository at this point in the history
  • Loading branch information
idoros authored Sep 14, 2023
1 parent ee3e3ae commit 3ef74c8
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/core/src/features/st-mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const diagnostics = {
VALUE_CANNOT_BE_STRING: mixinHelperDiagnostics.VALUE_CANNOT_BE_STRING,
INVALID_NAMED_PARAMS: mixinHelperDiagnostics.INVALID_NAMED_PARAMS,
INVALID_MERGE_OF: utilDiagnostics.INVALID_MERGE_OF,
INVALID_RECURSIVE_MIXIN: utilDiagnostics.INVALID_RECURSIVE_MIXIN,
PARTIAL_MIXIN_MISSING_ARGUMENTS: createDiagnosticReporter(
'10001',
'error',
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/helpers/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ export function createSubsetAst<T extends postcss.Root | postcss.AtRule | postcs
node.name === 'container'
) {
let scopeSelector = node.name === 'st-scope' ? node.params : '';
let isNestedInMixin = false;
if (scopeSelector) {
const ast = parseSelectorWithCache(scopeSelector, { clone: true });
const matchesSelectors = isRoot
Expand Down
84 changes: 80 additions & 4 deletions packages/core/src/stylable-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isAbsolute } from 'path';
import type * as postcss from 'postcss';
import * as postcss from 'postcss';
import { createDiagnosticReporter, Diagnostics } from './diagnostics';
import type { ImportSymbol, StylableSymbol } from './features';
import { isChildOfAtRule, stMixinMarker, isStMixinMarker } from './helpers/rule';
Expand All @@ -15,6 +15,11 @@ export const utilDiagnostics = {
'error',
(mergeValue: string) => `invalid merge of: \n"${mergeValue}"`
),
INVALID_RECURSIVE_MIXIN: createDiagnosticReporter(
'10010',
'error',
() => `invalid recursive mixin`
),
};

// ToDo: move to helpers/mixin
Expand Down Expand Up @@ -46,27 +51,52 @@ export function mergeRules(
mixinRoot = 'NoRoot';
mixinRule.selector = selector;
}
} else {
} 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]) {
rule.insertBefore(mixinDecl, nested);
mixIntoRule(nested);
}
} else if (node.type === 'decl') {
rule.insertBefore(mixinDecl, node);
// stand alone decl - most likely from js mixin
mixIntoRule(node);
} else if (node.type === 'rule' || node.type === 'atrule') {
const valid = !nestedInKeyframes;
if (valid) {
Expand All @@ -83,11 +113,57 @@ export function mergeRules(
}
}
});
// 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',
Expand Down
Loading

0 comments on commit 3ef74c8

Please sign in to comment.