Repository: seek-oss/css-modules-typescript-loader Branch: master Commit: bb4892a5cae1 Files: 35 Total size: 16.0 KB Directory structure: gitextract_rb2bm0ye/ ├── .editorconfig ├── .gitignore ├── .npmrc ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test/ ├── compiler.js ├── emit-declaration/ │ ├── .gitignore │ ├── __snapshots__/ │ │ └── emit-declaration.test.js.snap │ ├── common.css │ ├── emit-declaration.test.js │ ├── index.css │ └── index.js ├── emit-empty-declaration/ │ ├── .gitignore │ ├── __snapshots__/ │ │ └── emit-empty-declaration.test.js.snap │ ├── emit-empty-declaration.test.js │ ├── index.css │ └── index.js ├── getErrorMessage.js ├── verify-invalid-declaration/ │ ├── __snapshots__/ │ │ └── verify-invalid-declaration.test.js.snap │ ├── index.css │ ├── index.css.d.ts │ ├── index.js │ └── verify-invalid-declaration.test.js ├── verify-missing-declaration/ │ ├── __snapshots__/ │ │ └── verify-missing-declaration.test.js.snap │ ├── index.css │ ├── index.js │ └── verify-missing-declaration.test.js └── verify-valid-declaration/ ├── index.css ├── index.css.d.ts ├── index.js └── verify-valid-declaration.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitignore ================================================ node_modules/ *.log yarn.lock package-lock.json ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: .nvmrc ================================================ lts/* ================================================ FILE: .travis.yml ================================================ language: node_js cache: directories: - node_modules notifications: email: false before_script: - npm prune script: - npm test after_success: - npm run travis-deploy-once "npm run semantic-release" branches: except: - /^v\d+\.\d+\.\d+$/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 SEEK Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Build Status](https://img.shields.io/travis/seek-oss/css-modules-typescript-loader/master.svg?style=flat-square)](http://travis-ci.org/seek-oss/css-modules-typescript-loader) [![npm](https://img.shields.io/npm/v/css-modules-typescript-loader.svg?style=flat-square)](https://www.npmjs.com/package/css-modules-typescript-loader) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=flat-square)](http://commitizen.github.io/cz-cli/) # css-modules-typescript-loader [Webpack](https://webpack.js.org/) loader to create [TypeScript](https://www.typescriptlang.org/) declarations for [CSS Modules](https://github.com/css-modules/css-modules). 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`. ## Why? 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: - Encourages generated TypeScript declarations to be checked into source control, which allows `webpack` and `tsc` commands to be run in parallel in CI. - Ensures committed TypeScript declarations are in sync with the code that generated them via the [`verify` mode](#verify-mode). ## Usage Place `css-modules-typescript-loader` directly after `css-loader` in your webpack config. ```js module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'css-modules-typescript-loader', { loader: 'css-loader', options: { modules: true } } ] } ] } }; ``` ### Verify Mode 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. For example: ```js { loader: 'css-modules-typescript-loader', options: { mode: process.env.CI ? 'verify' : 'emit' } } ``` 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. 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). ## With Thanks This package borrows heavily from [typings-for-css-modules-loader](https://github.com/Jimdo/typings-for-css-modules-loader). ## License MIT. ================================================ FILE: index.js ================================================ const fs = require('fs'); const path = require('path'); const loaderUtils = require('loader-utils'); const LineDiff = require('line-diff'); const bannerMessage = '// This file is automatically generated.\n// Please do not change this file!'; const cssModuleExport = 'export const cssExports: CssExports;\nexport default cssExports;\n'; const getNoDeclarationFileError = ({ filename }) => new Error( `Generated type declaration does not exist. Run webpack and commit the type declaration for '${filename}'` ); const getTypeMismatchError = ({ filename, expected, actual }) => { const diff = new LineDiff(enforceLFLineSeparators(actual), expected).toString(); return new Error( `Generated type declaration file is outdated. Run webpack and commit the updated type declaration for '${filename}'\n\n${diff}` ); }; const cssModuleToInterface = (cssModuleKeys) => { const interfaceFields = cssModuleKeys .sort() .map(key => ` '${key}': string;`) .join('\n'); return `interface CssExports {\n${interfaceFields}\n}`; }; const filenameToTypingsFilename = filename => { const dirName = path.dirname(filename); const baseName = path.basename(filename); return path.join(dirName, `${baseName}.d.ts`); }; const enforceLFLineSeparators = text => { if (text) { // replace all CRLFs (Windows) by LFs (Unix) return text.replace(/\r\n/g, "\n"); } else { return text; } }; const compareText = (contentA, contentB) => { return enforceLFLineSeparators(contentA) === enforceLFLineSeparators(contentB); }; const validModes = ['emit', 'verify']; const isFileNotFound = err => err && err.code === 'ENOENT'; const makeDoneHandlers = (callback, content, rest) => ({ failed: e => callback(e), success: () => callback(null, content, ...rest) }); const makeFileHandlers = filename => ({ read: handler => fs.readFile(filename, { encoding: 'utf-8' }, handler), write: (content, handler) => fs.writeFile(filename, content, { encoding: 'utf-8' }, handler) }); const extractLocalExports = (content) => { let localExports = content.split('exports.locals')[1]; if (!localExports) { localExports = content.split('___CSS_LOADER_EXPORT___.locals')[1]; } return localExports; } module.exports = function(content, ...rest) { const { failed, success } = makeDoneHandlers(this.async(), content, rest); const filename = this.resourcePath; const { mode = 'emit' } = loaderUtils.getOptions(this) || {}; if (!validModes.includes(mode)) { return failed(new Error(`Invalid mode option: ${mode}`)); } const cssModuleInterfaceFilename = filenameToTypingsFilename(filename); const { read, write } = makeFileHandlers(cssModuleInterfaceFilename); const keyRegex = /"([^\\"]+)":/g; let match; const cssModuleKeys = []; const localExports = extractLocalExports(content); while ((match = keyRegex.exec(localExports))) { if (cssModuleKeys.indexOf(match[1]) < 0) { cssModuleKeys.push(match[1]); } } const cssModuleDefinition = `${bannerMessage}\n${cssModuleToInterface(cssModuleKeys)}\n${cssModuleExport}`; if (mode === 'verify') { read((err, fileContents) => { if (isFileNotFound(err)) { return failed( getNoDeclarationFileError({ filename: cssModuleInterfaceFilename }) ); } if (err) { return failed(err); } if (!compareText(cssModuleDefinition, fileContents)) { return failed( getTypeMismatchError({ filename: cssModuleInterfaceFilename, expected: cssModuleDefinition, actual: fileContents }) ); } return success(); }); } else { read((_, fileContents) => { if (!compareText(cssModuleDefinition, fileContents)) { write(cssModuleDefinition, err => { if (err) { failed(err); } else { success(); } }); } else { success(); } }); } }; ================================================ FILE: package.json ================================================ { "name": "css-modules-typescript-loader", "version": "0.0.0-development", "description": "Webpack loader to create TypeScript declarations for CSS Modules", "main": "index.js", "repository": { "type": "git", "url": "https://github.com/seek-oss/css-modules-typescript-loader.git" }, "author": "SEEK", "license": "MIT", "bugs": { "url": "https://github.com/seek-oss/css-modules-typescript-loader/issues" }, "scripts": { "commit": "git-cz", "travis-deploy-once": "travis-deploy-once", "semantic-release": "semantic-release", "test": "jest" }, "jest": { "testEnvironment": "node" }, "husky": { "hooks": { "commit-msg": "commitlint --edit --extends seek" } }, "homepage": "https://github.com/seek-oss/css-modules-typescript-loader#readme", "dependencies": { "line-diff": "^2.0.1", "loader-utils": "^1.2.3" }, "devDependencies": { "@commitlint/cli": "^7.2.1", "commitizen": "^3.0.2", "commitlint-config-seek": "^1.0.0", "css-loader": "^4.2.1", "cz-conventional-changelog": "^2.1.0", "husky": "^1.1.2", "jest": "^24.7.1", "memory-fs": "^0.4.1", "semantic-release": "^15.9.17", "travis-deploy-once": "^5.0.9", "webpack": "^4.21.0" }, "release": { "success": false }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } } } ================================================ FILE: test/compiler.js ================================================ const path = require('path'); const webpack = require('webpack'); const memoryfs = require('memory-fs'); module.exports = (entry, options = {}) => { const { sourceMap, ...loaderOptions } = options; const compiler = webpack({ context: path.dirname(entry), entry, output: { path: path.dirname(entry), filename: 'bundle.js' }, module: { rules: [ { test: /\.css$/, use: [ { loader: require.resolve('../index.js'), options: loaderOptions }, { loader: 'css-loader', options: { modules: true, sourceMap: !!sourceMap } } ] } ] } }); compiler.outputFileSystem = new memoryfs(); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err || stats.hasErrors()) { reject({ failed: true, errors: err || stats.toJson().errors }); } resolve(stats); }); }); }; ================================================ FILE: test/emit-declaration/.gitignore ================================================ index.css.d.ts ================================================ FILE: test/emit-declaration/__snapshots__/emit-declaration.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Can emit valid declaration with sourceMap 1`] = ` "// This file is automatically generated. // Please do not change this file! interface CssExports { 'composedClass': string; 'otherClass': string; 'someClass': string; 'validClass': string; } export const cssExports: CssExports; export default cssExports; " `; exports[`Can emit valid declaration without sourceMaps 1`] = ` "// This file is automatically generated. // Please do not change this file! interface CssExports { 'composedClass': string; 'otherClass': string; 'someClass': string; 'validClass': string; } export const cssExports: CssExports; export default cssExports; " `; ================================================ FILE: test/emit-declaration/common.css ================================================ .baseClass { position: relative; } ================================================ FILE: test/emit-declaration/emit-declaration.test.js ================================================ const fs = require('fs'); const compiler = require('../compiler.js'); test('Can emit valid declaration without sourceMaps', async () => { await compiler(require.resolve('./index.js')); const declaration = fs.readFileSync( require.resolve('./index.css.d.ts'), 'utf-8' ); expect(declaration).toMatchSnapshot(); }); test('Can emit valid declaration with sourceMap', async () => { await compiler(require.resolve('./index.js'), { sourceMap: true }); const declaration = fs.readFileSync( require.resolve('./index.css.d.ts'), 'utf-8' ); expect(declaration).toMatchSnapshot(); }); ================================================ FILE: test/emit-declaration/index.css ================================================ .validClass { position: relative; } .someClass { position: relative; } .otherClass { display: block; } .composedClass { composes: baseClass from './common.css'; color: cornflowerblue; } ================================================ FILE: test/emit-declaration/index.js ================================================ import styles from './index.css'; ================================================ FILE: test/emit-empty-declaration/.gitignore ================================================ index.css.d.ts ================================================ FILE: test/emit-empty-declaration/__snapshots__/emit-empty-declaration.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Can emit valid declaration without classes 1`] = ` "// This file is automatically generated. // Please do not change this file! interface CssExports { } export const cssExports: CssExports; export default cssExports; " `; ================================================ FILE: test/emit-empty-declaration/emit-empty-declaration.test.js ================================================ const fs = require('fs'); const compiler = require('../compiler.js'); test('Can emit valid declaration without classes', async () => { await compiler(require.resolve('./index.js')); const declaration = fs.readFileSync( require.resolve('./index.css.d.ts'), 'utf-8' ); expect(declaration).toMatchSnapshot(); }); ================================================ FILE: test/emit-empty-declaration/index.css ================================================ html { /* No class selectors in this file ... */ } ================================================ FILE: test/emit-empty-declaration/index.js ================================================ import "./index.css"; ================================================ FILE: test/getErrorMessage.js ================================================ module.exports = error => error .split(process.cwd()) .join('') .match(/(Error: .*?)\s{4}at /s)[1]; ================================================ FILE: test/verify-invalid-declaration/__snapshots__/verify-invalid-declaration.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Can error on invalid declaration 1`] = ` "Error: Generated type declaration file is outdated. Run webpack and commit the updated type declaration for '/test/verify-invalid-declaration/index.css.d.ts' // This file is automatically generated. // Please do not change this file! interface CssExports { 'classInBothFiles': string; - 'classInTypeScriptFile': string; + 'classInCssFile': string; } export const cssExports: CssExports; export default cssExports; " `; ================================================ FILE: test/verify-invalid-declaration/index.css ================================================ .classInBothFiles { position: relative; } .classInCssFile { display: block; } ================================================ FILE: test/verify-invalid-declaration/index.css.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'classInBothFiles': string; 'classInTypeScriptFile': string; } export const cssExports: CssExports; export default cssExports; ================================================ FILE: test/verify-invalid-declaration/index.js ================================================ import styles from './index.css'; ================================================ FILE: test/verify-invalid-declaration/verify-invalid-declaration.test.js ================================================ const compiler = require('../compiler.js'); const getErrorMessage = require('../getErrorMessage'); test('Can error on invalid declaration', async () => { expect.assertions(1); try { await compiler(require.resolve('./index.js'), { mode: 'verify' }); } catch (err) { // make test robust for Windows by replacing backslashes in the file path with slashes let errorMessage = getErrorMessage(err.errors[0]).replace(/(?<=Error:.*)\\/g, "/"); expect(errorMessage).toMatchSnapshot(); } }); ================================================ FILE: test/verify-missing-declaration/__snapshots__/verify-missing-declaration.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Can error on invalid declaration 1`] = ` "Error: Generated type declaration does not exist. Run webpack and commit the type declaration for '/test/verify-missing-declaration/index.css.d.ts' " `; ================================================ FILE: test/verify-missing-declaration/index.css ================================================ .someClass { position: relative; } .otherClass { display: block; } ================================================ FILE: test/verify-missing-declaration/index.js ================================================ import styles from './index.css'; ================================================ FILE: test/verify-missing-declaration/verify-missing-declaration.test.js ================================================ const compiler = require('../compiler.js'); const getErrorMessage = require('../getErrorMessage'); test('Can error on invalid declaration', async () => { expect.assertions(1); try { await compiler(require.resolve('./index.js'), { mode: 'verify' }); } catch (err) { // make test robust for Windows by replacing backslashes in the file path with slashes let errorMessage = getErrorMessage(err.errors[0]).replace(/(?<=Error:.*)\\/g, "/"); expect(errorMessage).toMatchSnapshot(); } }); ================================================ FILE: test/verify-valid-declaration/index.css ================================================ .validClass { position: relative; } .someClass { position: relative; } .otherClass { display: block; } .hyphened-classname, .underscored_classname { color: papayawhip; } ================================================ FILE: test/verify-valid-declaration/index.css.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'hyphened-classname': string; 'otherClass': string; 'someClass': string; 'underscored_classname': string; 'validClass': string; } export const cssExports: CssExports; export default cssExports; ================================================ FILE: test/verify-valid-declaration/index.js ================================================ import styles from './index.css'; ================================================ FILE: test/verify-valid-declaration/verify-valid-declaration.test.js ================================================ const compiler = require('../compiler.js'); test('Can verify valid declaration', async () => { // just validate webpack build passes await compiler(require.resolve('./index.js'), { mode: 'verify' }); });