UNPKG

@lipemat/js-boilerplate

Version:

Dependencies and scripts for a no config JavaScript app

116 lines (96 loc) 3.42 kB
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 }