@stylable/core
Version:
CSS for Components
406 lines • 18.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StylablePublicApi = exports.hooks = exports.diagnostics = exports.MixinType = void 0;
const feature_1 = require("./feature");
const STSymbol = __importStar(require("./st-symbol"));
const STCustomSelector = __importStar(require("./st-custom-selector"));
const STVar = __importStar(require("./st-var"));
const rule_1 = require("../helpers/rule");
const selector_1 = require("../helpers/selector");
const mixin_1 = require("../helpers/mixin");
const functions_1 = require("../functions");
const parser_1 = require("../parser");
const postcss = __importStar(require("postcss"));
const postcss_value_parser_1 = __importDefault(require("postcss-value-parser"));
const stylable_assets_1 = require("../stylable-assets");
const stylable_utils_1 = require("../stylable-utils");
const path_1 = require("path");
const diagnostics_1 = require("../diagnostics");
const css_selector_parser_1 = require("@tokey/css-selector-parser");
exports.MixinType = {
ALL: `-st-mixin`,
PARTIAL: `-st-partial-mixin`,
};
exports.diagnostics = {
VALUE_CANNOT_BE_STRING: mixin_1.mixinHelperDiagnostics.VALUE_CANNOT_BE_STRING,
INVALID_NAMED_PARAMS: mixin_1.mixinHelperDiagnostics.INVALID_NAMED_PARAMS,
INVALID_MERGE_OF: stylable_utils_1.utilDiagnostics.INVALID_MERGE_OF,
INVALID_RECURSIVE_MIXIN: stylable_utils_1.utilDiagnostics.INVALID_RECURSIVE_MIXIN,
PARTIAL_MIXIN_MISSING_ARGUMENTS: (0, diagnostics_1.createDiagnosticReporter)('10001', 'error', (type) => `"${exports.MixinType.PARTIAL}" can only be used with override arguments provided, missing overrides on "${type}"`),
UNKNOWN_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10002', 'error', (name) => `unknown mixin: "${name}"`),
OVERRIDE_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10003', 'warning', (mixinType) => `override ${mixinType} on same rule`),
FAILED_TO_APPLY_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10004', 'error', (error) => `could not apply mixin: ${error}`),
JS_MIXIN_NOT_A_FUNC: (0, diagnostics_1.createDiagnosticReporter)('10005', 'error', () => `js mixin must be a function`),
UNSUPPORTED_MIXIN_SYMBOL: (0, diagnostics_1.createDiagnosticReporter)('10007', 'error', (name, symbolType) => `cannot mix unsupported symbol "${name}" of type "${STSymbol.readableTypeMap[symbolType]}"`),
CIRCULAR_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10006', 'error', (circularPaths) => `circular mixin found: ${circularPaths.join(' --> ')}`),
UNKNOWN_ARG: (0, diagnostics_1.createDiagnosticReporter)('10009', 'warning', (argName) => `unknown mixin argument "${argName}"`),
};
// HOOKS
exports.hooks = (0, feature_1.createFeature)({
transformSelectorNode({ selectorContext, node }) {
const isMarker = (0, rule_1.isStMixinMarker)(node);
if (isMarker) {
selectorContext.setNextSelectorScope(selectorContext.inferredSelectorMixin, node, node.value);
}
return isMarker;
},
transformLastPass({ context, ast, transformer, path }) {
ast.walkRules((rule) => appendMixins(context, transformer, rule, path));
},
});
// API
class StylablePublicApi {
constructor(stylable) {
this.stylable = stylable;
}
resolveExpr(meta, expr, { diagnostics = new diagnostics_1.Diagnostics(), resolveOptionalArgs = false, } = {}) {
const resolvedSymbols = this.stylable.resolver.resolveSymbols(meta, diagnostics);
const { mainNamespace } = resolvedSymbols;
const analyzedMixins = collectDeclMixins({ meta, diagnostics }, resolvedSymbols, postcss.decl({ prop: '-st-mixin', value: expr }), (mixinSymbolName) => (mainNamespace[mixinSymbolName] === 'js' ? 'args' : 'named'));
const result = [];
for (const { data } of analyzedMixins) {
const name = data.type;
const symbolKind = mainNamespace[name];
if (symbolKind === 'class' || symbolKind === 'element') {
const mixRef = {
name,
kind: 'css-fragment',
args: [],
optionalArgs: new Map(),
};
for (const [argName, argValue] of Object.entries(data.options)) {
mixRef.args.push({ [argName]: argValue });
}
if (resolveOptionalArgs) {
const varMap = new Map();
const resolveChain = resolvedSymbols[symbolKind][name];
getCSSMixinRoots(meta, resolveChain, ({ mixinRoot }) => {
const names = new Set();
collectOptionalArgs({ meta, resolver: this.stylable.resolver }, mixinRoot, names);
names.forEach((name) => varMap.set(name, { name }));
});
mixRef.optionalArgs = varMap;
}
result.push(mixRef);
}
else if (symbolKind === 'js' &&
typeof resolvedSymbols.js[name].symbol === 'function') {
const mixRef = {
name,
kind: 'js-func',
args: [],
func: resolvedSymbols.js[name].symbol,
};
for (const arg of Object.values(data.options)) {
mixRef.args.push(arg.value);
}
result.push(mixRef);
}
else {
result.push({
name,
kind: 'invalid',
args: data.valueNode?.type === 'function'
? postcss_value_parser_1.default.stringify(data.valueNode.nodes)
: '',
});
}
}
return result;
}
scopeNestedSelector(scopeSelector, nestSelector) {
return (0, selector_1.scopeNestedSelector)((0, css_selector_parser_1.parseCssSelector)(scopeSelector), (0, css_selector_parser_1.parseCssSelector)(nestSelector))
.selector;
}
}
exports.StylablePublicApi = StylablePublicApi;
function appendMixins(context, transformer, rule, path = []) {
const [decls, mixins] = collectRuleMixins(context, rule);
if (!mixins || mixins.length === 0) {
return;
}
for (const mixin of mixins) {
if (mixin.valid) {
appendMixin(context, { transformer, mixDef: mixin, rule, path });
}
}
for (const mixinDecl of decls) {
mixinDecl.remove();
}
}
function collectRuleMixins(context, rule) {
let mixins = [];
const resolvedSymbols = context.getResolvedSymbols(context.meta);
const { mainNamespace } = resolvedSymbols;
const decls = [];
for (const node of rule.nodes) {
if (node.type === 'decl' &&
(node.prop === `-st-mixin` || node.prop === `-st-partial-mixin`)) {
decls.push(node);
mixins = collectDeclMixins(context, resolvedSymbols, node, (mixinSymbolName) => {
return mainNamespace[mixinSymbolName] === 'js' ? 'args' : 'named';
}, mixins);
}
}
return [decls, mixins];
}
function collectDeclMixins(context, resolvedSymbols, decl, paramSignature, previousMixins) {
const { meta } = context;
let mixins = [];
const parser = decl.prop === exports.MixinType.ALL
? mixin_1.parseStMixin
: decl.prop === exports.MixinType.PARTIAL
? mixin_1.parseStPartialMixin
: null;
if (!parser) {
return previousMixins || mixins;
}
parser(decl, paramSignature, context.diagnostics, /*emitStrategyDiagnostics*/ true).forEach((mixin) => {
const mixinRefSymbol = STSymbol.get(meta, mixin.type);
const symbolName = mixin.type;
const resolvedType = resolvedSymbols.mainNamespace[symbolName];
if (resolvedType &&
((resolvedType === 'js' &&
typeof resolvedSymbols.js[symbolName].symbol === 'function') ||
resolvedType === 'class' ||
resolvedType === 'element')) {
mixins.push({
valid: true,
data: mixin,
symbol: mixinRefSymbol,
});
if (mixin.partial && Object.keys(mixin.options).length === 0) {
context.diagnostics.report(exports.diagnostics.PARTIAL_MIXIN_MISSING_ARGUMENTS(mixin.type), {
node: decl,
word: mixin.type,
});
}
}
else {
mixins.push({
valid: false,
data: mixin,
symbol: mixinRefSymbol,
});
if (resolvedType === 'js') {
context.diagnostics.report(exports.diagnostics.JS_MIXIN_NOT_A_FUNC(), {
node: decl,
word: mixin.type,
});
}
else if (resolvedType) {
context.diagnostics.report(exports.diagnostics.UNSUPPORTED_MIXIN_SYMBOL(mixin.type, resolvedType), {
node: decl,
word: mixin.type,
});
}
else {
context.diagnostics.report(exports.diagnostics.UNKNOWN_MIXIN(mixin.type), {
node: decl,
word: mixin.type,
});
}
}
});
if (previousMixins) {
const partials = previousMixins.filter((r) => r.data.partial);
const nonPartials = previousMixins.filter((r) => !r.data.partial);
const isInPartial = decl.prop === exports.MixinType.PARTIAL;
if ((partials.length && decl.prop === exports.MixinType.PARTIAL) ||
(nonPartials.length && decl.prop === exports.MixinType.ALL)) {
context.diagnostics.report(exports.diagnostics.OVERRIDE_MIXIN(decl.prop), { node: decl });
}
if (partials.length && nonPartials.length) {
mixins = isInPartial ? nonPartials.concat(mixins) : partials.concat(mixins);
}
else if (partials.length) {
mixins = isInPartial ? mixins : partials.concat(mixins);
}
else if (nonPartials.length) {
mixins = isInPartial ? nonPartials.concat(mixins) : mixins;
}
}
return mixins;
}
function appendMixin(context, config) {
if (checkRecursive(context, config)) {
return;
}
const resolvedSymbols = context.getResolvedSymbols(context.meta);
const symbolName = config.mixDef.data.type;
const resolvedType = resolvedSymbols.mainNamespace[symbolName];
if (resolvedType === `class` || resolvedType === `element`) {
const resolveChain = resolvedSymbols[resolvedType][symbolName];
handleCSSMixin(context, config, resolveChain);
return;
}
else if (resolvedType === `js`) {
const jsValue = resolvedSymbols.js[symbolName].symbol;
if (typeof jsValue === 'function') {
try {
handleJSMixin(context, config, jsValue);
}
catch (e) {
context.diagnostics.report(exports.diagnostics.FAILED_TO_APPLY_MIXIN(String(e)), {
node: config.rule,
word: config.mixDef.data.type,
});
return;
}
}
return;
}
}
function checkRecursive({ meta, diagnostics: report }, { mixDef, path, rule }) {
const symbolName = mixDef.symbol.name === meta.root
? mixDef.symbol._kind === 'class'
? meta.root
: 'default'
: mixDef.data.type;
const isRecursive = path.includes(symbolName + ' from ' + meta.source);
if (isRecursive) {
// Todo: add test verifying word
report.report(exports.diagnostics.CIRCULAR_MIXIN(path), {
node: rule,
word: symbolName,
});
return true;
}
return false;
}
function handleJSMixin(context, config, mixinFunction) {
const stVarOverride = context.evaluator.stVarOverride || {};
const meta = context.meta;
const mixDef = config.mixDef;
const res = mixinFunction(mixDef.data.options.map((v) => v.value));
const mixinRoot = (0, parser_1.cssObjectToAst)(res);
mixinRoot.walkDecls((decl) => {
if (!(0, stylable_utils_1.isValidDeclaration)(decl)) {
decl.value = String(decl);
}
});
config.transformer.transformAst(mixinRoot, meta, undefined, stVarOverride, [], true);
const mixinPath = mixDef.symbol.import.request;
(0, stylable_assets_1.fixRelativeUrls)(mixinRoot, context.resolver.resolvePath((0, path_1.dirname)(meta.source), mixinPath), meta.source);
(0, stylable_utils_1.mergeRules)(mixinRoot, config.rule, mixDef.data.originDecl, context.diagnostics, true);
}
function handleCSSMixin(context, config, resolveChain) {
const mixDef = config.mixDef;
const isPartial = mixDef.data.partial;
const namedArgs = mixDef.data.options;
const overrideKeys = Object.keys(namedArgs);
if (isPartial && overrideKeys.length === 0) {
return;
}
const optionalArgs = new Set();
const roots = getCSSMixinRoots(context.meta, resolveChain, ({ mixinRoot, resolved, isRootMixin }) => {
const stVarOverride = context.evaluator.stVarOverride || {};
const mixDef = config.mixDef;
const namedArgs = mixDef.data.options;
if (mixDef.data.partial) {
filterPartialMixinDecl(context.meta, mixinRoot, Object.keys(namedArgs));
}
// resolve override args
const resolvedArgs = (0, functions_1.resolveArgumentsValue)(namedArgs, config.transformer, context.meta, context.diagnostics, mixDef.data.originDecl, stVarOverride, config.path);
collectOptionalArgs({ meta: resolved.meta, resolver: context.resolver }, mixinRoot, optionalArgs);
// transform mixin
const mixinMeta = resolved.meta;
const symbolName = isRootMixin && resolved.meta !== context.meta ? 'default' : mixDef.data.type;
config.transformer.transformAst(mixinRoot, mixinMeta, undefined, resolvedArgs, config.path.concat(symbolName + ' from ' + context.meta.source), true, config.transformer.createInferredSelector(mixinMeta, {
name: resolved.symbol.name,
type: resolved.symbol._kind,
}));
(0, stylable_assets_1.fixRelativeUrls)(mixinRoot, resolved.meta.source, context.meta.source);
});
for (const overrideArg of overrideKeys) {
if (!optionalArgs.has(overrideArg)) {
context.diagnostics.report(exports.diagnostics.UNKNOWN_ARG(overrideArg), {
node: mixDef.data.originDecl,
word: overrideArg,
});
}
}
if (roots.length === 1) {
(0, stylable_utils_1.mergeRules)(roots[0], config.rule, mixDef.data.originDecl, config.transformer.diagnostics, false);
}
else if (roots.length > 1) {
const mixinRoot = postcss.root();
roots.forEach((root) => mixinRoot.prepend(...root.nodes));
(0, stylable_utils_1.mergeRules)(mixinRoot, config.rule, mixDef.data.originDecl, config.transformer.diagnostics, false);
}
}
function collectOptionalArgs(context, mixinRoot, optionalArgs = new Set()) {
mixinRoot.walk((node) => {
const value = node.type === 'decl' ? node.value : node.type === 'atrule' ? node.params : '';
const varNames = STVar.parseVarsFromExpr(value);
for (const name of varNames) {
for (const refName of STVar.resolveReferencedVarNames(context, name)) {
optionalArgs.add(refName);
}
}
});
}
function getCSSMixinRoots(contextMeta, resolveChain, processMixinRoot) {
const roots = [];
for (const resolved of resolveChain) {
const isRootMixin = resolved.symbol.name === resolved.meta.root;
const mixinRoot = (0, rule_1.createSubsetAst)(resolved.meta.sourceAst, (resolved.symbol._kind === 'class' ? '.' : '') + resolved.symbol.name, undefined, isRootMixin, (name) => STCustomSelector.getCustomSelector(contextMeta, name));
processMixinRoot({ mixinRoot, resolved, isRootMixin });
roots.push(mixinRoot);
if (resolved.symbol[`-st-extends`]) {
break;
}
}
return roots;
}
/** we assume that mixinRoot is freshly created nodes from the ast */
function filterPartialMixinDecl(meta, mixinRoot, overrideKeys) {
let regexp;
const overrideSet = new Set(overrideKeys);
let size;
do {
size = overrideSet.size;
regexp = new RegExp(`value\\((\\s*${Array.from(overrideSet).join('\\s*)|(\\s*')}\\s*)\\)`);
for (const { text, name } of Object.values(meta.getAllStVars())) {
if (!overrideSet.has(name) && text.match(regexp)) {
overrideSet.add(name);
}
}
} while (overrideSet.size !== size);
mixinRoot.walkDecls((decl) => {
if (!decl.value.match(regexp)) {
const parent = decl.parent;
decl.remove();
if (parent?.nodes?.length === 0) {
parent.remove();
}
}
});
}
//# sourceMappingURL=st-mixin.js.map