Skip to content

Commit c1e68ac

Browse files
committed
initial commit
0 parents  commit c1e68ac

17 files changed

+373
-0
lines changed

.babelrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["es2015", "stage-3"]
3+
}

.editorconfig

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# http://editorconfig.org
2+
3+
# A special property that should be specified at the top of the file outside of
4+
# any sections. Set to true to stop .editor config file search on current file
5+
root = true
6+
7+
[*]
8+
# Indentation style
9+
# Possible values - tab, space
10+
indent_style = space
11+
12+
# Indentation size in single-spaced characters
13+
# Possible values - an integer, tab
14+
indent_size = 2
15+
16+
# Line ending file format
17+
# Possible values - lf, crlf, cr
18+
end_of_line = lf
19+
20+
# File character encoding
21+
# Possible values - latin1, utf-8, utf-16be, utf-16le
22+
charset = utf-8
23+
24+
# Denotes whether to trim whitespace at the end of lines
25+
# Possible values - true, false
26+
trim_trailing_whitespace = true
27+
28+
# Denotes whether file should end with a newline
29+
# Possible values - true, false
30+
insert_final_newline = true

.eslintrc

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"env": {
3+
"es6": true,
4+
"node": true
5+
},
6+
"extends": "eslint:recommended",
7+
"parser": "babel-eslint",
8+
"plugins": [
9+
"babel"
10+
],
11+
"parserOptions": {
12+
"sourceType": "module"
13+
},
14+
"rules": {
15+
"indent": [
16+
"error",
17+
2
18+
],
19+
"linebreak-style": [
20+
"error",
21+
"unix"
22+
],
23+
"quotes": [
24+
"error",
25+
"single"
26+
],
27+
"semi": [
28+
"error",
29+
"always"
30+
],
31+
"no-console": [
32+
0
33+
],
34+
"no-var": [
35+
"error"
36+
],
37+
"comma-dangle": [
38+
"error",
39+
"always"
40+
],
41+
"no-unused-vars": [
42+
2, {"vars": "all", "args": "after-used"}
43+
]
44+
}
45+
}

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
lib
3+
bundle.js
4+
example.css.d.ts

.npmignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.babelrc
2+
.eslintrc
3+
.editorconfig
4+
jsconfig.json
5+
src
6+
test

README.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# typings-for-css-modules-loaderg
2+
3+
Webpack loader that works as a css-loader drop-in replacement to generate TypeScript typings for CSS modules on the fly
4+
5+
## Installation
6+
7+
Install via npm `npm install --save-dev typings-for-css-modules-loader`
8+
9+
## Usage
10+
11+
Keep your `webpack.config` as is just instead of using `css-loader` use `typings-for-css-modules-loader`
12+
*its important you keep all the params that you used for the css-loader before, as they will be passed along in the process*
13+
14+
before:
15+
```js
16+
webpackConfig.module.loaders: [
17+
{ test: /\.css$/, loader: 'css?modules' }
18+
{ test: /\.scss$/, loader: 'css?modules&sass' }
19+
];
20+
```
21+
22+
after:
23+
```js
24+
webpackConfig.module.loaders: [
25+
{ test: /\.css$/, loader: 'typings-for-css-modules?modules' }
26+
{ test: /\.scss$/, loader: 'typings-for-css-modules?modules&sass' }
27+
];
28+
```
29+
30+
## Example
31+
32+
Imagine you have a file `~/my-project/src/component/MyComponent/component.scss` in your project with the following content:
33+
```
34+
.some-class {
35+
// some styles
36+
&.someOtherClass {
37+
// some other styles
38+
}
39+
&-sayWhat {
40+
// more styles
41+
}
42+
}
43+
```
44+
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+
```
47+
export interface IMyComponentScss {
48+
'some-class': string;
49+
'someOtherClass': string;
50+
'some-class-sayWhat': string;
51+
}
52+
declare const styles: IMyComponentScss;
53+
54+
export default styles;
55+
```
56+
57+
### Example in Visual Studio Code
58+
![typed-css-modules](https://cloud.githubusercontent.com/assets/749171/16340497/c1cb6888-3a28-11e6-919b-f2f51a282bba.gif)
59+
60+
## Support
61+
62+
As the loader just acts as an intermediary it can handle all kind of css preprocessors (`sass`, `scss`, `stylus`, `less`, ...).
63+
The only requirement is that those preprocessors have proper webpack loaders defined - meaning they can already be loaded by webpack anyways.
64+
65+
## Requirements
66+
67+
The loader uses `css-loader`(https://github.com/webpack/css-loader) under the hood. Thus it is a peer-dependency and the expected loader to create CSS Modules.
68+
69+
## Known issues
70+
71+
- There may be a lag or a reload necessary when adding a new style-file to your project as the typescript loader may take a while to "find" the new typings file.

jsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES6"
4+
},
5+
"exclude": [
6+
"node_modules"
7+
]
8+
}

