Skip to content

Commit 7cfea5d

Browse files
authored
Merge pull request #9 from Ch-Valentine/add-ability-to-provide-keywordPrefix
feat: added ability to provide keywordPrefix
2 parents 148ae19 + 291b84e commit 7cfea5d

File tree

7 files changed

+407
-16
lines changed

7 files changed

+407
-16
lines changed

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ module.exports = {
9494
| ------------------------ | --------------------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9595
| `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. |
9696
| `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. |
97+
| `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`. |
9798
| `quote` | `"single"` \| `"double"` | `"double"` | Quote style used for interface properties when `namedExport` is `false`. |
9899
| `indentStyle` | `"tab"` \| `"space"` | `"space"` | Indentation style for interface properties. |
99100
| `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
122123
- Keywords are exported using **aliased exports** to provide full type safety
123124
- Non-keyword classes are exported normally as named exports
124125
- Keywords are accessible via namespace import with full type safety
126+
- The prefix for aliased exports can be customized using the `keywordPrefix` option
125127

126128
**Example:**
127129

@@ -131,7 +133,7 @@ When using `namedExport: true`, JavaScript reserved keywords (like `class`, `exp
131133
.container { padding: 10px; }
132134
```
133135

134-
Generated with `namedExport: true`:
136+
Generated with `namedExport: true` and default `keywordPrefix`:
135137
```ts
136138
// styles.module.css.d.ts
137139
export const container: string;
@@ -140,15 +142,29 @@ declare const __dts_class: string;
140142
export { __dts_class as "class" };
141143
```
142144

145+
Generated with `namedExport: true` and `keywordPrefix: "dts"`:
146+
```ts
147+
// styles.module.css.d.ts
148+
export const container: string;
149+
150+
declare const dtsclass: string;
151+
export { dtsclass as "class" };
152+
```
153+
143154
Usage in TypeScript:
144155
```ts
145156
import * as styles from './styles.module.css';
146157

147158
// Both are fully type-safe:
148159
styles.container; // ✅ Type-safe
149-
styles["class"]; // ✅ Type-safe via aliased export
160+
styles.class; // ✅ Type-safe via aliased export
150161
```
151162

163+
**Why customize `keywordPrefix`?**
164+
- **Linter compatibility**: Some ESLint configurations flag identifiers starting with `__` (double underscore)
165+
- **Naming conventions**: Match your project's naming standards
166+
- **Readability**: Use a prefix that's clearer in your codebase (e.g., `dts`, `css_`, `module_`)
167+
152168
**Note:** With `namedExport: false`, all classes (including keywords) are included in the interface, so there's no difference in behavior for keywords.
153169

154170
### Example Output
@@ -185,7 +201,19 @@ import * as styles from './styles.module.css';
185201

186202
styles.button;
187203
styles.container;
188-
styles["class"]; // Type-safe via aliased export
204+
styles.class; // Type-safe via aliased export
205+
206+
// Or
207+
208+
import {
209+
button,
210+
container,
211+
class as notReservedJsKeyword
212+
} from './styles.module.css';
213+
214+
<div className={notReservedJsKeyword}>
215+
No conflicts with JS keywords!
216+
</div>
189217
```
190218

191219
#### With `namedExport: false`

src/constants.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface LoaderOptions {
1616
sort?: boolean;
1717
namedExport?: boolean;
1818
banner?: string;
19+
keywordPrefix?: string;
1920
}
2021

2122
export interface ExportMarker {
@@ -61,7 +62,8 @@ export const SCHEMA: SchemaDefinition = {
6162
mode: { type: "string", enum: ["emit", "verify"] },
6263
sort: { type: "boolean" },
6364
namedExport: { type: "boolean" },
64-
banner: { type: "string" }
65+
banner: { type: "string" },
66+
keywordPrefix: { type: "string" }
6567
},
6668
additionalProperties: false
6769
};
@@ -79,7 +81,8 @@ export const DEFAULT_OPTIONS = {
7981
mode: "emit" as const,
8082
sort: false,
8183
namedExport: true,
82-
banner: "// This file is automatically generated.\n// Please do not change this file!"
84+
banner: "// This file is automatically generated.\n// Please do not change this file!",
85+
keywordPrefix: "__dts_"
8386
};
8487

8588
/**

src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ export default function cssModuleTypesLoader(this: LoaderContext, source: string
103103
mergedOptions.exportLocalsConvention = mergedOptions.namedExport ? "as-is" : "camel-case-only";
104104
}
105105

106+
// Validate keywordPrefix format
107+
if (mergedOptions.keywordPrefix !== undefined) {
108+
const keywordPrefix = mergedOptions.keywordPrefix;
109+
// Must be a valid JavaScript identifier start
110+
if (!/^[a-zA-Z_$][\w$]*$/.test(keywordPrefix)) {
111+
this.emitError(new Error(
112+
`Invalid keywordPrefix: "${keywordPrefix}". ` +
113+
"The prefix must be a valid JavaScript identifier (start with letter, _, or $, " +
114+
"followed by letters, digits, _, or $)."
115+
));
116+
return source;
117+
}
118+
}
119+
106120
const options = mergedOptions as Required<LoaderOptions>;
107121

108122
const classNames = extractClassNames(source);

src/utils.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface GenerateDtsContentParams {
3434
classNames: string[];
3535
options: Required<Omit<LoaderOptions, "camelCase" | "exportLocalsConvention">> & {
3636
exportLocalsConvention: ExportLocalsConvention;
37+
keywordPrefix: string;
3738
};
3839
}
3940

@@ -338,30 +339,30 @@ export function handleDtsFile({ dtsFilePath, dtsContent, mode, logger: _logger,
338339
*
339340
* @param params - Parameters object
340341
* @param params.classNames - Array of CSS class names extracted from the module
341-
* @param params.options - Loader options (exportLocalsConvention, quote, indentStyle, etc.)
342+
* @param params.options - Loader options (exportLocalsConvention, quote, indentStyle, keywordPrefix, etc.)
342343
* @returns Generated TypeScript declaration file content with trailing newline
343344
*
344345
* @example
345346
* ```ts
346347
* // Named exports (no keywords)
347348
* generateDtsContent({
348349
* classNames: ["button", "container"],
349-
* options: { namedExport: true, exportLocalsConvention: "as-is", ... }
350+
* options: { namedExport: true, exportLocalsConvention: "as-is", keywordPrefix: "__dts_", ... }
350351
* });
351352
* // Returns:
352353
* // export const button: string;
353354
* // export const container: string;
354355
*
355-
* // Named exports with keywords (keywords use aliased exports)
356+
* // Named exports with keywords (keywords use aliased exports with custom prefix)
356357
* generateDtsContent({
357358
* classNames: ["class", "button"],
358-
* options: { namedExport: true, exportLocalsConvention: "as-is", ... }
359+
* options: { namedExport: true, exportLocalsConvention: "as-is", keywordPrefix: "dts", ... }
359360
* });
360361
* // Returns:
361362
* // export const button: string;
362363
* //
363-
* // declare const __dts_class: string;
364-
* // export { __dts_class as "class" };
364+
* // declare const dtsclass: string;
365+
* // export { dtsclass as "class" };
365366
* ```
366367
*/
367368
export function generateDtsContent({ classNames, options }: GenerateDtsContentParams): string {
@@ -396,11 +397,12 @@ export function generateDtsContent({ classNames, options }: GenerateDtsContentPa
396397
content.push(...nonKeywords.map(cls => `export const ${cls}: string;`));
397398

398399
// For keywords, use aliased exports to provide type safety
399-
// declare const __dts_class: string; export { __dts_class as "class" };
400+
// declare const {prefix}class: string; export { {prefix}class as "class" };
400401
if (keywords.length > 0) {
401402
content.push("");
402-
content.push(...keywords.map(cls => `declare const __dts_${cls}: string;`));
403-
content.push(...keywords.map(cls => `export { __dts_${cls} as "${cls}" };`));
403+
const prefix = options.keywordPrefix;
404+
content.push(...keywords.map(cls => `declare const ${prefix}${cls}: string;`));
405+
content.push(...keywords.map(cls => `export { ${prefix}${cls} as "${cls}" };`));
404406
}
405407
} else {
406408
// namedExport:false - always use interface format

test/__snapshots__/index.test.ts.snap

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,66 @@ export const testClass: string;
8282
"
8383
`;
8484

85+
exports[`css-modules-dts-loader > Options: keywordPrefix > should not affect interface export mode (namedExport=false) 1`] = `
86+
"// This file is automatically generated.
87+
// Please do not change this file!
88+
interface CssExports {
89+
"class": string;
90+
"export": string;
91+
}
92+
93+
export const cssExports: CssExports;
94+
export default cssExports;
95+
"
96+
`;
97+
98+
exports[`css-modules-dts-loader > Options: keywordPrefix > should use custom keywordPrefix (dts) 1`] = `
99+
"// This file is automatically generated.
100+
// Please do not change this file!
101+
export const validClass: string;
102+
103+
declare const dtsclass: string;
104+
declare const dtsexport: string;
105+
export { dtsclass as "class" };
106+
export { dtsexport as "export" };
107+
"
108+
`;
109+
110+
exports[`css-modules-dts-loader > Options: keywordPrefix > should use custom keywordPrefix with underscores 1`] = `
111+
"// This file is automatically generated.
112+
// Please do not change this file!
113+
export const button: string;
114+
115+
declare const my_prefix_import: string;
116+
export { my_prefix_import as "import" };
117+
"
118+
`;
119+
120+
exports[`css-modules-dts-loader > Options: keywordPrefix > should use default __dts_ prefix for keywords 1`] = `
121+
"// This file is automatically generated.
122+
// Please do not change this file!
123+
export const validClass: string;
124+
125+
declare const __dts_class: string;
126+
declare const __dts_export: string;
127+
export { __dts_class as "class" };
128+
export { __dts_export as "export" };
129+
"
130+
`;
131+
132+
exports[`css-modules-dts-loader > Options: keywordPrefix > should work with keywordPrefix and sort option 1`] = `
133+
"// This file is automatically generated.
134+
// Please do not change this file!
135+
export const alpha: string;
136+
export const zebra: string;
137+
138+
declare const dtsclass: string;
139+
declare const dtsexport: string;
140+
export { dtsclass as "class" };
141+
export { dtsexport as "export" };
142+
"
143+
`;
144+
85145
exports[`css-modules-dts-loader > Options: namedExport > should generate interface export when namedExport is false 1`] = `
86146
"// This file is automatically generated.
87147
// Please do not change this file!

0 commit comments

Comments
 (0)