From 15f83eea7b7618e8b786e5266a68ff28575e5087 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Tue, 5 Mar 2024 14:11:05 +0200 Subject: [PATCH] feat(core): expose css custom properties api (#2943) --- .../core/src/features/css-custom-property.ts | 57 +++++++- packages/core/src/features/st-symbol.ts | 16 ++- packages/core/src/stylable.ts | 1 + .../test/features/css-custom-property.spec.ts | 129 +++++++++++++++++- 4 files changed, 199 insertions(+), 4 deletions(-) diff --git a/packages/core/src/features/css-custom-property.ts b/packages/core/src/features/css-custom-property.ts index df9ea0bc4..d771fbb94 100644 --- a/packages/core/src/features/css-custom-property.ts +++ b/packages/core/src/features/css-custom-property.ts @@ -7,12 +7,17 @@ import { generateScopedCSSVar, atPropertyValidationWarnings, } from '../helpers/css-custom-property'; +import type { Stylable } from '../stylable'; import { validateAllowedNodesUntil, stringifyFunction } from '../helpers/value'; import { globalValue, GLOBAL_FUNC } from '../helpers/global'; import { plugableRecord } from '../helpers/plugable-record'; -import { createDiagnosticReporter } from '../diagnostics'; +import { createDiagnosticReporter, Diagnostics } from '../diagnostics'; import type { StylableMeta } from '../stylable-meta'; -import type { StylableResolver, CSSResolve } from '../stylable-resolver'; +import { + type StylableResolver, + type CSSResolve, + createSymbolResolverWithCache, +} from '../stylable-resolver'; import type * as postcss from 'postcss'; // ToDo: refactor out - parse once and pass to hooks import postcssValueParser, { WordNode } from 'postcss-value-parser'; @@ -315,6 +320,54 @@ export function addCSSProperty({ }); } +const UNKNOWN_LOCATION = Object.freeze({ + offset: -1, + line: -1, + column: -1, +}); + +export class StylablePublicApi { + constructor(private stylable: Stylable) {} + + public getProperties(meta: StylableMeta) { + const results: Record< + string, + { + meta: StylableMeta; + localName: string; + targetName: string; + source: { + meta: StylableMeta; + start: postcss.Position; + end: postcss.Position; + }; + } + > = {}; + + const topLevelDiagnostics = new Diagnostics(); + const getResolvedSymbols = createSymbolResolverWithCache( + this.stylable.resolver, + topLevelDiagnostics + ); + const { cssVar } = getResolvedSymbols(meta); + for (const [name, symbol] of Object.entries(cssVar)) { + const defAst = STSymbol.getSymbolAstNode(symbol.meta, symbol.symbol); + results[name] = { + meta: symbol.meta, + localName: symbol.symbol.name, + targetName: getTransformedName(symbol), + source: { + meta: symbol.meta, + start: defAst?.source?.start || UNKNOWN_LOCATION, + end: defAst?.source?.end || UNKNOWN_LOCATION, + }, + }; + } + + return results; + } +} + function analyzeDeclValueVarCalls(context: FeatureContext, decl: postcss.Declaration) { const parsed = postcssValueParser(decl.value); parsed.walk((node) => { diff --git a/packages/core/src/features/st-symbol.ts b/packages/core/src/features/st-symbol.ts index 6fc16089c..1e9ead205 100644 --- a/packages/core/src/features/st-symbol.ts +++ b/packages/core/src/features/st-symbol.ts @@ -70,6 +70,7 @@ function createState(clone?: State): State { container: clone ? { ...clone.byType.container } : {}, var: clone ? { ...clone.byType.var } : {}, }, + symbolToAst: new WeakMap(), }; } @@ -113,6 +114,7 @@ interface State { byType: { [T in keyof SymbolMap]: Record; }; + symbolToAst: WeakMap; } const dataKey = plugableRecord.key('mappedSymbols'); @@ -173,7 +175,10 @@ export function addSymbol({ safeRedeclare?: boolean; localName?: string; }) { - const { byNS, byNSFlat, byType } = plugableRecord.getUnsafe(context.meta.data, dataKey); + const { byNS, byNSFlat, byType, symbolToAst } = plugableRecord.getUnsafe( + context.meta.data, + dataKey + ); const name = localName || symbol.name; const typeTable = byType[symbol._kind]; const nsName = NAMESPACES[symbol._kind]; @@ -187,9 +192,18 @@ export function addSymbol({ byNS[nsName].push({ name, symbol, ast: node, safeRedeclare }); byNSFlat[nsName][name] = symbol; typeTable[name] = symbol; + node && symbolToAst.set(symbol, node); return symbol; } +export function getSymbolAstNode( + meta: StylableMeta, + symbol: StylableSymbol +): postcss.Node | undefined { + const { symbolToAst } = plugableRecord.getUnsafe(meta.data, dataKey); + return symbolToAst.get(symbol); +} + export function reportRedeclare(context: FeatureContext) { const { byNS } = plugableRecord.getUnsafe(context.meta.data, dataKey); for (const symbols of Object.values(byNS)) { diff --git a/packages/core/src/stylable.ts b/packages/core/src/stylable.ts index 39dcc3e8b..fb4d49df7 100644 --- a/packages/core/src/stylable.ts +++ b/packages/core/src/stylable.ts @@ -83,6 +83,7 @@ export class Stylable { public resolver: StylableResolver; public stModule = new STImport.StylablePublicApi(this); public stScope = new STScope.StylablePublicApi(this); + public cssCustomProperty = new CSSCustomProperty.StylablePublicApi(this); public stVar = new STVar.StylablePublicApi(this); public stMixin = new STMixin.StylablePublicApi(this); public cssClass = new CSSClass.StylablePublicApi(this); diff --git a/packages/core/test/features/css-custom-property.spec.ts b/packages/core/test/features/css-custom-property.spec.ts index 24c574e08..67c814f6f 100644 --- a/packages/core/test/features/css-custom-property.spec.ts +++ b/packages/core/test/features/css-custom-property.spec.ts @@ -6,7 +6,11 @@ import { diagnosticBankReportToStrings, deindent, } from '@stylable/core-test-kit'; -import { expect } from 'chai'; +import chai, { expect } from 'chai'; +import chaiSubset from 'chai-subset'; +import type { StylableMeta } from '../../src'; + +chai.use(chaiSubset); const stImportDiagnostics = diagnosticBankReportToStrings(STImport.diagnostics); const stSymbolDiagnostics = diagnosticBankReportToStrings(STSymbol.diagnostics); @@ -1170,4 +1174,127 @@ describe(`features/css-custom-property`, () => { ); }); }); + describe('introspection', () => { + function expectSourceLocation({ + source: { meta, start, end }, + expected, + }: { + source: { meta: StylableMeta; start: { offset: number }; end: { offset: number } }; + expected: string; + }) { + const actualSrc = meta.sourceAst.toString().slice(start.offset, end.offset); + expect(actualSrc).to.eql(expected); + } + describe('getProperties', () => { + it('should resolve all local properties', () => { + const { stylable, sheets } = testStylableCore( + deindent(` + @property --defInAtRule { + syntax: ''; + initial-value: green; + inherits: false; + } + + .root { + --defineInPropName: green; + + color: var(--defineInDeclValue); + } + `) + ); + + const { meta } = sheets['/entry.st.css']; + + const properties = stylable.cssCustomProperty.getProperties(meta); + + expect(properties).to.containSubset({ + '--defInAtRule': { + meta, + localName: '--defInAtRule', + targetName: '--entry-defInAtRule', + }, + '--defineInPropName': { + meta, + localName: '--defineInPropName', + targetName: '--entry-defineInPropName', + }, + '--defineInDeclValue': { + meta, + localName: '--defineInDeclValue', + targetName: '--entry-defineInDeclValue', + }, + }); + expectSourceLocation({ + source: properties['--defInAtRule'].source, + expected: `@property --defInAtRule {\n syntax: '';\n initial-value: green;\n inherits: false;\n}`, + }); + expectSourceLocation({ + source: properties['--defineInPropName'].source, + expected: `--defineInPropName: green;`, + }); + expectSourceLocation({ + source: properties['--defineInDeclValue'].source, + expected: `color: var(--defineInDeclValue);`, + }); + }); + it('should resolve imported properties', () => { + const { stylable, sheets } = testStylableCore({ + 'deep.st.css': ` + .x { + --deep: red; + } + `, + 'proxy.st.css': ` + @st-import [--deep as --deepReassign1] from './deep.st.css'; + .x { + --proxy: var(--deepReassign1); + } + `, + 'entry.st.css': deindent(` + @st-import [--proxy as --proxyReassign, --deepReassign1 as --deepReassign2] from './proxy.st.css'; + + .x { + --local: green; + } + `), + }); + + const { meta } = sheets['/entry.st.css']; + const { meta: proxyMeta } = sheets['/proxy.st.css']; + const { meta: deepMeta } = sheets['/deep.st.css']; + + const properties = stylable.cssCustomProperty.getProperties(meta); + + expect(properties).to.containSubset({ + '--local': { + meta, + localName: '--local', + targetName: '--entry-local', + }, + '--proxyReassign': { + meta: proxyMeta, + localName: '--proxy', + targetName: '--proxy-proxy', + }, + '--deepReassign2': { + meta: deepMeta, + localName: '--deep', + targetName: '--deep-deep', + }, + }); + expectSourceLocation({ + source: properties['--local'].source, + expected: `--local: green;`, + }); + expectSourceLocation({ + source: properties['--proxyReassign'].source, + expected: `--proxy: var(--deepReassign1);`, + }); + expectSourceLocation({ + source: properties['--deepReassign2'].source, + expected: `--deep: red;`, + }); + }); + }); + }); });