@stylable/core
Version:
CSS for Components
1,327 lines (1,243 loc) • 54.6 kB
text/typescript
import { basename } from 'path';
import postcss from 'postcss';
import postcssValueParser from 'postcss-value-parser';
import cloneDeep from 'lodash.clonedeep';
import { FileProcessor } from './cached-process-file';
import { unbox } from './custom-values';
import { Diagnostics } from './diagnostics';
import { evalDeclarationValue, processDeclarationValue } from './functions';
import {
nativePseudoClasses,
nativePseudoElements,
reservedKeyFrames,
} from './native-reserved-lists';
import {
setStateToNode,
stateErrors,
transformPseudoStateSelector,
validateStateDefinition,
} from './pseudo-states';
import {
createWarningRule,
getOriginDefinition,
isChildOfAtRule,
mergeChunks,
parseSelector,
SelectorAstNode,
SelectorChunk2,
separateChunks2,
stringifySelector,
traverseNode,
} from './selector-utils';
import { appendMixins } from './stylable-mixins';
import {
ClassSymbol,
ElementSymbol,
SDecl,
SRule,
StylableMeta,
StylableSymbol,
} from './stylable-processor';
import { CSSResolve, JSResolve, StylableResolver } from './stylable-resolver';
import { findRule, generateScopedCSSVar, getDeclStylable, isCSSVarProp } from './stylable-utils';
import { valueMapping } from './stylable-value-parsers';
const { hasOwnProperty } = Object.prototype;
const USE_SCOPE_SELECTOR_2 = true;
const isVendorPrefixed = require('is-vendor-prefixed');
export interface ResolvedElement {
name: string;
type: string;
resolved: Array<CSSResolve<ClassSymbol | ElementSymbol>>;
}
export interface KeyFrameWithNode {
value: string;
node: postcss.Node;
}
export interface StylableExports {
classes: Record<string, string>;
vars: Record<string, string>;
stVars: Record<string, string>;
keyframes: Record<string, string>;
}
export interface StylableResults {
meta: StylableMeta;
exports: StylableExports;
}
export interface ScopedSelectorResults {
current: StylableMeta;
symbol: StylableSymbol | null;
selectorAst: SelectorAstNode;
selector: string;
elements: ResolvedElement[][];
}
export type replaceValueHook = (
value: string,
name: string | { name: string; args: string[] },
isLocal: boolean,
passedThrough: string[]
) => string;
export type postProcessor<T = {}> = (
stylableResults: StylableResults,
transformer: StylableTransformer
) => StylableResults & T;
export interface TransformHooks {
postProcessor?: postProcessor;
replaceValueHook?: replaceValueHook;
}
type EnvMode = 'production' | 'development';
export interface TransformerOptions {
fileProcessor: FileProcessor<StylableMeta>;
requireModule: (modulePath: string) => any;
diagnostics: Diagnostics;
delimiter?: string;
keepValues?: boolean;
replaceValueHook?: replaceValueHook;
postProcessor?: postProcessor;
mode?: EnvMode;
}
export interface AdditionalSelector {
selectorNode: SelectorAstNode;
node: SelectorAstNode;
customElementChunk: string;
}
export const transformerWarnings = {
UNKNOWN_PSEUDO_ELEMENT(name: string) {
return `unknown pseudo element "${name}"`;
},
IMPORT_ISNT_EXTENDABLE() {
return 'import is not extendable';
},
CANNOT_EXTEND_UNKNOWN_SYMBOL(name: string) {
return `cannot extend unknown symbol "${name}"`;
},
CANNOT_EXTEND_JS() {
return 'JS import is not extendable';
},
KEYFRAME_NAME_RESERVED(name: string) {
return `keyframes "${name}" is reserved`;
},
UNKNOWN_IMPORT_ALIAS(name: string) {
return `cannot use alias for unknown import "${name}"`;
},
SCOPE_PARAM_NOT_ROOT(name: string) {
return `"@st-scope" parameter "${name}" does not resolve to a stylesheet root`;
},
SCOPE_PARAM_NOT_CSS(name: string) {
return `"@st-scope" parameter "${name}" must be a Stylable stylesheet, instead name originated from a JavaScript file`;
},
UNKNOWN_SCOPING_PARAM(name: string) {
return `"@st-scope" received an unknown symbol: "${name}"`;
},
};
export class StylableTransformer {
public fileProcessor: FileProcessor<StylableMeta>;
public diagnostics: Diagnostics;
public resolver: StylableResolver;
public delimiter: string;
public keepValues: boolean;
public replaceValueHook: replaceValueHook | undefined;
public postProcessor: postProcessor | undefined;
public mode: EnvMode;
private metaParts = new WeakMap<StylableMeta, MetaParts>();
constructor(options: TransformerOptions) {
this.diagnostics = options.diagnostics;
this.delimiter = options.delimiter || '__';
this.keepValues = options.keepValues || false;
this.fileProcessor = options.fileProcessor;
this.replaceValueHook = options.replaceValueHook;
this.postProcessor = options.postProcessor;
this.resolver = new StylableResolver(options.fileProcessor, options.requireModule);
this.mode = options.mode || 'production';
}
public transform(meta: StylableMeta): StylableResults {
const metaExports: StylableExports = {
classes: {},
vars: {},
stVars: {},
keyframes: {},
};
const ast = this.resetTransformProperties(meta);
this.resolver.validateImports(meta, this.diagnostics);
validateScopes(meta, this.resolver, this.diagnostics);
this.transformAst(ast, meta, metaExports);
this.transformGlobals(ast, meta);
meta.transformDiagnostics = this.diagnostics;
const result = { meta, exports: metaExports };
return this.postProcessor ? this.postProcessor(result, this) : result;
}
public transformAst(
ast: postcss.Root,
meta: StylableMeta,
metaExports?: StylableExports,
variableOverride?: Record<string, string>,
path: string[] = [],
mixinTransform = false
) {
const keyframeMapping = this.scopeKeyframes(ast, meta);
const cssVarsMapping = this.createCSSVarsMapping(ast, meta);
ast.walkRules((rule) => {
if (isChildOfAtRule(rule, 'keyframes')) {
return;
}
rule.selector = this.scopeRule(meta, rule, metaExports && metaExports.classes);
});
ast.walkAtRules(/media$/, (atRule) => {
atRule.params = evalDeclarationValue(
this.resolver,
atRule.params,
meta,
atRule,
variableOverride,
this.replaceValueHook,
this.diagnostics,
path.slice()
);
});
ast.walkDecls((decl) => {
getDeclStylable(decl as SDecl).sourceValue = decl.value;
if (isCSSVarProp(decl.prop)) {
decl.prop = this.getScopedCSSVar(decl, meta, cssVarsMapping);
}
switch (decl.prop) {
case valueMapping.mixin:
break;
case valueMapping.states:
validateStateDefinition(decl, meta, this.resolver, this.diagnostics);
break;
default:
decl.value = evalDeclarationValue(
this.resolver,
decl.value,
meta,
decl,
variableOverride,
this.replaceValueHook,
this.diagnostics,
path.slice(),
cssVarsMapping
);
}
});
if (USE_SCOPE_SELECTOR_2) {
if (!mixinTransform && meta.outputAst && this.mode === 'development') {
this.addDevRules(meta);
}
}
ast.walkRules((rule) =>
appendMixins(this, rule as SRule, meta, variableOverride || {}, cssVarsMapping, path)
);
if (metaExports) {
if (USE_SCOPE_SELECTOR_2) {
Object.assign(metaExports.classes, this.exportClasses(meta));
} else {
this.exportRootClass(meta, metaExports.classes);
}
this.exportLocalVars(meta, metaExports.stVars, variableOverride);
this.exportKeyframes(keyframeMapping, metaExports.keyframes);
this.exportCSSVars(cssVarsMapping, metaExports.vars);
}
}
public exportLocalVars(
meta: StylableMeta,
stVarsExport: Record<string, string>,
variableOverride?: Record<string, string>
) {
for (const varSymbol of meta.vars) {
const { outputValue, topLevelType } = processDeclarationValue(
this.resolver,
varSymbol.text,
meta,
varSymbol.node,
variableOverride
);
stVarsExport[varSymbol.name] = topLevelType ? unbox(topLevelType) : outputValue;
}
}
public exportCSSVars(
cssVarsMapping: Record<string, string>,
varsExport: Record<string, string>
) {
for (const varName of Object.keys(cssVarsMapping)) {
varsExport[varName.slice(2)] = cssVarsMapping[varName];
}
}
public exportKeyframes(
keyframeMapping: Record<string, KeyFrameWithNode>,
keyframesExport: Record<string, string>
) {
for (const keyframeName of Object.keys(keyframeMapping)) {
keyframesExport[keyframeName] = keyframeMapping[keyframeName].value;
}
}
public exportRootClass(meta: StylableMeta, classesExport: Record<string, string>) {
const classExports: Record<string, string> = {};
this.handleClass(
meta,
{
type: 'class',
name: meta.mappedSymbols[meta.root].name,
nodes: [],
},
meta.mappedSymbols[meta.root].name,
classExports
);
classesExport[meta.root] = classExports[meta.mappedSymbols[meta.root].name];
}
public exportClass(
meta: StylableMeta,
name: string,
classSymbol: ClassSymbol,
metaExports?: Record<string, string>
) {
const scopedName = this.scope(name, meta.namespace);
if (metaExports && !metaExports[name]) {
const extend = classSymbol ? classSymbol[valueMapping.extends] : undefined;
let exportedClasses = scopedName;
if (extend && extend !== classSymbol) {
let finalSymbol;
let finalName;
let finalMeta;
if (extend._kind === 'class') {
finalSymbol = extend;
finalName = extend.name;
finalMeta = meta;
} else if (extend._kind === 'import') {
const resolved = this.resolver.deepResolve(extend);
if (resolved && resolved._kind === 'css' && resolved.symbol) {
if (resolved.symbol._kind === 'class') {
finalSymbol = resolved.symbol;
finalName = resolved.symbol.name;
finalMeta = resolved.meta;
} else {
const found = findRule(meta.ast, '.' + classSymbol.name);
if (found) {
this.diagnostics.error(
found,
transformerWarnings.IMPORT_ISNT_EXTENDABLE(),
{ word: found.value }
);
}
}
} else if (resolved) {
const found = findRule(meta.ast, '.' + classSymbol.name);
if (found) {
if (!resolved.symbol) {
this.diagnostics.error(
found,
transformerWarnings.CANNOT_EXTEND_UNKNOWN_SYMBOL(found.value),
{ word: found.value }
);
} else {
this.diagnostics.error(
found,
transformerWarnings.CANNOT_EXTEND_JS(),
{
word: found.value,
}
);
}
}
}
}
if (finalSymbol && finalName && finalMeta && !finalSymbol[valueMapping.root]) {
const classExports: Record<string, string> = {};
this.handleClass(
finalMeta,
{ type: 'class', name: finalName, nodes: [] },
finalName,
classExports
);
if (classExports[finalName]) {
exportedClasses += ' ' + classExports[finalName];
} else {
console.error(
`something went wrong when exporting '${finalName}', ` +
`please file an issue in stylable. With specific use case`
);
}
}
}
metaExports[name] = exportedClasses;
}
return scopedName;
}
public scopeKeyframes(ast: postcss.Root, meta: StylableMeta) {
const keyframesExports: Record<string, KeyFrameWithNode> = {};
ast.walkAtRules(/keyframes$/, (atRule) => {
const name = atRule.params;
if (~reservedKeyFrames.indexOf(name)) {
this.diagnostics.error(atRule, transformerWarnings.KEYFRAME_NAME_RESERVED(name), {
word: name,
});
}
if (!keyframesExports[name]) {
keyframesExports[name] = {
value: this.scope(name, meta.namespace),
node: atRule,
};
}
atRule.params = keyframesExports[name].value;
});
ast.walkDecls(/animation$|animation-name$/, (decl: postcss.Declaration) => {
const parsed = postcssValueParser(decl.value);
parsed.nodes.forEach((node) => {
const alias = keyframesExports[node.value] && keyframesExports[node.value].value;
if (node.type === 'word' && Boolean(alias)) {
node.value = alias;
}
});
decl.value = parsed.toString();
});
return keyframesExports;
}
public createCSSVarsMapping(_ast: postcss.Root, meta: StylableMeta) {
const cssVarsMapping: Record<string, string> = {};
// imported vars
for (const imported of meta.imports) {
for (const symbolName of Object.keys(imported.named)) {
if (isCSSVarProp(symbolName)) {
const importedVar = this.resolver.deepResolve(meta.mappedSymbols[symbolName]);
if (
importedVar &&
importedVar._kind === 'css' &&
importedVar.symbol &&
importedVar.symbol._kind === 'cssVar'
) {
cssVarsMapping[symbolName] = importedVar.symbol.global
? importedVar.symbol.name
: generateScopedCSSVar(
importedVar.meta.namespace,
importedVar.symbol.name.slice(2)
);
}
}
}
}
// locally defined vars
for (const localVarName of Object.keys(meta.cssVars)) {
const cssVar = meta.cssVars[localVarName];
if (!cssVarsMapping[localVarName]) {
cssVarsMapping[localVarName] = cssVar.global
? localVarName
: generateScopedCSSVar(meta.namespace, localVarName.slice(2));
}
}
return cssVarsMapping;
}
public getScopedCSSVar(
decl: postcss.Declaration,
meta: StylableMeta,
cssVarsMapping: Record<string, string>
) {
let prop = decl.prop;
if (meta.cssVars[prop]) {
prop = cssVarsMapping[prop];
}
return prop;
}
public addGlobalsToMeta(selectorAst: SelectorAstNode[], meta?: StylableMeta) {
if (!meta) {
return;
}
for (const ast of selectorAst) {
traverseNode(ast, (inner) => {
if (inner.type === 'class') {
meta.globals[inner.name] = true;
}
});
}
}
public transformGlobals(ast: postcss.Root, meta: StylableMeta) {
ast.walkRules((r) => {
const selectorAst = parseSelector(r.selector);
traverseNode(selectorAst, (node) => {
if (node.type === 'nested-pseudo-class' && node.name === 'global') {
this.addGlobalsToMeta([node], meta);
node.type = 'selector';
return true;
}
return undefined;
});
// this.addGlobalsToMeta([selectorAst], meta);
r.selector = stringifySelector(selectorAst);
});
}
public resolveSelectorElements(meta: StylableMeta, selector: string): ResolvedElement[][] {
if (USE_SCOPE_SELECTOR_2) {
return this.scopeSelector2(meta, selector, undefined, true).elements;
} else {
return this.scopeSelector(meta, selector, undefined, true).elements;
}
}
public scopeSelector(
originMeta: StylableMeta,
selector: string,
classesExport?: Record<string, string>,
calcPaths = false,
rule?: postcss.Rule
): ScopedSelectorResults {
let meta = originMeta;
let current = meta;
let symbol: StylableSymbol | null = null;
let nestedSymbol: StylableSymbol | null;
let originSymbol: ClassSymbol | ElementSymbol;
const selectorAst = parseSelector(selector);
const addedSelectors: AdditionalSelector[] = [];
const elements = selectorAst.nodes.map((selectorNode) => {
const selectorElements: ResolvedElement[] = [];
traverseNode(selectorNode, (node) => {
const { name, type } = node;
if (
calcPaths &&
(type === 'class' || type === 'element' || type === 'pseudo-element')
) {
selectorElements.push({
name,
type,
resolved: this.resolver.resolveExtends(
current,
name,
type === 'element',
this
),
});
}
if (type === 'selector' || type === 'spacing' || type === 'operator') {
if (nestedSymbol) {
symbol = nestedSymbol;
nestedSymbol = null;
} else {
meta = originMeta;
current = originMeta;
symbol = originMeta.classes[originMeta.root];
originSymbol = symbol;
}
} else if (type === 'class') {
const next = this.handleClass(
current,
node,
name,
classesExport,
rule,
originMeta
);
originSymbol = current.classes[name];
symbol = next.symbol;
current = next.meta;
} else if (type === 'element') {
const next = this.handleElement(current, node, name, originMeta);
originSymbol = current.elements[name];
symbol = next.symbol;
current = next.meta;
} else if (type === 'pseudo-element') {
const next = this.handlePseudoElement(
current,
node,
name,
selectorNode,
addedSelectors,
rule,
originMeta
);
originSymbol = current.classes[name];
meta = current;
symbol = next.symbol;
current = next.meta;
} else if (type === 'pseudo-class') {
current = transformPseudoStateSelector(
current,
node,
name,
symbol,
meta,
originSymbol,
this.resolver,
this.diagnostics,
rule
);
} else if (type === 'nested-pseudo-class') {
if (name === 'global') {
// node.type = 'selector';
return true;
}
nestedSymbol = symbol;
} else if (type === 'invalid' && node.value === '&' && current.parent) {
const origin = current.mappedSymbols[current.root];
const next = this.handleClass(
current,
{
type: 'class',
nodes: [],
name: origin.name,
},
origin.name,
undefined,
undefined,
originMeta
);
originSymbol = current.classes[origin.name];
symbol = next.symbol;
current = next.meta;
}
/* do nothing */
return undefined;
});
return selectorElements;
});
this.addAdditionalSelectors(addedSelectors, selectorAst);
return {
current,
symbol,
selectorAst,
elements,
selector: stringifySelector(selectorAst),
};
}
public addAdditionalSelectors(
addedSelectors: AdditionalSelector[],
selectorAst: SelectorAstNode
) {
addedSelectors.forEach((s) => {
const clone = cloneDeep(s.selectorNode);
const i = s.selectorNode.nodes.indexOf(s.node);
if (i === -1) {
throw new Error('not supported inside nested classes');
} else {
clone.nodes[i].value = s.customElementChunk;
}
selectorAst.nodes.push(clone);
});
}
public applyRootScoping(meta: StylableMeta, selectorAst: SelectorAstNode) {
const scopedRoot =
(meta.mappedSymbols[meta.root] as ClassSymbol)[valueMapping.global] ||
this.scope(meta.root, meta.namespace);
selectorAst.nodes.forEach((selector) => {
const first = selector.nodes[0];
/* This finds a transformed or non transform global selector */
if (
first &&
(first.type === 'selector' || first.type === 'nested-pseudo-class') &&
first.name === 'global'
) {
return;
}
// -st-global can make anther global inside root
if (first && first.nodes === scopedRoot) {
return;
}
if (first && first.before && first.before === '.' + scopedRoot) {
return;
}
if (first && first.type === 'invalid' && first.value === '&') {
return;
}
if (!first || first.name !== scopedRoot) {
selector.nodes = [
typeof scopedRoot !== 'string'
? { type: 'selector', nodes: scopedRoot, name: 'global' }
: { type: 'class', name: scopedRoot, nodes: [] },
{ type: 'spacing', value: ' ', name: '', nodes: [] },
...selector.nodes,
];
}
});
}
public scopeRule(
meta: StylableMeta,
rule: postcss.Rule,
_classesExport?: Record<string, string>
): string {
if (USE_SCOPE_SELECTOR_2) {
return this.scopeSelector2(meta, rule.selector, undefined, false, rule).selector;
} else {
return this.scopeSelector(meta, rule.selector, _classesExport, false, rule).selector;
}
}
public handleClass(
meta: StylableMeta,
node: SelectorAstNode,
name: string,
classesExport?: Record<string, string>,
rule?: postcss.Rule,
originMeta?: StylableMeta
): CSSResolve {
const symbol = meta.classes[name];
const extend = symbol ? symbol[valueMapping.extends] : undefined;
if (!extend && symbol && symbol.alias) {
let next = this.resolver.deepResolve(symbol.alias);
if (next && next._kind === 'css' && next.symbol && next.symbol._kind === 'class') {
const globalMappedNodes = next.symbol[valueMapping.global];
if (globalMappedNodes) {
node.before = '';
node.type = 'selector';
node.nodes = globalMappedNodes;
this.addGlobalsToMeta(globalMappedNodes, originMeta);
} else {
node.name = this.exportClass(
next.meta,
next.symbol.name,
next.symbol,
classesExport
);
}
if (next.symbol[valueMapping.extends]) {
next = this.resolver.deepResolve(next.symbol[valueMapping.extends]);
if (next && next._kind === 'css') {
return next;
}
} else {
return next;
}
} else if (rule) {
this.diagnostics.error(rule, transformerWarnings.UNKNOWN_IMPORT_ALIAS(name), {
word: symbol.alias.name,
});
}
}
let scopedName = '';
let globalScopedSelector = '';
const globalMappedNodes = symbol && symbol[valueMapping.global];
if (globalMappedNodes) {
globalScopedSelector = stringifySelector({
type: 'selector',
name: '',
nodes: globalMappedNodes,
});
} else {
scopedName = this.exportClass(meta, name, symbol, classesExport);
}
if (globalScopedSelector) {
node.before = '';
node.type = 'selector';
node.nodes = symbol[valueMapping.global] || [];
this.addGlobalsToMeta(globalMappedNodes!, originMeta);
} else {
node.name = scopedName;
}
const next = this.resolver.deepResolve(extend);
if (next && next._kind === 'css' && next.symbol && next.symbol._kind === 'class') {
if (this.mode === 'development' && rule && (rule as SRule).selectorType === 'class') {
rule.after(
createWarningRule(
next.symbol.name,
this.scope(next.symbol.name, next.meta.namespace),
basename(next.meta.source),
name,
this.scope(symbol.name, meta.namespace),
basename(meta.source)
)
);
}
return next;
}
// local
if (extend && extend._kind === 'class') {
if (extend === symbol && extend.alias) {
const next = this.resolver.deepResolve(extend.alias);
if (next && next._kind === 'css' && next.symbol) {
return next;
}
}
}
return { _kind: 'css', meta, symbol };
}
public handleElement(
meta: StylableMeta,
node: SelectorAstNode,
name: string,
originMeta?: StylableMeta
) {
const tRule = meta.elements[name] as StylableSymbol;
const extend = tRule ? meta.mappedSymbols[name] : undefined;
const next = this.resolver.deepResolve(extend);
if (next && next._kind === 'css' && next.symbol) {
if (next.symbol._kind === 'class' && next.symbol[valueMapping.global]) {
node.before = '';
node.type = 'selector';
node.nodes = next.symbol[valueMapping.global] || [];
this.addGlobalsToMeta(node.nodes, originMeta);
} else {
node.type = 'class';
node.name = this.scope(next.symbol.name, next.meta.namespace);
}
// node.name = (next.symbol as ClassSymbol)[valueMapping.global] ||
// this.scope(next.symbol.name, next.meta.namespace);
return next;
}
return { meta, symbol: tRule };
}
public handlePseudoElement(
meta: StylableMeta,
node: SelectorAstNode,
name: string,
selectorNode: SelectorAstNode,
addedSelectors: AdditionalSelector[],
rule?: postcss.Rule,
originMeta?: StylableMeta
): CSSResolve {
let next: JSResolve | CSSResolve | null;
const customSelector = meta.customSelectors[':--' + name];
if (customSelector) {
const rootRes = this.scopeSelector(meta, '.root', {}, false);
const res = this.scopeSelector(meta, customSelector, {}, false);
const rootEg = new RegExp('^\\s*' + rootRes.selector.replace(/\./, '\\.') + '\\s*');
const selectors = res.selectorAst.nodes.map((sel) =>
stringifySelector(sel).trim().replace(rootEg, '')
);
if (selectors[0]) {
node.type = 'invalid'; /*just take it */
node.before = ' ';
node.value = selectors[0];
}
for (let i = 1 /*start from second one*/; i < selectors.length; i++) {
addedSelectors.push({
selectorNode,
node,
customElementChunk: selectors[i],
});
}
if (res.selectorAst.nodes.length === 1 && res.symbol) {
return { _kind: 'css', meta: res.current, symbol: res.symbol };
}
// this is an error mode fallback
return {
_kind: 'css',
meta,
symbol: { _kind: 'element', name: '*' },
};
}
// find if the current symbol exists in the initial meta;
let symbol = meta.mappedSymbols[name];
let current = meta;
while (!symbol) {
// go up the root extends path and find first symbol
const root = current.mappedSymbols[current.root] as ClassSymbol;
next = this.resolver.deepResolve(root[valueMapping.extends]);
if (next && next._kind === 'css') {
current = next.meta;
symbol = next.meta.mappedSymbols[name];
} else {
break;
}
}
if (symbol) {
if (symbol._kind === 'class') {
node.type = 'class';
node.before = symbol[valueMapping.root] ? '' : ' ';
next = this.resolver.deepResolve(symbol);
if (symbol[valueMapping.global]) {
node.type = 'selector';
node.nodes = symbol[valueMapping.global] || [];
this.addGlobalsToMeta(node.nodes, originMeta);
} else {
if (symbol.alias && !symbol[valueMapping.extends]) {
if (next && next.meta && next.symbol) {
node.name = this.scope(next.symbol.name, next.meta.namespace);
} else {
// TODO: maybe warn on un resolved alias
}
} else {
node.name = this.scope(symbol.name, current.namespace);
}
}
if (next && next._kind === 'css') {
return next;
}
}
} else if (rule) {
if (!nativePseudoElements.includes(name) && !isVendorPrefixed(name)) {
this.diagnostics.warn(rule, transformerWarnings.UNKNOWN_PSEUDO_ELEMENT(name), {
word: name,
});
}
}
return { _kind: 'css', meta: current, symbol };
}
public scope(name: string, namespace: string, delimiter: string = this.delimiter) {
return namespace ? namespace + delimiter + name : name;
}
public exportClasses(meta: StylableMeta) {
const locals: Record<string, string> = {};
const metaParts = this.resolveMetaParts(meta);
for (const [localName, resolved] of Object.entries(metaParts.class)) {
const exportedClasses = this.getPartExports(resolved);
locals[localName] = exportedClasses.join(' ');
}
return locals;
}
/* None alias symbol */
public getPartExports(resolved: Array<CSSResolve<ClassSymbol | ElementSymbol>>) {
const exportedClasses = [];
let first = true;
for (const { meta, symbol } of resolved) {
if (!first && symbol[valueMapping.root]) {
break;
}
first = false;
if (symbol.alias && !symbol[valueMapping.extends]) {
continue;
}
exportedClasses.push(this.scope(symbol.name, meta.namespace));
}
return exportedClasses;
}
public scopeSelector2(
originMeta: StylableMeta,
selector: string,
_classesExport?: Record<string, string>,
_calcPaths = false,
rule?: postcss.Rule
): { selector: string; elements: ResolvedElement[][] } {
const context = new ScopeContext(
originMeta,
parseSelector(selector),
rule || postcss.rule({ selector })
);
return {
selector: stringifySelector(this.scopeSelectorAst(context)),
elements: context.elements,
};
}
public scopeSelectorAst(context: ScopeContext): SelectorAstNode {
const { originMeta, selectorAst } = context;
// split selectors to chunks: .a.b .c:hover, a .c:hover -> [[[.a.b], [.c:hover]], [[.a], [.c:hover]]]
const selectorListChunks = separateChunks2(selectorAst);
// resolve meta classes and elements
context.metaParts = this.resolveMetaParts(originMeta);
// set stylesheet root as the global anchor
if (!context.currentAnchor) {
context.initRootAnchor({
name: originMeta.root,
type: 'class',
resolved: context.metaParts.class[originMeta.root],
});
}
// loop over selectors
for (const selectorChunks of selectorListChunks) {
context.elements.push([]);
context.selectorIndex++;
context.chunks = selectorChunks;
// loop over chunks
for (const chunk of selectorChunks) {
context.chunk = chunk;
// loop over each node in a chunk
for (const node of chunk.nodes) {
context.node = node;
// transfrom node
this.handleChunkNode(context);
}
}
if (selectorListChunks.length - 1 > context.selectorIndex) {
context.initRootAnchor({
name: originMeta.root,
type: 'class',
resolved: context.metaParts.class[originMeta.root],
});
}
}
const outputAst = mergeChunks(selectorListChunks);
context.additionalSelectors.forEach((addSelector) => outputAst.nodes.push(addSelector()));
return outputAst;
}
private handleChunkNode(context: ScopeContext) {
const {
currentAnchor,
metaParts,
node,
originMeta,
transformGlobals,
} = context as Required<ScopeContext>;
const { type, name } = node;
if (type === 'class') {
const resolved = metaParts.class[name] || [
// used to scope classes from js mixins
{ _kind: 'css', meta: originMeta, symbol: { _kind: 'class', name } },
];
context.setCurrentAnchor({ name, type: 'class', resolved });
const { symbol, meta } = getOriginDefinition(resolved);
this.scopeClassNode(symbol, meta, node, originMeta);
} else if (type === 'element') {
const resolved = metaParts.element[name] || [
// provides resolution for native elements
{ _kind: 'css', meta: originMeta, symbol: { _kind: 'element', name } },
];
context.setCurrentAnchor({ name, type: 'element', resolved });
// native node does not resolve e.g. div
if (resolved && resolved.length > 1) {
const { symbol, meta } = getOriginDefinition(resolved);
this.scopeClassNode(symbol, meta, node, originMeta);
}
} else if (type === 'pseudo-element') {
const len = currentAnchor.resolved.length;
const lookupStartingPoint = len === 1 /* no extends */ ? 0 : 1;
let resolved: Array<CSSResolve<ClassSymbol | ElementSymbol>> | undefined;
for (let i = lookupStartingPoint; i < len; i++) {
const { symbol, meta } = currentAnchor.resolved[i];
if (!symbol[valueMapping.root]) {
// debugger
continue;
}
const customSelector = meta.customSelectors[':--' + name];
if (customSelector) {
this.handleCustomSelector(customSelector, meta, context, name, node);
return;
}
const requestedPart = meta.classes[name];
if (symbol.alias || !requestedPart) {
// skip alias since thay cannot add parts
continue;
}
resolved = this.resolveMetaParts(meta).class[name];
// first definition of a part in the extends/alias chain
context.setCurrentAnchor({
name,
type: 'pseudo-element',
resolved,
});
const resolvedPart = getOriginDefinition(resolved);
node.before = resolvedPart.symbol[valueMapping.root] ? '' : ' ';
this.scopeClassNode(resolvedPart.symbol, resolvedPart.meta, node, originMeta);
break;
}
if (!resolved) {
// first definition of a part in the extends/alias chain
context.setCurrentAnchor({
name,
type: 'pseudo-element',
resolved: [],
});
if (!nativePseudoElements.includes(name) && !isVendorPrefixed(name)) {
this.diagnostics.warn(
context.rule,
transformerWarnings.UNKNOWN_PSEUDO_ELEMENT(name),
{
word: name,
}
);
}
}
} else if (type === 'pseudo-class') {
let found = false;
for (const { symbol, meta } of currentAnchor.resolved) {
const states = symbol[valueMapping.states];
if (states && hasOwnProperty.call(states, name)) {
found = true;
setStateToNode(
states,
meta,
name,
node,
meta.namespace,
this.resolver,
this.diagnostics,
context.rule
);
break;
}
}
if (!found && !nativePseudoClasses.includes(name) && !isVendorPrefixed(name)) {
this.diagnostics.warn(context.rule, stateErrors.UNKNOWN_STATE_USAGE(name), {
word: name,
});
}
} else if (type === 'nested-pseudo-class') {
if (name === 'global') {
// :global(.a) -> .a
if (transformGlobals) {
node.type = 'selector';
}
} else {
const nestedContext = context.createNestedContext({
type: 'selectors',
name: `${name}`,
nodes: node.nodes,
});
this.scopeSelectorAst(nestedContext);
// delegate elements of first selector
context.elements[context.selectorIndex].push(...nestedContext.elements[0]);
}
} else if (type === 'invalid' && node.value === '&') {
if (/* maybe should be currentAnchor meta */ originMeta.parent) {
const origin = originMeta.mappedSymbols[originMeta.root] as ClassSymbol;
context.setCurrentAnchor({
name: origin.name,
type: 'class',
resolved: metaParts.class[origin.name],
});
}
}
}
private handleCustomSelector(
customSelector: string,
meta: StylableMeta,
context: ScopeContext,
name: string,
node: SelectorAstNode
) {
const selectorListChunks = separateChunks2(parseSelector(customSelector));
const hasSingleSelector = selectorListChunks.length === 1;
removeFirstRootInEachSelectorChunk(selectorListChunks, meta);
const internalContext = new ScopeContext(
meta,
mergeChunks(selectorListChunks),
context.rule
);
const customAstSelectors = this.scopeSelectorAst(internalContext).nodes;
customAstSelectors.forEach(trimLeftSelectorAst);
if (hasSingleSelector && internalContext.currentAnchor) {
context.setCurrentAnchor({
name,
type: 'pseudo-element',
resolved: internalContext.currentAnchor.resolved,
});
} else {
// unknown context due to multiple selectors
context.setCurrentAnchor({
name,
type: 'pseudo-element',
resolved: anyElementAnchor(meta).resolved,
});
}
Object.assign(node, customAstSelectors[0]);
// first one handled inline above
for (let i = 1; i < customAstSelectors.length; i++) {
const selectorNode = context.selectorAst.nodes[context.selectorIndex];
const nodeIndex = selectorNode.nodes.indexOf(node);
context.additionalSelectors.push(
lazyCreateSelector(customAstSelectors[i], selectorNode, nodeIndex)
);
}
}
private scopeClassNode(symbol: any, meta: any, node: any, originMeta: any) {
if (symbol[valueMapping.global]) {
const globalMappedNodes = symbol[valueMapping.global];
node.type = 'selector';
node.nodes = globalMappedNodes;
this.addGlobalsToMeta(globalMappedNodes, originMeta);
} else {
node.type = 'class';
node.name = this.scope(symbol.name, meta.namespace);
}
}
private resolveMetaParts(meta: StylableMeta): MetaParts {
let metaParts = this.metaParts.get(meta);
if (!metaParts) {
const resolvedClasses: Record<
string,
Array<CSSResolve<ClassSymbol | ElementSymbol>>
> = {};
for (const className of Object.keys(meta.classes)) {
resolvedClasses[className] = this.resolver.resolveExtends(
meta,
className,
false,
undefined,
(res, extend) => {
const decl = findRule(meta.ast, '.' + className);
if (decl) {
if (res && res._kind === 'js') {
this.diagnostics.error(
decl,
transformerWarnings.CANNOT_EXTEND_JS(),
{
word: decl.value,
}
);
} else if (res && !res.symbol) {
this.diagnostics.error(
decl,
transformerWarnings.CANNOT_EXTEND_UNKNOWN_SYMBOL(extend.name),
{ word: decl.value }
);
} else {
this.diagnostics.error(
decl,
transformerWarnings.IMPORT_ISNT_EXTENDABLE(),
{ word: decl.value }
);
}
} else {
if (meta.classes[className] && meta.classes[className].alias) {
meta.ast.walkRules(new RegExp('\\.' + className), (rule) => {
this.diagnostics.error(
rule,
transformerWarnings.UNKNOWN_IMPORT_ALIAS(className),
{ word: className }
);
return false;
});
}
}
}
);
}
const resolvedElements: Record<
string,
Array<CSSResolve<ClassSymbol | ElementSymbol>>
> = {};
for (const k of Object.keys(meta.elements)) {
resolvedElements[k] = this.resolver.resolveExtends(meta, k, true);
}
metaParts = { class: resolvedClasses, element: resolvedElements };
this.metaParts.set(meta, metaParts);
}
return metaParts;
}
private addDevRules(meta: StylableMeta) {
const metaParts = this.resolveMetaParts(meta);
for (const [className, resolved] of Object.entries(metaParts.class)) {
if (resolved.length > 1) {
meta.outputAst!.walkRules('.' + this.scope(className, meta.namespace), (rule) => {
const a = resolved[0];
const b = resolved[1];
rule.after(
createWarningRule(
b.symbol.name,
this.scope(b.symbol.name, b.meta.namespace),
basename(b.meta.source),
a.symbol.name,
this.scope(a.symbol.name, a.meta.namespace),
basename(a.meta.source),
true
)
);
});
}
}
}
private resetTransformProperties(meta: StylableMeta) {
meta.globals = {};
return (meta.outputAst = meta.ast.clone());
}
}
export function removeSTDirective(root: postcss.Root) {
const toRemove: postcss.Node[] = [];
root.walkRules((rule: postcss.Rule) => {
if (rule.nodes && rule.nodes.length === 0) {
toRemove.push(rule);
return;
}
rule.walkDecls((decl: postcss.Declaration) => {
if (decl.prop.startsWith('-st-')) {
toRemove.push(decl);
}
});
if (rule.raws) {
rule.raws = {
after: '\n',
};
}
});
if (root.raws) {
root.raws = {};
}
function removeRecursiveUpIfEmpty(node: postcss.Node) {
const parent = node.parent;
node.remove();
if (parent && parent.nodes && parent.nodes.length === 0) {
removeRecursiveUpIfEmpty(parent);
}
}
toRemove.forEach((node) => {
removeRecu