Skip to content

Commit 53dd50e

Browse files
authored
feat: Initial commit (#1)
* feat: Initial commit * chore: Update nvm to use lts * chore: Use NVM for travis node version * docs: Make Mark happy * refactor: Clean up named exports function * fix: Remove loader reference in banner * docs: Update readme * fix: Error properly when incorrect mode * chore: Update author * chore: Update description * fix: Add missing const
1 parent cf9265c commit 53dd50e

26 files changed

+409
-1
lines changed

.editorconfig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# editorconfig.org
2+
root = true
3+
4+
[*]
5+
indent_style = space
6+
indent_size = 2
7+
end_of_line = lf
8+
charset = utf-8
9+
trim_trailing_whitespace = true
10+
insert_final_newline = true
11+
12+
[*.md]
13+
trim_trailing_whitespace = false

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
*.log
3+
yarn.lock
4+
package-lock.json

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lts/*

.travis.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
language: node_js
2+
cache:
3+
directories:
4+
- node_modules
5+
notifications:
6+
email: false
7+
before_script:
8+
- npm prune
9+
script:
10+
- npm test
11+
after_success:
12+
- npm run travis-deploy-once "npm run semantic-release"
13+
branches:
14+
except:
15+
- /^v\d+\.\d+\.\d+$/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 SEEK
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,67 @@
11
# css-modules-typescript-loader
22

3-
Webpack loader to create type declarations for css modules
3+
[Webpack](https://webpack.js.org/) loader to create [TypeScript](https://www.typescriptlang.org/) declarations for [CSS Modules](https://github.com/css-modules/css-modules).
4+
5+
Emits TypeScript declaration files matching your CSS Modules in the same location as your source files, e.g. `src/Component.css` will generate `src/Component.css.d.ts`.
6+
7+
## Why?
8+
9+
There are currently a lot of [solutions to this problem](https://www.npmjs.com/search?q=css%20modules%20typescript%20loader). However, this package differs in the following ways:
10+
11+
- Encourages generated TypeScript declarations to be checked into source control, which allows `webpack` and `tsc` commands to be run in parallel in CI.
12+
13+
- Ensures committed TypeScript declarations are in sync with the code that generated them via the [`verify` mode](#verify-mode).
14+
15+
- Doesn't silently ignore invalid TypeScript identifiers. If a class name is not valid TypeScript (e.g. `.foo-bar`, instead of `.fooBar`), `tsc` should report an error.
16+
17+
## Usage
18+
19+
Place `css-modules-typescript-loader` directly after `css-loader` in your webpack config.
20+
21+
```js
22+
module.exports = {
23+
module: {
24+
rules: [
25+
{
26+
test: /\.css$/,
27+
use: [
28+
'css-modules-typescript-loader',
29+
{
30+
loader: 'css-loader',
31+
options: {
32+
modules: true
33+
}
34+
}
35+
]
36+
}
37+
]
38+
}
39+
};
40+
```
41+
42+
### Verify Mode
43+
44+
Since the TypeScript declarations are generated by `webpack`, they may potentially be out of date by the time you run `tsc`. To ensure your types are up to date, you can run the loader in `verify` mode, which is particularly useful in CI.
45+
46+
For example:
47+
48+
```js
49+
{
50+
loader: 'css-modules-typescript-loader',
51+
options: {
52+
mode: process.env.CI ? 'verify' : 'emit'
53+
}
54+
}
55+
```
56+
57+
Instead of emitting new TypeScript declarations, this will throw an error if a generated declaration doesn't match the committed one. This allows `tsc` and `webpack` to run in parallel in CI, if desired.
58+
59+
This workflow is similar to using the [Prettier](https://github.com/prettier/prettier) [`--list-different` option](https://prettier.io/docs/en/cli.html#list-different).
60+
61+
## With Thanks
62+
63+
This package borrows heavily from [typings-for-css-modules-loader](https://github.com/Jimdo/typings-for-css-modules-loader).
64+
65+
## License
66+
67+
MIT.

index.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const loaderUtils = require('loader-utils');
4+
5+
const bannerMessage =
6+
'// This file is automatically generated.\n// Please do not change this file!';
7+
8+
const getNoDeclarationFileError = cssModuleInterfaceFilename =>
9+
new Error(
10+
`Generated type declaration does not exist. Run webpack and commit the type declaration for '${cssModuleInterfaceFilename}'`
11+
);
12+
13+
const getTypeMismatchError = cssModuleInterfaceFilename =>
14+
new Error(
15+
`Generated type declaration file is outdated. Run webpack and commit the updated type declaration for '${cssModuleInterfaceFilename}'`
16+
);
17+
18+
const cssModuleToNamedExports = cssModuleKeys => {
19+
return cssModuleKeys
20+
.map(key => `export const ${key}: string;`)
21+
.join('\n')
22+
.concat('\n');
23+
};
24+
25+
const filenameToTypingsFilename = filename => {
26+
const dirName = path.dirname(filename);
27+
const baseName = path.basename(filename);
28+
return path.join(dirName, `${baseName}.d.ts`);
29+
};
30+
31+
const validModes = ['emit', 'verify'];
32+
33+
module.exports = function(content, ...rest) {
34+
const callback = this.async();
35+
36+
const filename = this.resourcePath;
37+
const { mode = 'emit' } = loaderUtils.getOptions(this) || {};
38+
if (!validModes.includes(mode)) {
39+
return callback(new Error(`Invalid mode option: ${mode}`));
40+
}
41+
42+
const cssModuleInterfaceFilename = filenameToTypingsFilename(filename);
43+
44+
const keyRegex = /"([^\\"]+)":/g;
45+
let match;
46+
const cssModuleKeys = [];
47+
48+
while ((match = keyRegex.exec(content))) {
49+
if (cssModuleKeys.indexOf(match[1]) < 0) {
50+
cssModuleKeys.push(match[1]);
51+
}
52+
}
53+
54+
const cssModuleDefinition = `${bannerMessage}\n${cssModuleToNamedExports(
55+
cssModuleKeys
56+
)}`;
57+
58+
if (mode === 'verify') {
59+
fs.readFile(
60+
cssModuleInterfaceFilename,
61+
{ encoding: 'utf-8' },
62+
(err, fileContents) => {
63+
if (err) {
64+
const error =
65+
err.code === 'ENOENT'
66+
? getNoDeclarationFileError(cssModuleInterfaceFilename)
67+
: err;
68+
return callback(error);
69+
}
70+
71+
if (cssModuleDefinition !== fileContents) {
72+
return callback(getTypeMismatchError(cssModuleInterfaceFilename));
73+
}
74+
75+
return callback(null, content, ...rest);
76+
}
77+
);
78+
} else {
79+
fs.writeFile(
80+
cssModuleInterfaceFilename,
81+
cssModuleDefinition,
82+
{ encoding: 'utf-8' },
83+
err => {
84+
if (err) {
85+
return callback(err);
86+
} else {
87+
return callback(null, content, ...rest);
88+
}
89+
}
90+
);
91+
}
92+
};

package.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "css-modules-typescript-loader",
3+
"version": "0.0.0-development",
4+
"description": "Webpack loader to create TypeScript declarations for CSS Modules",
5+
"main": "index.js",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/seek-oss/css-modules-typescript-loader.git"
9+
},
10+
"author": "SEEK",
11+
"license": "MIT",
12+
"bugs": {
13+
"url": "https://github.com/seek-oss/css-modules-typescript-loader/issues"
14+
},
15+
"scripts": {
16+
"commit": "git-cz",
17+
"travis-deploy-once": "travis-deploy-once",
18+
"semantic-release": "semantic-release",
19+
"test": "jest"
20+
},
21+
"jest": {
22+
"testEnvironment": "node"
23+
},
24+
"husky": {
25+
"hooks": {
26+
"commit-msg": "commitlint --edit --extends seek"
27+
}
28+
},
29+
"homepage": "https://github.com/seek-oss/css-modules-typescript-loader#readme",
30+
"devDependencies": {
31+
"@commitlint/cli": "^7.2.1",
32+
"commitizen": "^3.0.2",
33+
"commitlint-config-seek": "^1.0.0",
34+
"css-loader": "^1.0.0",
35+
"cz-conventional-changelog": "^2.1.0",
36+
"husky": "^1.1.2",
37+
"jest": "^23.6.0",
38+
"memory-fs": "^0.4.1",
39+
"semantic-release": "^15.9.17",
40+
"travis-deploy-once": "^5.0.9",
41+
"webpack": "^4.21.0"
42+
},
43+
"release": {
44+
"success": false
45+
},
46+
"config": {
47+
"commitizen": {
48+
"path": "./node_modules/cz-conventional-changelog"
49+
}
50+
}
51+
}

test/compiler.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const path = require('path');
2+
const webpack = require('webpack');
3+
const memoryfs = require('memory-fs');
4+
5+
module.exports = (entry, options = {}) => {
6+
const compiler = webpack({
7+
context: path.dirname(entry),
8+
entry,
9+
output: {
10+
path: path.dirname(entry),
11+
filename: 'bundle.js'
12+
},
13+
module: {
14+
rules: [
15+
{
16+
test: /\.css$/,
17+
use: [
18+
{
19+
loader: require.resolve('../index.js'),
20+
options
21+
},
22+
{
23+
loader: 'css-loader',
24+
options: {
25+
modules: true
26+
}
27+
}
28+
]
29+
}
30+
]
31+
}
32+
});
33+
34+
compiler.outputFileSystem = new memoryfs();
35+
36+
return new Promise((resolve, reject) => {
37+
compiler.run((err, stats) => {
38+
if (err || stats.hasErrors()) {
39+
reject({
40+
failed: true,
41+
errors: err || stats.toJson().errors
42+
});
43+
}
44+
45+
resolve(stats);
46+
});
47+
});
48+
};

test/emit-declaration/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
index.css.d.ts
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Can emit valid declaration 1`] = `
4+
"// This file is automatically generated.
5+
// Please do not change this file!
6+
export const validClass: string;
7+
export const someClass: string;
8+
export const otherClass: string;
9+
"
10+
`;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const fs = require('fs');
2+
const compiler = require('../compiler.js');
3+
4+
test('Can emit valid declaration', async () => {
5+
await compiler(require.resolve('./index.js'));
6+
7+
const declaration = fs.readFileSync(
8+
require.resolve('./index.css.d.ts'),
9+
'utf-8'
10+
);
11+
12+
expect(declaration).toMatchSnapshot();
13+
});

test/emit-declaration/index.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.validClass {
2+
position: relative;
3+
}
4+
5+
.someClass {
6+
position: relative;
7+
}
8+
9+
.otherClass {
10+
display: block;
11+
}

test/emit-declaration/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import styles from './index.css';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.someClass {
2+
position: relative;
3+
}
4+
5+
.otherClass {
6+
display: block;
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This file is automatically generated.
2+
// Please do not change this file!
3+
export const someClass: string;
4+
export const differentClass: string;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import styles from './index.css';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const compiler = require('../compiler.js');
2+
3+
test('Can error on invalid declaration', async () => {
4+
await expect(
5+
compiler(require.resolve('./index.js'), {
6+
mode: 'verify'
7+
})
8+
).rejects.toMatchObject({
9+
failed: true
10+
});
11+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.someClass {
2+
position: relative;
3+
}
4+
5+
.otherClass {
6+
display: block;
7+
}

0 commit comments

Comments
 (0)