diff --git a/README.md b/README.md
index 4849407..d7e6b1a 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,7 @@ module.exports = {
| ------------------------ | --------------------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
+| `keywordPrefix` | `string` | `"__dts_"` | Prefix used for aliased exports of JavaScript reserved keywords (e.g., `class`, `export`). Must be a valid JavaScript identifier. Only applies when `namedExport` is `true`. |
| `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"`. |
@@ -122,6 +123,7 @@ When using `namedExport: true`, JavaScript reserved keywords (like `class`, `exp
- Keywords are exported using **aliased exports** to provide full type safety
- Non-keyword classes are exported normally as named exports
- Keywords are accessible via namespace import with full type safety
+- The prefix for aliased exports can be customized using the `keywordPrefix` option
**Example:**
@@ -131,7 +133,7 @@ When using `namedExport: true`, JavaScript reserved keywords (like `class`, `exp
.container { padding: 10px; }
```
-Generated with `namedExport: true`:
+Generated with `namedExport: true` and default `keywordPrefix`:
```ts
// styles.module.css.d.ts
export const container: string;
@@ -140,15 +142,29 @@ declare const __dts_class: string;
export { __dts_class as "class" };
```
+Generated with `namedExport: true` and `keywordPrefix: "dts"`:
+```ts
+// styles.module.css.d.ts
+export const container: string;
+
+declare const dtsclass: string;
+export { dtsclass as "class" };
+```
+
Usage in TypeScript:
```ts
import * as styles from './styles.module.css';
// Both are fully type-safe:
styles.container; // ✅ Type-safe
-styles["class"]; // ✅ Type-safe via aliased export
+styles.class; // ✅ Type-safe via aliased export
```
+**Why customize `keywordPrefix`?**
+- **Linter compatibility**: Some ESLint configurations flag identifiers starting with `__` (double underscore)
+- **Naming conventions**: Match your project's naming standards
+- **Readability**: Use a prefix that's clearer in your codebase (e.g., `dts`, `css_`, `module_`)
+
**Note:** With `namedExport: false`, all classes (including keywords) are included in the interface, so there's no difference in behavior for keywords.
### Example Output
@@ -185,7 +201,19 @@ import * as styles from './styles.module.css';
styles.button;
styles.container;
-styles["class"]; // Type-safe via aliased export
+styles.class; // Type-safe via aliased export
+
+// Or
+
+import {
+ button,
+ container,
+ class as notReservedJsKeyword
+} from './styles.module.css';
+
+
+ No conflicts with JS keywords!
+
```
#### With `namedExport: false`
diff --git a/src/constants.ts b/src/constants.ts
index 889bbc9..5d2eaea 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -16,6 +16,7 @@ export interface LoaderOptions {
sort?: boolean;
namedExport?: boolean;
banner?: string;
+ keywordPrefix?: string;
}
export interface ExportMarker {
@@ -61,7 +62,8 @@ export const SCHEMA: SchemaDefinition = {
mode: { type: "string", enum: ["emit", "verify"] },
sort: { type: "boolean" },
namedExport: { type: "boolean" },
- banner: { type: "string" }
+ banner: { type: "string" },
+ keywordPrefix: { type: "string" }
},
additionalProperties: false
};
@@ -79,7 +81,8 @@ export const DEFAULT_OPTIONS = {
mode: "emit" as const,
sort: false,
namedExport: true,
- banner: "// This file is automatically generated.\n// Please do not change this file!"
+ banner: "// This file is automatically generated.\n// Please do not change this file!",
+ keywordPrefix: "__dts_"
};
/**
diff --git a/src/index.ts b/src/index.ts
index 4644760..cad456e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -103,6 +103,20 @@ export default function cssModuleTypesLoader(this: LoaderContext, source: string
mergedOptions.exportLocalsConvention = mergedOptions.namedExport ? "as-is" : "camel-case-only";
}
+ // Validate keywordPrefix format
+ if (mergedOptions.keywordPrefix !== undefined) {
+ const keywordPrefix = mergedOptions.keywordPrefix;
+ // Must be a valid JavaScript identifier start
+ if (!/^[a-zA-Z_$][\w$]*$/.test(keywordPrefix)) {
+ this.emitError(new Error(
+ `Invalid keywordPrefix: "${keywordPrefix}". ` +
+ "The prefix must be a valid JavaScript identifier (start with letter, _, or $, " +
+ "followed by letters, digits, _, or $)."
+ ));
+ return source;
+ }
+ }
+
const options = mergedOptions as Required;
const classNames = extractClassNames(source);
diff --git a/src/utils.ts b/src/utils.ts
index 6a89ec3..7a0b416 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -34,6 +34,7 @@ export interface GenerateDtsContentParams {
classNames: string[];
options: Required> & {
exportLocalsConvention: ExportLocalsConvention;
+ keywordPrefix: string;
};
}
@@ -338,7 +339,7 @@ export function handleDtsFile({ dtsFilePath, dtsContent, mode, logger: _logger,
*
* @param params - Parameters object
* @param params.classNames - Array of CSS class names extracted from the module
- * @param params.options - Loader options (exportLocalsConvention, quote, indentStyle, etc.)
+ * @param params.options - Loader options (exportLocalsConvention, quote, indentStyle, keywordPrefix, etc.)
* @returns Generated TypeScript declaration file content with trailing newline
*
* @example
@@ -346,22 +347,22 @@ export function handleDtsFile({ dtsFilePath, dtsContent, mode, logger: _logger,
* // Named exports (no keywords)
* generateDtsContent({
* classNames: ["button", "container"],
- * options: { namedExport: true, exportLocalsConvention: "as-is", ... }
+ * options: { namedExport: true, exportLocalsConvention: "as-is", keywordPrefix: "__dts_", ... }
* });
* // Returns:
* // export const button: string;
* // export const container: string;
*
- * // Named exports with keywords (keywords use aliased exports)
+ * // Named exports with keywords (keywords use aliased exports with custom prefix)
* generateDtsContent({
* classNames: ["class", "button"],
- * options: { namedExport: true, exportLocalsConvention: "as-is", ... }
+ * options: { namedExport: true, exportLocalsConvention: "as-is", keywordPrefix: "dts", ... }
* });
* // Returns:
* // export const button: string;
* //
- * // declare const __dts_class: string;
- * // export { __dts_class as "class" };
+ * // declare const dtsclass: string;
+ * // export { dtsclass as "class" };
* ```
*/
export function generateDtsContent({ classNames, options }: GenerateDtsContentParams): string {
@@ -396,11 +397,12 @@ export function generateDtsContent({ classNames, options }: GenerateDtsContentPa
content.push(...nonKeywords.map(cls => `export const ${cls}: string;`));
// For keywords, use aliased exports to provide type safety
- // declare const __dts_class: string; export { __dts_class as "class" };
+ // declare const {prefix}class: string; export { {prefix}class as "class" };
if (keywords.length > 0) {
content.push("");
- content.push(...keywords.map(cls => `declare const __dts_${cls}: string;`));
- content.push(...keywords.map(cls => `export { __dts_${cls} as "${cls}" };`));
+ const prefix = options.keywordPrefix;
+ content.push(...keywords.map(cls => `declare const ${prefix}${cls}: string;`));
+ content.push(...keywords.map(cls => `export { ${prefix}${cls} as "${cls}" };`));
}
} else {
// namedExport:false - always use interface format
diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap
index eb98897..3eeda34 100644
--- a/test/__snapshots__/index.test.ts.snap
+++ b/test/__snapshots__/index.test.ts.snap
@@ -82,6 +82,66 @@ export const testClass: string;
"
`;
+exports[`css-modules-dts-loader > Options: keywordPrefix > should not affect interface export mode (namedExport=false) 1`] = `
+"// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ "class": string;
+ "export": string;
+}
+
+export const cssExports: CssExports;
+export default cssExports;
+"
+`;
+
+exports[`css-modules-dts-loader > Options: keywordPrefix > should use custom keywordPrefix (dts) 1`] = `
+"// This file is automatically generated.
+// Please do not change this file!
+export const validClass: string;
+
+declare const dtsclass: string;
+declare const dtsexport: string;
+export { dtsclass as "class" };
+export { dtsexport as "export" };
+"
+`;
+
+exports[`css-modules-dts-loader > Options: keywordPrefix > should use custom keywordPrefix with underscores 1`] = `
+"// This file is automatically generated.
+// Please do not change this file!
+export const button: string;
+
+declare const my_prefix_import: string;
+export { my_prefix_import as "import" };
+"
+`;
+
+exports[`css-modules-dts-loader > Options: keywordPrefix > should use default __dts_ prefix for keywords 1`] = `
+"// This file is automatically generated.
+// Please do not change this file!
+export const validClass: string;
+
+declare const __dts_class: string;
+declare const __dts_export: string;
+export { __dts_class as "class" };
+export { __dts_export as "export" };
+"
+`;
+
+exports[`css-modules-dts-loader > Options: keywordPrefix > should work with keywordPrefix and sort option 1`] = `
+"// This file is automatically generated.
+// Please do not change this file!
+export const alpha: string;
+export const zebra: string;
+
+declare const dtsclass: string;
+declare const dtsexport: string;
+export { dtsclass as "class" };
+export { dtsexport as "export" };
+"
+`;
+
exports[`css-modules-dts-loader > Options: namedExport > should generate interface export when namedExport is false 1`] = `
"// This file is automatically generated.
// Please do not change this file!
diff --git a/test/index.test.ts b/test/index.test.ts
index 5c19e1f..a19c2d5 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -318,6 +318,155 @@ describe("css-modules-dts-loader", () => {
});
});
+ describe("Options: keywordPrefix", () => {
+ it("should use default __dts_ prefix for keywords", async () => {
+ const files = {
+ "index.js": "import styles from './styles.module.css';",
+ "styles.module.css": `
+ .class { color: blue; }
+ .export { color: red; }
+ .validClass { color: black; }
+ `
+ };
+
+ const { tmpDir } = await compileProject({
+ files,
+ loaderOptions: { namedExport: true }
+ });
+
+ const dtsContent = readFile(tmpDir, "styles.module.css.d.ts");
+ expect(normalizeLineEndings(dtsContent)).toMatchSnapshot();
+
+ // Should use default __dts_ prefix
+ expect(dtsContent).toContain("declare const __dts_class: string;");
+ expect(dtsContent).toContain("declare const __dts_export: string;");
+ expect(dtsContent).toContain('export { __dts_class as "class" };');
+ expect(dtsContent).toContain('export { __dts_export as "export" };');
+ expect(dtsContent).toContain("export const validClass: string;");
+ });
+
+ it("should use custom keywordPrefix (dts)", async () => {
+ const files = {
+ "index.js": "import styles from './styles.module.css';",
+ "styles.module.css": `
+ .class { color: blue; }
+ .export { color: red; }
+ .validClass { color: black; }
+ `
+ };
+
+ const { tmpDir } = await compileProject({
+ files,
+ loaderOptions: {
+ namedExport: true,
+ keywordPrefix: "dts"
+ }
+ });
+
+ const dtsContent = readFile(tmpDir, "styles.module.css.d.ts");
+ expect(normalizeLineEndings(dtsContent)).toMatchSnapshot();
+
+ // Should use custom prefix
+ expect(dtsContent).toContain("declare const dtsclass: string;");
+ expect(dtsContent).toContain("declare const dtsexport: string;");
+ expect(dtsContent).toContain('export { dtsclass as "class" };');
+ expect(dtsContent).toContain('export { dtsexport as "export" };');
+
+ // Should NOT use default prefix
+ expect(dtsContent).not.toContain("__dts_");
+
+ // Non-keyword class should be normal export
+ expect(dtsContent).toContain("export const validClass: string;");
+ });
+
+ it("should use custom keywordPrefix with underscores", async () => {
+ const files = {
+ "index.js": "import styles from './styles.module.css';",
+ "styles.module.css": `
+ .import { color: blue; }
+ .button { color: red; }
+ `
+ };
+
+ const { tmpDir } = await compileProject({
+ files,
+ loaderOptions: {
+ namedExport: true,
+ keywordPrefix: "my_prefix_"
+ }
+ });
+
+ const dtsContent = readFile(tmpDir, "styles.module.css.d.ts");
+ expect(normalizeLineEndings(dtsContent)).toMatchSnapshot();
+
+ expect(dtsContent).toContain("declare const my_prefix_import: string;");
+ expect(dtsContent).toContain('export { my_prefix_import as "import" };');
+ expect(dtsContent).toContain("export const button: string;");
+ });
+
+ it("should work with keywordPrefix and sort option", async () => {
+ const files = {
+ "index.js": "import styles from './styles.module.css';",
+ "styles.module.css": `
+ .zebra { color: black; }
+ .class { color: blue; }
+ .alpha { color: green; }
+ .export { color: red; }
+ `
+ };
+
+ const { tmpDir } = await compileProject({
+ files,
+ loaderOptions: {
+ namedExport: true,
+ keywordPrefix: "dts",
+ sort: true
+ }
+ });
+
+ const dtsContent = readFile(tmpDir, "styles.module.css.d.ts");
+ expect(normalizeLineEndings(dtsContent)).toMatchSnapshot();
+
+ // Keywords with custom prefix
+ expect(dtsContent).toContain("declare const dtsclass: string;");
+ expect(dtsContent).toContain("declare const dtsexport: string;");
+
+ // Check sorted order of non-keywords
+ const lines = dtsContent.split("\n");
+ const exportLines = lines.filter(l => l.startsWith("export const"));
+ expect(exportLines[0]).toContain("alpha");
+ expect(exportLines[1]).toContain("zebra");
+ });
+
+ it("should not affect interface export mode (namedExport=false)", async () => {
+ const files = {
+ "index.js": "import styles from './styles.module.css';",
+ "styles.module.css": `
+ .class { color: blue; }
+ .export { color: red; }
+ `
+ };
+
+ const { tmpDir } = await compileProject({
+ files,
+ loaderOptions: {
+ namedExport: false,
+ keywordPrefix: "customPrefix"
+ }
+ });
+
+ const dtsContent = readFile(tmpDir, "styles.module.css.d.ts");
+ expect(normalizeLineEndings(dtsContent)).toMatchSnapshot();
+
+ // Interface mode should not use prefix
+ expect(dtsContent).toContain("interface CssExports");
+ expect(dtsContent).toContain('"class"');
+ expect(dtsContent).toContain('"export"');
+ expect(dtsContent).not.toContain("customPrefix");
+ expect(dtsContent).not.toContain("declare const");
+ });
+ });
+
describe("File Extensions", () => {
// Note: These tests verify that the loader supports different CSS preprocessor extensions
// In a real project, you would need sass-loader, less-loader, or stylus-loader installed
diff --git a/test/utils.test.ts b/test/utils.test.ts
index b36729b..b18991b 100644
--- a/test/utils.test.ts
+++ b/test/utils.test.ts
@@ -205,7 +205,8 @@ describe("utils", () => {
sort: false,
namedExport: true,
mode: "emit",
- banner: "// Test"
+ banner: "// Test",
+ keywordPrefix: "__dts_"
}
});
@@ -302,7 +303,8 @@ describe("utils", () => {
sort: false,
namedExport: true,
mode: "emit",
- banner: "// Test"
+ banner: "// Test",
+ keywordPrefix: "__dts_"
}
});
@@ -320,5 +322,138 @@ describe("utils", () => {
// This will cause a TypeScript error, but it's an extremely unlikely edge case
// where users are intentionally naming classes with the __dts_ prefix
});
+
+ test("should use custom keywordPrefix (dts) when specified", () => {
+ const result = generateDtsContent({
+ classNames: ["class", "export", "validName"],
+ options: {
+ exportLocalsConvention: "as-is",
+ quote: "double",
+ indentStyle: "space",
+ indentSize: 2,
+ sort: false,
+ namedExport: true,
+ mode: "emit",
+ banner: "// Test",
+ keywordPrefix: "dts"
+ }
+ });
+
+ // Should export non-keyword classes normally
+ expect(result).toContain("export const validName: string;");
+
+ // Should use custom prefix for keywords
+ expect(result).toContain("declare const dtsclass: string;");
+ expect(result).toContain("declare const dtsexport: string;");
+ expect(result).toContain('export { dtsclass as "class" };');
+ expect(result).toContain('export { dtsexport as "export" };');
+
+ // Should NOT use default __dts_ prefix
+ expect(result).not.toContain("__dts_class");
+ expect(result).not.toContain("__dts_export");
+ });
+
+ test("should use custom keywordPrefix with underscores", () => {
+ const result = generateDtsContent({
+ classNames: ["class", "import"],
+ options: {
+ exportLocalsConvention: "as-is",
+ quote: "double",
+ indentStyle: "space",
+ indentSize: 2,
+ sort: false,
+ namedExport: true,
+ mode: "emit",
+ banner: "// Test",
+ keywordPrefix: "my_prefix_"
+ }
+ });
+
+ expect(result).toContain("declare const my_prefix_class: string;");
+ expect(result).toContain("declare const my_prefix_import: string;");
+ expect(result).toContain('export { my_prefix_class as "class" };');
+ expect(result).toContain('export { my_prefix_import as "import" };');
+ });
+
+ test("should use custom keywordPrefix with camelCase style", () => {
+ const result = generateDtsContent({
+ classNames: ["class", "for"],
+ options: {
+ exportLocalsConvention: "as-is",
+ quote: "double",
+ indentStyle: "space",
+ indentSize: 2,
+ sort: false,
+ namedExport: true,
+ mode: "emit",
+ banner: "// Test",
+ keywordPrefix: "dtsKeyword"
+ }
+ });
+
+ expect(result).toContain("declare const dtsKeywordclass: string;");
+ expect(result).toContain("declare const dtsKeywordfor: string;");
+ expect(result).toContain('export { dtsKeywordclass as "class" };');
+ expect(result).toContain('export { dtsKeywordfor as "for" };');
+ });
+
+ test("should not affect interface export mode (namedExport=false)", () => {
+ const result = generateDtsContent({
+ classNames: ["class", "export", "normal"],
+ options: {
+ exportLocalsConvention: "as-is",
+ quote: "double",
+ indentStyle: "space",
+ indentSize: 2,
+ sort: false,
+ namedExport: false,
+ mode: "emit",
+ banner: "// Test",
+ keywordPrefix: "customPrefix"
+ }
+ });
+
+ // Interface mode should not use keyword prefix at all
+ expect(result).toContain("interface CssExports");
+ expect(result).toContain('"class"');
+ expect(result).toContain('"export"');
+ expect(result).toContain('"normal"');
+ expect(result).not.toContain("customPrefix");
+ expect(result).not.toContain("declare const");
+ });
+
+ test("should work with sorted keywords using custom prefix", () => {
+ const result = generateDtsContent({
+ classNames: ["zebra", "class", "alpha", "export"],
+ options: {
+ exportLocalsConvention: "as-is",
+ quote: "double",
+ indentStyle: "space",
+ indentSize: 2,
+ sort: true,
+ namedExport: true,
+ mode: "emit",
+ banner: "// Test",
+ keywordPrefix: "dts"
+ }
+ });
+
+ const lines = result.split("\n");
+ const exportLines = lines.filter(l => l.startsWith("export const"));
+ const declareLines = lines.filter(l => l.startsWith("declare const"));
+ const aliasedLines = lines.filter(l => l.startsWith("export {"));
+
+ // Normal classes should be sorted
+ expect(exportLines[0]).toContain("alpha");
+ expect(exportLines[1]).toContain("zebra");
+
+ // Keywords should use custom prefix
+ expect(declareLines).toHaveLength(2);
+ expect(declareLines[0]).toContain("dtsclass");
+ expect(declareLines[1]).toContain("dtsexport");
+
+ expect(aliasedLines[0]).toContain('dtsclass as "class"');
+ expect(aliasedLines[1]).toContain('dtsexport as "export"');
+ });
});
});