UNPKG

css-module-type-definitions

Version:
197 lines (170 loc) 7.03 kB
import process from 'process'; import path from 'path'; import sane from 'sane'; import glob from 'glob-promise'; import chalk from 'chalk'; import os from 'os'; import fs from 'fs-extra'; import defaultTo from 'lodash/defaultTo'; import parser from './parser'; import validateToken from './validateToken'; export type CMTDOptions = { rootDirectoryPath?: string; inputDirectoryName?: string; outputDirectoryName?: string; globPattern?: string; dropExtensions?: boolean; camelCase?: boolean; logger?: Logger; config?: any; }; export type Logger = { info: (x: string) => void; warn: (x: string) => void; error: (x: string) => void; }; export class CMTD { private readonly rootDirectoryPath: string; private readonly inputDirectoryName: string; private readonly outputDirectoryName: string; private readonly globPattern: string; private readonly dropExtensions: boolean; private readonly camelCase: boolean; private readonly logger: Logger; private readonly config: any; constructor( { rootDirectoryPath, inputDirectoryName, outputDirectoryName, globPattern, dropExtensions, camelCase, logger, config, }: CMTDOptions ) { this.rootDirectoryPath = defaultTo(rootDirectoryPath, process.cwd()); this.inputDirectoryName = defaultTo(inputDirectoryName, '.'); this.outputDirectoryName = defaultTo(outputDirectoryName, this.inputDirectoryName); this.globPattern = defaultTo(globPattern, '**.*.css'); this.dropExtensions = defaultTo(dropExtensions, false); this.camelCase = defaultTo(camelCase, false); this.logger = defaultTo(logger, console); this.config = defaultTo(config, undefined); } private async generateTypes(filePath: string) { try { const tokens = await parser(filePath, this.config); const fileName = path.isAbsolute(filePath) ? path.relative(this.inputDirectoryName, filePath) : filePath; const outputDirectoryPath = path.resolve(this.rootDirectoryPath, this.outputDirectoryName); const outputFileBase = this.dropExtensions ? CMTD.removeExtension(fileName) : fileName; const outputFilePath = path.join(outputDirectoryPath, `${outputFileBase}.d.ts`); const declarations: string[] = []; for(const token of Array.from(tokens.keys()).sort()) { let key = token; let valid = validateToken(key); if(this.camelCase) { const camelKey = CMTD.toCamelCase(key); if(camelKey !== key) { declarations.push(`'${key}'`); key = camelKey; valid = validateToken(key); } } if(valid.isValid) { declarations.push(`'${key}'`); } else { declarations.push(`'${key}'`); this.logger.warn(`{CMTD} ${chalk.yellow(`${fileName}: ${valid.message}`)}`); } } if(!CMTD.exists(outputDirectoryPath)) fs.mkdirpSync(outputDirectoryPath); var fileContent = [ '/* eslint-disable max-len */', '/*', ' * This file is automatically generated by css-module-type-definitions', ' */', '', ]; if(declarations.length > 0) { fileContent.push( `export type Keys = ${declarations.join(' | ')};`, 'export type Css = {[key in Keys]: string };', ); } else { fileContent.push( 'export type Keys = never;', 'export type Css = never;', ); } fileContent.push( '', 'declare const css: Css;', 'export default css;', ); let existing = ''; const replacement: string = fileContent.join(os.EOL) + os.EOL; if(fs.existsSync(outputFilePath)) existing = fs.readFileSync(outputFilePath, 'utf8'); if(existing !== replacement) { fs.writeFileSync(outputFilePath, replacement, 'utf8'); this.logger.info(`{CMTD} ${chalk.green('Types Generated for')} ${fileName}`); } } catch(err: unknown) { this.logger.error(`{CMTD} ${err} ${JSON.stringify(err)}`); } } public async scan() { try { const files = await glob( path.join(path.resolve(this.rootDirectoryPath, this.inputDirectoryName), this.globPattern) ); await Promise.all(files.map(async f => this.generateTypes(f))); } catch(err: unknown) { this.logger.error(`{CMTD} ${JSON.stringify(err)}`); throw err; } } public watch() { const DELAY = 10; // Number of milliseconds to delay for file to finish writing const target = path.resolve(this.rootDirectoryPath, this.inputDirectoryName); this.logger.info(`{CMTD} ${chalk.blue('Watching')} ${this.inputDirectoryName} ${this.globPattern}`); const watcher = sane(target, { glob: this.globPattern }); watcher.on( 'add', (file: string) => { void (async () => this.generateTypes(path.join(target, file)))(); } ); watcher.on( 'change', (file: string) => { setTimeout( () => { void (async () => this.generateTypes(path.join(target, file)))(); }, DELAY ); } ); } private static removeExtension(filePath: string) { const ext = path.extname(filePath); return filePath.replace(new RegExp(`${ext}$`, 'u'), ''); } private static exists(pathname: string): boolean { try { fs.statSync(pathname); return true; } catch{ return false; } } private static toCamelCase(str: string) { return str.replace(/-+(\w)/ug, (_match, firstLetter) => firstLetter.toUpperCase()); } } export { CMTDWebpackPlugin } from './webpack-plugin'; export default CMTD;