package.json

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "typings-for-css-modules-loader",
3+
"version": "0.0.1",
4+
"description": "Drop-in replacement for css-loader to generate typings for your CSS-Modules on the fly in webpack",
5+
"main": "lib/index.js",
6+
"scripts": {
7+
"build": "babel src -d lib",
8+
"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",
11+
"test": "npm run test:run > /dev/null 2>&1 && npm run test:run"
12+
},
13+
"author": "Tim Sebastian <[email protected]>",
14+
"license": "MIT",
15+
"keywords": [
16+
"Typescript",
17+
"TypeScript",
18+
"CSS Modules",
19+
"CSSModules",
20+
"CSS Modules typings",
21+
"Webpack",
22+
"Webpack loader",
23+
"Webpack css module typings loader",
24+
"typescript webpack typings",
25+
"css modules webpack typings"
26+
],
27+
"dependencies": {
28+
"graceful-fs": "4.1.4"
29+
},
30+
"devDependencies": {
31+
"babel-cli": "6.10.1",
32+
"babel-eslint": "6.1.0",
33+
"babel-loader": "^6.2.5",
34+
"babel-polyfill": "^6.13.0",
35+
"babel-preset-es2015": "6.9.0",
36+
"babel-preset-stage-0": "6.5.0",
37+
"eslint": "2.13.1",
38+
"eslint-plugin-babel": "3.3.0",
39+
"ts-loader": "^0.8.2",
40+
"typescript": "^1.8.10",
41+
"webpack": "^1.13.2"
42+
},
43+
"peerDependencies": {
44+
"css-loader": "^0.23.1"
45+
},
46+
"repository": {
47+
"type": "git",
48+
"url": "git+https://github.com/Jimdo/typings-for-css-modules-loader.git"
49+
},
50+
"bugs": {
51+
"url": "https://github.com/Jimdo/typings-for-css-modules-loader/issues"
52+
},
53+
"homepage": "https://github.com/Jimdo/typings-for-css-modules-loader#readme"
54+
}

src/cssModuleHelper.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import path from 'path';
2+
import vm from 'vm';
3+
4+
const isCssModule = (module) => {
5+
if (!module || typeof module.request !== 'string') {
6+
return false;
7+
}
8+
9+
const extname = path.extname(module.request);
10+
return /\/css-loader\//.test(module.request) && extname !== '.js';
11+
};
12+
13+
export const filterCssModules = (modules) => {
14+
return modules.filter(isCssModule);
15+
};
16+
17+
export const removeLoadersBeforeCssLoader = (loaders) => {
18+
let sawCssLoader = false;
19+
// remove all loaders before the css-loader
20+
return loaders.filter((loader)=> {
21+
if (loader.indexOf('/css-loader/') > -1) {
22+
sawCssLoader = true;
23+
}
24+
25+
return sawCssLoader;
26+
});
27+
};
28+
29+
export const extractCssModuleFromSource = (source) => {
30+
const sandbox = {
31+
exports: null,
32+
module: {},
33+
require: () => () => [],
34+
};
35+
const script = new vm.Script(source);
36+
const context = new vm.createContext(sandbox);
37+
script.runInContext(context);
38+
return sandbox.exports.locals;
39+
};

