Skip to content
This repository was archived by the owner on Oct 30, 2020. It is now read-only.

Commit bf809cb

Browse files
authored
Merge pull request #4 from Jimdo/add-named-exports-option
Add named exports option
2 parents caf1267 + bc77b55 commit bf809cb

13 files changed

+167
-22
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
node_modules
22
lib
33
bundle.js
4-
example.css.d.ts
4+
example*.css.d.ts

README.md

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,55 @@ Webpack loader that works as a css-loader drop-in replacement to generate TypeSc
66

77
Install via npm `npm install --save-dev typings-for-css-modules-loader`
88

9+
## Options
10+
11+
Just like any other loader you can specify options e.g. as query-params
12+
13+
### css-loader options
14+
Any option that your installed version of css-loader supports can be used and will be passed to it.
15+
16+
### `namedExport`-option
17+
As your fellow css-developer may tend to use characters like dashes(`-`) that are not valid characters for a typescript variable the default behaviour for this loader is to export an interface as the default export that contains the classnames as properties.
18+
e.g.:
19+
```ts
20+
export interface IExampleCss {
21+
'foo': string;
22+
'bar-baz': string;
23+
}
24+
declare const styles: IExampleCss;
25+
26+
export default styles;
27+
```
28+
29+
A cleaner way is to expose all classes as named exports, this can be done if you enable the `namedExport`-option.
30+
e.g.
31+
```js
32+
{ test: /\.css$/, loader: 'typings-for-css-modules?modules&namedExport' }
33+
```
34+
35+
As mentioned above, this requires classnames to only contain valid typescript characters, thus filtering out all classnames that do not match /^\w+$/i. (feel free to improve that regexp)
36+
In order to make sure that even classnames with non-legal characters are used it is highly recommended to use the `camelCase`-option as well, that - once passed to the css-loader - makes sure all classnames are transformed to valid variables.
37+
with:
38+
```js
39+
{ test: /\.css$/, loader: 'typings-for-css-modules?modules&namedExport&camelCase' }
40+
```
41+
using the following css:
42+
```css
43+
.foo {
44+
color: white;
45+
}
46+
47+
.bar-baz {
48+
color: green;
49+
}
50+
```
51+
52+
will generate the following typings file:
53+
```ts
54+
export const foo: string;
55+
export const barBaz: string;
56+
```
57+
958
## Usage
1059

