From b2dbd811ca3a41c4f49729e435b5f4b74190d47e Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Fri, 29 Dec 2023 13:11:57 +0200 Subject: [PATCH] feat(core): support style queries --- packages/core/src/features/css-contains.ts | 129 ++++++++++++------ .../core/test/features/css-contains.spec.ts | 79 ++++++++++- 2 files changed, 165 insertions(+), 43 deletions(-) diff --git a/packages/core/src/features/css-contains.ts b/packages/core/src/features/css-contains.ts index fa493765b..1ef01083c 100644 --- a/packages/core/src/features/css-contains.ts +++ b/packages/core/src/features/css-contains.ts @@ -1,6 +1,7 @@ import { createFeature, FeatureContext } from './feature'; import * as STSymbol from './st-symbol'; import * as STImport from './st-import'; +import * as CSSCustomProperty from './css-custom-property'; import type { StylableMeta } from '../stylable-meta'; import { createDiagnosticReporter } from '../diagnostics'; import { plugableRecord } from '../helpers/plugable-record'; @@ -130,47 +131,71 @@ export const hooks = createFeature<{ } }, analyzeAtRule({ context, atRule }) { - if (!atRule.nodes) { - // treat @container with no body as definition - const ast = valueParser(atRule.params).nodes; - let searching = true; - let name = ''; - let global = false; - for (const node of ast) { - if (node.type === 'comment' || node.type === 'space') { - // do nothing - continue; - } else if (searching && node.type === 'word') { - name = node.value; - } else if (searching && node.type === 'function' && node.value === GLOBAL_FUNC) { - name = globalValueFromFunctionNode(node) || ''; - global = true; - } else { - const def = valueParser.stringify(node); - context.diagnostics.report(diagnostics.UNEXPECTED_DEFINITION(def), { - node: atRule, - word: def, - }); - break; + const ast = valueParser(atRule.params).nodes; + let name = ''; + let global = false; + let searchForContainerName = true; + let searchForLogicalOp = false; + for (const node of ast) { + if (node.type === 'comment' || node.type === 'space') { + // do nothing + continue; + } else if (searchForContainerName && node.type === 'word') { + searchForContainerName = false; + searchForLogicalOp = true; + name = node.value; + } else if ( + searchForContainerName && + node.type === 'function' && + node.value === GLOBAL_FUNC + ) { + searchForContainerName = false; + searchForLogicalOp = true; + name = globalValueFromFunctionNode(node) || ''; + global = true; + } else if (node.type === 'function' && node.value === 'style') { + searchForContainerName = false; + searchForLogicalOp = true; + // check for custom properties + for (const queryNode of node.nodes) { + if (queryNode.type === 'word' && queryNode.value.startsWith('--')) { + CSSCustomProperty.addCSSProperty({ + context, + node: atRule, + name: queryNode.value, + global: context.meta.type === 'css', + final: false, + }); + } } - searching = false; + } else if ( + node.type !== 'function' && + (!searchForLogicalOp || (node.type === 'word' && !logicalOpNames[node.value])) + ) { + const def = valueParser.stringify(node); + context.diagnostics.report(diagnostics.UNEXPECTED_DEFINITION(def), { + node: atRule, + word: def, + }); + break; } - if (name) { - if (invalidContainerNames[name]) { - context.diagnostics.report(diagnostics.INVALID_CONTAINER_NAME(name), { - node: atRule, - word: name, - }); - } - addContainer({ - context, - ast: atRule, - name, - importName: name, - global, - forceDefinition: true, + } + if (name && !atRule.nodes) { + // treat @container with no body as definition + if (invalidContainerNames[name]) { + context.diagnostics.report(diagnostics.INVALID_CONTAINER_NAME(name), { + node: atRule, + word: name, }); } + addContainer({ + context, + ast: atRule, + name, + importName: name, + global, + forceDefinition: true, + }); } }, transformResolve({ context }) { @@ -220,10 +245,11 @@ export const hooks = createFeature<{ } const ast = valueParser(atRule.params).nodes; let changed = false; + let searchForContainerName = true; search: for (const node of ast) { if (node.type === 'comment' || node.type === 'space') { // do nothing - } else if (node.type === 'word') { + } else if (node.type === 'word' && searchForContainerName) { const resolve = resolved.record[node.value]; if (resolve) { node.value = getTransformedName(resolve); @@ -235,7 +261,11 @@ export const hooks = createFeature<{ }); } break search; - } else if (node.type === 'function' && node.value === GLOBAL_FUNC) { + } else if ( + node.type === 'function' && + node.value === GLOBAL_FUNC && + searchForContainerName + ) { const globalName = globalValueFromFunctionNode(node) || ''; if (globalName) { changed = true; @@ -243,8 +273,19 @@ export const hooks = createFeature<{ wordNode.type = 'word'; wordNode.value = globalName; } - } else { - break search; + } else if (node.type === 'function' && node.value === 'style') { + // check for custom properties + searchForContainerName = false; + for (const queryNode of node.nodes) { + if (queryNode.type === 'word' && queryNode.value.startsWith('--')) { + changed = true; + CSSCustomProperty.transformPropertyIdent( + context.meta, + queryNode, + context.getResolvedSymbols + ); + } + } } } if (changed) { @@ -265,6 +306,12 @@ export const hooks = createFeature<{ }); const invalidContainerNames: Record = { + none: true, + and: true, + not: true, + or: true, +}; +const logicalOpNames: Record = { and: true, not: true, or: true, diff --git a/packages/core/test/features/css-contains.spec.ts b/packages/core/test/features/css-contains.spec.ts index 172ba76c4..8a65dbce0 100644 --- a/packages/core/test/features/css-contains.spec.ts +++ b/packages/core/test/features/css-contains.spec.ts @@ -1,4 +1,4 @@ -import { STSymbol, CSSContains } from '@stylable/core/dist/features'; +import { STSymbol, CSSContains, CSSCustomProperty } from '@stylable/core/dist/features'; import { testStylableCore, shouldReportNoDiagnostics, @@ -543,6 +543,61 @@ describe('features/css-contains', () => { }); }); }); + describe('custom-property', () => { + it('should register and transform style query custom properties', () => { + const { sheets } = testStylableCore(` + /* @atrule(single-standalone) style(--entry-x) */ + @container style(--x) {} + + /* @atrule(multi) style(--entry-y) and style(--entry-z) */ + @container style(--y) and style(--z) {} + + /* @atrule(value) style(--entry-y: green) or style(--entry-z > 50px) */ + @container style(--y: green) or style(--z > 50px) {} + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + + expect(CSSCustomProperty.get(meta, `--x`), `--x symbol`).to.eql({ + _kind: `cssVar`, + name: `--x`, + global: false, + alias: undefined, + }); + expect(CSSCustomProperty.get(meta, `--y`), `--y symbol`).to.eql({ + _kind: `cssVar`, + name: `--y`, + global: false, + alias: undefined, + }); + expect(CSSCustomProperty.get(meta, `--z`), `--z symbol`).to.eql({ + _kind: `cssVar`, + name: `--z`, + global: false, + alias: undefined, + }); + }); + it('should reference imported custom properties', () => { + const { sheets } = testStylableCore({ + '/imported.st.css': ` + @property --x; + @property st-global(--g); + `, + '/entry.st.css': ` + @st-import [--x, --g] from './imported.st.css'; + + /* @atrule(single-standalone) style(--imported-x) or style(--g) */ + @container style(--x) or style(--g) {} + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); describe('st-mixin', () => { it('should mix @container for nested mixin', () => { const { sheets } = testStylableCore( @@ -582,7 +637,7 @@ describe('features/css-contains', () => { }); }); describe('native css', () => { - it('should not namespace', () => { + it('should not namespace container name', () => { const { stylable } = testStylableCore({ '/native.css': deindent(` .x { @@ -616,5 +671,25 @@ describe('features/css-contains', () => { // JS exports expect(exports.containers, `JS export only locals`).to.eql({}); }); + it('should not namespace style query custom properties', () => { + const { stylable } = testStylableCore({ + '/native.css': deindent(` + /* @atrule style(--x) */ + @container style(--x) {} + `), + '/entry.st.css': ` + @st-import [--x] from './native.css'; + + /* @atrule style(--x) */ + @container style(--x) {} + `, + }); + + const { meta: nativeMeta } = stylable.transform('/native.css'); + const { meta } = stylable.transform('/entry.st.css'); + + shouldReportNoDiagnostics(nativeMeta); + shouldReportNoDiagnostics(meta); + }); }); });