From fd51163f633d03a37dc47dfacfdb548fbdc5e180 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 21 Mar 2022 10:23:38 +0200 Subject: [PATCH 01/23] refactor: initial feature setup - feature file - moved mixin symbol to feature - initialized feature in meta --- packages/core/src/features/index.ts | 5 ++++- packages/core/src/features/st-mixin.ts | 16 ++++++++++++++++ packages/core/src/features/types.ts | 7 +------ packages/core/src/stylable-meta.ts | 2 ++ 4 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/features/st-mixin.ts diff --git a/packages/core/src/features/index.ts b/packages/core/src/features/index.ts index 553ac64c2..47a58149d 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 } 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-mixin.ts b/packages/core/src/features/st-mixin.ts new file mode 100644 index 000000000..cb1bf23fd --- /dev/null +++ b/packages/core/src/features/st-mixin.ts @@ -0,0 +1,16 @@ +import { createFeature } from './feature'; +import type { ImportSymbol } from './st-import'; +import type { ClassSymbol } from './css-class'; +// ToDo: extract +import type { MixinValue } from '../stylable-value-parsers'; + +export interface RefedMixin { + mixin: MixinValue; + ref: ImportSymbol | ClassSymbol; +} + +export const diagnostics = {}; + +// HOOKS + +export const hooks = createFeature({}); 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/stylable-meta.ts b/packages/core/src/stylable-meta.ts index 67b51c7eb..4033b6d53 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, From 52837092ed49acfa689c894bcbdcd8b85e45b509 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 21 Mar 2022 10:55:34 +0200 Subject: [PATCH 02/23] refactor: moved `MixinValue` type to feature --- packages/core/src/features/index.ts | 2 +- packages/core/src/features/st-mixin.ts | 12 ++++++++++-- packages/core/src/index.ts | 2 +- packages/core/src/stylable-value-parsers.ts | 12 ++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/core/src/features/index.ts b/packages/core/src/features/index.ts index 47a58149d..4770ae154 100644 --- a/packages/core/src/features/index.ts +++ b/packages/core/src/features/index.ts @@ -12,7 +12,7 @@ export * as STVar from './st-var'; export type { VarSymbol } from './st-var'; export * as STMixin from './st-mixin'; -export type { RefedMixin } from './st-mixin'; +export type { RefedMixin, MixinValue } from './st-mixin'; export * as CSSClass from './css-class'; export type { ClassSymbol } from './css-class'; diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index cb1bf23fd..ad7fa53cb 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -1,8 +1,16 @@ import { createFeature } from './feature'; import type { ImportSymbol } from './st-import'; import type { ClassSymbol } from './css-class'; -// ToDo: extract -import type { MixinValue } from '../stylable-value-parsers'; +import type * as postcss from 'postcss'; +import type { FunctionNode, WordNode } from 'postcss-value-parser'; + +export interface MixinValue { + type: string; + options: Array<{ value: string }> | Record; + partial?: boolean; + valueNode?: FunctionNode | WordNode; + originDecl?: postcss.Declaration; +} export interface RefedMixin { mixin: MixinValue; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9c3ee770a..54c04ee3b 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, diff --git a/packages/core/src/stylable-value-parsers.ts b/packages/core/src/stylable-value-parsers.ts index 1f0ade322..6626af046 100644 --- a/packages/core/src/stylable-value-parsers.ts +++ b/packages/core/src/stylable-value-parsers.ts @@ -1,12 +1,12 @@ 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 type { StateParsedValue } from './types'; import type { SelectorNodes } from '@tokey/css-selector-parser'; -import { CSSClass } from './features'; +import { CSSClass, MixinValue } from './features'; export const valueParserWarnings = { VALUE_CANNOT_BE_STRING() { @@ -25,14 +25,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; From 8fcb5d95c384bbf75de5d0ddcd15f5f5706b7c2a Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 21 Mar 2022 13:34:18 +0200 Subject: [PATCH 03/23] refactor: extract processor into feature - move mixin analyze to `analyzeDeclaration` - move parsers from `stylable-value-parsers` to `helpers/mixin` - move `valueMapping` of mixins from `stylable-value-parsers` to feature `MixinType` - remove `valueParserWarning` from core index --- packages/core/src/features/st-mixin.ts | 107 +++++++++++++++++- packages/core/src/helpers/mixin.ts | 61 ++++++++++ packages/core/src/index.ts | 1 - packages/core/src/stylable-processor.ts | 84 +------------- packages/core/src/stylable-value-parsers.ts | 66 ++--------- packages/core/test/diagnostics.spec.ts | 9 +- .../mixin.spec.ts} | 6 +- packages/core/test/mixins/css-mixins.spec.ts | 4 +- .../test/mixins/partial-css-mixins.spec.ts | 4 +- 9 files changed, 189 insertions(+), 153 deletions(-) create mode 100644 packages/core/src/helpers/mixin.ts rename packages/core/test/{stylable-value-parsers.spec.ts => helpers/mixin.spec.ts} (96%) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index ad7fa53cb..f9d9d7a54 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -1,8 +1,17 @@ import { createFeature } from './feature'; +import * as STSymbol from './st-symbol'; import type { ImportSymbol } from './st-import'; import type { ClassSymbol } from './css-class'; +import { + diagnostics as MixinHelperDiagnostics, + parseStMixin, + parseStPartialMixin, +} from '../helpers/mixin'; +import { ignoreDeprecationWarn } from '../helpers/deprecation'; import type * as postcss from 'postcss'; import type { FunctionNode, WordNode } from 'postcss-value-parser'; +// ToDo: deprecate - stop usage +import type { SRule } from '../deprecated/postcss-ast-extension'; export interface MixinValue { type: string; @@ -17,8 +26,102 @@ export interface RefedMixin { ref: ImportSymbol | ClassSymbol; } -export const diagnostics = {}; +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, + 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`; + }, +}; // HOOKS -export const hooks = createFeature({}); +export const hooks = createFeature({ + analyzeDeclaration({ context, decl }) { + const parser = + decl.prop === MixinType.ALL + ? parseStMixin + : decl.prop === MixinType.PARTIAL + ? parseStPartialMixin + : null; + if (!parser) { + return; + } + const rule = decl.parent as SRule; + const { meta } = context; + 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 + */ + parser( + decl, + (type) => { + const symbol = STSymbol.get(meta, type); + return symbol?._kind === 'import' && !symbol.import.from.match(/.css$/) + ? 'args' + : 'named'; + }, + context.diagnostics, + false + ).forEach((mixin) => { + const mixinRefSymbol = STSymbol.get(meta, mixin.type); + if ( + mixinRefSymbol && + (mixinRefSymbol._kind === 'import' || mixinRefSymbol._kind === 'class') + ) { + 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, + }); + } + }); + + 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 === 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) { + 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; + } + }, +}); diff --git a/packages/core/src/helpers/mixin.ts b/packages/core/src/helpers/mixin.ts new file mode 100644 index 000000000..cf4609889 --- /dev/null +++ b/packages/core/src/helpers/mixin.ts @@ -0,0 +1,61 @@ +import type { Diagnostics } from '../diagnostics'; +import { strategies } from './value'; +import type { MixinValue } from '../features'; +import type * as postcss from 'postcss'; +import postcssValueParser from 'postcss-value-parser'; + +export const diagnostics = { + 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 +) { + return parseStMixin(mixinNode, strategy, report).map((mixin) => { + mixin.partial = true; + return mixin; + }); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 54c04ee3b..82c7ed285 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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-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-value-parsers.ts b/packages/core/src/stylable-value-parsers.ts index 6626af046..4419362d1 100644 --- a/packages/core/src/stylable-value-parsers.ts +++ b/packages/core/src/stylable-value-parsers.ts @@ -3,16 +3,11 @@ 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, MixinValue } from './features'; - -export const valueParserWarnings = { - VALUE_CANNOT_BE_STRING() { - return 'value can not be a string (remove quotes?)'; - }, -}; +import { CSSClass } from './features'; export interface MappedStates { [s: string]: StateParsedValue | string | null; @@ -50,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, }; @@ -125,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/diagnostics.spec.ts b/packages/core/test/diagnostics.spec.ts index adbadf3c1..2c05b65ec 100644 --- a/packages/core/test/diagnostics.spec.ts +++ b/packages/core/test/diagnostics.spec.ts @@ -9,11 +9,10 @@ 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 { STImport, CSSClass, CSSType, STVar, STMixin } from '@stylable/core/dist/features'; import { generalDiagnostics } from '@stylable/core/dist/features/diagnostics'; describe('findTestLocations', () => { @@ -349,7 +348,7 @@ describe('diagnostics: warnings and errors', () => { |-st-mixin: $myMixin$|; } `, - [{ message: processorWarnings.UNKNOWN_MIXIN('myMixin'), file: 'main.css' }] + [{ message: STMixin.diagnostics.UNKNOWN_MIXIN('myMixin'), file: 'main.css' }] ); }); @@ -531,7 +530,7 @@ describe('diagnostics: warnings and errors', () => { }, }; expectTransformDiagnostics(config, [ - { message: valueParserWarnings.VALUE_CANNOT_BE_STRING(), file: '/main.css' }, + { message: STMixin.diagnostics.VALUE_CANNOT_BE_STRING(), file: '/main.css' }, ]); }); @@ -650,7 +649,7 @@ describe('diagnostics: warnings and errors', () => { }, }; expectTransformDiagnostics(config, [ - { message: processorWarnings.OVERRIDE_MIXIN('-st-mixin'), file: '/main.css' }, + { message: STMixin.diagnostics.OVERRIDE_MIXIN('-st-mixin'), file: '/main.css' }, ]); }); }); diff --git a/packages/core/test/stylable-value-parsers.spec.ts b/packages/core/test/helpers/mixin.spec.ts similarity index 96% rename from packages/core/test/stylable-value-parsers.spec.ts rename to packages/core/test/helpers/mixin.spec.ts index 89b3c5ffd..3276b7868 100644 --- a/packages/core/test/stylable-value-parsers.spec.ts +++ b/packages/core/test/helpers/mixin.spec.ts @@ -25,8 +25,8 @@ const parsePartialMixin = (mixinValue: string) => { return mix; }; -describe('stylable-value-parsers', () => { - describe('-st-mixin', () => { +describe('helpers/mixin', () => { + describe('-st-mixin parse', () => { it('named arguments with no params', () => { expect(parseMixin('Button')).to.eql([ { type: 'Button', options: {}, valueNode: postcssValueParser('Button').nodes[0] }, @@ -90,7 +90,7 @@ describe('stylable-value-parsers', () => { }); }); - it('partial mixin annotation', () => { + it('partial mixin annotation parse', () => { expect(parsePartialMixin('Button(border 1px solid red)')).to.eql([ { type: 'Button', diff --git a/packages/core/test/mixins/css-mixins.spec.ts b/packages/core/test/mixins/css-mixins.spec.ts index 695132cc8..db4b21b47 100644 --- a/packages/core/test/mixins/css-mixins.spec.ts +++ b/packages/core/test/mixins/css-mixins.spec.ts @@ -10,7 +10,7 @@ import { testStylableCore, shouldReportNoDiagnostics, } from '@stylable/core-test-kit'; -import { processorWarnings } from '@stylable/core'; +import { STMixin } from '@stylable/core/dist/features'; describe('CSS Mixins', () => { it('apply simple class mixins declarations', () => { @@ -59,7 +59,7 @@ describe('CSS Mixins', () => { }); const report = result.meta.diagnostics.reports[0]; - expect(report.message).to.equal(processorWarnings.OVERRIDE_MIXIN('-st-mixin')); + expect(report.message).to.equal(STMixin.diagnostics.OVERRIDE_MIXIN('-st-mixin')); testInlineExpects(result.meta.outputAst!); }); diff --git a/packages/core/test/mixins/partial-css-mixins.spec.ts b/packages/core/test/mixins/partial-css-mixins.spec.ts index ddfd9d60f..e88454514 100644 --- a/packages/core/test/mixins/partial-css-mixins.spec.ts +++ b/packages/core/test/mixins/partial-css-mixins.spec.ts @@ -4,7 +4,7 @@ import { generateStylableRoot, matchRuleAndDeclaration, } from '@stylable/core-test-kit'; -import { processorWarnings } from '@stylable/core'; +import { STMixin } from '@stylable/core/dist/features'; describe('Partial CSS Mixins', () => { it('should warn on partial mixins with no override arguments', () => { @@ -27,7 +27,7 @@ describe('Partial CSS Mixins', () => { const report = result.meta.diagnostics.reports[0]; expect(report.message).to.equal( - processorWarnings.PARTIAL_MIXIN_MISSING_ARGUMENTS('my-mixin') + STMixin.diagnostics.PARTIAL_MIXIN_MISSING_ARGUMENTS('my-mixin') ); matchRuleAndDeclaration( result.meta.outputAst!, From 9b0c4a1031af8eaf73591dace8e5f8c126ea146e Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Tue, 22 Mar 2022 12:27:22 +0200 Subject: [PATCH 04/23] refactor: move `st-import` feature constants to feature --- packages/core/src/features/st-import.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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( From 7ecc0f82ecf66daa941b6165bd066107caba2b16 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Tue, 22 Mar 2022 12:43:56 +0200 Subject: [PATCH 05/23] chore: correct `tsVarOverride` to `stVarOverride` --- packages/core/src/features/st-var.ts | 6 +++--- packages/core/src/functions.ts | 16 ++++++++-------- packages/core/src/stylable-transformer.ts | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/core/src/features/st-var.ts b/packages/core/src/features/st-var.ts index 8fd073c85..e5411b375 100644 --- a/packages/core/src/features/st-var.ts +++ b/packages/core/src/features/st-var.ts @@ -246,7 +246,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); @@ -260,8 +260,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/functions.ts b/packages/core/src/functions.ts index 2866ab3f5..4b2bfd5cd 100644 --- a/packages/core/src/functions.ts +++ b/packages/core/src/functions.ts @@ -27,7 +27,7 @@ export interface EvalValueData { node?: postcss.Node; valueHook?: replaceValueHook; meta: StylableMeta; - tsVarOverride?: Record | null; + stVarOverride?: Record | null; cssVarsMapping?: Record; args?: string[]; } @@ -39,9 +39,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, @@ -53,7 +53,7 @@ export class StylableEvaluator { data.value, data.meta, data.node, - data.tsVarOverride || this.tsVarOverride, + data.stVarOverride || this.stVarOverride, data.valueHook, context.diagnostics, data.passedThrough, @@ -113,7 +113,7 @@ export function processDeclarationValue( cssVarsMapping: Record = {}, args: string[] = [] ): 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) => { @@ -134,7 +134,7 @@ export function processDeclarationValue( node, valueHook, meta, - tsVarOverride: variableOverride, + stVarOverride: variableOverride, cssVarsMapping, args, }, @@ -201,7 +201,7 @@ export function processDeclarationValue( node, valueHook, meta, - tsVarOverride: variableOverride, + stVarOverride: variableOverride, cssVarsMapping, args, }, diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index c9a00874f..7ee9ed4a3 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -167,12 +167,12 @@ export class StylableTransformer { ast: postcss.Root, meta: StylableMeta, metaExports?: StylableExports, - tsVarOverride?: Record, + stVarOverride?: Record, path: string[] = [], mixinTransform = false, topNestClassName = `` ) { - this.evaluator.tsVarOverride = tsVarOverride; + this.evaluator.stVarOverride = stVarOverride; const transformContext = { meta, diagnostics: this.diagnostics, From ccac48a82f45a6dcb66c81f2fb759cb0e638a289 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 28 Mar 2022 09:36:37 +0300 Subject: [PATCH 06/23] refactor: move append mixins in feature --- packages/core/src/features/feature.ts | 10 +- packages/core/src/features/st-mixin.ts | 458 +++++++++++++++++++++- packages/core/src/stylable-mixins.ts | 443 --------------------- packages/core/src/stylable-transformer.ts | 29 +- packages/core/test/diagnostics.spec.ts | 14 +- 5 files changed, 487 insertions(+), 467 deletions(-) delete mode 100644 packages/core/src/stylable-mixins.ts 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/st-mixin.ts b/packages/core/src/features/st-mixin.ts index f9d9d7a54..77b1261f7 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -8,7 +8,7 @@ import { parseStPartialMixin, } from '../helpers/mixin'; import { ignoreDeprecationWarn } from '../helpers/deprecation'; -import type * as postcss from 'postcss'; +import * as postcss from 'postcss'; import type { FunctionNode, WordNode } from 'postcss-value-parser'; // ToDo: deprecate - stop usage import type { SRule } from '../deprecated/postcss-ast-extension'; @@ -124,4 +124,460 @@ export const hooks = createFeature({ rule.mixins = mixins; } }, + transformLastPass({ context, ast, transformer, cssVarsMapping, path }) { + ast.walkRules((rule) => + appendMixins( + context, + transformer, + rule as SRule, + context.meta, + context.evaluator.stVarOverride || {}, + cssVarsMapping, + path + ) + ); + }, }); + +// taken from "src/stylable/mixins" - ToDo: refactor + +import { dirname } from 'path'; +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 type { ElementSymbol } from './css-type'; +import type { FeatureTransformContext } from './feature'; +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'; + +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-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-transformer.ts b/packages/core/src/stylable-transformer.ts index 7ee9ed4a3..1545f5177 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 }; @@ -257,17 +255,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 lastPathParams = { + context: transformContext, + ast, + transformer: this, + cssVarsMapping, + path, + }; + STMixin.hooks.transformLastPass(lastPathParams); + if (!mixinTransform) { + STGlobal.hooks.transformLastPass(lastPathParams); + } if (metaExports) { CSSClass.hooks.transformJSExports({ diff --git a/packages/core/test/diagnostics.spec.ts b/packages/core/test/diagnostics.spec.ts index 2c05b65ec..ce4b3f0a1 100644 --- a/packages/core/test/diagnostics.spec.ts +++ b/packages/core/test/diagnostics.spec.ts @@ -10,7 +10,6 @@ import { transformerWarnings, nativePseudoElements, } from '@stylable/core'; -import { mixinWarnings } from '@stylable/core/dist/stylable-mixins'; import { valueDiagnostics } from '@stylable/core/dist/helpers/value'; import { STImport, CSSClass, CSSType, STVar, STMixin } from '@stylable/core/dist/features'; import { generalDiagnostics } from '@stylable/core/dist/features/diagnostics'; @@ -410,7 +409,10 @@ describe('diagnostics: warnings and errors', () => { skip: true, skipLocationCheck: true, }, - { message: mixinWarnings.UNKNOWN_MIXIN_SYMBOL('my-mixin'), file: '/main.css' }, + { + message: STMixin.mixinWarnings.UNKNOWN_MIXIN_SYMBOL('my-mixin'), + file: '/main.css', + }, ]); }); @@ -435,12 +437,12 @@ describe('diagnostics: warnings and errors', () => { const yPath = [`x from ${mainPath}`, `y from ${mainPath}`]; expectTransformDiagnostics(config, [ { - message: mixinWarnings.CIRCULAR_MIXIN(xPath), + message: STMixin.mixinWarnings.CIRCULAR_MIXIN(xPath), file: '/main.css', skipLocationCheck: true, }, { - message: mixinWarnings.CIRCULAR_MIXIN(yPath), + message: STMixin.mixinWarnings.CIRCULAR_MIXIN(yPath), file: '/main.css', skipLocationCheck: true, }, @@ -473,7 +475,7 @@ describe('diagnostics: warnings and errors', () => { }; expectTransformDiagnostics(config, [ { - message: mixinWarnings.FAILED_TO_APPLY_MIXIN('bug in mixin'), + message: STMixin.mixinWarnings.FAILED_TO_APPLY_MIXIN('bug in mixin'), file: '/main.css', }, ]); @@ -505,7 +507,7 @@ describe('diagnostics: warnings and errors', () => { }; expectTransformDiagnostics(config, [ - { message: mixinWarnings.JS_MIXIN_NOT_A_FUNC(), file: '/main.css' }, + { message: STMixin.mixinWarnings.JS_MIXIN_NOT_A_FUNC(), file: '/main.css' }, ]); }); From 2311fa5ce305c607c9e5a82e1cee6e177221a02d Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 4 Apr 2022 15:38:05 +0300 Subject: [PATCH 07/23] test: refactor mixins tests - add `features/st-mixin.spec` - move, change to new test format in to related feature specs - wrap deprecated usage in `filterPartialMixinDecl` with ignoreDeprecationWarn - remove regression test related to `inheritedMeta` that is no longer used --- packages/core/src/features/st-mixin.ts | 13 +- packages/core/src/helpers/mixin.ts | 3 +- packages/core/test/diagnostics.spec.ts | 268 +-- packages/core/test/features/css-class.spec.ts | 254 +++ .../core/test/features/css-keyframes.spec.ts | 136 ++ packages/core/test/features/css-type.spec.ts | 33 + packages/core/test/features/st-mixin.spec.ts | 1660 +++++++++++++++++ packages/core/test/features/st-var.spec.ts | 16 +- packages/core/test/functions.spec.ts | 15 - packages/core/test/mixins/css-mixins.spec.ts | 1590 ---------------- packages/core/test/mixins/js-mixins.spec.ts | 794 -------- .../test/mixins/partial-css-mixins.spec.ts | 309 --- .../test/stylable-transformer/global.spec.ts | 85 +- 13 files changed, 2110 insertions(+), 3066 deletions(-) create mode 100644 packages/core/test/features/st-mixin.spec.ts delete mode 100644 packages/core/test/mixins/css-mixins.spec.ts delete mode 100644 packages/core/test/mixins/js-mixins.spec.ts delete mode 100644 packages/core/test/mixins/partial-css-mixins.spec.ts diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index 77b1261f7..e18668657 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -33,6 +33,7 @@ export const MixinType = { export const diagnostics = { VALUE_CANNOT_BE_STRING: MixinHelperDiagnostics.VALUE_CANNOT_BE_STRING, + INVALID_NAMED_PARAMS: MixinHelperDiagnostics.INVALID_NAMED_PARAMS, PARTIAL_MIXIN_MISSING_ARGUMENTS(type: string) { return `"${MixinType.PARTIAL}" can only be used with override arguments provided, missing overrides on "${type}"`; }, @@ -527,11 +528,13 @@ function filterPartialMixinDecl( 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); - } + ignoreDeprecationWarn(() => { + if (decl.prop === valueMapping.mixin) { + parent.mixins = parent.mixins!.filter(partialsOnly); + } else if (decl.prop === valueMapping.partialMixin) { + parent.mixins = parent.mixins!.filter(nonPartials); + } + }); } } }); diff --git a/packages/core/src/helpers/mixin.ts b/packages/core/src/helpers/mixin.ts index cf4609889..1aa31b887 100644 --- a/packages/core/src/helpers/mixin.ts +++ b/packages/core/src/helpers/mixin.ts @@ -1,10 +1,11 @@ import type { Diagnostics } from '../diagnostics'; -import { strategies } from './value'; +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?)'; }, diff --git a/packages/core/test/diagnostics.spec.ts b/packages/core/test/diagnostics.spec.ts index ce4b3f0a1..f09c31c55 100644 --- a/packages/core/test/diagnostics.spec.ts +++ b/packages/core/test/diagnostics.spec.ts @@ -10,8 +10,7 @@ import { transformerWarnings, nativePseudoElements, } from '@stylable/core'; -import { valueDiagnostics } from '@stylable/core/dist/helpers/value'; -import { STImport, CSSClass, CSSType, STVar, STMixin } from '@stylable/core/dist/features'; +import { CSSClass, CSSType } from '@stylable/core/dist/features'; import { generalDiagnostics } from '@stylable/core/dist/features/diagnostics'; describe('findTestLocations', () => { @@ -339,249 +338,6 @@ describe('diagnostics: warnings and errors', () => { }); }); - describe('-st-mixin', () => { - it('should return warning for unknown mixin', () => { - expectAnalyzeDiagnostics( - ` - .gaga{ - |-st-mixin: $myMixin$|; - } - `, - [{ message: STMixin.diagnostics.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: STMixin.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: STMixin.mixinWarnings.CIRCULAR_MIXIN(xPath), - file: '/main.css', - skipLocationCheck: true, - }, - { - message: STMixin.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: STMixin.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: STMixin.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: STMixin.diagnostics.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( @@ -634,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: STMixin.diagnostics.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..1e66d9c57 100644 --- a/packages/core/test/features/css-class.spec.ts +++ b/packages/core/test/features/css-class.spec.ts @@ -774,4 +774,258 @@ 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 + // ToDo: fix crash when enrich uses `MixRoot::part` - https://shorturl.at/jsRU4 + 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; + } + /* bug: causes crash: + 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; } + */ + .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 + // ToDo: fix crash when enrich uses `MixRoot::part` - https://shorturl.at/jsRU4 + 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; + } + /* bug: causes crash: + MixRoot::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; } + */ + .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..3076ba87f 100644 --- a/packages/core/test/features/css-keyframes.spec.ts +++ b/packages/core/test/features/css-keyframes.spec.ts @@ -475,4 +475,140 @@ 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.skip(`should not mix @keyframes`, () => { + // ToDo: report mixin of selector !== `&` + testStylableCore(` + .x { + color: green; + } + .x:hover { + color: red; + } + + @keyframes my-name { + /* @rule 0% { + color: green; + bug: ".x:hover mixed-in" + } */ + 0% { + -st-mixin: x; + } + 100% {} + } + `); + }); + }); }); diff --git a/packages/core/test/features/css-type.spec.ts b/packages/core/test/features/css-type.spec.ts index 2f75f3175..466ef52bf 100644 --- a/packages/core/test/features/css-type.spec.ts +++ b/packages/core/test/features/css-type.spec.ts @@ -188,6 +188,39 @@ 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); + }); + }); 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..0e8f54cce --- /dev/null +++ b/packages/core/test/features/st-mixin.spec.ts @@ -0,0 +1,1660 @@ +import chaiSubset from 'chai-subset'; +import { STMixin } from '@stylable/core/dist/features'; +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`, () => { + // ToDo: fix mixin within nested selectors: `:is(.mix)` -> `.entry__root:is(.entry__root)` + const { sheets } = testStylableCore(` + .mix { + id: mix; + } + .mix:hover { + id: mix-hover;; + } + .mix .child { + id: mix-child; + } + :is(.mix) { + propD: is-mix; + } + + /* + @rule[0] .entry__root { id: mix } + @rule[1] .entry__root:hover { id: mix-hover } + @rule[2] .entry__root .entry__child { id: mix-child } + */ + .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 + */ + .y { + -st-mixin: mixin; + } + `, + }); + //@TODO-rule[5] :is(.entry__y:is(.entry__y.mixin__y).mixin__x) + 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.mixinWarnings.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.mixinWarnings.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; + } + `); + }); + 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'; + /* + @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; } + */ + .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.mixinWarnings.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.mixinWarnings.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.mixinWarnings.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.mixinWarnings.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`, () => { + // ToDo: in case of v2 override + 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 { + background: red; + width: 1px; + background: green; + } */ + .a { + -st-mixin: mix; + -st-partial-mixin: mix(color green); + } + + /* @rule(partial before) .entry__a { + background: green; + background: red; + width: 1px; + } */ + .a { + -st-partial-mixin: mix(color green); + -st-mixin: mix; + } + `); + }); + }); + describe(`JavaScript mixin`, () => { + it(`should append mixin declarations`, () => { + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = { + addGreen() { + return { + color: "green" + } + }, + fallbackDecl() { + return { + color: ["blue", "green"] + } + } + } + `, + '/entry.st.css': ` + @st-import [addGreen, fallbackDecl] 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; + } + `, + }); + + 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 append multiple mixins`, () => { + // ToDo: fix ":global(.part)" to transform with mixin root + 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.mixinWarnings.JS_MIXIN_NOT_A_FUNC()} */ + .a { + -st-mixin: notAFunction; + } + + /* @transform-error(mix throw) word(throw) ${STMixin.mixinWarnings.FAILED_TO_APPLY_MIXIN( + `bug in js mix` + )} */ + .a { + -st-mixin: throw; + } + `, + }); + }); + }); + 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); + }); + // ToDo: handle error in formatter + }); + 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`, () => { + // ToDo: fix JS mixin dropping the before nesting + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + // "&": { id: "before" }, + "@media screen": { + "&": { id: "nested" } + }, + "&": { id: "after" }, + } + } + `, + '/entry.st.css': ` + @st-import js-mix from './mixin.js'; + + /* + @skip-rule[1] .entry__a { id: before } + @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`, () => { + // ToDo: fix JS mixin dropping the before nesting + const { sheets } = testStylableCore({ + '/mixin.js': ` + module.exports = function() { + return { + // "&": { id: "before" }, + "@supports (color: green)": { + "&": { id: "nested" } + }, + "&": { id: "after" }, + } + } + `, + '/entry.st.css': ` + @st-import js-mix from './mixin.js'; + + /* + @skip-rule[1] .entry__a { id: before } + @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/mixins/css-mixins.spec.ts b/packages/core/test/mixins/css-mixins.spec.ts deleted file mode 100644 index db4b21b47..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 { STMixin } from '@stylable/core/dist/features'; - -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(STMixin.diagnostics.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 e88454514..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 { STMixin } from '@stylable/core/dist/features'; - -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( - STMixin.diagnostics.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-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' - ); }); }); From 7f56ddf71566552c8e3af2fc029e5505803450a5 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Tue, 5 Apr 2022 12:33:50 +0300 Subject: [PATCH 08/23] fix: crash of imported alias mixin with pseudo class or element --- packages/core/src/stylable-transformer.ts | 11 ++++--- packages/core/test/features/css-class.spec.ts | 30 ++++++++----------- packages/core/test/features/st-mixin.spec.ts | 9 ------ 3 files changed, 18 insertions(+), 32 deletions(-) diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index 39d642cfc..3b5c876ce 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -543,14 +543,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/test/features/css-class.spec.ts b/packages/core/test/features/css-class.spec.ts index 1e66d9c57..25dca3c5d 100644 --- a/packages/core/test/features/css-class.spec.ts +++ b/packages/core/test/features/css-class.spec.ts @@ -826,7 +826,6 @@ describe(`features/css-class`, () => { }); 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 - // ToDo: fix crash when enrich uses `MixRoot::part` - https://shorturl.at/jsRU4 const { sheets } = testStylableCore({ '/base.st.css': ` .root { @@ -850,18 +849,16 @@ describe(`features/css-class`, () => { .root:state { id: extend-root-state; } - + `, '/enrich.st.css': ` @st-import MixRoot, [mix as mixClass] from './extend.st.css'; MixRoot { id: enrich-MixRoot; } - /* bug: causes crash: MixRoot:state { id: enrich-MixRoot-state; } - */ .mixClass { id: enrich-mixClass; } @@ -872,22 +869,23 @@ describe(`features/css-class`, () => { '/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; } + /* + @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[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; @@ -951,7 +949,6 @@ describe(`features/css-class`, () => { }); it(`should mix imported class with pseudo-element`, () => { // ToDo: fix case where extend.st.css has .root between mix rules: https://shorturl.at/cwBMP - // ToDo: fix crash when enrich uses `MixRoot::part` - https://shorturl.at/jsRU4 const { sheets } = testStylableCore({ '/base.st.css': ` .part { @@ -983,11 +980,9 @@ describe(`features/css-class`, () => { MixRoot { id: enrich-MixRoot; } - /* bug: causes crash: - MixRoot::part { + MixRoot::part .part { id: enrich-MixRoot-part; } - */ .mixClass { id: enrich-mixClass; } @@ -1015,6 +1010,7 @@ describe(`features/css-class`, () => { @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; diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 0e8f54cce..e6418a03b 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -591,7 +591,6 @@ describe(`features/st-mixin`, () => { shouldReportNoDiagnostics(meta); }); it(`should append nested partial mixins`, () => { - // ToDo: in case of v2 override const { sheets } = testStylableCore(` :vars { v1: red; @@ -934,7 +933,6 @@ describe(`features/st-mixin`, () => { shouldReportNoDiagnostics(meta); }); it(`should append multiple mixins`, () => { - // ToDo: fix ":global(.part)" to transform with mixin root const { sheets } = testStylableCore({ '/mixin.js': ` module.exports = function(params) { @@ -1133,7 +1131,6 @@ describe(`features/st-mixin`, () => { shouldReportNoDiagnostics(meta); }); - // ToDo: handle error in formatter }); describe(`st-var`, () => { it(`should resolve mixin vars in mixin origin context `, () => { @@ -1476,12 +1473,10 @@ describe(`features/st-mixin`, () => { ); }); it(`should mix @media queries for nested JS mixin`, () => { - // ToDo: fix JS mixin dropping the before nesting const { sheets } = testStylableCore({ '/mixin.js': ` module.exports = function() { return { - // "&": { id: "before" }, "@media screen": { "&": { id: "nested" } }, @@ -1493,7 +1488,6 @@ describe(`features/st-mixin`, () => { @st-import js-mix from './mixin.js'; /* - @skip-rule[1] .entry__a { id: before } @rule[1] screen @rule[2] .entry__a { id: after } */ @@ -1584,12 +1578,10 @@ describe(`features/st-mixin`, () => { ); }); it(`should mix @supports queries for nested JS mixin`, () => { - // ToDo: fix JS mixin dropping the before nesting const { sheets } = testStylableCore({ '/mixin.js': ` module.exports = function() { return { - // "&": { id: "before" }, "@supports (color: green)": { "&": { id: "nested" } }, @@ -1601,7 +1593,6 @@ describe(`features/st-mixin`, () => { @st-import js-mix from './mixin.js'; /* - @skip-rule[1] .entry__a { id: before } @rule[1] (color: green) @rule[2] .entry__a { id: after } */ From 8ef70f795bff49c6f194637559d883e4eb5ac6bc Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 10 Apr 2022 09:46:13 +0300 Subject: [PATCH 09/23] fix: mixin imported with alias --- packages/core/src/features/st-mixin.ts | 162 ++++++------------- packages/core/test/features/css-type.spec.ts | 24 +++ packages/core/test/features/st-mixin.spec.ts | 6 + 3 files changed, 77 insertions(+), 115 deletions(-) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index e18668657..9d7536fd9 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -23,7 +23,7 @@ export interface MixinValue { export interface RefedMixin { mixin: MixinValue; - ref: ImportSymbol | ClassSymbol; + ref: ImportSymbol | ClassSymbol | ElementSymbol; } export const MixinType = { @@ -79,7 +79,9 @@ export const hooks = createFeature({ const mixinRefSymbol = STSymbol.get(meta, mixin.type); if ( mixinRefSymbol && - (mixinRefSymbol._kind === 'import' || mixinRefSymbol._kind === 'class') + (mixinRefSymbol._kind === 'import' || + mixinRefSymbol._kind === 'class' || + mixinRefSymbol._kind === 'element') ) { if (mixin.partial && Object.keys(mixin.options).length === 0) { context.diagnostics.warn( @@ -208,72 +210,55 @@ export function appendMixin( return; } - const local = STSymbol.get(meta, mix.mixin.type); - if (local && (local._kind === 'class' || local._kind === 'element')) { - handleLocalClassMixin( - reParseMixinNamedArgs(mix, rule, context.diagnostics), + const resolvedSymbols = context.getResolvedSymbols(meta); + const symbolName = mix.mixin.type; + const resolvedType = resolvedSymbols.mainNamespace[symbolName]; + if (resolvedType === `class` || resolvedType === `element`) { + const resolveChain = resolvedSymbols[resolvedType][symbolName]; + handleCSSMixin( + resolveChain, transformer, + reParseMixinNamedArgs(mix, rule, context.diagnostics), + rule, meta, - variableOverride, - cssVarsMapping, path, - rule + variableOverride, + cssVarsMapping ); - } 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(), { + 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; } - 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 } - ); - } + // 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, + }); } } @@ -365,7 +350,7 @@ function createMixinRootFromCSSResolve( ); const mixinMeta: StylableMeta = resolvedClass.meta; - const symbolName = isRootMixin ? 'default' : mix.mixin.type; + const symbolName = isRootMixin && resolvedClass.meta !== meta ? 'default' : mix.mixin.type; transformer.transformAst( mixinRoot, @@ -382,7 +367,7 @@ function createMixinRootFromCSSResolve( return mixinRoot; } -function handleImportedCSSMixin( +function handleCSSMixin( resolveChain: CSSResolve[], transformer: StylableTransformer, mix: RefedMixin, @@ -403,8 +388,7 @@ function handleImportedCSSMixin( 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) { + for (let i = 0; i < resolveChain.length; ++i) { const resolved = resolveChain[i]; roots.push( createMixinRootFromCSSResolve( @@ -432,58 +416,6 @@ function handleImportedCSSMixin( } } -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 && diff --git a/packages/core/test/features/css-type.spec.ts b/packages/core/test/features/css-type.spec.ts index 466ef52bf..9c7d990e7 100644 --- a/packages/core/test/features/css-type.spec.ts +++ b/packages/core/test/features/css-type.spec.ts @@ -220,6 +220,30 @@ describe(`features/css-type`, () => { 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`, () => { diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index e6418a03b..47a730137 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -301,11 +301,17 @@ describe(`features/st-mixin`, () => { `, '/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; From bd681073c58f445dbc7815c1ed00c37712973ae4 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 10 Apr 2022 11:56:46 +0300 Subject: [PATCH 10/23] fix: support mixin withing nested selector --- packages/core/src/helpers/rule.ts | 16 ++++++------ packages/core/src/helpers/selector.ts | 26 +++++++++++--------- packages/core/test/features/st-mixin.spec.ts | 6 ++--- packages/core/test/helpers/rule.spec.ts | 11 ++++++++- packages/core/test/helpers/selector.spec.ts | 7 +++++- 5 files changed, 41 insertions(+), 25 deletions(-) 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/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 47a730137..2b68e6682 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -35,7 +35,6 @@ describe(`features/st-mixin`, () => { shouldReportNoDiagnostics(meta); }); it(`should append mixin rules`, () => { - // ToDo: fix mixin within nested selectors: `:is(.mix)` -> `.entry__root:is(.entry__root)` const { sheets } = testStylableCore(` .mix { id: mix; @@ -47,13 +46,14 @@ describe(`features/st-mixin`, () => { id: mix-child; } :is(.mix) { - propD: is-mix; + id: is-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 } */ .root { -st-mixin: mix; @@ -96,13 +96,13 @@ describe(`features/st-mixin`, () => { @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; } `, }); - //@TODO-rule[5] :is(.entry__y:is(.entry__y.mixin__y).mixin__x) shouldReportNoDiagnostics(sheets[`/entry.st.css`].meta); }); it(`should append mixin within a mixin`, () => { 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..ddde42c28 100644 --- a/packages/core/test/helpers/selector.spec.ts +++ b/packages/core/test/helpers/selector.spec.ts @@ -76,7 +76,12 @@ describe(`helpers/selector`, () => { { scope: '.a', nested: ':not(&)', - expected: '.a :not(.a)', + expected: ':not(.a)', + }, + { + scope: '.a', + nested: ':nth-child(5n+2 of &)', + expected: ':nth-child(5n+2 of .a)', }, { scope: '&', From b192762d2511fe18492d18ab65f73672bce9b9aa Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 10 Apr 2022 15:14:55 +0300 Subject: [PATCH 11/23] test: add more tests to selector mixin --- packages/core/test/features/st-mixin.spec.ts | 5 +++ packages/core/test/helpers/selector.spec.ts | 38 ++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 2b68e6682..8c343e1ef 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -48,12 +48,17 @@ describe(`features/st-mixin`, () => { :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; diff --git a/packages/core/test/helpers/selector.spec.ts b/packages/core/test/helpers/selector.spec.ts index ddde42c28..fc103942d 100644 --- a/packages/core/test/helpers/selector.spec.ts +++ b/packages/core/test/helpers/selector.spec.ts @@ -7,83 +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: ':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', From 9d8f66eb355e1d5ac25c785b1dfb3085724b2aae Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 10 Apr 2022 16:40:24 +0300 Subject: [PATCH 12/23] fix: report warning on zero arguments mixin call --- packages/core/src/helpers/value.ts | 2 +- packages/core/test/arguement-parser.spec.ts | 4 +- packages/core/test/features/st-mixin.spec.ts | 44 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) 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/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/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 8c343e1ef..d68863037 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -943,6 +943,50 @@ describe(`features/st-mixin`, () => { 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': ` From ce6cf0041a04079b466964a3c04939c756aeef51 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 11 Apr 2022 11:45:01 +0300 Subject: [PATCH 13/23] fix: filter invalid rule/atrule mix into keyframe --- packages/core/src/features/st-mixin.ts | 9 ++++--- packages/core/src/stylable-utils.ts | 20 ++++++++++---- .../core/test/features/css-keyframes.spec.ts | 26 ++++++++++++++++++- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index 9d7536fd9..8d2d0911a 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -10,6 +10,7 @@ import { import { ignoreDeprecationWarn } from '../helpers/deprecation'; import * as postcss from 'postcss'; import type { FunctionNode, WordNode } from 'postcss-value-parser'; +import { isValidDeclaration, mergeRules, INVALID_MERGE_OF } from '../stylable-utils'; // ToDo: deprecate - stop usage import type { SRule } from '../deprecated/postcss-ast-extension'; @@ -34,6 +35,7 @@ export const MixinType = { 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}"`; }, @@ -156,7 +158,6 @@ 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'; export const mixinWarnings = { @@ -311,7 +312,7 @@ function handleJSMixin( meta.source ); - mergeRules(mixinRoot, rule); + mergeRules(mixinRoot, rule, transformer.diagnostics); } function createMixinRootFromCSSResolve( @@ -408,11 +409,11 @@ function handleCSSMixin( } if (roots.length === 1) { - mergeRules(roots[0], rule); + mergeRules(roots[0], rule, transformer.diagnostics); } else if (roots.length > 1) { const mixinRoot = postcss.root(); roots.forEach((root) => mixinRoot.prepend(...root.nodes)); - mergeRules(mixinRoot, rule); + mergeRules(mixinRoot, rule, transformer.diagnostics); } } diff --git a/packages/core/src/stylable-utils.ts b/packages/core/src/stylable-utils.ts index 7966c4f6c..fa5cfb20c 100644 --- a/packages/core/src/stylable-utils.ts +++ b/packages/core/src/stylable-utils.ts @@ -42,8 +42,13 @@ 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, report?: Diagnostics) { let mixinRoot: postcss.Rule | null | 'NoRoot' = null; + const nestedInKeyframes = isChildOfAtRule(rule, `keyframes`); mixinAst.walkRules((mixinRule: postcss.Rule) => { if (isChildOfAtRule(mixinRule, 'keyframes')) { return; @@ -87,12 +92,17 @@ export function mergeRules(mixinAst: postcss.Root, rule: postcss.Rule) { } else if (node.type === 'decl') { rule.insertBefore(mixinEntry!, 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/test/features/css-keyframes.spec.ts b/packages/core/test/features/css-keyframes.spec.ts index 3076ba87f..65706d945 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'; @@ -588,6 +588,30 @@ describe(`features/css-keyframes`, () => { `, }); }); + 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% {} + } + `); + }); it.skip(`should not mix @keyframes`, () => { // ToDo: report mixin of selector !== `&` testStylableCore(` From 311d93f3f783c08d44ec2fbe0680041f5a1de348 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 11 Apr 2022 15:22:56 +0300 Subject: [PATCH 14/23] fix: catch missing `-st-mixin` decl --- packages/core/src/features/st-mixin.ts | 8 ++++- packages/core/src/stylable-utils.ts | 16 +++++---- packages/core/test/features/st-mixin.spec.ts | 36 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index 8d2d0911a..bdeeca378 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -10,7 +10,12 @@ import { import { ignoreDeprecationWarn } from '../helpers/deprecation'; import * as postcss from 'postcss'; import type { FunctionNode, WordNode } from 'postcss-value-parser'; -import { isValidDeclaration, mergeRules, INVALID_MERGE_OF } from '../stylable-utils'; +import { + isValidDeclaration, + mergeRules, + MISSING_MIXIN_DECL, + INVALID_MERGE_OF, +} from '../stylable-utils'; // ToDo: deprecate - stop usage import type { SRule } from '../deprecated/postcss-ast-extension'; @@ -35,6 +40,7 @@ export const MixinType = { export const diagnostics = { VALUE_CANNOT_BE_STRING: MixinHelperDiagnostics.VALUE_CANNOT_BE_STRING, INVALID_NAMED_PARAMS: MixinHelperDiagnostics.INVALID_NAMED_PARAMS, + MISSING_MIXIN_DECL: MISSING_MIXIN_DECL, 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}"`; diff --git a/packages/core/src/stylable-utils.ts b/packages/core/src/stylable-utils.ts index fa5cfb20c..10527fde3 100644 --- a/packages/core/src/stylable-utils.ts +++ b/packages/core/src/stylable-utils.ts @@ -42,6 +42,9 @@ export function transformMatchesOnRule(rule: postcss.Rule, lineBreak: boolean) { return replaceRuleSelector(rule, { lineBreak }); } +export const MISSING_MIXIN_DECL = () => { + return `-st-mixin is missing and is required for mixin insertion"`; +}; export const INVALID_MERGE_OF = (mergeValue: string) => { return `invalid merge of: \n"${mergeValue}"`; }; @@ -75,22 +78,23 @@ export function mergeRules(mixinAst: postcss.Root, rule: postcss.Rule, report?: if (mixinAst.nodes) { let nextRule: postcss.Rule | postcss.AtRule = rule; - let mixinEntry: postcss.Declaration | null = null; + let mixinDecl: postcss.Declaration | null = null; rule.walkDecls(mixinDeclRegExp, (decl) => { - mixinEntry = decl; + mixinDecl = decl; }); - if (!mixinEntry) { - throw rule.error('missing mixin entry'); + if (!mixinDecl) { + report?.error(rule, MISSING_MIXIN_DECL()); + return; } // 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') { const valid = !nestedInKeyframes; if (valid) { diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index d68863037..0e2d48bdc 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -234,6 +234,42 @@ describe(`features/st-mixin`, () => { } `); }); + it(`should append mixin rules`, () => { + // This case mostly protects from a user programmatically removing + // `-st-mixin` declarations from the modified pre transformed AST. + // The mixin reports that something went wrong and mixins were not applied. + testStylableCore( + ` + .mix { + id: mix; + } + + /* + @transform-error ${STMixin.diagnostics.MISSING_MIXIN_DECL()} + @rule .entry__mixToClass { before: a; after: z; } + */ + .mixToClass { + before: a; + -st-mixin: mix; + after: z; + + } + `, + { + stylableConfig: { + onProcess(meta) { + // remove -st-mixin origin before apply. + // -st-mixin position must be preserved + // in order to mix between declarations + const mixToClass = meta.ast.nodes[2] as postcss.Rule; + const stMixinDecl = mixToClass.nodes[1]; + stMixinDecl.remove(); + return meta; + }, + }, + } + ); + }); describe(`st-import`, () => { it(`should mix imported class`, () => { const { sheets } = testStylableCore({ From 562f275228c987197766cd5249a1cb84e7517c16 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 11 Apr 2022 15:40:05 +0300 Subject: [PATCH 15/23] refactor: deprecate `meta.mixins` like other fields - move get/set to prototype - move initializer to head --- packages/core/src/stylable-meta.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/src/stylable-meta.ts b/packages/core/src/stylable-meta.ts index 4033b6d53..25bf27ee0 100644 --- a/packages/core/src/stylable-meta.ts +++ b/packages/core/src/stylable-meta.ts @@ -69,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 = {}; @@ -83,8 +84,7 @@ export class StylableMeta { const rootSymbol = CSSClass.addClass(context, RESERVED_ROOT_NAME); rootSymbol[valueMapping.root] = true; - setFieldForDeprecation(this, `mixins`, { objectType: `stylableMeta` }); - this.mixins = []; + // setFieldForDeprecation(this, `mixins`, { objectType: `stylableMeta` }); } getSymbol(name: string) { return STSymbol.get(this, name); @@ -151,3 +151,7 @@ setFieldForDeprecation(StylableMeta.prototype, `vars`, { valueOnThis: true, pleaseUse: `meta.getAllStVars() or meta.getStVar(name)`, }); +setFieldForDeprecation(StylableMeta.prototype, `mixins`, { + objectType: `stylableMeta`, + valueOnThis: true, +}); From 517258a7c06705e896c660bd75e61d9e9f32a32f Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 17 Apr 2022 12:43:43 +0300 Subject: [PATCH 16/23] fix: mixin insertion when `-st-mixin` and `-st-partial-mixin` together - no need for missing mixin declaration diagnostic before transform - refactor `SRule.mixins` to not be used internally - refactor collectDeclMixins out of `analyzeDeclaration` and reused it in `transformLastPass` --- .../src/deprecated/postcss-ast-extension.ts | 1 + packages/core/src/features/st-mixin.ts | 255 +++++++++--------- packages/core/src/stylable-utils.ts | 25 +- packages/core/test/features/st-mixin.spec.ts | 22 +- packages/core/test/helpers/mixin.spec.ts | 67 +++-- 5 files changed, 197 insertions(+), 173 deletions(-) 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/st-mixin.ts b/packages/core/src/features/st-mixin.ts index bdeeca378..3ba464cf6 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -1,4 +1,4 @@ -import { createFeature } from './feature'; +import { createFeature, FeatureContext, FeatureTransformContext } from './feature'; import * as STSymbol from './st-symbol'; import type { ImportSymbol } from './st-import'; import type { ClassSymbol } from './css-class'; @@ -10,12 +10,7 @@ import { import { ignoreDeprecationWarn } from '../helpers/deprecation'; import * as postcss from 'postcss'; import type { FunctionNode, WordNode } from 'postcss-value-parser'; -import { - isValidDeclaration, - mergeRules, - MISSING_MIXIN_DECL, - INVALID_MERGE_OF, -} from '../stylable-utils'; +import { isValidDeclaration, mergeRules, INVALID_MERGE_OF } from '../stylable-utils'; // ToDo: deprecate - stop usage import type { SRule } from '../deprecated/postcss-ast-extension'; @@ -24,7 +19,7 @@ export interface MixinValue { options: Array<{ value: string }> | Record; partial?: boolean; valueNode?: FunctionNode | WordNode; - originDecl?: postcss.Declaration; + originDecl: postcss.Declaration; } export interface RefedMixin { @@ -40,7 +35,6 @@ export const MixinType = { export const diagnostics = { VALUE_CANNOT_BE_STRING: MixinHelperDiagnostics.VALUE_CANNOT_BE_STRING, INVALID_NAMED_PARAMS: MixinHelperDiagnostics.INVALID_NAMED_PARAMS, - MISSING_MIXIN_DECL: MISSING_MIXIN_DECL, 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}"`; @@ -57,83 +51,14 @@ export const diagnostics = { export const hooks = createFeature({ analyzeDeclaration({ context, decl }) { - const parser = - decl.prop === MixinType.ALL - ? parseStMixin - : decl.prop === MixinType.PARTIAL - ? parseStPartialMixin - : null; - if (!parser) { - return; - } - const rule = decl.parent as SRule; - const { meta } = context; - 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 - */ - parser( - decl, - (type) => { - const symbol = STSymbol.get(meta, type); - return symbol?._kind === 'import' && !symbol.import.from.match(/.css$/) - ? 'args' - : 'named'; - }, - context.diagnostics, - false - ).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, - }); + ignoreDeprecationWarn(() => { + const parentRule = decl.parent as SRule; + const prevMixins = ignoreDeprecationWarn(() => parentRule?.mixins || []); + const mixins = collectDeclMixins(context, decl, prevMixins); + if (mixins.length) { + parentRule.mixins = mixins; } }); - - 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 === 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) { - 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; - } }, transformLastPass({ context, ast, transformer, cssVarsMapping, path }) { ast.walkRules((rule) => @@ -159,12 +84,12 @@ import { cssObjectToAst } from '../parser'; import { fixRelativeUrls } from '../stylable-assets'; import type { StylableMeta } from '../stylable-meta'; import type { ElementSymbol } from './css-type'; -import type { FeatureTransformContext } from './feature'; +import type {} from './feature'; import type { CSSResolve } from '../stylable-resolver'; import type { StylableTransformer } from '../stylable-transformer'; import { createSubsetAst } from '../helpers/rule'; import { strategies } from '../helpers/value'; -import { valueMapping, mixinDeclRegExp } from '../stylable-value-parsers'; +import { valueMapping } from '../stylable-value-parsers'; export const mixinWarnings = { FAILED_TO_APPLY_MIXIN(error: string) { @@ -190,17 +115,123 @@ export function appendMixins( cssVarsMapping: Record, path: string[] = [] ) { - const mixins = ignoreDeprecationWarn(() => rule.mixins); + const [decls, mixins] = collectRuleMixins(context, rule); if (!mixins || mixins.length === 0) { return; } - mixins.forEach((mix) => { - appendMixin(context, mix, transformer, rule, meta, variableOverride, cssVarsMapping, path); + for (const mixin of mixins) { + appendMixin( + context, + mixin, + transformer, + rule, + meta, + variableOverride, + cssVarsMapping, + path + ); + } + mixins.length = 0; // ToDo: remove + for (const mixinDecl of decls) { + mixinDecl.remove(); + } +} + +function collectRuleMixins( + context: FeatureTransformContext, + rule: postcss.Rule +): [decls: postcss.Declaration[], mixins: RefedMixin[]] { + let mixins: RefedMixin[] = []; + const decls: postcss.Declaration[] = []; + rule.walkDecls((decl) => { + if (decl.prop === `-st-mixin` || decl.prop === `-st-partial-mixin`) { + decls.push(decl); + mixins = collectDeclMixins(context, decl, mixins); + } }); - mixins.length = 0; - rule.walkDecls(mixinDeclRegExp, (node) => { - node.remove(); + return [decls, mixins]; +} + +function collectDeclMixins( + context: FeatureContext, + decl: postcss.Declaration, + 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; + } + + /** + * This functionality is broken we don't know what strategy to choose here. + * Should be fixed when we refactor to the new flow + */ + parser( + decl, + (type) => { + const symbol = STSymbol.get(meta, type); + return symbol?._kind === 'import' && !symbol.import.from.match(/.css$/) + ? 'args' + : 'named'; + }, + context.diagnostics, + false + ).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; } export function appendMixin( @@ -225,7 +256,7 @@ export function appendMixin( handleCSSMixin( resolveChain, transformer, - reParseMixinNamedArgs(mix, rule, context.diagnostics), + reParseMixinNamedArgs(mix, context.diagnostics), rule, meta, path, @@ -239,7 +270,7 @@ export function appendMixin( try { handleJSMixin( transformer, - reParseMixinArgs(mix, rule, context.diagnostics), + reParseMixinArgs(mix, context.diagnostics), resolvedMixin.symbol, meta, rule, @@ -318,7 +349,7 @@ function handleJSMixin( meta.source ); - mergeRules(mixinRoot, rule, transformer.diagnostics); + mergeRules(mixinRoot, rule, mix.mixin.originDecl, transformer.diagnostics); } function createMixinRootFromCSSResolve( @@ -415,11 +446,11 @@ function handleCSSMixin( } if (roots.length === 1) { - mergeRules(roots[0], rule, transformer.diagnostics); + mergeRules(roots[0], rule, mix.mixin.originDecl, transformer.diagnostics); } else if (roots.length > 1) { const mixinRoot = postcss.root(); roots.forEach((root) => mixinRoot.prepend(...root.nodes)); - mergeRules(mixinRoot, rule, transformer.diagnostics); + mergeRules(mixinRoot, rule, mix.mixin.originDecl, transformer.diagnostics); } } @@ -434,12 +465,6 @@ function getMixinDeclaration(rule: postcss.Rule): postcss.Declaration | undefine }) 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( @@ -466,29 +491,17 @@ function filterPartialMixinDecl( decl.remove(); if (parent?.nodes?.length === 0) { parent.remove(); - } else if (parent) { - ignoreDeprecationWarn(() => { - 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 { +function reParseMixinNamedArgs(mix: RefedMixin, 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); + diagnostics.warn(mix.mixin.originDecl, message, options); }) : (mix.mixin.options as Record) || {}; @@ -501,15 +514,11 @@ function reParseMixinNamedArgs( }; } -function reParseMixinArgs( - mix: RefedMixin, - rule: postcss.Rule, - diagnostics: Diagnostics -): RefedMixin { +function reParseMixinArgs(mix: RefedMixin, 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); + diagnostics.warn(mix.mixin.originDecl, message, options); }) : Array.isArray(mix.mixin.options) ? (mix.mixin.options as { value: string }[]) diff --git a/packages/core/src/stylable-utils.ts b/packages/core/src/stylable-utils.ts index 10527fde3..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,14 +42,16 @@ export function transformMatchesOnRule(rule: postcss.Rule, lineBreak: boolean) { return replaceRuleSelector(rule, { lineBreak }); } -export const MISSING_MIXIN_DECL = () => { - return `-st-mixin is missing and is required for mixin insertion"`; -}; 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, report?: Diagnostics) { +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) => { @@ -78,23 +80,14 @@ export function mergeRules(mixinAst: postcss.Root, rule: postcss.Rule, report?: if (mixinAst.nodes) { let nextRule: postcss.Rule | postcss.AtRule = rule; - let mixinDecl: postcss.Declaration | null = null; - - rule.walkDecls(mixinDeclRegExp, (decl) => { - mixinDecl = decl; - }); - if (!mixinDecl) { - report?.error(rule, MISSING_MIXIN_DECL()); - return; - } // TODO: handle rules before and after decl on entry mixinAst.nodes.slice().forEach((node) => { if (node === mixinRoot) { node.walkDecls((node) => { - rule.insertBefore(mixinDecl!, node); + rule.insertBefore(mixinDecl, node); }); } else if (node.type === 'decl') { - rule.insertBefore(mixinDecl!, node); + rule.insertBefore(mixinDecl, node); } else if (node.type === 'rule' || node.type === 'atrule') { const valid = !nestedInKeyframes; if (valid) { diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 0e2d48bdc..57fd090d2 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -234,10 +234,7 @@ describe(`features/st-mixin`, () => { } `); }); - it(`should append mixin rules`, () => { - // This case mostly protects from a user programmatically removing - // `-st-mixin` declarations from the modified pre transformed AST. - // The mixin reports that something went wrong and mixins were not applied. + it(`should not mix mixin that is removed before transform`, () => { testStylableCore( ` .mix { @@ -245,7 +242,6 @@ describe(`features/st-mixin`, () => { } /* - @transform-error ${STMixin.diagnostics.MISSING_MIXIN_DECL()} @rule .entry__mixToClass { before: a; after: z; } */ .mixToClass { @@ -258,9 +254,7 @@ describe(`features/st-mixin`, () => { { stylableConfig: { onProcess(meta) { - // remove -st-mixin origin before apply. - // -st-mixin position must be preserved - // in order to mix between declarations + // remove -st-mixin origin before apply mixin. const mixToClass = meta.ast.nodes[2] as postcss.Rule; const stMixinDecl = mixToClass.nodes[1]; stMixinDecl.remove(); @@ -751,23 +745,35 @@ describe(`features/st-mixin`, () => { } /* @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; } `); }); diff --git a/packages/core/test/helpers/mixin.spec.ts b/packages/core/test/helpers/mixin.spec.ts index 3276b7868..56e26c56a 100644 --- a/packages/core/test/helpers/mixin.spec.ts +++ b/packages/core/test/helpers/mixin.spec.ts @@ -3,100 +3,115 @@ 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; - }); +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 = (mixinValue: string) => { - const mix = SBTypesParsers[valueMapping.partialMixin]( - postcss.decl({ prop: valueMapping.partialMixin, value: mixinValue }), - () => 'named' - ); - mix.forEach((m) => { - delete m.originDecl; - }); +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', () => { - expect(parseMixin('Button')).to.eql([ - { type: 'Button', options: {}, valueNode: postcssValueParser('Button').nodes[0] }, + 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', () => { - expect(parseMixin('Button()')).to.eql([ - { type: 'Button', options: {}, valueNode: postcssValueParser('Button()').nodes[0] }, + 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', () => { - expect(parseMixin('Button(color red)')).to.eql([ + 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', () => { - expect(parseMixin('Button(color red, color2 green)')).to.eql([ + 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', () => { - expect(parseMixin('Button(color red,)')).to.eql([ + 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', () => { - expect(parseMixin('Button(color red, size 2px,)')).to.eql([ + 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', () => { - expect(parseMixin('Button(border 1px solid red)')).to.eql([ + 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', () => { - expect(parsePartialMixin('Button(border 1px solid red)')).to.eql([ + 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, }, ]); }); From f19fce4784ae642dc986c3ad962fb754c12441df Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 17 Apr 2022 13:08:50 +0300 Subject: [PATCH 17/23] test: moved `SRule` process tests in feature spec --- packages/core/test/features/st-mixin.spec.ts | 144 ++++++++++++++++++ packages/core/test/stylable-processor.spec.ts | 95 +----------- 2 files changed, 145 insertions(+), 94 deletions(-) diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 57fd090d2..5f0cd8504 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -1,5 +1,7 @@ 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, @@ -264,6 +266,148 @@ describe(`features/st-mixin`, () => { } ); }); + 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({ 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( From b98b5163a8b021139bfd005bf6d38fda604cd18e Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 17 Apr 2022 15:03:34 +0300 Subject: [PATCH 18/23] refactor: remove mixin params re-parse - mixin is parsed against the resolved mixin symbol, so params signature is known --- packages/core/src/features/st-mixin.ts | 93 ++++++-------------- packages/core/src/helpers/mixin.ts | 5 +- packages/core/test/features/st-mixin.spec.ts | 31 +++++++ 3 files changed, 63 insertions(+), 66 deletions(-) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index 3ba464cf6..e341a6412 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -54,7 +54,18 @@ export const hooks = createFeature({ ignoreDeprecationWarn(() => { const parentRule = decl.parent as SRule; const prevMixins = ignoreDeprecationWarn(() => parentRule?.mixins || []); - const mixins = collectDeclMixins(context, decl, prevMixins); + 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; } @@ -75,6 +86,8 @@ export const hooks = createFeature({ }, }); +// API + // taken from "src/stylable/mixins" - ToDo: refactor import { dirname } from 'path'; @@ -88,7 +101,6 @@ import type {} from './feature'; import type { CSSResolve } from '../stylable-resolver'; import type { StylableTransformer } from '../stylable-transformer'; import { createSubsetAst } from '../helpers/rule'; -import { strategies } from '../helpers/value'; import { valueMapping } from '../stylable-value-parsers'; export const mixinWarnings = { @@ -142,11 +154,20 @@ function collectRuleMixins( 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, mixins); + mixins = collectDeclMixins( + context, + decl, + (mixinSymbolName) => { + return mainNamespace[mixinSymbolName] === 'js' ? 'args' : 'named'; + }, + true /* report param signature diagnostics */, + mixins + ); } }); return [decls, mixins]; @@ -155,6 +176,8 @@ function collectRuleMixins( function collectDeclMixins( context: FeatureContext, decl: postcss.Declaration, + paramSignature: (mixinSymbolName: string) => 'named' | 'args', + emitStrategyDiagnostics: boolean, previousMixins?: RefedMixin[] ): RefedMixin[] { const { meta } = context; @@ -169,21 +192,7 @@ function collectDeclMixins( return previousMixins || mixins; } - /** - * This functionality is broken we don't know what strategy to choose here. - * Should be fixed when we refactor to the new flow - */ - parser( - decl, - (type) => { - const symbol = STSymbol.get(meta, type); - return symbol?._kind === 'import' && !symbol.import.from.match(/.css$/) - ? 'args' - : 'named'; - }, - context.diagnostics, - false - ).forEach((mixin) => { + parser(decl, paramSignature, context.diagnostics, emitStrategyDiagnostics).forEach((mixin) => { const mixinRefSymbol = STSymbol.get(meta, mixin.type); if ( mixinRefSymbol && @@ -256,7 +265,7 @@ export function appendMixin( handleCSSMixin( resolveChain, transformer, - reParseMixinNamedArgs(mix, context.diagnostics), + mix, rule, meta, path, @@ -268,14 +277,7 @@ export function appendMixin( const resolvedMixin = resolvedSymbols.js[symbolName]; if (typeof resolvedMixin.symbol === 'function') { try { - handleJSMixin( - transformer, - reParseMixinArgs(mix, context.diagnostics), - resolvedMixin.symbol, - meta, - rule, - variableOverride - ); + handleJSMixin(transformer, mix, resolvedMixin.symbol, meta, rule, variableOverride); } catch (e) { context.diagnostics.error(rule, mixinWarnings.FAILED_TO_APPLY_MIXIN(String(e)), { word: mix.mixin.type, @@ -495,40 +497,3 @@ function filterPartialMixinDecl( } }); } - -/** this is a workaround for parsing the mixin args too early */ -function reParseMixinNamedArgs(mix: RefedMixin, diagnostics: Diagnostics): RefedMixin { - const options = - mix.mixin.valueNode?.type === 'function' - ? strategies.named(mix.mixin.valueNode, (message, options) => { - diagnostics.warn(mix.mixin.originDecl, message, options); - }) - : (mix.mixin.options as Record) || {}; - - return { - ...mix, - mixin: { - ...mix.mixin, - options, - }, - }; -} - -function reParseMixinArgs(mix: RefedMixin, diagnostics: Diagnostics): RefedMixin { - const options = - mix.mixin.valueNode?.type === 'function' - ? strategies.args(mix.mixin.valueNode, (message, options) => { - diagnostics.warn(mix.mixin.originDecl, 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/helpers/mixin.ts b/packages/core/src/helpers/mixin.ts index 1aa31b887..7ea1a5e2f 100644 --- a/packages/core/src/helpers/mixin.ts +++ b/packages/core/src/helpers/mixin.ts @@ -53,9 +53,10 @@ export function parseStMixin( export function parseStPartialMixin( mixinNode: postcss.Declaration, strategy: (type: string) => 'named' | 'args', - report?: Diagnostics + report?: Diagnostics, + emitStrategyDiagnostics?: boolean ) { - return parseStMixin(mixinNode, strategy, report).map((mixin) => { + return parseStMixin(mixinNode, strategy, report, emitStrategyDiagnostics).map((mixin) => { mixin.partial = true; return mixin; }); diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 5f0cd8504..b3e95480b 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -1256,6 +1256,37 @@ describe(`features/st-mixin`, () => { `, }); }); + 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`, () => { From cfddb6503f91271b45c0c34082bcc6347cc400e6 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 18 Apr 2022 10:15:36 +0300 Subject: [PATCH 19/23] refactor: minimize internal mixin API signatures - remove unneeded `SRule` usage --- packages/core/src/features/st-mixin.ts | 219 ++++++++-------------- packages/core/src/stylable-transformer.ts | 4 + 2 files changed, 80 insertions(+), 143 deletions(-) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index e341a6412..0c2d1511f 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -72,17 +72,7 @@ export const hooks = createFeature({ }); }, transformLastPass({ context, ast, transformer, cssVarsMapping, path }) { - ast.walkRules((rule) => - appendMixins( - context, - transformer, - rule as SRule, - context.meta, - context.evaluator.stVarOverride || {}, - cssVarsMapping, - path - ) - ); + ast.walkRules((rule) => appendMixins(context, transformer, rule, cssVarsMapping, path)); }, }); @@ -91,7 +81,6 @@ export const hooks = createFeature({ // taken from "src/stylable/mixins" - ToDo: refactor import { dirname } from 'path'; -import type { Diagnostics } from '../diagnostics'; import { resolveArgumentsValue } from '../functions'; import { cssObjectToAst } from '../parser'; import { fixRelativeUrls } from '../stylable-assets'; @@ -121,10 +110,8 @@ export const mixinWarnings = { export function appendMixins( context: FeatureTransformContext, transformer: StylableTransformer, - rule: SRule, - meta: StylableMeta, - variableOverride: Record, - cssVarsMapping: Record, + rule: postcss.Rule, + cssPropertyMapping: Record, path: string[] = [] ) { const [decls, mixins] = collectRuleMixins(context, rule); @@ -132,16 +119,7 @@ export function appendMixins( return; } for (const mixin of mixins) { - appendMixin( - context, - mixin, - transformer, - rule, - meta, - variableOverride, - cssVarsMapping, - path - ); + appendMixin(context, { transformer, mixDef: mixin, rule, path, cssPropertyMapping }); } mixins.length = 0; // ToDo: remove for (const mixinDecl of decls) { @@ -243,78 +221,65 @@ function collectDeclMixins( return mixins; } -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)) { +interface ApplyMixinConfig { + transformer: StylableTransformer; + mixDef: RefedMixin; + rule: postcss.Rule; + path: string[]; + cssPropertyMapping: Record; +} + +export function appendMixin(context: FeatureTransformContext, config: ApplyMixinConfig) { + if (checkRecursive(context, config)) { return; } - - const resolvedSymbols = context.getResolvedSymbols(meta); - const symbolName = mix.mixin.type; + 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( - resolveChain, - transformer, - mix, - rule, - meta, - path, - variableOverride, - cssVarsMapping - ); + handleCSSMixin(context, config, resolveChain); return; } else if (resolvedType === `js`) { const resolvedMixin = resolvedSymbols.js[symbolName]; if (typeof resolvedMixin.symbol === 'function') { try { - handleJSMixin(transformer, mix, resolvedMixin.symbol, meta, rule, variableOverride); + handleJSMixin(context, config, resolvedMixin.symbol); } catch (e) { - context.diagnostics.error(rule, mixinWarnings.FAILED_TO_APPLY_MIXIN(String(e)), { - word: mix.mixin.type, - }); + context.diagnostics.error( + config.rule, + mixinWarnings.FAILED_TO_APPLY_MIXIN(String(e)), + { + word: config.mixDef.mixin.type, + } + ); return; } } else { - context.diagnostics.error(rule, mixinWarnings.JS_MIXIN_NOT_A_FUNC(), { - word: mix.mixin.type, + context.diagnostics.error(config.rule, mixinWarnings.JS_MIXIN_NOT_A_FUNC(), { + word: config.mixDef.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, - }); - } + const mixinDecl = config.mixDef.mixin.originDecl; + 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[] + { meta, diagnostics }: FeatureTransformContext, + { mixDef, path, rule }: ApplyMixinConfig ) { const symbolName = - mix.ref.name === meta.root - ? mix.ref._kind === 'class' + mixDef.ref.name === meta.root + ? mixDef.ref._kind === 'class' ? meta.root : 'default' - : mix.mixin.type; + : mixDef.mixin.type; const isRecursive = path.includes(symbolName + ' from ' + meta.source); if (isRecursive) { // Todo: add test verifying word @@ -327,14 +292,14 @@ function checkRecursive( } function handleJSMixin( - transformer: StylableTransformer, - mix: RefedMixin, - mixinFunction: (...args: any[]) => any, - meta: StylableMeta, - rule: postcss.Rule, - variableOverride?: Record + context: FeatureTransformContext, + config: ApplyMixinConfig, + mixinFunction: (...args: any[]) => any ) { - const res = mixinFunction((mix.mixin.options as any[]).map((v) => v.value)); + 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) => { @@ -343,27 +308,25 @@ function handleJSMixin( } }); - transformer.transformAst(mixinRoot, meta, undefined, variableOverride, [], true); - const mixinPath = (mix.ref as ImportSymbol).import.request; + config.transformer.transformAst(mixinRoot, meta, undefined, stVarOverride, [], true); + const mixinPath = (mixDef.ref as ImportSymbol).import.request; fixRelativeUrls( mixinRoot, - transformer.resolver.resolvePath(dirname(meta.source), mixinPath), + context.resolver.resolvePath(dirname(meta.source), mixinPath), meta.source ); - mergeRules(mixinRoot, rule, mix.mixin.originDecl, transformer.diagnostics); + mergeRules(mixinRoot, config.rule, mixDef.mixin.originDecl, context.diagnostics); } function createMixinRootFromCSSResolve( - transformer: StylableTransformer, - mix: RefedMixin, - meta: StylableMeta, - resolvedClass: CSSResolve, - path: string[], - decl: postcss.Declaration, - variableOverride: Record, - cssVarsMapping: Record + context: FeatureTransformContext, + config: ApplyMixinConfig, + 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, @@ -372,32 +335,32 @@ function createMixinRootFromCSSResolve( isRootMixin ); - const namedArgs = mix.mixin.options as Record; + const namedArgs = mixDef.mixin.options as Record; - if (mix.mixin.partial) { + if (mixDef.mixin.partial) { filterPartialMixinDecl(meta, mixinRoot, Object.keys(namedArgs)); } const resolvedArgs = resolveArgumentsValue( namedArgs, - transformer, - meta, - transformer.diagnostics, - decl, - variableOverride, - path, - cssVarsMapping + 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' : mix.mixin.type; + const symbolName = isRootMixin && resolvedClass.meta !== meta ? 'default' : mixDef.mixin.type; - transformer.transformAst( + config.transformer.transformAst( mixinRoot, mixinMeta, undefined, resolvedArgs, - path.concat(symbolName + ' from ' + meta.source), + config.path.concat(symbolName + ' from ' + meta.source), true, resolvedClass.symbol.name ); @@ -408,66 +371,36 @@ function createMixinRootFromCSSResolve( } function handleCSSMixin( - resolveChain: CSSResolve[], - transformer: StylableTransformer, - mix: RefedMixin, - rule: postcss.Rule, - meta: StylableMeta, - path: string[], - variableOverride: Record, - cssVarsMapping: Record + context: FeatureTransformContext, + config: ApplyMixinConfig, + resolveChain: CSSResolve[] ) { - const isPartial = mix.mixin.partial; - const namedArgs = mix.mixin.options as Record; + 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 mixinDecl = getMixinDeclaration(rule) || postcss.decl(); - const roots = []; - for (let i = 0; i < resolveChain.length; ++i) { - const resolved = resolveChain[i]; - roots.push( - createMixinRootFromCSSResolve( - transformer, - mix, - meta, - resolved, - path, - mixinDecl, - variableOverride, - cssVarsMapping - ) - ); + for (const resolved of resolveChain) { + roots.push(createMixinRootFromCSSResolve(context, config, resolved)); if (resolved.symbol[valueMapping.extends]) { break; } } if (roots.length === 1) { - mergeRules(roots[0], rule, mix.mixin.originDecl, transformer.diagnostics); + 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, rule, mix.mixin.originDecl, transformer.diagnostics); + mergeRules(mixinRoot, config.rule, mixDef.mixin.originDecl, config.transformer.diagnostics); } } -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) - ); -} - /** we assume that mixinRoot is freshly created nodes from the ast */ function filterPartialMixinDecl( meta: StylableMeta, @@ -489,7 +422,7 @@ function filterPartialMixinDecl( mixinRoot.walkDecls((decl) => { if (!decl.value.match(regexp)) { - const parent = decl.parent as SRule; // ref the parent before remove + const parent = decl.parent; decl.remove(); if (parent?.nodes?.length === 0) { parent.remove(); diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index 3b5c876ce..19e814a35 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -170,6 +170,7 @@ export class StylableTransformer { mixinTransform = false, topNestClassName = `` ) { + const prevStVarOverride = this.evaluator.stVarOverride; this.evaluator.stVarOverride = stVarOverride; const transformContext = { meta, @@ -287,6 +288,9 @@ export class StylableTransformer { resolved: cssVarsMapping, }); } + + // restore evaluator state + this.evaluator.stVarOverride = prevStVarOverride; } /** @deprecated */ public getScopedCSSVar( From 5e3054a85d1b0a7d4514aef24213e0d8165d0049 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 18 Apr 2022 10:27:13 +0300 Subject: [PATCH 20/23] refactor: organize `st-mixin` feature code --- packages/core/src/features/st-mixin.ts | 122 +++++++++---------- packages/core/test/features/st-mixin.spec.ts | 16 +-- 2 files changed, 65 insertions(+), 73 deletions(-) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index 0c2d1511f..b3d6a2f89 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -1,16 +1,25 @@ 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'; @@ -45,6 +54,18 @@ export const diagnostics = { 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 @@ -78,35 +99,6 @@ export const hooks = createFeature({ // API -// taken from "src/stylable/mixins" - ToDo: refactor - -import { dirname } from 'path'; -import { resolveArgumentsValue } from '../functions'; -import { cssObjectToAst } from '../parser'; -import { fixRelativeUrls } from '../stylable-assets'; -import type { StylableMeta } from '../stylable-meta'; -import type { ElementSymbol } from './css-type'; -import type {} from './feature'; -import type { CSSResolve } from '../stylable-resolver'; -import type { StylableTransformer } from '../stylable-transformer'; -import { createSubsetAst } from '../helpers/rule'; -import { valueMapping } from '../stylable-value-parsers'; - -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, @@ -248,7 +240,7 @@ export function appendMixin(context: FeatureTransformContext, config: ApplyMixin } catch (e) { context.diagnostics.error( config.rule, - mixinWarnings.FAILED_TO_APPLY_MIXIN(String(e)), + diagnostics.FAILED_TO_APPLY_MIXIN(String(e)), { word: config.mixDef.mixin.type, } @@ -256,7 +248,7 @@ export function appendMixin(context: FeatureTransformContext, config: ApplyMixin return; } } else { - context.diagnostics.error(config.rule, mixinWarnings.JS_MIXIN_NOT_A_FUNC(), { + context.diagnostics.error(config.rule, diagnostics.JS_MIXIN_NOT_A_FUNC(), { word: config.mixDef.mixin.type, }); } @@ -265,13 +257,13 @@ export function appendMixin(context: FeatureTransformContext, config: ApplyMixin // ToDo: report on unsupported mixed in symbol type const mixinDecl = config.mixDef.mixin.originDecl; - context.diagnostics.error(mixinDecl, mixinWarnings.UNKNOWN_MIXIN_SYMBOL(mixinDecl.value), { + context.diagnostics.error(mixinDecl, diagnostics.UNKNOWN_MIXIN_SYMBOL(mixinDecl.value), { word: mixinDecl.value, }); } function checkRecursive( - { meta, diagnostics }: FeatureTransformContext, + { meta, diagnostics: report }: FeatureTransformContext, { mixDef, path, rule }: ApplyMixinConfig ) { const symbolName = @@ -283,7 +275,7 @@ function checkRecursive( const isRecursive = path.includes(symbolName + ' from ' + meta.source); if (isRecursive) { // Todo: add test verifying word - diagnostics.warn(rule, mixinWarnings.CIRCULAR_MIXIN(path), { + report.warn(rule, diagnostics.CIRCULAR_MIXIN(path), { word: symbolName, }); return true; @@ -319,6 +311,37 @@ function handleJSMixin( mergeRules(mixinRoot, config.rule, mixDef.mixin.originDecl, context.diagnostics); } +function handleCSSMixin( + context: FeatureTransformContext, + config: ApplyMixinConfig, + 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: ApplyMixinConfig, @@ -370,37 +393,6 @@ function createMixinRootFromCSSResolve( return mixinRoot; } -function handleCSSMixin( - context: FeatureTransformContext, - config: ApplyMixinConfig, - 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[valueMapping.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); - } -} - /** we assume that mixinRoot is freshly created nodes from the ast */ function filterPartialMixinDecl( meta: StylableMeta, diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index b3e95480b..4f7e8d3e9 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -134,7 +134,7 @@ describe(`features/st-mixin`, () => { it(`should handle circular mixins`, () => { testStylableCore(` /* - @transform-warn(a) ${STMixin.mixinWarnings.CIRCULAR_MIXIN([ + @transform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ `b from /entry.st.css`, `a from /entry.st.css`, ])} @@ -149,7 +149,7 @@ describe(`features/st-mixin`, () => { } /* - @transform-warn(a) ${STMixin.mixinWarnings.CIRCULAR_MIXIN([ + @transform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ `a from /entry.st.css`, `b from /entry.st.css`, ])} @@ -534,7 +534,7 @@ describe(`features/st-mixin`, () => { '/sheet1.st.css': ` @st-import [b] from './sheet2.st.css'; /* - @xtransform-warn(a) ${STMixin.mixinWarnings.CIRCULAR_MIXIN([ + @xtransform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ `b from /sheet2.st.css`, `a from /sheet1.st.css`, ])} @@ -551,7 +551,7 @@ describe(`features/st-mixin`, () => { '/sheet2.st.css': ` @st-import [a] from './sheet1.st.css'; /* - @xtransform-warn(a) ${STMixin.mixinWarnings.CIRCULAR_MIXIN([ + @xtransform-warn(a) ${STMixin.diagnostics.CIRCULAR_MIXIN([ `a from /sheet1.st.css`, `b from /sheet2.st.css`, ])} @@ -577,7 +577,7 @@ describe(`features/st-mixin`, () => { } .a { - /* @transform-error ${STMixin.mixinWarnings.UNKNOWN_MIXIN_SYMBOL( + /* @transform-error ${STMixin.diagnostics.UNKNOWN_MIXIN_SYMBOL( `unresolved` )} */ -st-mixin: unresolved; @@ -616,7 +616,7 @@ describe(`features/st-mixin`, () => { it(`should report on circular mixin when mixed on local class`, () => { testStylableCore(` /* - @transform-warn ${STMixin.mixinWarnings.CIRCULAR_MIXIN([`root from /entry.st.css`])} + @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 {} @@ -1242,12 +1242,12 @@ describe(`features/st-mixin`, () => { '/entry.st.css': ` @st-import [notAFunction, throw] from './mixins.js'; - /* @transform-error(not a function) word(notAFunction) ${STMixin.mixinWarnings.JS_MIXIN_NOT_A_FUNC()} */ + /* @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.mixinWarnings.FAILED_TO_APPLY_MIXIN( + /* @transform-error(mix throw) word(throw) ${STMixin.diagnostics.FAILED_TO_APPLY_MIXIN( `bug in js mix` )} */ .a { From 8d72611e4d81c4baab953b03f03cf20bb8b7a708 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Mon, 18 Apr 2022 13:40:14 +0300 Subject: [PATCH 21/23] chore: typo and comment --- packages/core/src/stylable-meta.ts | 2 -- packages/core/src/stylable-transformer.ts | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/src/stylable-meta.ts b/packages/core/src/stylable-meta.ts index 25bf27ee0..492f1c533 100644 --- a/packages/core/src/stylable-meta.ts +++ b/packages/core/src/stylable-meta.ts @@ -83,8 +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` }); } getSymbol(name: string) { return STSymbol.get(this, name); diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index 19e814a35..e6ea7750e 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -258,16 +258,16 @@ export class StylableTransformer { this.addDevRules(meta); } - const lastPathParams = { + const lastPassParams = { context: transformContext, ast, transformer: this, cssVarsMapping, path, }; - STMixin.hooks.transformLastPass(lastPathParams); + STMixin.hooks.transformLastPass(lastPassParams); if (!mixinTransform) { - STGlobal.hooks.transformLastPass(lastPathParams); + STGlobal.hooks.transformLastPass(lastPassParams); } if (metaExports) { From 45cce7424992f55163062456e37b5fafc8404b23 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 24 Apr 2022 11:42:24 +0300 Subject: [PATCH 22/23] test: add checks for JS mixin delcs --- packages/core/test/features/st-mixin.spec.ts | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 4f7e8d3e9..c3296a8ac 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -936,11 +936,26 @@ describe(`features/st-mixin`, () => { 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] from './mixin.js'; + @st-import [addGreen, fallbackDecl, camelToKebab, notAStringDecl] from './mixin.js'; /* @rule(single) .entry__root { before: val; @@ -964,6 +979,23 @@ describe(`features/st-mixin`, () => { -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; + } `, }); From 152b0dcddac76d6af220af2a64837895125a58a4 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 24 Apr 2022 11:59:50 +0300 Subject: [PATCH 23/23] chore: fix review renames / removes --- packages/core/src/features/st-mixin.ts | 15 ++++++------- .../core/test/features/css-keyframes.spec.ts | 22 ------------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index b3d6a2f89..97119b00c 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -74,7 +74,7 @@ export const hooks = createFeature({ analyzeDeclaration({ context, decl }) { ignoreDeprecationWarn(() => { const parentRule = decl.parent as SRule; - const prevMixins = ignoreDeprecationWarn(() => parentRule?.mixins || []); + const prevMixins = parentRule?.mixins || []; const mixins = collectDeclMixins( context, decl, @@ -113,7 +113,6 @@ export function appendMixins( for (const mixin of mixins) { appendMixin(context, { transformer, mixDef: mixin, rule, path, cssPropertyMapping }); } - mixins.length = 0; // ToDo: remove for (const mixinDecl of decls) { mixinDecl.remove(); } @@ -213,7 +212,7 @@ function collectDeclMixins( return mixins; } -interface ApplyMixinConfig { +interface ApplyMixinContext { transformer: StylableTransformer; mixDef: RefedMixin; rule: postcss.Rule; @@ -221,7 +220,7 @@ interface ApplyMixinConfig { cssPropertyMapping: Record; } -export function appendMixin(context: FeatureTransformContext, config: ApplyMixinConfig) { +export function appendMixin(context: FeatureTransformContext, config: ApplyMixinContext) { if (checkRecursive(context, config)) { return; } @@ -264,7 +263,7 @@ export function appendMixin(context: FeatureTransformContext, config: ApplyMixin function checkRecursive( { meta, diagnostics: report }: FeatureTransformContext, - { mixDef, path, rule }: ApplyMixinConfig + { mixDef, path, rule }: ApplyMixinContext ) { const symbolName = mixDef.ref.name === meta.root @@ -285,7 +284,7 @@ function checkRecursive( function handleJSMixin( context: FeatureTransformContext, - config: ApplyMixinConfig, + config: ApplyMixinContext, mixinFunction: (...args: any[]) => any ) { const stVarOverride = context.evaluator.stVarOverride || {}; @@ -313,7 +312,7 @@ function handleJSMixin( function handleCSSMixin( context: FeatureTransformContext, - config: ApplyMixinConfig, + config: ApplyMixinContext, resolveChain: CSSResolve[] ) { const mixDef = config.mixDef; @@ -344,7 +343,7 @@ function handleCSSMixin( function createMixinRootFromCSSResolve( context: FeatureTransformContext, - config: ApplyMixinConfig, + config: ApplyMixinContext, resolvedClass: CSSResolve ) { const stVarOverride = context.evaluator.stVarOverride || {}; diff --git a/packages/core/test/features/css-keyframes.spec.ts b/packages/core/test/features/css-keyframes.spec.ts index 65706d945..fb5cd14af 100644 --- a/packages/core/test/features/css-keyframes.spec.ts +++ b/packages/core/test/features/css-keyframes.spec.ts @@ -612,27 +612,5 @@ describe(`features/css-keyframes`, () => { } `); }); - it.skip(`should not mix @keyframes`, () => { - // ToDo: report mixin of selector !== `&` - testStylableCore(` - .x { - color: green; - } - .x:hover { - color: red; - } - - @keyframes my-name { - /* @rule 0% { - color: green; - bug: ".x:hover mixed-in" - } */ - 0% { - -st-mixin: x; - } - 100% {} - } - `); - }); }); });