UNPKG

@stylable/core

Version:

CSS for Components

1,327 lines (1,243 loc) 54.6 kB
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