UNPKG

@stylable/core

Version:

CSS for Components

524 lines 22.2 kB
"use strict"; 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.parseStExtends = exports.extendTypedRule = exports.disableDirectivesForClass = exports.checkForScopedNodeAfter = exports.validateClassScoping = exports.createWarningRule = exports.addDevRules = exports.namespaceClass = exports.addClass = exports.createSymbol = exports.getAll = exports.get = exports.StylablePublicApi = exports.hooks = exports.diagnostics = void 0; const feature_1 = require("./feature"); const diagnostics_1 = require("./diagnostics"); const STSymbol = __importStar(require("./st-symbol")); const STCustomState = __importStar(require("./st-custom-state")); const resolve_1 = require("../helpers/resolve"); const namespace_1 = require("../helpers/namespace"); const escape_1 = require("../helpers/escape"); const value_1 = require("../helpers/value"); const selector_1 = require("../helpers/selector"); const stylable_utils_1 = require("../stylable-utils"); const custom_state_1 = require("../helpers/custom-state"); const css_selector_parser_1 = require("@tokey/css-selector-parser"); const postcss = __importStar(require("postcss")); const path_1 = require("path"); const diagnostics_2 = require("../diagnostics"); const postcss_value_parser_1 = __importDefault(require("postcss-value-parser")); const plugable_record_1 = require("../helpers/plugable-record"); const stPartDirectives = { '-st-root': true, '-st-states': true, '-st-extends': true, '-st-global': true, }; exports.diagnostics = { INVALID_FUNCTIONAL_SELECTOR: diagnostics_1.generalDiagnostics.INVALID_FUNCTIONAL_SELECTOR, UNSCOPED_CLASS: (0, diagnostics_2.createDiagnosticReporter)('00002', 'warning', (name) => `unscoped class "${name}" will affect all elements of the same type in the document`), STATE_DEFINITION_IN_ELEMENT: (0, diagnostics_2.createDiagnosticReporter)('11002', 'error', () => 'cannot define pseudo states inside a type selector'), STATE_DEFINITION_IN_COMPLEX: (0, diagnostics_2.createDiagnosticReporter)('11003', 'error', () => 'cannot define pseudo states inside complex selectors'), OVERRIDE_TYPED_RULE: (0, diagnostics_2.createDiagnosticReporter)('11006', 'warning', (key, name) => `override "${key}" on typed rule "${name}"`), CANNOT_RESOLVE_EXTEND: (0, diagnostics_2.createDiagnosticReporter)('11004', 'error', (name) => `cannot resolve '-st-extends' type for '${name}'`), CANNOT_EXTEND_IN_COMPLEX: (0, diagnostics_2.createDiagnosticReporter)('11005', 'error', () => `cannot define "-st-extends" inside a complex selector`), EMPTY_ST_GLOBAL: (0, diagnostics_2.createDiagnosticReporter)('00003', 'error', () => `-st-global must contain a valid selector`), UNSUPPORTED_MULTI_SELECTORS_ST_GLOBAL: (0, diagnostics_2.createDiagnosticReporter)('00004', 'error', () => `unsupported multi selector in -st-global`), UNSUPPORTED_COMPLEX_SELECTOR: (0, diagnostics_2.createDiagnosticReporter)('00010', 'error', () => `unsupported complex selector`), IMPORT_ISNT_EXTENDABLE: (0, diagnostics_2.createDiagnosticReporter)('00005', 'error', () => 'import is not extendable'), CANNOT_EXTEND_UNKNOWN_SYMBOL: (0, diagnostics_2.createDiagnosticReporter)('00006', 'error', (name) => `cannot extend unknown symbol "${name}"`), CANNOT_EXTEND_JS: (0, diagnostics_2.createDiagnosticReporter)('00007', 'error', () => 'JS import is not extendable'), UNKNOWN_IMPORT_ALIAS: (0, diagnostics_2.createDiagnosticReporter)('00008', 'error', (name) => `cannot use alias for unknown import "${name}"`), DISABLED_DIRECTIVE: (0, diagnostics_2.createDiagnosticReporter)('00009', 'error', (className, directive) => { const alternative = directive === '-st-extends' ? ` use "@st .${className} :is(.base)" instead` : directive === '-st-global' ? `use "@st .${className} => :global(<selector>)" instead` : directive === '-st-states' ? `use "@st .${className} { @st .state; }" instead` : ''; return `cannot use ${directive} on .${className} since class is defined with "@st" - ${alternative}`; }), }; const dataKey = plugable_record_1.plugableRecord.key('st-structure'); // HOOKS exports.hooks = (0, feature_1.createFeature)({ metaInit({ meta }) { plugable_record_1.plugableRecord.set(meta.data, dataKey, { classesDefinedWithAtSt: new Set(), }); }, analyzeSelectorNode({ context, node, rule }) { if (node.nodes) { // error on functional class context.diagnostics.report(exports.diagnostics.INVALID_FUNCTIONAL_SELECTOR(`.` + node.value, `class`), { node: rule, word: (0, selector_1.stringifySelector)(node), }); } addClass(context, node.value, rule); }, analyzeDeclaration({ context, decl }) { if (context.meta.type === 'stylable' && isDirectiveDeclaration(decl)) { handleDirectives(context, decl); } }, transformResolve({ context }) { const resolvedSymbols = context.getResolvedSymbols(context.meta); const locals = {}; for (const [localName, resolved] of Object.entries(resolvedSymbols.class)) { const exportedClasses = []; let first = true; // collect list of css classes for exports for (const { meta, symbol } of resolved) { if (!first && symbol[`-st-root`]) { // extended stylesheet root: stop collection as root is expected to // be placed by inner component, for example in <Button class={classes.primaryBtn} /> // `primaryBtn` shouldn't contain `button__root` as it is placed by the Button component break; } first = false; if (symbol[`-st-global`]) { // collect global override just in case of // compound set of CSS classes let isOnlyClasses = true; const globalClasses = symbol[`-st-global`].reduce((globalClasses, node) => { if (node.type === `class`) { globalClasses.push(node.value); context.meta.globals[node.value] = true; } else { isOnlyClasses = false; } return globalClasses; }, []); if (isOnlyClasses) { exportedClasses.push(...globalClasses); } continue; } if (symbol.alias && !symbol[`-st-extends`]) { continue; } exportedClasses.push((0, namespace_1.namespace)(symbol.name, meta.namespace)); } const classNames = (0, escape_1.unescapeCSS)(exportedClasses.join(' ')); if (classNames) { const directResolve = resolved[0]; const isLocal = directResolve.meta === context.meta && !directResolve.symbol.alias; locals[localName] = { classes: classNames, isLocal }; } } return locals; }, transformSelectorNode({ context, selectorContext, node }) { const { originMeta, resolver } = selectorContext; const resolvedSymbols = context.getResolvedSymbols(context.meta); const resolved = resolvedSymbols.class[node.value] || [ // used to namespace classes from js mixins since js mixins // are scoped in the context of the mixed-in stylesheet // which might not have a definition for the mixed-in class { _kind: 'css', meta: originMeta, symbol: createSymbol({ name: node.value }) }, ]; selectorContext.setNextSelectorScope(resolved, node, node.value); const { symbol, meta } = (0, resolve_1.getOriginDefinition)(resolved); if (selectorContext.originMeta === meta && symbol[`-st-states`]) { // ToDo: refactor out to transformer validation phase (0, custom_state_1.validateRuleStateDefinition)(selectorContext.selectorStr, selectorContext.ruleOrAtRule, context.meta, resolver, context.diagnostics); } if (selectorContext.transform) { namespaceClass(meta, symbol, node); } }, transformJSExports({ exports, resolved }) { for (const [localName, { classes, isLocal }] of Object.entries(resolved)) { if (isLocal) { exports.classes[localName] = classes; } } }, }); // API class StylablePublicApi { constructor(stylable) { this.stylable = stylable; } transformIntoSelector(meta, name) { const localSymbol = STSymbol.get(meta, name); const resolved = localSymbol?._kind === 'import' ? this.stylable.resolver.deepResolve(localSymbol) : { _kind: 'css', meta, symbol: localSymbol }; if (resolved?._kind !== 'css' || resolved.symbol?._kind !== 'class') { return undefined; } const node = { type: 'class', value: '', start: 0, end: 0, dotComments: [], }; namespaceClass(resolved.meta, resolved.symbol, node, false); return (0, css_selector_parser_1.stringifySelectorAst)(node); } } exports.StylablePublicApi = StylablePublicApi; function get(meta, name) { return STSymbol.get(meta, name, `class`); } exports.get = get; function getAll(meta) { return STSymbol.getAllByType(meta, `class`); } exports.getAll = getAll; function createSymbol(input) { const parts = input['-st-parts'] || {}; return { ...input, _kind: 'class', '-st-parts': parts }; } exports.createSymbol = createSymbol; function addClass(context, name, rule) { let symbol = STSymbol.get(context.meta, name, `class`); if (!symbol) { let alias = STSymbol.get(context.meta, name); if (alias && alias._kind !== 'import') { alias = undefined; } symbol = STSymbol.addSymbol({ context, symbol: createSymbol({ name, alias }), node: rule, safeRedeclare: !!alias, }); } // mark native css as global if (context.meta.type === 'css' && !symbol['-st-global']) { symbol['-st-global'] = [ { type: 'class', value: name, dotComments: [], start: 0, end: 0, }, ]; } return symbol; } exports.addClass = addClass; function namespaceClass(meta, symbol, node, // ToDo: check this is the correct type, should this be inline selector? wrapInGlobal = true) { if (`-st-global` in symbol && symbol[`-st-global`]) { // change node to `-st-global` value if (wrapInGlobal) { const globalMappedNodes = symbol[`-st-global`]; (0, selector_1.convertToPseudoClass)(node, 'global', [ { type: 'selector', nodes: globalMappedNodes, after: '', before: '', end: 0, start: 0, }, ]); } else { const flatNode = (0, selector_1.convertToSelector)(node); const globalMappedNodes = symbol[`-st-global`]; flatNode.nodes = globalMappedNodes; } } else { node = (0, selector_1.convertToClass)(node); node.value = (0, escape_1.namespaceEscape)(symbol.name, meta.namespace); } } exports.namespaceClass = namespaceClass; function getNamespacedClass(meta, symbol) { if (`-st-global` in symbol && symbol[`-st-global`]) { const selector = symbol[`-st-global`]; return (0, css_selector_parser_1.stringifySelectorAst)(selector); } else { return '.' + (0, escape_1.namespaceEscape)(symbol.name, meta.namespace); } } function addDevRules({ getResolvedSymbols, meta }) { const resolvedSymbols = getResolvedSymbols(meta); for (const resolved of Object.values(resolvedSymbols.class)) { const a = resolved[0]; if (resolved.length > 1 && a.symbol['-st-extends']) { const b = resolved[resolved.length - 1]; meta.targetAst.append(createWarningRule('.' + b.symbol.name, getNamespacedClass(b.meta, b.symbol), (0, path_1.basename)(b.meta.source), '.' + a.symbol.name, getNamespacedClass(a.meta, a.symbol), (0, path_1.basename)(a.meta.source))); } } } exports.addDevRules = addDevRules; function createWarningRule(extendedNode, scopedExtendedNode, extendedFile, extendingNode, scopedExtendingNode, extendingFile) { const message = `"class extending component '${extendingNode} => ${scopedExtendingNode}' in stylesheet '${extendingFile}' was set on a node that does not extend '${extendedNode} => ${scopedExtendedNode}' from stylesheet '${extendedFile}'" !important`; return postcss.rule({ raws: { between: ' ' }, selector: `${scopedExtendingNode}:not(${scopedExtendedNode})::before`, nodes: [ postcss.decl({ prop: 'content', value: message, }), postcss.decl({ prop: 'display', value: `block !important`, }), postcss.decl({ prop: 'font-family', value: `monospace !important`, }), postcss.decl({ prop: 'background-color', value: `red !important`, }), postcss.decl({ prop: 'color', value: `white !important`, }), ], }); } exports.createWarningRule = createWarningRule; function validateClassScoping({ context, classSymbol, locallyScoped, reportUnscoped, node, nodes, index, rule, }) { if (context.meta.type !== 'stylable') { // ignore in native CSS return true; } if (!classSymbol.alias) { return true; } else if (locallyScoped === false) { if (checkForScopedNodeAfter(context, rule, nodes, index) === false) { if (reportUnscoped) { context.diagnostics.report(exports.diagnostics.UNSCOPED_CLASS(node.value), { node: rule, word: node.value, }); } return false; } else { return true; } } return locallyScoped; } exports.validateClassScoping = validateClassScoping; // ToDo: support more complex cases (e.g. `:is`) function checkForScopedNodeAfter(context, rule, nodes, index) { for (let i = index + 1; i < nodes.length; i++) { const node = nodes[i]; if (!node) { // ToDo: can this get here??? break; } if (node.type === 'combinator') { break; } if (node.type === 'class') { const name = node.value; const classSymbol = addClass(context, name, rule); if (classSymbol && !classSymbol.alias) { return true; } } } return false; } exports.checkForScopedNodeAfter = checkForScopedNodeAfter; function isDirectiveDeclaration(decl) { return decl.prop in stPartDirectives; } function disableDirectivesForClass(context, className) { // ToDo: move directive analyze to @st-structure // called when class is defined with @st const { classesDefinedWithAtSt } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); classesDefinedWithAtSt.add(className); } exports.disableDirectivesForClass = disableDirectivesForClass; function handleDirectives(context, decl) { const rule = decl.parent; if (rule?.type !== 'rule') { return; } const isSimplePerSelector = (0, selector_1.isSimpleSelector)(rule.selector); const type = isSimplePerSelector.reduce((accType, { type }) => { return !accType ? type : accType !== type ? `complex` : type; }, ``); const isSimple = type !== `complex`; const { classesDefinedWithAtSt } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); if (type === 'class' && classesDefinedWithAtSt.has(rule.selector.replace('.', ''))) { context.diagnostics.report(exports.diagnostics.DISABLED_DIRECTIVE(rule.selector.replace('.', ''), decl.prop), { node: decl, }); return; } else if (decl.prop === `-st-states`) { if (isSimple && type !== 'type') { extendTypedRule(context, decl, rule.selector, `-st-states`, STCustomState.parsePseudoStates(decl.value, decl, context.diagnostics)); } else { if (type === 'type') { context.diagnostics.report(exports.diagnostics.STATE_DEFINITION_IN_ELEMENT(), { node: decl, }); } else { context.diagnostics.report(exports.diagnostics.STATE_DEFINITION_IN_COMPLEX(), { node: decl, }); } } } else if (decl.prop === `-st-extends`) { if (isSimple) { const parsed = parseStExtends(decl.value); const symbolName = parsed.types[0] && parsed.types[0].symbolName; const extendsRefSymbol = STSymbol.get(context.meta, symbolName); if ((extendsRefSymbol && (extendsRefSymbol._kind === 'import' || extendsRefSymbol._kind === 'class' || extendsRefSymbol._kind === 'element')) || decl.value === context.meta.root) { extendTypedRule(context, decl, rule.selector, `-st-extends`, (0, stylable_utils_1.getAlias)(extendsRefSymbol) || extendsRefSymbol); } else { context.diagnostics.report(exports.diagnostics.CANNOT_RESOLVE_EXTEND(decl.value), { node: decl, word: decl.value, }); } } else { context.diagnostics.report(exports.diagnostics.CANNOT_EXTEND_IN_COMPLEX(), { node: decl, }); } } else if (decl.prop === `-st-global`) { if (isSimple && type !== 'type') { // set class global mapping const name = rule.selector.replace('.', ''); const classSymbol = get(context.meta, name); if (classSymbol) { const globalSelectorAst = parseStGlobal(context, decl); if (globalSelectorAst) { classSymbol[`-st-global`] = globalSelectorAst; } } } else { // TODO: diagnostics - scoped on none class } } } function extendTypedRule(context, node, selector, key, value) { const name = selector.replace('.', ''); const typedRule = STSymbol.get(context.meta, name); if (typedRule && typedRule[key]) { context.diagnostics.report(exports.diagnostics.OVERRIDE_TYPED_RULE(key, name), { node, word: name, }); } if (typedRule) { typedRule[key] = value; } } exports.extendTypedRule = extendTypedRule; function parseStExtends(value) { const ast = (0, postcss_value_parser_1.default)(value); const types = []; ast.walk((node) => { if (node.type === 'function') { const args = (0, value_1.getNamedArgs)(node); types.push({ symbolName: node.value, args, }); return false; } else if (node.type === 'word') { types.push({ symbolName: node.value, args: null, }); } return undefined; }, false); return { ast, types, }; } exports.parseStExtends = parseStExtends; function parseStGlobal(context, decl) { const selector = (0, selector_1.parseSelectorWithCache)(decl.value.replace(/^['"]/, '').replace(/['"]$/, ''), { clone: true, }); if (!selector[0]) { context.diagnostics.report(exports.diagnostics.EMPTY_ST_GLOBAL(), { node: decl, }); return; } else if (selector.length > 1) { context.diagnostics.report(exports.diagnostics.UNSUPPORTED_MULTI_SELECTORS_ST_GLOBAL(), { node: decl, }); return; } else { for (const node of selector[0].nodes) { if (node.type === 'combinator') { context.diagnostics.report(exports.diagnostics.UNSUPPORTED_COMPLEX_SELECTOR(), { node: decl, }); return; } } } return selector[0].nodes; } //# sourceMappingURL=css-class.js.map