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

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
timse committed Sep 17, 2016
0 parents commit c1e68ac
Show file tree
Hide file tree
Showing 17 changed files with 373 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["es2015", "stage-3"]
}
30 changes: 30 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# http://editorconfig.org

# A special property that should be specified at the top of the file outside of
# any sections. Set to true to stop .editor config file search on current file
root = true

[*]
# Indentation style
# Possible values - tab, space
indent_style = space

# Indentation size in single-spaced characters
# Possible values - an integer, tab
indent_size = 2

# Line ending file format
# Possible values - lf, crlf, cr
end_of_line = lf

# File character encoding
# Possible values - latin1, utf-8, utf-16be, utf-16le
charset = utf-8

# Denotes whether to trim whitespace at the end of lines
# Possible values - true, false
trim_trailing_whitespace = true

# Denotes whether file should end with a newline
# Possible values - true, false
insert_final_newline = true
45 changes: 45 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"parser": "babel-eslint",
"plugins": [
"babel"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-console": [
0
],
"no-var": [
"error"
],
"comma-dangle": [
"error",
"always"
],
"no-unused-vars": [
2, {"vars": "all", "args": "after-used"}
]
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
lib
bundle.js
example.css.d.ts
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.babelrc
.eslintrc
.editorconfig
jsconfig.json
src
test
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# typings-for-css-modules-loaderg

Webpack loader that works as a css-loader drop-in replacement to generate TypeScript typings for CSS modules on the fly

## Installation

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

## Usage

Keep your `webpack.config` as is just instead of using `css-loader` use `typings-for-css-modules-loader`
*its important you keep all the params that you used for the css-loader before, as they will be passed along in the process*

before:
```js
webpackConfig.module.loaders: [
{ test: /\.css$/, loader: 'css?modules' }
{ test: /\.scss$/, loader: 'css?modules&sass' }
];
```

after:
```js
webpackConfig.module.loaders: [
{ test: /\.css$/, loader: 'typings-for-css-modules?modules' }
{ test: /\.scss$/, loader: 'typings-for-css-modules?modules&sass' }
];
```

## Example

Imagine you have a file `~/my-project/src/component/MyComponent/component.scss` in your project with the following content:
```
.some-class {
// some styles
&.someOtherClass {
// some other styles
}
&-sayWhat {
// more styles
}
}
```

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:
```
export interface IMyComponentScss {
'some-class': string;
'someOtherClass': string;
'some-class-sayWhat': string;
}
declare const styles: IMyComponentScss;
export default styles;
```

### Example in Visual Studio Code
![typed-css-modules](https://cloud.githubusercontent.com/assets/749171/16340497/c1cb6888-3a28-11e6-919b-f2f51a282bba.gif)

## Support

As the loader just acts as an intermediary it can handle all kind of css preprocessors (`sass`, `scss`, `stylus`, `less`, ...).
The only requirement is that those preprocessors have proper webpack loaders defined - meaning they can already be loaded by webpack anyways.

## Requirements

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.

## Known issues

- 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.
8 changes: 8 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES6"
},
"exclude": [
"node_modules"
]
}
54 changes: 54 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "typings-for-css-modules-loader",
"version": "0.0.1",
"description": "Drop-in replacement for css-loader to generate typings for your CSS-Modules on the fly in webpack",
"main": "lib/index.js",
"scripts": {
"build": "babel src -d lib",
"prepublish": "npm run build",
"pretest": "rm -f ./test/example.css.d.ts && touch ./test/example.css.d.ts",
"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",
"test": "npm run test:run > /dev/null 2>&1 && npm run test:run"
},
"author": "Tim Sebastian <tim.sebastian@gmail.com>",
"license": "MIT",
"keywords": [
"Typescript",
"TypeScript",
"CSS Modules",
"CSSModules",
"CSS Modules typings",
"Webpack",
"Webpack loader",
"Webpack css module typings loader",
"typescript webpack typings",
"css modules webpack typings"
],
"dependencies": {
"graceful-fs": "4.1.4"
},
"devDependencies": {
"babel-cli": "6.10.1",
"babel-eslint": "6.1.0",
"babel-loader": "^6.2.5",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "6.9.0",
"babel-preset-stage-0": "6.5.0",
"eslint": "2.13.1",
"eslint-plugin-babel": "3.3.0",
"ts-loader": "^0.8.2",
"typescript": "^1.8.10",
"webpack": "^1.13.2"
},
"peerDependencies": {
"css-loader": "^0.23.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Jimdo/typings-for-css-modules-loader.git"
},
"bugs": {
"url": "https://github.com/Jimdo/typings-for-css-modules-loader/issues"
},
"homepage": "https://github.com/Jimdo/typings-for-css-modules-loader#readme"
}
39 changes: 39 additions & 0 deletions src/cssModuleHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import path from 'path';
import vm from 'vm';

