@stylable/core
Version:
CSS for Components
232 lines (218 loc) • 7.44 kB
text/typescript
import { FeatureContext, createFeature } from './feature';
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';
import type { KeyframesSymbol } from './css-keyframes';
import type { LayerSymbol } from './css-layer';
import type { ContainerSymbol } from './css-contains';
import { plugableRecord } from '../helpers/plugable-record';
import type { StylableMeta } from '../stylable-meta';
import type * as postcss from 'postcss';
import { createDiagnosticReporter } from '../diagnostics';
// SYMBOLS DEFINITION
// union of all of the symbols
export type StylableSymbol =
| ImportSymbol
| VarSymbol
| ClassSymbol
| ElementSymbol
| CSSVarSymbol
| KeyframesSymbol
| LayerSymbol
| ContainerSymbol;
// the namespace that each symbol exists on
const NAMESPACES = {
import: `main`,
class: `main`,
cssVar: `main`,
element: `main`,
keyframes: `keyframes`,
layer: `layer`,
container: `container`,
var: `main`,
} as const;
export const readableTypeMap: Record<StylableSymbol['_kind'], string> = {
class: 'css class',
element: 'css element type',
cssVar: 'css custom property',
import: 'stylable imported symbol',
keyframes: 'css keyframes',
layer: 'css layer',
container: 'css container name',
var: 'stylable var',
};
// state structure
function createState(clone?: State): State {
return {
byNS: {
main: clone ? [...clone.byNS.main] : [],
keyframes: clone ? [...clone.byNS.keyframes] : [],
layer: clone ? [...clone.byNS.layer] : [],
container: clone ? [...clone.byNS.container] : [],
},
byNSFlat: {
main: clone ? { ...clone.byNSFlat.main } : {},
keyframes: clone ? { ...clone.byNSFlat.keyframes } : {},
layer: clone ? { ...clone.byNSFlat.layer } : {},
container: clone ? { ...clone.byNSFlat.container } : {},
},
byType: {
import: clone ? { ...clone.byType.import } : {},
class: clone ? { ...clone.byType.class } : {},
cssVar: clone ? { ...clone.byType.cssVar } : {},
element: clone ? { ...clone.byType.element } : {},
keyframes: clone ? { ...clone.byType.keyframes } : {},
layer: clone ? { ...clone.byType.layer } : {},
container: clone ? { ...clone.byType.container } : {},
var: clone ? { ...clone.byType.var } : {},
},
symbolToAst: new WeakMap(),
};
}
// internal types
type SymbolTypes = StylableSymbol['_kind'];
type filterSymbols<T extends SymbolTypes> = Extract<StylableSymbol, { _kind: T }>;
type SymbolMap = {
[K in SymbolTypes]: filterSymbols<K>;
};
type SymbolTypeToNamespace = typeof NAMESPACES;
type FilterByNamespace<NS extends Namespaces, T extends SymbolTypes = SymbolTypes> = T extends any
? SymbolTypeToNamespace[T] extends NS
? T
: never
: never;
type NamespaceToSymbolType = {
[NS in SymbolTypeToNamespace[SymbolTypes]]: FilterByNamespace<NS>;
};
export type Namespaces = keyof NamespaceToSymbolType;
export type SymbolByNamespace<NS extends Namespaces> = Extract<
StylableSymbol,
{
_kind: NamespaceToSymbolType[NS];
}
>;
interface SymbolDeclaration<NS = Namespaces> {
name: string;
symbol: filterSymbols<
SymbolTypes extends any ? (Namespaces extends NS ? SymbolTypes : any) : any
>;
ast: postcss.Node | undefined;
safeRedeclare: boolean;
}
interface State {
byNS: {
[NS in Namespaces]: SymbolDeclaration<NS>[];
};
byNSFlat: {
[NS in Namespaces]: Record<string, filterSymbols<NamespaceToSymbolType[NS]>>;
};
byType: {
[T in keyof SymbolMap]: Record<string, SymbolMap[T]>;
};
symbolToAst: WeakMap<StylableSymbol, postcss.Node>;
}
const dataKey = plugableRecord.key<State>('mappedSymbols');
export const diagnostics = {
REDECLARE_SYMBOL: createDiagnosticReporter(
'06001',
'warning',
(name: string) => `redeclare symbol "${name}"`
),
REDECLARE_ROOT: createDiagnosticReporter(
'06002',
'error',
() => `root is used for the stylesheet and cannot be overridden`
),
};
// HOOKS
export const hooks = createFeature({
metaInit({ meta }) {
plugableRecord.set(meta.data, dataKey, createState());
},
});
// API
export function get<T extends keyof SymbolMap>(meta: StylableMeta, name: string, type?: T) {
const { byNSFlat, byType } = plugableRecord.getUnsafe(meta.data, dataKey);
return (type ? byType[type][name] : byNSFlat['main'][name]) as filterSymbols<T> | undefined;
}
export function getAll<NS extends keyof NamespaceToSymbolType = `main`>(
meta: StylableMeta,
ns?: NS
) {
const { byNSFlat } = plugableRecord.getUnsafe(meta.data, dataKey);
return byNSFlat[ns || `main`] as State['byNSFlat'][NS];
}
export function getAllByType<T extends SymbolTypes>(
meta: StylableMeta,
type: T
): State['byType'][T] {
const { byType } = plugableRecord.getUnsafe(meta.data, dataKey);
return byType[type];
}
export function addSymbol({
context,
symbol,
node,
safeRedeclare = false,
localName,
}: {
context: FeatureContext;
symbol: StylableSymbol;
node?: postcss.Node;
safeRedeclare?: boolean;
localName?: string;
}) {
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];
if (node && name === `root` && nsName === `main` && byNSFlat[nsName][name]) {
context.diagnostics.report(diagnostics.REDECLARE_ROOT(), {
node,
word: `root`,
});
return;
}
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)) {
const flat: Record<string, SymbolDeclaration[]> = {};
const collisions: Set<string> = new Set();
for (const symbolDecl of symbols) {
const { name, safeRedeclare } = symbolDecl;
flat[name] = flat[name] || [];
if (!safeRedeclare && flat[name].length) {
collisions.add(name);
}
flat[name].push(symbolDecl);
}
for (const name of collisions) {
for (const { safeRedeclare, ast } of flat[name]) {
if (!safeRedeclare && ast) {
context.diagnostics.report(diagnostics.REDECLARE_SYMBOL(name), {
node: ast,
word: name,
});
}
}
}
}
}