diff --git a/packages/core/src/deprecated/postcss-ast-extension.ts b/packages/core/src/deprecated/postcss-ast-extension.ts index 3c8645a8e..e6df6ccf0 100644 --- a/packages/core/src/deprecated/postcss-ast-extension.ts +++ b/packages/core/src/deprecated/postcss-ast-extension.ts @@ -43,6 +43,7 @@ export interface SRule extends Rule { selectorAst: SelectorAstNode; isSimpleSelector: boolean; selectorType: 'class' | 'element' | 'complex'; + /**@deprecated*/ mixins?: RefedMixin[]; stScopeSelector?: string; } diff --git a/packages/core/src/features/feature.ts b/packages/core/src/features/feature.ts index 4f7b913be..4ee495bbe 100644 --- a/packages/core/src/features/feature.ts +++ b/packages/core/src/features/feature.ts @@ -1,5 +1,5 @@ import type { StylableMeta } from '../stylable-meta'; -import type { ScopeContext, StylableExports } from '../stylable-transformer'; +import type { ScopeContext, StylableExports, StylableTransformer } from '../stylable-transformer'; import type { StylableResolver, MetaResolvedSymbols } from '../stylable-resolver'; import type { StylableEvaluator, EvalValueData } from '../functions'; import type * as postcss from 'postcss'; @@ -69,7 +69,13 @@ export interface FeatureHooks { data: EvalValueData; }) => void; transformJSExports: (options: { exports: StylableExports; resolved: T['RESOLVED'] }) => void; - transformLastPass: (options: { context: FeatureTransformContext }) => void; + transformLastPass: (options: { + context: FeatureTransformContext; + ast: postcss.Root; + transformer: StylableTransformer; + cssVarsMapping: Record; + path: string[]; + }) => void; } const defaultHooks: FeatureHooks = { metaInit() { diff --git a/packages/core/src/features/index.ts b/packages/core/src/features/index.ts index 553ac64c2..4770ae154 100644 --- a/packages/core/src/features/index.ts +++ b/packages/core/src/features/index.ts @@ -11,6 +11,9 @@ export * as STGlobal from './st-global'; export * as STVar from './st-var'; export type { VarSymbol } from './st-var'; +export * as STMixin from './st-mixin'; +export type { RefedMixin, MixinValue } from './st-mixin'; + export * as CSSClass from './css-class'; export type { ClassSymbol } from './css-class'; @@ -23,4 +26,4 @@ export type { CSSVarSymbol } from './css-custom-property'; export * as CSSKeyframes from './css-keyframes'; export type { KeyframesSymbol } from './css-keyframes'; -export type { RefedMixin, StylableDirectives } from './types'; +export type { StylableDirectives } from './types'; diff --git a/packages/core/src/features/st-import.ts b/packages/core/src/features/st-import.ts index 737debd0c..cbcd13319 100644 --- a/packages/core/src/features/st-import.ts +++ b/packages/core/src/features/st-import.ts @@ -7,7 +7,6 @@ import { ignoreDeprecationWarn } from '../helpers/deprecation'; import { parseStImport, parsePseudoImport, parseImportMessages } from '../helpers/import'; import { isCSSVarProp } from '../helpers/css-custom-property'; import type { StylableMeta } from '../stylable-meta'; -import { rootValueMapping, valueMapping } from '../stylable-value-parsers'; import path from 'path'; import type { ImmutablePseudoClass, PseudoClass } from '@tokey/css-selector-parser'; import type * as postcss from 'postcss'; @@ -30,6 +29,13 @@ export interface Imported { context: string; } +export const PseudoImport = `:import`; +export const PseudoImportDecl = { + DEFAULT: `-st-default` as const, + NAMED: `-st-named` as const, + FROM: `-st-from` as const, +}; + /** * ImportTypeHook is used as a way to cast imported symbols before resolving their actual type. * currently used only for `keyframes` as they are completely on a separate namespace from other symbols. @@ -113,7 +119,7 @@ export const hooks = createFeature<{ if (rule.selector !== `:import`) { context.diagnostics.warn( rule, - diagnostics.FORBIDDEN_DEF_IN_COMPLEX_SELECTOR(rootValueMapping.import) + diagnostics.FORBIDDEN_DEF_IN_COMPLEX_SELECTOR(PseudoImport) ); return; } @@ -201,7 +207,7 @@ function validateImports(context: FeatureTransformContext) { const fromDecl = importObj.rule.nodes && importObj.rule.nodes.find( - (decl) => decl.type === 'decl' && decl.prop === valueMapping.from + (decl) => decl.type === 'decl' && decl.prop === PseudoImportDecl.FROM ); context.diagnostics.warn( @@ -218,7 +224,7 @@ function validateImports(context: FeatureTransformContext) { const namedDecl = importObj.rule.nodes && importObj.rule.nodes.find( - (decl) => decl.type === 'decl' && decl.prop === valueMapping.named + (decl) => decl.type === 'decl' && decl.prop === PseudoImportDecl.NAMED ); context.diagnostics.warn( diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts new file mode 100644 index 000000000..97119b00c --- /dev/null +++ b/packages/core/src/features/st-mixin.ts @@ -0,0 +1,423 @@ +import { createFeature, FeatureContext, FeatureTransformContext } from './feature'; +import * as STSymbol from './st-symbol'; +import type { ImportSymbol } from './st-import'; +import type { ElementSymbol } from './css-type'; +import type { ClassSymbol } from './css-class'; +import { createSubsetAst } from '../helpers/rule'; +import { + diagnostics as MixinHelperDiagnostics, + parseStMixin, + parseStPartialMixin, +} from '../helpers/mixin'; +import { ignoreDeprecationWarn } from '../helpers/deprecation'; +import { resolveArgumentsValue } from '../functions'; +import { cssObjectToAst } from '../parser'; +import * as postcss from 'postcss'; +import type { FunctionNode, WordNode } from 'postcss-value-parser'; +import { fixRelativeUrls } from '../stylable-assets'; +import { isValidDeclaration, mergeRules, INVALID_MERGE_OF } from '../stylable-utils'; +import type { StylableMeta } from '../stylable-meta'; +import type { CSSResolve } from '../stylable-resolver'; +import type { StylableTransformer } from '../stylable-transformer'; +import { dirname } from 'path'; +// ToDo: deprecate - stop usage +import type { SRule } from '../deprecated/postcss-ast-extension'; + +export interface MixinValue { + type: string; + options: Array<{ value: string }> | Record; + partial?: boolean; + valueNode?: FunctionNode | WordNode; + originDecl: postcss.Declaration; +} + +export interface RefedMixin { + mixin: MixinValue; + ref: ImportSymbol | ClassSymbol | ElementSymbol; +} + +export const MixinType = { + ALL: `-st-mixin` as const, + PARTIAL: `-st-partial-mixin` as const, +}; + +export const diagnostics = { + VALUE_CANNOT_BE_STRING: MixinHelperDiagnostics.VALUE_CANNOT_BE_STRING, + INVALID_NAMED_PARAMS: MixinHelperDiagnostics.INVALID_NAMED_PARAMS, + INVALID_MERGE_OF: INVALID_MERGE_OF, + PARTIAL_MIXIN_MISSING_ARGUMENTS(type: string) { + return `"${MixinType.PARTIAL}" can only be used with override arguments provided, missing overrides on "${type}"`; + }, + UNKNOWN_MIXIN(name: string) { + return `unknown mixin: "${name}"`; + }, + OVERRIDE_MIXIN(mixinType: string) { + return `override ${mixinType} on same rule`; + }, + FAILED_TO_APPLY_MIXIN(error: string) { + return `could not apply mixin: ${error}`; + }, + JS_MIXIN_NOT_A_FUNC() { + return `js mixin must be a function`; + }, + CIRCULAR_MIXIN(circularPaths: string[]) { + return `circular mixin found: ${circularPaths.join(' --> ')}`; + }, + UNKNOWN_MIXIN_SYMBOL(name: string) { + return `cannot mixin unknown symbol "${name}"`; + }, +}; + +// HOOKS + +export const hooks = createFeature({ + analyzeDeclaration({ context, decl }) { + ignoreDeprecationWarn(() => { + const parentRule = decl.parent as SRule; + const prevMixins = parentRule?.mixins || []; + const mixins = collectDeclMixins( + context, + decl, + (mixinSymbolName) => { + const symbol = STSymbol.get(context.meta, mixinSymbolName); + return symbol?._kind === 'import' && !symbol.import.from.match(/.css$/) + ? 'args' + : 'named'; + }, + false /*dont report param signature diagnostics*/, + prevMixins + ); + if (mixins.length) { + parentRule.mixins = mixins; + } + }); + }, + transformLastPass({ context, ast, transformer, cssVarsMapping, path }) { + ast.walkRules((rule) => appendMixins(context, transformer, rule, cssVarsMapping, path)); + }, +}); + +// API + +export function appendMixins( + context: FeatureTransformContext, + transformer: StylableTransformer, + rule: postcss.Rule, + cssPropertyMapping: Record, + path: string[] = [] +) { + const [decls, mixins] = collectRuleMixins(context, rule); + if (!mixins || mixins.length === 0) { + return; + } + for (const mixin of mixins) { + appendMixin(context, { transformer, mixDef: mixin, rule, path, cssPropertyMapping }); + } + for (const mixinDecl of decls) { + mixinDecl.remove(); + } +} + +function collectRuleMixins( + context: FeatureTransformContext, + rule: postcss.Rule +): [decls: postcss.Declaration[], mixins: RefedMixin[]] { + let mixins: RefedMixin[] = []; + const { mainNamespace } = context.getResolvedSymbols(context.meta); + const decls: postcss.Declaration[] = []; + rule.walkDecls((decl) => { + if (decl.prop === `-st-mixin` || decl.prop === `-st-partial-mixin`) { + decls.push(decl); + mixins = collectDeclMixins( + context, + decl, + (mixinSymbolName) => { + return mainNamespace[mixinSymbolName] === 'js' ? 'args' : 'named'; + }, + true /* report param signature diagnostics */, + mixins + ); + } + }); + return [decls, mixins]; +} + +function collectDeclMixins( + context: FeatureContext, + decl: postcss.Declaration, + paramSignature: (mixinSymbolName: string) => 'named' | 'args', + emitStrategyDiagnostics: boolean, + previousMixins?: RefedMixin[] +): RefedMixin[] { + const { meta } = context; + let mixins: RefedMixin[] = []; + const parser = + decl.prop === MixinType.ALL + ? parseStMixin + : decl.prop === MixinType.PARTIAL + ? parseStPartialMixin + : null; + if (!parser) { + return previousMixins || mixins; + } + + parser(decl, paramSignature, context.diagnostics, emitStrategyDiagnostics).forEach((mixin) => { + const mixinRefSymbol = STSymbol.get(meta, mixin.type); + if ( + mixinRefSymbol && + (mixinRefSymbol._kind === 'import' || + mixinRefSymbol._kind === 'class' || + mixinRefSymbol._kind === 'element') + ) { + if (mixin.partial && Object.keys(mixin.options).length === 0) { + context.diagnostics.warn( + decl, + diagnostics.PARTIAL_MIXIN_MISSING_ARGUMENTS(mixin.type), + { + word: mixin.type, + } + ); + } + const refedMixin = { + mixin, + ref: mixinRefSymbol, + }; + mixins.push(refedMixin); + ignoreDeprecationWarn(() => meta.mixins).push(refedMixin); + } else { + context.diagnostics.warn(decl, diagnostics.UNKNOWN_MIXIN(mixin.type), { + word: mixin.type, + }); + } + }); + + if (previousMixins) { + const partials = previousMixins.filter((r) => r.mixin.partial); + const nonPartials = previousMixins.filter((r) => !r.mixin.partial); + const isInPartial = decl.prop === MixinType.PARTIAL; + if ( + (partials.length && decl.prop === MixinType.PARTIAL) || + (nonPartials.length && decl.prop === MixinType.ALL) + ) { + context.diagnostics.warn(decl, diagnostics.OVERRIDE_MIXIN(decl.prop)); + } + if (partials.length && nonPartials.length) { + mixins = isInPartial ? nonPartials.concat(mixins) : partials.concat(mixins); + } else if (partials.length) { + mixins = isInPartial ? mixins : partials.concat(mixins); + } else if (nonPartials.length) { + mixins = isInPartial ? nonPartials.concat(mixins) : mixins; + } + } + return mixins; +} + +interface ApplyMixinContext { + transformer: StylableTransformer; + mixDef: RefedMixin; + rule: postcss.Rule; + path: string[]; + cssPropertyMapping: Record; +} + +export function appendMixin(context: FeatureTransformContext, config: ApplyMixinContext) { + if (checkRecursive(context, config)) { + return; + } + const resolvedSymbols = context.getResolvedSymbols(context.meta); + const symbolName = config.mixDef.mixin.type; + const resolvedType = resolvedSymbols.mainNamespace[symbolName]; + if (resolvedType === `class` || resolvedType === `element`) { + const resolveChain = resolvedSymbols[resolvedType][symbolName]; + handleCSSMixin(context, config, resolveChain); + return; + } else if (resolvedType === `js`) { + const resolvedMixin = resolvedSymbols.js[symbolName]; + if (typeof resolvedMixin.symbol === 'function') { + try { + handleJSMixin(context, config, resolvedMixin.symbol); + } catch (e) { + context.diagnostics.error( + config.rule, + diagnostics.FAILED_TO_APPLY_MIXIN(String(e)), + { + word: config.mixDef.mixin.type, + } + ); + return; + } + } else { + context.diagnostics.error(config.rule, diagnostics.JS_MIXIN_NOT_A_FUNC(), { + word: config.mixDef.mixin.type, + }); + } + return; + } + + // ToDo: report on unsupported mixed in symbol type + const mixinDecl = config.mixDef.mixin.originDecl; + context.diagnostics.error(mixinDecl, diagnostics.UNKNOWN_MIXIN_SYMBOL(mixinDecl.value), { + word: mixinDecl.value, + }); +} + +function checkRecursive( + { meta, diagnostics: report }: FeatureTransformContext, + { mixDef, path, rule }: ApplyMixinContext +) { + const symbolName = + mixDef.ref.name === meta.root + ? mixDef.ref._kind === 'class' + ? meta.root + : 'default' + : mixDef.mixin.type; + const isRecursive = path.includes(symbolName + ' from ' + meta.source); + if (isRecursive) { + // Todo: add test verifying word + report.warn(rule, diagnostics.CIRCULAR_MIXIN(path), { + word: symbolName, + }); + return true; + } + return false; +} + +function handleJSMixin( + context: FeatureTransformContext, + config: ApplyMixinContext, + mixinFunction: (...args: any[]) => any +) { + const stVarOverride = context.evaluator.stVarOverride || {}; + const meta = context.meta; + const mixDef = config.mixDef; + const res = mixinFunction((mixDef.mixin.options as any[]).map((v) => v.value)); + const mixinRoot = cssObjectToAst(res).root; + + mixinRoot.walkDecls((decl) => { + if (!isValidDeclaration(decl)) { + decl.value = String(decl); + } + }); + + config.transformer.transformAst(mixinRoot, meta, undefined, stVarOverride, [], true); + const mixinPath = (mixDef.ref as ImportSymbol).import.request; + fixRelativeUrls( + mixinRoot, + context.resolver.resolvePath(dirname(meta.source), mixinPath), + meta.source + ); + + mergeRules(mixinRoot, config.rule, mixDef.mixin.originDecl, context.diagnostics); +} + +function handleCSSMixin( + context: FeatureTransformContext, + config: ApplyMixinContext, + resolveChain: CSSResolve[] +) { + const mixDef = config.mixDef; + const isPartial = mixDef.mixin.partial; + const namedArgs = mixDef.mixin.options as Record; + const overrideKeys = Object.keys(namedArgs); + + if (isPartial && overrideKeys.length === 0) { + return; + } + + const roots = []; + for (const resolved of resolveChain) { + roots.push(createMixinRootFromCSSResolve(context, config, resolved)); + if (resolved.symbol[`-st-extends`]) { + break; + } + } + + if (roots.length === 1) { + mergeRules(roots[0], config.rule, mixDef.mixin.originDecl, config.transformer.diagnostics); + } else if (roots.length > 1) { + const mixinRoot = postcss.root(); + roots.forEach((root) => mixinRoot.prepend(...root.nodes)); + mergeRules(mixinRoot, config.rule, mixDef.mixin.originDecl, config.transformer.diagnostics); + } +} + +function createMixinRootFromCSSResolve( + context: FeatureTransformContext, + config: ApplyMixinContext, + resolvedClass: CSSResolve +) { + const stVarOverride = context.evaluator.stVarOverride || {}; + const meta = context.meta; + const mixDef = config.mixDef; + const isRootMixin = resolvedClass.symbol.name === resolvedClass.meta.root; + const mixinRoot = createSubsetAst( + resolvedClass.meta.ast, + (resolvedClass.symbol._kind === 'class' ? '.' : '') + resolvedClass.symbol.name, + undefined, + isRootMixin + ); + + const namedArgs = mixDef.mixin.options as Record; + + if (mixDef.mixin.partial) { + filterPartialMixinDecl(meta, mixinRoot, Object.keys(namedArgs)); + } + + const resolvedArgs = resolveArgumentsValue( + namedArgs, + config.transformer, + context.meta, + context.diagnostics, + mixDef.mixin.originDecl, + stVarOverride, + config.path, + config.cssPropertyMapping + ); + + const mixinMeta: StylableMeta = resolvedClass.meta; + const symbolName = isRootMixin && resolvedClass.meta !== meta ? 'default' : mixDef.mixin.type; + + config.transformer.transformAst( + mixinRoot, + mixinMeta, + undefined, + resolvedArgs, + config.path.concat(symbolName + ' from ' + meta.source), + true, + resolvedClass.symbol.name + ); + + fixRelativeUrls(mixinRoot, mixinMeta.source, meta.source); + + return mixinRoot; +} + +/** we assume that mixinRoot is freshly created nodes from the ast */ +function filterPartialMixinDecl( + meta: StylableMeta, + mixinRoot: postcss.Root, + overrideKeys: string[] +) { + let regexp: RegExp; + const overrideSet = new Set(overrideKeys); + let size; + do { + size = overrideSet.size; + regexp = new RegExp(`value\\((\\s*${Array.from(overrideSet).join('\\s*)|(\\s*')}\\s*)\\)`); + for (const { text, name } of Object.values(meta.getAllStVars())) { + if (!overrideSet.has(name) && text.match(regexp)) { + overrideSet.add(name); + } + } + } while (overrideSet.size !== size); + + mixinRoot.walkDecls((decl) => { + if (!decl.value.match(regexp)) { + const parent = decl.parent; + decl.remove(); + if (parent?.nodes?.length === 0) { + parent.remove(); + } + } + }); +} diff --git a/packages/core/src/features/st-var.ts b/packages/core/src/features/st-var.ts index 91cae8384..456bbf47a 100644 --- a/packages/core/src/features/st-var.ts +++ b/packages/core/src/features/st-var.ts @@ -283,7 +283,7 @@ function evaluateValueCall( parsedNode: ParsedValue, data: EvalValueData ): void { - const { tsVarOverride, passedThrough, value, node } = data; + const { stVarOverride, passedThrough, value, node } = data; const parsedArgs = strategies.args(parsedNode).map((x) => x.value); const varName = parsedArgs[0]; const restArgs = parsedArgs.slice(1); @@ -297,8 +297,8 @@ function evaluateValueCall( } } else if (parsedArgs.length >= 1) { // override with value - if (tsVarOverride?.[varName]) { - parsedNode.resolvedValue = tsVarOverride?.[varName]; + if (stVarOverride?.[varName]) { + parsedNode.resolvedValue = stVarOverride?.[varName]; return; } // check cyclic diff --git a/packages/core/src/features/types.ts b/packages/core/src/features/types.ts index 485d780fd..6ad8a68a5 100644 --- a/packages/core/src/features/types.ts +++ b/packages/core/src/features/types.ts @@ -1,7 +1,7 @@ import type { ImportSymbol } from './st-import'; import type { ClassSymbol } from './css-class'; import type { ElementSymbol } from './css-type'; -import type { MappedStates, MixinValue } from '../stylable-value-parsers'; +import type { MappedStates } from '../stylable-value-parsers'; import type { SelectorNode } from '@tokey/css-selector-parser'; // ToDo: distribute types to features @@ -12,8 +12,3 @@ export interface StylableDirectives { '-st-extends'?: ImportSymbol | ClassSymbol | ElementSymbol; '-st-global'?: SelectorNode[]; } - -export interface RefedMixin { - mixin: MixinValue; - ref: ImportSymbol | ClassSymbol; -} diff --git a/packages/core/src/functions.ts b/packages/core/src/functions.ts index fed554db3..34086cf2f 100644 --- a/packages/core/src/functions.ts +++ b/packages/core/src/functions.ts @@ -28,7 +28,7 @@ export interface EvalValueData { node?: postcss.Node; valueHook?: replaceValueHook; meta: StylableMeta; - tsVarOverride?: Record | null; + stVarOverride?: Record | null; cssVarsMapping?: Record; args?: string[]; rootArgument?: string; @@ -43,9 +43,9 @@ export interface EvalValueResult { } export class StylableEvaluator { - public tsVarOverride: Record | null | undefined; - constructor(options: { tsVarOverride?: Record | null } = {}) { - this.tsVarOverride = options.tsVarOverride; + public stVarOverride: Record | null | undefined; + constructor(options: { stVarOverride?: Record | null } = {}) { + this.stVarOverride = options.stVarOverride; } evaluateValue( context: FeatureTransformContext, @@ -57,7 +57,7 @@ export class StylableEvaluator { data.value, data.meta, data.node, - data.tsVarOverride || this.tsVarOverride, + data.stVarOverride || this.stVarOverride, data.valueHook, context.diagnostics, data.passedThrough, @@ -121,7 +121,7 @@ export function processDeclarationValue( rootArgument?: string, initialNode?: postcss.Node ): EvalValueResult { - const evaluator = new StylableEvaluator({ tsVarOverride: variableOverride }); + const evaluator = new StylableEvaluator({ stVarOverride: variableOverride }); const resolvedSymbols = getResolvedSymbols(meta); const parsedValue: any = postcssValueParser(value); parsedValue.walk((parsedNode: ParsedValue) => { @@ -142,7 +142,7 @@ export function processDeclarationValue( node, valueHook, meta, - tsVarOverride: variableOverride, + stVarOverride: variableOverride, cssVarsMapping, args, rootArgument, @@ -211,7 +211,7 @@ export function processDeclarationValue( node, valueHook, meta, - tsVarOverride: variableOverride, + stVarOverride: variableOverride, cssVarsMapping, args, rootArgument, diff --git a/packages/core/src/helpers/mixin.ts b/packages/core/src/helpers/mixin.ts new file mode 100644 index 000000000..7ea1a5e2f --- /dev/null +++ b/packages/core/src/helpers/mixin.ts @@ -0,0 +1,63 @@ +import type { Diagnostics } from '../diagnostics'; +import { strategies, valueDiagnostics } from './value'; +import type { MixinValue } from '../features'; +import type * as postcss from 'postcss'; +import postcssValueParser from 'postcss-value-parser'; + +export const diagnostics = { + INVALID_NAMED_PARAMS: valueDiagnostics.INVALID_NAMED_PARAMS, + VALUE_CANNOT_BE_STRING() { + return 'value can not be a string (remove quotes?)'; + }, +}; + +export function parseStMixin( + mixinNode: postcss.Declaration, + strategy: (type: string) => 'named' | 'args', + report?: Diagnostics, + emitStrategyDiagnostics = true +) { + const ast = postcssValueParser(mixinNode.value); + const mixins: Array = []; + + function reportWarning(message: string, options?: { word: string }) { + if (emitStrategyDiagnostics) { + report?.warn(mixinNode, message, options); + } + } + + ast.nodes.forEach((node) => { + if (node.type === 'function') { + mixins.push({ + type: node.value, + options: strategies[strategy(node.value)](node, reportWarning), + valueNode: node, + originDecl: mixinNode, + }); + } else if (node.type === 'word') { + mixins.push({ + type: node.value, + options: strategy(node.value) === 'named' ? {} : [], + valueNode: node, + originDecl: mixinNode, + }); + } else if (node.type === 'string') { + report?.error(mixinNode, diagnostics.VALUE_CANNOT_BE_STRING(), { + word: mixinNode.value, + }); + } + }); + + return mixins; +} +export function parseStPartialMixin( + mixinNode: postcss.Declaration, + strategy: (type: string) => 'named' | 'args', + report?: Diagnostics, + emitStrategyDiagnostics?: boolean +) { + return parseStMixin(mixinNode, strategy, report, emitStrategyDiagnostics).map((mixin) => { + mixin.partial = true; + return mixin; + }); +} diff --git a/packages/core/src/helpers/rule.ts b/packages/core/src/helpers/rule.ts index 2eaf400fc..c286b0b86 100644 --- a/packages/core/src/helpers/rule.ts +++ b/packages/core/src/helpers/rule.ts @@ -178,14 +178,14 @@ function containsMatchInFirstChunk( if (node.type === `combinator`) { return walkSelector.stopAll; } else if (node.type === 'pseudo_class') { - // TODO: support nested match :is(.mixin) - // if (node.nodes) { - // for (const innerSelectorNode of node.nodes) { - // if (containsMatchInFirstChunk(prefixType, innerSelectorNode)) { - // isMatch = true; - // } - // } - // } + // handle nested match :is(.mix) + if (node.nodes) { + for (const innerSelectorNode of node.nodes) { + if (containsMatchInFirstChunk(prefixType, innerSelectorNode)) { + isMatch = true; + } + } + } return walkSelector.skipNested; } else if (matchTypeAndValue(node, prefixType)) { isMatch = true; diff --git a/packages/core/src/helpers/selector.ts b/packages/core/src/helpers/selector.ts index 5ca9214be..17df3d4f1 100644 --- a/packages/core/src/helpers/selector.ts +++ b/packages/core/src/helpers/selector.ts @@ -196,20 +196,10 @@ export function scopeNestedSelector( return matchTypeAndValue(node, outputAst.nodes[i]); }) : false; - if (first && !parentRef && !startWithScoping && !globalSelector) { - outputAst.nodes.unshift(...cloneDeep(scopeAst.nodes as SelectorNode[]), { - type: `combinator`, - combinator: `space`, - value: ` `, - before: ``, - after: ``, - start: first.start, - end: first.start, - invalid: false, - }); - } + let nestedMixRoot = false; walkSelector(outputAst, (node, i, nodes) => { if (node.type === 'nesting') { + nestedMixRoot = true; nodes.splice(i, 1, { type: `selector`, nodes: cloneDeep(scopeAst.nodes as SelectorNode[]), @@ -220,6 +210,18 @@ export function scopeNestedSelector( }); } }); + if (first && !parentRef && !startWithScoping && !globalSelector && !nestedMixRoot) { + outputAst.nodes.unshift(...cloneDeep(scopeAst.nodes as SelectorNode[]), { + type: `combinator`, + combinator: `space`, + value: ` `, + before: ``, + after: ``, + start: first.start, + end: first.start, + invalid: false, + }); + } resultSelectors.push(outputAst); }); diff --git a/packages/core/src/helpers/value.ts b/packages/core/src/helpers/value.ts index 6d1a5c9b5..a70d226a1 100644 --- a/packages/core/src/helpers/value.ts +++ b/packages/core/src/helpers/value.ts @@ -71,7 +71,7 @@ export function getFormatterArgs( return argsResult; function checkEmptyArg() { - if (reportWarning && currentArg.trim() === '') { + if (reportWarning && argsResult.length && currentArg.trim() === '') { reportWarning( `${postcssValueParser.stringify( node as postcssValueParser.Node diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 183909660..4fcede82c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,6 +14,7 @@ export type { Imported, KeyframesSymbol, RefedMixin, + MixinValue, StylableDirectives, StylableSymbol, VarSymbol, @@ -79,7 +80,6 @@ export { ArgValue, ExtendsValue, MappedStates, - MixinValue, ReportWarning, SBTypesParsers, STYLABLE_NAMED_MATCHER, @@ -92,7 +92,6 @@ export { stValues, stValuesMap, valueMapping, - valueParserWarnings, } from './stylable-value-parsers'; export { createStylableFileProcessor } from './create-stylable-processor'; export { CreateProcessorOptions, Stylable, StylableConfig } from './stylable'; diff --git a/packages/core/src/stylable-meta.ts b/packages/core/src/stylable-meta.ts index 67b51c7eb..492f1c533 100644 --- a/packages/core/src/stylable-meta.ts +++ b/packages/core/src/stylable-meta.ts @@ -21,6 +21,7 @@ import { STImport, STGlobal, STVar, + STMixin, CSSClass, CSSType, CSSCustomProperty, @@ -34,6 +35,7 @@ const features = [ STImport, STGlobal, STVar, + STMixin, CSSClass, CSSType, CSSCustomProperty, @@ -67,7 +69,8 @@ export class StylableMeta { public transformDiagnostics: Diagnostics | null = null; public transformedScopes: Record | null = null; public scopes: postcss.AtRule[] = []; - public mixins: RefedMixin[]; + /** @deprecated */ + public mixins: RefedMixin[] = []; // Generated during transform public outputAst?: postcss.Root; public globals: Record = {}; @@ -80,9 +83,6 @@ export class StylableMeta { // set default root const rootSymbol = CSSClass.addClass(context, RESERVED_ROOT_NAME); rootSymbol[valueMapping.root] = true; - - setFieldForDeprecation(this, `mixins`, { objectType: `stylableMeta` }); - this.mixins = []; } getSymbol(name: string) { return STSymbol.get(this, name); @@ -149,3 +149,7 @@ setFieldForDeprecation(StylableMeta.prototype, `vars`, { valueOnThis: true, pleaseUse: `meta.getAllStVars() or meta.getStVar(name)`, }); +setFieldForDeprecation(StylableMeta.prototype, `mixins`, { + objectType: `stylableMeta`, + valueOnThis: true, +}); diff --git a/packages/core/src/stylable-mixins.ts b/packages/core/src/stylable-mixins.ts deleted file mode 100644 index 7e139e04a..000000000 --- a/packages/core/src/stylable-mixins.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { dirname } from 'path'; -import * as postcss from 'postcss'; -import type { Diagnostics } from './diagnostics'; -import { resolveArgumentsValue } from './functions'; -import { cssObjectToAst } from './parser'; -import { fixRelativeUrls } from './stylable-assets'; -import type { StylableMeta } from './stylable-meta'; -import { RefedMixin, ImportSymbol, STSymbol, ClassSymbol, ElementSymbol } from './features'; -import type { FeatureTransformContext } from './features/feature'; -import type { SRule } from './deprecated/postcss-ast-extension'; -import type { CSSResolve } from './stylable-resolver'; -import type { StylableTransformer } from './stylable-transformer'; -import { createSubsetAst } from './helpers/rule'; -import { strategies } from './helpers/value'; -import { isValidDeclaration, mergeRules } from './stylable-utils'; -import { valueMapping, mixinDeclRegExp } from './stylable-value-parsers'; -import { ignoreDeprecationWarn } from './helpers/deprecation'; - -export const mixinWarnings = { - FAILED_TO_APPLY_MIXIN(error: string) { - return `could not apply mixin: ${error}`; - }, - JS_MIXIN_NOT_A_FUNC() { - return `js mixin must be a function`; - }, - CIRCULAR_MIXIN(circularPaths: string[]) { - return `circular mixin found: ${circularPaths.join(' --> ')}`; - }, - UNKNOWN_MIXIN_SYMBOL(name: string) { - return `cannot mixin unknown symbol "${name}"`; - }, -}; - -export function appendMixins( - context: FeatureTransformContext, - transformer: StylableTransformer, - rule: SRule, - meta: StylableMeta, - variableOverride: Record, - cssVarsMapping: Record, - path: string[] = [] -) { - const mixins = ignoreDeprecationWarn(() => rule.mixins); - if (!mixins || mixins.length === 0) { - return; - } - mixins.forEach((mix) => { - appendMixin(context, mix, transformer, rule, meta, variableOverride, cssVarsMapping, path); - }); - mixins.length = 0; - rule.walkDecls(mixinDeclRegExp, (node) => { - node.remove(); - }); -} - -export function appendMixin( - context: FeatureTransformContext, - mix: RefedMixin, - transformer: StylableTransformer, - rule: SRule, - meta: StylableMeta, - variableOverride: Record, - cssVarsMapping: Record, - path: string[] = [] -) { - if (checkRecursive(context.diagnostics, meta, mix, rule, path)) { - return; - } - - const local = STSymbol.get(meta, mix.mixin.type); - if (local && (local._kind === 'class' || local._kind === 'element')) { - handleLocalClassMixin( - reParseMixinNamedArgs(mix, rule, context.diagnostics), - transformer, - meta, - variableOverride, - cssVarsMapping, - path, - rule - ); - } else { - const resolvedSymbols = context.getResolvedSymbols(meta); - const symbolName = mix.mixin.type; - const resolvedType = resolvedSymbols.mainNamespace[symbolName]; - if (resolvedType === `class`) { - const resolveChain = resolvedSymbols.class[symbolName]; - handleImportedCSSMixin( - resolveChain, - transformer, - reParseMixinNamedArgs(mix, rule, context.diagnostics), - rule, - meta, - path, - variableOverride, - cssVarsMapping - ); - return; - } else if (resolvedType === `js`) { - const resolvedMixin = resolvedSymbols.js[symbolName]; - if (typeof resolvedMixin.symbol === 'function') { - try { - handleJSMixin( - transformer, - reParseMixinArgs(mix, rule, context.diagnostics), - resolvedMixin.symbol, - meta, - rule, - variableOverride - ); - } catch (e) { - context.diagnostics.error( - rule, - mixinWarnings.FAILED_TO_APPLY_MIXIN(String(e)), - { word: mix.mixin.type } - ); - return; - } - } else { - context.diagnostics.error(rule, mixinWarnings.JS_MIXIN_NOT_A_FUNC(), { - word: mix.mixin.type, - }); - } - return; - } - - // ToDo: report on unsupported mixed in symbol type - const mixinDecl = getMixinDeclaration(rule); - if (mixinDecl) { - // ToDo: report on rule if decl is not found - context.diagnostics.error( - mixinDecl, - mixinWarnings.UNKNOWN_MIXIN_SYMBOL(mixinDecl.value), - { word: mixinDecl.value } - ); - } - } -} - -function checkRecursive( - diagnostics: Diagnostics, - meta: StylableMeta, - mix: RefedMixin, - rule: postcss.Rule, - path: string[] -) { - const symbolName = - mix.ref.name === meta.root - ? mix.ref._kind === 'class' - ? meta.root - : 'default' - : mix.mixin.type; - const isRecursive = path.includes(symbolName + ' from ' + meta.source); - if (isRecursive) { - // Todo: add test verifying word - diagnostics.warn(rule, mixinWarnings.CIRCULAR_MIXIN(path), { - word: symbolName, - }); - return true; - } - return false; -} - -function handleJSMixin( - transformer: StylableTransformer, - mix: RefedMixin, - mixinFunction: (...args: any[]) => any, - meta: StylableMeta, - rule: postcss.Rule, - variableOverride?: Record -) { - const res = mixinFunction((mix.mixin.options as any[]).map((v) => v.value)); - const mixinRoot = cssObjectToAst(res).root; - - mixinRoot.walkDecls((decl) => { - if (!isValidDeclaration(decl)) { - decl.value = String(decl); - } - }); - - transformer.transformAst(mixinRoot, meta, undefined, variableOverride, [], true); - const mixinPath = (mix.ref as ImportSymbol).import.request; - fixRelativeUrls( - mixinRoot, - transformer.resolver.resolvePath(dirname(meta.source), mixinPath), - meta.source - ); - - mergeRules(mixinRoot, rule); -} - -function createMixinRootFromCSSResolve( - transformer: StylableTransformer, - mix: RefedMixin, - meta: StylableMeta, - resolvedClass: CSSResolve, - path: string[], - decl: postcss.Declaration, - variableOverride: Record, - cssVarsMapping: Record -) { - const isRootMixin = resolvedClass.symbol.name === resolvedClass.meta.root; - const mixinRoot = createSubsetAst( - resolvedClass.meta.ast, - (resolvedClass.symbol._kind === 'class' ? '.' : '') + resolvedClass.symbol.name, - undefined, - isRootMixin - ); - - const namedArgs = mix.mixin.options as Record; - - if (mix.mixin.partial) { - filterPartialMixinDecl(meta, mixinRoot, Object.keys(namedArgs)); - } - - const resolvedArgs = resolveArgumentsValue( - namedArgs, - transformer, - meta, - transformer.diagnostics, - decl, - variableOverride, - path, - cssVarsMapping - ); - - const mixinMeta: StylableMeta = resolvedClass.meta; - const symbolName = isRootMixin ? 'default' : mix.mixin.type; - - transformer.transformAst( - mixinRoot, - mixinMeta, - undefined, - resolvedArgs, - path.concat(symbolName + ' from ' + meta.source), - true, - resolvedClass.symbol.name - ); - - fixRelativeUrls(mixinRoot, mixinMeta.source, meta.source); - - return mixinRoot; -} - -function handleImportedCSSMixin( - resolveChain: CSSResolve[], - transformer: StylableTransformer, - mix: RefedMixin, - rule: postcss.Rule, - meta: StylableMeta, - path: string[], - variableOverride: Record, - cssVarsMapping: Record -) { - const isPartial = mix.mixin.partial; - const namedArgs = mix.mixin.options as Record; - const overrideKeys = Object.keys(namedArgs); - - if (isPartial && overrideKeys.length === 0) { - return; - } - - const mixinDecl = getMixinDeclaration(rule) || postcss.decl(); - - const roots = []; - // start from 1 to ignore the local symbol - just mix the imported parts - for (let i = 1; i < resolveChain.length; ++i) { - const resolved = resolveChain[i]; - roots.push( - createMixinRootFromCSSResolve( - transformer, - mix, - meta, - resolved, - path, - mixinDecl, - variableOverride, - cssVarsMapping - ) - ); - if (resolved.symbol[valueMapping.extends]) { - break; - } - } - - if (roots.length === 1) { - mergeRules(roots[0], rule); - } else if (roots.length > 1) { - const mixinRoot = postcss.root(); - roots.forEach((root) => mixinRoot.prepend(...root.nodes)); - mergeRules(mixinRoot, rule); - } -} - -function handleLocalClassMixin( - mix: RefedMixin, - transformer: StylableTransformer, - meta: StylableMeta, - variableOverride: ({ [key: string]: string } & object) | undefined, - cssVarsMapping: Record, - path: string[], - rule: SRule -) { - const isPartial = mix.mixin.partial; - const namedArgs = mix.mixin.options as Record; - const overrideKeys = Object.keys(namedArgs); - - if (isPartial && overrideKeys.length === 0) { - return; - } - const isRootMixin = mix.ref.name === meta.root; - const mixinDecl = getMixinDeclaration(rule) || postcss.decl(); - const resolvedArgs = resolveArgumentsValue( - namedArgs, - transformer, - meta, - transformer.diagnostics, - mixinDecl, - variableOverride, - path, - cssVarsMapping - ); - - const mixinRoot = createSubsetAst( - meta.ast, - '.' + mix.ref.name, - undefined, - isRootMixin - ); - - if (isPartial) { - filterPartialMixinDecl(meta, mixinRoot, overrideKeys); - } - - transformer.transformAst( - mixinRoot, - meta, - undefined, - resolvedArgs, - path.concat(mix.mixin.type + ' from ' + meta.source), - true, - mix.ref.name - ); - mergeRules(mixinRoot, rule); -} - -function getMixinDeclaration(rule: postcss.Rule): postcss.Declaration | undefined { - return ( - rule.nodes && - (rule.nodes.find((node) => { - return ( - node.type === 'decl' && - (node.prop === valueMapping.mixin || node.prop === valueMapping.partialMixin) - ); - }) as postcss.Declaration) - ); -} -const partialsOnly = ({ mixin: { partial } }: RefedMixin): boolean => { - return !!partial; -}; -const nonPartials = ({ mixin: { partial } }: RefedMixin): boolean => { - return !partial; -}; - -/** we assume that mixinRoot is freshly created nodes from the ast */ -function filterPartialMixinDecl( - meta: StylableMeta, - mixinRoot: postcss.Root, - overrideKeys: string[] -) { - let regexp: RegExp; - const overrideSet = new Set(overrideKeys); - let size; - do { - size = overrideSet.size; - regexp = new RegExp(`value\\((\\s*${Array.from(overrideSet).join('\\s*)|(\\s*')}\\s*)\\)`); - for (const { text, name } of Object.values(meta.getAllStVars())) { - if (!overrideSet.has(name) && text.match(regexp)) { - overrideSet.add(name); - } - } - } while (overrideSet.size !== size); - - mixinRoot.walkDecls((decl) => { - if (!decl.value.match(regexp)) { - const parent = decl.parent as SRule; // ref the parent before remove - decl.remove(); - if (parent?.nodes?.length === 0) { - parent.remove(); - } else if (parent) { - if (decl.prop === valueMapping.mixin) { - parent.mixins = parent.mixins!.filter(partialsOnly); - } else if (decl.prop === valueMapping.partialMixin) { - parent.mixins = parent.mixins!.filter(nonPartials); - } - } - } - }); -} - -/** this is a workaround for parsing the mixin args too early */ -function reParseMixinNamedArgs( - mix: RefedMixin, - rule: postcss.Rule, - diagnostics: Diagnostics -): RefedMixin { - const options = - mix.mixin.valueNode?.type === 'function' - ? strategies.named(mix.mixin.valueNode, (message, options) => { - diagnostics.warn(mix.mixin.originDecl || rule, message, options); - }) - : (mix.mixin.options as Record) || {}; - - return { - ...mix, - mixin: { - ...mix.mixin, - options, - }, - }; -} - -function reParseMixinArgs( - mix: RefedMixin, - rule: postcss.Rule, - diagnostics: Diagnostics -): RefedMixin { - const options = - mix.mixin.valueNode?.type === 'function' - ? strategies.args(mix.mixin.valueNode, (message, options) => { - diagnostics.warn(mix.mixin.originDecl || rule, message, options); - }) - : Array.isArray(mix.mixin.options) - ? (mix.mixin.options as { value: string }[]) - : []; - - return { - ...mix, - mixin: { - ...mix.mixin, - options, - }, - }; -} diff --git a/packages/core/src/stylable-processor.ts b/packages/core/src/stylable-processor.ts index 2e14a7f65..176ebaaa2 100644 --- a/packages/core/src/stylable-processor.ts +++ b/packages/core/src/stylable-processor.ts @@ -9,9 +9,9 @@ import { ClassSymbol, CSSCustomProperty, ElementSymbol, - RefedMixin, StylableDirectives, STVar, + STMixin, } from './features'; import { generalDiagnostics } from './features/diagnostics'; import { @@ -43,7 +43,7 @@ import { valueMapping, } from './stylable-value-parsers'; import { stripQuotation, filename2varname } from './helpers/string'; -import { ignoreDeprecationWarn, warnOnce } from './helpers/deprecation'; +import { warnOnce } from './helpers/deprecation'; const parseStates = SBTypesParsers[valueMapping.states]; const parseGlobal = SBTypesParsers[valueMapping.global]; @@ -65,18 +65,9 @@ export const processorWarnings = { CANNOT_EXTEND_IN_COMPLEX() { return `cannot define "${valueMapping.extends}" inside a complex selector`; }, - UNKNOWN_MIXIN(name: string) { - return `unknown mixin: "${name}"`; - }, - OVERRIDE_MIXIN(mixinType: string) { - return `override ${mixinType} on same rule`; - }, OVERRIDE_TYPED_RULE(key: string, name: string) { return `override "${key}" on typed rule "${name}"`; }, - PARTIAL_MIXIN_MISSING_ARGUMENTS(type: string) { - return `"${valueMapping.partialMixin}" can only be used with override arguments provided, missing overrides on "${type}"`; - }, INVALID_NAMESPACE_DEF() { return 'invalid @namespace'; }, @@ -239,7 +230,7 @@ export class StylableProcessor implements FeatureContext { let pathToSource: string | undefined; let length = this.meta.ast.nodes.length; - while(length--) { + while (length--) { const node = this.meta.ast.nodes[length]; if (node.type === 'comment' && node.text.includes('st-namespace-reference')) { const i = node.text.indexOf('='); @@ -437,73 +428,8 @@ export class StylableProcessor implements FeatureContext { } else { this.diagnostics.warn(decl, processorWarnings.CANNOT_EXTEND_IN_COMPLEX()); } - } else if (decl.prop === valueMapping.mixin || decl.prop === valueMapping.partialMixin) { - const mixins: RefedMixin[] = []; - /** - * This functionality is broken we don't know what strategy to choose here. - * Should be fixed when we refactor to the new flow - */ - SBTypesParsers[decl.prop]( - decl, - (type) => { - const symbol = STSymbol.get(this.meta, type); - return symbol?._kind === 'import' && !symbol.import.from.match(/.css$/) - ? 'args' - : 'named'; - }, - this.diagnostics, - false - ).forEach((mixin) => { - const mixinRefSymbol = STSymbol.get(this.meta, mixin.type); - if ( - mixinRefSymbol && - (mixinRefSymbol._kind === 'import' || mixinRefSymbol._kind === 'class') - ) { - if (mixin.partial && Object.keys(mixin.options).length === 0) { - this.diagnostics.warn( - decl, - processorWarnings.PARTIAL_MIXIN_MISSING_ARGUMENTS(mixin.type), - { - word: mixin.type, - } - ); - } - const refedMixin = { - mixin, - ref: mixinRefSymbol, - }; - mixins.push(refedMixin); - ignoreDeprecationWarn(() => this.meta.mixins).push(refedMixin); - } else { - this.diagnostics.warn(decl, processorWarnings.UNKNOWN_MIXIN(mixin.type), { - word: mixin.type, - }); - } - }); - - const previousMixins = ignoreDeprecationWarn(() => rule.mixins); - if (previousMixins) { - const partials = previousMixins.filter((r) => r.mixin.partial); - const nonPartials = previousMixins.filter((r) => !r.mixin.partial); - const isInPartial = decl.prop === valueMapping.partialMixin; - if ( - (partials.length && decl.prop === valueMapping.partialMixin) || - (nonPartials.length && decl.prop === valueMapping.mixin) - ) { - this.diagnostics.warn(decl, processorWarnings.OVERRIDE_MIXIN(decl.prop)); - } - if (partials.length && nonPartials.length) { - rule.mixins = isInPartial - ? nonPartials.concat(mixins) - : partials.concat(mixins); - } else if (partials.length) { - rule.mixins = isInPartial ? mixins : partials.concat(mixins); - } else if (nonPartials.length) { - rule.mixins = isInPartial ? nonPartials.concat(mixins) : mixins; - } - } else if (mixins.length) { - rule.mixins = mixins; - } + } else if (decl.prop === STMixin.MixinType.ALL || decl.prop === STMixin.MixinType.PARTIAL) { + STMixin.hooks.analyzeDeclaration({ context: this, decl }); } else if (decl.prop === valueMapping.global) { if (isSimple && type !== 'type') { this.setClassGlobalMapping(decl, rule); diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index 32f974a65..e6ea7750e 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -19,8 +19,7 @@ import { import { createWarningRule, isChildOfAtRule, getRuleScopeSelector } from './helpers/rule'; import { namespace } from './helpers/namespace'; import { getOriginDefinition } from './helpers/resolve'; -import { appendMixins } from './stylable-mixins'; -import type { ClassSymbol, ElementSymbol } from './features'; +import { ClassSymbol, ElementSymbol, STMixin } from './features'; import type { StylableMeta } from './stylable-meta'; import { STSymbol, @@ -32,7 +31,7 @@ import { CSSKeyframes, CSSCustomProperty, } from './features'; -import type { SRule, SDecl } from './deprecated/postcss-ast-extension'; +import type { SDecl } from './deprecated/postcss-ast-extension'; import { CSSResolve, StylableResolverCache, @@ -157,7 +156,6 @@ export class StylableTransformer { STGlobal.hooks.transformInit({ context }); meta.transformedScopes = validateScopes(this, meta); this.transformAst(meta.outputAst, meta, metaExports); - STGlobal.hooks.transformLastPass({ context }); meta.transformDiagnostics = this.diagnostics; const result = { meta, exports: metaExports }; @@ -167,12 +165,13 @@ export class StylableTransformer { ast: postcss.Root, meta: StylableMeta, metaExports?: StylableExports, - tsVarOverride?: Record, + stVarOverride?: Record, path: string[] = [], mixinTransform = false, topNestClassName = `` ) { - this.evaluator.tsVarOverride = tsVarOverride; + const prevStVarOverride = this.evaluator.stVarOverride; + this.evaluator.stVarOverride = stVarOverride; const transformContext = { meta, diagnostics: this.diagnostics, @@ -258,17 +257,18 @@ export class StylableTransformer { if (!mixinTransform && meta.outputAst && this.mode === 'development') { this.addDevRules(meta); } - ast.walkRules((rule) => - appendMixins( - transformContext, - this, - rule as SRule, - meta, - tsVarOverride || {}, - cssVarsMapping, - path - ) - ); + + const lastPassParams = { + context: transformContext, + ast, + transformer: this, + cssVarsMapping, + path, + }; + STMixin.hooks.transformLastPass(lastPassParams); + if (!mixinTransform) { + STGlobal.hooks.transformLastPass(lastPassParams); + } if (metaExports) { CSSClass.hooks.transformJSExports({ @@ -288,6 +288,9 @@ export class StylableTransformer { resolved: cssVarsMapping, }); } + + // restore evaluator state + this.evaluator.stVarOverride = prevStVarOverride; } /** @deprecated */ public getScopedCSSVar( @@ -544,14 +547,13 @@ export class StylableTransformer { * the general `st-symbol` feature because the actual symbol can * be a type-element symbol that is actually an imported root in a mixin */ - const origin = STSymbol.get( - originMeta, - topNestClassName || originMeta.root - ) as ClassSymbol; + const origin = STSymbol.get(originMeta, topNestClassName || originMeta.root) as + | ClassSymbol + | ElementSymbol; // ToDo: handle other cases context.setCurrentAnchor({ name: origin.name, - type: 'class', - resolved: resolvedSymbols.class[origin.name], + type: origin._kind, + resolved: resolvedSymbols[origin._kind][origin.name], }); } } diff --git a/packages/core/src/stylable-utils.ts b/packages/core/src/stylable-utils.ts index 7966c4f6c..1968ff05f 100644 --- a/packages/core/src/stylable-utils.ts +++ b/packages/core/src/stylable-utils.ts @@ -5,7 +5,7 @@ import type { Diagnostics } from './diagnostics'; import type { Imported, ImportSymbol, StylableSymbol } from './features'; import { isChildOfAtRule } from './helpers/rule'; import { scopeNestedSelector, parseSelectorWithCache } from './helpers/selector'; -import { valueMapping, mixinDeclRegExp } from './stylable-value-parsers'; +import { valueMapping } from './stylable-value-parsers'; export const CUSTOM_SELECTOR_RE = /:--[\w-]+/g; @@ -42,8 +42,18 @@ export function transformMatchesOnRule(rule: postcss.Rule, lineBreak: boolean) { return replaceRuleSelector(rule, { lineBreak }); } -export function mergeRules(mixinAst: postcss.Root, rule: postcss.Rule) { +export const INVALID_MERGE_OF = (mergeValue: string) => { + return `invalid merge of: \n"${mergeValue}"`; +}; +// ToDo: move to helpers/mixin +export function mergeRules( + mixinAst: postcss.Root, + rule: postcss.Rule, + mixinDecl: postcss.Declaration, + report?: Diagnostics +) { let mixinRoot: postcss.Rule | null | 'NoRoot' = null; + const nestedInKeyframes = isChildOfAtRule(rule, `keyframes`); mixinAst.walkRules((mixinRule: postcss.Rule) => { if (isChildOfAtRule(mixinRule, 'keyframes')) { return; @@ -70,29 +80,26 @@ export function mergeRules(mixinAst: postcss.Root, rule: postcss.Rule) { if (mixinAst.nodes) { let nextRule: postcss.Rule | postcss.AtRule = rule; - let mixinEntry: postcss.Declaration | null = null; - - rule.walkDecls(mixinDeclRegExp, (decl) => { - mixinEntry = decl; - }); - if (!mixinEntry) { - throw rule.error('missing mixin entry'); - } // TODO: handle rules before and after decl on entry mixinAst.nodes.slice().forEach((node) => { if (node === mixinRoot) { node.walkDecls((node) => { - rule.insertBefore(mixinEntry!, node); + rule.insertBefore(mixinDecl, node); }); } else if (node.type === 'decl') { - rule.insertBefore(mixinEntry!, node); + rule.insertBefore(mixinDecl, node); } else if (node.type === 'rule' || node.type === 'atrule') { - if (rule.parent!.last === nextRule) { - rule.parent!.append(node); + const valid = !nestedInKeyframes; + if (valid) { + if (rule.parent!.last === nextRule) { + rule.parent!.append(node); + } else { + rule.parent!.insertAfter(nextRule, node); + } + nextRule = node; } else { - rule.parent!.insertAfter(nextRule, node); + report?.warn(rule, INVALID_MERGE_OF(node.toString())); } - nextRule = node; } }); } diff --git a/packages/core/src/stylable-value-parsers.ts b/packages/core/src/stylable-value-parsers.ts index 1f0ade322..4419362d1 100644 --- a/packages/core/src/stylable-value-parsers.ts +++ b/packages/core/src/stylable-value-parsers.ts @@ -1,19 +1,14 @@ import type * as postcss from 'postcss'; -import postcssValueParser, { FunctionNode, WordNode } from 'postcss-value-parser'; +import postcssValueParser from 'postcss-value-parser'; import type { Diagnostics } from './diagnostics'; import { processPseudoStates } from './pseudo-states'; import { parseSelectorWithCache } from './helpers/selector'; -import { getNamedArgs, strategies } from './helpers/value'; +import { parseStMixin, parseStPartialMixin } from './helpers/mixin'; +import { getNamedArgs } from './helpers/value'; import type { StateParsedValue } from './types'; import type { SelectorNodes } from '@tokey/css-selector-parser'; import { CSSClass } from './features'; -export const valueParserWarnings = { - VALUE_CANNOT_BE_STRING() { - return 'value can not be a string (remove quotes?)'; - }, -}; - export interface MappedStates { [s: string]: StateParsedValue | string | null; } @@ -25,14 +20,6 @@ export interface TypedClass { '-st-extends'?: string; } -export interface MixinValue { - type: string; - options: Array<{ value: string }> | Record; - partial?: boolean; - valueNode?: FunctionNode | WordNode; - originDecl?: postcss.Declaration; -} - export interface ArgValue { type: string; value: string; @@ -58,8 +45,8 @@ export const valueMapping = { root: '-st-root' as const, states: '-st-states' as const, extends: '-st-extends' as const, - mixin: '-st-mixin' as const, - partialMixin: '-st-partial-mixin' as const, + mixin: '-st-mixin' as const, // ToDo: change to STMixin.MixinType.ALL, + partialMixin: '-st-partial-mixin' as const, // ToDo: change to STMixin.MixinType.PARTIAL, global: '-st-global' as const, }; @@ -133,53 +120,6 @@ export const SBTypesParsers = { types, }; }, - '-st-mixin'( - mixinNode: postcss.Declaration, - strategy: (type: string) => 'named' | 'args', - diagnostics?: Diagnostics, - emitStrategyDiagnostics = true - ) { - const ast = postcssValueParser(mixinNode.value); - const mixins: Array = []; - - function reportWarning(message: string, options?: { word: string }) { - if (emitStrategyDiagnostics) { - diagnostics?.warn(mixinNode, message, options); - } - } - - ast.nodes.forEach((node) => { - if (node.type === 'function') { - mixins.push({ - type: node.value, - options: strategies[strategy(node.value)](node, reportWarning), - valueNode: node, - originDecl: mixinNode, - }); - } else if (node.type === 'word') { - mixins.push({ - type: node.value, - options: strategy(node.value) === 'named' ? {} : [], - valueNode: node, - originDecl: mixinNode, - }); - } else if (node.type === 'string') { - diagnostics?.error(mixinNode, valueParserWarnings.VALUE_CANNOT_BE_STRING(), { - word: mixinNode.value, - }); - } - }); - - return mixins; - }, - '-st-partial-mixin'( - mixinNode: postcss.Declaration, - strategy: (type: string) => 'named' | 'args', - diagnostics?: Diagnostics - ) { - return SBTypesParsers['-st-mixin'](mixinNode, strategy, diagnostics).map((mixin) => { - mixin.partial = true; - return mixin; - }); - }, + '-st-mixin': parseStMixin, + '-st-partial-mixin': parseStPartialMixin, }; diff --git a/packages/core/test/arguement-parser.spec.ts b/packages/core/test/arguement-parser.spec.ts index 58d35ae7b..3f35883cd 100644 --- a/packages/core/test/arguement-parser.spec.ts +++ b/packages/core/test/arguement-parser.spec.ts @@ -53,6 +53,9 @@ describe('Value argument parsing (split by comma)', () => { test('should process a function with string argument and extra value', 'func("A" 10px)', [ 'A 10px', ]); + test('should process a function without any parameters', 'func()', [], true, [ + /*no errors*/ + ]); }); describe('invalid inputs', () => { @@ -64,7 +67,6 @@ describe('Value argument parsing (split by comma)', () => { 'func(/*with comment*/ /*with comment*/, b, c)', ['', 'b', 'c'] ); - test('should process a function without any parameters', 'func()', []); test('should process a function with a empty argument', 'func(a,)', ['a']); test('should process a function with too many commas', 'func(a,,)', ['a']); test('should process a function with too many commas', 'func(a,,b)', ['a', '', 'b']); diff --git a/packages/core/test/diagnostics.spec.ts b/packages/core/test/diagnostics.spec.ts index adbadf3c1..f09c31c55 100644 --- a/packages/core/test/diagnostics.spec.ts +++ b/packages/core/test/diagnostics.spec.ts @@ -9,11 +9,8 @@ import { processorWarnings, transformerWarnings, nativePseudoElements, - valueParserWarnings, } from '@stylable/core'; -import { mixinWarnings } from '@stylable/core/dist/stylable-mixins'; -import { valueDiagnostics } from '@stylable/core/dist/helpers/value'; -import { STImport, CSSClass, CSSType, STVar } from '@stylable/core/dist/features'; +import { CSSClass, CSSType } from '@stylable/core/dist/features'; import { generalDiagnostics } from '@stylable/core/dist/features/diagnostics'; describe('findTestLocations', () => { @@ -341,246 +338,6 @@ describe('diagnostics: warnings and errors', () => { }); }); - describe('-st-mixin', () => { - it('should return warning for unknown mixin', () => { - expectAnalyzeDiagnostics( - ` - .gaga{ - |-st-mixin: $myMixin$|; - } - `, - [{ message: processorWarnings.UNKNOWN_MIXIN('myMixin'), file: 'main.css' }] - ); - }); - - it('should return a warning for a CSS mixin using un-named params', () => { - expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - .mixed { - color: red; - } - .gaga{ - |-st-mixin: mixed($1$)|; - } - - `, - }, - }, - }, - [ - { - message: valueDiagnostics.INVALID_NAMED_PARAMS(), - file: '/style.st.css', - }, - ] - ); - }); - - it('should add error when attempting to mix in an unknown mixin symbol', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: my-mixin; - } - .container { - |-st-mixin: $my-mixin$|; - } - `, - }, - '/imported.st.css': { - content: ``, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { - message: STImport.diagnostics.UNKNOWN_IMPORTED_SYMBOL( - 'my-mixin', - './imported.st.css' - ), - file: '/main.css', - skip: true, - skipLocationCheck: true, - }, - { message: mixinWarnings.UNKNOWN_MIXIN_SYMBOL('my-mixin'), file: '/main.css' }, - ]); - }); - - it('should add error on circular mixins', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - .x { - -st-mixin: y; - } - .y { - -st-mixin: x; - } - `, - }, - }, - }; - const mainPath = '/main.css'; - const xPath = [`y from ${mainPath}`, `x from ${mainPath}`]; - const yPath = [`x from ${mainPath}`, `y from ${mainPath}`]; - expectTransformDiagnostics(config, [ - { - message: mixinWarnings.CIRCULAR_MIXIN(xPath), - file: '/main.css', - skipLocationCheck: true, - }, - { - message: mixinWarnings.CIRCULAR_MIXIN(yPath), - file: '/main.css', - skipLocationCheck: true, - }, - ]); - }); - - it('should add diagnostics when there is a bug in mixin', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - :import { - -st-from: "./imported.js"; - -st-default: myMixin; - } - |.container { - -st-mixin: $myMixin$; - }| - `, - }, - '/imported.js': { - content: ` - module.exports = function(){ - throw 'bug in mixin' - } - `, - }, - }, - }; - expectTransformDiagnostics(config, [ - { - message: mixinWarnings.FAILED_TO_APPLY_MIXIN('bug in mixin'), - file: '/main.css', - }, - ]); - }); - - it('js mixin must be a function', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - :import { - -st-from: "./imported.js"; - -st-named: myMixin; - } - |.container { - -st-mixin: $myMixin$; - }| - `, - }, - '/imported.js': { - content: ` - module.exports = { - myMixin: "not a function", - } - `, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { message: mixinWarnings.JS_MIXIN_NOT_A_FUNC(), file: '/main.css' }, - ]); - }); - - it('should not add warning when mixin value is a string', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - :import { - -st-from: "./imported.js"; - -st-default: myMixin; - } - .container { - |-st-mixin: $"myMixin"$|; - } - `, - }, - '/imported.js': { - content: ``, - }, - }, - }; - expectTransformDiagnostics(config, [ - { message: valueParserWarnings.VALUE_CANNOT_BE_STRING(), file: '/main.css' }, - ]); - }); - - it('should warn about non-existing variables in mixin overrides', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - .mixed {} - .container { - |-st-mixin: mixed(arg value($missingVar$))|; - } - `, - }, - }, - }; - expectTransformDiagnostics(config, [ - { message: STVar.diagnostics.UNKNOWN_VAR('missingVar'), file: '/main.css' }, - ]); - }); - - it('should warn about non-existing variables in a multi-argument mixin override', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - :vars { - color1: red; - color2: green; - } - .mixed { - color: value(color1); - background: value(color2); - } - .container { - |-st-mixin: mixed(color1 blue, color2 value($missingVar$))|; - } - `, - }, - }, - }; - expectTransformDiagnostics(config, [ - { message: STVar.diagnostics.UNKNOWN_VAR('missingVar'), file: '/main.css' }, - ]); - }); - }); - describe('-st-extends', () => { it('should return warning when defined under complex selector', () => { expectAnalyzeDiagnostics( @@ -633,28 +390,6 @@ describe('diagnostics: warnings and errors', () => { }); }); - describe('redeclare symbols', () => { - it('should warn override mixin on same rule', () => { - const config = { - entry: '/main.css', - files: { - '/main.css': { - content: ` - .a {} - .b { - -st-mixin: a; - |-st-mixin: a|; - } - `, - }, - }, - }; - expectTransformDiagnostics(config, [ - { message: processorWarnings.OVERRIDE_MIXIN('-st-mixin'), file: '/main.css' }, - ]); - }); - }); - describe('selectors', () => { // TODO2: next phase xit('should not allow conflicting extends', () => { diff --git a/packages/core/test/features/css-class.spec.ts b/packages/core/test/features/css-class.spec.ts index 6782e0183..25dca3c5d 100644 --- a/packages/core/test/features/css-class.spec.ts +++ b/packages/core/test/features/css-class.spec.ts @@ -774,4 +774,254 @@ describe(`features/css-class`, () => { }); }); }); + describe(`css-pseudo-class`, () => { + // ToDo: move to css-pseudo-class spec once feature is created + describe(`st-mixin`, () => { + it(`should mix custom state`, () => { + const { sheets } = testStylableCore({ + '/base.st.css': ` + .root { + -st-states: toggled; + } + .root:toggled { + value: from base; + } + `, + '/extend.st.css': ` + @st-import Base from './base.st.css'; + Base {} + .root { + -st-extends: Base; + } + .root:toggled { + value: from extend; + } + `, + '/entry.st.css': ` + @st-import Extend, [Base] from './extend.st.css'; + + /* @rule[1] + .entry__a.base--toggled { + value: from base; + } */ + .a { + -st-mixin: Base; + } + + /* + ToDo: change to 1 once empty AST is filtered + @rule[2] + .entry__a.base--toggled { + value: from extend; + } */ + .a { + -st-mixin: Extend; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix imported class with custom-pseudo-state`, () => { + // ToDo: fix case where extend.st.css has .root between mix rules: https://shorturl.at/cwBMP + const { sheets } = testStylableCore({ + '/base.st.css': ` + .root { + /* not going to be mixed through -st-extends */ + id: base-root; + -st-states: state; + } + `, + '/extend.st.css': ` + @st-import Base from './base.st.css'; + .root { + -st-extends: Base; + } + .mix { + -st-extends: Base; + id: extend-mix; + } + .mix:state { + id: extend-mix-state; + }; + .root:state { + id: extend-root-state; + } + + `, + '/enrich.st.css': ` + @st-import MixRoot, [mix as mixClass] from './extend.st.css'; + MixRoot { + id: enrich-MixRoot; + } + MixRoot:state { + id: enrich-MixRoot-state; + } + .mixClass { + id: enrich-mixClass; + } + .mixClass:state { + id: enrich-mixClass-state; + } + `, + '/entry.st.css': ` + @st-import [MixRoot, mixClass] from './enrich.st.css'; + + /* + @rule[0] .entry__a { -st-extends: Base; id: extend-mix; } + @rule[1] .entry__a.base--state { id: extend-mix-state; } + @rule[2] .entry__a { id: enrich-mixClass; } + @rule[3] .entry__a.base--state { id: enrich-mixClass-state; } + */ + .a { + -st-mixin: mixClass; + } + + /* + @rule[0] .entry__a { -st-extends: Base; } + @rule[1] .entry__a .extend__mix { -st-extends: Base; id: extend-mix; } + @rule[2] .entry__a .extend__mix.base--state { id: extend-mix-state; } + @rule[3] .entry__a.base--state { id: extend-root-state; } + @rule[4] .entry__a { id: enrich-MixRoot; } + @rule[5] .entry__a.base--state { id: enrich-MixRoot-state; } + */ + .a { + -st-mixin: MixRoot; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + }); + describe(`css-pseudo-element`, () => { + // ToDo: move to css-pseudo-element spec once feature is created + describe(`st-mixin`, () => { + it(`should mix local class with pseudo-element`, () => { + const { sheets } = testStylableCore({ + '/base.st.css': ` + .part { prop: a; } + `, + '/extend.st.css': ` + @st-import Base from './base.st.css'; + Base { prop: b; } + .part {prop: c; /* override part */} + `, + '/entry.st.css': ` + @st-import Extend, [Base] from './extend.st.css'; + + .mix-base { + -st-extends: Base; + prop: d; + } + .mix-base::part { prop: e; } + + /* + @rule(base)[1] .entry__a .base__part { prop: e; } + */ + .a { + -st-mixin: mix-base; + } + + .mix-extend { + -st-extends: Extend; + prop: f; + } + .mix-extend::part { prop: g; } + + /* + @rule(extend)[1] .entry__a .extend__part { prop: g; } + */ + .a { + -st-mixin: mix-extend; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix imported class with pseudo-element`, () => { + // ToDo: fix case where extend.st.css has .root between mix rules: https://shorturl.at/cwBMP + const { sheets } = testStylableCore({ + '/base.st.css': ` + .part { + /* not going to be mixed through -st-extends */ + id: base-part; + } + `, + '/extend.st.css': ` + @st-import Base from './base.st.css'; + .root { + -st-extends: Base; + } + .part { id: extend-part; } + .mix { + -st-extends: Base; + id: extend-mix; + } + .mix::part .part { + id: extend-mix-part; + }; + .root::part .part{ + id: extend-root-part; + } + + `, + '/enrich.st.css': ` + @st-import MixRoot, [mix as mixClass] from './extend.st.css'; + .part { id: enrich-part; } + MixRoot { + id: enrich-MixRoot; + } + MixRoot::part .part { + id: enrich-MixRoot-part; + } + .mixClass { + id: enrich-mixClass; + } + .mixClass::part .part { + id: enrich-mixClass-part + } + `, + '/entry.st.css': ` + @st-import [MixRoot, mixClass] from './enrich.st.css'; + + /* + @rule[0] .entry__a { -st-extends: Base; id: extend-mix; } + @rule[1] .entry__a .base__part .extend__part { id: extend-mix-part; } + @rule[2] .entry__a { id: enrich-mixClass; } + @rule[3] .entry__a .base__part .enrich__part { id: enrich-mixClass-part; } + */ + .a { + -st-mixin: mixClass; + } + + /* + @rule[0] .entry__a { -st-extends: Base; } + @rule[1] .entry__a .extend__part { id: extend-part; } + @rule[2] .entry__a .extend__mix { -st-extends: Base; id: extend-mix; } + @rule[3] .entry__a .extend__mix .base__part .extend__part { id: extend-mix-part; } + @rule[4] .entry__a .base__part .extend__part { id: extend-root-part; } + @rule[5] .entry__a { id: enrich-MixRoot; } + @rule[6] .entry__a .extend__part .enrich__part { id: enrich-MixRoot-part; } + */ + .a { + -st-mixin: MixRoot; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + }); }); diff --git a/packages/core/test/features/css-keyframes.spec.ts b/packages/core/test/features/css-keyframes.spec.ts index b3311b444..fb5cd14af 100644 --- a/packages/core/test/features/css-keyframes.spec.ts +++ b/packages/core/test/features/css-keyframes.spec.ts @@ -1,4 +1,4 @@ -import { STSymbol, CSSKeyframes } from '@stylable/core/dist/features'; +import { STSymbol, CSSKeyframes, STMixin } from '@stylable/core/dist/features'; import { ignoreDeprecationWarn } from '@stylable/core/dist/helpers/deprecation'; import { testStylableCore, shouldReportNoDiagnostics } from '@stylable/core-test-kit'; import chai, { expect } from 'chai'; @@ -475,4 +475,142 @@ describe(`features/css-keyframes`, () => { shouldReportNoDiagnostics(meta); }); }); + describe(`st-mixin`, () => { + it(`should mix local class referring to local @keyframes`, () => { + const { sheets } = testStylableCore(` + .mix { + animation: jump; + } + @keyframes jump {} + + /* + @rule .entry__a { animation: entry__jump; } + */ + .a { + -st-mixin: mix; + } + `); + + expect( + sheets[`/entry.st.css`].meta.outputAst?.toString().match(/@keyframes/g)!.length, + `only original @keyframes` + ).to.eql(1); + }); + it(`should mix class with mixin context @keyframes`, () => { + const { sheets } = testStylableCore({ + '/imported.st.css': ` + .mix { + animation: jump; + } + @keyframes jump {} + `, + '/entry.st.css': ` + @st-import [mix] from './imported.st.css'; + + /* + @rule .entry__a { animation: imported__jump; } + */ + .a { + -st-mixin: mix; + } + `, + }); + + expect( + sheets[`/entry.st.css`].meta.outputAst?.toString(), + `@keyframes referenced & not copied` + ).to.not.include(`@keyframes`); + }); + it(`should mix root with with copy of @keyframes`, () => { + // ToDo(bug): fix copied @keyframes to be namespaced by entry or use reference + // should be `animation: entry__jump` and `@keyframes entry__jump` or reference to imported__jump + const { sheets } = testStylableCore({ + '/imported.st.css': ` + .root { + animation: jump; + } + @keyframes jump {} + `, + '/entry.st.css': ` + @st-import Mix from './imported.st.css'; + + /* + @rule(reference animation name) .entry__a { animation: imported__jump; } + @rule[1](mixed-in @keyframes) imported__jump + */ + .a { + -st-mixin: Mix; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should handle @keyframes in Javascript mixin`, () => { + // ToDo: fix symbols inside mixin - animation-name + // should match between @keyframes and declarations. + testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + "animation": [ + "unknown", + "conflict", + "global-name", + ], + "@keyframes unknown": {}, + "@keyframes conflict": {}, + "@keyframes st-global(global-name)": {}, + } + } + `, + '/entry.st.css': ` + @st-import mix from './mixin'; + + /* @atrule(stay global) conflict */ + @keyframes st-global(conflict) {} + + /* + @rule[0] .entry__a { + animation: unknown; + animation: conflict; + animation: global-name; + } + @rule(ns for sheet)[1] entry__unknown + @rule(global by sheet)[2] conflict + @rule(global by mixin)[3] global-name + */ + .a { + -st-mixin: mix; + } + `, + }); + }); + it(`should not mix rules/atrules into keyframe`, () => { + testStylableCore(` + .mix { + color: green; + } + .mix:hover { + color: red; + } + + @keyframes move { + /* + @transform-warn ${STMixin.diagnostics.INVALID_MERGE_OF( + `0%:hover { + color: red; + }` + )} + @rule[0] 0% { color: green } + @rule[1] 100% { } + */ + 0% { -st-mixin: mix; } + 100% {} + } + `); + }); + }); }); diff --git a/packages/core/test/features/css-type.spec.ts b/packages/core/test/features/css-type.spec.ts index 2f75f3175..9c7d990e7 100644 --- a/packages/core/test/features/css-type.spec.ts +++ b/packages/core/test/features/css-type.spec.ts @@ -188,6 +188,63 @@ describe(`features/css-type`, () => { }); }); }); + describe(`st-mixin`, () => { + it(`should mix element type`, () => { + const { sheets } = testStylableCore({ + '/mixin.st.css': ` + Mix { + color: green; + } + `, + '/entry.st.css': ` + @st-import MixRoot, [Mix as mixType] from './mixin.st.css'; + + /* + @rule(type) .entry__a { color: green } + */ + .a { + -st-mixin: mixType; + } + + /* + @rule(root.0)[0] .entry__a { } + @rule(root.1)[1] .entry__a Mix { color: green } + */ + .a { + -st-mixin: MixRoot; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix element type alias`, () => { + testStylableCore({ + '/mixin.st.css': ` + Mix { + from: imported; + } + `, + '/entry.st.css': ` + @st-import [Mix as MixType] from './mixin.st.css'; + + MixType { + from: local; + } + + /* + @rule[0] .entry__a { from: imported; } + @rule[1] .entry__a { from: local; } + */ + .a { + -st-mixin: MixType; + } + `, + }); + }); + }); describe(`css-class`, () => { it(`should transform according to -st-global`, () => { const { sheets } = testStylableCore({ diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts new file mode 100644 index 000000000..c3296a8ac --- /dev/null +++ b/packages/core/test/features/st-mixin.spec.ts @@ -0,0 +1,1955 @@ +import chaiSubset from 'chai-subset'; +import type { SRule } from '@stylable/core'; +import { STMixin } from '@stylable/core/dist/features'; +import { ignoreDeprecationWarn } from '@stylable/core/dist/helpers/deprecation'; +import { + testStylableCore, + shouldReportNoDiagnostics, + matchRuleAndDeclaration, +} from '@stylable/core-test-kit'; +import chai, { expect } from 'chai'; +import type * as postcss from 'postcss'; + +chai.use(chaiSubset); +describe(`features/st-mixin`, () => { + it(`should append mixin declarations`, () => { + const { sheets } = testStylableCore(` + .mix { + propA: blue; + propB: green; + } + + /* @rule .entry__empty {propA: blue; propB: green;} */ + .empty { + -st-mixin: mix; + } + + /* @rule .entry__insert {before:1; propA:blue; propB:green; after: 2} */ + .insert { + before: 1; + -st-mixin: mix; + after: 2; + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append mixin rules`, () => { + const { sheets } = testStylableCore(` + .mix { + id: mix; + } + .mix:hover { + id: mix-hover;; + } + .mix .child { + id: mix-child; + } + :is(.mix) { + id: is-mix; + } + .y:not(.mix) { + id: class-not-mix; + } + + + /* + @rule[0] .entry__root { id: mix } + @rule[1] .entry__root:hover { id: mix-hover } + @rule[2] .entry__root .entry__child { id: mix-child } + @rule[3] :is(.entry__root) { id: is-mix } + @rule[4] .entry__y:not(.entry__root) { id: class-not-mix } + */ + .root { + -st-mixin: mix; + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it('should reorder selector to context', () => { + const { sheets } = testStylableCore({ + '/mixin.st.css': ` + .root { + -st-states: x; + } + .mixin {-st-states: mix-state;} + .root:x.mixin:mix-state { + z-index: 1; + } + .root:x.mixin:mix-state[attr].y { + z-index: 1; + } + .mixin:is(.y.mixin:mix-state) { + z-index: 1; + } + .x.mixin[a] .y.mixin[b] { + z-index: 1; + } + :is(.x.mixin:is(.y.mixin)) { + z-index: 1; + } + + `, + 'entry.st.css': ` + @st-import [mixin] from "./mixin.st.css"; + + /* + @rule[1] .entry__y.mixin--mix-state.mixin__root.mixin--x + @rule[2] .entry__y.mixin--mix-state[attr].mixin__y.mixin__root.mixin--x + @rule[3] .entry__y:is(.entry__y.mixin--mix-state.mixin__y) + @rule[4] .entry__y[a].mixin__x .entry__y[b].mixin__y + @rule[5] :is(.entry__y:is(.entry__y.mixin__y).mixin__x) + */ + .y { + -st-mixin: mixin; + } + `, + }); + shouldReportNoDiagnostics(sheets[`/entry.st.css`].meta); + }); + it(`should append mixin within a mixin`, () => { + const { sheets } = testStylableCore(` + .deep-mix { + prop: green; + } + .top-mix { + -st-mixin: deep-mix; + } + + /* @rule .entry__a {prop: green;} */ + .a { + -st-mixin: top-mix; + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should handle circular mixins`, () => { + testStylableCore(` + /* + @transform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ + `b from /entry.st.css`, + `a from /entry.st.css`, + ])} + @rule .entry__a { + prop: green; + prop: green; + } + */ + .a { + prop: green; + -st-mixin: b; + } + + /* + @transform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ + `a from /entry.st.css`, + `b from /entry.st.css`, + ])} + @rule .entry__b { + prop: green; + } + */ + .b { + -st-mixin: a; + } + `); + }); + it(`should append mixin with complex selector`, () => { + const { sheets } = testStylableCore(` + .mix {} + .mix .mix.other { + prop: b; + } + .mix:hover, .filter-out-unrelated, .mix:focus { + prop: c; + } + + /* + @rule(mix repeat)[1] .entry__a .entry__a.entry__other {prop: b;} + @rule(multi selector)[2] .entry__a:hover, .entry__a:focus {prop: c;} + */ + .a { + -st-mixin: mix; + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append mixin to complex selector`, () => { + const { sheets } = testStylableCore({ + '/ns.st.css': ` + .mix { + prop: a; + } + .mix .mix.other { + prop: b; + } + .mix:hover, .filter-out-unrelated, .mix:focus { + prop: c; + } + + /* + @rule(only mixin class)[0] .ns__a .ns__b {prop: a;} + @rule(mix repeat)[1] .ns__a .ns__b .ns__a .ns__b.ns__other {prop: b;} + @rule(multi selector)[2] .ns__a .ns__b:hover, .ns__a .ns__b:focus {prop: c;} + */ + .a .b { + -st-mixin: mix; + } + `, + }); + + const { meta } = sheets['/ns.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should handle invalid cases`, () => { + testStylableCore(` + .mixA { + color: red; + } + .mixB { + color: green; + } + + /* @rule .entry__root { -st-mixin: "mixA" } */ + .root { + /* @analyze-error ${STMixin.diagnostics.VALUE_CANNOT_BE_STRING()} */ + -st-mixin: "mixA"; + } + + /* @rule .entry__root { color: green } */ + .root { + -st-mixin: mixA; + /* @analyze-warn ${STMixin.diagnostics.OVERRIDE_MIXIN(`-st-mixin`)} */ + -st-mixin: mixB; + } + `); + }); + it(`should not mix mixin that is removed before transform`, () => { + testStylableCore( + ` + .mix { + id: mix; + } + + /* + @rule .entry__mixToClass { before: a; after: z; } + */ + .mixToClass { + before: a; + -st-mixin: mix; + after: z; + + } + `, + { + stylableConfig: { + onProcess(meta) { + // remove -st-mixin origin before apply mixin. + const mixToClass = meta.ast.nodes[2] as postcss.Rule; + const stMixinDecl = mixToClass.nodes[1]; + stMixinDecl.remove(); + return meta; + }, + }, + } + ); + }); + describe(`SRule (deprecated)`, () => { + // ToDo: remove in v5 when SRule is removed + it(`should collect mixins on rules`, () => { + testStylableCore( + ` + .x { + -st-mixin: my-mixin + } + .my-mixin {} + `, + { + stylableConfig: { + onProcess(meta) { + const mixinRule = meta.ast.nodes[0] as SRule; + expect( + ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type + ).to.eql('my-mixin'); + + return meta; + }, + }, + } + ); + }); + it(`should use last mixin deceleration`, () => { + testStylableCore( + ` + .x { + -st-mixin: my-mixin1; + -st-mixin: my-mixin2; + } + .my-mixin1 {} + .my-mixin2 {} + `, + { + stylableConfig: { + onProcess(meta) { + const mixinRule = meta.ast.nodes[0] as SRule; + expect( + ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type + ).to.eql('my-mixin2'); + + return meta; + }, + }, + } + ); + }); + it(`should use last mixin deceleration for -st-partial-mixin`, () => { + testStylableCore( + ` + .x { + -st-partial-mixin: my-mixin1; + -st-partial-mixin: my-mixin2; + } + .my-mixin1 {} + .my-mixin2 {} + `, + { + stylableConfig: { + onProcess(meta) { + const mixinRule = meta.ast.nodes[0] as SRule; + expect( + ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type + ).to.eql('my-mixin2'); + + return meta; + }, + }, + } + ); + }); + it(`should use mixin deceleration in order for mixed -st-mixin and -st-partial-mixin`, () => { + testStylableCore( + ` + .x { + -st-mixin: my-mixin1; + -st-partial-mixin: my-mixin2; + } + .y { + -st-partial-mixin: my-mixin2; + -st-mixin: my-mixin1; + } + .my-mixin1 {} + .my-mixin2 {} + `, + { + stylableConfig: { + onProcess(meta) { + const mixinRule1 = meta.ast.nodes[0] as SRule; + const mixinRule2 = meta.ast.nodes[1] as SRule; + expect( + ignoreDeprecationWarn(() => mixinRule1.mixins!)[0].mixin.type + ).to.eql('my-mixin1'); + expect( + ignoreDeprecationWarn(() => mixinRule1.mixins!)[1].mixin.type + ).to.eql('my-mixin2'); + expect( + ignoreDeprecationWarn(() => mixinRule2.mixins!)[0].mixin.type + ).to.eql('my-mixin2'); + expect( + ignoreDeprecationWarn(() => mixinRule2.mixins!)[1].mixin.type + ).to.eql('my-mixin1'); + + return meta; + }, + }, + } + ); + }); + it(`should use mixin last deceleration in order for mixed -st-mixin and -st-partial-mixin with duplicates`, () => { + testStylableCore( + ` + .x { + -st-mixin: my-mixin1; + -st-partial-mixin: my-mixin2; + -st-mixin: my-mixin3; + -st-partial-mixin: my-mixin4; + } + .my-mixin1 {} + .my-mixin2 {} + .my-mixin3 {} + .my-mixin4 {} + `, + { + stylableConfig: { + onProcess(meta) { + const mixinRule = meta.ast.nodes[0] as SRule; + expect( + ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type + ).to.eql('my-mixin3'); + expect( + ignoreDeprecationWarn(() => mixinRule.mixins!)[1].mixin.type + ).to.eql('my-mixin4'); + + return meta; + }, + }, + } + ); + }); + }); + describe(`st-import`, () => { + it(`should mix imported class`, () => { + const { sheets } = testStylableCore({ + '/imported.st.css': ` + .mix { + prop: green; + } + .mix .local { + prop: blue; + } + `, + '/entry.st.css': ` + @st-import [mix] from './imported.st.css'; + /* + @rule .entry__a { prop: green; } + @rule[1] .entry__a .imported__local { prop: blue; } + */ + .a { + -st-mixin: mix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix imported mapped class into class with same local name`, () => { + const { sheets } = testStylableCore({ + '/imported.st.css': ` + .mix { + prop: green; + } + .mix .local { + prop: blue; + } + `, + '/entry.st.css': ` + @st-import [mix as mappedMix] from './imported.st.css'; + /* + @rule .entry__mix { prop: green; } + @rule[1] .entry__mix .imported__local { prop: blue; } + */ + .mix { + -st-mixin: mappedMix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix aliased CSS overrides`, () => { + const { sheets } = testStylableCore({ + '/base.st.css': ` + .mix { + prop: a; + } + .mix:hover .local { + prop: b; + } + `, + '/enriched.st.css': ` + @st-import [mix] from './base.st.css'; + .mix { + prop: c; + } + .mix:hover .local { + prop: d; + } + `, + '/entry.st.css': ` + @st-import [mix] from './enriched.st.css'; + + .mix:hover.local { + prop: e; + } + + /* + @rule[0] .entry__a { prop: a; } + @rule[1] .entry__a:hover .base__local { prop: b; } + @rule[2] .entry__a { prop: c; } + @rule[3] .entry__a:hover .enriched__local { prop: d; } + @rule[4] .entry__a:hover.entry__local { prop: e; } + */ + .a { + -st-mixin: mix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix nested mixins`, () => { + const { sheets } = testStylableCore({ + '/base.st.css': ` + .c {} + `, + '/enriched.st.css': ` + @st-import Base from './base.st.css'; + .b { + -st-mixin: Base; + } + `, + '/entry.st.css': ` + @st-import Enriched from './enriched.st.css'; + /* + @rule[0] .entry__a + @rule[1] .entry__a .enriched__b + @rule[2] .entry__a .enriched__b .base__c + */ + .a { + -st-mixin: Enriched; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should handle circular mixins from multiple stylesheets`, () => { + // ToDo: check why circular_mixin is not reported + testStylableCore({ + '/sheet1.st.css': ` + @st-import [b] from './sheet2.st.css'; + /* + @xtransform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ + `b from /sheet2.st.css`, + `a from /sheet1.st.css`, + ])} + @rule .sheet1__a { + prop: green; + prop: green; + } + */ + .a { + prop: green; + -st-mixin: b; + } + `, + '/sheet2.st.css': ` + @st-import [a] from './sheet1.st.css'; + /* + @xtransform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ + `a from /sheet1.st.css`, + `b from /sheet2.st.css`, + ])} + @rule .sheet2__b { + prop: green; + } + */ + .b { + -st-mixin: a; + } + `, + }); + }); + it(`should handle unresolved mixin`, () => { + testStylableCore({ + '/mixin.st.css': ``, + '/entry.st.css': ` + @st-import [unresolved] from './mixin.st.css'; + + .a { + /* @analyze-warn ${STMixin.diagnostics.UNKNOWN_MIXIN(`unknown`)} */ + -st-mixin: unknown; + } + + .a { + /* @transform-error ${STMixin.diagnostics.UNKNOWN_MIXIN_SYMBOL( + `unresolved` + )} */ + -st-mixin: unresolved; + } + `, + }); + }); + }); + describe(`root mixin`, () => { + it(`should mix all content`, () => { + const { sheets } = testStylableCore({ + '/mix.st.css': ` + .a { + origin: a; + } + .b { + origin: b; + } + `, + '/entry.st.css': ` + @st-import Mix from './mix.st.css'; + /* + @rule(.a)[1] .entry__into .mix__a {origin: a;} + @rule(.b)[2] .entry__into .mix__b {origin: b;} + */ + .into { + -st-mixin: Mix; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should report on circular mixin when mixed on local class`, () => { + testStylableCore(` + /* + @transform-warn ${STMixin.diagnostics.CIRCULAR_MIXIN([`root from /entry.st.css`])} + @rule(self)[0] .entry__a {} + @rule(self appended)[1] .entry__a .entry__a {} + @rule(other appended)[2] .entry__a .entry__b {} + @rule(other original)[3] .entry__b {} + */ + .a { + -st-mixin: root; + } + + .b {} + `); + }); + it.skip(`should mix root class (bug)`, () => { + // ToDo:fix .root mixed order bug (should be between .a and .b) + // ToDo:fix .root mixed specificity bug (should be .entry__root .entry__root?) + const { sheets } = testStylableCore(` + .a { + origin: a; + } + .root { + origin: root; + } + .b { + origin: b; + } + + /* + @rule(.a)[1] .entry__into .entry__a {origin: a;} + @rule(.root)[2] .entry__into .entry__root {origin: root;} + @rule(.b)[3] .entry__into .entry__b {origin: b;} + */ + .into { + -st-mixin: root; + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + describe(`-st-partial-mixin`, () => { + it(`should append only declaration that includes the overridden params`, () => { + const { sheets } = testStylableCore(` + :vars { + v1: red; + v2: green; + v3: blue; + } + + .mix { + propA: value(v1), value(v2); + propB: value(v1), value(v3); + } + + /* @rule(just v2) .entry__a { + propA: red, white + }*/ + .a { + -st-partial-mixin: mix(v2 white); + } + + /* @rule(just v3) .entry__a { + propB: red, white + }*/ + .a { + -st-partial-mixin: mix(v3 white); + } + + /* @rule(v2 & v3) .entry__a { + propA: red, purple; + propB: red, white; + }*/ + .a { + -st-partial-mixin: mix(v2 purple, v3 white); + } + + /* @rule(just v1) .entry__a { + propA: white, green; + propB: white, blue; + }*/ + .a { + -st-partial-mixin: mix(v1 white); + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append only rules that contains included declarations`, () => { + const { sheets } = testStylableCore(` + :vars { + v1: red; + v2: green; + v3: blue; + } + + /* + @rule(origin - v1)[0] .entry__mix { propA: red } + @rule(origin - v2)[1] .entry__mix:hover { propB: green } + @rule(origin - v3)[2] .entry__mix:focus { propC: blue } + */ + .mix { + propA: value(v1); + } + .mix:hover { + propB: value(v2); + } + .mix:focus { + propC: value(v3); + } + + /* + @rule(v1 - override)[0] .entry__a { propA: white; } + @rule(v1- end-of-mix)[1] .entry__SEP { } + */ + .a { + -st-partial-mixin: mix(v1 white); + } + .SEP {} + + /* + @rule(v2 - empty)[0] .entry__a { } + @rule(v2 - override)[1] .entry__a:hover { propB: white } + @rule(v2- end-of-mix)[2] .entry__SEP { } + */ + .a { + -st-partial-mixin: mix(v2 white); + } + .SEP {} + + /* + @rule(v3 - empty)[0] .entry__a { } + @rule(v3 - override)[1] .entry__a:focus { propC: white } + @rule(v3- end-of-mix)[2] .entry__SEP { } + */ + .a { + -st-partial-mixin: mix(v3 white); + } + .SEP {} + + /* + @rule(v2&v3 - empty)[0] .entry__a { } + @rule(v2&v3 - override)[1] .entry__a:hover { propB: yellow } + @rule(v2&v3 - override)[2] .entry__a:focus { propC: white } + @rule(v2&v3- end-of-mix)[3] .entry__SEP { } + */ + .a { + -st-partial-mixin: mix(v2 yellow, v3 white); + } + .SEP {} + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append nested partial mixins`, () => { + const { sheets } = testStylableCore(` + :vars { + v1: red; + v2: green; + v3: blue; + } + + .mix-deep { + propA: value(v1); + propB: value(v2); + propC: value(v3); + } + .mix { + -st-partial-mixin: mix-deep(v2 value(v1)); + propX: value(v3); + } + + /* + @rule(v1) .entry__a { propB: white; } + @rule(v1-end)[1] .entry__SEP + */ + .a { + -st-partial-mixin: mix(v1 white); + } + .SEP {} + + /* + @rule(v2) .entry__a { } + @rule(v2-end)[1] .entry__SEP + */ + .a { + -st-partial-mixin: mix(v2 white); + } + .SEP {} + + /* + @rule(v3) .entry__a { propX: white } + @rule(v3-end)[1] .entry__SEP + */ + .a { + -st-partial-mixin: mix(v3 white); + } + .SEP {} + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should resolve variable value from overrides`, () => { + const { sheets } = testStylableCore(` + :vars { + v1: red; + v2: v2 -> value(v1); + v3: v3 -> value(v2); + } + + .mix { + /* @decl prop: v3 -> v2 -> red */ + prop: value(v3); + } + + /* + @rule(v1) .entry__a { prop: v3 -> v2 -> green; } + */ + .a { + -st-partial-mixin: mix(v1 green); + } + + /* + @rule(v2) .entry__a { prop: v3 -> green; } + */ + .a { + -st-partial-mixin: mix(v2 green); + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should warn for no params`, () => { + testStylableCore(` + :vars { + v1: red; + } + + .mix-color { + color: value(v1); + } + + /* @rule(v1) .entry__a { } */ + .a { + /* @analyze-warn word(mix-color) ${STMixin.diagnostics.PARTIAL_MIXIN_MISSING_ARGUMENTS( + `mix-color` + )} */ + -st-partial-mixin: mix-color(); + } + `); + }); + it(`should be applied next to -st-mixin`, () => { + testStylableCore(` + :vars { + color: red; + size: 1px; + } + + .mix { + background: value(color); + width: value(size); + } + + /* @rule(partial after) .entry__a { + pos: before; + background: red; + width: 1px; + pos: between; + background: green; + pos: after; + } */ + .a { + pos: before; + -st-mixin: mix; + pos: between; + -st-partial-mixin: mix(color green); + pos: after; + } + + /* @rule(partial before) .entry__a { + pos: before; + background: green; + pos: between; + background: red; + width: 1px; + pos: after; + } */ + .a { + pos: before; + -st-partial-mixin: mix(color green); + pos: between; + -st-mixin: mix; + pos: after; + } + `); + }); + }); + describe(`JavaScript mixin`, () => { + it(`should append mixin declarations`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = { + addGreen() { + return { + color: "green" + } + }, + fallbackDecl() { + return { + color: ["blue", "green"] + } + }, + camelToKebab() { + return { + declPropName: "declValue" + } + }, + notAStringDecl() { + return { + number: 56, + 'obj-as-fallback': [ + 'before', + {toString(){return "stringified decl val"}}, + 'after' + ], + } + } + } + `, + '/entry.st.css': ` + @st-import [addGreen, fallbackDecl, camelToKebab, notAStringDecl] from './mixin.js'; + + /* @rule(single) .entry__root { + before: val; + color: green; + after: val; + } */ + .root { + before: val; + -st-mixin: addGreen; + after: val; + } + + /* @rule(fallback) .entry__root { + before: val; + color: blue; + color: green; + after: val; + } */ + .root { + before: val; + -st-mixin: fallbackDecl; + after: val; + } + + /* @rule(fallback) .entry__root { + decl-prop-name: declValue; + } */ + .root { + -st-mixin: camelToKebab; + } + + /* @rule .entry__root { + number: 56px; + obj-as-fallback: before; + obj-as-fallback: stringified decl val; + obj-as-fallback: after; + } */ + .root { + -st-mixin: notAStringDecl; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append mixin rules`, () => { + // ToDo: fix ":global(.part)" to transform with mixin root + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + Element: { + d: "Capital element" + }, + element: { + d: "lowercase element" + }, + ".part": { + d: "class namespaced in context" + }, + ":global(.part)": { + d: "global class" + }, + ".x, .y": { + d: "multiple selectors" + } + } + } + `, + '/entry.st.css': ` + @st-import multiRules from './mixin.js'; + + /* + @rule[0] .entry__root {} + @rule[1] .entry__root Element { d: Capital element } + @rule[2] .entry__root element { d: lowercase element } + @rule[3] .entry__root .entry__part { d: class namespaced in context } + @rule[4] .part { d: global class } + @rule[5] .entry__root .entry__x, .entry__root .entry__y { d: multiple selectors } + */ + .root { + -st-mixin: multiRules; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append mixin on multiple selectors`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + prop: "x", + div: { + prop: "y" + }, + } + } + `, + '/entry.st.css': ` + @st-import multiRules from './mixin.js'; + + /* + @rule[0] .entry__a, .entry__b { prop: x } + @rule[1] .entry__a div, .entry__b div { prop: y } + */ + .a, .b { + -st-mixin: multiRules; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append nested mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + prop: "1", + "&:hover": { + prop: "2" + }, + ".child": { + prop: "3", + "&:focus": { + prop: "4" + } + } + } + } + `, + '/entry.st.css': ` + @st-import nestedRules from './mixin.js'; + + /* + @rule[0] .entry__root { prop: 1 } + @rule[1] .entry__root:hover { prop: 2 } + @rule[2] .entry__root .entry__child { prop: 3 } + @rule[3] .entry__root .entry__child:focus { prop: 4 } + */ + .root { + -st-mixin: nestedRules; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should accept list of values (as first argument)`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function(params) { + return { + color: params[0], + background: params[1], + } + } + `, + '/entry.st.css': ` + @st-import paint from './mixin.js'; + + /* @rule(inline) .entry__root { + color: black; + background: white; + } */ + .root { + -st-mixin: paint(black, white); + } + + :vars { + color1: white; + color2: green; + } + /* @rule(values) .entry__root { + color: white; + background: green; + } */ + .root { + -st-mixin: paint(value(color1), value(color2)); + } + + /* @rule(strings) .entry__root { + color: orange; + background: gold; + } */ + .root { + -st-mixin: paint("orange", "gold"); + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should handle no values`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function(params) { + return { + param1: params[0] || 'default', + param2: params[1] || 'default', + } + } + `, + '/entry.st.css': ` + @st-import twoParams from './mixin.js'; + + /* @rule(no parans) .entry__no-parenthesis { + param1: default; + param2: default; + } */ + .no-parenthesis { + -st-mixin: twoParams; + } + + /* @rule(empty parans) .entry__no-args { + param1: default; + param2: default; + } */ + .no-args { + /* - report: report useless diagnostics "argument at index 0 is empty" */ + -st-mixin: twoParams(); + } + + /* @rule(only first) .entry__partial { + param1: v1; + param2: default; + } */ + .partial { + -st-mixin: twoParams("v1"); + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should append multiple mixins`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function(params) { + return { + [params[0]]: params[1] + } + } + `, + '/entry.st.css': ` + @st-import decl from './mixin.js'; + + /* + @rule[0] .entry__root { + color: green; + background: blue; + } + */ + .root { + -st-mixin: decl(color, green) decl(background, blue); + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should re-export JS mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + color: "green" + } + } + `, + '/pass-through.st.css': ` + @st-import addGreen from './mixin.js'; + `, + '/entry.st.css': ` + @st-import [addGreen] from './pass-through.st.css'; + + /* @rule .entry__root { + color: green; + } */ + .root { + -st-mixin: addGreen; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should handle invalid cases`, () => { + testStylableCore({ + '/mixins.js': ` + module.exports = { + notAFunction: "not a function", + throw() { + throw "bug in js mix"; + } + }; + `, + '/entry.st.css': ` + @st-import [notAFunction, throw] from './mixins.js'; + + /* @transform-error(not a function) word(notAFunction) ${STMixin.diagnostics.JS_MIXIN_NOT_A_FUNC()} */ + .a { + -st-mixin: notAFunction; + } + + /* @transform-error(mix throw) word(throw) ${STMixin.diagnostics.FAILED_TO_APPLY_MIXIN( + `bug in js mix` + )} */ + .a { + -st-mixin: throw; + } + `, + }); + }); + it(`should apply on -st-partial-mixin (same as -st-mixin)`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = { + addColor(params) { + return { + color: params || 'red' + } + }, + } + `, + '/entry.st.css': ` + @st-import [addColor] from './mixin.js'; + + /* @rule(single) .entry__root { + before: val; + color: green; + after: val; + } */ + .root { + before: val; + -st-partial-mixin: addColor(green); + after: val; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + describe(`st-global`, () => { + it(`should keep global selectors from mixin`, () => { + const { sheets } = testStylableCore({ + '/mix.st.css': ` + .mixA .before :global(.a) .after {} + .mixB .before :global(.b) .after {} + `, + '/entry.st.css': ` + @st-import [mixA, mixB] from './mix.st.css'; + + /* @rule(direct)[1] .entry__root .mix__before .a .mix__after */ + .root { + -st-mixin: mixA; + } + + .local-mix { + -st-mixin: mixB; + } + + /* @rule(nested)[1] .entry__root .mix__before .b .mix__after */ + .root { + -st-mixin: local-mix; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + expect(meta.globals, 'globals reflected out').to.eql({ + a: true, + b: true, + }); + }); + }); + describe(`st-formatter`, () => { + it(`should resolve formatter value in mixin `, () => { + const { sheets } = testStylableCore({ + '/connect-args.js': ` + module.exports = function() { + return \`\${[...arguments].join(', ')}\`; + } + `, + '/entry.st.css': ` + @st-import connectArgs from './connect-args'; + :vars { + shallow: connectArgs(defaultA, defaultB); + deep: value(shallow); + } + .mix { + prop: connectArgs(color1, color2); + } + .mix-with-param { + propA: value(shallow); + propB: value(deep); + } + + /* @rule(no params) + .entry__a { + prop: color1, color2; + } */ + .a { + -st-mixin: mix; + } + + /* @rule(default param) + .entry__a { + propA: defaultA, defaultB; + propB: defaultA, defaultB; + } */ + .a { + -st-mixin: mix-with-param(); + } + + /* @rule(override all) + .entry__a { + propA: 1, 2; + propB: 3, 4; + } */ + .a { + -st-mixin: mix-with-param( + shallow connectArgs(1, 2), + deep connectArgs(3, 4) + ); + } + + /* @rule(partial override shallow) + .entry__a { + propA: 1, 2; + propB: 1, 2; + } */ + .a { + -st-mixin: mix-with-param( + shallow connectArgs(1, 2) + ); + } + + /* @rule(partial override deep) + .entry__a { + propA: defaultA, defaultB; + propB: 1, 2; + } */ + .a { + -st-mixin: mix-with-param( + deep connectArgs(1, 2) + ); + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + describe(`st-var`, () => { + it(`should resolve mixin vars in mixin origin context `, () => { + const { sheets } = testStylableCore({ + '/mix.st.css': ` + :vars { + x: imported; + } + .mix { + val: value(x); + } + `, + '/entry.st.css': ` + @st-import [mix as imported-mix] from './mix.st.css'; + :vars { + x: local; + } + .local-mix { + val: value(x); + } + + /* @rule(imported) .entry__root {val: imported} */ + .root { + -st-mixin: imported-mix; + } + + /* @rule(local) .entry__root {val: local} */ + .root { + -st-mixin: local-mix; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should handle invalid cases `, () => { + testStylableCore(` + :vars { + x: local; + } + .mix { + val: value(x); + } + + /* @rule .entry__root {val: local} */ + .root { + /* @transform-warn ${STMixin.diagnostics.INVALID_NAMED_PARAMS()} */ + -st-mixin: mix(varNameWithNoValue); + } + `); + }); + it(`should override mixin vars `, () => { + const { sheets } = testStylableCore({ + '/mix.st.css': ` + :vars { + x: importedX; + y: importedY; + } + .mix { + valX: value(x); + valY: value(y); + } + `, + '/entry.st.css': ` + @st-import [mix as imported-mix] from './mix.st.css'; + :vars { + x: localX; + y: localY; + } + .local-mix { + valX: value(x); + valY: value(y); + } + + /* @rule(imported partial override) + .entry__root {valX: override; valY: importedY} + */ + .root { + -st-mixin: imported-mix(x override); + } + + /* @rule(local partial override) + .entry__root {valX: override; valY: localY} + */ + .root { + -st-mixin: local-mix(x override); + } + + /* @rule(imported multi override) + .entry__root {valX: overrideX; valY: overrideY} + */ + .root { + -st-mixin: imported-mix(x overrideX, y overrideY); + } + + /* @rule(local multi override) + .entry__root {valX: overrideX; valY: overrideY} + */ + .root { + -st-mixin: local-mix(x overrideX, y overrideY); + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should override vars with spaced value `, () => { + const { sheets } = testStylableCore(` + :vars { + color: red; + border: red; + } + .mix { + color: value(color); + border: value(border); + } + + /* @rule .entry__root { + color: blue; + border: 1px solid green; + } */ + .root { + -st-mixin: mix( + border 1px solid green, + color blue + ); + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should override vars that are used as override `, () => { + const { sheets } = testStylableCore({ + '/mix.st.css': ` + :vars { + base-value: base; + } + .mix { + val: value(base-value); + } + `, + '/override-mix.st.css': ` + @st-import [mix as base-mix] from './mix.st.css'; + :vars { + override-a: a; + } + .mix { + -st-mixin: base-mix( + base-value value(override-a) + ); + } + `, + '/entry.st.css': ` + @st-import [mix as imported-override-mix] from './override-mix.st.css'; + + /* @rule(imported) .entry__root {val: green} */ + .root { + -st-mixin: imported-override-mix( + override-a green + ); + } + + :vars { + override-b: b; + } + .local-override-mix { + -st-mixin: imported-override-mix( + override-a value(override-b) + ); + } + + /* @rule(local) .entry__root {val: green} */ + .root { + -st-mixin: local-override-mix( + override-b green + ); + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + describe(`higher-level feature integrations`, () => { + // ToDo: move to their higher level feature spec when created + describe(`css-asset`, () => { + it(`should mix url relative to origin stylesheet path`, () => { + const { sheets } = testStylableCore({ + '/a/b/base.st.css': ` + .mix { + background: url(./base.png); + content: url(../1-up.png); + } + `, + '/a/enrich.st.css': ` + @st-import [mix] from './b/base.st.css'; + .mix { + background: url(./enrich.png); + content: url(../1-up.png); + } + `, + '/entry.st.css': ` + @st-import [mix] from './a/enrich.st.css'; + + .skip-mix { + /* ToDo: add check for asset from local override */ + background: url(./entry.png); + } + + /* + @rule[0] .entry__a { + background: url(./a/b/base.png); + content: url(./a/1-up.png); + } + @rule[1] .entry__a { + background: url(./a/enrich.png); + content: url(./1-up.png); + } + */ + .a { + -st-mixin: mix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should keep url relative to mixin Javascript source`, () => { + const { sheets } = testStylableCore({ + '/a/b/mixin.js': ` + module.exports = function(options) { + return { + background: [ + "url(./next-to-mixin.png)", + "url(../../next-to-sheet.png)", + ] + } + } + `, + '/entry.st.css': ` + @st-import mix from './a/b/mixin.js'; + + /* @rule .entry__root { + background: url(./a/b/next-to-mixin.png); + background: url(./next-to-sheet.png); + } */ + .root { + -st-mixin: mix; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix url relative to node_modules stylesheet`, () => { + const { sheets } = testStylableCore({ + '/node_modules/fake-package/package.json': { + content: '{"name": "fake-package", "version": "0.0.1"}', + }, + '/node_modules/fake-package/mixin.st.css': ` + .mix { + background: url(./css.png); + } + `, + '/node_modules/fake-package/mixin.js': ` + module.exports.mix = function() { + return { + "background": 'url(./js.png)' + }; + } + `, + '/entry.st.css': ` + @st-import [mix as cssMixin] from 'fake-package/mixin.st.css'; + @st-import [mix as jsMixin] from 'fake-package/mixin.js'; + + /* @rule(css) .entry__a { + background: url(./node_modules/fake-package/css.png); + } */ + .a { + -st-mixin: cssMixin; + } + + /* @rule(js) .entry__a { + background: url(./node_modules/fake-package/js.png); + } */ + .a { + -st-mixin: jsMixin; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + describe(`css-media`, () => { + // ToDo: move nested expectation inline once inline nested path is available + it(`should mix @media queries for nested CSS mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.st.css': ` + .mix { id: before } + @media screen { + .mix { id: nested } + } + .mix { id: after } + `, + '/entry.st.css': ` + @st-import [mix as css-mix] from './mixin.st.css'; + + /* + @rule[0] .entry__a { id: before } + @rule[1] screen + @rule[2] .entry__a { id: after } + */ + .a { + -st-mixin: css-mix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + matchRuleAndDeclaration( + meta.outputAst!.nodes[2] as postcss.Container, + 0, + '.entry__a', + 'id: nested' + ); + }); + it(`should mix @media queries for nested JS mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + "@media screen": { + "&": { id: "nested" } + }, + "&": { id: "after" }, + } + } + `, + '/entry.st.css': ` + @st-import js-mix from './mixin.js'; + + /* + @rule[1] screen + @rule[2] .entry__a { id: after } + */ + .a { + -st-mixin: js-mix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + matchRuleAndDeclaration( + meta.outputAst!.nodes[2] as postcss.Container, + 0, + '.entry__a', + 'id: nested' + ); + }); + it(`should mix @media queries as part of root mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.st.css': ` + .mix { id: before } + @media screen { + .mix { id: nested } + } + .mix { id: after } + `, + '/entry.st.css': ` + @st-import MixRoot from './mixin.st.css'; + + /* + @rule[0] .entry__a { } + @rule[1] .entry__a .mixin__mix { id: before } + @rule[2] screen + @rule[3] .entry__a .mixin__mix { id: after } + */ + .a { + -st-mixin: MixRoot; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + matchRuleAndDeclaration( + meta.outputAst!.nodes[3] as postcss.Container, + 0, + '.entry__a .mixin__mix', + 'id: nested' + ); + }); + }); + describe(`css-supports`, () => { + // ToDo: move nested expectation inline once inline nested path is available + it(`should mix @supports queries for nested CSS mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.st.css': ` + .mix { id: before } + @supports (color: green) { + .mix { id: nested } + } + .mix { id: after } + `, + '/entry.st.css': ` + @st-import [mix as css-mix] from './mixin.st.css'; + + /* + @rule[0] .entry__a { id: before } + @rule[1] (color: green) + @rule[2] .entry__a { id: after } + */ + .a { + -st-mixin: css-mix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + matchRuleAndDeclaration( + meta.outputAst!.nodes[2] as postcss.Container, + 0, + '.entry__a', + 'id: nested' + ); + }); + it(`should mix @supports queries for nested JS mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + "@supports (color: green)": { + "&": { id: "nested" } + }, + "&": { id: "after" }, + } + } + `, + '/entry.st.css': ` + @st-import js-mix from './mixin.js'; + + /* + @rule[1] (color: green) + @rule[2] .entry__a { id: after } + */ + .a { + -st-mixin: js-mix; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + matchRuleAndDeclaration( + meta.outputAst!.nodes[2] as postcss.Container, + 0, + '.entry__a', + 'id: nested' + ); + }); + it(`should mix @supports queries as part of root mixin`, () => { + const { sheets } = testStylableCore({ + '/mixin.st.css': ` + .mix { id: before } + @supports (color: green) { + .mix { id: nested } + } + .mix { id: after } + `, + '/entry.st.css': ` + @st-import MixRoot from './mixin.st.css'; + + /* + @rule[0] .entry__a { } + @rule[1] .entry__a .mixin__mix { id: before } + @rule[2] (color: green) + @rule[3] .entry__a .mixin__mix { id: after } + */ + .a { + -st-mixin: MixRoot; + } + `, + }); + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + matchRuleAndDeclaration( + meta.outputAst!.nodes[3] as postcss.Container, + 0, + '.entry__a .mixin__mix', + 'id: nested' + ); + }); + }); + }); +}); diff --git a/packages/core/test/features/st-var.spec.ts b/packages/core/test/features/st-var.spec.ts index e595084c1..4e7453c34 100644 --- a/packages/core/test/features/st-var.spec.ts +++ b/packages/core/test/features/st-var.spec.ts @@ -171,6 +171,9 @@ describe(`features/st-var`, () => { .root { /* @decl(simple) prop: green */ prop: value(varA); + + /* @decl(concat) prop: before green-after */ + prop: before value(varA)-after; /* @decl(in unknown function) prop: unknown(green) */ prop: unknown(value(varA)) @@ -837,18 +840,24 @@ describe(`features/st-var`, () => { jsStr: '123', }; `, + '/re-export.st.css': ` + @st-import [jsStr as mappedJsStr] from './code'; + `, '/entry.st.css': ` @st-import [jsStr] from './code'; + @st-import [mappedJsStr as reexportJsStr] from './re-export.st.css'; :vars { a: value(jsStr); + b: value(reexportJsStr); } .root { - /* - @decl prop: 123 - */ + /* @decl(direct) prop: 123 */ prop: value(jsStr); + + /* @decl(re-export) prop: 123 */ + prop: value(reexportJsStr); } `, }); @@ -859,6 +868,7 @@ describe(`features/st-var`, () => { // JS exports expect(exports.stVars.a, `a JS export`).to.eql(`123`); + expect(exports.stVars.b, `a re-exported JS export`).to.eql(`123`); }); it(`should report unhandled imported non var symbols in value`, () => { testStylableCore({ diff --git a/packages/core/test/functions.spec.ts b/packages/core/test/functions.spec.ts index 86bacf63d..fa9f93c3c 100644 --- a/packages/core/test/functions.spec.ts +++ b/packages/core/test/functions.spec.ts @@ -47,13 +47,8 @@ describe('Stylable functions (native, formatter and variable)', () => { -st-from: "./formatter"; -st-default: formatter; } - :import { - -st-from: "./mixin"; - -st-default: mixin; - } .container { background: formatter(1, "2px solid red" 10px); - -st-mixin: mixin(1, "2"); } `, }, @@ -64,21 +59,11 @@ describe('Stylable functions (native, formatter and variable)', () => { } `, }, - '/mixin.js': { - content: ` - module.exports = function(args) { - return { - content: [...args].map((x)=>\`url(\${JSON.stringify(x)})\`).join(', ') - }; - } - `, - }, }, }); const rule = result.nodes[0] as postcss.Rule; expect(rule.nodes[0].toString()).to.equal('background: 1 2px solid red 10px'); - expect(rule.nodes[1].toString()).to.equal('content: url("1"), url("2")'); }); it('apply simple js formatter with a single argument', () => { diff --git a/packages/core/test/helpers/mixin.spec.ts b/packages/core/test/helpers/mixin.spec.ts new file mode 100644 index 000000000..56e26c56a --- /dev/null +++ b/packages/core/test/helpers/mixin.spec.ts @@ -0,0 +1,118 @@ +import { expect } from 'chai'; +import * as postcss from 'postcss'; +import { SBTypesParsers, valueMapping } from '@stylable/core'; +import postcssValueParser from 'postcss-value-parser'; + +const createMixinDecl = (value: string) => postcss.decl({ prop: valueMapping.mixin, value }); +const createPartialMixinDecl = (value: string) => + postcss.decl({ prop: valueMapping.partialMixin, value }); +const parseMixin = (mixinDecl: postcss.Declaration) => { + const mix = SBTypesParsers[valueMapping.mixin](mixinDecl, () => 'named'); + return mix; +}; + +const parsePartialMixin = (mixinDecl: postcss.Declaration) => { + const mix = SBTypesParsers[valueMapping.partialMixin](mixinDecl, () => 'named'); + return mix; +}; + +describe('helpers/mixin', () => { + describe('-st-mixin parse', () => { + it('named arguments with no params', () => { + const mixinDecl = createMixinDecl('Button'); + expect(parseMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: {}, + valueNode: postcssValueParser('Button').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); + + it('named arguments with empty params', () => { + const mixinDecl = createMixinDecl('Button()'); + expect(parseMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: {}, + valueNode: postcssValueParser('Button()').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); + + it('named arguments with one simple param', () => { + const mixinDecl = createMixinDecl('Button(color red)'); + expect(parseMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: { color: 'red' }, + valueNode: postcssValueParser('Button(color red)').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); + + it('named arguments with two simple params', () => { + const mixinDecl = createMixinDecl('Button(color red, color2 green)'); + expect(parseMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: { color: 'red', color2: 'green' }, + valueNode: postcssValueParser('Button(color red, color2 green)').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); + + it('named arguments with a trailing comma', () => { + const mixinDecl = createMixinDecl('Button(color red,)'); + expect(parseMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: { color: 'red' }, + valueNode: postcssValueParser('Button(color red,)').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); + + it('multiple named arguments with a trailing comma', () => { + const mixinDecl = createMixinDecl('Button(color red, size 2px,)'); + expect(parseMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: { color: 'red', size: '2px' }, + valueNode: postcssValueParser('Button(color red, size 2px,)').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); + + it('named arguments with one param with spaces', () => { + const mixinDecl = createMixinDecl('Button(border 1px solid red)'); + expect(parseMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: { border: '1px solid red' }, + valueNode: postcssValueParser('Button(border 1px solid red)').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); + }); + + it('partial mixin annotation parse', () => { + const mixinDecl = createPartialMixinDecl('Button(border 1px solid red)'); + expect(parsePartialMixin(mixinDecl)).to.eql([ + { + type: 'Button', + options: { border: '1px solid red' }, + partial: true, + valueNode: postcssValueParser('Button(border 1px solid red)').nodes[0], + originDecl: mixinDecl, + }, + ]); + }); +}); diff --git a/packages/core/test/helpers/rule.spec.ts b/packages/core/test/helpers/rule.spec.ts index e9da550f9..1b79bc0b3 100644 --- a/packages/core/test/helpers/rule.spec.ts +++ b/packages/core/test/helpers/rule.spec.ts @@ -38,12 +38,17 @@ describe(`helpers/rule`, () => { .i.i{} + /*nested selectors*/ + :not(.i) .i{} + :nth-child(5n - 1 of .i) {} + :nth-child(5n - 2 of .i, .i) {} + :nth-child(5n - 3 of .i, .x, .i) {} + /*extracted as decl on root*/ .i{color: red} /*not extracted*/ .x .i{} - :not(.i) .i{} `), '.i' ); @@ -64,6 +69,10 @@ describe(`helpers/rule`, () => { { selector: '& &.x:hover' }, { selector: '&.y.x' }, { selector: '&&' }, // TODO: check if possible + { selector: ':not(&) &' }, + { selector: ':nth-child(5n - 1 of &)' }, + { selector: ':nth-child(5n - 2 of &, &)' }, + { selector: ':nth-child(5n - 3 of &, .x, &)' }, // ToDo: check if to remove unrelated nested selectors { selector: '&' }, ]; diff --git a/packages/core/test/helpers/selector.spec.ts b/packages/core/test/helpers/selector.spec.ts index 6520e09db..fc103942d 100644 --- a/packages/core/test/helpers/selector.spec.ts +++ b/packages/core/test/helpers/selector.spec.ts @@ -7,78 +7,117 @@ import { describe(`helpers/selector`, () => { describe(`scopeNestedSelector`, () => { - const tests: Array<{ scope: string; nested: string; expected: string; only?: boolean }> = [ + const tests: Array<{ + label: string; + scope: string; + nested: string; + expected: string; + only?: boolean; + }> = [ { + label: '+ no nesting selector', scope: '.a', nested: '.x', expected: '.a .x', }, { + label: '+ complex selector with no nesting selector', scope: '.a', nested: '.x:hover', expected: '.a .x:hover', }, { + label: '+ nesting selector', scope: '.a', nested: '&', expected: '.a', }, { + label: 'compound scope + nesting selector', scope: '.a:hover', nested: '&', expected: '.a:hover', }, { + label: 'compound scope + nesting selector (2)', scope: '.a.x', nested: '&', expected: '.a.x', }, { + label: 'complex scope + nesting selector', scope: '.a.x .b:hover', nested: '&', expected: '.a.x .b:hover', }, { + label: '+ nesting selector with compound class', scope: '.a', nested: '&.x', expected: '.a.x', }, { + label: '+ nesting selector with complex class', scope: '.a', nested: '&.x .y', expected: '.a.x .y', }, { + label: 'complex scope + nesting selector with complex class', scope: '.a .b', nested: '&.x .y', expected: '.a .b.x .y', }, { + label: '+ multiple nesting selector', scope: '.a', nested: '& &', expected: '.a .a', }, { + label: 'multi scopes + multiple nesting selectors', scope: '.a, .b', nested: '& & &', expected: '.a .a .a, .b .b .b', }, { + label: 'multi compound scopes + multi nesting selector', scope: '.a:hover, .b:focus', nested: '& & &', expected: '.a:hover .a:hover .a:hover, .b:focus .b:focus .b:focus', }, { + label: '+ global before', scope: '.a', nested: ':global(.x) &', expected: ':global(.x) .a', }, { + label: '+ nested nesting selector', scope: '.a', nested: ':not(&)', - expected: '.a :not(.a)', + expected: ':not(.a)', }, { + label: 'multi scopes + nested nesting selector', + scope: '.a, .b', + nested: ':not(&)', + expected: ':not(.a), :not(.b)', + }, + { + label: '+ nested deep nesting selector', + scope: '.a', + nested: ':not(&, :not(&))', + expected: ':not(.a, :not(.a))', + }, + { + label: '+ nested nth of nesting selector', + scope: '.a', + nested: ':nth-child(5n+2 of &)', + expected: ':nth-child(5n+2 of .a)', + }, + { + label: 'nesting scope persists', scope: '&', nested: '.no-parent-re-scoping', expected: '& .no-parent-re-scoping', diff --git a/packages/core/test/mixins/css-mixins.spec.ts b/packages/core/test/mixins/css-mixins.spec.ts deleted file mode 100644 index 695132cc8..000000000 --- a/packages/core/test/mixins/css-mixins.spec.ts +++ /dev/null @@ -1,1590 +0,0 @@ -import { expect } from 'chai'; -import type * as postcss from 'postcss'; -import { - generateStylableEnvironment, - generateStylableResult, - generateStylableRoot, - matchAllRulesAndDeclarations, - matchRuleAndDeclaration, - testInlineExpects, - testStylableCore, - shouldReportNoDiagnostics, -} from '@stylable/core-test-kit'; -import { processorWarnings } from '@stylable/core'; - -describe('CSS Mixins', () => { - it('apply simple class mixins declarations', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-mixin { - color: red; - } - /* @check .entry__container {color: red;} */ - .container { - -st-mixin: my-mixin; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('last mixin wins with warning', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-mixin1 { - color: red; - } - .my-mixin2 { - color: green; - } - /* @check .entry__container {color: green;} */ - .container { - -st-mixin: my-mixin1; - -st-mixin: my-mixin2; - } - `, - }, - }, - }); - - const report = result.meta.diagnostics.reports[0]; - expect(report.message).to.equal(processorWarnings.OVERRIDE_MIXIN('-st-mixin')); - testInlineExpects(result.meta.outputAst!); - }); - - it('Mixin with function arguments with multiple params (comma separated)', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./formatter"; - -st-default: formatter; - } - - /* @check .entry__container {color: color-1, color-2} */ - .container { - -st-mixin: Text(ZZZ formatter(color-1, color-2)); - } - - .Text { - color: value(ZZZ); - } - `, - }, - '/formatter.js': { - content: ` - module.exports = function() { - return \`\${[...arguments].join(', ')}\`; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('transform state form imported element', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./design.st.css"; - -st-named: Base; - } - /* @check[1] .entry__y.base--disabled { color: red; } */ - .y { - -st-mixin: Base; - } - `, - }, - '/design.st.css': { - namespace: 'design', - content: ` - :import { - -st-from: "./base.st.css"; - -st-default: Base; - } - Base{} - `, - }, - '/base.st.css': { - namespace: 'base', - content: ` - .root { - -st-states: disabled; - } - .root:disabled { - color: red; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('transform state form extended root when used as mixin', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./design.st.css"; - -st-default: Design; - } - /* @check[1] .entry__y.base--disabled {color: red;} */ - .y { - -st-mixin: Design; - } - `, - }, - '/design.st.css': { - namespace: 'design', - content: ` - :import { - -st-from: "./base.st.css"; - -st-default: Base; - } - .root { - -st-extends: Base; - } - .root:disabled { color: red; } - `, - }, - '/base.st.css': { - namespace: 'base', - content: ` - .root { - -st-states: disabled; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('should reorder selector to context', () => { - const { sheets } = testStylableCore({ - '/mixin.st.css': ` - .root { - -st-states: x; - } - .mixin {-st-states: mix-state;} - .root:x.mixin:mix-state { - z-index: 1; - } - .root:x.mixin:mix-state[attr].y { - z-index: 1; - } - .mixin:is(.y.mixin:mix-state) { - z-index: 1; - } - .x.mixin[a] .y.mixin[b] { - z-index: 1; - } - :is(.x.mixin:is(.y.mixin)) { - z-index: 1; - } - - `, - 'entry.st.css': ` - @st-import [mixin] from "./mixin.st.css"; - - /* - @rule[1] .entry__y.mixin--mix-state.mixin__root.mixin--x - @rule[2] .entry__y.mixin--mix-state[attr].mixin__y.mixin__root.mixin--x - @rule[3] .entry__y:is(.entry__y.mixin--mix-state.mixin__y) - @rule[4] .entry__y[a].mixin__x .entry__y[b].mixin__y - */ - .y { - -st-mixin: mixin; - } - `, - }); - //@TODO-rule[5] :is(.entry__y:is(.entry__y.mixin__y).mixin__x) - shouldReportNoDiagnostics(sheets[`/entry.st.css`].meta); - }); - - it.skip('mixin with multiple rules in keyframes', () => { - // const result = generateStylableRoot({ - // entry: `/entry.st.css`, - // files: { - // '/entry.st.css': { - // namespace: 'entry', - // content: ` - // .x { - // color: red; - // } - // .x:hover { - // color: green; - // } - - // @keyframes my-name { - - // 0% { - // -st-mixin: x; - // } - // 100% { - - // } - - // } - // ` - // } - // } - // }); - - throw new Error('Test me'); - }); - - it('apply simple class mixin that uses mixin itself', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .x { - color: red; - } - .y { - -st-mixin: x; - } - /* @check .entry__container {color: red;} */ - .container { - -st-mixin: y; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('apply simple class mixin with circular refs to the same selector', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - /* @check .entry__x {color: red; color: red;} */ - .x { - color: red; - -st-mixin: y; - } - /* @check .entry__y {color: red;} */ - .y { - -st-mixin: x; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('apply simple class mixin with circular refs from multiple files', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./style1.st.css"; - -st-named: y; - } - /* @check .entry__x {color: red; color: red;} */ - .x { - color: red; - -st-mixin: y; - } - `, - }, - '/style1.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./entry.st.css"; - -st-named: x; - } - .y { - -st-mixin: x; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('append complex selector that starts with the mixin name', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - - .my-mixin:hover { - color: blue; - } - .my-mixin .my-other-class { - color: green; - } - /* - @check[1] .entry__container:hover {color: blue;} - @check[2] .entry__container .entry__my-other-class {color: green;} - */ - .container { - -st-mixin: my-mixin; - } - `, - }, - }, - }); - - testInlineExpects(result); - }); - - it('should scope @keyframes from local mixin without duplicating the animation', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-mixin { - animation: original 2s; - } - @keyframes original { - 0% { color: red; } - 100% { color: green; } - } - .container { - -st-mixin: my-mixin; - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 2, '.entry__container', 'animation: entry__original 2s'); - }); - - it('should scope @keyframes from imported mixin without duplicating the animation', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: my-mixin; - } - .container { - -st-mixin: my-mixin; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .my-mixin { - animation: original 2s; - } - @keyframes original { - 0% { color: red; } - 100% { color: green; } - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', 'animation: imported__original 2s'); - }); - - it('should scope @keyframes from root mixin (duplicate the entire @keyframe with origin context)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-default: Imported; - } - .container { - -st-mixin: Imported; - } - `, - }, - - '/imported.st.css': { - namespace: 'imported', - content: ` - .my-mixin { - animation: original 2s; - } - @keyframes original { - 0% { color: red; } - 100% { color: green; } - } - `, - }, - }, - }); - - matchRuleAndDeclaration( - result, - 1, - '.entry__container .imported__my-mixin', - 'animation: imported__original 2s' - ); - result.walkAtRules(/@keyframes/, (rule) => { - expect(rule.params).to.equal('imported__original'); - }); - }); - - it('apply class mixins from import', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: my-mixin; - } - .container { - -st-mixin: my-mixin; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .my-mixin { - color: red; - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', 'color: red'); - }); - - it('apply mixin from named import (scope classes from mixin origin)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: my-mixin; - } - .container { - -st-mixin: my-mixin; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .my-mixin { - color: red; - } - .my-mixin .local { - color: green; - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', 'color: red'); - - matchRuleAndDeclaration(result, 1, '.entry__container .imported__local', 'color: green'); - }); - - it('separate mixin roots', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin.st.css"; - -st-named: a; - } - .b { -st-mixin: a; } - `, - }, - '/mixin.st.css': { - namespace: 'mixin', - content: ` - - .a { color: green; background: red; } - - .a:hover { color: yellow; } - - .a { color: black; } - - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__b', 'color: green;background: red'); - matchRuleAndDeclaration(result, 1, '.entry__b:hover', 'color: yellow'); - matchRuleAndDeclaration(result, 2, '.entry__b', 'color: black'); - }); - - it('re-exported mixin maintains original definitions', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./enriched.st.css"; - -st-named: a; - } - .b { -st-mixin: a; } - `, - }, - '/enriched.st.css': { - namespace: 'enriched', - content: ` - :import { - -st-from: "./base.st.css"; - -st-named: a; - } - .a { color: green; } - `, - }, - '/base.st.css': { - namespace: 'base', - content: ` - .a { color: red; } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__b', 'color: red'); - matchRuleAndDeclaration(result, 1, '.entry__b', 'color: green'); - }); - - it('re-exported mixin maintains original definitions (with multiple selectors)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./enriched.st.css"; - -st-named: a; - } - .b { -st-mixin: a; } - `, - }, - '/enriched.st.css': { - namespace: 'enriched', - content: ` - :import { - -st-from: "./base.st.css"; - -st-named: a; - } - .a { color: green; } - .a:hover { - color: yellow; - } - .a { color: purple; } - `, - }, - '/base.st.css': { - namespace: 'base', - content: ` - .a { color: red; } - .a:hover { - color: gold; - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__b', 'color: red'); - matchRuleAndDeclaration(result, 1, '.entry__b:hover', 'color: gold'); - matchRuleAndDeclaration(result, 2, '.entry__b', 'color: green'); - matchRuleAndDeclaration(result, 3, '.entry__b:hover', 'color: yellow'); - matchRuleAndDeclaration(result, 4, '.entry__b', 'color: purple'); - }); - - it(`apply mixin from named "as" import to a target class sharing the mixin source name`, () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./base.st.css"; - -st-named: a as b; - } - .a { -st-mixin: b; } - `, - }, - '/base.st.css': { - namespace: 'base', - content: ` - .a { color: red; } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__a', 'color: red'); - }); - - it('apply mixin from local class with extends (scope class as root)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./base.st.css"; - -st-default: Base; - } - - .container { - -st-mixin: my-mixin; - } - - .my-mixin { - -st-extends: Base; - color: red; - } - .my-mixin::part{ - color: green; - } - `, - }, - '/base.st.css': { - namespace: 'base', - content: `.part{}`, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', '-st-extends: Base;color: red'); - - matchRuleAndDeclaration(result, 1, '.entry__container .base__part', 'color: green'); - }); - - it('apply mixin from named import with extends (scope classes from mixin origin)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: my-mixin; - } - .container { - -st-mixin: my-mixin; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :import { - -st-from: "./base.st.css"; - -st-default: Base; - } - .my-mixin { - -st-extends: Base; - color: red; - } - .my-mixin::part{ - color: green; - } - `, - }, - '/base.st.css': { - namespace: 'base', - content: `.part{}`, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', '-st-extends: Base;color: red'); - - matchRuleAndDeclaration(result, 1, '.entry__container .base__part', 'color: green'); - }); - - it('should apply root mixin on child class (Root mixin mode)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - - .container { - -st-mixin: root; - } - - .class { - - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', ''); - - matchRuleAndDeclaration(result, 1, '.entry__container .entry__container', ''); - - matchRuleAndDeclaration(result, 2, '.entry__container .entry__class', ''); - - matchRuleAndDeclaration(result, 3, '.entry__class', ''); - }); - - it('apply mixin from named import with extends (scope classes from mixin origin) !! with alias jump', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./jump.st.css"; - -st-named: my-mixin; - } - .container { - -st-mixin: my-mixin; - } - `, - }, - '/jump.st.css': { - namespace: 'imported', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: my-mixin; - } - .my-mixin {} - .my-mixin::part {} - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :import { - -st-from: "./base.st.css"; - -st-default: Base; - } - .my-mixin { - -st-extends: Base; - color: red; - } - .my-mixin::part{ - color: green; - } - `, - }, - '/base.st.css': { - namespace: 'base', - content: `.part{}`, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', '-st-extends: Base;color: red'); - - matchRuleAndDeclaration(result, 1, '.entry__container .base__part', 'color: green'); - }); - - it('apply mixin with two root replacements', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: i; - } - .x { - -st-mixin: i; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .i .i.y { - color: yellow; - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 1, '.entry__x .entry__x.imported__y', 'color: yellow'); - }); - - it('apply complex mixin on complex selector', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .i { - color: red; - } - - .i:hover, .local:hover, .i.local:hover .inner { - color: green; - } - - .x:hover .y { - -st-mixin: i; - } - `, - }, - }, - }); - - matchAllRulesAndDeclarations( - result, - [ - ['.entry__x:hover .entry__y', 'color: red'], - [ - '.entry__x:hover .entry__y:hover, .entry__x:hover .entry__y.entry__local:hover .entry__inner', - 'color: green', - ], - ], - '', - 2 - ); - }); - - it('apply mixin with media query', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: i; - } - .x { - -st-mixin: i; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .y {background: #000} - .i {color: red;} - @media (max-width: 300px) { - .y {background: #000} - .i {color: yellow;} - .i:hover {color: red;} - } - .i:hover {color: blue;} - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'color: red'); - - const media = result.nodes[1] as postcss.AtRule; - expect(media.params, 'media params').to.equal('(max-width: 300px)'); - - matchAllRulesAndDeclarations( - media, - [ - ['.entry__x', 'color: yellow'], - ['.entry__x:hover', 'color: red'], - ], - '@media' - ); - - matchRuleAndDeclaration(result, 2, '.entry__x:hover', 'color: blue'); - }); - - it('apply mixin with @supports', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: i; - } - .x { - -st-mixin: i; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .y {background: #000} - .i {color: red;} - @supports not (appearance: auto) { - .y {background: #000} - .i {color: yellow;} - .i:hover {color: red;} - } - .i:hover {color: blue;} - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'color: red'); - - const supports = result.nodes[1] as postcss.AtRule; - expect(supports.params, 'supports params').to.equal('not (appearance: auto)'); - - matchAllRulesAndDeclarations( - supports, - [ - ['.entry__x', 'color: yellow'], - ['.entry__x:hover', 'color: red'], - ], - '@supports' - ); - - matchRuleAndDeclaration(result, 2, '.entry__x:hover', 'color: blue'); - }); - - it('apply mixin from root style sheet', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-default: X; - } - - .x { - -st-mixin: X; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .root {color:red;} - .y {color:green;} - @media (max-width: 100px) { - .root{color:yellow;} - .y{color:gold;} - } - @supports not (appearance: auto) { - .i {color:purple;} - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'color:red'); - matchRuleAndDeclaration(result, 1, '.entry__x .imported__y', 'color:green'); - const media = result.nodes[2] as postcss.AtRule; - matchRuleAndDeclaration(media, 0, '.entry__x', 'color:yellow', '@media'); - matchRuleAndDeclaration(media, 1, '.entry__x .imported__y', 'color:gold', '@media'); - const supports = result.nodes[3] as postcss.AtRule; - matchRuleAndDeclaration(supports, 0, '.entry__x .imported__i', 'color:purple', '@supports'); - }); - - it('apply named mixin with extends and conflicting pseudo-element class at mixin deceleration level', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: mixme; - } - .x { - -st-mixin: mixme; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :import { - -st-from: "./comp.st.css"; - -st-default: Comp; - } - .part {} - .mixme { - -st-extends: Comp; - color: red; - } - .mixme::part .part { - color: green; - } - `, - }, - '/comp.st.css': { - namespace: 'comp', - content: ` - .part{} - `, - }, - }, - }); - matchRuleAndDeclaration(result, 1, '.entry__x .comp__part .imported__part', 'color: green'); - }); - - it('apply mixin when rootScoping enabled', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./look1.st.css"; - -st-default: Look1; - } - .root { - -st-mixin: Look1(c1 yellow); - } - `, - }, - '/look1.st.css': { - namespace: 'look1', - content: ` - :import { - -st-from: "./base.st.css"; - -st-default: Base; - } - :vars { - c1: red; - } - .root { - -st-extends:Base; - color:value(c1); - } - .panel { - color:gold; - } - .root::label { - color:green; - } - `, - }, - '/base.st.css': { - namespace: 'base', - content: ` - .root {} - .label {} - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__root', '-st-extends:Base;color:yellow'); - matchRuleAndDeclaration(result, 1, '.entry__root .look1__panel', 'color:gold'); - matchRuleAndDeclaration(result, 2, '.entry__root .base__label', 'color:green'); - }); - - it('apply mixin from imported element', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: X; - } - - .x { - -st-mixin: X; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - X {color:green;} - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'color:green'); - }); - - it('apply nested mixins', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./r.st.css"; - -st-default: R; - } - .x { - -st-mixin: R; - } - `, - }, - '/r.st.css': { - namespace: 'r', - content: ` - :import { - -st-from: "./y.st.css"; - -st-default: Y; - } - .r{ - -st-mixin: Y; - } - `, - }, - '/y.st.css': { - namespace: 'y', - content: ` - .y { - - } - `, - }, - }, - }); - - matchAllRulesAndDeclarations( - result, - [ - ['.entry__x', ''], - ['.entry__x .r__r', ''], - ['.entry__x .r__r .y__y', ''], - ], - '' - ); - }); - - it('should maintain mapped symbols when performing a local mixin (regression)', () => { - const { stylable } = generateStylableEnvironment({ - '/entry.st.css': ` - @st-import Comp from "./inner.st.css"; - - Comp::inner {} - `, - '/inner.st.css': ` - .inner {} - - .mixin { - position: absolute; - width: 100%; - height: 100%; - top: 0px; - left: 0px; - z-index: 1; - } - - .mixTarget { - -st-mixin: mixin; - } - - `, - }); - - const { meta } = stylable.transform(stylable.process('/inner.st.css')); - const { meta: entryMeta } = stylable.transform(stylable.process('/entry.st.css')); - - expect(meta.getAllSymbols()).to.have.keys('root', 'inner', 'mixin', 'mixTarget'); - expect(entryMeta.transformDiagnostics!.reports.length).to.equal(0); - }); - - describe('url() handling', () => { - it('should rewrite relative urls', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./a/mix.st.css"; - -st-named: mix; - } - .x { - -st-mixin: mix; - } - `, - }, - '/a/mix.st.css': { - namespace: 'mix', - content: ` - :import { - -st-from: "./b/other-mix.st.css"; - -st-named: other-mix; - } - .mix { - background: url(./asset.png); - -st-mixin: other-mix; - } - `, - }, - '/a/b/other-mix.st.css': { - namespace: 'other-mix', - content: ` - .other-mix { - background: url(./asset.png) - } - `, - }, - }, - }); - - matchAllRulesAndDeclarations( - result, - [['.entry__x', 'background: url(./a/asset.png);background: url(./a/b/asset.png)']], - '' - ); - }); - it('should rewrite relative urls (case2)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./a/mix.st.css"; - -st-named: mix; - } - .x { - -st-mixin: mix; - } - `, - }, - '/a/mix.st.css': { - namespace: 'mix', - content: ` - .mix { - background: url(../asset.png); - } - `, - }, - }, - }); - - matchAllRulesAndDeclarations( - result, - [['.entry__x', 'background: url(./asset.png)']], - '' - ); - }); - - it('should rewrite relative urls used through a 3rd-party css mixin', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "fake-package/index.st.css"; - -st-named: mix; - } - .x { - -st-mixin: mix; - } - `, - }, - '/node_modules/fake-package/index.st.css': { - namespace: 'mix', - content: ` - .mix { - background: url(./asset.png); - } - `, - }, - '/node_modules/fake-package/package.json': { - content: '{"name": "fake-package", "version": "0.0.1"}', - }, - }, - }); - - matchAllRulesAndDeclarations( - result.meta.outputAst!, - [['.entry__x', 'background: url(./node_modules/fake-package/asset.png)']], - '' - ); - }); - - it('should rewrite relative urls used through a 3rd-party js mixin', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "fake-package/mixin.js"; - -st-named: mix; - } - .x { - -st-mixin: mix(); - } - `, - }, - '/node_modules/fake-package/mixin.js': { - content: ` - module.exports.mix = function() { - return { - "background": 'url(./asset.png)' - }; - } - `, - }, - '/node_modules/fake-package/package.json': { - content: '{"name": "fake-package", "version": "0.0.1"}', - }, - }, - }); - - matchAllRulesAndDeclarations( - result.meta.outputAst!, - [['.entry__x', 'background: url(./node_modules/fake-package/asset.png)']], - '' - ); - }); - }); - - describe('Mixins with named parameters', () => { - it('apply mixin with :vars override (local scope)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - color1: red; - } - - .x { - -st-mixin: y(color1 green); - } - - .y {color:value(color1);} - - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'color:green'); - }); - - it('apply mixin with :vars override with space in value', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - border1: red; - } - - .x { - -st-mixin: y(border1 1px solid red); - } - - .y {border:value(border1);} - - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'border:1px solid red'); - }); - - it('apply mixin with :vars override', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: y; - } - - .x { - -st-mixin: y(color1 green); - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :vars { - color1: red; - } - .y {color:value(color1);} - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'color:green'); - }); - - it('apply mixin with :vars multiple override', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .x { - -st-mixin: y(color1 green, color2 yellow); - } - - .y { - color:value(color1); - background:value(color2); - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__x', 'color:green;background:yellow'); - }); - - it('apply mixin with :vars multiple levels', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: y; - } - - .x { - -st-mixin: y(color1 green, color2 yellow); - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :import { - -st-from: "./mixin.st.css"; - -st-named: z; - } - :vars { - color1: red; - color2: blue; - } - .y { - -st-mixin: z(color3 value(color1), color4 value(color2)); - } - `, - }, - '/mixin.st.css': { - namespace: 'mixin', - content: ` - :vars { - color3: red; - color4: blue; - } - .z { - border: 1px solid value(color3); - background: value(color4); - } - `, - }, - }, - }); - - matchRuleAndDeclaration( - result, - 0, - '.entry__x', - 'border: 1px solid green;background: yellow' - ); - }); - }); -}); diff --git a/packages/core/test/mixins/js-mixins.spec.ts b/packages/core/test/mixins/js-mixins.spec.ts deleted file mode 100644 index 2cd8d3845..000000000 --- a/packages/core/test/mixins/js-mixins.spec.ts +++ /dev/null @@ -1,794 +0,0 @@ -import { expect } from 'chai'; -import type * as postcss from 'postcss'; -import { - generateStylableRoot, - matchAllRulesAndDeclarations, - matchRuleAndDeclaration, -} from '@stylable/core-test-kit'; - -describe('Javascript Mixins', () => { - it('javascript value', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./values"; - -st-named: myValue; - } - .container { - background: value(myValue); - } - `, - }, - '/values.js': { - content: ` - module.exports.myValue = 'red'; - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('background: red'); - }); - - it('javascript value in var definition', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./values"; - -st-named: myValue; - } - :vars { - myCSSValue: value(myValue); - } - .container { - background: value(myCSSValue); - } - `, - }, - '/values.js': { - content: ` - module.exports.myValue = 'red'; - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('background: red'); - }); - - it('javascript value does re-export to css', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./x.st.css"; - -st-named: myValue; - } - .container { - background: value(myValue); - } - `, - }, - '/x.st.css': { - content: ` - :import { - -st-from: "./values"; - -st-named: myValue; - } - `, - }, - '/values.js': { - content: ` - module.exports.myValue = 'red'; - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('background: red'); - }); - - it('simple mixin', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .container { - background: green; - -st-mixin: mixin; - border: 0; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - color: "red" - } - } - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[1].toString()).to.equal('color: red'); - }); - - it('exported js mixin via st.css file', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./index.st.css"; - -st-named: mixin; - } - .container { - -st-mixin: mixin; - } - `, - }, - '/index.st.css': { - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - color: "red" - } - } - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('color: red'); - }); - - it('exported js mixin via st.css file (with params)', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./index.st.css"; - -st-named: mixin; - } - .container { - -st-mixin: mixin(red, green); - } - `, - }, - '/index.st.css': { - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function(params) { - return { - color: params.join(' ') - } - } - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('color: red green'); - }); - - it('simple mixin with element', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - namespace: 'style', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .container { - -st-mixin: mixin; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - Test: { - color: "red" - } - } - } - `, - }, - }, - }); - - const rule = result.nodes[1] as postcss.Rule; - - expect(rule.selector).to.equal('.style__container Test'); - expect(rule.nodes[0].toString()).to.equal('color: red'); - }); - - it('simple mixin with fallback', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - namespace: 'style', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .container { - -st-mixin: mixin; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - color: ["red", "blue"] - } - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - - expect(rule.selector).to.equal('.style__container'); - expect(rule.nodes[0].toString()).to.equal('color: red'); - expect(rule.nodes[1].toString()).to.equal('color: blue'); - }); - - it('simple mixin and remove all -st-mixins', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .container { - -st-mixin: mixin; - -st-mixin: mixin; - -st-mixin: mixin; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - color: "red" - } - } - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('color: red'); - }); - - it('complex mixin', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .container { - -st-mixin: mixin; - -st-mixin: mixin; - -st-mixin: mixin; - } - .containerB { - color: blue; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - color: "red", - ".my-selector": { - color: "green", - "&:hover": { - background: "yellow" - } - }, - "&:hover": { - color: "gold" - } - } - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - expect(rule.selector, 'rule 1 selector').to.equal('.entry__container'); - expect(rule.nodes[0].toString(), 'rule 1 decl').to.equal('color: red'); - - const rule2 = result.nodes[1] as postcss.Rule; - expect(rule2.selector, 'rule 2 selector').to.equal('.entry__container .entry__my-selector'); - expect(rule2.nodes[0].toString(), 'rule 2 decl').to.equal('color: green'); - - const rule3 = result.nodes[2] as postcss.Rule; - expect(rule3.selector, 'rule 3 selector').to.equal( - '.entry__container .entry__my-selector:hover' - ); - expect(rule3.nodes[0].toString(), 'rule 3 decl').to.equal('background: yellow'); - - const rule4 = result.nodes[3] as postcss.Rule; - expect(rule4.selector, 'rule 4 selector').to.equal('.entry__container:hover'); - expect(rule4.nodes[0].toString(), 'rule 4 decl').to.equal('color: gold'); - }); - - it('mixin on multiple selectors', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .containerA,.containerB { - -st-mixin: mixin; - - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - color: "red", - "&:hover": { - color: "green" - } - } - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__containerA,.entry__containerB', 'color: red'); - - matchRuleAndDeclaration( - result, - 1, - '.entry__containerA:hover,.entry__containerB:hover', - 'color: green' - ); - }); - - it('mixin with nested at-rule', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .containerA { - -st-mixin: mixin; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - "@supports not (appearance: auto)": { - "&": { - color: "red" - } - }, - "&": { - color: "green" - } - } - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__containerA', ''); - matchRuleAndDeclaration( - result.nodes[1] as postcss.Container, - 0, - '.entry__containerA', - 'color: red' - ); - matchRuleAndDeclaration(result, 2, '.entry__containerA', 'color: green'); - }); - - it('mixin with multiple selectors', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .containerA { - -st-mixin: mixin; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - "&:hover,.class": { - color: "green" - } - } - } - `, - }, - }, - }); - - matchRuleAndDeclaration( - result, - 1, - '.entry__containerA:hover,.entry__containerA .entry__class', - 'color: green' - ); - }); - - it('mixin with multiple var values', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - :vars { - color1: red; - color2: blue; - } - .container { - -st-mixin: mixin(value(color1), value(color2)); - } - `, - }, - '/mixin.js': { - content: ` - module.exports = function(options) { - return { - color: options[0], - background: options[1] - } - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', 'color: red;background: blue'); - }); - - it('should not root scope js mixins', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import{ - -st-from:'./mixin.js'; - -st-named: mixStuff; - } - .gaga{ - color:red; - -st-mixin: mixStuff; - } - `, - }, - '/mixin.js': { - content: ` - module.exports = { - mixStuff:function(){ - return { - "background":"green", - ".child":{ - "color": "yellow" - } - } - } - }; - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__gaga', 'color:red;background:green'); - matchRuleAndDeclaration(result, 1, '.entry__gaga .entry__child', 'color:yellow'); - }); - - it('multiple mixins', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin1"; - -st-default: mixin1; - } - :import { - -st-from: "./mixin2"; - -st-default: mixin2; - } - .container { - -st-mixin: mixin1(red) mixin2(blue); - } - `, - }, - '/mixin1.js': { - content: ` - module.exports = function(options) { - return { - color: options[0] - } - } - `, - }, - '/mixin2.js': { - content: ` - module.exports = function(options) { - return { - background: options[0] - } - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', 'color: red;background: blue'); - }); - - it('multiple same mixin', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin1"; - -st-default: mixin1; - } - .container-a { - -st-mixin: mixin1(red); - } - .container-b { - -st-mixin: mixin1(blue); - } - `, - }, - '/mixin1.js': { - content: ` - module.exports = function(options) { - return { - color: options[0] - } - } - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container-a', 'color: red'); - - matchRuleAndDeclaration(result, 1, '.entry__container-b', 'color: blue'); - }); - - it('@keyframes mixin', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin"; - -st-default: mixin; - } - .container { - -st-mixin: mixin; - } - @keyframes st-global(conflict) {} - `, - }, - '/mixin.js': { - content: ` - module.exports = function() { - return { - "@keyframes unknown": { - "0%": { "color": "red" }, - "100%": { "color": "green" } - }, - "@keyframes conflict": {}, - "@keyframes st-global(global-name)": {}, - ".x": { - "animation-name": [ - "unknown", - "conflict", - "global-name", - ], - } - } - } - `, - }, - }, - }); - - const { - 0: rule, - 1: unknownKeyframes, - 2: knownKeyframes, - 3: globalKeyframes, - 4: animationDeclRule, - } = result.nodes; - expect((rule as any).nodes.length, 'rule is empty').to.equal(0); - if ( - unknownKeyframes.type !== 'atrule' || - knownKeyframes.type !== 'atrule' || - globalKeyframes.type !== 'atrule' || - animationDeclRule.type !== 'rule' - ) { - throw new Error('expected 3 injected to be the @keyframes'); - } - expect(unknownKeyframes.params, 'new id').to.equal('entry__unknown'); - expect((unknownKeyframes as any).nodes[0].selector, 'first keyframe').to.equal('0%'); - expect((unknownKeyframes as any).nodes[1].selector, 'last keyframe').to.equal('100%'); - expect(knownKeyframes.params, 'existing id').to.equal('conflict'); - expect(globalKeyframes.params, 'global id').to.equal('global-name'); - expect(globalKeyframes.params, 'global id').to.equal('global-name'); - expect( - (animationDeclRule as any).nodes[1].value, - `conflict value - prefer stylesheet` - ).to.equal(`conflict`); - // ToDo: pass with mixin symbols - once mixin symbols are available in transformer - // expect((animationDeclRule as any).nodes[0].value, `unknown value`).to.equal( - // `entry__unknown` - // ); - // expect((animationDeclRule as any).nodes[2].value, `global value`).to.equal(`global-name`); - }); - - describe('url() handling', () => { - it('should rewrite relative urls', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./a/b/mixin1.js"; - -st-default: mix; - } - .x { - -st-mixin: mix; - } - `, - }, - '/a/b/mixin1.js': { - content: ` - module.exports = function(options) { - return { - background: "url(./asset.png)" - } - } - `, - }, - }, - }); - - matchAllRulesAndDeclarations( - result, - [['.entry__x', 'background: url(./a/b/asset.png)']], - '' - ); - }); - it('should rewrite relative urls (case2)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./a/mixin1.js"; - -st-default: mix; - } - .x { - -st-mixin: mix; - } - `, - }, - '/a/mixin1.js': { - content: ` - module.exports = function(options) { - return { - background: "url(../asset.png)" - } - } - `, - }, - }, - }); - - matchAllRulesAndDeclarations( - result, - [['.entry__x', 'background: url(./asset.png)']], - '' - ); - }); - }); -}); diff --git a/packages/core/test/mixins/partial-css-mixins.spec.ts b/packages/core/test/mixins/partial-css-mixins.spec.ts deleted file mode 100644 index ddfd9d60f..000000000 --- a/packages/core/test/mixins/partial-css-mixins.spec.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { expect } from 'chai'; -import { - generateStylableResult, - generateStylableRoot, - matchRuleAndDeclaration, -} from '@stylable/core-test-kit'; -import { processorWarnings } from '@stylable/core'; - -describe('Partial CSS Mixins', () => { - it('should warn on partial mixins with no override arguments', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-mixin { - color: red; - } - .container { - -st-partial-mixin: my-mixin; - } - `, - }, - }, - }); - - const report = result.meta.diagnostics.reports[0]; - expect(report.message).to.equal( - processorWarnings.PARTIAL_MIXIN_MISSING_ARGUMENTS('my-mixin') - ); - matchRuleAndDeclaration( - result.meta.outputAst!, - 1, - '.entry__container', - '', - 'mixin does not apply' - ); - }); - - it('should include any declaration that contains overridden variables', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - - :vars { - c1: red; - c2: blue; - c3: green; - } - - .my-mixin { - color: value(c1); - background: value(c1), value(c2); - background: value(c1), value(c3); - } - .container { - -st-partial-mixin: my-mixin(c1 black, c2 white); - } - `, - }, - }, - }); - - expect(result.meta.diagnostics.reports).to.have.lengthOf(0); - matchRuleAndDeclaration( - result.meta.outputAst!, - 1, - '.entry__container', - 'color: black;background: black, white;background: black, green' - ); - }); - - it('should work with -st-mixin', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - - :vars { - c1: red; - c2: blue; - c3: green; - } - - .my-mixin1 { - z-index: 0; - color: value(c1); - } - - .my-mixin2 { - z-index: 1; - border: 1px solid value(c1); - } - - .container { - -st-mixin: my-mixin1; - -st-partial-mixin: my-mixin2(c1 black); - } - `, - }, - }, - }); - - expect(result.meta.diagnostics.reports).to.have.lengthOf(0); - matchRuleAndDeclaration( - result.meta.outputAst!, - 2, - '.entry__container', - 'z-index: 0;color: red;border: 1px solid black' - ); - }); - - it('should include any rules and declaration that contains overridden variables (local partial mixin)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - myColor: red; - size: 1px; - } - - .container { - -st-partial-mixin: my-mixin(myColor yellow); - } - - .my-mixin { - color: value(myColor); - background: green; - } - .my-mixin .x { - border: value(size) solid value(myColor); - z-index: 0; - } - .my-mixin .y { - border: 1px solid value(myColor); - z-index: 1; - } - .my-mixin .z { - z-index: 2; - } - - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', 'color: yellow'); - matchRuleAndDeclaration( - result, - 1, - '.entry__container .entry__x', - 'border: 1px solid yellow' - ); - matchRuleAndDeclaration( - result, - 2, - '.entry__container .entry__y', - 'border: 1px solid yellow' - ); - // mixin does not change - matchRuleAndDeclaration(result, 3, '.entry__my-mixin', 'color: red;background: green'); - }); - - it('should include any rules and declaration that contains overridden variables (imported partial mixin)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./mixin.st.css"; - -st-named: my-mixin; - } - .container { - -st-partial-mixin: my-mixin(myColor yellow); - } - - `, - }, - '/mixin.st.css': { - namespace: 'imported', - content: ` - :vars { - myColor: red; - size: 1px; - } - - .my-mixin { - color: value(myColor); - background: green; - } - .my-mixin .x { - border: value(size) solid value(myColor); - z-index: 0; - } - .my-mixin .y { - border: 1px solid value(myColor); - z-index: 1; - } - .my-mixin .z { - z-index: 2; - } - - `, - }, - }, - }); - - matchRuleAndDeclaration(result, 0, '.entry__container', 'color: yellow'); - matchRuleAndDeclaration( - result, - 1, - '.entry__container .imported__x', - 'border: 1px solid yellow' - ); - matchRuleAndDeclaration( - result, - 2, - '.entry__container .imported__y', - 'border: 1px solid yellow' - ); - }); - - it('nested partial mixins', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - - :vars { - c1: red; - c2: blue; - c3: green; - c4: purple; - } - - .my-mixin { - color: value(c1); - background: value(c1), value(c2); - background: value(c1), value(c3); - background: value(c4); - } - - .my-mixin2 { - -st-partial-mixin: my-mixin(c3 value(c1)); - background: value(c4); - } - - .container { - -st-partial-mixin: my-mixin2(c4 gold, c1 yellow); - } - - `, - }, - }, - }); - - expect(result.meta.diagnostics.reports).to.have.lengthOf(0); - matchRuleAndDeclaration( - result.meta.outputAst!, - 2, - '.entry__container', - 'background: red, yellow;background: gold' - ); - }); - - it('should follow variable binding and include derived variables', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - - :vars { - c1: red; - c2: value(c1); - c3: value(c2); - } - - .my-mixin { - color: value(c3); - } - - .container { - -st-partial-mixin: my-mixin(c1 green); - } - - `, - }, - }, - }); - - expect(result.meta.diagnostics.reports).to.have.lengthOf(0); - matchRuleAndDeclaration(result.meta.outputAst!, 1, '.entry__container', 'color: green'); - }); -}); diff --git a/packages/core/test/stylable-processor.spec.ts b/packages/core/test/stylable-processor.spec.ts index d88471ac0..a11d3c4ac 100644 --- a/packages/core/test/stylable-processor.spec.ts +++ b/packages/core/test/stylable-processor.spec.ts @@ -1,8 +1,7 @@ import { resolve } from 'path'; import chai, { expect } from 'chai'; import { flatMatch, processSource } from '@stylable/core-test-kit'; -import { processNamespace, processorWarnings, SRule } from '@stylable/core'; -import { ignoreDeprecationWarn } from '@stylable/core/dist/helpers/deprecation'; +import { processNamespace, processorWarnings } from '@stylable/core'; import { knownPseudoClassesWithNestedSelectors } from '@stylable/core/dist/native-reserved-lists'; chai.use(flatMatch); @@ -220,98 +219,6 @@ describe('Stylable postcss process', () => { expect(Object.keys(result.getAllClasses()).length).to.eql(6); }); - it('should collect mixins on rules', () => { - const result = processSource( - ` - .x { - -st-mixin: my-mixin - } - .my-mixin {} - `, - { from: 'path/to/style.css' } - ); - - const mixinRule = result.ast.nodes[0] as SRule; - expect(ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type).to.eql('my-mixin'); - }); - it('should use last mixin deceleration', () => { - const result = processSource( - ` - .x { - -st-mixin: my-mixin1; - -st-mixin: my-mixin2; - } - .my-mixin1 {} - .my-mixin2 {} - `, - { from: 'path/to/style.css' } - ); - - const mixinRule = result.ast.nodes[0] as SRule; - expect(ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type).to.eql('my-mixin2'); - }); - it('should use last mixin deceleration for -st-partial-mixin', () => { - const result = processSource( - ` - .x { - -st-partial-mixin: my-mixin1; - -st-partial-mixin: my-mixin2; - } - .my-mixin1 {} - .my-mixin2 {} - `, - { from: 'path/to/style.css' } - ); - - const mixinRule = result.ast.nodes[0] as SRule; - expect(ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type).to.eql('my-mixin2'); - }); - it('should use mixin deceleration in order for mixed -st-mixin and -st-partial-mixin', () => { - const result = processSource( - ` - .x { - -st-mixin: my-mixin1; - -st-partial-mixin: my-mixin2; - } - .y { - -st-partial-mixin: my-mixin2; - -st-mixin: my-mixin1; - } - .my-mixin1 {} - .my-mixin2 {} - `, - { from: 'path/to/style.css' } - ); - - const mixinRule1 = result.ast.nodes[0] as SRule; - const mixinRule2 = result.ast.nodes[1] as SRule; - expect(ignoreDeprecationWarn(() => mixinRule1.mixins!)[0].mixin.type).to.eql('my-mixin1'); - expect(ignoreDeprecationWarn(() => mixinRule1.mixins!)[1].mixin.type).to.eql('my-mixin2'); - expect(ignoreDeprecationWarn(() => mixinRule2.mixins!)[0].mixin.type).to.eql('my-mixin2'); - expect(ignoreDeprecationWarn(() => mixinRule2.mixins!)[1].mixin.type).to.eql('my-mixin1'); - }); - it('should use mixin last deceleration in order for mixed -st-mixin and -st-partial-mixin with duplicates', () => { - const result = processSource( - ` - .x { - -st-mixin: my-mixin1; - -st-partial-mixin: my-mixin2; - -st-mixin: my-mixin3; - -st-partial-mixin: my-mixin4; - } - .my-mixin1 {} - .my-mixin2 {} - .my-mixin3 {} - .my-mixin4 {} - `, - { from: 'path/to/style.css' } - ); - - const mixinRule = result.ast.nodes[0] as SRule; - expect(ignoreDeprecationWarn(() => mixinRule.mixins!)[0].mixin.type).to.eql('my-mixin3'); - expect(ignoreDeprecationWarn(() => mixinRule.mixins!)[1].mixin.type).to.eql('my-mixin4'); - }); - describe('process assets', () => { it('should collect url assets from :vars', () => { const result = processSource( diff --git a/packages/core/test/stylable-transformer/global.spec.ts b/packages/core/test/stylable-transformer/global.spec.ts index 450fcb42c..fc7ea5af6 100644 --- a/packages/core/test/stylable-transformer/global.spec.ts +++ b/packages/core/test/stylable-transformer/global.spec.ts @@ -1,76 +1,8 @@ -import { generateStylableResult, generateStylableRoot } from '@stylable/core-test-kit'; +import { generateStylableResult } from '@stylable/core-test-kit'; import { expect } from 'chai'; import type * as postcss from 'postcss'; describe('Stylable postcss transform (Global)', () => { - it('should support :global() as mixin', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - namespace: 'style', - content: ` - :import { - -st-from: "./comp.st.css"; - -st-default: Comp; - } - .root { - -st-mixin: Comp; - } - `, - }, - '/comp.st.css': { - namespace: 'comp', - content: ` - :global(.btn) .root {} - `, - }, - }, - }); - - expect((result.nodes[1] as postcss.Rule).selector).to.equal('.btn .style__root'); - }); - - it('should support nested :global() as mixin', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - namespace: 'style', - content: ` - :import { - -st-from: "./mixin.st.css"; - -st-default: Mixin; - } - .root { - -st-mixin: Mixin; - } - `, - }, - '/mixin.st.css': { - namespace: 'mixin', - content: ` - :import { - -st-from: "./comp.st.css"; - -st-default: Comp; - } - .root { - -st-mixin: Comp; - } - `, - }, - '/comp.st.css': { - namespace: 'comp', - content: ` - :global(.btn) .root {} - `, - }, - }, - }); - - expect((result.nodes[1] as postcss.Rule).selector).to.equal('.btn .style__root'); - }); - it('should register to all global classes to "meta.globals"', () => { const { meta } = generateStylableResult({ entry: `/style.st.css`, @@ -78,26 +10,20 @@ describe('Stylable postcss transform (Global)', () => { '/style.st.css': { namespace: 'style', content: ` - :import { - -st-from: "./mixin.st.css"; - -st-named: test, mix; - } + @st-import [test] from './imported.st.css'; .root {} .test {} .x { -st-global: '.a .b'; } :global(.c .d) {} :global(.e) {} - .mixIntoMe { -st-mixin: mix; } `, }, - '/mixin.st.css': { + '/imported.st.css': { namespace: 'mixin', content: ` .test { -st-global: ".global-test"; } - - .mix :global(.global-test2) {} `, }, }, @@ -105,7 +31,6 @@ describe('Stylable postcss transform (Global)', () => { expect(meta.globals).to.eql({ 'global-test': true, - 'global-test2': true, a: true, b: true, c: true, @@ -116,9 +41,5 @@ describe('Stylable postcss transform (Global)', () => { expect((meta.outputAst!.nodes[2] as postcss.Rule).selector).to.equal('.a .b'); expect((meta.outputAst!.nodes[3] as postcss.Rule).selector).to.equal('.c .d'); expect((meta.outputAst!.nodes[4] as postcss.Rule).selector).to.equal('.e'); - expect((meta.outputAst!.nodes[5] as postcss.Rule).selector).to.equal('.style__mixIntoMe'); - expect((meta.outputAst!.nodes[6] as postcss.Rule).selector).to.equal( - '.style__mixIntoMe .global-test2' - ); }); }); diff --git a/packages/core/test/stylable-value-parsers.spec.ts b/packages/core/test/stylable-value-parsers.spec.ts deleted file mode 100644 index 89b3c5ffd..000000000 --- a/packages/core/test/stylable-value-parsers.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { expect } from 'chai'; -import * as postcss from 'postcss'; -import { SBTypesParsers, valueMapping } from '@stylable/core'; -import postcssValueParser from 'postcss-value-parser'; - -const parseMixin = (mixinValue: string) => { - const mix = SBTypesParsers[valueMapping.mixin]( - postcss.decl({ prop: '', value: mixinValue }), - () => 'named' - ); - mix.forEach((m) => { - delete m.originDecl; - }); - return mix; -}; - -const parsePartialMixin = (mixinValue: string) => { - const mix = SBTypesParsers[valueMapping.partialMixin]( - postcss.decl({ prop: valueMapping.partialMixin, value: mixinValue }), - () => 'named' - ); - mix.forEach((m) => { - delete m.originDecl; - }); - return mix; -}; - -describe('stylable-value-parsers', () => { - describe('-st-mixin', () => { - it('named arguments with no params', () => { - expect(parseMixin('Button')).to.eql([ - { type: 'Button', options: {}, valueNode: postcssValueParser('Button').nodes[0] }, - ]); - }); - - it('named arguments with empty params', () => { - expect(parseMixin('Button()')).to.eql([ - { type: 'Button', options: {}, valueNode: postcssValueParser('Button()').nodes[0] }, - ]); - }); - - it('named arguments with one simple param', () => { - expect(parseMixin('Button(color red)')).to.eql([ - { - type: 'Button', - options: { color: 'red' }, - valueNode: postcssValueParser('Button(color red)').nodes[0], - }, - ]); - }); - - it('named arguments with two simple params', () => { - expect(parseMixin('Button(color red, color2 green)')).to.eql([ - { - type: 'Button', - options: { color: 'red', color2: 'green' }, - valueNode: postcssValueParser('Button(color red, color2 green)').nodes[0], - }, - ]); - }); - - it('named arguments with a trailing comma', () => { - expect(parseMixin('Button(color red,)')).to.eql([ - { - type: 'Button', - options: { color: 'red' }, - valueNode: postcssValueParser('Button(color red,)').nodes[0], - }, - ]); - }); - - it('multiple named arguments with a trailing comma', () => { - expect(parseMixin('Button(color red, size 2px,)')).to.eql([ - { - type: 'Button', - options: { color: 'red', size: '2px' }, - valueNode: postcssValueParser('Button(color red, size 2px,)').nodes[0], - }, - ]); - }); - - it('named arguments with one param with spaces', () => { - expect(parseMixin('Button(border 1px solid red)')).to.eql([ - { - type: 'Button', - options: { border: '1px solid red' }, - valueNode: postcssValueParser('Button(border 1px solid red)').nodes[0], - }, - ]); - }); - }); - - it('partial mixin annotation', () => { - expect(parsePartialMixin('Button(border 1px solid red)')).to.eql([ - { - type: 'Button', - options: { border: '1px solid red' }, - partial: true, - valueNode: postcssValueParser('Button(border 1px solid red)').nodes[0], - }, - ]); - }); -});