const isCssModule = (module) => {
if (!module || typeof module.request !== 'string') {
return false;
}

const extname = path.extname(module.request);
return /\/css-loader\//.test(module.request) && extname !== '.js';
};

export const filterCssModules = (modules) => {
return modules.filter(isCssModule);
};

export const removeLoadersBeforeCssLoader = (loaders) => {
let sawCssLoader = false;
// remove all loaders before the css-loader
return loaders.filter((loader)=> {
if (loader.indexOf('/css-loader/') > -1) {
sawCssLoader = true;
}

return sawCssLoader;
});
};

export const extractCssModuleFromSource = (source) => {
const sandbox = {
exports: null,
module: {},
require: () => () => [],
};
const script = new vm.Script(source);
const context = new vm.createContext(sandbox);
script.runInContext(context);
return sandbox.exports.locals;
};
32 changes: 32 additions & 0 deletions src/cssModuleToInterface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import path from 'path';

const filenameToInterfaceName = (filename) => {
return path.basename(filename)
.replace(/^(\w)/, (_, c) => 'I' + c.toUpperCase())
.replace(/\W+(\w)/g, (_, c) => c.toUpperCase());
};

const cssModuleToTypescriptInterfaceProperties = (cssModuleObject, indent = ' ') => {
return Object.keys(cssModuleObject)
.map((key) => `${indent}'${key}': string;`)
.join('\n');
};

export const filenameToTypingsFilename = (filename) => {
const dirName = path.dirname(filename);
const baseName = path.basename(filename);
return path.join(dirName, `${baseName}.d.ts`);
};

export const generateInterface = (cssModuleObject, filename, indent) => {
const interfaceName = filenameToInterfaceName(filename);
const interfaceProperties = cssModuleToTypescriptInterfaceProperties(cssModuleObject, indent);
return (
`export interface ${interfaceName} {
${interfaceProperties}
}
declare const styles: ${interfaceName};
export default styles;
`);
};
27 changes: 27 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import cssLoader from 'css-loader';
import cssLocalsLoader from 'css-loader/locals';
import {
generateInterface,
filenameToTypingsFilename,
} from './cssModuleToInterface';
import * as persist from './persist';

module.exports = function(input) {
if(this.cacheable) this.cacheable();

// mock async step 1 - css loader is async, we need to intercept this so we get async ourselves
const callback = this.async();
// mock async step 2 - offer css loader a "fake" callback
this.async = () => (err, content) => {
const cssmodules = this.exec(content);
const requestedResource = this.resourcePath;

const cssModuleInterfaceFilename = filenameToTypingsFilename(requestedResource);
const cssModuleInterface = generateInterface(cssmodules, requestedResource);
persist.writeToFileIfChanged(cssModuleInterfaceFilename, cssModuleInterface);
// mock async step 3 - make `async` return the actual callback again before calling the 'real' css-loader
this.async = () => callback;
cssLoader.call(this, input);
};
cssLocalsLoader.call(this, input);
}
17 changes: 17 additions & 0 deletions src/persist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import fs from 'graceful-fs';
import crypto from 'crypto';

export const writeToFileIfChanged = (filename, content) => {
try {
const currentInput = fs.readFileSync(filename, 'utf-8');
const oldHash = crypto.createHash('md5').update(currentInput).digest("hex");
const newHash = crypto.createHash('md5').update(content).digest("hex");
// the definitions haven't changed - ignore this
if (oldHash === newHash) {
return false;
}
} catch(e) {
} finally {
fs.writeFileSync(filename, content);
}
};
4 changes: 4 additions & 0 deletions test/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import styles from './example.css';

const foo = styles.foo;
const barBaz = styles['bar-baz'];
7 changes: 7 additions & 0 deletions test/example.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.foo {
color: white;
}

.bar-baz {
color: green;
}
7 changes: 7 additions & 0 deletions test/expected-example.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface IExampleCss {
'foo': string;
'bar-baz': string;
}
declare const styles: IExampleCss;

export default styles;
6 changes: 6 additions & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"target": "es6",
"noImplicitAny": true
}
}
13 changes: 13 additions & 0 deletions test/webpack.config.babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
entry: './test/entry.ts',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.ts$/, loaders: ['babel', 'ts'] },
{ test: /\.css$/, loader: '../src/index.js?modules' }
]
}
};

0 comments on commit c1e68ac

Please sign in to comment.