diff --git a/packages/cli/src/base-generator.ts b/packages/cli/src/base-generator.ts index 79e22594c..e3f17a117 100644 --- a/packages/cli/src/base-generator.ts +++ b/packages/cli/src/base-generator.ts @@ -104,10 +104,13 @@ export function reExportsAllSymbols(filePath: string, generator: IndexGenerator) acc[className] = `${rootExport}__${className}`; return acc; }, {}); - const stVars = meta.vars.reduce>((acc, { name }) => { - acc[name] = `${rootExport}__${name}`; - return acc; - }, {}); + const stVars = Object.values(meta.getAllStVars()).reduce>( + (acc, { name }) => { + acc[name] = `${rootExport}__${name}`; + return acc; + }, + {} + ); const vars = Object.keys(STSymbol.getAllByType(meta, `cssVar`)).reduce>( (acc, varName) => { acc[varName] = `--${rootExport}__${varName.slice(2)}`; diff --git a/packages/cli/test/build.spec.ts b/packages/cli/test/build.spec.ts index f4b9460b7..32e4b8fe3 100644 --- a/packages/cli/test/build.spec.ts +++ b/packages/cli/test/build.spec.ts @@ -1,9 +1,9 @@ import { expect } from 'chai'; -import { Stylable, functionWarnings, processorWarnings, murmurhash3_32_gc } from '@stylable/core'; +import { Stylable, processorWarnings, murmurhash3_32_gc } from '@stylable/core'; import { build } from '@stylable/cli'; import { createMemoryFs } from '@file-services/memory'; import { DiagnosticsManager } from '@stylable/cli/dist/diagnostics-manager'; -import { STImport } from '@stylable/core/dist/features'; +import { STImport, STVar } from '@stylable/core/dist/features'; const log = () => { /**/ @@ -187,7 +187,7 @@ describe('build stand alone', () => { expect(messages[1].message).to.contain( STImport.diagnostics.UNKNOWN_IMPORTED_FILE('./missing-file.st.css') ); - expect(messages[2].message).to.contain(functionWarnings.UNKNOWN_VAR('missingVar')); + expect(messages[2].message).to.contain(STVar.diagnostics.UNKNOWN_VAR('missingVar')); }); it('should optimize css (remove empty nodes, remove stylable-directives, remove comments)', async () => { diff --git a/packages/cli/test/cli.spec.ts b/packages/cli/test/cli.spec.ts index d598454b5..982885b39 100644 --- a/packages/cli/test/cli.spec.ts +++ b/packages/cli/test/cli.spec.ts @@ -4,8 +4,7 @@ import { createTempDirectory, ITempDirectory } from 'create-temp-directory'; import { evalStylableModule } from '@stylable/module-utils/dist/test/test-kit'; import { resolveNamespace } from '@stylable/node'; import { loadDirSync, populateDirectorySync, runCliSync } from '@stylable/e2e-test-kit'; -import { processorWarnings } from '@stylable/core'; -import { STImport } from '@stylable/core/dist/features'; +import { STImport, STVar } from '@stylable/core/dist/features'; describe('Stylable Cli', function () { this.timeout(25000); @@ -368,7 +367,7 @@ describe('Stylable Cli', function () { expect(stdout, 'stdout').to.match(/style\.st\.css/); expect(stdout, 'stdout').to.match( new RegExp( - `\\[info\\]: ${processorWarnings.DEPRECATED_ST_FUNCTION_NAME( + `\\[info\\]: ${STVar.diagnostics.DEPRECATED_ST_FUNCTION_NAME( 'stArray', 'st-array' )}` diff --git a/packages/cli/test/config-projects.spec.ts b/packages/cli/test/config-projects.spec.ts index 0629c6468..8e6c2623d 100644 --- a/packages/cli/test/config-projects.spec.ts +++ b/packages/cli/test/config-projects.spec.ts @@ -7,7 +7,7 @@ import { populateDirectorySync, runCliSync, } from '@stylable/e2e-test-kit'; -import { functionWarnings } from '@stylable/core'; +import { STVar } from '@stylable/core/dist/features'; describe('Stylable CLI config multiple projects', function () { this.timeout(25000); @@ -471,12 +471,12 @@ describe('Stylable CLI config multiple projects', function () { const firstError = stdout.indexOf('error 3'); const secondError = stdout.indexOf('error 2'); - const thirdError = stdout.indexOf(functionWarnings.UNKNOWN_VAR('unknown')); + const thirdError = stdout.indexOf(STVar.diagnostics.UNKNOWN_VAR('unknown')); expect(firstError, 'sorted by location') .to.be.lessThan(secondError) .and.lessThan(thirdError); - expect(stdout.match(functionWarnings.UNKNOWN_VAR('unknown'))?.length).to.eql(1); + expect(stdout.match(STVar.diagnostics.UNKNOWN_VAR('unknown'))?.length).to.eql(1); }); it('should throw when the property "projects" is invalid', () => { diff --git a/packages/core-test-kit/src/inline-expectation.ts b/packages/core-test-kit/src/inline-expectation.ts index 880f8764e..551d4085b 100644 --- a/packages/core-test-kit/src/inline-expectation.ts +++ b/packages/core-test-kit/src/inline-expectation.ts @@ -66,14 +66,13 @@ export function testInlineExpects(result: postcss.Root | Context, expectedTestIn ? { meta: { outputAst: result, - rawAst: null as unknown as StylableMeta['rawAst'], + rawAst: result, diagnostics: null as unknown as StylableMeta['diagnostics'], transformDiagnostics: null as unknown as StylableMeta['transformDiagnostics'], }, } : result; - // ToDo: support analyze mode - const rootAst = context.meta.outputAst!; + const rootAst = context.meta.rawAst; const expectedTestAmount = expectedTestInput ?? (rootAst.toString().match(new RegExp(`${testScopesRegex()}`, `gm`))?.length || 0); @@ -82,10 +81,10 @@ export function testInlineExpects(result: postcss.Root | Context, expectedTestIn // collect checks rootAst.walkComments((comment) => { const input = comment.text.split(/@/gm); - const testCommentTarget = comment; - const testCommentSrc = isDeprecatedInput + const testCommentSrc = comment; + const testCommentTarget = isDeprecatedInput ? comment - : getSourceComment(context.meta, comment) || comment; + : getTargetComment(context.meta, comment) || comment; const nodeTarget = testCommentTarget.next() as AST; const nodeSrc = testCommentSrc.next() as AST; const isRemoved = isRemovedFromTarget(nodeTarget, nodeSrc); @@ -423,14 +422,17 @@ function diagnosticTest( return result; } -function getSourceComment(meta: Context['meta'], { source }: postcss.Comment) { +function getTargetComment(meta: Context['meta'], { source }: postcss.Comment) { let match: postcss.Comment | undefined = undefined; - meta.rawAst.walkComments((srcComment) => { + if (!meta.outputAst) { + return; + } + meta.outputAst.walkComments((outputComment) => { if ( - srcComment.source?.start?.offset === source?.start?.offset && - srcComment.source?.end?.offset === source?.end?.offset + outputComment.source?.start?.offset === source?.start?.offset && + outputComment.source?.end?.offset === source?.end?.offset ) { - match = srcComment; + match = outputComment; return false; } return; diff --git a/packages/core/src/features/css-custom-property.ts b/packages/core/src/features/css-custom-property.ts index 5dfc7e29c..a64265e9d 100644 --- a/packages/core/src/features/css-custom-property.ts +++ b/packages/core/src/features/css-custom-property.ts @@ -171,12 +171,12 @@ export const hooks = createFeature<{ transformDeclaration({ decl, resolved }) { decl.prop = resolved[decl.prop] || decl.prop; }, - transformDeclarationValue({ node, resolved }) { + transformValue({ node, data: { cssVarsMapping } }) { const { value } = node; const varWithPrefix = node.nodes[0]?.value || ``; if (isCSSVarProp(varWithPrefix)) { - if (resolved && resolved[varWithPrefix]) { - node.nodes[0].value = resolved[varWithPrefix]; + if (cssVarsMapping && cssVarsMapping[varWithPrefix]) { + node.nodes[0].value = cssVarsMapping[varWithPrefix]; } } // handle default values - ToDo: check if required diff --git a/packages/core/src/features/feature.ts b/packages/core/src/features/feature.ts index cc78afe09..6d51d89ae 100644 --- a/packages/core/src/features/feature.ts +++ b/packages/core/src/features/feature.ts @@ -1,6 +1,7 @@ import type { StylableMeta } from '../stylable-meta'; import type { ScopeContext, StylableExports } from '../stylable-transformer'; import type { StylableResolver } from '../stylable-resolver'; +import type { StylableEvaluator, EvalValueData } from '../functions'; import type * as postcss from 'postcss'; import type { ImmutableSelectorNode } from '@tokey/css-selector-parser'; import type { Diagnostics } from '../diagnostics'; @@ -18,6 +19,7 @@ export interface FeatureContext { } export interface FeatureTransformContext extends FeatureContext { resolver: StylableResolver; + evaluator: StylableEvaluator; } export interface NodeTypes { @@ -26,6 +28,8 @@ export interface NodeTypes { RESOLVED?: any; } +type SelectorWalkReturn = number | undefined | void; + export interface FeatureHooks { metaInit: (context: FeatureContext) => void; analyzeInit: (context: FeatureContext) => void; @@ -39,7 +43,7 @@ export interface FeatureHooks { node: T['IMMUTABLE_SELECTOR']; rule: postcss.Rule; walkContext: SelectorNodeContext; - }) => void; + }) => SelectorWalkReturn; analyzeDeclaration: (options: { context: FeatureContext; decl: postcss.Declaration }) => void; transformInit: (options: { context: FeatureTransformContext }) => void; transformResolve: (options: { context: FeatureTransformContext }) => T['RESOLVED']; @@ -58,10 +62,10 @@ export interface FeatureHooks { decl: postcss.Declaration; resolved: T['RESOLVED']; }) => void; - transformDeclarationValue: (options: { + transformValue: (options: { context: FeatureTransformContext; node: ParsedValue; - resolved: T['RESOLVED']; + data: EvalValueData; }) => void; transformJSExports: (options: { exports: StylableExports; resolved: T['RESOLVED'] }) => void; } @@ -96,7 +100,7 @@ const defaultHooks: FeatureHooks = { transformDeclaration() { /**/ }, - transformDeclarationValue() { + transformValue() { /**/ }, transformJSExports() { diff --git a/packages/core/src/features/index.ts b/packages/core/src/features/index.ts index c61afd331..553ac64c2 100644 --- a/packages/core/src/features/index.ts +++ b/packages/core/src/features/index.ts @@ -8,6 +8,9 @@ export type { ImportSymbol, Imported } from './st-import'; export * as STGlobal from './st-global'; +export * as STVar from './st-var'; +export type { VarSymbol } from './st-var'; + export * as CSSClass from './css-class'; export type { ClassSymbol } from './css-class'; @@ -20,4 +23,4 @@ export type { CSSVarSymbol } from './css-custom-property'; export * as CSSKeyframes from './css-keyframes'; export type { KeyframesSymbol } from './css-keyframes'; -export type { RefedMixin, StylableDirectives, VarSymbol } from './types'; +export type { RefedMixin, StylableDirectives } from './types'; diff --git a/packages/core/src/features/st-symbol.ts b/packages/core/src/features/st-symbol.ts index f3b8a44c6..ce843baa3 100644 --- a/packages/core/src/features/st-symbol.ts +++ b/packages/core/src/features/st-symbol.ts @@ -1,6 +1,6 @@ import { FeatureContext, createFeature } from './feature'; -import type { VarSymbol } from './types'; import type { ImportSymbol } from './st-import'; +import type { VarSymbol } from './st-var'; import type { ClassSymbol } from './css-class'; import type { ElementSymbol } from './css-type'; import type { CSSVarSymbol } from './css-custom-property'; diff --git a/packages/core/src/features/st-var.ts b/packages/core/src/features/st-var.ts new file mode 100644 index 000000000..f9f968bd7 --- /dev/null +++ b/packages/core/src/features/st-var.ts @@ -0,0 +1,338 @@ +import { createFeature, FeatureContext, FeatureTransformContext } from './feature'; +import { deprecatedStFunctions } from '../custom-values'; +import { generalDiagnostics } from './diagnostics'; +import * as STSymbol from './st-symbol'; +import type { StylableSymbol } from './st-symbol'; +import type { StylableMeta } from '../stylable-meta'; +import type { EvalValueData, EvalValueResult } from '../functions'; +import { isChildOfAtRule } from '../helpers/rule'; +import { walkSelector } from '../helpers/selector'; +import { stringifyFunction, getStringValue, strategies } from '../helpers/value'; +import { stripQuotation } from '../helpers/string'; +import { ignoreDeprecationWarn } from '../helpers/deprecation'; +import type { ImmutablePseudoClass, PseudoClass } from '@tokey/css-selector-parser'; +import type * as postcss from 'postcss'; +import { processDeclarationFunctions } from '../process-declaration-functions'; +import { Diagnostics } from '../diagnostics'; +import { unbox } from '../custom-values'; +import type { ParsedValue } from '../types'; + +export interface VarSymbol { + _kind: 'var'; + name: string; + value: string; + text: string; + valueType: string | null; + node: postcss.Node; +} + +export const diagnostics = { + FORBIDDEN_DEF_IN_COMPLEX_SELECTOR: generalDiagnostics.FORBIDDEN_DEF_IN_COMPLEX_SELECTOR, + NO_VARS_DEF_IN_ST_SCOPE() { + return `cannot define ":vars" inside of "@st-scope"`; + }, + DEPRECATED_ST_FUNCTION_NAME: (name: string, alternativeName: string) => { + return `"${name}" is deprecated, use "${alternativeName}"`; + }, + CYCLIC_VALUE: (cyclicChain: string[]) => + `Cyclic value definition detected: "${cyclicChain + .map((s, i) => (i === cyclicChain.length - 1 ? '↻ ' : i === 0 ? '→ ' : '↪ ') + s) + .join('\n')}"`, + MISSING_VAR_IN_VALUE: () => `invalid value() with no var identifier`, + COULD_NOT_RESOLVE_VALUE: (args: string) => + `cannot resolve value function using the arguments provided: "${args}"`, + MULTI_ARGS_IN_VALUE: (args: string) => + `value function accepts only a single argument: "value(${args})"`, + CANNOT_USE_AS_VALUE: (type: string, varName: string) => + `${type} "${varName}" cannot be used as a variable`, + CANNOT_USE_JS_AS_VALUE: (varName: string) => + `JavaScript import "${varName}" cannot be used as a variable`, + CANNOT_FIND_IMPORTED_VAR: (varName: string) => `cannot use unknown imported "${varName}"`, + UNKNOWN_VAR: (name: string) => `unknown var "${name}"`, +}; + +// HOOKS + +export const hooks = createFeature<{ + SELECTOR: PseudoClass; + IMMUTABLE_SELECTOR: ImmutablePseudoClass; + RESOLVED: Record; +}>({ + analyzeSelectorNode({ context, node, rule }) { + if (node.type !== `pseudo_class` || node.value !== `vars`) { + return; + } + // make sure `:vars` is the only selector + if (rule.selector === `:vars`) { + if (isChildOfAtRule(rule, `st-scope`)) { + context.diagnostics.warn(rule, diagnostics.NO_VARS_DEF_IN_ST_SCOPE()); + } else { + collectVarSymbols(context, rule); + } + rule.remove(); + // stop further walk into `:vars {}` + return walkSelector.stopAll; + } else { + context.diagnostics.warn(rule, diagnostics.FORBIDDEN_DEF_IN_COMPLEX_SELECTOR(`:vars`)); + } + return; + }, + transformResolve({ context }) { + // Resolve local vars + const resolved: Record = {}; + const symbols = STSymbol.getAllByType(context.meta, `var`); + // Temporarily don't report issues here // ToDo: move reporting here (from value() transformation) + const noDaigContext = { + ...context, + diagnostics: new Diagnostics(), + }; + for (const name of Object.keys(symbols)) { + const symbol = symbols[name]; + const evaluated = context.evaluator.evaluateValue(noDaigContext, { + value: stripQuotation(symbol.text), + meta: context.meta, + node: symbol.node, + }); + resolved[name] = evaluated; + } + return resolved; + }, + transformValue({ context, node, data }) { + evaluateValueCall(context, node, data); + }, + transformJSExports({ exports, resolved }) { + for (const [name, { topLevelType, outputValue }] of Object.entries(resolved)) { + exports.stVars[name] = topLevelType ? unbox(topLevelType) : outputValue; + } + }, +}); + +// API + +export function get(meta: StylableMeta, name: string): VarSymbol | undefined { + return STSymbol.get(meta, name, `var`); +} + +function collectVarSymbols(context: FeatureContext, rule: postcss.Rule) { + rule.walkDecls((decl) => { + collectUrls(context.meta, decl); // ToDo: remove + warnOnDeprecatedCustomValues(context, decl); + + // check type annotation + let type = null; + const prev = decl.prev() as postcss.Comment; + if (prev && prev.type === 'comment') { + const typeMatch = prev.text.match(/^@type (.+)$/); + if (typeMatch) { + type = typeMatch[1]; + } + } + // add symbol + const name = decl.prop; + STSymbol.addSymbol({ + context, + symbol: { + _kind: 'var', + name, + value: '', + text: decl.value, + node: decl, + valueType: type, + }, + node: decl, + }); + // deprecated + ignoreDeprecationWarn(() => { + context.meta.vars.push(STSymbol.get(context.meta, name, `var`)!); + }); + }); +} + +function warnOnDeprecatedCustomValues(context: FeatureContext, decl: postcss.Declaration) { + processDeclarationFunctions( + decl, + (node) => { + if (node.type === 'nested-item' && deprecatedStFunctions[node.name]) { + const { alternativeName } = deprecatedStFunctions[node.name]; + context.diagnostics.info( + decl, + diagnostics.DEPRECATED_ST_FUNCTION_NAME(node.name, alternativeName), + { word: node.name } + ); + } + }, + false + ); +} + +// ToDo: remove after moving :vars removal to end of analyze. +// url collection should pickup vars value during general decls walk +function collectUrls(meta: StylableMeta, decl: postcss.Declaration) { + processDeclarationFunctions( + decl, + (node) => { + if (node.type === 'url') { + meta.urls.push(node.url); + } + }, + false + ); +} + +function evaluateValueCall( + context: FeatureTransformContext, + parsedNode: ParsedValue, + data: EvalValueData +): void { + const { tsVarOverride, passedThrough, value, node } = data; + const parsedArgs = strategies.args(parsedNode).map((x) => x.value); + const varName = parsedArgs[0]; + const restArgs = parsedArgs.slice(1); + + // check var not empty + if (!varName) { + if (node) { + context.diagnostics.warn(node, diagnostics.MISSING_VAR_IN_VALUE(), { + word: getStringValue(parsedNode), + }); + } + } else if (parsedArgs.length >= 1) { + // override with value + if (tsVarOverride?.[varName]) { + parsedNode.resolvedValue = tsVarOverride?.[varName]; + return; + } + // check cyclic + const refUniqID = createUniqID(context.meta.source, varName); + if (passedThrough.includes(refUniqID)) { + // TODO: move diagnostic to original value usage instead of the end of the cyclic chain + handleCyclicValues(context, passedThrough, refUniqID, data.node, value, parsedNode); + return; + } + // resolve + const varSymbol = STSymbol.get(context.meta, varName); + if (varSymbol && varSymbol._kind === 'var') { + // evaluate local var + const { outputValue, topLevelType, typeError } = context.evaluator.evaluateValue( + context, + { + ...data, + passedThrough: passedThrough.concat(createUniqID(context.meta.source, varName)), + value: stripQuotation(varSymbol.text), + args: restArgs, + node: varSymbol.node, + } + ); + // report errors + if (node) { + const argsAsString = parsedArgs.join(', '); + if (typeError) { + context.diagnostics.warn( + node, + diagnostics.COULD_NOT_RESOLVE_VALUE(argsAsString) + ); + } else if (!topLevelType && parsedArgs.length > 1) { + context.diagnostics.warn(node, diagnostics.MULTI_ARGS_IN_VALUE(argsAsString)); + } + } + + parsedNode.resolvedValue = data.valueHook + ? data.valueHook(outputValue, varName, true, passedThrough) + : outputValue; + } else if (varSymbol && varSymbol._kind === 'import') { + // evaluate imported var + const resolvedVar = context.resolver.deepResolve(varSymbol); + if (resolvedVar && resolvedVar.symbol) { + const resolvedVarSymbol = resolvedVar.symbol; + if (resolvedVar._kind === 'css') { + if (resolvedVarSymbol._kind === 'var') { + // var from stylesheet + const { outputValue } = context.evaluator.evaluateValue(context, { + ...data, + passedThrough: passedThrough.concat( + createUniqID(context.meta.source, varName) + ), + value: stripQuotation(resolvedVarSymbol.text), + meta: resolvedVar.meta, + node: resolvedVarSymbol.node, + args: restArgs, + }); + parsedNode.resolvedValue = data.valueHook + ? data.valueHook(outputValue, varName, false, passedThrough) + : outputValue; + } else { + reportUnsupportedSymbolInValue(context, varName, resolvedVarSymbol, node); + } + } else if (resolvedVar._kind === 'js' && typeof resolvedVar.symbol === 'string') { + // value from Javascript + parsedNode.resolvedValue = data.valueHook + ? data.valueHook(resolvedVar.symbol, varName, false, passedThrough) + : resolvedVar.symbol; + } else if (resolvedVar._kind === 'js' && node) { + // unsupported Javascript value + // ToDo: provide actual exported id (default/named as x) + context.diagnostics.warn(node, diagnostics.CANNOT_USE_JS_AS_VALUE(varName), { + word: varName, + }); + } + } else { + // missing imported symbol + const importAst = varSymbol.import.rule; + const foundImport = + importAst.type === `atrule` + ? importAst + : importAst.nodes.find((node) => { + return node.type === 'decl' && node.prop === `-st-named`; + }); + if (foundImport && node) { + // ToDo: provide actual exported id (default/named as x) + context.diagnostics.error(node, diagnostics.CANNOT_FIND_IMPORTED_VAR(varName), { + word: varName, + }); + } + } + } else if (varSymbol) { + reportUnsupportedSymbolInValue(context, varName, varSymbol, node); + } else if (node) { + context.diagnostics.warn(node, diagnostics.UNKNOWN_VAR(varName), { + word: varName, + }); + } + } +} + +function reportUnsupportedSymbolInValue( + context: FeatureTransformContext, + name: string, + symbol: StylableSymbol, + node: postcss.Node | undefined +) { + const errorKind = symbol._kind === 'class' && symbol[`-st-root`] ? 'stylesheet' : symbol._kind; + if (node) { + context.diagnostics.warn(node, diagnostics.CANNOT_USE_AS_VALUE(errorKind, name), { + word: name, + }); + } +} + +function handleCyclicValues( + context: FeatureTransformContext, + passedThrough: string[], + refUniqID: string, + node: postcss.Node | undefined, + value: string, + parsedNode: ParsedValue +) { + if (node) { + const cyclicChain = passedThrough.map((variable) => variable || ''); + cyclicChain.push(refUniqID); + context.diagnostics.warn(node, diagnostics.CYCLIC_VALUE(cyclicChain), { + word: refUniqID, // ToDo: check word is path+var and not var name + }); + } + return stringifyFunction(value, parsedNode); +} + +function createUniqID(source: string, varName: string) { + return `${source}: ${varName}`; +} diff --git a/packages/core/src/features/types.ts b/packages/core/src/features/types.ts index 606ce306b..485d780fd 100644 --- a/packages/core/src/features/types.ts +++ b/packages/core/src/features/types.ts @@ -3,7 +3,6 @@ import type { ClassSymbol } from './css-class'; import type { ElementSymbol } from './css-type'; import type { MappedStates, MixinValue } from '../stylable-value-parsers'; import type { SelectorNode } from '@tokey/css-selector-parser'; -import type * as postcss from 'postcss'; // ToDo: distribute types to features @@ -18,12 +17,3 @@ export interface RefedMixin { mixin: MixinValue; ref: ImportSymbol | ClassSymbol; } - -export interface VarSymbol { - _kind: 'var'; - name: string; - value: string; - text: string; - valueType: string | null; - node: postcss.Node; -} diff --git a/packages/core/src/functions.ts b/packages/core/src/functions.ts index de77107dd..7a140b820 100644 --- a/packages/core/src/functions.ts +++ b/packages/core/src/functions.ts @@ -2,40 +2,68 @@ import { dirname, relative } from 'path'; import postcssValueParser from 'postcss-value-parser'; import type * as postcss from 'postcss'; import { resolveCustomValues } from './custom-values'; -import type { Diagnostics } from './diagnostics'; +import { Diagnostics } from './diagnostics'; import { isCssNativeFunction } from './native-reserved-lists'; import { assureRelativeUrlPrefix } from './stylable-assets'; import type { StylableMeta } from './stylable-meta'; import type { CSSResolve, JSResolve, StylableResolver } from './stylable-resolver'; import type { replaceValueHook, StylableTransformer } from './stylable-transformer'; -import { strategies, valueMapping } from './stylable-value-parsers'; import { getFormatterArgs, getStringValue, stringifyFunction } from './helpers/value'; import type { ParsedValue } from './types'; -import { stripQuotation } from './utils'; -import { CSSCustomProperty, STSymbol } from './features'; +import type { FeatureTransformContext } from './features/feature'; +import { CSSCustomProperty, STSymbol, STVar } from './features'; export type ValueFormatter = (name: string) => string; export type ResolvedFormatter = Record; +export interface EvalValueData { + value: string; + passedThrough: string[]; + node?: postcss.Node; + valueHook?: replaceValueHook; + meta: StylableMeta; + tsVarOverride?: Record | null; + cssVarsMapping?: Record; + args?: string[]; +} + +export interface EvalValueResult { + topLevelType: any; + outputValue: string; + typeError?: Error; +} + +export class StylableEvaluator { + public tsVarOverride: Record | null | undefined; + constructor(options: { tsVarOverride?: Record | null } = {}) { + this.tsVarOverride = options.tsVarOverride; + } + evaluateValue( + context: FeatureTransformContext, + data: Omit & { passedThrough?: string[] } + ) { + return processDeclarationValue( + context.resolver, + data.value, + data.meta, + data.node, + data.tsVarOverride || this.tsVarOverride, + data.valueHook, + context.diagnostics, + data.passedThrough, + data.cssVarsMapping, + data.args + ); + } +} + +// old API + export const functionWarnings = { FAIL_TO_EXECUTE_FORMATTER: (resolvedValue: string, message: string) => `failed to execute formatter "${resolvedValue}" with error: "${message}"`, - CYCLIC_VALUE: (cyclicChain: string[]) => - `Cyclic value definition detected: "${cyclicChain - .map((s, i) => (i === cyclicChain.length - 1 ? '↻ ' : i === 0 ? '→ ' : '↪ ') + s) - .join('\n')}"`, - CANNOT_USE_AS_VALUE: (type: string, varName: string) => - `${type} "${varName}" cannot be used as a variable`, - CANNOT_USE_JS_AS_VALUE: (varName: string) => - `JavaScript import "${varName}" cannot be used as a variable`, - CANNOT_FIND_IMPORTED_VAR: (varName: string) => `cannot use unknown imported "${varName}"`, - MULTI_ARGS_IN_VALUE: (args: string) => - `value function accepts only a single argument: "value(${args})"`, - COULD_NOT_RESOLVE_VALUE: (args: string) => - `cannot resolve value function using the arguments provided: "${args}"`, UNKNOWN_FORMATTER: (name: string) => `cannot find native function or custom formatter called ${name}`, - UNKNOWN_VAR: (name: string) => `unknown var "${name}"`, }; export function resolveArgumentsValue( @@ -73,12 +101,12 @@ export function processDeclarationValue( node?: postcss.Node, variableOverride?: Record | null, valueHook?: replaceValueHook, - diagnostics?: Diagnostics, + diagnostics: Diagnostics = new Diagnostics(), passedThrough: string[] = [], - cssVarsMapping?: Record, + cssVarsMapping: Record = {}, args: string[] = [] -): { topLevelType: any; outputValue: string; typeError?: Error } { - diagnostics = node ? diagnostics : undefined; +): EvalValueResult { + const evaluator = new StylableEvaluator({ tsVarOverride: variableOverride }); const customValues = resolveCustomValues(meta, resolver); const parsedValue: any = postcssValueParser(value); parsedValue.walk((parsedNode: ParsedValue) => { @@ -86,163 +114,25 @@ export function processDeclarationValue( switch (type) { case 'function': if (value === 'value') { - const parsedArgs = strategies.args(parsedNode).map((x) => x.value); - if (parsedArgs.length >= 1) { - const varName = parsedArgs[0]; - const getArgs = parsedArgs - .slice(1) - .map((arg) => - evalDeclarationValue( - resolver, - arg, - meta, - node, - variableOverride, - valueHook, - diagnostics, - passedThrough.concat(createUniqID(meta.source, varName)), - cssVarsMapping, - undefined - ) - ); - if (variableOverride && variableOverride[varName]) { - return (parsedNode.resolvedValue = variableOverride[varName]); - } - const refUniqID = createUniqID(meta.source, varName); - if (passedThrough.includes(refUniqID)) { - // TODO: move diagnostic to original value usage instead of the end of the cyclic chain - return handleCyclicValues( - passedThrough, - refUniqID, - diagnostics, - node, - value, - parsedNode - ); - } - const varSymbol = STSymbol.get(meta, varName); - if (varSymbol && varSymbol._kind === 'var') { - const resolved = processDeclarationValue( - resolver, - stripQuotation(varSymbol.text), - meta, - varSymbol.node, - variableOverride, - valueHook, - diagnostics, - passedThrough.concat(createUniqID(meta.source, varName)), - cssVarsMapping, - getArgs - ); - - const { outputValue, topLevelType, typeError } = resolved; - - if (diagnostics && node) { - const argsAsString = parsedArgs.join(', '); - if (typeError) { - diagnostics.warn( - node, - functionWarnings.COULD_NOT_RESOLVE_VALUE(argsAsString) - ); - } else if (!topLevelType && parsedArgs.length > 1) { - diagnostics.warn( - node, - functionWarnings.MULTI_ARGS_IN_VALUE(argsAsString) - ); - } - } - - parsedNode.resolvedValue = valueHook - ? valueHook(outputValue, varName, true, passedThrough) - : outputValue; - } else if (varSymbol && varSymbol._kind === 'import') { - const resolvedVar = resolver.deepResolve(varSymbol); - if (resolvedVar && resolvedVar.symbol) { - const resolvedVarSymbol = resolvedVar.symbol; - if (resolvedVar._kind === 'css') { - if (resolvedVarSymbol._kind === 'var') { - const resolvedValue = evalDeclarationValue( - resolver, - stripQuotation(resolvedVarSymbol.text), - resolvedVar.meta, - resolvedVarSymbol.node, - variableOverride, - valueHook, - diagnostics, - passedThrough.concat( - createUniqID(meta.source, varName) - ), - cssVarsMapping, - getArgs - ); - parsedNode.resolvedValue = valueHook - ? valueHook( - resolvedValue, - varName, - false, - passedThrough - ) - : resolvedValue; - } else { - const errorKind = - resolvedVarSymbol._kind === 'class' && - resolvedVarSymbol[valueMapping.root] - ? 'stylesheet' - : resolvedVarSymbol._kind; - if (diagnostics && node) { - diagnostics.warn( - node, - functionWarnings.CANNOT_USE_AS_VALUE( - errorKind, - varName - ), - { word: varName } - ); - } - } - } else if ( - resolvedVar._kind === 'js' && - typeof resolvedVar.symbol === 'string' - ) { - parsedNode.resolvedValue = valueHook - ? valueHook( - resolvedVar.symbol, - varName, - false, - passedThrough - ) - : resolvedVar.symbol; - } else if (resolvedVar._kind === 'js' && diagnostics && node) { - // ToDo: provide actual exported id (default/named as x) - diagnostics.warn( - node, - functionWarnings.CANNOT_USE_JS_AS_VALUE(varName), - { - word: varName, - } - ); - } - } else { - const namedDecl = varSymbol.import.rule.nodes.find((node) => { - return node.type === 'decl' && node.prop === valueMapping.named; - }); - if (namedDecl && diagnostics && node) { - // ToDo: provide actual exported id (default/named as x) - diagnostics.error( - node, - functionWarnings.CANNOT_FIND_IMPORTED_VAR(varName), - { word: varName } - ); - } - } - } else if (diagnostics && node) { - diagnostics.warn(node, functionWarnings.UNKNOWN_VAR(varName), { - word: varName, - }); - } - } else { - // TODO: warn - } + STVar.hooks.transformValue({ + context: { + meta, + diagnostics, + resolver, + evaluator, + }, + data: { + value, + passedThrough, + node, + valueHook, + meta, + tsVarOverride: variableOverride, + cssVarsMapping, + args, + }, + node: parsedNode, + }); } else if (value === '') { parsedNode.resolvedValue = stringifyFunction(value, parsedNode); } else if (customValues[value]) { @@ -295,14 +185,24 @@ export function processDeclarationValue( } } } else if (value === 'var') { - CSSCustomProperty.hooks.transformDeclarationValue({ + CSSCustomProperty.hooks.transformValue({ context: { meta, - diagnostics: diagnostics || (null as unknown as Diagnostics), // ToDo: make sure context is available here + diagnostics, resolver, + evaluator, + }, + data: { + value, + passedThrough, + node, + valueHook, + meta, + tsVarOverride: variableOverride, + cssVarsMapping, + args, }, node: parsedNode, - resolved: cssVarsMapping || {}, }); } else if (isCssNativeFunction(value)) { parsedNode.resolvedValue = stringifyFunction(value, parsedNode); @@ -371,25 +271,3 @@ export function evalDeclarationValue( args ).outputValue; } - -function handleCyclicValues( - passedThrough: string[], - refUniqID: string, - diagnostics: Diagnostics | undefined, - node: postcss.Node | undefined, - value: string, - parsedNode: ParsedValue -) { - const cyclicChain = passedThrough.map((variable) => variable || ''); - cyclicChain.push(refUniqID); - if (diagnostics && node) { - diagnostics.warn(node, functionWarnings.CYCLIC_VALUE(cyclicChain), { - word: refUniqID, - }); - } - return stringifyFunction(value, parsedNode); -} - -function createUniqID(source: string, varName: string) { - return `${source}: ${varName}`; -} diff --git a/packages/core/src/helpers/css-custom-property.ts b/packages/core/src/helpers/css-custom-property.ts index 3b6bcc944..647971047 100644 --- a/packages/core/src/helpers/css-custom-property.ts +++ b/packages/core/src/helpers/css-custom-property.ts @@ -1,6 +1,6 @@ import type * as postcss from 'postcss'; import type { Diagnostics } from '../diagnostics'; -import { stripQuotation } from '../utils'; +import { stripQuotation } from '../helpers/string'; const UNIVERSAL_SYNTAX_DEFINITION = '*'; const AT_PROPERTY_DISCRIPTOR_LIST = ['initial-value', 'syntax', 'inherits']; diff --git a/packages/core/src/helpers/import.ts b/packages/core/src/helpers/import.ts index 7312f0100..3c49be0dc 100644 --- a/packages/core/src/helpers/import.ts +++ b/packages/core/src/helpers/import.ts @@ -4,7 +4,7 @@ import { Diagnostics } from '../diagnostics'; import type { Imported } from '../features'; import { Root, decl, Declaration, atRule, rule, Rule, AtRule } from 'postcss'; import { rootValueMapping, valueMapping } from '../stylable-value-parsers'; -import { stripQuotation } from '../utils'; +import { stripQuotation } from '../helpers/string'; import { isCompRoot } from './selector'; import type { ParsedValue } from '../types'; import type * as postcss from 'postcss'; diff --git a/packages/core/src/helpers/string.ts b/packages/core/src/helpers/string.ts new file mode 100644 index 000000000..93b65af2e --- /dev/null +++ b/packages/core/src/helpers/string.ts @@ -0,0 +1,11 @@ +export function stripQuotation(str: string) { + return str.replace(/^['"](.*?)['"]$/g, '$1'); +} + +export function filename2varname(filename: string) { + return string2varname(filename.replace(/(?=.*)\.\w+$/, '').replace(/\.st$/, '')); +} + +function string2varname(str: string) { + return str.replace(/[^0-9a-zA-Z_]/gm, '').replace(/^[^a-zA-Z_]+/gm, ''); +} diff --git a/packages/core/src/helpers/value.ts b/packages/core/src/helpers/value.ts index ecdb57a8b..6d1a5c9b5 100644 --- a/packages/core/src/helpers/value.ts +++ b/packages/core/src/helpers/value.ts @@ -147,3 +147,45 @@ export function validateAllowedNodesUntil( return true; } + +export const valueDiagnostics = { + INVALID_NAMED_PARAMS: () => + `invalid named parameters (e.g. "func(name value, [name value, ...])")`, +}; + +export const strategies = { + named: (node: any, reportWarning?: ReportWarning) => { + const named: Record = {}; + getNamedArgs(node).forEach((mixinArgsGroup) => { + const argsDivider = mixinArgsGroup[1]; + if (mixinArgsGroup.length < 3 || (argsDivider && argsDivider.type !== 'space')) { + if (reportWarning) { + const argValue = mixinArgsGroup[0]; + reportWarning(valueDiagnostics.INVALID_NAMED_PARAMS(), { + word: argValue.value, + }); + } + return; + } + named[mixinArgsGroup[0].value] = stringifyParam(mixinArgsGroup.slice(2)); + }); + return named; + }, + args: (node: any, reportWarning?: ReportWarning) => { + return getFormatterArgs(node, true, reportWarning).map((value) => ({ value })); + }, +}; + +function stringifyParam(nodes: any) { + return postcssValueParser.stringify(nodes, (n: any) => { + if (n.type === 'function') { + return postcssValueParser.stringify(n); + } else if (n.type === 'div') { + return null; + } else if (n.type === 'string') { + return n.value; + } else { + return undefined; + } + }); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 339ac94b3..b97c51b89 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -90,7 +90,6 @@ export { stKeys, stValues, stValuesMap, - strategies, valueMapping, valueParserWarnings, } from './stylable-value-parsers'; @@ -191,6 +190,7 @@ export { groupValues, listOptions, validateAllowedNodesUntil, + strategies, } from './helpers/value'; // *** deprecated *** diff --git a/packages/core/src/pseudo-states.ts b/packages/core/src/pseudo-states.ts index e62bd8ed7..622fa516f 100644 --- a/packages/core/src/pseudo-states.ts +++ b/packages/core/src/pseudo-states.ts @@ -17,7 +17,7 @@ import type { StylableResolver } from './stylable-resolver'; import { MappedStates, valueMapping } from './stylable-value-parsers'; import type { ParsedValue, StateParsedValue } from './types'; import { CSSClass } from './features'; -import { stripQuotation } from './utils'; +import { stripQuotation } from './helpers/string'; import { reservedFunctionalPseudoClasses } from './native-reserved-lists'; import cssesc from 'cssesc'; diff --git a/packages/core/src/stylable-meta.ts b/packages/core/src/stylable-meta.ts index efa7e200e..683c48046 100644 --- a/packages/core/src/stylable-meta.ts +++ b/packages/core/src/stylable-meta.ts @@ -20,6 +20,7 @@ import { STSymbol, STImport, STGlobal, + STVar, CSSClass, CSSType, CSSCustomProperty, @@ -28,7 +29,16 @@ import { export const RESERVED_ROOT_NAME = 'root'; -const features = [STSymbol, STImport, STGlobal, CSSClass, CSSType, CSSCustomProperty, CSSKeyframes]; +const features = [ + STSymbol, + STImport, + STGlobal, + STVar, + CSSClass, + CSSType, + CSSCustomProperty, + CSSKeyframes, +]; export class StylableMeta { public data: PlugableRecord = {}; @@ -38,6 +48,7 @@ export class StylableMeta { public namespace = ''; /** @deprecated use meta.getImportStatements() */ public imports: Imported[] = []; + /** @deprecated use meta.getAllStVars() or meta.getStVar(name) */ public vars: VarSymbol[] = []; /** @deprecated */ public cssVars: Record = {}; @@ -95,6 +106,12 @@ export class StylableMeta { getImportStatements() { return STImport.getImportStatements(this); } + getStVar(name: string) { + return STSymbol.get(this, name, `var`); + } + getAllStVars() { + return STSymbol.getAllByType(this, `var`); + } } setFieldForDeprecation(StylableMeta.prototype, `elements`, { objectType: `stylableMeta`, @@ -128,3 +145,8 @@ setFieldForDeprecation(StylableMeta.prototype, `cssVars`, { objectType: `stylableMeta`, valueOnThis: true, }); +setFieldForDeprecation(StylableMeta.prototype, `vars`, { + objectType: `stylableMeta`, + valueOnThis: true, + pleaseUse: `meta.getAllStVars() or meta.getStVar(name)`, +}); diff --git a/packages/core/src/stylable-mixins.ts b/packages/core/src/stylable-mixins.ts index bae8b62eb..399b785ca 100644 --- a/packages/core/src/stylable-mixins.ts +++ b/packages/core/src/stylable-mixins.ts @@ -10,8 +10,9 @@ 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, strategies } from './stylable-value-parsers'; +import { valueMapping, mixinDeclRegExp } from './stylable-value-parsers'; import { ignoreDeprecationWarn } from './helpers/deprecation'; export const mixinWarnings = { @@ -379,7 +380,7 @@ function filterPartialMixinDecl( do { size = overrideSet.size; regexp = new RegExp(`value\\((\\s*${Array.from(overrideSet).join('\\s*)|(\\s*')}\\s*)\\)`); - for (const { text, name } of meta.vars) { + for (const { text, name } of Object.values(meta.getAllStVars())) { if (!overrideSet.has(name) && text.match(regexp)) { overrideSet.add(name); } diff --git a/packages/core/src/stylable-processor.ts b/packages/core/src/stylable-processor.ts index a1cff6ed8..74df21c56 100644 --- a/packages/core/src/stylable-processor.ts +++ b/packages/core/src/stylable-processor.ts @@ -1,6 +1,5 @@ import path from 'path'; import * as postcss from 'postcss'; -import { deprecatedStFunctions } from './custom-values'; import { Diagnostics } from './diagnostics'; import { parseSelector as deprecatedParseSelector } from './deprecated/deprecated-selector-utils'; import { murmurhash3_32_gc } from './murmurhash'; @@ -12,7 +11,7 @@ import { ElementSymbol, RefedMixin, StylableDirectives, - VarSymbol, + STVar, } from './features'; import { generalDiagnostics } from './features/diagnostics'; import { FeatureContext, STSymbol, STImport, CSSClass, CSSType, CSSKeyframes } from './features'; @@ -35,8 +34,8 @@ import { stValuesMap, valueMapping, } from './stylable-value-parsers'; -import { deprecated, filename2varname, stripQuotation } from './utils'; -import { ignoreDeprecationWarn } from './helpers/deprecation'; +import { stripQuotation, filename2varname } from './helpers/string'; +import { ignoreDeprecationWarn, warnOnce } from './helpers/deprecation'; const parseStates = SBTypesParsers[valueMapping.states]; const parseGlobal = SBTypesParsers[valueMapping.global]; @@ -76,9 +75,6 @@ export const processorWarnings = { EMPTY_NAMESPACE_DEF() { return '@namespace must contain at least one character or digit'; }, - NO_VARS_DEF_IN_ST_SCOPE() { - return `cannot define "${rootValueMapping.vars}" inside of "@st-scope"`; - }, MISSING_SCOPING_PARAM() { return '"@st-scope" missing scoping selector parameter'; }, @@ -88,9 +84,6 @@ export const processorWarnings = { INVALID_NESTING(child: string, parent: string) { return `nesting of rules within rules is not supported, found: "${child}" inside "${parent}"`; }, - DEPRECATED_ST_FUNCTION_NAME: (name: string, alternativeName: string) => { - return `"${name}" is deprecated, use "${alternativeName}"`; - }, }; export class StylableProcessor implements FeatureContext { @@ -234,25 +227,6 @@ export class StylableProcessor implements FeatureContext { ); } - private handleStFunctions(decl: postcss.Declaration) { - processDeclarationFunctions( - decl, - (node) => { - if (node.type === 'nested-item' && deprecatedStFunctions[node.name]) { - const { alternativeName } = deprecatedStFunctions[node.name]; - this.diagnostics.info( - decl, - processorWarnings.DEPRECATED_ST_FUNCTION_NAME(node.name, alternativeName), - { - word: node.name, - } - ); - } - }, - false - ); - } - private handleNamespaceReference(namespace: string): string { let pathToSource: string | undefined; for (const node of this.meta.ast.nodes) { @@ -301,26 +275,12 @@ export class StylableProcessor implements FeatureContext { walkContext: nodeContext, }); } else if (node.value === 'vars') { - if (rule.selector === rootValueMapping.vars) { - if (isChildOfAtRule(rule, rootValueMapping.stScope)) { - this.diagnostics.warn( - rule, - processorWarnings.NO_VARS_DEF_IN_ST_SCOPE() - ); - rule.remove(); - return walkSelector.stopAll; - } - - this.addVarSymbols(rule); - return walkSelector.stopAll; - } else { - this.diagnostics.warn( - rule, - generalDiagnostics.FORBIDDEN_DEF_IN_COMPLEX_SELECTOR( - rootValueMapping.vars - ) - ); - } + return STVar.hooks.analyzeSelectorNode({ + context: this, + node, + rule, + walkContext: nodeContext, + }); } else if (!knownPseudoClassesWithNestedSelectors.includes(node.value)) { return walkSelector.skipNested; } @@ -409,38 +369,6 @@ export class StylableProcessor implements FeatureContext { } } - protected addVarSymbols(rule: postcss.Rule) { - rule.walkDecls((decl) => { - this.collectUrls(decl); - this.handleStFunctions(decl); - let type = null; - - const prev = decl.prev() as postcss.Comment; - if (prev && prev.type === 'comment') { - const typeMatch = prev.text.match(/^@type (.+)$/); - if (typeMatch) { - type = typeMatch[1]; - } - } - - const varSymbol: VarSymbol = { - _kind: 'var', - name: decl.prop, - value: '', - text: decl.value, - node: decl, - valueType: type, - }; - this.meta.vars.push(varSymbol); - STSymbol.addSymbol({ - context: this, - symbol: varSymbol, - node: decl, - }); - }); - rule.remove(); - } - protected handleDirectives(rule: SRule, decl: postcss.Declaration) { const isSimplePerSelector = isSimpleSelector(rule.selector); const type = isSimplePerSelector.reduce((accType, { type }) => { @@ -626,7 +554,7 @@ export function validateScopingSelector( } export function createEmptyMeta(root: postcss.Root, diagnostics: Diagnostics): StylableMeta { - deprecated( + warnOnce( 'createEmptyMeta is deprecated and will be removed in the next version. Use "new StylableMeta()"' ); return new StylableMeta(root, diagnostics); diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index f46be2dee..716ead9fb 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -3,9 +3,8 @@ import cloneDeep from 'lodash.clonedeep'; import { basename } from 'path'; import * as postcss from 'postcss'; import type { FileProcessor } from './cached-process-file'; -import { unbox } from './custom-values'; import type { Diagnostics } from './diagnostics'; -import { evalDeclarationValue, processDeclarationValue } from './functions'; +import { StylableEvaluator } from './functions'; import { nativePseudoClasses, nativePseudoElements } from './native-reserved-lists'; import { setStateToNode, stateErrors } from './pseudo-states'; import { @@ -26,9 +25,18 @@ import { createWarningRule, isChildOfAtRule, getRuleScopeSelector } from './help import { namespace } from './helpers/namespace'; import { getOriginDefinition } from './helpers/resolve'; import { appendMixins } from './stylable-mixins'; -import { STImport, ClassSymbol, ElementSymbol, CSSCustomProperty } from './features'; +import type { ClassSymbol, ElementSymbol } from './features'; import type { StylableMeta } from './stylable-meta'; -import { STSymbol, STGlobal, CSSClass, CSSType, CSSKeyframes } from './features'; +import { + STSymbol, + STImport, + STGlobal, + STVar, + CSSClass, + CSSType, + CSSKeyframes, + CSSCustomProperty, +} from './features'; import type { SRule, SDecl } from './deprecated/postcss-ast-extension'; import { CSSResolve, StylableResolverCache, StylableResolver } from './stylable-resolver'; import { isCSSVarProp } from './helpers/css-custom-property'; @@ -110,6 +118,7 @@ export class StylableTransformer { public replaceValueHook: replaceValueHook | undefined; public postProcessor: postProcessor | undefined; public mode: EnvMode; + private evaluator: StylableEvaluator = new StylableEvaluator(); private metaParts = new WeakMap(); constructor(options: TransformerOptions) { @@ -140,6 +149,7 @@ export class StylableTransformer { meta, diagnostics: this.diagnostics, resolver: this.resolver, + evaluator: this.evaluator, }, }); meta.transformedScopes = validateScopes(this, meta); @@ -154,23 +164,25 @@ export class StylableTransformer { ast: postcss.Root, meta: StylableMeta, metaExports?: StylableExports, - variableOverride?: Record, + tsVarOverride?: Record, path: string[] = [], mixinTransform = false ) { + this.evaluator.tsVarOverride = tsVarOverride; + const transformContext = { + meta, + diagnostics: this.diagnostics, + resolver: this.resolver, + evaluator: this.evaluator, + }; + const stVarResolve = STVar.hooks.transformResolve({ + context: transformContext, + }); const keyframesResolve = CSSKeyframes.hooks.transformResolve({ - context: { - meta, - diagnostics: this.diagnostics, - resolver: this.resolver, - }, + context: transformContext, }); const cssVarsMapping = CSSCustomProperty.hooks.transformResolve({ - context: { - meta, - diagnostics: this.diagnostics, - resolver: this.resolver, - }, + context: transformContext, }); ast.walkRules((rule) => { @@ -183,35 +195,22 @@ export class StylableTransformer { ast.walkAtRules((atRule) => { const { name } = atRule; if (name === 'media') { - atRule.params = evalDeclarationValue( - this.resolver, - atRule.params, + atRule.params = this.evaluator.evaluateValue(transformContext, { + value: atRule.params, meta, - atRule, - variableOverride, - this.replaceValueHook, - this.diagnostics, - path.slice(), - undefined, - undefined - ); + node: atRule, + valueHook: this.replaceValueHook, + passedThrough: path.slice(), + }).outputValue; } else if (name === 'property') { CSSCustomProperty.hooks.transformAtRuleNode({ - context: { - meta, - diagnostics: this.diagnostics, - resolver: this.resolver, - }, + context: transformContext, atRule, resolved: cssVarsMapping, }); } else if (name === 'keyframes') { CSSKeyframes.hooks.transformAtRuleNode({ - context: { - meta, - diagnostics: this.diagnostics, - resolver: this.resolver, - }, + context: transformContext, atRule, resolved: keyframesResolve, }); @@ -223,21 +222,13 @@ export class StylableTransformer { if (isCSSVarProp(decl.prop)) { CSSCustomProperty.hooks.transformDeclaration({ - context: { - meta, - diagnostics: this.diagnostics, - resolver: this.resolver, - }, + context: transformContext, decl, resolved: cssVarsMapping, }); } else if (decl.prop === `animation` || decl.prop === `animation-name`) { CSSKeyframes.hooks.transformDeclaration({ - context: { - meta, - diagnostics: this.diagnostics, - resolver: this.resolver, - }, + context: transformContext, decl, resolved: keyframesResolve, }); @@ -249,18 +240,14 @@ export class StylableTransformer { case valueMapping.states: break; default: - decl.value = evalDeclarationValue( - this.resolver, - decl.value, + decl.value = this.evaluator.evaluateValue(transformContext, { + value: decl.value, meta, - decl, - variableOverride, - this.replaceValueHook, - this.diagnostics, - path.slice(), + node: decl, + valueHook: this.replaceValueHook, + passedThrough: path.slice(), cssVarsMapping, - undefined - ); + }).outputValue; } }); @@ -268,12 +255,15 @@ export class StylableTransformer { this.addDevRules(meta); } ast.walkRules((rule) => - appendMixins(this, rule as SRule, meta, variableOverride || {}, cssVarsMapping, path) + appendMixins(this, rule as SRule, meta, tsVarOverride || {}, cssVarsMapping, path) ); if (metaExports) { Object.assign(metaExports.classes, this.exportClasses(meta)); - this.exportLocalVars(meta, metaExports.stVars, variableOverride); + STVar.hooks.transformJSExports({ + exports: metaExports, + resolved: stVarResolve, + }); CSSKeyframes.hooks.transformJSExports({ exports: metaExports, resolved: keyframesResolve, @@ -284,23 +274,6 @@ export class StylableTransformer { }); } } - public exportLocalVars( - meta: StylableMeta, - stVarsExport: StylableExports['stVars'], - variableOverride?: Record - ) { - for (const varSymbol of meta.vars) { - const { outputValue, topLevelType } = processDeclarationValue( - this.resolver, - varSymbol.text, - meta, - varSymbol.node, - variableOverride - ); - - stVarsExport[varSymbol.name] = topLevelType ? unbox(topLevelType) : outputValue; - } - } /** @deprecated */ public getScopedCSSVar( decl: postcss.Declaration, @@ -437,6 +410,7 @@ export class StylableTransformer { meta: originMeta, diagnostics: this.diagnostics, resolver: this.resolver, + evaluator: this.evaluator, }, selectorContext: context, node, @@ -447,6 +421,7 @@ export class StylableTransformer { meta: originMeta, diagnostics: this.diagnostics, resolver: this.resolver, + evaluator: this.evaluator, }, selectorContext: context, node, diff --git a/packages/core/src/stylable-value-parsers.ts b/packages/core/src/stylable-value-parsers.ts index bd1a6e4f8..09dc2faeb 100644 --- a/packages/core/src/stylable-value-parsers.ts +++ b/packages/core/src/stylable-value-parsers.ts @@ -3,16 +3,13 @@ import postcssValueParser, { FunctionNode, WordNode } from 'postcss-value-parser import type { Diagnostics } from './diagnostics'; import { processPseudoStates } from './pseudo-states'; import { parseSelectorWithCache } from './helpers/selector'; -import { getNamedArgs, getFormatterArgs } from './helpers/value'; +import { getNamedArgs, strategies } from './helpers/value'; import type { StateParsedValue } from './types'; export const valueParserWarnings = { VALUE_CANNOT_BE_STRING() { return 'value can not be a string (remove quotes?)'; }, - CSS_MIXIN_FORCE_NAMED_PARAMS() { - return 'CSS mixins must use named parameters (e.g. "func(name value, [name value, ...])")'; - }, }; export interface MappedStates { @@ -180,40 +177,3 @@ export const SBTypesParsers = { }); }, }; - -export const strategies = { - named: (node: any, reportWarning?: ReportWarning) => { - const named: Record = {}; - getNamedArgs(node).forEach((mixinArgsGroup) => { - const argsDivider = mixinArgsGroup[1]; - if (mixinArgsGroup.length < 3 || (argsDivider && argsDivider.type !== 'space')) { - if (reportWarning) { - const argValue = mixinArgsGroup[0]; - reportWarning(valueParserWarnings.CSS_MIXIN_FORCE_NAMED_PARAMS(), { - word: argValue.value, - }); - } - return; - } - named[mixinArgsGroup[0].value] = stringifyParam(mixinArgsGroup.slice(2)); - }); - return named; - }, - args: (node: any, reportWarning?: ReportWarning) => { - return getFormatterArgs(node, true, reportWarning).map((value) => ({ value })); - }, -}; - -function stringifyParam(nodes: any) { - return postcssValueParser.stringify(nodes, (n: any) => { - if (n.type === 'function') { - return postcssValueParser.stringify(n); - } else if (n.type === 'div') { - return null; - } else if (n.type === 'string') { - return n.value; - } else { - return undefined; - } - }); -} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts deleted file mode 100644 index 3d3011da4..000000000 --- a/packages/core/src/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -export function stripQuotation(str: string) { - return str.replace(/^['"](.*?)['"]$/g, '$1'); -} - -export function filename2varname(filename: string) { - return string2varname(filename.replace(/(?=.*)\.\w+$/, '').replace(/\.st$/, '')); -} - -export function string2varname(str: string) { - return str.replace(/[^0-9a-zA-Z_]/gm, '').replace(/^[^a-zA-Z_]+/gm, ''); -} - -const deprecatedCache: { [message: string]: boolean } = {}; - -export function deprecated(staticMessage: string) { - if (!deprecatedCache[staticMessage]) { - deprecatedCache[staticMessage] = true; - try { - console.warn('DEPRECATED: ' + staticMessage); - } catch { - /**/ - } - } -} diff --git a/packages/core/test/diagnostics.spec.ts b/packages/core/test/diagnostics.spec.ts index 6c486e8ad..2f59bea5e 100644 --- a/packages/core/test/diagnostics.spec.ts +++ b/packages/core/test/diagnostics.spec.ts @@ -5,16 +5,15 @@ import { findTestLocations, } from '@stylable/core-test-kit'; import { - functionWarnings, mixinWarnings, valueMapping, processorWarnings, transformerWarnings, nativePseudoElements, - rootValueMapping, valueParserWarnings, } from '@stylable/core'; -import { STImport, CSSClass, CSSType, STSymbol } from '@stylable/core/dist/features'; +import { valueDiagnostics } from '@stylable/core/dist/helpers/value'; +import { STImport, CSSClass, CSSType, STVar } from '@stylable/core/dist/features'; import { generalDiagnostics } from '@stylable/core/dist/features/diagnostics'; describe('findTestLocations', () => { @@ -374,7 +373,7 @@ describe('diagnostics: warnings and errors', () => { }, [ { - message: valueParserWarnings.CSS_MIXIN_FORCE_NAMED_PARAMS(), + message: valueDiagnostics.INVALID_NAMED_PARAMS(), file: '/style.st.css', }, ] @@ -549,7 +548,7 @@ describe('diagnostics: warnings and errors', () => { }, }; expectTransformDiagnostics(config, [ - { message: functionWarnings.UNKNOWN_VAR('missingVar'), file: '/main.css' }, + { message: STVar.diagnostics.UNKNOWN_VAR('missingVar'), file: '/main.css' }, ]); }); @@ -575,33 +574,11 @@ describe('diagnostics: warnings and errors', () => { }, }; expectTransformDiagnostics(config, [ - { message: functionWarnings.UNKNOWN_VAR('missingVar'), file: '/main.css' }, + { message: STVar.diagnostics.UNKNOWN_VAR('missingVar'), file: '/main.css' }, ]); }); }); - describe(':vars', () => { - it('should return warning when defined in a complex selector', () => { - expectAnalyzeDiagnostics( - ` - |.gaga:vars|{ - myColor:red; - } - - `, - - [ - { - message: generalDiagnostics.FORBIDDEN_DEF_IN_COMPLEX_SELECTOR( - rootValueMapping.vars - ), - file: 'main.css', - }, - ] - ); - }); - }); - describe('-st-extends', () => { it('should return warning when defined under complex selector', () => { expectAnalyzeDiagnostics( @@ -674,77 +651,6 @@ describe('diagnostics: warnings and errors', () => { { message: processorWarnings.OVERRIDE_MIXIN('-st-mixin'), file: '/main.css' }, ]); }); - - describe('from import', () => { - it('should warn when import redeclare same symbol (in different block types)', () => { - expectAnalyzeDiagnostics( - ` - |:import { - -st-from: './file.st.css'; - -st-default: $Name$; - }| - :vars { - Name: red; - } - `, - [ - { - message: STSymbol.diagnostics.REDECLARE_SYMBOL('Name'), - file: 'main.st.css', - }, - { - message: STSymbol.diagnostics.REDECLARE_SYMBOL('Name'), - file: 'main.st.css', - skipLocationCheck: true, - }, - ] - ); - }); - }); - }); - - describe('complex examples', () => { - describe(':import', () => { - it('should return warning for unknown var import', () => { - const config = { - entry: '/main.st.css', - files: { - '/main.st.css': { - content: ` - :import{ - -st-from: "./file.st.css"; - -st-named: myVar; - } - .root { - |color: value($myVar$);| - }`, - }, - '/file.st.css': { - content: ` - :vars { - otherVar: someValue; - } - `, - }, - }, - }; - expectTransformDiagnostics(config, [ - { - message: STImport.diagnostics.UNKNOWN_IMPORTED_SYMBOL( - 'myVar', - './file.st.css' - ), - file: '/main.st.css', - skip: true, - skipLocationCheck: true, - }, - { - message: functionWarnings.CANNOT_FIND_IMPORTED_VAR('myVar'), - file: '/main.st.css', - }, - ]); - }); - }); }); describe('selectors', () => { diff --git a/packages/core/test/features/st-var.spec.ts b/packages/core/test/features/st-var.spec.ts new file mode 100644 index 000000000..26e1a2e3c --- /dev/null +++ b/packages/core/test/features/st-var.spec.ts @@ -0,0 +1,1256 @@ +import { STSymbol, STVar } from '@stylable/core/dist/features'; +import { stTypes, box } from '@stylable/core/dist/custom-values'; +import { ignoreDeprecationWarn } from '@stylable/core/dist/helpers/deprecation'; +import { testStylableCore, shouldReportNoDiagnostics } from '@stylable/core-test-kit'; +import { expect } from 'chai'; +import postcssValueParser from 'postcss-value-parser'; + +describe(`features/st-var`, () => { + it(`should process :vars definitions`, () => { + const { sheets } = testStylableCore(` + /* @transform-remove */ + :vars { + varA: a-val; + varB: b-val; + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // symbols + expect(STVar.get(meta, `varA`), `varA symbol`).to.contain({ + _kind: `var`, + name: `varA`, + value: ``, + text: `a-val`, + valueType: null, + // node: (meta.rawAst.nodes[1] as any).nodes[0], + }); + expect(STVar.get(meta, `varB`), `varB symbol`).to.contain({ + _kind: `var`, + name: `varB`, + value: ``, + text: `b-val`, + valueType: null, + // node: (meta.rawAst.nodes[1] as any).nodes[1], + }); + + // JS exports + expect(exports.stVars.varA, `varA JS export`).to.eql(`a-val`); + expect(exports.stVars.varB, `varB JS export`).to.eql(`b-val`); + + // deprecation + ignoreDeprecationWarn(() => { + expect(meta.vars, `deprecated 'meta.vars'`).to.eql([ + STVar.get(meta, `varA`), + STVar.get(meta, `varB`), + ]); + }); + }); + it(`should process multiple :vars definitions`, () => { + const { sheets } = testStylableCore(` + /* @transform-remove */ + :vars { + varA: a-val; + } + :vars { + varB: b-val; + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // symbols + expect(STVar.get(meta, `varA`), `varA symbol`).to.contain({ + _kind: `var`, + name: `varA`, + value: ``, + text: `a-val`, + valueType: null, + }); + expect(STVar.get(meta, `varB`), `varB symbol`).to.contain({ + _kind: `var`, + name: `varB`, + value: ``, + text: `b-val`, + valueType: null, + }); + + // JS exports + expect(exports.stVars.varA, `varA JS export`).to.eql(`a-val`); + expect(exports.stVars.varB, `varB JS export`).to.eql(`b-val`); + }); + it(`should only be defined on root`, () => { + const { sheets } = testStylableCore(` + @st-scope { + /* + @analyze-warn ${STVar.diagnostics.NO_VARS_DEF_IN_ST_SCOPE()} + @transform-remove + */ + :vars { + invalid: red; + } + } + + /* @analyze-warn(complex selector) ${STVar.diagnostics.FORBIDDEN_DEF_IN_COMPLEX_SELECTOR( + `:vars` + )} */ + .root:vars {} + `); + + const { meta, exports } = sheets['/entry.st.css']; + + // symbols + expect(STVar.get(meta, `invalid`), `symbol not registered`).to.eql(undefined); + + // JS exports + expect(exports.stVars, `no JS export`).to.eql({}); + }); + it(`should collect @type annotation`, () => { + const { sheets } = testStylableCore(` + /* @transform-remove */ + :vars { + /*@type inline*/a: val; + + /*@type before*/ + b: val; + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // symbols + expect(STVar.get(meta, `a`), `a type`).to.contain({ + valueType: `inline`, + }); + expect(STVar.get(meta, `b`), `b type`).to.contain({ + valueType: `before`, + }); + }); + it(`should resolve :vars value using value() function`, () => { + testStylableCore(` + /* @transform-remove */ + :vars { + varA: green; + } + .root { + /* @decl(simple) prop: green */ + prop: value(varA); + + /* @decl(in unknown function) prop: unknown(green) */ + prop: unknown(value(varA)) + } + `); + }); + it(`should handle invalid value() cases`, () => { + const { sheets } = testStylableCore(` + /* @transform-remove */ + :vars { + varA: green; + } + .part {} + .root { + /* + @decl(empty) prop: value() + @transform-warn(empty) ${STVar.diagnostics.MISSING_VAR_IN_VALUE()} + */ + prop: value(); + + /* + @decl(no first arg) prop: value(, path) + @transform-warn(no first arg) ${STVar.diagnostics.MISSING_VAR_IN_VALUE()} + */ + prop: value(, path); + + /* + @decl(unknown var) prop: value(unknown) + @transform-warn(unknown var) ${STVar.diagnostics.UNKNOWN_VAR('unknown')} + */ + prop: value(unknown); + + /* + @decl(non var symbol) prop: value(part) + @transform-warn(non var symbol) word(part) ${STVar.diagnostics.CANNOT_USE_AS_VALUE( + 'class', + `part` + )} + */ + prop: value(part); + + /* + @decl(unknown 2nd arg) prop: green + @transform-warn(unknown 2nd arg) ${STVar.diagnostics.MULTI_ARGS_IN_VALUE( + 'varA, invalidSecondArgument' + )} + */ + prop: value(varA, invalidSecondArgument); + } + `); + const { meta } = sheets[`/entry.st.css`]; + // checks reports words that contain parenthesis (inline test cannot) + expect( + meta.transformDiagnostics!.reports[0], + `missing var diagnostic word 1` + ).to.deep.contain({ + message: STVar.diagnostics.MISSING_VAR_IN_VALUE(), + options: { + word: `value()`, + }, + }); + expect( + meta.transformDiagnostics!.reports[1], + `missing var diagnostic word 2` + ).to.deep.contain({ + message: STVar.diagnostics.MISSING_VAR_IN_VALUE(), + options: { + word: `value(, path)`, + }, + }); + }); + it(`should handle escaping`, () => { + const { sheets } = testStylableCore(` + :vars { + color\\.1: green; + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // symbols + expect(STVar.get(meta, `color\\.1`), `symbol`).to.contain({ + name: `color\\.1`, + }); + + // JS exports + // ToDo: fix bug - this should be 'color.1' + expect(exports.stVars['color\\.1'], `JS export`).to.eql(`green`); + }); + it(`should remove outer quotation with content unchanged`, () => { + const { sheets } = testStylableCore(` + :vars { + double: "double"; + single: 'single'; + quotes: "'with-quotes'"; + } + .root { + /* @decl(double) prop: double */ + prop: value(double); + + /* @decl(single) prop: single */ + prop: value(single); + + /* @decl(quotes) prop: 'with-quotes' */ + prop: value(quotes); + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // symbols + expect(STVar.get(meta, `double`), `double symbol`).to.contain({ + text: `"double"`, + }); + expect(STVar.get(meta, `single`), `single symbol`).to.contain({ + text: `'single'`, + }); + expect(STVar.get(meta, `quotes`), `quotes symbol`).to.contain({ + text: `"'with-quotes'"`, + }); + + // exports + expect(exports.stVars, `JS exports`).to.eql({ + double: `double`, + single: `single`, + quotes: `'with-quotes'`, + }); + }); + it(`should resolve value in :vars definition`, () => { + const { sheets } = testStylableCore(` + /* @transform-remove */ + :vars { + varA: a-val; + varB: value(varA); + } + .root { + /* @decl prop: a-val */ + prop: value(varB); + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // JS exports + expect(exports.stVars.varA, `varA JS export`).to.eql(`a-val`); + expect(exports.stVars.varB, `varB JS export`).to.eql(`a-val`); + }); + it(`should resolve cyclic vars`, () => { + const { sheets } = testStylableCore(` + :vars { + /* @transform-warn(varA) ${STVar.diagnostics.CYCLIC_VALUE([ + `/entry.st.css: varB`, + `/entry.st.css: varA`, + `/entry.st.css: varB`, + ])} */ + varA: a(value(varB)); + + /* @transform-warn(varB) ${STVar.diagnostics.CYCLIC_VALUE([ + `/entry.st.css: varA`, + `/entry.st.css: varB`, + `/entry.st.css: varA`, + ])} */ + varB: b(value(varA)); + } + .root { + /* @decl(varA) prop: a(b(value(varA))) */ + prop: value(varA); + + /* @decl(varB) prop: b(a(value(varB))) */ + prop: value(varB); + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + // symbols + expect(STVar.get(meta, `varA`), `varA symbol`).to.contain({ + text: `a(value(varB))`, + }); + expect(STVar.get(meta, `varB`), `varB symbol`).to.contain({ + text: `b(value(varA))`, + }); + + // exports + expect(exports.stVars, `JS exports`).to.eql({ + varA: `a(b(a(value(varB))))`, + varB: `b(a(b(value(varA))))`, + }); + }); + describe(`custom-value`, () => { + it(`should support build-in st-array`, () => { + const { sheets } = testStylableCore(` + :vars { + shallow: st-array(a, b); + + str: inline-value; + + deep: st-array( + inline-text, + value(str), + abc value(str) xyz, + st-array(y, z), + value(shallow), + ); + } + .root { + /* @decl(shallow 1) prop: a */ + prop: value(shallow, 0); + + /* @decl(shallow 2) prop: b */ + prop: value(shallow, 1); + + /* @decl(deep inline text) prop: inline-text */ + prop: value(deep, 0); + + /* @decl(deep inner value) prop: inline-value */ + prop: value(deep, 1); + + /* @decl(deep concat) prop: abc inline-value xyz */ + prop: value(deep, 2); + + /* @decl(deep inline array) prop: z */ + prop: value(deep, 3, 1); + + /* @decl(deep ref array) prop: b */ + prop: value(deep, 4, 1); + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // JS exports + expect(exports.stVars.shallow, `shallow JS export`).to.eql([`a`, `b`]); + expect(exports.stVars.deep, `deep JS export`).to.eql([ + `inline-text`, + `inline-value`, + `abc inline-value xyz`, + [`y`, `z`], + [`a`, `b`], + ]); + }); + it(`should support build-in st-map`, () => { + // ToDo: fix path to nested reference map + const { sheets } = testStylableCore(` + :vars { + shallow: st-map( + a A, + b B + ); + + str: INLINE-VALUE; + + deep: st-map( + inline-text INLINE-TEXT, + inline-value value(str), + concat abc value(str) xyz, + inline-map st-map( + y Y, + z Z + ), + ref-map value(shallow), + ); + } + .root { + /* @decl(shallow 1) prop: A */ + prop: value(shallow, a); + + /* @decl(shallow 2) prop: B */ + prop: value(shallow, b); + + /* @decl(deep inline text) prop: INLINE-TEXT */ + prop: value(deep, inline-text); + + /* @decl(deep inline value) prop: INLINE-VALUE */ + prop: value(deep, inline-value); + + /* @decl(deep concat) prop: abc INLINE-VALUE xyz */ + prop: value(deep, concat); + + /* @decl(deep inline map) prop: Z */ + prop: value(deep, inline-map, z); + + /* @ToDo-decl(deep ref map) prop: B */ + prop: value(deep, ref-map, b); + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // JS exports + expect(exports.stVars.shallow, `shallow JS export`).to.eql({ a: `A`, b: `B` }); + expect(exports.stVars.deep, `deep JS export`).to.eql({ + 'inline-text': `INLINE-TEXT`, + 'inline-value': `INLINE-VALUE`, + concat: `abc INLINE-VALUE xyz`, + 'inline-map': { y: `Y`, z: `Z` }, + // ToDo: fix + 'ref-map': `st-map(\n a A,\n b B\n )`, + }); + }); + it(`should support extended custom type`, () => { + // ToDo: extend tests for full API + const { sheets } = testStylableCore({ + '/custom.js': ` + module.exports = { + _kind: 'CustomValue', + register(id){ + return { + evalVarAst() {}, + getValue() { + return 'custom value with id="' + id + '"' + } + } + } + } + `, + '/entry.st.css': ` + @st-import CustomValue from './custom'; + + :vars { + varA: CustomValue(); + } + + .root { + /* @decl prop: custom value with id="CustomValue" */ + prop: value(varA); + } + `, + }); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // JS exports + expect(exports.stVars.varA, `JS export`).to.eql(`custom value with id="CustomValue"`); + }); + it(`should support composed types`, () => { + const { sheets } = testStylableCore(` + :vars { + deep: st-array( + st-map( + idx st-array(a, b) + ) + ); + + array-in-map: st-array( + st-map( + size 1px, + style solid, + color red + ), + st-map( + size 3px, + style dashed, + color yellow + ), + st-map( + size 5px, + style dotted, + color green + ) + ); + + map-in-array: st-map( + reds st-array( + rgb(100, 0, 0), + rgb(255, 0, 0) + ), + greens st-array( + rgb(0, 100, 0), + rgb(0, 255, 0) + ) + ); + } + .root { + /* @decl(deep) prop: b */ + prop: value(deep, 0, idx, 1); + + /* @decl(array-in-map) prop: 1px solid red */ + prop: value(array-in-map, 0, size) value(array-in-map, 0, style) value(array-in-map, 0, color); + + /* @decl(map-in-array) prop: rgb(255, 0, 0) */ + prop: value(map-in-array, reds, 1); + } + `); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // JS exports + expect(exports.stVars, `JS export`).to.eql({ + deep: [ + { + idx: [`a`, `b`], + }, + ], + 'array-in-map': [ + { size: `1px`, style: `solid`, color: `red` }, + { size: `3px`, style: `dashed`, color: `yellow` }, + { size: `5px`, style: `dotted`, color: `green` }, + ], + 'map-in-array': { + reds: [`rgb(100, 0, 0)`, `rgb(255, 0, 0)`], + greens: [`rgb(0, 100, 0)`, `rgb(0, 255, 0)`], + }, + }); + }); + it(`should resolve value path from resolved value() as key`, () => { + testStylableCore(` + :vars { + arr-key: 0; + map-key: idx; + mixed: st-array( + st-map( + idx deep-value + ) + ); + } + .root { + /* @decl prop: deep-value */ + prop: value(mixed, value(arr-key), value(map-key)); + + /* + @decl(success despite internal path error) prop: deep-value + @transform-warn(index un-required path) ${STVar.diagnostics.MULTI_ARGS_IN_VALUE( + 'arr-key, no-path' + )} + @transform-warn(key un-required path) ${STVar.diagnostics.MULTI_ARGS_IN_VALUE( + 'map-key, no-path' + )} + */ + prop: value(mixed, value(arr-key, no-path), value(map-key, no-path)); + } + `); + }); + it(`should report on unknown entry`, () => { + const { sheets } = testStylableCore(` + :vars { + myVar: st-map( + key1 red, + key2 st-map( + key3 green + ) + ); + } + .root { + /* @transform-warn(1st level) ${STVar.diagnostics.COULD_NOT_RESOLVE_VALUE( + `myVar, unknown` + )}*/ + prop: value(myVar, unknown); + + /* @transform-warn(2nd level) ${STVar.diagnostics.COULD_NOT_RESOLVE_VALUE( + `myVar, key2, unknown` + )}*/ + prop: value(myVar, key2, unknown); + } + `); + + // check valid multiple arguments to value() + expect( + sheets['/entry.st.css'].meta.transformDiagnostics!.reports.length, + `only access reports` + ).to.eql(2); + }); + it(`should report deprecated forms`, () => { + testStylableCore(` + :vars { + /* @analyze-info(stMap) word(stMap) ${STVar.diagnostics.DEPRECATED_ST_FUNCTION_NAME( + `stMap`, + `st-map` + )}*/ + varA: stMap(a 1); + + /* @analyze-info(stArray) word(stArray) ${STVar.diagnostics.DEPRECATED_ST_FUNCTION_NAME( + `stArray`, + `st-array` + )}*/ + varA: stArray(a, b); + + /* @analyze-info(nested) word(stMap) ${STVar.diagnostics.DEPRECATED_ST_FUNCTION_NAME( + `stMap`, + `st-map` + )}*/ + varA: stArray( + a, + stMap(a 1) + ); + } + `); + }); + it(`*** st-map and st-array contract test ***`, () => { + const test = ({ + label, + typeDef, + path, + expectedMatch, + expectedDataStructure, + deepIncludeTest = false, + }: { + label: string; + typeDef: string; + path: string[]; + expectedMatch: string; + expectedDataStructure: any; + deepIncludeTest?: boolean; + }) => { + const valueAst = postcssValueParser(typeDef).nodes[0]; + const typeExtension = stTypes[valueAst.value]; + const dataStructure = typeExtension.evalVarAst(valueAst, stTypes).value; + const match = typeExtension.getValue( + path, + typeExtension.evalVarAst(valueAst, stTypes), + valueAst, + stTypes + ); + const dataStructureExpect = expect(dataStructure, `${label} data structure`); + deepIncludeTest + ? dataStructureExpect.to.deep.include(expectedDataStructure) + : dataStructureExpect.to.eql(expectedDataStructure); + expect(match, `${label} string value from path`).to.equal(expectedMatch); + }; + + test({ + label: `st-map flat access`, + typeDef: `st-map(k1 v1, k2 v2)`, + path: [`k1`], + expectedMatch: `v1`, + expectedDataStructure: { k1: `v1`, k2: `v2` }, + }); + test({ + label: `st-map nested access`, + typeDef: `st-map(k1 v1, k2 st-map(k3 v3, k4 st-map(k5 v5) ))`, + path: [`k2`, `k4`, `k5`], + expectedMatch: `v5`, + deepIncludeTest: true, + expectedDataStructure: { + k1: `v1`, + k2: box(`st-map`, { + k3: `v3`, + k4: box(`st-map`, { + k5: `v5`, + }), + }), + }, + }); + test({ + label: `st-array flat access`, + typeDef: `st-array(v0, v1)`, + path: [`1`], + expectedMatch: `v1`, + expectedDataStructure: [`v0`, `v1`], + }); + test({ + label: `st-array nested access`, + typeDef: `st-array(v0, st-array(v1))`, + path: [`1`, `0`], + expectedMatch: `v1`, + expectedDataStructure: [`v0`, box(`st-array`, [`v1`])], + }); + test({ + label: `composed array/map/array access`, + typeDef: `st-array(v0, st-map(k2 st-array(v2))`, + path: [`1`, `k2`, `0`], + expectedMatch: `v2`, + expectedDataStructure: [`v0`, box(`st-map`, { k2: box(`st-array`, [`v2`]) })], + }); + test({ + label: `composed map/array/map access`, + typeDef: `st-map(k0 v0, k1 st-array(v2, st-map(k3 v3)))`, + path: [`k1`, `1`, `k3`], + expectedMatch: `v3`, + expectedDataStructure: { + k0: `v0`, + k1: box(`st-array`, [`v2`, box(`st-map`, { k3: `v3` })]), + }, + }); + }); + }); + describe(`st-import`, () => { + it(`should resolve imported var`, () => { + const { sheets } = testStylableCore({ + '/vars.st.css': ` + :vars { + before: before-val; + after: after-val; + } + `, + '/entry.st.css': ` + :vars { + localBefore: value(before); + } + .root { + /* @decl(before) prop: before-val */ + prop: value(before); + + /* @decl(localBefore) prop: before-val */ + prop: value(localBefore); + } + + @st-import [before, after] from './vars.st.css'; + + :vars { + localAfter: value(after); + } + .root { + /* @decl(after) prop: after-val */ + prop: value(after); + + /* @decl(localAfter) prop: after-val */ + prop: value(localAfter); + } + `, + }); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // symbols + expect(STVar.get(meta, `before`), `import symbol not linked locally`).to.eql(undefined); + expect(STVar.get(meta, `localBefore`), `local symbol`).to.contain({ + name: `localBefore`, + value: ``, + text: `value(before)`, + }); + + // exports + expect(exports.stVars, `JS export not contains imported`).to.eql({ + localBefore: `before-val`, + localAfter: `after-val`, + }); + }); + it(`should resolve imported Javascript strings`, () => { + const { sheets } = testStylableCore({ + '/code.js': ` + module.exports = { + jsStr: '123', + }; + `, + '/entry.st.css': ` + @st-import [jsStr] from './code'; + + :vars { + a: value(jsStr); + } + + .root { + /* + @decl prop: 123 + */ + prop: value(jsStr); + } + `, + }); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // JS exports + expect(exports.stVars.a, `a JS export`).to.eql(`123`); + }); + it(`should report unhandled imported non var symbols in value`, () => { + testStylableCore({ + '/vars.st.css': ` + .imported-class {} + `, + '/code.js': ` + module.exports = { + jsNum: 123, + jsFunc: function abc() {} + }; + `, + '/entry.st.css': ` + @st-import Sheet, [imported-class, unknown] from './vars.st.css'; + @st-import [jsNum, jsFunc] from './code'; + + .root { + /* + @decl(imported sheet) prop: value(Sheet) + @transform-warn(imported sheet) word(Sheet) ${STVar.diagnostics.CANNOT_USE_AS_VALUE( + `stylesheet`, + `Sheet` + )} + */ + prop: value(Sheet); + + /* + @decl(imported-class) prop: value(imported-class) + @transform-warn(imported-class) word(imported-class) ${STVar.diagnostics.CANNOT_USE_AS_VALUE( + `class`, + `imported-class` + )} + */ + prop: value(imported-class); + + /* + @decl(unknown) prop: value(unknown) + @transform-error(unknown) word(unknown) ${STVar.diagnostics.CANNOT_FIND_IMPORTED_VAR( + `unknown` + )} + */ + prop: value(unknown); + + /* + @decl(JS number) prop: value(jsNum) + @transform-warn(JS number) word(jsNum) ${STVar.diagnostics.CANNOT_USE_JS_AS_VALUE( + `jsNum` + )} + */ + prop: value(jsNum); + + /* + @decl(JS function) prop: value(jsFunc) + @transform-warn(JS function) word(jsFunc) ${STVar.diagnostics.CANNOT_USE_JS_AS_VALUE( + `jsFunc` + )} + */ + prop: value(jsFunc); + } + `, + }); + }); + it(`should override imported with local definition`, () => { + const { sheets } = testStylableCore({ + '/vars.st.css': ` + :vars { + before: imported-before-val; + after: imported-after-val; + } + `, + '/entry.st.css': ` + :vars { + /* @analyze-warn(local before) word(before) ${STSymbol.diagnostics.REDECLARE_SYMBOL( + `before` + )} */ + before: local-before-val; + } + .root { + /* @decl(before) prop: local-before-val */ + prop: value(before); + } + + /* + @analyze-warn(import before) word(before) ${STSymbol.diagnostics.REDECLARE_SYMBOL( + `before` + )} + @analyze-warn(import after) word(after) ${STSymbol.diagnostics.REDECLARE_SYMBOL( + `after` + )} + */ + @st-import [before, after] from './vars.st.css'; + + :vars { + /* @analyze-warn(local after) word(after) ${STSymbol.diagnostics.REDECLARE_SYMBOL( + `after` + )} */ + after: local-after-val; + } + .root { + /* @decl(after) prop: local-after-val */ + prop: value(after); + } + `, + }); + const { exports } = sheets['/entry.st.css']; + + // exports + expect(exports.stVars, `JS export`).to.eql({ + after: `local-after-val`, + before: `local-before-val`, + }); + }); + it(`should resolve deep imported var`, () => { + const { sheets } = testStylableCore({ + '/deep.st.css': ` + :vars { + str: str-val; + map: st-map( + a st-map( + b deep-map-val + ) + ); + arr: st-array( + red, green + ); + } + `, + '/mid.st.css': ` + @st-import [str, map, arr] from './deep.st.css'; + `, + '/entry.st.css': ` + @st-import [str, map, arr] from './mid.st.css'; + + .root { + /* @decl(str) prop: str-val */ + prop: value(str); + + /* @decl(map) prop: deep-map-val */ + prop: value(map, a, b); + + /* @decl(array) prop: green */ + prop: value(arr, 1); + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should resolve cyclic vars between stylesheets`, () => { + const { sheets } = testStylableCore({ + '/a.st.css': ` + @st-import [varB] from './b.st.css'; + :vars { + /* @transform-warn(varA) ${STVar.diagnostics.CYCLIC_VALUE([ + `/a.st.css: varB`, + `/b.st.css: varA`, + `/a.st.css: varB`, + ])} */ + varA: a(value(varB)); + } + .root { + /* @decl(varB in a) prop: b(a(value(varB))) */ + prop: value(varB); + + /* @decl(varA in a) prop: a(b(a(value(varB)))) */ + prop: value(varA); + } + `, + '/b.st.css': ` + @st-import [varA] from './a.st.css'; + :vars { + /* @transform-warn(varB) ${STVar.diagnostics.CYCLIC_VALUE([ + `/b.st.css: varA`, + `/a.st.css: varB`, + `/b.st.css: varA`, + ])} */ + varB: b(value(varA)); + } + .root { + /* @decl(varA in b) prop: a(b(value(varA))) */ + prop: value(varA); + + /* @decl(varB in b) prop: b(a(b(value(varA)))) */ + prop: value(varB); + } + `, + }); + + const aExports = sheets['/a.st.css'].exports; + const bExports = sheets['/b.st.css'].exports; + + // exports + expect(aExports.stVars.varA, `varA JS export`).to.eql(`a(b(a(value(varB))))`); + expect(bExports.stVars.varB, `varB JS export`).to.eql(`b(a(b(value(varA))))`); + }); + }); + describe(`st-formatter`, () => { + it(`should accept formatter in declaration`, () => { + const { sheets } = testStylableCore({ + '/formatter.js': ` + module.exports = function add(a, b) { + return Number(a) + Number(b); + } + `, + '/entry.st.css': ` + @st-import add from './formatter.js'; + + :vars { + amount: add(1, 2); + } + .root { + /* @decl prop: 3 */ + prop: value(amount); + } + `, + }); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // symbols + expect(STVar.get(meta, `amount`), `symbol`).to.contain({ + text: `add(1, 2)`, + }); + + // exports + expect(exports.stVars, `JS export`).to.eql({ + amount: `3`, + }); + }); + it(`should resolve imported var with formatter in declaration`, () => { + testStylableCore({ + '/formatter.js': ` + module.exports = function getYellowAndBlue() { + return 'green'; + } + `, + '/vars.st.css': ` + @st-import getYellowAndBlue from './formatter.js'; + + :vars { + color: getYellowAndBlue(); + } + `, + '/entry.st.css': ` + @st-import [color] from './vars.st.css'; + + .root { + /* @decl background: green */ + background: value(color); + } + `, + }); + }); + it(`should handle error`, () => { + const { sheets } = testStylableCore({ + '/formatter.js': ` + module.exports = function fail() { + throw new Error("FAIL!"); + } + `, + '/entry.st.css': ` + @st-import fail from './formatter.js'; + + :vars { + definition: fail(input); + color: red; + } + .root { + /* @decl(definition) prop: fail(input) */ + prop: value(definition); + + /* @decl(value) prop: fail(a, red, z) */ + prop: fail(a, value(color), z); + } + `, + }); + + const { meta, exports } = sheets['/entry.st.css']; + + // symbols + expect(STVar.get(meta, `definition`), `definition symbol`).to.contain({ + text: `fail(input)`, + }); + + // exports + expect(exports.stVars.definition, `definition JS export`).to.eql(`fail(input)`); + }); + }); + describe(`css-media`, () => { + it(`should resolve value() function`, () => { + const { sheets } = testStylableCore(` + :vars { + mobile-width: 200px; + } + + /* @atrule screen (200px) */ + @media screen (value(mobile-width)) {} + `); + + const { meta } = sheets[`/entry.st.css`]; + + shouldReportNoDiagnostics(meta); + }); + }); + describe(`hooks.replaceValueHook`, () => { + it(`should override value() result"`, () => { + let valueCallCount = 0; + const { sheets } = testStylableCore( + ` + :vars { + a: "red"; + b: green; + } + .container { + /* @decl prop: override(0) red-a-true */ + prop: value(a); + + /* @decl prop: override(1) green-b-true */ + prop: value(b); + } + `, + { + stylableConfig: { + hooks: { + replaceValueHook(resolved, name, isLocal) { + return `override(${valueCallCount++}) ${resolved}-${name}-${isLocal}`; + }, + }, + }, + } + ); + + const { meta, exports } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + // JS exports + expect(exports.stVars.a, `a JS export`).to.eql(`red`); + expect(exports.stVars.b, `b JS export`).to.eql(`green`); + }); + it(`should override value() from JS import"`, () => { + // ToDo: move to CSSValue feature when it will be created + testStylableCore( + { + '/functions.js': ` + module.exports.fn1 = function(x){return 'fn1'} + module.exports.fn2 = function(x){return 'fn2'} + `, + '/entry.st.css': ` + @st-import [fn1, fn2] from './functions'; + + .root { + /* @decl prop: hooked_fn1(hooked_fn2(input)) */ + prop: fn1(fn2(input)); + } + `, + }, + { + stylableConfig: { + hooks: { + replaceValueHook(_resolved, fn) { + if (typeof fn !== 'string') { + return `hooked_${fn.name}(${fn.args})`; + } + return ''; + }, + }, + }, + } + ); + }); + it(`should override value() passed to mixin"`, () => { + testStylableCore( + { + '/deep.st.css': ` + :vars { + deepVar: green; + } + .part { + deepProp: value(deepVar); + } + `, + '/mix.st.css': ` + @st-import Deep from './deep.st.css'; + + :vars { + a: original-a; + b: original-b; + c: original-c; + } + .level-1 { + a: value(a); + b: value(b); + c: value(c); + } + .level-2 { + -st-mixin: Deep(deepVar value(c)); + } + `, + '/entry.st.css': ` + @st-import Mix from './mix.st.css'; + + :vars { + topA: 1; + topB: 2; + } + /* + @rule[1] .entry__root .mix__level-1 { + a: 1,topA,true,; + b: 2,topB,true,; + c: original-c,c,true,default from /entry.st.css; + } + @rule[3] .entry__root .mix__level-2 .deep__part { + deepProp: original-c,c,true,default from /entry.st.css; + } + */ + .root { + -st-mixin: Mix(a value(topA), b value(topB)); + } + `, + }, + { + stylableConfig: { + hooks: { + replaceValueHook(resolved, name, isLocal, path) { + return [resolved, name, isLocal, path].join(`,`); + }, + }, + }, + } + ); + }); + }); + it.skip(`should provide valid var path introspection - value(var, ...path)`); +}); diff --git a/packages/core/test/functions.spec.ts b/packages/core/test/functions.spec.ts index 016ac2f27..86bacf63d 100644 --- a/packages/core/test/functions.spec.ts +++ b/packages/core/test/functions.spec.ts @@ -1,4 +1,4 @@ -import { functionWarnings, nativeFunctionsDic, processorWarnings } from '@stylable/core'; +import { functionWarnings, nativeFunctionsDic } from '@stylable/core'; import { expectTransformDiagnostics, generateStylableRoot } from '@stylable/core-test-kit'; import { expect } from 'chai'; import type * as postcss from 'postcss'; @@ -173,6 +173,7 @@ describe('Stylable functions (native, formatter and variable)', () => { }); it('should parse arguments passed to a formatter, seperated by commas', () => { + // ToDo: move to formatter / value feature spec const result = generateStylableRoot({ entry: `/style.st.css`, files: { @@ -246,6 +247,7 @@ describe('Stylable functions (native, formatter and variable)', () => { }); it('should pass-through native css functions', () => { + // ToDo: move to formatter feature spec const result = generateStylableRoot({ entry: `/style.st.css`, files: { @@ -303,27 +305,6 @@ describe('Stylable functions (native, formatter and variable)', () => { expect(rule.nodes[0].toString()).to.equal("src: url(/test.woff) format('woff')"); }); - it('should perserve native format function quotation with stylable var', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :vars { - fontType: "'woff'"; - } - @font-face { - src: url(/test.woff) format(value(fontType)); - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal("src: url(/test.woff) format('woff')"); - }); - xit('should allow using formatters inside a url native function', () => { // see: https://github.com/TrySound/postcss-value-parser/issues/34 const result = generateStylableRoot({ @@ -412,6 +393,7 @@ describe('Stylable functions (native, formatter and variable)', () => { }); it('passes through cyclic vars', () => { + // ToDo: check if this test is necessary const result = generateStylableRoot({ entry: `/style.st.css`, files: { @@ -444,115 +426,8 @@ describe('Stylable functions (native, formatter and variable)', () => { expect(rule.nodes[0].toString()).to.equal('border: value(a)'); }); - it('passes through cyclic vars through multiple files', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./style1.st.css"; - -st-named: color2; - } - :vars { - color1: 1px value(color2); - } - .container { - background: value(color2); - } - `, - }, - '/style1.st.css': { - content: ` - :import { - -st-from: "./style.st.css"; - -st-named: color1 - } - :vars { - color2: value(color1) - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('background: 1px value(color2)'); - }); - - it('should support using formatters in variable declarations', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./formatter"; - -st-default: myBorder; - } - :vars { - border: myBorder(5, 1); - } - .container { - border: value(border); - } - `, - }, - '/formatter.js': { - content: ` - module.exports = function myBorder(amount, size) { - return (Number(size) + Number(amount)) + 'px'; - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('border: 6px'); - }); - - it('should support using formatters in an imported variable declarations', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./vars.st.css"; - -st-named: color1; - } - .container { - background: value(color1); - } - `, - }, - '/vars.st.css': { - content: ` - :import { - -st-from: "./formatter"; - -st-default: getGreen; - } - :vars { - color1: getGreen(); - } - `, - }, - '/formatter.js': { - content: ` - module.exports = function getGreen() { - return 'green'; - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('background: green'); - }); - it('should support using formatters in a complex multi file scenario', () => { + // ToDo: move to css-value feature spec const result = generateStylableRoot({ entry: `/style.st.css`, files: { @@ -615,6 +490,7 @@ describe('Stylable functions (native, formatter and variable)', () => { }); it('should support using a formatter in a media query param', () => { + // ToDo: move to css-media-query feature const result = generateStylableRoot({ entry: `/style.st.css`, files: { @@ -644,405 +520,6 @@ describe('Stylable functions (native, formatter and variable)', () => { expect(rule.params).to.equal('max-width: 1850px'); }); - it('should gracefully fail when a formatter throws an error and return the source', () => { - const result = generateStylableRoot({ - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :import { - -st-from: "./formatter"; - -st-default: fail; - } - :vars { - param1: red; - } - .some-class { - color: fail(a, value(param1), c); - } - `, - }, - '/formatter.js': { - content: ` - module.exports = function fail() { - throw new Error("FAIL FAIL FAIL"); - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - expect(rule.nodes[0].toString()).to.equal('color: fail(a, red, c)'); - }); - }); - - describe('diagnostics', () => { - describe('deprecation', () => { - it('should return a warning when using deprecated "stMap" syntax', () => { - expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - :vars { - |color1: $stMap$(key1 red)|; - } - - `, - }, - }, - }, - [ - { - message: processorWarnings.DEPRECATED_ST_FUNCTION_NAME( - 'stMap', - 'st-map' - ), - file: '/style.st.css', - severity: 'info', - }, - ] - ); - }); - - it('should return a warning when using deprecated "stArray" syntax', () => { - expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - :vars { - |color1: $stArray$(red, blue)|; - } - `, - }, - }, - }, - [ - { - message: processorWarnings.DEPRECATED_ST_FUNCTION_NAME( - 'stArray', - 'st-array' - ), - file: '/style.st.css', - severity: 'info', - }, - ] - ); - }); - - it('should return a warning when using a nested deprecated "stArray" or "stMap" syntax', () => { - const config = { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - :vars { - |color1: stArray( - red, - stMap(key1 blue)| - ); - } - `, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { - message: processorWarnings.DEPRECATED_ST_FUNCTION_NAME( - 'stArray', - 'st-array' - ), - file: '/style.st.css', - severity: 'info', - }, - { - message: processorWarnings.DEPRECATED_ST_FUNCTION_NAME('stMap', 'st-map'), - skip: true, - file: '/style.st.css', - severity: 'info', - }, - ]); - }); - }); - - describe('value()', () => { - it('should return a warning when passing more than one argument to a value() function', () => { - expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - :vars { - color1: red; - color2: gold; - } - .my-class { - |color:value(color1, color2)|; - } - `, - }, - }, - }, - [ - { - message: functionWarnings.MULTI_ARGS_IN_VALUE('color1, color2'), - file: '/style.st.css', - }, - ] - ); - }); - - it('should return a warning when trying to access unknown custom-value entries', () => { - expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - :vars { - myVar: st-map( - key1 red, - key2 st-map( - key3 green - ) - ); - } - .root { - color: value(myVar, key1); - color: value(myVar, key2, key3); - |color: value(myVar, key2, key4)|; - } - `, - }, - }, - }, - [ - { - message: functionWarnings.COULD_NOT_RESOLVE_VALUE('myVar, key2, key4'), - file: '/style.st.css', - }, - ] - ); - }); - - it('should return multiple warnings for unknown custom-value keys and too many arguments in a simple var', () => { - expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - :vars { - v1: red; - v2: green; - myVar: st-map( - key1 red, - key2 st-map( - key3 green - ) - ); - } - .root { - |background: value(myVar, key2, key4), value(v1, v2)|; - } - `, - }, - }, - }, - [ - { - message: functionWarnings.COULD_NOT_RESOLVE_VALUE('myVar, key2, key4'), - file: '/style.st.css', - }, - { - message: functionWarnings.MULTI_ARGS_IN_VALUE('v1, v2'), - file: '/style.st.css', - }, - ] - ); - }); - - it('should return a warning when passing more than one argument to custom value with working fallback', () => { - const { meta } = expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - :vars { - v1: key3; - v2: key4; - myVar: st-map( - key1 red, - key2 st-map( - key3 green - ) - ); - } - .root { - |background: value(myVar, key2, value(v1, v2))|; - } - `, - }, - }, - }, - [ - { - message: functionWarnings.MULTI_ARGS_IN_VALUE('v1, v2'), - file: '/style.st.css', - }, - ] - ); - - expect( - ((meta.outputAst!.nodes[0] as postcss.Rule).nodes[0] as postcss.Declaration) - .value - ).to.eql('green'); - }); - - it('should return warning for unknown var on transform', () => { - expectTransformDiagnostics( - { - entry: '/style.st.css', - files: { - '/style.st.css': { - content: ` - .gaga{ - |color:value($myColor$)|; - } - `, - }, - }, - }, - [{ message: functionWarnings.UNKNOWN_VAR('myColor'), file: '/style.st.css' }] - ); - }); - - it('class cannot be used as var', () => { - const config = { - entry: '/main.st.css', - files: { - '/main.st.css': { - content: ` - :import{ - -st-from:"./style.st.css"; - -st-named:my-class; - } - .root{ - |color:value($my-class$)|; - } - `, - }, - '/style.st.css': { - content: ` - .my-class {} - `, - }, - }, - }; - expectTransformDiagnostics(config, [ - { - message: functionWarnings.CANNOT_USE_AS_VALUE('class', 'my-class'), - file: '/main.st.css', - }, - ]); - }); - - it('stylesheet cannot be used as var', () => { - const config = { - entry: '/main.st.css', - files: { - '/main.st.css': { - content: ` - :import{ - -st-from:"./file.st.css"; - -st-default:Comp; - } - .root{ - |color:value($Comp$)|; - } - `, - }, - '/file.st.css': { - content: '', - }, - }, - }; - expectTransformDiagnostics(config, [ - { - message: functionWarnings.CANNOT_USE_AS_VALUE('stylesheet', 'Comp'), - file: '/main.st.css', - }, - ]); - }); - - it('JS imports cannot be used as vars', () => { - const config = { - entry: '/main.st.css', - files: { - '/main.st.css': { - content: ` - :import{ - -st-from:"./mixins"; - -st-default:my-mixin; - } - .root{ - |color:value($my-mixin$)|; - } - `, - }, - '/mixins.js': { - content: `module.exports = function myMixin() {};`, - }, - }, - }; - expectTransformDiagnostics(config, [ - { - message: functionWarnings.CANNOT_USE_JS_AS_VALUE('my-mixin'), - file: '/main.st.css', - }, - ]); - }); - - it('should warn when encountering a cyclic dependency in a var definition', () => { - const config = { - entry: '/main.st.css', - files: { - '/main.st.css': { - content: ` - :vars { - a: value(b); - b: value(c); - |c: value(a)|; - } - .root{ - color: value(a); - } - `, - }, - }, - }; - const mainPath = '/main.st.css'; - expectTransformDiagnostics(config, [ - { - message: functionWarnings.CYCLIC_VALUE([ - `${mainPath}: a`, - `${mainPath}: b`, - `${mainPath}: c`, - `${mainPath}: a`, - ]), - file: mainPath, - }, - ]); - }); - }); - describe('formatters', () => { it('should warn when trying to use a missing formatter', () => { const key = 'print'; @@ -1102,29 +579,6 @@ describe('Stylable functions (native, formatter and variable)', () => { }, ]); }); - - it('should handle empty functions', () => { - expectTransformDiagnostics( - { - entry: `/style.st.css`, - files: { - '/style.st.css': { - content: ` - :vars { - a: 100px; - b: "max-width: 100px"; - } - .x{font-family: (aaa)} - @media screen (max-width: 100px) {} - @media screen (max-width: value(a)) {} - @media screen (value(b)) {} - `, - }, - }, - }, - [] - ); - }); }); describe('native', () => { diff --git a/packages/core/test/scope-directive.spec.ts b/packages/core/test/scope-directive.spec.ts index 960b74049..3b8c2f521 100644 --- a/packages/core/test/scope-directive.spec.ts +++ b/packages/core/test/scope-directive.spec.ts @@ -576,34 +576,5 @@ describe('@st-scope', () => { ]); expect((meta.outputAst!.first as Rule).selector).to.equal('.entry__part'); }); - - it('should warn about vars definition inside a scope', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - @st-scope .root { - |:vars { - myColor: red; - }| - - .part {} - } - `, - }, - }, - }; - - const { meta } = expectTransformDiagnostics(config, [ - { - message: processorWarnings.NO_VARS_DEF_IN_ST_SCOPE(), - file: '/entry.st.css', - severity: 'warning', - }, - ]); - expect((meta.outputAst!.first as Rule).selector).to.equal('.entry__root .entry__part'); - }); }); }); diff --git a/packages/core/test/stylable-processor.spec.ts b/packages/core/test/stylable-processor.spec.ts index 66167bbc3..d2bdd44a7 100644 --- a/packages/core/test/stylable-processor.spec.ts +++ b/packages/core/test/stylable-processor.spec.ts @@ -107,57 +107,6 @@ describe('Stylable postcss process', () => { expect(result.namespace).to.eql(processNamespace('style', from)); }); - it('collect :vars', () => { - const result = processSource( - ` - :vars { - name: value; - } - :vars { - name: value; - name1: value1; - } - `, - { from: 'path/to/style.css' } - ); - - expect(result.vars.length).to.eql(3); - }); - - it('collect :vars types', () => { - const result = processSource( - ` - :vars { - /*@type VALUE_INLINE*/name: inline; - /*@type VALUE_LINE_BEFORE*/ - name1: line before; - } - `, - { from: 'path/to/style.css' } - ); - - expect(result.vars[0].valueType).to.eql('VALUE_INLINE'); - expect(result.vars[1].valueType).to.eql('VALUE_LINE_BEFORE'); - }); - - it('resolve local :vars (dont warn if name is imported)', () => { - // ToDo: check if test is needed - const result = processSource( - ` - :import { - -st-from: "./file.css"; - -st-named: name; - } - :vars { - myname: value(name); - } - `, - { from: 'path/to/style.css' } - ); - - expect(result.diagnostics.reports.length, 'no reports').to.eql(0); - }); - it('collect typed classes extends', () => { const result = processSource( ` diff --git a/packages/core/test/stylable-transformer/exports.spec.ts b/packages/core/test/stylable-transformer/exports.spec.ts index b2fd7fdd7..b9d98c689 100644 --- a/packages/core/test/stylable-transformer/exports.spec.ts +++ b/packages/core/test/stylable-transformer/exports.spec.ts @@ -450,135 +450,6 @@ describe('Exports to js', () => { }); }); - describe('stylable vars', () => { - it('contains local vars', () => { - const cssExports = generateStylableExports({ - entry: '/entry.st.css', - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - color1: red; - } - `, - }, - }, - }); - - expect(cssExports.stVars).to.eql({ - color1: 'red', - }); - }); - - it('should not contain imported vars', () => { - const cssExports = generateStylableExports({ - entry: '/entry.st.css', - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: color1; - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :vars { - color1: red; - } - `, - }, - }, - }); - - expect(cssExports.stVars).to.eql({}); - }); - - it('should not resolve imported vars value on exported var', () => { - const cssExports = generateStylableExports({ - entry: '/entry.st.css', - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-named: color1; - } - :vars { - color2: value(color1); - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :vars { - color1: red; - } - `, - }, - }, - }); - - expect(cssExports.stVars).to.eql({ - color2: 'red', - }); - }); - - it('should export custom values using their data structure', () => { - const cssExports = generateStylableExports({ - entry: '/entry.st.css', - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - myArray: st-array(1, 2, 3); - deepArray: st-array(1, st-array(2, 3), value(myArray)); - object: st-map(x 1, y 2); - deepObject: st-map(x 1, y 2, z st-map(x 1, y 2)); - mixed: st-map(x 1, y st-array(2, 3, st-array(4, st-map(z 5)))); - } - `, - }, - }, - }); - - expect(cssExports.stVars).to.eql({ - myArray: ['1', '2', '3'], - deepArray: ['1', ['2', '3'], ['1', '2', '3']], - object: { x: '1', y: '2' }, - deepObject: { x: '1', y: '2', z: { x: '1', y: '2' } }, - mixed: { x: '1', y: ['2', '3', ['4', { z: '5' }]] }, - }); - }); - - it('should preserve escaping', () => { - const cssExports = generateStylableExports({ - entry: '/entry.st.css', - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - color\\.1: red; - } - `, - }, - }, - }); - - expect(cssExports.stVars).to.eql({ - 'color\\.1': 'red', - }); - }); - }); - describe('complex example', () => { it('with classes, vars, st-vars, and keyframes', () => { const cssExports = generateStylableExports({ @@ -587,17 +458,8 @@ describe('Exports to js', () => { '/entry.st.css': { namespace: 'entry', content: ` - :vars { - stVar: green; - } - - @keyframes name { - - } - - .root { - --cssVar: blue; - } + @keyframes name {} + .root {} .part {} `, }, @@ -609,12 +471,8 @@ describe('Exports to js', () => { root: 'entry__root', part: 'entry__part', }, - vars: { - cssVar: '--entry-cssVar', - }, - stVars: { - stVar: 'green', - }, + vars: {}, + stVars: {}, keyframes: { name: 'entry__name', }, diff --git a/packages/core/test/stylable-transformer/general.spec.ts b/packages/core/test/stylable-transformer/general.spec.ts index 9f44bd5a7..614bf5769 100644 --- a/packages/core/test/stylable-transformer/general.spec.ts +++ b/packages/core/test/stylable-transformer/general.spec.ts @@ -16,23 +16,6 @@ describe('Stylable postcss transform (General)', () => { expect(result.toString()).to.equal(''); }); - it('should not output :vars', () => { - const result = generateStylableRoot({ - entry: `/a/b/style.st.css`, - files: { - '/a/b/style.st.css': { - content: ` - :vars { - myvar: red; - } - `, - }, - }, - }); - - expect(result.nodes.length, 'remove all vars').to.equal(0); - }); - it('should support multiple selectors/properties with same name', () => { const result = generateStylableRoot({ entry: `/a/b/style.st.css`, diff --git a/packages/core/test/stylable-transformer/post-process-and-hooks.spec.ts b/packages/core/test/stylable-transformer/post-process-and-hooks.spec.ts deleted file mode 100644 index 97b1c12b6..000000000 --- a/packages/core/test/stylable-transformer/post-process-and-hooks.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { expect } from 'chai'; -import type * as postcss from 'postcss'; -import { createTransformer } from '@stylable/core-test-kit'; - -describe('post-process-and-hooks', () => { - it("should call postProcess after transform and use it's return value", () => { - const t = createTransformer( - { - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - param: "red"; - param1: green; - } - .container { - color: value(param); - background: value(param1); - } - `, - }, - }, - }, - { - postProcessor: (res) => { - return { ...res, postProcessed: true }; - }, - } - ); - - const res = t.transform(t.fileProcessor.process('/entry.st.css')); - - expect(res).to.contain({ postProcessed: true }); - }); - - it('should call replaceValueHook on js function', () => { - const t = createTransformer( - { - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: './function.js'; - -st-named: fn1, fn2; - } - .container { - color: fn1(fn2(1)); - } - `, - }, - '/function.js': { - content: ` - module.exports.fn1 = function(x){return 'fn1'} - module.exports.fn2 = function(x){return 'fn2'} - `, - }, - }, - }, - { - replaceValueHook: (_resolved, fn) => { - if (typeof fn !== 'string') { - return `hooked_${fn.name}(${fn.args})`; - } - return ''; - }, - } - ); - - const res = t.transform(t.fileProcessor.process('/entry.st.css')); - const rule = res.meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('hooked_fn1(hooked_fn2(1))'); - }); - - it("should call replaceValueHook and use it's return value", () => { - let valueCallCount = 0; - const t = createTransformer( - { - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - param: "red"; - param1: green; - } - .container { - color: value(param); - background: value(param1); - } - `, - }, - }, - }, - { - replaceValueHook: (resolved, name, isLocal) => { - return `__VALUE__${valueCallCount++} ${resolved}-${name}-${isLocal}`; - }, - } - ); - - const res = t.transform(t.fileProcessor.process('/entry.st.css')); - const rule = res.meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('__VALUE__0 red-param-true'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal( - '__VALUE__1 green-param1-true' - ); - }); - - it('should call replaceValueHook on mixin overrides', () => { - let index = 0; - - const t = createTransformer( - { - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./style.st.css"; - -st-default: Style - } - :vars { - myColor: red; - myBG: green; - } - .root { - -st-mixin: Style(param value(myColor), param1 value(myBG)); - } - `, - }, - '/style.st.css': { - namespace: 'style', - content: ` - :import { - -st-from: "./style1.st.css"; - -st-named: x - } - :vars { - param: red; - param1: green; - param2: "Ariel"; - } - .root { - -st-mixin: x(var1 value(param2)); - color: value(param); - background: value(param1); - font-family: value(param2); - } - `, - }, - '/style1.st.css': { - namespace: 'style1', - content: ` - :vars { - var1: green; - } - .x { - border: 4px solid value(var1); - } - `, - }, - }, - }, - { - replaceValueHook: (resolved, name, isLocal, path) => { - const m = expected[index]; - expect( - [resolved, name, isLocal, path], - [resolved, name, isLocal, path].join(',') - ).to.eqls(m); - index++; - return isLocal && path.length === 0 ? `[${name}]` : resolved; - }, - } - ); - - const expected = [ - ['red', 'myColor', true, []], - ['green', 'myBG', true, []], - ['Ariel', 'param2', true, [`default from /entry.st.css`]], - ['Ariel', 'param2', true, [`default from /entry.st.css`]], - ]; - - t.transform(t.fileProcessor.process('/entry.st.css')); - }); -}); diff --git a/packages/core/test/stylable-transformer/post-process.spec.ts b/packages/core/test/stylable-transformer/post-process.spec.ts new file mode 100644 index 000000000..3ec180385 --- /dev/null +++ b/packages/core/test/stylable-transformer/post-process.spec.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import { createTransformer } from '@stylable/core-test-kit'; + +describe('post-process', () => { + it("should call postProcess after transform and use it's return value", () => { + const t = createTransformer( + { + files: { + '/entry.st.css': { + namespace: 'entry', + content: ``, + }, + }, + }, + { + postProcessor: (res) => { + return { ...res, postProcessed: true }; + }, + } + ); + + const res = t.transform(t.fileProcessor.process('/entry.st.css')); + + expect(res).to.contain({ postProcessed: true }); + }); +}); diff --git a/packages/core/test/stylable-transformer/value.spec.ts b/packages/core/test/stylable-transformer/value.spec.ts deleted file mode 100644 index 1383d03b2..000000000 --- a/packages/core/test/stylable-transformer/value.spec.ts +++ /dev/null @@ -1,747 +0,0 @@ -import { expect } from 'chai'; -import type * as postcss from 'postcss'; -import postcssValueParser from 'postcss-value-parser'; -import { generateStylableResult, generateStylableRoot } from '@stylable/core-test-kit'; -import { box, CustomValueExtension, functionWarnings, stTypes } from '@stylable/core'; - -describe('Generator variables interpolation', () => { - it('should inline value() usage with and without quotes', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - param: "red"; - param1: green; - } - .container { - color: value(param); - background: value(param1); - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('green'); - }); - - it('should resolve value inside @media', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - xxl: "(max-width: 301px)"; - } - @media value(xxl) {} - `, - }, - }, - }); - - expect((result.nodes[0] as postcss.AtRule).params).to.equal('(max-width: 301px)'); - }); - - it('should resolve value() usage in variable declaration', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - param2: red; - param: value(param2); - } - .container { - color: value(param); - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - }); - - it('should resolve to recursive entry', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - param1: value(param2); - param2: value(param3); - param3: value(param1); - } - .container { - color: value(param1); - } - `, - }, - }, - }); - - const rule = result.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('value(param1)'); - }); - - it('should support imported vars', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: './imported.st.css'; - -st-named: param1, param2; - } - :vars { - param: value(param1); - } - .container { - color: value(param); - background-color: value(param2) - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :vars { - param1: red; - param2: blue; - } - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('blue'); - }); - - it('should support imported vars (deep)', () => { - const result = generateStylableRoot({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: './imported.st.css'; - -st-named: param1, param2; - } - :vars { - param: value(param1); - } - .container { - color: value(param); - background-color: value(param2) - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :import { - -st-from: './deep.st.css'; - -st-named: param0; - } - :vars { - param1: value(param0); - param2: blue; - } - `, - }, - '/deep.st.css': { - namespace: 'deep', - content: ` - :vars { - param0: red; - } - `, - }, - }, - }); - const rule = result.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('blue'); - }); - - it('should resolve a variable inside unknown functions', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - param: green; - } - .container { - color: xxx(value(param)); - } - `, - }, - }, - }); - - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('xxx(green)'); - expect(meta.transformDiagnostics!.reports[0].message).to.equal( - functionWarnings.UNKNOWN_FORMATTER('xxx') - ); - }); - - xit('should resolve value() usage in mixin call', () => { - // const env = defineStylableEnv([ - // JS('./mixins.js', 'Mixins', { - // mixin(options: string[]) { - // return { - // color: options[0], - // }; - // }, - // otherMixin(options: string[]) { - // return { - // backgroundColor: options[0], - // }; - // }, - // noParamsMixin() { - // return { - // borderColor: 'orange', - // }; - // } - // }), - // CSS('./main.css', 'Main', ` - // :import("./mixins.js") { - // -st-named: mixin, otherMixin, noParamsMixin; - // } - // :vars { - // param: red; - // } - // .container { - // -st-mixin: mixin(value(param)) noParamsMixin otherMixin(blue); - // } - // `) - // ], {}); - // env.validate.output([ - // '.Main__container {\n background-color: blue\n}', - // '.Main__container {\n border-color: orange\n}', - // '.Main__container {\n color: red/*param*/\n}' - // ]); // ToDo: fix order and combine into a single CSS ruleset - }); - - describe('custom stylable variables', () => { - it('should support imported typed values from js', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.js"; - -st-default: CustomValue; - } - :vars { - customValue: CustomValue(); - } - .root { - border: value(customValue); - } - `, - }, - '/imported.js': { - namespace: 'imported', - content: ` - module.exports = { - _kind: 'CustomValue', - register(id){ - return { - evalVarAst() { - - }, - getValue() { - return \`my custom value \${id}\` - } - } - } - } - `, - }, - }, - }); - const root = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((root.nodes[0] as postcss.Declaration).value).to.equal( - 'my custom value CustomValue' - ); - }); - - describe('st-map', () => { - it('should support st-map type', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - otherColor: red; - colors: st-map( - bg orange, - text green - ); - } - .root { - background-color: value(otherColor); - color: value(colors, text); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('green'); - }); - - it('should support st-map type with deep structure', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - color1: stColor(red); - designs: st-map( - bg green, - box st-map( - border 1px solid green, - font monospace - ) - ); - } - .root { - background-color: value(designs, bg); - color: value(designs, box, border); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('green'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('1px solid green'); - }); - - it('should support st-map type with imported deep structure', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: '/imported.st.css'; - -st-named: colors - } - .root { - background-color: value(colors, bg); - color: value(colors, text, body); - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :vars { - colors: st-map( - bg red, - text st-map( - header gold, - body green - ) - ); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('green'); - }); - - it('should support st-map type with var usage in variable invocation', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - content: text; - colors: st-map( - bg red, - text green - ); - } - .root { - background-color: value(colors, bg); - color: value(colors, value(content)); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('green'); - }); - - it('should support st-map type with imported var usage in variable invocation', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: '/imported.st.css'; - -st-named: colors; - } - :vars { - key: bg; - } - .root { - background-color: value(colors, value(key)); - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :vars { - colors: st-map( - bg red, - text green - ); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - }); - - it('should support st-map type with inner var in definition', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - size: 1px; - style: solid; - color: green; - borders: st-map( - border1 2px dashed red, - border2 value(size) value(style) value(color) - ); - } - .root { - border: value(borders, border1); - } - .part { - border: value(borders, border2); - } - `, - }, - }, - }); - const root = meta.outputAst!.nodes[0] as postcss.Rule; - const part = meta.outputAst!.nodes[1] as postcss.Rule; - - expect((root.nodes[0] as postcss.Declaration).value).to.equal('2px dashed red'); - expect((part.nodes[0] as postcss.Declaration).value).to.equal('1px solid green'); - }); - }); - - describe('st-array', () => { - it('should support st-array type', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - otherColor: red; - colors: st-array(yellow, green); - } - .root { - background-color: value(otherColor); - border-color: value(colors, 0); - color: value(colors, 1); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('yellow'); - expect((rule.nodes[2] as postcss.Declaration).value).to.equal('green'); - }); - - it('should support imported st-array type', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: './imported.st.css'; - -st-named: colors; - } - .root { - background-color: value(colors, 0); - color: value(colors, 1); - } - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - :vars { - colors: st-array(red, green); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('red'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('green'); - }); - }); - - describe('complex examples', () => { - it('should support an st-map nested inside an st-array type', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - otherColor: red; - borders: st-array( - st-map( - size 1px, - style solid, - color red - ), - st-map( - size 3px, - style dashed, - color yellow - ), - st-map( - size 5px, - style dotted, - color green - ) - ); - } - .root { - border: value(borders, 0, size) value(borders, 0, style) value(borders, 0, color); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('1px solid red'); - }); - - it('should support an st-array nested inside an st-map type', () => { - const { meta } = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - otherColor: red; - colors: st-map( - reds st-array( - rgb(100, 0, 0), - rgb(255, 0, 0) - ), - greens st-array( - rgb(0, 100, 0), - rgb(0, 255, 0) - ) - ); - } - .root { - background-color: value(colors, reds, 0); - color: value(colors, reds, 1); - } - `, - }, - }, - }); - const rule = meta.outputAst!.nodes[0] as postcss.Rule; - - expect((rule.nodes[0] as postcss.Declaration).value).to.equal('rgb(100, 0, 0)'); - expect((rule.nodes[1] as postcss.Declaration).value).to.equal('rgb(255, 0, 0)'); - }); - }); - - describe('Custom type contract', () => { - type GetCustomTypeExtensionValueType = T extends CustomValueExtension - ? U - : never; - - function contract( - desc: string, - { typeDef, path }: { typeDef: string; path: string[] }, - { - matchValue, - match, - }: { - matchValue(value: GetCustomTypeExtensionValueType): void; - match(value: string): void; - } - ) { - describe('Api Test: ' + desc, () => { - const valueAst = postcssValueParser(typeDef).nodes[0]; - const typeExtension = stTypes[valueAst.value]; - - it('should create a runtime value from ast', () => { - matchValue(typeExtension.evalVarAst(valueAst, stTypes).value); - }); - it('should get a string value form path', () => { - match( - typeExtension.getValue( - path, - typeExtension.evalVarAst(valueAst, stTypes), - valueAst, - stTypes - ) - ); - }); - }); - } - - contract( - 'basic st-map functionality', - { typeDef: 'st-map(k1 v1, k2 v2)', path: ['k1'] }, - { - matchValue: (map) => expect(map).to.eql({ k1: 'v1', k2: 'v2' }), - match: (value) => expect(value).to.equal('v1'), - } - ); - - contract( - 'nested st-map functionality', - { - typeDef: 'st-map(k1 v1, k2 st-map(k3 v3, k4 st-map(k5 v5) ))', - path: ['k2', 'k4', 'k5'], - }, - { - matchValue: (map) => - expect(map).to.deep.include({ - k1: 'v1', - k2: box('st-map', { - k3: 'v3', - k4: box('st-map', { - k5: 'v5', - }), - }), - }), - match: (value) => expect(value).to.equal('v5'), - } - ); - - contract( - 'basic st-array functionality', - { typeDef: 'st-array(v0, v1)', path: ['1'] }, - { - matchValue: (array) => expect(array).to.eql(['v0', 'v1']), - match: (value) => expect(value).to.equal('v1'), - } - ); - - contract( - 'nested st-array functionality', - { typeDef: 'st-array(v0, st-array(v1))', path: ['1', '0'] }, - { - matchValue: (array) => expect(array).to.eql(['v0', box('st-array', ['v1'])]), - match: (value) => expect(value).to.equal('v1'), - } - ); - - contract( - 'complex nested st-array/st-map/st-array functionality', - { typeDef: 'st-array(v0, st-map(k2 st-array(v2))', path: ['1', 'k2', '0'] }, - { - matchValue: (array) => - expect(array).to.eql([ - 'v0', - box('st-map', { k2: box('st-array', ['v2']) }), - ]), - match: (value) => expect(value).to.equal('v2'), - } - ); - - contract( - 'complex nested st-map/st-array/st-map functionality', - { - typeDef: 'st-map(k0 v0, k1 st-array(v2, st-map(k3 v3)))', - path: ['k1', '1', 'k3'], - }, - { - matchValue: (array) => - expect(array).to.eql({ - k0: 'v0', - k1: box('st-array', ['v2', box('st-map', { k3: 'v3' })]), - }), - match: (value) => expect(value).to.equal('v3'), - } - ); - }); - }); -}); diff --git a/packages/language-service/src/lib/completion-providers.ts b/packages/language-service/src/lib/completion-providers.ts index f6ad76e65..af4dffe27 100644 --- a/packages/language-service/src/lib/completion-providers.ts +++ b/packages/language-service/src/lib/completion-providers.ts @@ -1473,7 +1473,7 @@ export const ValueCompletionProvider: CompletionProvider = { .trim(); const comps: Completion[] = []; - meta.vars.forEach((v) => { + Object.values(meta.getAllStVars()).forEach((v) => { if (v.name.startsWith(inner)) { const value = evalDeclarationValue(stylable.resolver, v.text, meta, v.node); comps.push( @@ -1497,7 +1497,9 @@ export const ValueCompletionProvider: CompletionProvider = { meta.getImportStatements().forEach((imp) => { try { const resolvedPath = stylable.resolvePath(dirname(meta.source), imp.request); - stylable.fileProcessor.process(resolvedPath).vars.forEach((v) => + Object.values( + stylable.fileProcessor.process(resolvedPath).getAllStVars() + ).forEach((v) => importVars.push({ name: v.name, value: v.text, diff --git a/packages/language-service/src/lib/feature/color-provider.ts b/packages/language-service/src/lib/feature/color-provider.ts index dcff2675e..e3621db00 100644 --- a/packages/language-service/src/lib/feature/color-provider.ts +++ b/packages/language-service/src/lib/feature/color-provider.ts @@ -50,7 +50,7 @@ export function resolveDocumentColors( const impMeta = processor.process( stylable.resolvePath(dirname(meta.source), sym.import.request) ); - const relevantVar = impMeta.vars.find((v) => v.name === sym.name); + const relevantVar = Object.values(impMeta.getAllStVars()).find((v) => v.name === sym.name); if (relevantVar) { const doc = TextDocument.create( '', diff --git a/packages/module-utils/src/generate-dts-sourcemaps.ts b/packages/module-utils/src/generate-dts-sourcemaps.ts index 69fc491a2..dca9460de 100644 --- a/packages/module-utils/src/generate-dts-sourcemaps.ts +++ b/packages/module-utils/src/generate-dts-sourcemaps.ts @@ -51,7 +51,7 @@ function getVarsSrcPosition(varName: string, meta: StylableMeta): Position | und } function getStVarsSrcPosition(varName: string, meta: StylableMeta): Position | undefined { - const stVar = meta.vars.find((v) => v.name === varName); + const stVar = Object.values(meta.getAllStVars()).find((v) => v.name === varName); if (stVar?.node.source?.start) { return {