UNPKG

wix-style-react

Version:
215 lines (187 loc) • 6.51 kB
import { Stylable } from '@stylable/core'; import { FileSystem } from '@stylable/node'; import { camelCase, upperFirst } from 'lodash'; import { basename, join, relative, dirname, resolve } from 'path'; import components from '../../../../.wuf/components.json'; function addDotSlash(p: string) { p = p.replace(/\\/g, '/'); return p.startsWith('.') ? p : './' + p; } function ensureDirectory(dir: string, fs: FileSystem) { if (dir === '.' || fs.existsSync(dir)) { return; } try { fs.mkdirSync(dir); } catch (e) { const parentDir = dirname(dir); if (parentDir !== dir) { ensureDirectory(parentDir, fs); fs.mkdirSync(dir); } } } function tryRun<T>(fn: () => T, errorMessage: string): T { try { return fn(); } catch (e) { throw new Error(errorMessage + ': \n' + e.stack); } } export interface ReExports { root: string; classes: Record<string, string>; keyframes: Record<string, string>; vars: Record<string, string>; stVars: Record<string, string>; } export class Generator { private indexFileOutput: Array<{ from: string; reExports: ReExports; }> = []; private collisionDetector = new NameCollisionDetector<string>(); constructor( public stylable: Stylable, private log: (...args: string[]) => void, ) {} public generateReExports(filePath: string): ReExports { const meta = this.stylable.process(resolve(filePath)); const rootExport = this.filename2varname(filePath); const vars = Object.keys(meta.cssVars).reduce<Record<string, string>>( (acc, varName) => { acc[varName] = `--${rootExport}__${varName.slice(2)}`; return acc; }, {}, ); const keyframes = Object.keys(meta.mappedKeyframes).reduce< Record<string, string> >((acc, keyframe) => { acc[keyframe] = `${rootExport}__${keyframe}`; return acc; }, {}); const classes = Object.keys(meta.classes) .filter(name => name !== meta.root) .reduce<Record<string, string>>((acc, className) => { acc[className] = `${rootExport}__${className}`; return acc; }, {}); const stVars = meta.vars.reduce<Record<string, string>>((acc, { name }) => { acc[name] = `${rootExport}__${name}`; return acc; }, {}); return { root: rootExport, classes, keyframes, stVars, vars, }; } public generateFileIndexEntry(filePath: string, fullOutDir: string) { const extractedFileName = filePath .match(/[ \w-]+\.st.css/)[0] .replace('.st.css', ''); // ignore non component name stylable stylesheets if (!components[extractedFileName]) { return; } const componentRelativePath = components[extractedFileName].path; const componentFullPath = `${ filePath.split('src/')[0] }${componentRelativePath}/${extractedFileName}.st.css`; // ignore all other stylable files that are not directly positioned next to the component if (componentFullPath !== filePath) { return; } const from = addDotSlash(relative(fullOutDir, filePath)); const reExports = this.generateReExports(filePath); this.checkForCollisions(reExports, filePath); this.log('[Generator Index]', `Add file: ${filePath}`); this.indexFileOutput.push({ reExports, from }); } public generateIndexFile( fs: FileSystem, fullOutDir: string, indexFile: string, ) { const indexFileTargetPath = join(fullOutDir, indexFile); const indexFileContent = this.generateIndexSource(indexFileTargetPath); ensureDirectory(fullOutDir, fs); tryRun( () => fs.writeFileSync(indexFileTargetPath, '\n' + indexFileContent + '\n'), 'Write Index File Error', ); this.log( '[Generator Index]', 'creating index file: ' + indexFileTargetPath, ); } public filename2varname(filePath: string) { const varname = basename(basename(filePath, '.css'), '.st') // remove prefixes and .st.css ext .replace(/^\d+/, ''); // remove leading numbers return upperFirst(camelCase(varname)); } protected generateIndexSource(_indexFileTargetPath: string) { return this.indexFileOutput .map(_ => createImportForComponent(_.from, _.reExports)) .join('\n'); } private checkForCollisions(reExports: ReExports, filePath: string) { this.collisionDetector.detect(reExports.root, filePath); for (const asName of Object.values(reExports.classes)) { this.collisionDetector.detect(asName, filePath); } for (const asName of Object.values(reExports.vars)) { this.collisionDetector.detect(asName, filePath); } for (const asName of Object.values(reExports.stVars)) { this.collisionDetector.detect(asName, filePath); } for (const asName of Object.values(reExports.keyframes)) { this.collisionDetector.detect(`keyframes(${asName})`, filePath); } if (this.collisionDetector.collisions.size) { let errorMessage = 'Name Collision Error:'; for (const [name, origin] of this.collisionDetector.collisions) { errorMessage += `\nexport symbol ${name} from ${filePath} is already used by ${origin}`; } throw new Error(errorMessage); } } } class NameCollisionDetector<Origin> { nameMapping = new Map<string, Origin>(); collisions = new Map<string, Origin>(); detect(name: string, origin: Origin) { if (this.nameMapping.has(name)) { this.collisions.set(name, this.nameMapping.get(name)!); } else { this.nameMapping.set(name, origin); } } } function createImportForComponent(from: string, reExports: ReExports) { const namedPart = [ ...Object.entries(reExports.classes).map(symbolMapper), ...Object.entries(reExports.stVars).map(symbolMapper), ...Object.entries(reExports.vars).map(symbolMapper), ...Object.entries(reExports.keyframes).map(keyframesSymbolMapper), ].join(', '); const usagePart = Object.values(reExports.classes) .map(exportName => `.root .${exportName}{}`) .join(' '); return `:import {-st-from: ${JSON.stringify(from)};-st-default:${ reExports.root };${namedPart ? `-st-named: ${namedPart};` : ''}}\n.root ${reExports.root}{}${ usagePart ? `\n${usagePart}` : '' }`; } function symbolMapper([name, as]: [string, string]) { return name === as ? as : `${name} as ${as}`; } function keyframesSymbolMapper([name, as]: [string, string]) { return name === as ? `keyframes(${as})` : `keyframes(${name} as ${as})`; }