src/cssModuleToInterface.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import path from 'path';
2+
3+
const filenameToInterfaceName = (filename) => {
4+
return path.basename(filename)
5+
.replace(/^(\w)/, (_, c) => 'I' + c.toUpperCase())
6+
.replace(/\W+(\w)/g, (_, c) => c.toUpperCase());
7+
};
8+
9+
const cssModuleToTypescriptInterfaceProperties = (cssModuleObject, indent = ' ') => {
10+
return Object.keys(cssModuleObject)
11+
.map((key) => `${indent}'${key}': string;`)
12+
.join('\n');
13+
};
14+
15+
export const filenameToTypingsFilename = (filename) => {
16+
const dirName = path.dirname(filename);
17+
const baseName = path.basename(filename);
18+
return path.join(dirName, `${baseName}.d.ts`);
19+
};
20+
21+
export const generateInterface = (cssModuleObject, filename, indent) => {
22+
const interfaceName = filenameToInterfaceName(filename);
23+
const interfaceProperties = cssModuleToTypescriptInterfaceProperties(cssModuleObject, indent);
24+
return (
25+
`export interface ${interfaceName} {
26+
${interfaceProperties}
27+
}
28+
declare const styles: ${interfaceName};
29+
30+
export default styles;
31+
`);
32+
};

src/index.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import cssLoader from 'css-loader';
2+
import cssLocalsLoader from 'css-loader/locals';
3+
import {
4+
generateInterface,
5+
filenameToTypingsFilename,
6+
} from './cssModuleToInterface';
7+
import * as persist from './persist';
8+
9+
module.exports = function(input) {
10+
if(this.cacheable) this.cacheable();
11+
12+
// mock async step 1 - css loader is async, we need to intercept this so we get async ourselves
13+
const callback = this.async();
14+
// mock async step 2 - offer css loader a "fake" callback
15+
this.async = () => (err, content) => {
16+
const cssmodules = this.exec(content);
17+
const requestedResource = this.resourcePath;
18+
19+
const cssModuleInterfaceFilename = filenameToTypingsFilename(requestedResource);
20+
const cssModuleInterface = generateInterface(cssmodules, requestedResource);
21+
persist.writeToFileIfChanged(cssModuleInterfaceFilename, cssModuleInterface);
22+
// mock async step 3 - make `async` return the actual callback again before calling the 'real' css-loader
23+
this.async = () => callback;
24+
cssLoader.call(this, input);
25+
};
26+
cssLocalsLoader.call(this, input);
27+
}

src/persist.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import fs from 'graceful-fs';
2+
import crypto from 'crypto';
3+
4+
export const writeToFileIfChanged = (filename, content) => {
5+
try {
6+
const currentInput = fs.readFileSync(filename, 'utf-8');
7+
const oldHash = crypto.createHash('md5').update(currentInput).digest("hex");
8+
const newHash = crypto.createHash('md5').update(content).digest("hex");
9+
// the definitions haven't changed - ignore this
10+
if (oldHash === newHash) {
11+
return false;
12+
}
13+
} catch(e) {
14+
} finally {
15+
fs.writeFileSync(filename, content);
16+
}
17+
};

test/entry.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import styles from './example.css';
2+
3+
const foo = styles.foo;
4+
const barBaz = styles['bar-baz'];

test/example.css

+7
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/expected-example.css.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface IExampleCss {
2+
'foo': string;
3+
'bar-baz': string;
4+
}
5+
declare const styles: IExampleCss;
6+
7+
export default styles;

test/tsconfig.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es6",
4+
"noImplicitAny": true
5+
}
6+
}

test/webpack.config.babel.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
entry: './test/entry.ts',
3+
output: {
4+
path: __dirname,
5+
filename: 'bundle.js'
6+
},
7+
module: {
8+
loaders: [
9+
{ test: /\.ts$/, loaders: ['babel', 'ts'] },
10+
{ test: /\.css$/, loader: '../src/index.js?modules' }
11+
]
12+
}
13+
};

0 commit comments

Comments
 (0)