@stylable/core
Version:
CSS for Components
396 lines (381 loc) • 13.4 kB
text/typescript
import { createFeature, FeatureContext } from './feature';
import * as STSymbol from './st-symbol';
import * as STImport from './st-import';
import type { StylableMeta } from '../stylable-meta';
import { plugableRecord } from '../helpers/plugable-record';
import { namespace } from '../helpers/namespace';
import { globalValueFromFunctionNode, GLOBAL_FUNC } from '../helpers/global';
import { CSSWideKeywords } from '../native-reserved-lists';
import valueParser from 'postcss-value-parser';
import type * as postcss from 'postcss';
import { createDiagnosticReporter, Diagnostics } from '../diagnostics';
export interface LayerSymbol {
_kind: 'layer';
name: string;
alias: string;
global?: boolean;
import?: STImport.Imported;
}
export interface ResolvedLayer {
meta: StylableMeta;
symbol: LayerSymbol;
}
export const diagnostics = {
MISSING_LAYER_NAME_INSIDE_GLOBAL: createDiagnosticReporter(
'19001',
'warning',
() => `"@layer" missing parameter inside "${GLOBAL_FUNC}()"`
),
LAYER_SORT_STATEMENT_WITH_STYLE: createDiagnosticReporter(
'19002',
'error',
() => `"@layer" ordering statement cannot have a style block`
),
RESERVED_KEYWORD: createDiagnosticReporter(
'19003',
'error',
(name: string) => `"@layer" name cannot be reserved word "${name}"`
),
NOT_IDENT: createDiagnosticReporter(
'19004',
'error',
(name: string) => `"@layer" expected ident, but got "${name}"`
),
RECONFIGURE_IMPORTED: createDiagnosticReporter(
'19005',
'error',
(name: string) => `cannot reconfigure imported layer "${name}"`
),
UNKNOWN_IMPORTED_LAYER: createDiagnosticReporter(
'19006',
'error',
(name: string, path: string) =>
`cannot resolve imported layer "${name}" from stylesheet "${path}"`
),
};
const dataKey = plugableRecord.key<{
analyzedParams: Record<
string,
{
names: string[];
transformNames: (getTransformedName: (name: string) => string) => string;
}
>;
layerDefs: Record<string, postcss.AtRule | postcss.Rule>;
}>('layer');
// HOOKS
STImport.ImportTypeHook.set(`layer`, (context, localName, importName, importDef) => {
addLayer({
context,
name: localName,
importName,
ast: importDef.rule,
global: false,
importDef,
});
});
interface ResolvedSymbols {
record: Record<string, ResolvedLayer>;
locals: Set<string>;
}
export const hooks = createFeature<{
RESOLVED: ResolvedSymbols;
}>({
metaInit({ meta }) {
plugableRecord.set(meta.data, dataKey, { analyzedParams: {}, layerDefs: {} });
},
analyzeAtRule({ context, atRule }) {
if (!atRule.params) {
return;
}
if (atRule.name === 'import') {
// native css import
analyzeCSSImportLayer(context, atRule);
} else if (atRule.name === 'layer') {
// layer atrule
const analyzeMetaData = plugableRecord.getUnsafe(context.meta.data, dataKey);
const analyzedParams = parseLayerParams(
atRule.params,
context.diagnostics,
atRule,
context.meta.type === 'stylable'
);
if (analyzedParams.multiple && atRule.nodes) {
context.diagnostics.report(diagnostics.LAYER_SORT_STATEMENT_WITH_STYLE(), {
node: atRule,
});
}
// cache params
analyzeMetaData.analyzedParams[atRule.params] = analyzedParams;
// cache symbols
for (const name of analyzedParams.names) {
addLayer({
context,
name,
importName: name,
global: !!analyzedParams.globals[name] || context.meta.type === 'css',
ast: atRule,
});
}
}
},
transformResolve({ context }) {
const symbols = STSymbol.getAllByType(context.meta, `layer`);
const resolved: ResolvedSymbols = {
record: {},
locals: new Set(),
};
const resolvedSymbols = context.getResolvedSymbols(context.meta);
for (const [name, symbol] of Object.entries(symbols)) {
const res = resolvedSymbols.layer[name];
if (res) {
resolved.record[name] = res;
if (res.meta === context.meta) {
resolved.locals.add(name);
}
} else if (symbol.import) {
context.diagnostics.report(
diagnostics.UNKNOWN_IMPORTED_LAYER(symbol.name, symbol.import.request),
{
node: symbol.import.rule,
word: symbol.name,
}
);
}
}
return resolved;
},
transformAtRuleNode({ context, atRule, resolved }) {
if (!atRule.params) {
return;
}
if (atRule.name === 'import') {
// native css import
transformCSSImportLayer(context, atRule, resolved.record);
} else if (atRule.name === 'layer') {
// layer atrule
const { analyzedParams } = plugableRecord.getUnsafe(context.meta.data, dataKey);
const analyzed = analyzedParams[atRule.params];
if (analyzed) {
atRule.params = analyzed.transformNames((name) => {
const resolve = resolved.record[name];
return resolve ? getTransformedName(resolved.record[name]) : name;
});
}
}
},
transformJSExports({ exports, resolved }) {
for (const name of resolved.locals) {
exports.layers[name] = getTransformedName(resolved.record[name]);
}
},
});
// API
export function get(meta: StylableMeta, name: string): LayerSymbol | undefined {
return STSymbol.get(meta, name, `layer`);
}
export function getAll(meta: StylableMeta): Record<string, LayerSymbol> {
return STSymbol.getAllByType(meta, `layer`);
}
export function getDefinition(
meta: StylableMeta,
name: string
): postcss.AtRule | postcss.Rule | undefined {
const analyzeMetaData = plugableRecord.getUnsafe(meta.data, dataKey);
return analyzeMetaData.layerDefs[name];
}
function parseLayerParams(
params: string,
report: Diagnostics,
atRule: postcss.AtRule,
isStylable: boolean
) {
const names: string[] = [];
const globals: Record<string, true> = {};
let readyForName = true;
let multiple = false;
const ast = valueParser(params).nodes;
const namedNodeRefs: Record<string, valueParser.Node[]> = {};
for (let i = 0; i < ast.length; ++i) {
const node = ast[i];
const { type, value } = node;
if (type === 'word') {
if (readyForName) {
const layers: valueParser.WordNode[] = [];
for (const name of getDotSeparatedNames(value)) {
// ToDo: handle name duplications
const splittedLayer = { ...node, value: name };
if (layers.length) {
layers.push({ ...node, value: '.' });
}
layers.push(splittedLayer);
namedNodeRefs[name] ??= [];
namedNodeRefs[name].push(splittedLayer);
names.push(name);
}
readyForName = false;
ast.splice(i, 1, ...layers);
}
} else if (type === 'function' && value === GLOBAL_FUNC && readyForName && isStylable) {
const globalName = globalValueFromFunctionNode(node);
if (globalName) {
namedNodeRefs[globalName] ??= [];
namedNodeRefs[globalName].push(node);
names.push(globalName);
globals[globalName] = true;
} else if (globalName === '') {
report.report(diagnostics.MISSING_LAYER_NAME_INSIDE_GLOBAL(), { node: atRule });
}
readyForName = false;
} else if (type === 'div' && value === ',') {
readyForName = true;
multiple = true;
} else if (type === 'comment') {
// doesn't change meaning: do nothing
} else {
readyForName = false;
const source = valueParser.stringify(node);
report.report(diagnostics.NOT_IDENT(source), { node: atRule, word: source });
}
}
return {
names,
globals,
multiple,
transformNames(getTransformedName: (name: string) => string) {
for (const [name, nodes] of Object.entries(namedNodeRefs)) {
const transformedName = getTransformedName(name);
for (const modifiedNode of nodes) {
if (modifiedNode.type === 'function') {
// mutate to word - this is safe since this node is not exposed
(modifiedNode as any).type = 'word';
}
modifiedNode.value = transformedName;
}
}
return valueParser.stringify(ast);
},
};
}
function getDotSeparatedNames(value: string) {
if (!value.includes('.')) {
return [value];
}
const names = [];
let lastIndex = 0;
for (let index = 0; index < value.length; ++index) {
const char = value[index];
switch (char) {
case '.': {
if (value[index - 1] !== '\\') {
names.push(value.substring(lastIndex, index));
lastIndex = index + 1;
}
break;
}
}
}
if (lastIndex <= value.length - 1) {
names.push(value.substring(lastIndex, value.length));
}
return names;
}
function analyzeCSSImportLayer(context: FeatureContext, importAtRule: postcss.AtRule) {
const ast = valueParser(importAtRule.params).nodes;
for (let i = 0; i < ast.length; ++i) {
const node = ast[i];
const { type, value } = node;
if (type === 'function' && value === 'layer' && node.nodes.length) {
for (const nestedNode of node.nodes) {
if (nestedNode.type === 'word') {
for (const name of getDotSeparatedNames(nestedNode.value)) {
addLayer({
context,
name,
importName: name,
ast: importAtRule,
global: false,
});
}
}
}
}
}
}
function transformCSSImportLayer(
_context: FeatureContext,
importAtRule: postcss.AtRule,
resolved: Record<string, ResolvedLayer>
) {
const ast = valueParser(importAtRule.params).nodes;
for (let i = 0; i < ast.length; ++i) {
const node = ast[i];
const { type, value } = node;
if (type === 'function' && value === 'layer' && node.nodes.length) {
for (const nestedNode of node.nodes) {
const { type, value } = nestedNode;
if (type === 'word') {
nestedNode.value = getDotSeparatedNames(value)
.map((name) => {
const resolve = resolved[name];
return resolve ? getTransformedName(resolved[name]) : name;
})
.join('.');
}
}
}
}
importAtRule.params = valueParser.stringify(ast);
}
function getTransformedName({ symbol, meta }: ResolvedLayer) {
return symbol.global ? symbol.alias : namespace(symbol.alias, meta.namespace);
}
function addLayer({
context,
name,
importName,
ast,
global,
importDef,
}: {
context: FeatureContext;
name: string;
importName: string;
ast: postcss.AtRule | postcss.Rule;
global: boolean;
importDef?: STImport.Imported;
}) {
const definedSymbol = STSymbol.get(context.meta, name, 'layer');
if (!definedSymbol) {
if (CSSWideKeywords.includes(name)) {
// keep
global = true;
context.diagnostics.report(diagnostics.RESERVED_KEYWORD(name), {
node: ast,
word: name,
});
}
const analyzeMetaData = plugableRecord.getUnsafe(context.meta.data, dataKey);
analyzeMetaData.layerDefs[name] = ast;
STSymbol.addSymbol({
context,
node: ast,
localName: name,
symbol: {
_kind: 'layer',
name: importName,
alias: name,
global,
import: importDef,
},
safeRedeclare: false,
});
} else if (!definedSymbol.import && global) {
definedSymbol.global = true;
} else if (definedSymbol.import && global) {
context.diagnostics.report(diagnostics.RECONFIGURE_IMPORTED(name), {
node: ast,
word: name,
});
}
}