diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 9785c26..0000000 --- a/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -/.circleci -/.github -/.vscode -/lib/**/__tests__ -/node_modules -/src -.editorconfig -npm-debug.log -tsconfig.json -tslint.json -yarn-error.log -yarn.lock diff --git a/README.md b/README.md index 0308ddd..2daa5e6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ for [CSS Modules](https://github.com/css-modules/css-modules). This project was inspired by this [`create-react-app` issue](https://github.com/facebook/create-react-app/issues/5677) and was based on [`css-module-types`](https://github.com/timothykang/css-module-types). -## Usage +## Installation To install with Yarn: @@ -36,6 +36,26 @@ Once installed, add this plugin to your `tsconfig.json`: } ``` +### Importing CSS + +A default export is always provided for your CSS module. + +```tsx +import styles from 'my.module.css'; + +const a = styles.myClass; +const b = styles['my_other-class']; +``` + +As of version 1.1.0, you can also use named exports for classes that don't contain hyphens or underscores. You can still access other classes via the default export. + +```tsx +import styles, { myClass } from 'my.module.css'; + +const a = myClass; +const b = styles['my_other-class']; +``` + ### Options | Option | Default value | Description | diff --git a/package.json b/package.json index 3b48f07..c37dc0d 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,17 @@ }, "keywords": [ "css", + "scss", + "sass", + "less", "modules", "plugin", "postcss", - "sass", "typescript" ], + "files": [ + "lib" + ], "scripts": { "build": "rm -rf ./lib && tsc", "prepublishOnly": "yarn build", @@ -47,13 +52,14 @@ "lodash": "^4.17.11", "postcss": "^7.0.16", "postcss-icss-selectors": "^2.0.3", + "reserved-words": "^0.1.2", "sass": "^1.20.1" }, "devDependencies": { "@types/jest": "^24.0.13", "@types/less": "^3.0.0", "@types/lodash": "^4.14.132", - "@types/node": "^10.12.18", + "@types/node": "^10.0.0", "@types/sass": "^1.16.0", "husky": "^2.3.0", "jest": "^24.8.0", diff --git a/src/@types/reserved-words.d.ts b/src/@types/reserved-words.d.ts new file mode 100644 index 0000000..e9c956c --- /dev/null +++ b/src/@types/reserved-words.d.ts @@ -0,0 +1,3 @@ +declare module 'reserved-words' { + export const check: (string: string, esVersion?: string) => boolean; +} diff --git a/src/helpers/__tests__/__snapshots__/cssSnapshots.test.ts.snap b/src/helpers/__tests__/__snapshots__/cssSnapshots.test.ts.snap index d55983b..09b9bb5 100644 --- a/src/helpers/__tests__/__snapshots__/cssSnapshots.test.ts.snap +++ b/src/helpers/__tests__/__snapshots__/cssSnapshots.test.ts.snap @@ -25,12 +25,19 @@ exports[`utils / cssSnapshots with file 'test.module.css' createExports should c 'classA': string; 'ClassB': string; 'class-c': string; + 'class_d': string; 'parent': string; 'childA': string; 'childB': string; 'nestedChild': string; }; export default classes; +export const classA: string; +export const ClassB: string; +export const parent: string; +export const childA: string; +export const childB: string; +export const nestedChild: string; " `; @@ -41,6 +48,7 @@ Object { "childB": "file__childB---pq4Ks", "class-c": "file__class-c---DZ1TD", "classA": "file__classA---2xcnJ", + "class_d": "file__class_d---1mwNi", "nestedChild": "file__nestedChild---2d15b", "parent": "file__parent---1ATMj", } @@ -84,6 +92,9 @@ exports[`utils / cssSnapshots with file 'test.module.scss' createExports should 'local-class': string; 'local-class-2': string; 'local-class-inside-local': string; + 'reserved-words': string; + 'default': string; + 'const': string; 'nested-class-parent': string; 'child-class': string; 'nested-class-parent--extended': string; @@ -104,12 +115,15 @@ export default classes; exports[`utils / cssSnapshots with file 'test.module.scss' getClasses should return an object matching expected CSS 1`] = ` Object { "child-class": "file__child-class---1QWYM", + "const": "file__const---MIe_0", + "default": "file__default---2RWlj", "local-class": "file__local-class---3SW3k", "local-class-2": "file__local-class-2----c5z7", "local-class-inside-global": "file__local-class-inside-global---1T0um", "local-class-inside-local": "file__local-class-inside-local---1Z9pB", "nested-class-parent": "file__nested-class-parent---3qXdF", "nested-class-parent--extended": "file__nested-class-parent--extended---qsVau", + "reserved-words": "file__reserved-words---_rrID", "section-1": "file__section-1---1IHCS", "section-2": "file__section-2---cLFhf", "section-3": "file__section-3---1ldKa", diff --git a/src/helpers/__tests__/fixtures/test.module.css b/src/helpers/__tests__/fixtures/test.module.css index 719fdaa..f355d0b 100644 --- a/src/helpers/__tests__/fixtures/test.module.css +++ b/src/helpers/__tests__/fixtures/test.module.css @@ -10,6 +10,10 @@ color: rebeccapurple; } +.class_d { + color: rebeccapurple; +} + .parent { .childA { color: rebeccapurple; diff --git a/src/helpers/__tests__/fixtures/test.module.scss b/src/helpers/__tests__/fixtures/test.module.scss index 4441fe2..3a899a2 100644 --- a/src/helpers/__tests__/fixtures/test.module.scss +++ b/src/helpers/__tests__/fixtures/test.module.scss @@ -18,6 +18,15 @@ } } +.reserved-words { + .default { + color: rebeccapurple; + } + .const { + color: rebeccapurple; + } +} + .nested-class-parent { .child-class { color: rebeccapurple; diff --git a/src/helpers/cssSnapshots.ts b/src/helpers/cssSnapshots.ts index 1d35c5d..b3a2c7b 100644 --- a/src/helpers/cssSnapshots.ts +++ b/src/helpers/cssSnapshots.ts @@ -2,14 +2,18 @@ import { extractICSS, IICSSExports } from 'icss-utils'; import * as postcss from 'postcss'; import * as postcssIcssSelectors from 'postcss-icss-selectors'; import * as ts_module from 'typescript/lib/tsserverlibrary'; -import * as sass from 'sass'; import * as less from 'less'; +import * as sass from 'sass'; +import * as reserved from 'reserved-words'; import { transformClasses } from './classTransforms'; import { Options } from '../options'; +const NOT_CAMELCASE_REGEXP = /[\-_]/; const processor = postcss(postcssIcssSelectors({ mode: 'local' })); const classNameToProperty = (className: string) => `'${className}': string;`; +const classNameToNamedExport = (className: string) => + `export const ${className}: string;`; const flattenClassNames = ( previousValue: string[] = [], @@ -45,17 +49,32 @@ export const getClasses = ( } }; -export const createExports = (classes: IICSSExports, options: Options) => `\ -declare const classes: { - ${Object.keys(classes) +export const createExports = (classes: IICSSExports, options: Options) => { + const isCamelCase = (className: string) => + !NOT_CAMELCASE_REGEXP.test(className); +const isReservedWord = (className: string) => !reserved.check(className); + + const processedClasses = Object.keys(classes) .map(transformClasses(options.camelCase)) - .reduce(flattenClassNames, []) - .map(classNameToProperty) - .join('\n ')} + .reduce(flattenClassNames, []); + const camelCasedKeys = processedClasses + .filter(isCamelCase) + .filter(isReservedWord) + .map(classNameToNamedExport); + + const defaultExport = `\ +declare const classes: { + ${processedClasses.map(classNameToProperty).join('\n ')} }; export default classes; `; + if (camelCasedKeys.length) { + return defaultExport + camelCasedKeys.join('\n') + '\n'; + } + return defaultExport; +}; + export const getDtsSnapshot = ( ts: typeof ts_module, scriptSnapshot: ts.IScriptSnapshot, diff --git a/yarn.lock b/yarn.lock index 0147b4a..8aa9685 100644 --- a/yarn.lock +++ b/yarn.lock @@ -365,7 +365,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40" integrity sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA== -"@types/node@^10.12.18": +"@types/node@^10.0.0": version "10.14.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.7.tgz#1854f0a9aa8d2cd6818d607b3d091346c6730362" integrity sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A== @@ -3479,6 +3479,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +reserved-words@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1" + integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE= + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"