@stylable/core
Version:
CSS for Components
318 lines (296 loc) • 11.4 kB
text/typescript
import { FileProcessor } from './cached-process-file';
import { Diagnostics } from './diagnostics';
import { ClassSymbol, ElementSymbol, Imported } from './stylable-meta';
import { ImportSymbol, StylableMeta, StylableSymbol } from './stylable-processor';
import { StylableTransformer } from './stylable-transformer';
import { valueMapping } from './stylable-value-parsers';
export const resolverWarnings = {
UNKNOWN_IMPORTED_FILE(path: string) {
return `cannot resolve imported file: "${path}"`;
},
UNKNOWN_IMPORTED_SYMBOL(name: string, path: string) {
return `cannot resolve imported symbol "${name}" from stylesheet "${path}"`;
},
};
export interface CSSResolve<T extends StylableSymbol = StylableSymbol> {
_kind: 'css';
symbol: T;
meta: StylableMeta;
}
export interface JSResolve {
_kind: 'js';
symbol: any;
meta: null;
}
export function isInPath(
extendPath: Array<CSSResolve<ClassSymbol | ElementSymbol>>,
{ symbol: { name: name1 }, meta: { source: source1 } }: CSSResolve<ClassSymbol | ElementSymbol>
) {
return extendPath.find(({ symbol: { name }, meta: { source } }) => {
return name1 === name && source === source1;
});
}
export class StylableResolver {
constructor(
protected fileProcessor: FileProcessor<StylableMeta>,
protected requireModule: (modulePath: string) => any
) {}
public resolveImported(imported: Imported, name: string) {
const { context, from } = imported;
let symbol: StylableSymbol;
if (from.match(/\.css$/)) {
let meta;
try {
meta = this.fileProcessor.process(from, false, context);
symbol = !name ? meta.mappedSymbols[meta.root] : meta.mappedSymbols[name];
} catch (e) {
return null;
}
return { _kind: 'css', symbol, meta } as CSSResolve;
} else {
let _module;
try {
_module = this.requireModule(this.fileProcessor.resolvePath(from));
} catch {
return null;
}
symbol = !name ? _module.default || _module : _module[name];
return { _kind: 'js', symbol, meta: null } as JSResolve;
}
}
public resolveImport(importSymbol: ImportSymbol) {
const name = importSymbol.type === 'named' ? importSymbol.name : '';
return this.resolveImported(importSymbol.import, name);
}
public resolve(maybeImport: StylableSymbol | undefined): CSSResolve | JSResolve | null {
if (!maybeImport || maybeImport._kind !== 'import') {
if (maybeImport && maybeImport._kind !== 'var' && maybeImport._kind !== 'cssVar') {
if (maybeImport.alias && !maybeImport[valueMapping.extends]) {
maybeImport = maybeImport.alias;
} else if (maybeImport[valueMapping.extends]) {
maybeImport = maybeImport[valueMapping.extends];
} else {
return null;
}
} else {
return null;
}
}
if (!maybeImport || maybeImport._kind !== 'import') {
return null;
}
return this.resolveImport(maybeImport);
}
public deepResolve(
maybeImport: StylableSymbol | undefined,
path: StylableSymbol[] = []
): CSSResolve | JSResolve | null {
let resolved = this.resolve(maybeImport);
while (
resolved &&
resolved._kind === 'css' &&
resolved.symbol &&
resolved.symbol._kind === 'import'
) {
resolved = this.resolve(resolved.symbol);
}
if (
resolved &&
resolved.symbol &&
resolved.meta &&
(resolved.symbol._kind === 'class' || resolved.symbol._kind === 'element') &&
resolved.symbol.alias &&
!resolved.symbol[valueMapping.extends]
) {
if (path.includes(resolved.symbol)) {
return { _kind: 'css', symbol: resolved.symbol, meta: resolved.meta };
}
path.push(resolved.symbol);
return this.deepResolve(resolved.symbol.alias, path);
}
return resolved;
}
public resolveSymbolOrigin(
symbol: StylableSymbol | undefined,
meta: StylableMeta,
path: StylableSymbol[] = []
): CSSResolve | null {
if (!symbol || !meta) {
return null;
}
if (symbol._kind === 'element' || symbol._kind === 'class') {
if (path.includes(symbol)) {
return { meta, symbol, _kind: 'css' };
}
path.push(symbol);
const isAliasOnly = symbol.alias && !symbol[valueMapping.extends];
return isAliasOnly
? this.resolveSymbolOrigin(symbol.alias, meta, path)
: { meta, symbol, _kind: 'css' };
} else if (symbol._kind === 'cssVar') {
if (path.includes(symbol)) {
return { meta, symbol, _kind: 'css' };
}
} else if (symbol._kind === 'import') {
const resolved = this.resolveImport(symbol);
if (resolved && resolved.symbol && resolved._kind === 'css') {
return this.resolveSymbolOrigin(resolved.symbol, resolved.meta, path);
} else {
return null;
}
}
return null;
}
public resolveClass(meta: StylableMeta, symbol: StylableSymbol) {
return this.resolveName(meta, symbol, false);
}
public resolveName(
meta: StylableMeta,
symbol: StylableSymbol,
isElement: boolean
): CSSResolve<ClassSymbol | ElementSymbol> | null {
const type = isElement ? 'element' : 'class';
let finalSymbol;
let finalMeta;
if (symbol._kind === type) {
finalSymbol = symbol;
finalMeta = meta;
} else if (symbol._kind === 'import') {
const resolved = this.deepResolve(symbol);
if (resolved && resolved._kind === 'css' && resolved.symbol) {
if (resolved.symbol._kind === 'class' || resolved.symbol._kind === 'element') {
finalSymbol = resolved.symbol;
finalMeta = resolved.meta;
} else {
// TODO: warn
}
} else {
// TODO: warn
}
} else {
// TODO: warn
}
if (finalMeta && finalSymbol) {
return {
_kind: 'css',
symbol: finalSymbol,
meta: finalMeta,
};
} else {
return null;
}
}
public resolveElement(meta: StylableMeta, symbol: StylableSymbol) {
return this.resolveName(meta, symbol, true);
}
public resolveExtends(
meta: StylableMeta,
className: string,
isElement = false,
transformer?: StylableTransformer,
reportError?: (
res: CSSResolve | JSResolve | null,
extend: ImportSymbol | ClassSymbol | ElementSymbol,
extendPath: Array<CSSResolve<ClassSymbol | ElementSymbol>>,
meta: StylableMeta,
className: string,
isElement: boolean
) => void
): Array<CSSResolve<ClassSymbol | ElementSymbol>> {
const bucket = isElement ? meta.elements : meta.classes;
const customSelector = isElement ? null : meta.customSelectors[':--' + className];
if (!bucket[className] && !customSelector) {
return [];
}
if (customSelector && transformer) {
const parsed = transformer.resolveSelectorElements(meta, customSelector);
if (parsed.length === 1) {
return parsed[0][parsed[0].length - 1].resolved;
} else {
return [];
}
}
let current = {
_kind: 'css' as const,
symbol: bucket[className],
meta,
};
const extendPath: Array<CSSResolve<ClassSymbol | ElementSymbol>> = [];
while (current?.symbol) {
if (isInPath(extendPath, current)) {
break;
}
extendPath.push(current);
const parent = current.symbol[valueMapping.extends] || current.symbol.alias;
if (parent) {
if (parent._kind === 'import') {
const res = this.resolve(parent);
if (
res &&
res._kind === 'css' &&
res.symbol &&
(res.symbol._kind === 'element' || res.symbol._kind === 'class')
) {
const { _kind, meta, symbol } = res;
current = {
_kind,
meta,
symbol,
};
} else {
if (reportError) {
reportError(res, parent, extendPath, meta, className, isElement);
}
break;
}
} else {
current = { _kind: 'css', symbol: parent, meta };
}
} else {
break;
}
}
return extendPath;
}
public validateImports(meta: StylableMeta, diagnostics: Diagnostics) {
for (const importObj of meta.imports) {
const resolvedImport = this.resolveImported(importObj, '');
if (!resolvedImport) {
// warn about unknown imported files
const fromDecl =
importObj.rule.nodes &&
importObj.rule.nodes.find(
(decl) => decl.type === 'decl' && decl.prop === valueMapping.from
);
if (fromDecl) {
diagnostics.warn(
fromDecl,
resolverWarnings.UNKNOWN_IMPORTED_FILE(importObj.fromRelative),
{ word: importObj.fromRelative }
);
}
} else if (resolvedImport._kind === 'css') {
// warn about unknown named imported symbols
for (const name in importObj.named) {
const origName = importObj.named[name];
const resolvedSymbol = this.resolveImported(importObj, origName);
const namedDecl =
importObj.rule.nodes &&
importObj.rule.nodes.find(
(decl) => decl.type === 'decl' && decl.prop === valueMapping.named
);
if (!resolvedSymbol!.symbol && namedDecl) {
diagnostics.warn(
namedDecl,
resolverWarnings.UNKNOWN_IMPORTED_SYMBOL(
origName,
importObj.fromRelative
),
{ word: origName }
);
}
}
}
}
}
}