import { existsSync, readdirSync, readFileSync, unlinkSync, watch, writeFileSync } from 'node:fs'; import path from 'node:path'; import postcss from 'postcss'; import * as sass from 'sass'; const packageDirectory = path.join(__dirname, '..'); const sourceDirectory = path.join(packageDirectory, 'src'); const loadPaths = [sourceDirectory, path.join(packageDirectory, '../../node_modules/include-media/dist')]; const compiler = sass.initCompiler(); generateAll(); if (process.argv.includes('--watch')) { watchModuleStylesheets(); } else { compiler.dispose(); } function generateAll() { const files = findModuleStylesheets(); for (const file of files) { generate(file); } console.log(`Generated type definitions for ${files.length} .module.scss files`); } function findModuleStylesheets(): string[] { return readdirSync(sourceDirectory, { encoding: 'utf8', recursive: true }) .filter((file) => file.endsWith('.module.scss')) .sort() .map((file) => path.join(sourceDirectory, file)); } function watchModuleStylesheets() { console.log(`Watching ${sourceDirectory} for .module.scss changes`); watch(sourceDirectory, { recursive: true }, (_event, filename) => { if (!filename || !filename.endsWith('.module.scss')) { return; } const file = path.join(sourceDirectory, filename); try { if (existsSync(file)) { generate(file); } else if (existsSync(getTypesFilepath(file))) { unlinkSync(getTypesFilepath(file)); } } catch (error) { console.error(String(error)); } }); } function generate(file: string) { const { css } = compiler.compile(file, { loadPaths, quietDeps: true }); const content = formatTypesFile(extractClassNames(css)); const typesFilepath = getTypesFilepath(file); if (!existsSync(typesFilepath) || readFileSync(typesFilepath, 'utf8') !== content) { writeFileSync(typesFilepath, content); console.log(`Generated ${path.relative(packageDirectory, typesFilepath)}`); } } function extractClassNames(css: string): string[] { const classNames = new Set(); const stylesheet = postcss.parse(css); stylesheet.walkAtRules('keyframes', (atRule) => { classNames.add(atRule.params); }); stylesheet.walkRules((rule) => { if (rule.selector === ':export') { rule.walkDecls((declaration) => { classNames.add(declaration.prop); }); } else { const localSelector = rule.selector.replace(/:global\([^)]*\)/g, ''); for (const [, className] of localSelector.matchAll(/\.(-?[A-Za-z_][\w-]*)/g)) { classNames.add(className); } } }); return [...classNames].sort(); } function formatTypesFile(classNames: string[]): string { return [ '// Generated by scripts/generate-scss-types.ts - do not edit.', 'declare const styles: {', ...classNames.map((className) => ` readonly ${formatKey(className)}: string;`), '};', '', 'export default styles;', '', ].join('\n'); } function formatKey(className: string): string { return /^[A-Za-z_$][\w$]*$/.test(className) ? className : `'${className}'`; } function getTypesFilepath(file: string): string { return `${file}.d.ts`; }