Skip to content

Commit ad6a458

Browse files
committed
Add camelCase options to match CSS Loader
1 parent 2c9ba27 commit ad6a458

File tree

16 files changed

+244
-27
lines changed

16 files changed

+244
-27
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/.vscode
2-
/lib
32
/node_modules
3+
/coverage
4+
/lib
45
npm-debug.log
56
yarn-error.log

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/.vscode
22
/node_modules
33
/src
4+
/lib/**/__tests__
45
.editorconfig
56
npm-debug.log
67
tsconfig.json

README.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A [TypeScript language service plugin](https://github.com/Microsoft/TypeScript/w
44
for [CSS Modules](https://github.com/css-modules/css-modules).
55

66
This project was inspired by this [`create-react-app` issue](https://github.com/facebook/create-react-app/issues/5677)
7-
and is heavily based on [`css-module-types`](https://github.com/timothykang/css-module-types).
7+
and is based on [`css-module-types`](https://github.com/timothykang/css-module-types).
88

99
## Usage
1010

@@ -30,7 +30,14 @@ Once installed, add this plugin to your `tsconfig.json`:
3030
}
3131
```
3232

33-
You can also pass in your own file extension matcher (the default matcher is shown as an example):
33+
### Options
34+
35+
| Option | Default value | Description |
36+
| --------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
37+
| `customMatcher` | `"\\.module\\.(sa\|sc\|c)ss$"` | Change the file extensions that this plugin works with. |
38+
| `camelCase` | `false` | Implements the behaviour of the [`camelCase` CSS Loader option](https://github.com/webpack-contrib/css-loader#camelcase) (accepting the same values). |
39+
40+
The below is an example that only matches "\*.m.css" files, and [camel-cases dashes](https://github.com/webpack-contrib/css-loader#camelcase).
3441

3542
```json
3643
{
@@ -39,20 +46,40 @@ You can also pass in your own file extension matcher (the default matcher is sho
3946
{
4047
"name": "typescript-plugin-css-modules",
4148
"options": {
42-
"customMatcher": "\\.module\\.(sa|sc|c)ss$"
49+
"customMatcher": "\\.m\\.css$",
50+
"camelCase": "dashes"
4351
}
4452
}
4553
]
4654
}
4755
}
4856
```
4957

58+
### Visual Studio Code
59+
60+
By default, VSCode will use it's own version of TypeScript. To make it work with this plugin, you have two options:
61+
62+
1. Add this plugin to `"typescript.tsserver.pluginPaths"` in settings. Note that this method doesn't currently support
63+
plugin options.
64+
65+
```json
66+
{
67+
"typescript.tsserver.pluginPaths": ["typescript-plugin-css-modules"]
68+
}
69+
```
70+
71+
2. Use your workspace's version of TypeScript, which will load the plugins from your `tsconfig.json` file.
72+
([instructions](https://code.visualstudio.com/docs/languages/typescript#_using-the-workspace-version-of-typescript)).
73+
5074
### Custom definitions
5175

52-
Depending on your project configuration, you may also need to declare modules. Where you store this is up to you. An
53-
example might look like: `src/@types/custom.d.ts`.
76+
_Note: Create React App users can skip this section if you're using `[email protected]` or higher._
77+
78+
If your project doesn't already have global declarations for CSS Modules, you will need to add these to help TypeScript understand the general shape of the imported CSS.
79+
80+
Where you store global declarations is up to you. An example might look like: `src/custom.d.ts`.
5481

55-
The below is an example that you can modify if you use a `customMatcher`.
82+
The below is an example that you can copy, or modify if you use a `customMatcher`.
5683

5784
```ts
5885
declare module '*.module.css' {

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"scripts": {
1818
"build": "rm -rf ./lib && tsc",
1919
"prepublish": "yarn build",
20-
"test": "jest"
20+
"test": "jest ./src"
2121
},
2222
"husky": {
2323
"hooks": {
@@ -26,6 +26,9 @@
2626
}
2727
},
2828
"jest": {
29+
"collectCoverageFrom": [
30+
"src/**/*.{ts}"
31+
],
2932
"preset": "ts-jest",
3033
"testEnvironment": "node"
3134
},
@@ -35,7 +38,9 @@
3538
"trailingComma": "all"
3639
},
3740
"dependencies": {
41+
"@types/lodash": "^4.14.118",
3842
"icss-utils": "^4.0.0",
43+
"lodash": "^4.17.11",
3944
"postcss": "^7.0.5",
4045
"postcss-icss-selectors": "^2.0.3"
4146
},

src/@types/global.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
declare interface IOptions {
2+
camelCase?: CamelCaseOptions;
3+
customMatcher?: string;
4+
}
5+
6+
declare type CamelCaseOptions =
7+
| true
8+
| 'dashes'
9+
| 'dashesOnly'
10+
| 'only'
11+
| undefined;
File renamed without changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`utils / classTransforms should not transform classes when no option is set 1`] = `
4+
Array [
5+
Array [
6+
"class-name-a",
7+
],
8+
Array [
9+
"classNameB",
10+
],
11+
Array [
12+
"class-Name-C",
13+
],
14+
Array [
15+
"__class_nAmeD--",
16+
],
17+
]
18+
`;
19+
20+
exports[`utils / classTransforms should transform classes correctly when \`camelCase\` set to \`dashes\` 1`] = `
21+
Array [
22+
Array [
23+
"class-name-a",
24+
"classNameA",
25+
],
26+
Array [
27+
"classNameB",
28+
],
29+
Array [
30+
"class-Name-C",
31+
"classNameC",
32+
],
33+
Array [
34+
"__class_nAmeD--",
35+
],
36+
]
37+
`;
38+
39+
exports[`utils / classTransforms should transform classes correctly when \`camelCase\` set to \`dashesOnly\` 1`] = `
40+
Array [
41+
Array [
42+
"classNameA",
43+
],
44+
Array [
45+
"classNameB",
46+
],
47+
Array [
48+
"classNameC",
49+
],
50+
Array [
51+
"__class_nAmeD--",
52+
],
53+
]
54+
`;
55+
56+
exports[`utils / classTransforms should transform classes correctly when \`camelCase\` set to \`only\` 1`] = `
57+
Array [
58+
Array [
59+
"classNameA",
60+
],
61+
Array [
62+
"classNameB",
63+
],
64+
Array [
65+
"classNameC",
66+
],
67+
Array [
68+
"classNAmeD",
69+
],
70+
]
71+
`;
72+
73+
exports[`utils / classTransforms should transform classes correctly when \`camelCase\` set to \`true\` 1`] = `
74+
Array [
75+
Array [
76+
"class-name-a",
77+
"classNameA",
78+
],
79+
Array [
80+
"classNameB",
81+
],
82+
Array [
83+
"class-Name-C",
84+
"classNameC",
85+
],
86+
Array [
87+
"__class_nAmeD--",
88+
"classNAmeD",
89+
],
90+
]
91+
`;

src/helpers/__tests__/__snapshots__/cssSnapshots.test.ts.snap

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`utils / cssSnapshots createExports should create an exports file 1`] = `
4-
"
5-
declare const classes: {
4+
"declare const classes: {
65
'classA': string;
76
'ClassB': string;
87
'class-c': string;
@@ -16,8 +15,7 @@ export default classes;
1615
`;
1716

1817
exports[`utils / cssSnapshots createExports should create an exports file 2`] = `
19-
"
20-
declare const classes: {
18+
"declare const classes: {
2119
'local-class-inside-global': string;
2220
'local-class': string;
2321
'local-class-2': string;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { transformClasses } from '../classTransforms';
2+
3+
describe('utils / classTransforms', () => {
4+
const classNames = [
5+
'class-name-a',
6+
'classNameB',
7+
'class-Name-C',
8+
'__class_nAmeD--',
9+
];
10+
const tests: CamelCaseOptions[] = [true, 'dashes', 'dashesOnly', 'only'];
11+
12+
it(`should not transform classes when no option is set`, () => {
13+
const transformer = transformClasses();
14+
const transformedClasses = classNames.map(transformer);
15+
expect(transformedClasses).toMatchSnapshot();
16+
});
17+
18+
tests.forEach((option) => {
19+
it(`should transform classes correctly when \`camelCase\` set to \`${option}\``, () => {
20+
const transformer = transformClasses(option);
21+
const transformedClasses = classNames.map(transformer);
22+
expect(transformedClasses).toMatchSnapshot();
23+
});
24+
});
25+
});

