css-module-type-definitions
Version:
Generate Type Definitions for CSS Modules
197 lines (170 loc) • 7.03 kB
text/typescript
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;