@lipemat/js-boilerplate
Version:
Dependencies and scripts for a no config JavaScript app
116 lines (96 loc) • 3.42 kB
text/typescript
import type {LoaderContext} from 'webpack';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {basename} from 'path';
import camelCase from '../helpers/camel-case.js';
/**
* Extracts CSS class names from a CSS Module into TypeScript definitions.
*
* Allows TS and PHPStorm to validate uses of CSS Modules in the codebase.
*
* Inspired by: `@teamsupercell/typings-for-css-modules-loader` but without dependencies and under our control.
*/
export default function createCssModuleTypings( this: LoaderContext<Record<string, never>>, content: string, ...args: [] ): void {
if ( 'cacheable' in this && 'function' === typeof this.cacheable ) {
this.cacheable();
}
try {
const indexOfLocals = content.indexOf( '.locals' );
const cssModuleKeys = -1 === indexOfLocals ? [] : getCssModuleKeys( content.substring( indexOfLocals ) );
if ( cssModuleKeys.length > 0 ) {
const fileName = this.resourcePath;
const cssModuleDefinition = generateModuleTypeDefinition( cssModuleKeys, camelCase( basename( fileName ), true ) );
const typingsPath = fileName.replace( /\.pcss$/, '.pcss.d.ts' );
writeTypingsFile( typingsPath, cssModuleDefinition );
}
this.callback( null, content, ...args );
} catch ( error ) {
this.emitError( error as Error );
}
}
/**
* Formats the content of a TypeScript definition file.
*
* Turns a list of CSS module keys into a TypeScript interface definition.
*/
function generateModuleTypeDefinition( cssModuleKeys: string[], pascalCaseFileName: string, ): string {
const interfaceName = `I${pascalCaseFileName}`;
const moduleName = `${pascalCaseFileName}Module`;
const namespaceName = `${pascalCaseFileName}Namespace`;
const interfaceProperties = [ ...cssModuleKeys ]
.sort()
.map( ( key: string ) => {
if ( key.includes( '-' ) ) {
return `\t\t'${key}': string;`;
}
return `\t\t${key}: string;`;
} )
.join( '\n' );
return `// Autogenerated by typings-for-css-modules-loader.
declare namespace ${namespaceName} {
export interface I${pascalCaseFileName} {
${interfaceProperties}
}
}
declare const ${moduleName}: ${namespaceName}.${interfaceName};
export = ${moduleName};
`;
}
/**
* Write the typing file if it does not exist or if the content has changed.
*/
function writeTypingsFile( fileName: string, content: string ): void {
if ( ! existsSync( fileName ) ) {
writeTypingsFile.fileWriter( fileName, content );
} else {
const existingContent = readFileSync( fileName, 'utf8' );
if ( existingContent !== content ) {
writeTypingsFile.fileWriter( fileName, content );
}
}
}
writeTypingsFile.fileWriter = writeFileSync;
/**
* Extracts CSS module keys from the content of a CSS Module file.
*
* @param {string} content - The content of the CSS Module file.
* @return {string[]} - An array of unique CSS module keys.
*/
function getCssModuleKeys( content: string ): string[] {
const keyRegex = /"([^"\n]+)":/g;
let match: string[] | null;
const cssModuleKeys: string[] = [];
while ( ( match = keyRegex.exec( content ) ) ) {
if ( -1 === cssModuleKeys.indexOf( match[ 1 ] ) ) {
cssModuleKeys.push( match[ 1 ] );
}
}
return cssModuleKeys;
}
/**
* Modifier file writer used by the writeTypingsFile function.
*
* Here for unit test overrides of `writeFileSync`.
*/
export function modifyFileWriter( fn: typeof writeTypingsFile.fileWriter ): void {
writeTypingsFile.fileWriter = fn
}