diff --git a/README.md b/README.md index 4441bc6..3a836e2 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,17 @@ A Rspack and Webpack loader for generating TypeScript declaration files (`.d.ts` ## Overview -**css-modules-dts-loader** automatically generates (or verifies) `.d.ts` files from your CSS Modules. By parsing the CSS module output, it extracts class names, applies optional transformations (such as camelCase conversion or sorting), and produces a corresponding TypeScript declaration file. This enhances type safety and improves the developer experience when using CSS Modules in TypeScript projects. +**css-modules-dts-loader** automatically generates (or verifies) `.d.ts` files from your CSS Modules. By parsing the CSS module output, it extracts class names, applies optional transformations (matching css-loader's behavior), and produces corresponding TypeScript declaration files. This enhances type safety and improves the developer experience when using CSS Modules in TypeScript projects. ## Features - **Automatic Declaration Generation:** Creates `.d.ts` files alongside your CSS files. -- **Verification Mode:** Checks if an existing declaration file is up-to-date. -- **Customizable Formatting:** Options to convert class names to camelCase, choose quote style, set indentation (tabs or spaces), and sort class names. -- **Seamless Webpack Integration:** Easily integrates into your Webpack configuration with minimal setup. -- **Logging and Error Handling:** Uses Webpack's logging and error emission for clear diagnostics. +- **css-loader v7 Compatible:** Fully supports css-loader's `exportLocalsConvention` option. +- **Named Exports Support:** Generates named exports or interface-based exports. +- **JavaScript Keywords Handling:** Properly handles reserved keywords as class names. +- **Verification Mode:** Checks if existing declaration files are up-to-date (perfect for CI/CD). +- **Customizable Formatting:** Choose quote style, indentation (tabs or spaces), and sorting. +- **Seamless Integration:** Works with Webpack 5+ and Rspack. ## Installation @@ -26,105 +28,129 @@ Install via npm: npm install --save-dev css-modules-dts-loader ``` +Or with pnpm: + +```bash +pnpm add -D css-modules-dts-loader +``` + ## Usage ### Webpack Configuration -Integrate the loader into your Webpack configuration. For example, if you are working with CSS or any supported preprocessor files: +**Important:** The loader options should match your css-loader configuration for consistent behavior. ```js -const isNamedExport = true; -// webpack.config.js +const isProduction = process.env.NODE_ENV === "production"; +const useNamedExports = true; // Should match css-loader setting + module.exports = { - // ... other webpack config settings - module: { - rules: [ - { - test: /\.module\.(css|postcss|pcss|scss|sass|less|styl|sss)$/i, - use: [ - "style-loader", - { - loader: "css-modules-dts-loader", - options: { - // Convert CSS class names to camelCase (default: false) - camelCase: true, - // Quote style: "single" or "double" (default: "double") - quote: "single", - // Indentation style: "tab" or "space" (default: "space") - indentStyle: "space", - // Number of spaces if indentStyle is "space" (default: 2) - indentSize: 2, - // Mode: "emit" to generate or "verify" to check the file (default: "emit") - mode: isProduction ? "verify" : "emit", - // Sort the exported class names alphabetically (default: false) - sort: true, - // Use named exports instead of interface (default: true) - namedExport: isNamedExport, - // Custom banner comment at the top of the file - banner: "// This file is automatically generated.\n// Please do not change this file!" - } - }, - { - loader: require.resolve("css-loader"), - options: { - sourceMap: true, - modules: { - namedExport: isNamedExport, - exportLocalsConvention: "as-is", - localIdentName: "[name]__[local]___[hash:base64]" - }, - importLoaders: 1 - } - } - ] - } - ] - } + module: { + rules: [ + { + test: /\.module\.(css|postcss|pcss|scss|sass|less|styl|sss)$/i, + use: [ + "style-loader", + { + loader: "css-modules-dts-loader", + options: { + // Export format (should match css-loader) + namedExport: useNamedExports, + exportLocalsConvention: "camel-case-only", + + // File generation mode + mode: isProduction ? "verify" : "emit", + + // Formatting options + quote: "double", + indentStyle: "space", + indentSize: 2, + sort: true, + + // Custom banner + banner: "// This file is automatically generated.\n// Please do not change this file!" + } + }, + { + loader: "css-loader", + options: { + modules: { + // These should match the dts-loader options above + namedExport: useNamedExports, + exportLocalsConvention: "camel-case-only" + } + } + } + ] + } + ] + } }; ``` ### Loader Options -| Option | Type | Default | Description | -| ------------- | -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `camelCase` | `boolean` | `false` | When set to `true`, converts CSS class names from kebab-case to camelCase in the generated `.d.ts` file. | -| `quote` | `"single"` or `"double"` | `"double"`| Sets the quote style used for keys in the declaration file. | -| `indentStyle` | `"tab"` or `"space"` | `"space"` | Determines whether to use tabs or spaces for indentation. | -| `indentSize` | `number` | `2` | The number of spaces used for indentation if `indentStyle` is set to `"space"`. | -| `mode` | `"emit"` or `"verify"` | `"emit"` | In `"emit"` mode, the loader writes (or overwrites) the `.d.ts` file. In `"verify"` mode, it checks if the file exists and is up to date, emitting an error if not. | -| `sort` | `boolean` | `false` | When `true`, sorts the extracted CSS class names alphabetically before generating the declaration file. | -| `namedExport` | `boolean` | `true` | When `true`, generates named exports for each class. When `false`, generates an interface with a default export. | -| `banner` | `string` | See desc. | Custom banner comment added at the top of the file. Default: `"// This file is automatically generated.\n// Please do not change this file!"` | +| Option | Type | Default | Description | +| ------------------------ | --------------------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `exportLocalsConvention` | `"as-is"` \| `"camel-case"` \| `"camel-case-only"` \| `"dashes"` \| `"dashes-only"` | See description | How to transform class names. Defaults based on `namedExport`: `"as-is"` if `true`, `"camel-case-only"` if `false`. Matches css-loader's option. | +| `namedExport` | `boolean` | `true` | When `true`, generates named exports (`export const foo: string;`). When `false`, generates an interface with default export. Should match css-loader's setting. | +| `quote` | `"single"` \| `"double"` | `"double"` | Quote style used for interface properties when `namedExport` is `false`. | +| `indentStyle` | `"tab"` \| `"space"` | `"space"` | Indentation style for interface properties. | +| `indentSize` | `number` | `2` | Number of spaces for indentation when `indentStyle` is `"space"`. | +| `mode` | `"emit"` \| `"verify"` | `"emit"` | In `"emit"` mode, writes/overwrites `.d.ts` files. In `"verify"` mode, checks files are up-to-date (useful for CI/CD). | +| `sort` | `boolean` | `false` | When `true`, sorts class names alphabetically in the generated file. | +| `banner` | `string` | Auto-generated | Custom banner comment at the top of generated files. Default: `"// This file is automatically generated.\n// Please do not change this file!"` | +| `camelCase` | `boolean` | `undefined` | **Deprecated (will be removed in v2.0.0):** Use `exportLocalsConvention` instead. When `true`, maps to `"camel-case-only"`. When `false`, maps to `"as-is"`. | -### How It Works +### exportLocalsConvention Values -1. **Caching:** - The loader marks its result as cacheable using `this.cacheable()` if supported by Webpack. +This option matches css-loader's behavior: -2. **Options Validation:** - It validates the provided options against a predefined schema (using `schema-utils`) to ensure all options are valid. +| Value | Description | Example: `.foo-bar` | +| ------------------ | ----------------------------------------------------------------- | ---------------------------------- | +| `"as-is"` | Class names exported exactly as written | `fooBar` → exports `"foo-bar"` | +| `"camel-case"` | Exports both original and camelCase versions | `fooBar` → exports both | +| `"camel-case-only"`| Exports only camelCase version | `fooBar` → exports `fooBar` only | +| `"dashes"` | Converts dashes to camelCase, exports both | Same as `"camel-case"` | +| `"dashes-only"` | Converts dashes to camelCase, exports only camelCase | Same as `"camel-case-only"` | -3. **Extracting Class Names:** - The loader extracts CSS class names from the module source using a regular expression that matches exported variables (e.g., `export const button = ...`). +### JavaScript Keywords Handling -4. **Processing Class Names:** - Depending on the configuration, it may convert the class names to camelCase and/or sort them. +When using `namedExport: true`, JavaScript reserved keywords (like `class`, `export`, `import`, etc.) **cannot** be exported as named exports because they're invalid JavaScript syntax. -5. **Generating the `.d.ts` Content:** - The loader constructs a declaration file with a header comment, an interface defining the class names (using configurable indentation and quotes), and exports the interface. +**Behavior:** +- Keywords are **skipped** in the generated `.d.ts` file +- You can still access them via namespace import: `import * as styles from './styles.css'` and then `styles["class"]` -6. **File Handling:** - - In `"emit"` mode, it writes or overwrites the declaration file next to the source file (by replacing the original file extension with `.d.ts`). - - In `"verify"` mode, it compares the generated content with the existing file and emits an error if discrepancies are found. +**Example:** -7. **Logging and Error Handling:** - Webpack’s logging API (`this.getLogger`) and error emission (`this.emitError`) are used to provide clear feedback during the build process. +```css +/* styles.module.css */ +.class { color: blue; } +.container { padding: 10px; } +``` -### Example Output +Generated with `namedExport: true`: +```ts +// styles.module.css.d.ts +export const container: string; +// Note: "class" is skipped because it's a keyword +``` -For a CSS module that exports class names like `button` and `container`, the generated declaration file will look like one of these formats based on the `namedExport` option: +Usage in TypeScript: +```ts +import * as styles from './styles.module.css'; -With `namedExport: true` (default): +// Works fine: +styles.container; // Type-safe +styles["class"]; // Also works, but not type-safe +``` + +**Recommendation:** If you need type-safety for keyword class names, use `namedExport: false` which generates an interface including all classes. + +### Example Output + +#### With `namedExport: true` (default) ```ts // This file is automatically generated. // Please do not change this file! @@ -132,41 +158,85 @@ export const button: string; export const container: string; ``` -With `namedExport: false`: +Usage: +```ts +import { button, container } from './styles.module.css'; +// or +import * as styles from './styles.module.css'; +``` + +#### With `namedExport: false` ```ts // This file is automatically generated. // Please do not change this file! interface CssExports { - 'button': string; - 'container': string; + "button": string; + "container": string; + "class": string; // Keywords work here! } export const cssExports: CssExports; export default cssExports; ``` -The banner comment at the top can be customized using the `banner` option. +Usage: +```ts +import styles from './styles.module.css'; + +styles.button; +styles.container; +styles.class; // Keywords are type-safe here +``` + +### Configuration Tips + +1. **Match css-loader options:** Your `namedExport` and `exportLocalsConvention` options should match css-loader's configuration to avoid runtime/type mismatches. + +2. **Use verify mode in CI:** Set `mode: "verify"` in production/CI to ensure `.d.ts` files are always up-to-date. + +3. **Avoid keywords in class names:** While technically possible, using JavaScript keywords as class names can cause confusion. Consider using prefixes like `btn-class` instead of `class`. + +## How It Works + +1. **Runs after css-loader:** The loader processes the JavaScript output from css-loader, not the raw CSS. + +2. **Extracts class names:** Parses the module exports to find all CSS class names (supports both object and named export formats). + +3. **Applies transformations:** Based on `exportLocalsConvention`, transforms class names (e.g., kebab-case → camelCase). + +4. **Generates declarations:** Creates TypeScript declaration files with appropriate export format. + +5. **Writes or verifies:** In `emit` mode, writes `.d.ts` files. In `verify` mode, checks they're up-to-date. + +## Migrating from camelCase to exportLocalsConvention + +If you're using the deprecated `camelCase` option: + +```js +// Old (deprecated) +{ + camelCase: true +} + +// New (recommended) +{ + exportLocalsConvention: "camel-case-only" +} +``` + +The `camelCase` option still works for backward compatibility but will be removed in v2.0.0. ## Contributing Contributions, bug reports, and feature requests are welcome! To contribute: -1. Fork the repository. -2. Create a feature branch: - ```bash - git checkout -b my-new-feature - ``` -3. Commit your changes: - ```bash - git commit -am 'Add some feature' - ``` -4. Push to the branch: - ```bash - git push origin my-new-feature - ``` -5. Create a new Pull Request. - -Before contributing, please review the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines (if available) for additional details. +1. Fork the repository +2. Create a feature branch: `git checkout -b my-new-feature` +3. Commit your changes: `git commit -am 'Add some feature'` +4. Push to the branch: `git push origin my-new-feature` +5. Create a new Pull Request + +Before contributing, please review the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines (if available). ## License @@ -178,4 +248,4 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and recent changes. ## Support -If you encounter any issues or have questions, please open an issue in the [GitHub repository](https://github.com/Ch-Valentine/css-modules-dts-loader/issues). \ No newline at end of file +If you encounter any issues or have questions, please open an issue in the [GitHub repository](https://github.com/Ch-Valentine/css-modules-dts-loader/issues). diff --git a/package.json b/package.json index b35ef83..9458fae 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "build:watch": "tsc --watch", "clean": "rm -rf dist", "type-check": "tsc --noEmit && tsc --project tsconfig.test.json", - "test": "vitest run --coverage", + "test": "pnpm build && vitest run --coverage", "test:watch": "vitest", "lint": "eslint .", "lint:fix": "eslint . --fix", diff --git a/src/constants.ts b/src/constants.ts index 3687caf..8c63a59 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,8 +1,20 @@ +/** + * Export locals convention types matching css-loader + */ +export type ExportLocalsConvention = + | "as-is" + | "camel-case" + | "camel-case-only" + | "dashes" + | "dashes-only"; + /** * Loader options interface */ export interface LoaderOptions { + /** @deprecated Use exportLocalsConvention instead. Will be removed in v2.0 */ camelCase?: boolean; + exportLocalsConvention?: ExportLocalsConvention; quote?: "single" | "double"; indentStyle?: "tab" | "space"; indentSize?: number; @@ -59,6 +71,10 @@ export const SCHEMA: SchemaDefinition = { type: "object", properties: { camelCase: { type: "boolean" }, + exportLocalsConvention: { + type: "string", + enum: ["as-is", "camel-case", "camel-case-only", "dashes", "dashes-only"] + }, quote: { type: "string", enum: ["single", "double"] }, indentStyle: { type: "string", enum: ["tab", "space"] }, indentSize: { type: "number" }, @@ -72,13 +88,15 @@ export const SCHEMA: SchemaDefinition = { /** * Default options for the loader. + * Note: exportLocalsConvention default depends on namedExport and is set dynamically */ -export const DEFAULT_OPTIONS: Required = { - camelCase: false, - quote: "double", - indentStyle: "space", +export const DEFAULT_OPTIONS = { + camelCase: undefined as boolean | undefined, + exportLocalsConvention: undefined as ExportLocalsConvention | undefined, + quote: "double" as const, + indentStyle: "space" as const, indentSize: 2, - mode: "emit", + mode: "emit" as const, sort: false, namedExport: true, banner: "// This file is automatically generated.\n// Please do not change this file!" diff --git a/src/index.ts b/src/index.ts index 6a67ced..4644760 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,7 +91,19 @@ export default function cssModuleTypesLoader(this: LoaderContext, source: string }); // Merge default options with user options - const options = { ...DEFAULT_OPTIONS, ...providedOptions }; + const mergedOptions = { ...DEFAULT_OPTIONS, ...providedOptions }; + + // Handle backward compatibility: camelCase -> exportLocalsConvention + if (mergedOptions.camelCase !== undefined && mergedOptions.exportLocalsConvention === undefined) { + mergedOptions.exportLocalsConvention = mergedOptions.camelCase ? "camel-case-only" : "as-is"; + } + + // Set exportLocalsConvention default based on namedExport (matching css-loader) + if (mergedOptions.exportLocalsConvention === undefined) { + mergedOptions.exportLocalsConvention = mergedOptions.namedExport ? "as-is" : "camel-case-only"; + } + + const options = mergedOptions as Required; const classNames = extractClassNames(source); diff --git a/src/utils.ts b/src/utils.ts index 46417ea..1661848 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ /** * Utility functions for CSS Modules DTS loader. */ -import { CSS_MODULE_PATTERNS, EXPORT_MARKERS, JS_KEYWORDS, LoaderOptions } from "./constants.js"; +import { CSS_MODULE_PATTERNS, EXPORT_MARKERS, JS_KEYWORDS, LoaderOptions, ExportLocalsConvention } from "./constants.js"; import { readFileSync, writeFileSync, existsSync } from "fs"; /** @@ -32,7 +32,9 @@ export interface HandleDtsFileParams { */ export interface GenerateDtsContentParams { classNames: string[]; - options: Required; + options: Required> & { + exportLocalsConvention: ExportLocalsConvention; + }; } /** @@ -79,6 +81,91 @@ export const enforceLFLineSeparators = (text: string): string => typeof text === */ export const toCamelCase = (name: string): string => name.replace(/-([a-z])/g, (_, p1) => p1.toUpperCase()); +/** + * Transforms class names according to exportLocalsConvention setting. + * Matches css-loader's behavior. + * + * @param classNames - Array of original class names + * @param convention - The export locals convention to apply + * @returns Array of transformed class names (may include duplicates if convention exports both forms) + * + * @example + * ```ts + * applyExportLocalsConvention(["foo-bar"], "as-is") // ["foo-bar"] + * applyExportLocalsConvention(["foo-bar"], "camel-case") // ["foo-bar", "fooBar"] + * applyExportLocalsConvention(["foo-bar"], "camel-case-only") // ["fooBar"] + * ``` + */ +export function applyExportLocalsConvention( + classNames: string[], + convention: ExportLocalsConvention +): string[] { + const result: string[] = []; + const seen = new Set(); + + for (const className of classNames) { + switch (convention) { + case "as-is": + // Export exactly as-is + if (!seen.has(className)) { + result.push(className); + seen.add(className); + } + break; + + case "camel-case": { + // Export both original and camelCase + if (!seen.has(className)) { + result.push(className); + seen.add(className); + } + const camelCased = toCamelCase(className); + if (!seen.has(camelCased) && camelCased !== className) { + result.push(camelCased); + seen.add(camelCased); + } + break; + } + + case "camel-case-only": { + // Export only camelCase + const camelCased = toCamelCase(className); + if (!seen.has(camelCased)) { + result.push(camelCased); + seen.add(camelCased); + } + break; + } + + case "dashes": { + // Export both original and camelCase (same as camel-case) + if (!seen.has(className)) { + result.push(className); + seen.add(className); + } + const camelCased = toCamelCase(className); + if (!seen.has(camelCased) && camelCased !== className) { + result.push(camelCased); + seen.add(camelCased); + } + break; + } + + case "dashes-only": { + // Export only camelCase (same as camel-case-only) + const camelCased = toCamelCase(className); + if (!seen.has(camelCased)) { + result.push(camelCased); + seen.add(camelCased); + } + break; + } + } + } + + return result; +} + /** * Determines the CSS loader version and export format based on source content. * Supports css-loader versions 3, 4, and 5 with different export formats. @@ -229,20 +316,19 @@ export function handleDtsFile({ dtsFilePath, dtsContent, mode, logger: _logger, * Generates the content for a .d.ts file based on extracted CSS class names and options. * * This function performs several transformations: - * 1. Applies camelCase conversion if enabled + * 1. Applies exportLocalsConvention transformation (or legacy camelCase) * 2. Sorts class names alphabetically if enabled * 3. Checks for JavaScript reserved keywords in class names * 4. Chooses appropriate export format based on options and keyword detection * 5. Formats the output with custom indentation and quotes * * Export format selection: - * - If namedExport=true and no keywords: generates named exports (export const foo: string;) - * - If namedExport=true but has keywords: falls back to interface + export = (for compatibility) - * - If namedExport=false: generates interface + default export + * - If namedExport=true: generates named exports for non-keyword classes only (keywords are skipped) + * - If namedExport=false: generates interface + default export with all classes * * @param params - Parameters object * @param params.classNames - Array of CSS class names extracted from the module - * @param params.options - Loader options (camelCase, quote, indentStyle, etc.) + * @param params.options - Loader options (exportLocalsConvention, quote, indentStyle, etc.) * @returns Generated TypeScript declaration file content with trailing newline * * @example @@ -250,35 +336,32 @@ export function handleDtsFile({ dtsFilePath, dtsContent, mode, logger: _logger, * // Named exports (no keywords) * generateDtsContent({ * classNames: ["button", "container"], - * options: { namedExport: true, quote: "double", ... } + * options: { namedExport: true, exportLocalsConvention: "as-is", ... } * }); * // Returns: * // export const button: string; * // export const container: string; * - * // Interface format (with keywords) + * // Named exports with keywords (keywords are skipped) * generateDtsContent({ * classNames: ["class", "button"], - * options: { namedExport: true, quote: "double", ... } + * options: { namedExport: true, exportLocalsConvention: "as-is", ... } * }); * // Returns: - * // interface CssExports { - * // "class": string; - * // "button": string; - * // } - * // export const cssExports: CssExports; - * // export = cssExports; + * // export const button: string; * ``` */ export function generateDtsContent({ classNames, options }: GenerateDtsContentParams): string { + // Apply exportLocalsConvention transformation + const transformedClassNames = applyExportLocalsConvention( + classNames, + options.exportLocalsConvention as ExportLocalsConvention + ); - const baseClassNames = options.camelCase - ? classNames.map(toCamelCase) - : classNames; - + // Sort if requested const processedClassNames = options.sort - ? [...baseClassNames].sort((a, b) => a.localeCompare(b)) - : baseClassNames; + ? [...transformedClassNames].sort((a, b) => a.localeCompare(b)) + : transformedClassNames; const quoteChar = options.quote === "single" ? "'" : "\""; const indent = options.indentStyle === "tab" @@ -291,16 +374,16 @@ export function generateDtsContent({ classNames, options }: GenerateDtsContentPa content.push(...options.banner.split("\n")); } - // Check if any class names are JS keywords - const hasKeywords = processedClassNames.some(cls => JS_KEYWORDS.has(cls)); - - // If namedExport is requested but we have keywords, fall back to interface format - // because we can't use keywords as named exports (e.g., export const class: string;) - const useNamedExport = options.namedExport && !hasKeywords; + // Separate keywords from non-keywords + const nonKeywords = processedClassNames.filter(cls => !JS_KEYWORDS.has(cls)); - if (useNamedExport) { - content.push(...processedClassNames.map(cls => `export const ${cls}: string;`)); + if (options.namedExport) { + // namedExport:true - only export non-keyword classes as named exports + // Keywords are skipped because they cannot be used as named exports in JavaScript + // Users can still access them via import * as styles and styles["keyword"] + content.push(...nonKeywords.map(cls => `export const ${cls}: string;`)); } else { + // namedExport:false - always use interface format content.push( "interface CssExports {", ...processedClassNames.map((cls) => `${indent}${quoteChar}${cls}${quoteChar}: string;`), diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index 13b2705..f17eade 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -30,15 +30,7 @@ export default cssExports; exports[`css-modules-dts-loader > JavaScript Keywords as Class Names > should handle JS keyword class names with namedExport=true 1`] = ` "// This file is automatically generated. // Please do not change this file! -interface CssExports { - "validClass": string; - "class": string; - "export": string; - "import": string; -} - -export const cssExports: CssExports; -export default cssExports; +export const validClass: string; " `; diff --git a/test/compiler.ts b/test/compiler.ts index c96fe8c..72c077c 100644 --- a/test/compiler.ts +++ b/test/compiler.ts @@ -64,7 +64,11 @@ function compileProject({ files, loaderOptions = {} }: CompileProjectOptions): P loader: "css-loader", options: { modules: { - namedExport: true + // Match css-loader's namedExport with the loader's namedExport option + namedExport: loaderOptions.namedExport !== false, + // Match exportLocalsConvention or use appropriate default + exportLocalsConvention: loaderOptions.exportLocalsConvention || + (loaderOptions.namedExport !== false ? "as-is" : "camel-case-only") } } } diff --git a/test/index.test.ts b/test/index.test.ts index c8e94f8..af352c3 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -243,11 +243,11 @@ describe("css-modules-dts-loader", () => { const dtsContent = readFile(tmpDir, "styles.module.css.d.ts"); expect(normalizeLineEndings(dtsContent)).toMatchSnapshot(); - // Should contain all class names - expect(dtsContent).toContain("class"); - expect(dtsContent).toContain("export"); - expect(dtsContent).toContain("import"); - expect(dtsContent).toContain("validClass"); + expect(dtsContent).toContain("export const validClass: string;"); + + expect(dtsContent).not.toContain("export const class: string;"); + expect(dtsContent).not.toContain("export const export: string;"); + expect(dtsContent).not.toContain("export const import: string;"); }); it("should handle JS keyword class names with namedExport=false", async () => { diff --git a/test/utils.test.ts b/test/utils.test.ts index cc3e582..a9cc305 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -178,7 +178,7 @@ describe("utils", () => { const result = generateDtsContent({ classNames: ["test"], options: { - camelCase: false, + exportLocalsConvention: "as-is", quote: "double", indentStyle: "space", indentSize: 2, @@ -194,11 +194,11 @@ describe("utils", () => { expect(result).toContain("export const test: string;"); }); - test("should fallback to interface when namedExport=true but has keywords", () => { + test("should skip keywords when namedExport=true", () => { const result = generateDtsContent({ classNames: ["class", "export", "validName"], options: { - camelCase: false, + exportLocalsConvention: "as-is", quote: "double", indentStyle: "space", indentSize: 2, @@ -209,19 +209,18 @@ describe("utils", () => { } }); - // Should use interface format because of keywords - expect(result).toContain("interface CssExports"); - expect(result).toContain('"class"'); - expect(result).toContain('"export"'); - expect(result).toContain('"validName"'); - expect(result).toContain("export default cssExports;"); + // Should only export non-keyword classes + expect(result).toContain("export const validName: string;"); + expect(result).not.toContain("export const class"); + expect(result).not.toContain("export const export"); + expect(result).not.toContain("interface"); }); test("should use default export when namedExport=false and no keywords", () => { const result = generateDtsContent({ classNames: ["foo", "bar"], options: { - camelCase: false, + exportLocalsConvention: "as-is", quote: "single", indentStyle: "tab", indentSize: 2, @@ -243,7 +242,7 @@ describe("utils", () => { const result = generateDtsContent({ classNames: ["zebra", "alpha", "beta"], options: { - camelCase: false, + exportLocalsConvention: "as-is", quote: "double", indentStyle: "space", indentSize: 2, @@ -257,16 +256,17 @@ describe("utils", () => { const lines = result.split("\n"); const exportLines = lines.filter(l => l.startsWith("export const")); + expect(exportLines.length).toBe(3); expect(exportLines[0]).toContain("alpha"); expect(exportLines[1]).toContain("beta"); expect(exportLines[2]).toContain("zebra"); }); - test("should apply camelCase transformation", () => { + test("should apply camelCase transformation with camel-case-only", () => { const result = generateDtsContent({ classNames: ["kebab-case-name", "another-class"], options: { - camelCase: true, + exportLocalsConvention: "camel-case-only", quote: "double", indentStyle: "space", indentSize: 2,