@lwc/style-compiler
Version:
Transform style sheet to be consumed by the LWC engine
153 lines (133 loc) • 4.95 kB
text/typescript
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import {
isPseudoElement,
isCombinator,
isPseudoClass,
Selector,
Root,
Node,
Pseudo,
Tag,
attribute,
} from 'postcss-selector-parser';
import validateSelectors from './validate';
import {
findNode,
replaceNodeWith,
trimNodeWhitespaces,
} from '../utils/selector-parser';
import {
SHADOW_ATTRIBUTE,
HOST_ATTRIBUTE
} from '../utils/selectors-scoping';
export interface SelectorScopingConfig {
/** When set to true, the :host selector gets replace with the the scoping token. */
transformHost: boolean;
}
function isHostPseudoClass(node: Node): node is Pseudo {
return isPseudoClass(node) && node.value === ':host';
}
/**
* Add scoping attributes to all the matching selectors:
* h1 -> h1[x-foo_tmpl]
* p a -> p[x-foo_tmpl] a[x-foo_tmpl]
*/
function scopeSelector(selector: Selector) {
const compoundSelectors: Node[][] = [[]];
// Split the selector per compound selector. Compound selectors are interleaved with combinator nodes.
// https://drafts.csswg.org/selectors-4/#typedef-complex-selector
selector.each(node => {
if (isCombinator(node)) {
compoundSelectors.push([]);
} else {
const current = compoundSelectors[compoundSelectors.length - 1];
current.push(node);
}
});
for (const compoundSelector of compoundSelectors) {
// Compound selectors containing :host have a special treatment and should not be scoped like the rest of the
// complex selectors.
const shouldScopeCompoundSelector = compoundSelector.every(node => !isHostPseudoClass(node));
if (shouldScopeCompoundSelector) {
let nodeToScope: Node | undefined;
// In each compound selector we need to locate the last selector to scope.
for (const node of compoundSelector) {
if (!isPseudoElement(node)) {
nodeToScope = node;
}
}
const shadowAttribute = attribute({
attribute: SHADOW_ATTRIBUTE,
value: undefined,
raws: {},
});
if (nodeToScope) {
// Add the scoping attribute right after the node scope
selector.insertAfter(nodeToScope, shadowAttribute);
} else {
// Add the scoping token in the first position of the compound selector as a fallback
// when there is no node to scope. For example: ::after {}
selector.insertBefore(compoundSelector[0], shadowAttribute);
}
}
}
}
/**
* Mark the :host selector with a placeholder. If the selector has a list of
* contextual selector it will generate a rule for each of them.
* :host -> [x-foo_tmpl-host]
* :host(.foo, .bar) -> [x-foo_tmpl-host].foo, [x-foo_tmpl-host].bar
*/
function transformHost(selector: Selector) {
// Locate the first :host pseudo-class
const hostNode = findNode(selector, isHostPseudoClass) as
| Pseudo
| undefined;
if (hostNode) {
// Store the original location of the :host in the selector
const hostIndex = selector.index(hostNode);
// Swap the :host pseudo-class with the host scoping token
const hostAttribute = attribute({
attribute: HOST_ATTRIBUTE,
value: undefined,
raws: {},
});
hostNode.replaceWith(hostAttribute);
// Generate a unique contextualized version of the selector for each selector pass as argument
// to the :host
const contextualSelectors = hostNode.nodes.map(
(contextSelectors: Selector) => {
const clonedSelector = selector.clone({}) as Selector;
const clonedHostNode = clonedSelector.at(hostIndex) as Tag;
// Add to the compound selector previously containing the :host pseudo class
// the contextual selectors.
contextSelectors.each(node => {
trimNodeWhitespaces(node);
clonedSelector.insertAfter(clonedHostNode, node);
});
return clonedSelector;
},
);
// Replace the current selector with the different variants
replaceNodeWith(selector, ...contextualSelectors);
}
}
export default function transformSelector(
root: Root,
transformConfig: SelectorScopingConfig,
) {
validateSelectors(root);
root.each((selector: Selector) => {
scopeSelector(selector);
});
if (transformConfig.transformHost) {
root.each((selector: Selector) => {
transformHost(selector);
});
}
}