src/helpers/__tests__/cssSnapshots.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ describe('utils / cssSnapshots', () => {
4242

4343
describe('createExports', () => {
4444
it('should create an exports file', () => {
45-
const exportsA = createExports(classesA);
46-
const exportsB = createExports(classesB);
45+
const exportsA = createExports(classesA, {});
46+
const exportsB = createExports(classesB, {});
4747
expect(exportsA).toMatchSnapshot();
4848
expect(exportsB).toMatchSnapshot();
4949
});

src/helpers/classTransforms.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { camelCase } from 'lodash';
2+
3+
// The below is based on the CSS Modules implementation found here:
4+
// https://github.com/webpack-contrib/css-loader/blob/master/lib/compile-exports.js
5+
6+
const dashesCamelCase = (className: string) =>
7+
className.replace(/-+(\w)/g, (match, firstLetter) =>
8+
firstLetter.toUpperCase(),
9+
);
10+
11+
export const transformClasses = (camelCaseOption?: boolean | string) => (
12+
className: string,
13+
) => {
14+
const entries: string[] = [];
15+
16+
switch (camelCaseOption) {
17+
case true: {
18+
entries.push(className);
19+
const targetClassName = camelCase(className);
20+
if (targetClassName !== className) {
21+
entries.push(targetClassName);
22+
}
23+
break;
24+
}
25+
case 'dashes': {
26+
entries.push(className);
27+
const targetClassName = dashesCamelCase(className);
28+
if (targetClassName !== className) {
29+
entries.push(targetClassName);
30+
}
31+
break;
32+
}
33+
case 'only':
34+
entries.push(camelCase(className));
35+
break;
36+
case 'dashesOnly':
37+
entries.push(dashesCamelCase(className));
38+
break;
39+
default:
40+
entries.push(className);
41+
break;
42+
}
43+
44+
return entries;
45+
};

src/helpers/cssSnapshots.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@ import { extractICSS, IICSSExports } from 'icss-utils';
22
import * as postcss from 'postcss';
33
import * as postcssIcssSelectors from 'postcss-icss-selectors';
44
import * as ts_module from 'typescript/lib/tsserverlibrary';
5+
import { transformClasses } from './classTransforms';
56

67
const processor = postcss(postcssIcssSelectors({ mode: 'local' }));
78

89
export const getClasses = (css: string) =>
910
extractICSS(processor.process(css).root).icssExports;
11+
const classNameToProperty = (className: string) => `'${className}': string;`;
12+
const flattenClassNames = (
13+
previousValue: string[] = [],
14+
currentValue: string[],
15+
) => previousValue.concat(currentValue);
1016

11-
const exportNameToProperty = (exportName: string) => `'${exportName}': string;`;
12-
export const createExports = (classes: IICSSExports) => `
17+
export const createExports = (classes: IICSSExports, options: IOptions) => `\
1318
declare const classes: {
1419
${Object.keys(classes)
15-
.map(exportNameToProperty)
20+
.map(transformClasses(options.camelCase))
21+
.reduce(flattenClassNames)
22+
.map(classNameToProperty)
1623
.join('\n ')}
1724
};
1825
export default classes;
@@ -21,9 +28,10 @@ export default classes;
2128
export const getDtsSnapshot = (
2229
ts: typeof ts_module,
2330
scriptSnapshot: ts.IScriptSnapshot,
31+
options: IOptions,
2432
) => {
2533
const css = scriptSnapshot.getText(0, scriptSnapshot.getLength());
2634
const classes = getClasses(css);
27-
const dts = createExports(classes);
35+
const dts = createExports(classes, options);
2836
return ts.ScriptSnapshot.fromString(dts);
2937
};

0 commit comments

Comments
 (0)