UNPKG

@stylable/core

Version:

CSS for Components

318 lines (296 loc) 11.4 kB
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 } ); } } } } } }