1160
Keep your `webpack.config` as is just instead of using `css-loader` use `typings-for-css-modules-loader`
@@ -29,8 +78,8 @@ webpackConfig.module.loaders: [
2978

3079
## Example
3180

32-
Imagine you have a file `~/my-project/src/component/MyComponent/component.scss` in your project with the following content:
33-
```
81+
Imagine you have a file `~/my-project/src/component/MyComponent/myComponent.scss` in your project with the following content:
82+
```css
3483
.some-class {
3584
// some styles
3685
&.someOtherClass {
@@ -42,8 +91,8 @@ Imagine you have a file `~/my-project/src/component/MyComponent/component.scss`
4291
}
4392
```
4493

45-
Adding the `typings-for-css-modules-loader` will generate a file `~/my-project/src/component/MyComponent/mycomponent.scss.d.ts` that has the following content:
46-
```
94+
Adding the `typings-for-css-modules-loader` will generate a file `~/my-project/src/component/MyComponent/myComponent.scss.d.ts` that has the following content:
95+
```ts
4796
export interface IMyComponentScss {
4897
'some-class': string;
4998
'someOtherClass': string;
@@ -54,6 +103,14 @@ declare const styles: IMyComponentScss;
54103
export default styles;
55104
```
56105

106+
### using `namedExport` with the `camelCase`-option
107+
Using the `namedExport` as well as the `camelCase` options the generated file will look as follow:
108+
```ts
109+
export const someClass: string;
110+
export const someOtherClass: string;
111+
export const someClassSayWhat: string;
112+
```
113+
57114
### Example in Visual Studio Code
58115
![typed-css-modules](https://cloud.githubusercontent.com/assets/749171/16340497/c1cb6888-3a28-11e6-919b-f2f51a282bba.gif)
59116

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
"scripts": {
77
"build": "babel src -d lib",
88
"prepublish": "npm run build",
9-
"pretest": "rm -f ./test/example.css.d.ts && touch ./test/example.css.d.ts",
10-
"test:run": "babel-node ./node_modules/webpack/bin/webpack --config ./test/webpack.config.babel.js && diff ./test/example.css.d.ts ./test/expected-example.css.d.ts",
9+
"pretest": "rm -f ./test/example*.css.d.ts",
10+
"test:diff": "(cd test; set -e; for f in example*css.d.ts; do diff $f expected-$f; done;)",
11+
"test:run": "babel-node ./node_modules/webpack/bin/webpack --config ./test/webpack.config.babel.js && npm run test:diff",
1112
"test": "npm run test:run > /dev/null 2>&1 && npm run test:run"
1213
},
1314
"author": "Tim Sebastian <[email protected]>",
@@ -25,6 +26,7 @@
2526
"css modules webpack typings"
2627
],
2728
"dependencies": {
29+
"colour": "0.7.1",
2830
"css-loader": ">=0.23.1",
2931
"graceful-fs": "4.1.4",
3032
"loader-utils": "0.2.16"

src/cssModuleToInterface.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,43 @@ const filenameToInterfaceName = (filename) => {
66
.replace(/\W+(\w)/g, (_, c) => c.toUpperCase());
77
};
88

9-
const cssModuleToTypescriptInterfaceProperties = (cssModuleObject, indent = ' ') => {
10-
return Object.keys(cssModuleObject)
9+
const cssModuleToTypescriptInterfaceProperties = (cssModuleKeys, indent = ' ') => {
10+
return cssModuleKeys
1111
.map((key) => `${indent}'${key}': string;`)
1212
.join('\n');
1313
};
1414

15+
const cssModuleToNamedExports = (cssModuleKeys) => {
16+
return cssModuleKeys
17+
.map((key) => `export const ${key}: string;`)
18+
.join('\n');
19+
};
20+
21+
const allWordsRegexp = /^\w+$/i;
22+
export const filterNonWordClasses = (cssModuleKeys) => {
23+
const filteredClassNames = cssModuleKeys.filter(classname => allWordsRegexp.test(classname));
24+
if (filteredClassNames.length === cssModuleKeys.length) {
25+
return [filteredClassNames, []];
26+
}
27+
const nonWordClassNames = cssModuleKeys.filter(classname => !allWordsRegexp.test(classname));
28+
return [filteredClassNames, nonWordClassNames];
29+
}
30+
1531
export const filenameToTypingsFilename = (filename) => {
1632
const dirName = path.dirname(filename);
1733
const baseName = path.basename(filename);
1834
return path.join(dirName, `${baseName}.d.ts`);
1935
};
2036

21-
export const generateInterface = (cssModuleObject, filename, indent) => {
37+
export const generateNamedExports = (cssModuleKeys) => {
38+
const namedExports = cssModuleToNamedExports(cssModuleKeys);
39+
return (`${namedExports}
40+
`);
41+
};
42+
43+
export const generateGenericExportInterface = (cssModuleKeys, filename, indent) => {
2244
const interfaceName = filenameToInterfaceName(filename);
23-
const interfaceProperties = cssModuleToTypescriptInterfaceProperties(cssModuleObject, indent);
45+
const interfaceProperties = cssModuleToTypescriptInterfaceProperties(cssModuleKeys, indent);
2446
return (
2547
`export interface ${interfaceName} {
2648
${interfaceProperties}

src/index.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import cssLoader from 'css-loader';
22
import cssLocalsLoader from 'css-loader/locals';
33
import loaderUtils from 'loader-utils';
4+
import 'colour';
5+
46
import {
5-
generateInterface,
7+
filterNonWordClasses,
8+
generateNamedExports,
9+
generateGenericExportInterface,
610
filenameToTypingsFilename,
711
} from './cssModuleToInterface';
812
import * as persist from './persist';
@@ -21,18 +25,31 @@ module.exports = function(input) {
2125
const query = loaderUtils.parseQuery(this.query);
2226
const moduleMode = query.modules || query.module;
2327
if (!moduleMode) {
24-
console.warn('Typings for CSS-Modules: option `modules` is not active - skipping extraction work...');
28+
console.warn('Typings for CSS-Modules: option `modules` is not active - skipping extraction work...').red;
2529
return delegateToCssLoader(ctx, input, callback);
2630
}
2731

2832
// mock async step 2 - offer css loader a "fake" callback
2933
this.async = () => (err, content) => {
30-
const cssmodules = this.exec(content, this.resource);
31-
const requestedResource = this.resourcePath;
34+
const filename = this.resourcePath;
35+
const cssModuleInterfaceFilename = filenameToTypingsFilename(filename);
36+
37+
let cssModuleKeys = Object.keys(this.exec(content, this.resource));
3238

33-
const cssModuleInterfaceFilename = filenameToTypingsFilename(requestedResource);
34-
const cssModuleInterface = generateInterface(cssmodules, requestedResource);
35-
persist.writeToFileIfChanged(cssModuleInterfaceFilename, cssModuleInterface);
39+
let cssModuleDefinition;
40+
if (!query.namedExport) {
41+
cssModuleDefinition = generateGenericExportInterface(cssModuleKeys, filename);
42+
} else {
43+
const [cleanedDefinitions, skippedDefinitions] = filterNonWordClasses(cssModuleKeys);
44+
if (skippedDefinitions.length > 0 && !query.camelCase) {
45+
console.warn(`Typings for CSS-Modules: option 'namedExport' was set but 'camelCase' for the css-loader not.
46+
The following classes will not be available as named exports:
47+
${skippedDefinitions.map(sd => ` - "${sd}"`).join('\n').red}
48+
`.yellow);
49+
}
50+
cssModuleDefinition = generateNamedExports(cleanedDefinitions);
51+
}
52+
persist.writeToFileIfChanged(cssModuleInterfaceFilename, cssModuleDefinition);
3653
// mock async step 3 - make `async` return the actual callback again before calling the 'real' css-loader
3754
delegateToCssLoader(this, input, callback);
3855
};

test/entry.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import styles from './example.css';
1+
import stylesBase from './example.css';
2+
import stylesCamelCase from './example-camelcase.css';
3+
import * as stylesNamedExport from './example-namedexport.css';
4+
import * as stylesCamelCasedNamedExport from './example-camelcase-namedexport.css';
25

3-
const foo = styles.foo;
4-
const barBaz = styles['bar-baz'];
6+
const foo = stylesBase.foo;
7+
const barBaz = stylesBase['bar-baz'];
8+
9+
const fooCamelCase = stylesCamelCase.foo;
10+
const barBazCamelCase = stylesCamelCase.barBaz;
11+
const barBazDashedCamelCase = stylesCamelCase['bar-baz'];
12+
13+
const fooNamedExport = stylesNamedExport.foo;
14+
15+
const fooCamelCaseNamedExport = stylesCamelCasedNamedExport.foo;
16+
const barBazCamelCaseNamedExport = stylesCamelCasedNamedExport.barBaz;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.foo {
2+
color: white;
3+
}
4+
5+
.bar-baz {
6+
color: green;
7+
}

test/example-camelcase.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.foo {
2+
color: white;
3+
}
4+
5+
.bar-baz {
6+
color: green;
7+
}

test/example-namedexport.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.foo {
2+
color: white;
3+
}
4+
5+
.bar-baz {
6+
color: green;
7+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const foo: string;
2+
export const barBaz: string;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface IExampleCamelcaseCss {
2+
'foo': string;
3+
'bar-baz': string;
4+
'barBaz': string;
5+
}
6+
declare const styles: IExampleCamelcaseCss;
7+
8+
export default styles;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const foo: string;

test/webpack.config.babel.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ module.exports = {
77
module: {
88
loaders: [
99
{ test: /\.ts$/, loaders: ['babel', 'ts'] },
10-
{ test: /\.css$/, loader: '../src/index.js?modules' }
10+
{ test: /example.css$/, loader: '../src/index.js?modules' },
11+
{ test: /example-camelcase.css$/, loader: '../src/index.js?modules&camelCase' },
12+
{ test: /example-namedexport.css$/, loader: '../src/index.js?modules&namedExport' },
13+
{ test: /example-camelcase-namedexport.css$/, loader: '../src/index.js?modules&camelCase&namedExport' }
1114
]
1215
}
1316
};

0 commit comments

Comments
 (0)