├── .all-contributorsrc
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ └── build.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── __tests__
├── __snapshots__
│ └── main.test.ts.snap
├── cli.test.ts
├── configs
│ ├── js-default-export
│ │ └── typed-scss-modules.config.js
│ ├── js-module-exports
│ │ └── typed-scss-modules.config.js
│ ├── js-named-export
│ │ └── typed-scss-modules.config.js
│ ├── ts-default-export
│ │ └── typed-scss-modules.config.ts
│ └── ts-named-export
│ │ └── typed-scss-modules.config.ts
├── core
│ ├── alerts.test.ts
│ ├── generate.test.ts
│ ├── list-different.test.ts
│ ├── list-different
│ │ ├── formatted.scss
│ │ ├── formatted.scss.d.ts
│ │ └── no-generated.scss
│ ├── list-files-and-perform-sanity-check.test.ts
│ ├── remove-file.test.ts
│ └── write-file.test.ts
├── dart-sass
│ ├── dart-sass.test.ts
│ ├── use.scss
│ └── variables.scss
├── dummy-styles
│ ├── alias-prefixes.scss
│ ├── alias-prefixes.scss.d.ts
│ ├── aliases.scss
│ ├── aliases.scss.d.ts
│ ├── complex.scss
│ ├── complex.scss.d.ts
│ ├── composes.scss
│ ├── composes.scss.d.ts
│ ├── dashes.scss
│ ├── dashes.scss.d.ts
│ ├── empty.scss
│ ├── global-variables.scss
│ ├── invalid.scss
│ ├── invalid.scss.d.ts
│ ├── nested-styles
│ │ ├── style.scss
│ │ └── style.scss.d.ts
│ ├── style.scss
│ ├── style.scss.d.ts
│ └── typed-scss-modules.config.js
├── helpers
│ └── index.ts
├── implementations
│ ├── get-default-implementation.test.ts
│ └── get-implementation.test.ts
├── load.test.ts
├── main.test.ts
├── prettier
│ ├── __snapshots__
│ │ └── prettier.test.ts.snap
│ └── prettier.test.ts
├── sass
│ ├── file-to-class-names.test.ts
│ └── importer.test.ts
└── typescript
│ ├── class-names-to-type-definitions.test.ts
│ └── get-type-definition-path.test.ts
├── babel.config.js
├── commitlint.config.js
├── docs
└── typed-scss-modules-example.gif
├── examples
├── basic
│ ├── README.md
│ ├── core
│ │ └── variables.scss
│ ├── feature-a
│ │ ├── index.ts
│ │ ├── style.scss
│ │ └── style.scss.d.ts
│ └── feature-b
│ │ ├── index.ts
│ │ ├── style.scss
│ │ └── style.scss.d.ts
├── config-file
│ ├── README.md
│ ├── feature
│ │ ├── a.scss
│ │ ├── a.scss.d.ts
│ │ └── variables.json
│ └── typed-scss-modules.config.js
├── default-export
│ ├── README.md
│ └── feature-a
│ │ ├── index.ts
│ │ ├── style.scss
│ │ └── style.scss.d.ts
└── output-folder
│ ├── README.md
│ ├── __generated__
│ └── examples
│ │ └── output-folder
│ │ ├── feature-a
│ │ └── a.scss.d.ts
│ │ ├── feature-b
│ │ └── b.scss.d.ts
│ │ └── feature-c
│ │ ├── c.scss.d.ts
│ │ └── nested
│ │ └── nested.scss.d.ts
│ ├── feature-a
│ ├── a.scss
│ └── index.ts
│ ├── feature-b
│ ├── b.scss
│ └── index.ts
│ ├── feature-c
│ ├── c.scss
│ ├── index.ts
│ └── nested
│ │ ├── index.ts
│ │ └── nested.scss
│ └── typed-scss-modules.config.js
├── jest.config.ts
├── lib
├── cli.ts
├── core
│ ├── alerts.ts
│ ├── generate.ts
│ ├── index.ts
│ ├── list-different.ts
│ ├── list-files-and-perform-sanity-checks.ts
│ ├── remove-file.ts
│ ├── types.ts
│ ├── watch.ts
│ └── write-file.ts
├── implementations
│ └── index.ts
├── index.ts
├── load.ts
├── main.ts
├── prettier
│ ├── can-resolve.ts
│ └── index.ts
├── sass
│ ├── file-to-class-names.ts
│ ├── importer.ts
│ ├── index.ts
│ └── source-to-class-names.ts
└── typescript
│ ├── class-names-to-type-definition.ts
│ ├── get-type-definition-path.ts
│ └── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "badgeTemplate": "[](#contributors-)",
8 | "contributors": [
9 | {
10 | "login": "dawnmist",
11 | "name": "Janeene Beeforth",
12 | "avatar_url": "https://avatars3.githubusercontent.com/u/5810277?v=4",
13 | "profile": "https://github.com/dawnmist",
14 | "contributions": [
15 | "bug",
16 | "code",
17 | "doc"
18 | ]
19 | },
20 | {
21 | "login": "ericbf",
22 | "name": "Eric Ferreira",
23 | "avatar_url": "https://avatars0.githubusercontent.com/u/2483476?v=4",
24 | "profile": "https://github.com/ericbf",
25 | "contributions": [
26 | "code",
27 | "doc"
28 | ]
29 | },
30 | {
31 | "login": "lkarmelo",
32 | "name": "Luis Lopes",
33 | "avatar_url": "https://avatars2.githubusercontent.com/u/20393808?v=4",
34 | "profile": "https://github.com/lkarmelo",
35 | "contributions": [
36 | "code"
37 | ]
38 | },
39 | {
40 | "login": "halfnibble",
41 | "name": "Josh Wedekind",
42 | "avatar_url": "https://avatars0.githubusercontent.com/u/5139752?v=4",
43 | "profile": "https://nostalg.io",
44 | "contributions": [
45 | "code",
46 | "doc",
47 | "test"
48 | ]
49 | },
50 | {
51 | "login": "peanutbother",
52 | "name": "Jared Gesser",
53 | "avatar_url": "https://avatars3.githubusercontent.com/u/6437182?v=4",
54 | "profile": "https://github.com/peanutbother",
55 | "contributions": [
56 | "ideas"
57 | ]
58 | },
59 | {
60 | "login": "raphael-leger",
61 | "name": "Raphaël L",
62 | "avatar_url": "https://avatars1.githubusercontent.com/u/12732777?v=4",
63 | "profile": "https://github.com/raphael-leger",
64 | "contributions": [
65 | "code",
66 | "ideas"
67 | ]
68 | },
69 | {
70 | "login": "nperez0111",
71 | "name": "Nick Perez",
72 | "avatar_url": "https://avatars1.githubusercontent.com/u/1852538?v=4",
73 | "profile": "https://NickTheSick.com",
74 | "contributions": [
75 | "bug",
76 | "code"
77 | ]
78 | },
79 | {
80 | "login": "deificx",
81 | "name": "Even Alander",
82 | "avatar_url": "https://avatars3.githubusercontent.com/u/1771462?v=4",
83 | "profile": "https://alander.org",
84 | "contributions": [
85 | "code",
86 | "test",
87 | "ideas"
88 | ]
89 | },
90 | {
91 | "login": "inkblotty",
92 | "name": "Katie Foster",
93 | "avatar_url": "https://avatars3.githubusercontent.com/u/14206003?v=4",
94 | "profile": "http://inkblotty.github.io",
95 | "contributions": [
96 | "code",
97 | "test",
98 | "doc"
99 | ]
100 | },
101 | {
102 | "login": "ccortezaguilera",
103 | "name": "Carlos Aguilera",
104 | "avatar_url": "https://avatars3.githubusercontent.com/u/10718803?v=4",
105 | "profile": "https://github.com/ccortezaguilera",
106 | "contributions": [
107 | "code"
108 | ]
109 | },
110 | {
111 | "login": "craigrmccown",
112 | "name": "Craig McCown",
113 | "avatar_url": "https://avatars1.githubusercontent.com/u/2373979?v=4",
114 | "profile": "https://github.com/craigrmccown",
115 | "contributions": [
116 | "ideas",
117 | "code",
118 | "test",
119 | "doc"
120 | ]
121 | },
122 | {
123 | "login": "capsuleman",
124 | "name": "Guillaume Vagner",
125 | "avatar_url": "https://avatars.githubusercontent.com/u/34281913?v=4",
126 | "profile": "https://github.com/capsuleman",
127 | "contributions": [
128 | "code",
129 | "test",
130 | "bug"
131 | ]
132 | },
133 | {
134 | "login": "srmagura",
135 | "name": "Sam Magura",
136 | "avatar_url": "https://avatars.githubusercontent.com/u/801549?v=4",
137 | "profile": "https://dev.to/srmagura",
138 | "contributions": [
139 | "code",
140 | "test"
141 | ]
142 | },
143 | {
144 | "login": "MichaelGregory",
145 | "name": "Mike Gregory",
146 | "avatar_url": "https://avatars.githubusercontent.com/u/1435960?v=4",
147 | "profile": "https://github.com/MichaelGregory",
148 | "contributions": [
149 | "bug",
150 | "code",
151 | "test"
152 | ]
153 | }
154 | ],
155 | "contributorsPerLine": 7,
156 | "projectName": "typed-scss-modules",
157 | "projectOwner": "skovy",
158 | "repoType": "github",
159 | "repoHost": "https://github.com",
160 | "commitConvention": "angular",
161 | "skipCi": false
162 | }
163 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | root: true,
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:jest-formatting/strict",
8 | "plugin:jest/recommended",
9 | ],
10 | parser: "@typescript-eslint/parser",
11 | plugins: ["@typescript-eslint", "promise", "jest", "jest-formatting"],
12 | ignorePatterns: ["dist/**"],
13 | overrides: [
14 | {
15 | files: ["*.ts", "*.tsx"],
16 | extends: [
17 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
18 | ],
19 | parserOptions: {
20 | project: ["./tsconfig.json"],
21 | },
22 | },
23 | ],
24 | rules: {
25 | "promise/prefer-await-to-then": "error",
26 | "jest/consistent-test-it": ["error", { fn: "it" }],
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Your issue may already be reported!
2 | Please search on the [issue tracker](../) before creating one.
3 |
4 | ## Expected Behavior
5 |
6 |
7 |
8 |
9 | ## Current Behavior
10 |
11 |
12 |
13 |
14 | ## Possible Solution
15 |
16 |
17 |
18 |
19 | ## Steps to Reproduce (for bugs)
20 |
21 |
22 |
23 |
24 | 1. 2. 3. 4.
25 |
26 | ## Context
27 |
28 |
29 |
30 |
31 | ## Your Environment
32 |
33 |
34 |
35 | - Version used:
36 | - Operating System and versions:
37 | - Link to your project:
38 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | branches:
5 | - "master"
6 | - "alpha"
7 | pull_request:
8 | branches:
9 | - "**"
10 |
11 | jobs:
12 | release:
13 | name: Test and Release
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 | with:
19 | fetch-depth: 0
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: "16"
24 | - name: Install dependencies
25 | run: npm ci
26 | - name: "Unit tests"
27 | run: npm test
28 | - name: "Reset changes from integration unit tests"
29 | run: git reset --hard HEAD
30 | - name: "Build"
31 | run: npm run build
32 | - name: "Type check"
33 | run: npm run check-types
34 | - name: "Code linting check"
35 | run: npm run check-linting
36 | - name: "Code formatting check"
37 | run: npm run check-formatting
38 | - name: "Commit formatting check"
39 | uses: wagoid/commitlint-github-action@v5
40 | - name: Release
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
44 | run: npm run semantic-release
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | dist
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-organize-imports"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Spencer Miskoviak
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🎁 typed-scss-modules
2 |
3 | [](https://www.npmjs.com/package/typed-scss-modules)
4 |
5 | Generate TypeScript definitions (`.d.ts`) files for CSS Modules that are written in SCSS (`.scss`). Check out [this post to learn more](https://skovy.dev/generating-typescript-definitions-for-css-modules-using-sass/) about the rationale and inspiration behind this package.
6 |
7 | 
8 |
9 | For example, given the following SCSS:
10 |
11 | ```scss
12 | @import "variables";
13 |
14 | .text {
15 | color: $blue;
16 |
17 | &-highlighted {
18 | color: $yellow;
19 | }
20 | }
21 | ```
22 |
23 | The following type definitions will be generated:
24 |
25 | ```typescript
26 | export declare const text: string;
27 | export declare const textHighlighted: string;
28 | ```
29 |
30 | ## Basic Usage
31 |
32 | Install and run as a `devDependency`:
33 |
34 | ```bash
35 | yarn add -D typed-scss-modules
36 | yarn typed-scss-modules src
37 | ```
38 |
39 | Or, install globally:
40 |
41 | ```bash
42 | yarn global add typed-scss-modules
43 | typed-scss-modules src
44 | ```
45 |
46 | Or, with npm:
47 |
48 | ```bash
49 | npm install -D typed-scss-modules
50 | npx typed-scss-modules src
51 | ```
52 |
53 | ## CLI Options
54 |
55 | For all possible commands, run `typed-scss-modules --help`.
56 |
57 | The only required argument is the directory where all SCSS files are located. Running `typed-scss-modules src` will search for all files matching `src/**/*.scss`. This can be overridden by providing a [glob](https://github.com/isaacs/node-glob#glob-primer) pattern instead of a directory. For example, `typed-scss-modules src/*.scss`
58 |
59 | ### `--watch` (`-w`)
60 |
61 | - **Type**: `boolean`
62 | - **Default**: `false`
63 | - **Example**: `typed-scss-modules src --watch`
64 |
65 | Watch for files that get added or are changed and generate the corresponding type definitions.
66 |
67 | ### `--ignoreInitial`
68 |
69 | - **Type**: `boolean`
70 | - **Default**: `false`
71 | - **Example**: `typed-scss-modules src --watch --ignoreInitial`
72 |
73 | Skips the initial build when passing the watch flag. Use this when running concurrently with another watch, but the initial build should happen first. You would run without watch first, then start off the concurrent runs after.
74 |
75 | ### `--ignore`
76 |
77 | - **Type**: `string[]`
78 | - **Default**: `[]`
79 | - **Example**: `typed-scss-modules src --watch --ignore "**/secret.scss"`
80 |
81 | A pattern or an array of glob patterns to exclude files that match and avoid generating type definitions.
82 |
83 | ### `--includePaths` (`-i`)
84 |
85 | - **Type**: `string[]`
86 | - **Default**: `[]`
87 | - **Example**: `typed-scss-modules src --includePaths src/core`
88 |
89 | An array of paths to look in to attempt to resolve your `@import` declarations. This example will search the `src/core` directory when resolving imports.
90 |
91 | ### `--implementation`
92 |
93 | - **Type**: `"node-sass" | "sass"`
94 | - **Default**: If an option is passed, it will always use the provided package implementation. If an option is not passed, it will first check if `node-sass` is installed. If it is, it will be used. Otherwise, it will then check if `sass` is installed. If it is, it will be used. Finally, falling back to `node-sass` if all checks and validations fail.
95 | - **Example**: `typed-scss-modules src --implementation sass`
96 |
97 | ### `--aliases` (`-a`)
98 |
99 | - **Type**: `object`
100 | - **Default**: `{}`
101 | - **Example**: `typed-scss-modules src --aliases.~some-alias src/core/variables`
102 |
103 | An object of aliases to map to their corresponding paths. This example will replace any `@import '~alias'` with `@import 'src/core/variables'`.
104 |
105 | ### `--aliasPrefixes` (`-p`)
106 |
107 | - **Type**: `object`
108 | - **Default**: `{}`
109 | - **Example**: `typed-scss-modules src --aliasPrefixes.~ node_modules/`
110 |
111 | An object of prefix strings to replace with their corresponding paths. This example will replace any `@import '~bootstrap/lib/bootstrap'` with `@import 'node_modules/bootstrap/lib/bootstrap'`.
112 | This matches the common use-case for importing scss files from node_modules when `sass-loader` will be used with `webpack` to compile the project.
113 |
114 | ### `--nameFormat` (`-n`)
115 |
116 | - **Type**: `"all" | "camel" | "kebab" | "param" | "snake" | "dashes" | "none"`
117 | - **Default**: `"camel"`
118 | - **Examples**:
119 | - `typed-scss-modules src --nameFormat camel`
120 | - `typed-scss-modules src --nameFormat kebab --nameFormat dashes --exportType default`. In order to use multiple formatters, you must use `--exportType default`.
121 |
122 | The class naming format to use when converting the classes to type definitions.
123 |
124 | - **all**: makes use of all formatters (except `all` and `none`) and converts all class names to their respective formats, with no duplication. In order to use this option, you must use `--exportType default`.
125 | - **camel**: convert all class names to camel-case, e.g. `App-Logo` => `appLogo`.
126 | - **kebab**/**param**: convert all class names to kebab/param case, e.g. `App-Logo` => `app-logo` (all lower case with '-' separators).
127 | - **dashes**: only convert class names containing dashes to camel-case, leave others alone, e.g. `App` => `App`, `App-Logo` => `appLogo`. Matches the webpack [css-loader camelCase 'dashesOnly'](https://github.com/webpack-contrib/css-loader#camelcase) option.
128 | - **snake**: convert all class names to lower case with underscores between words.
129 | - **none**: do not modify the given class names (you should use `--exportType default` when using `--nameFormat none` as any classes with a `-` in them are invalid as normal variable names).
130 | Note: If you are using create-react-app v2.x and have NOT ejected, `--nameFormat none --exportType default` matches the class names that are generated in CRA's webpack's config.
131 |
132 | ### `--listDifferent` (`-l`)
133 |
134 | - **Type**: `boolean`
135 | - **Default**: `false`
136 | - **Example**: `typed-scss-modules src --listDifferent`
137 |
138 | List any type definition files that are different than those that would be generated. If any are different, exit with a status code `1`.
139 |
140 | ### `--exportType` (`-e`)
141 |
142 | - **Type**: `"named" | "default"`
143 | - **Default**: `"named"`
144 | - **Example**: `typed-scss-modules src --exportType default`
145 |
146 | The export type to use when generating type definitions.
147 |
148 | #### `named`
149 |
150 | Given the following SCSS:
151 |
152 | ```scss
153 | .text {
154 | color: blue;
155 |
156 | &-highlighted {
157 | color: yellow;
158 | }
159 | }
160 | ```
161 |
162 | The following type definitions will be generated:
163 |
164 | ```typescript
165 | export declare const text: string;
166 | export declare const textHighlighted: string;
167 | ```
168 |
169 | #### `default`
170 |
171 | Given the following SCSS:
172 |
173 | ```scss
174 | .text {
175 | color: blue;
176 |
177 | &-highlighted {
178 | color: yellow;
179 | }
180 | }
181 | ```
182 |
183 | The following type definitions will be generated:
184 |
185 | ```typescript
186 | export type Styles = {
187 | text: string;
188 | textHighlighted: string;
189 | };
190 |
191 | export type ClassNames = keyof Styles;
192 |
193 | declare const styles: Styles;
194 |
195 | export default styles;
196 | ```
197 |
198 | This export type is useful when using kebab (param) cased class names since variables with a `-` are not valid variables and will produce invalid types or when a class name is a TypeScript keyword (eg: `while` or `delete`). Additionally, the `Styles` and `ClassNames` types are exported which can be useful for properly typing variables, functions, etc. when working with dynamic class names.
199 |
200 | ### `--exportTypeName`
201 |
202 | - **Type**: `string`
203 | - **Default**: `"ClassNames"`
204 | - **Example**: `typed-scss-modules src --exportType default --exportTypeName ClassesType`
205 |
206 | Customize the type name exported in the generated file when `--exportType` is set to `"default"`.
207 | Only default exports are affected by this command. This example will change the export type line to:
208 |
209 | ```typescript
210 | export type ClassesType = keyof Styles;
211 | ```
212 |
213 | ### `--exportTypeInterface`
214 |
215 | - **Type**: `string`
216 | - **Default**: `"Styles"`
217 | - **Example**: `typed-scss-modules src --exportType default --exportTypeInterface IStyles`
218 |
219 | Customize the interface name exported in the generated file when `--exportType` is set to `"default"`.
220 | Only default exports are affected by this command. This example will change the export interface line to:
221 |
222 | ```typescript
223 | export type IStyles = {
224 | // ...
225 | };
226 | ```
227 |
228 | ### `--quoteType` (`-q`)
229 |
230 | - **Type**: `"single" | "double"`
231 | - **Default**: `"single"`
232 | - **Example**: `typed-scss-modules src --exportType default --quoteType double`
233 |
234 | Specify a quote type to match your TypeScript configuration. Only default exports are affected by this command. This example will wrap class names with double quotes ("). If [Prettier](https://prettier.io) is installed and configured in the project, it will be used and is likely to override the effect of this setting.
235 |
236 | ### `--updateStaleOnly` (`-u`)
237 |
238 | - **Type**: `boolean`
239 | - **Default**: `false`
240 | - **Example**: `typed-scss-modules src --updateStaleOnly`
241 |
242 | Overwrite generated files only if the source file has more recent changes. This can be useful if you want to avoid extraneous file updates, which can cause watcher processes to trigger unnecessarily (e.g. `tsc --watch`). This is done by first checking if the generated file was modified more recently than the source file, and secondly by comparing the existing file contents to the generated file contents.
243 |
244 | Caveat: If a generated type definition file is updated manually, it won't be re-generated until the corresponding scss file is also updated.
245 |
246 | ### `--logLevel` (`-L`)
247 |
248 | - **Type**: `"verbose" | "error" | "info" | "silent"`
249 | - **Default**: `"verbose"`
250 | - **Example**: `typed-scss-modules src --logLevel error`
251 |
252 | Sets verbosity level of console output.
253 |
254 | #### `verbose`
255 |
256 | Print all messages
257 |
258 | #### `error`
259 |
260 | Print only errors
261 |
262 | #### `info`
263 |
264 | Print only some messages
265 |
266 | #### `silent`
267 |
268 | Print nothing
269 |
270 | ### `--banner`
271 |
272 | - **Type**: `string`
273 | - **Default**: `undefined`
274 | - **Example**: `typed-scss-modules src --banner '// This is an example banner\n'`
275 |
276 | Will prepend a string to the top of your output files
277 |
278 | ```typescript
279 | // This is an example banner
280 | export type Styles = {
281 | // ...
282 | };
283 | ```
284 |
285 | ### `--outputFolder` (`-o`)
286 |
287 | - **Type**: `string`
288 | - **Default**: _none_
289 | - **Example**: `typed-scss-modules src --outputFolder __generated__`
290 |
291 | Set a relative folder to output the generated type definitions. Instead of writing the type definitions directly next to each SCSS module (sibling file), it will write to the output folder with the same path.
292 |
293 | It will use the relative path to the SCSS module from where this tool is executed. This same path (including any directories) will be constructed in the output folder. This is important for this to work properly with TypeScript.
294 |
295 | **Important**: for this to work as expected the `tsconfig.json` needs to have [`rootDirs`](https://www.typescriptlang.org/tsconfig#rootDirs) added with the same output folder. This will allow TypeScript to pick up these type definitions and map them to the actual SCSS modules.
296 |
297 | ```json
298 | {
299 | "compilerOptions": {
300 | "rootDirs": [".", "__generated__"]
301 | }
302 | }
303 | ```
304 |
305 | ### `--additionalData` (`-d`)
306 |
307 | - **Type**: `string`
308 | - **Default**: _none_
309 | - **Example**: `typed-scss-modules src --additionalData '$global-var: green;'`
310 |
311 | Prepend the provided SCSS code before each file. This is useful for injecting globals into every file, such as adding an import to load global variables for each file.
312 |
313 | ## Config options
314 |
315 | All options above are also supported as a configuration file in the root of the project. The following configuration file names are supported:
316 |
317 | - `typed-scss-modules.config.ts`
318 | - `typed-scss-modules.config.js`
319 |
320 | The file can provide either a named `config` export or a default export.
321 |
322 | ```js
323 | // Example of a named export with some of the options sets.
324 | export const config = {
325 | banner: "// customer banner",
326 | exportType: "default",
327 | exportTypeName: "TheClasses",
328 | logLevel: "error",
329 | };
330 |
331 | // Example of a default export with some of the options sets.
332 | export default {
333 | banner: "// customer banner",
334 | exportType: "default",
335 | exportTypeName: "TheClasses",
336 | logLevel: "error",
337 | };
338 | ```
339 |
340 | > Note: the configuration options are the same as the CLI options without the leading dashes (`--`). Only the full option name is supported (not aliases) in the configuration file.
341 |
342 | CLI options will take precedence over configuration file options.
343 |
344 | In addition to all CLI options, the following are options only available with the configuration file:
345 |
346 | ### `importer`
347 |
348 | - **Type**: `Importer | Importer[]`
349 | - **Default**: _none_
350 |
351 | Define a [single custom SASS importer or an array of SASS importers](https://github.com/sass/sass/blob/f355f602fc15f55b0a0a795ebe6eb819963e08a5/js-api-doc/legacy/importer.d.ts#L51-L149). This should only be necessary if custom SASS importers are already being used in the build process. This is used internally to implement `aliases` and `aliasPrefixes`.
352 |
353 | Refer to [`lib/sass/importer.ts`](/blob/master/lib/sass/importer.ts) for more details and the `node-sass` and `sass` importer type definitions.
354 |
355 | ### `--allowArbitraryExtensions`
356 |
357 | - **Type**: `boolean`
358 | - **Default**: `false`
359 | - **Example**: `typed-scss-modules src --allowArbitraryExtensions`
360 |
361 | Output filenames that will be compatible with the "arbitrary file extensions" feature that was introduced in TypeScript 5.0. See [the docs](https://www.typescriptlang.org/tsconfig#allowArbitraryExtensions) for more info.
362 |
363 | In essence, the `*.scss.d.ts` extension now becomes `*.d.scss.ts` so that you can import SCSS modules in projects using ESM module resolution.
364 |
365 | ## Examples
366 |
367 | For examples of how this tool can be used and configured, see the `examples` directory:
368 |
369 | - [Basic example](/examples/basic)
370 | - [Default export example](/examples/default-export)
371 | - [Config file (with custom importer) example](/examples/config-file)
372 |
373 | ## Contributors ✨
374 |
375 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
376 |
377 |
378 |
379 |
380 |
402 |
403 |
404 |
405 |
406 |
407 |
408 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
409 |
410 | ## Alternatives
411 |
412 | This package was heavily influenced on [typed-css-modules](https://github.com/Quramy/typed-css-modules) which generates TypeScript definitions (`.d.ts`) files for CSS Modules that are written in CSS (`.css`).
413 |
414 | This package is currently used as a CLI. There are also [packages that generate types as a webpack loader](https://github.com/Jimdo/typings-for-css-modules-loader).
415 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/main.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`node-sass implementation main outputs the correct files when outputFolder is passed 1`] = `
4 | [
5 | {
6 | "contents": "export declare const myCustomClass: string;
7 | export declare const nestedAnother: string;
8 | export declare const nestedClass: string;
9 | export declare const nestedStyles: string;
10 | export declare const number1: string;
11 | export declare const someStyles: string;
12 | export declare const whereSelector: string;
13 | ",
14 | "path": "../__generated__/__tests__/dummy-styles/alias-prefixes.scss.d.ts",
15 | },
16 | {
17 | "contents": "export declare const myCustomClass: string;
18 | export declare const nestedAnother: string;
19 | export declare const nestedClass: string;
20 | export declare const number1: string;
21 | export declare const someClass: string;
22 | export declare const someStyles: string;
23 | export declare const whereSelector: string;
24 | ",
25 | "path": "../__generated__/__tests__/dummy-styles/aliases.scss.d.ts",
26 | },
27 | {
28 | "contents": "export declare const nestedAnother: string;
29 | export declare const nestedClass: string;
30 | export declare const number1: string;
31 | export declare const someStyles: string;
32 | export declare const whereSelector: string;
33 | ",
34 | "path": "../__generated__/__tests__/dummy-styles/complex.scss.d.ts",
35 | },
36 | {
37 | "contents": "export declare const composedClass: string;
38 | ",
39 | "path": "../__generated__/__tests__/dummy-styles/composes.scss.d.ts",
40 | },
41 | {
42 | "contents": "export declare const app: string;
43 | export declare const appHeader: string;
44 | export declare const logo: string;
45 | ",
46 | "path": "../__generated__/__tests__/dummy-styles/dashes.scss.d.ts",
47 | },
48 | {
49 | "contents": "export declare const globalStyle: string;
50 | ",
51 | "path": "../__generated__/__tests__/dummy-styles/global-variables.scss.d.ts",
52 | },
53 | {
54 | "contents": "export declare const randomClass: string;
55 | ",
56 | "path": "../__generated__/__tests__/dummy-styles/invalid.scss.d.ts",
57 | },
58 | {
59 | "contents": "export declare const nestedStyles: string;
60 | ",
61 | "path": "../__generated__/__tests__/dummy-styles/nested-styles/style.scss.d.ts",
62 | },
63 | {
64 | "contents": "export declare const someClass: string;
65 | ",
66 | "path": "../__generated__/__tests__/dummy-styles/style.scss.d.ts",
67 | },
68 | ]
69 | `;
70 |
71 | exports[`node-sass implementation main reads options from the configuration file 1`] = `
72 | [
73 | {
74 | "contents": "export type Styles = {
75 | myCustomClass: string;
76 | nestedAnother: string;
77 | nestedClass: string;
78 | nestedStyles: string;
79 | number1: string;
80 | someStyles: string;
81 | whereSelector: string;
82 | };
83 |
84 | export type ClassNames = keyof Styles;
85 |
86 | declare const styles: Styles;
87 |
88 | export default styles;
89 | ",
90 | "path": "dummy-styles/alias-prefixes.scss.d.ts",
91 | },
92 | {
93 | "contents": "export type Styles = {
94 | myCustomClass: string;
95 | nestedAnother: string;
96 | nestedClass: string;
97 | number1: string;
98 | someClass: string;
99 | someStyles: string;
100 | whereSelector: string;
101 | };
102 |
103 | export type ClassNames = keyof Styles;
104 |
105 | declare const styles: Styles;
106 |
107 | export default styles;
108 | ",
109 | "path": "dummy-styles/aliases.scss.d.ts",
110 | },
111 | {
112 | "contents": "export type Styles = {
113 | nestedAnother: string;
114 | nestedClass: string;
115 | number1: string;
116 | someStyles: string;
117 | whereSelector: string;
118 | };
119 |
120 | export type ClassNames = keyof Styles;
121 |
122 | declare const styles: Styles;
123 |
124 | export default styles;
125 | ",
126 | "path": "dummy-styles/complex.scss.d.ts",
127 | },
128 | {
129 | "contents": "export type Styles = {
130 | composedClass: string;
131 | };
132 |
133 | export type ClassNames = keyof Styles;
134 |
135 | declare const styles: Styles;
136 |
137 | export default styles;
138 | ",
139 | "path": "dummy-styles/composes.scss.d.ts",
140 | },
141 | {
142 | "contents": "export type Styles = {
143 | app: string;
144 | appHeader: string;
145 | logo: string;
146 | };
147 |
148 | export type ClassNames = keyof Styles;
149 |
150 | declare const styles: Styles;
151 |
152 | export default styles;
153 | ",
154 | "path": "dummy-styles/dashes.scss.d.ts",
155 | },
156 | {
157 | "contents": "export type Styles = {
158 | globalStyle: string;
159 | };
160 |
161 | export type ClassNames = keyof Styles;
162 |
163 | declare const styles: Styles;
164 |
165 | export default styles;
166 | ",
167 | "path": "dummy-styles/global-variables.scss.d.ts",
168 | },
169 | {
170 | "contents": "export type Styles = {
171 | randomClass: string;
172 | };
173 |
174 | export type ClassNames = keyof Styles;
175 |
176 | declare const styles: Styles;
177 |
178 | export default styles;
179 | ",
180 | "path": "dummy-styles/invalid.scss.d.ts",
181 | },
182 | {
183 | "contents": "export type Styles = {
184 | nestedStyles: string;
185 | };
186 |
187 | export type ClassNames = keyof Styles;
188 |
189 | declare const styles: Styles;
190 |
191 | export default styles;
192 | ",
193 | "path": "dummy-styles/nested-styles/style.scss.d.ts",
194 | },
195 | {
196 | "contents": "export type Styles = {
197 | someClass: string;
198 | };
199 |
200 | export type ClassNames = keyof Styles;
201 |
202 | declare const styles: Styles;
203 |
204 | export default styles;
205 | ",
206 | "path": "dummy-styles/style.scss.d.ts",
207 | },
208 | ]
209 | `;
210 |
211 | exports[`sass implementation main outputs the correct files when outputFolder is passed 1`] = `
212 | [
213 | {
214 | "contents": "export declare const myCustomClass: string;
215 | export declare const nestedAnother: string;
216 | export declare const nestedClass: string;
217 | export declare const nestedStyles: string;
218 | export declare const number1: string;
219 | export declare const someStyles: string;
220 | export declare const whereSelector: string;
221 | ",
222 | "path": "../__generated__/__tests__/dummy-styles/alias-prefixes.scss.d.ts",
223 | },
224 | {
225 | "contents": "export declare const myCustomClass: string;
226 | export declare const nestedAnother: string;
227 | export declare const nestedClass: string;
228 | export declare const number1: string;
229 | export declare const someClass: string;
230 | export declare const someStyles: string;
231 | export declare const whereSelector: string;
232 | ",
233 | "path": "../__generated__/__tests__/dummy-styles/aliases.scss.d.ts",
234 | },
235 | {
236 | "contents": "export declare const nestedAnother: string;
237 | export declare const nestedClass: string;
238 | export declare const number1: string;
239 | export declare const someStyles: string;
240 | export declare const whereSelector: string;
241 | ",
242 | "path": "../__generated__/__tests__/dummy-styles/complex.scss.d.ts",
243 | },
244 | {
245 | "contents": "export declare const composedClass: string;
246 | ",
247 | "path": "../__generated__/__tests__/dummy-styles/composes.scss.d.ts",
248 | },
249 | {
250 | "contents": "export declare const app: string;
251 | export declare const appHeader: string;
252 | export declare const logo: string;
253 | ",
254 | "path": "../__generated__/__tests__/dummy-styles/dashes.scss.d.ts",
255 | },
256 | {
257 | "contents": "export declare const globalStyle: string;
258 | ",
259 | "path": "../__generated__/__tests__/dummy-styles/global-variables.scss.d.ts",
260 | },
261 | {
262 | "contents": "export declare const randomClass: string;
263 | ",
264 | "path": "../__generated__/__tests__/dummy-styles/invalid.scss.d.ts",
265 | },
266 | {
267 | "contents": "export declare const nestedStyles: string;
268 | ",
269 | "path": "../__generated__/__tests__/dummy-styles/nested-styles/style.scss.d.ts",
270 | },
271 | {
272 | "contents": "export declare const someClass: string;
273 | ",
274 | "path": "../__generated__/__tests__/dummy-styles/style.scss.d.ts",
275 | },
276 | ]
277 | `;
278 |
279 | exports[`sass implementation main reads options from the configuration file 1`] = `
280 | [
281 | {
282 | "contents": "export type Styles = {
283 | myCustomClass: string;
284 | nestedAnother: string;
285 | nestedClass: string;
286 | nestedStyles: string;
287 | number1: string;
288 | someStyles: string;
289 | whereSelector: string;
290 | };
291 |
292 | export type ClassNames = keyof Styles;
293 |
294 | declare const styles: Styles;
295 |
296 | export default styles;
297 | ",
298 | "path": "dummy-styles/alias-prefixes.scss.d.ts",
299 | },
300 | {
301 | "contents": "export type Styles = {
302 | myCustomClass: string;
303 | nestedAnother: string;
304 | nestedClass: string;
305 | number1: string;
306 | someClass: string;
307 | someStyles: string;
308 | whereSelector: string;
309 | };
310 |
311 | export type ClassNames = keyof Styles;
312 |
313 | declare const styles: Styles;
314 |
315 | export default styles;
316 | ",
317 | "path": "dummy-styles/aliases.scss.d.ts",
318 | },
319 | {
320 | "contents": "export type Styles = {
321 | nestedAnother: string;
322 | nestedClass: string;
323 | number1: string;
324 | someStyles: string;
325 | whereSelector: string;
326 | };
327 |
328 | export type ClassNames = keyof Styles;
329 |
330 | declare const styles: Styles;
331 |
332 | export default styles;
333 | ",
334 | "path": "dummy-styles/complex.scss.d.ts",
335 | },
336 | {
337 | "contents": "export type Styles = {
338 | composedClass: string;
339 | };
340 |
341 | export type ClassNames = keyof Styles;
342 |
343 | declare const styles: Styles;
344 |
345 | export default styles;
346 | ",
347 | "path": "dummy-styles/composes.scss.d.ts",
348 | },
349 | {
350 | "contents": "export type Styles = {
351 | app: string;
352 | appHeader: string;
353 | logo: string;
354 | };
355 |
356 | export type ClassNames = keyof Styles;
357 |
358 | declare const styles: Styles;
359 |
360 | export default styles;
361 | ",
362 | "path": "dummy-styles/dashes.scss.d.ts",
363 | },
364 | {
365 | "contents": "export type Styles = {
366 | globalStyle: string;
367 | };
368 |
369 | export type ClassNames = keyof Styles;
370 |
371 | declare const styles: Styles;
372 |
373 | export default styles;
374 | ",
375 | "path": "dummy-styles/global-variables.scss.d.ts",
376 | },
377 | {
378 | "contents": "export type Styles = {
379 | randomClass: string;
380 | };
381 |
382 | export type ClassNames = keyof Styles;
383 |
384 | declare const styles: Styles;
385 |
386 | export default styles;
387 | ",
388 | "path": "dummy-styles/invalid.scss.d.ts",
389 | },
390 | {
391 | "contents": "export type Styles = {
392 | nestedStyles: string;
393 | };
394 |
395 | export type ClassNames = keyof Styles;
396 |
397 | declare const styles: Styles;
398 |
399 | export default styles;
400 | ",
401 | "path": "dummy-styles/nested-styles/style.scss.d.ts",
402 | },
403 | {
404 | "contents": "export type Styles = {
405 | someClass: string;
406 | };
407 |
408 | export type ClassNames = keyof Styles;
409 |
410 | declare const styles: Styles;
411 |
412 | export default styles;
413 | ",
414 | "path": "dummy-styles/style.scss.d.ts",
415 | },
416 | ]
417 | `;
418 |
--------------------------------------------------------------------------------
/__tests__/cli.test.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "child_process";
2 |
3 | describe("cli", () => {
4 | it("should run when no files are found", () => {
5 | const result = execSync("npm run typed-scss-modules src").toString();
6 |
7 | expect(result).toContain("No files found.");
8 | });
9 |
10 | describe("examples", () => {
11 | it("should run the basic example without errors", () => {
12 | const result = execSync(
13 | `npm run typed-scss-modules "examples/basic/**/*.scss" -- --includePaths examples/basic/core --aliases.~alias variables --banner '// example banner'`
14 | ).toString();
15 |
16 | expect(result).toContain("Found 3 files. Generating type definitions...");
17 | });
18 |
19 | it("should run the default-export example without errors", () => {
20 | const result = execSync(
21 | `npm run typed-scss-modules "examples/default-export/**/*.scss" -- --exportType default --nameFormat kebab --banner '// example banner'`
22 | ).toString();
23 |
24 | expect(result).toContain("Found 1 file. Generating type definitions...");
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/__tests__/configs/js-default-export/typed-scss-modules.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | banner: "// js-default-export",
3 | };
4 |
--------------------------------------------------------------------------------
/__tests__/configs/js-module-exports/typed-scss-modules.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | banner: "// js-module-exports",
4 | };
5 |
--------------------------------------------------------------------------------
/__tests__/configs/js-named-export/typed-scss-modules.config.js:
--------------------------------------------------------------------------------
1 | export const config = {
2 | banner: "// js-named-export",
3 | };
4 |
--------------------------------------------------------------------------------
/__tests__/configs/ts-default-export/typed-scss-modules.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | banner: "// ts-default-export",
3 | };
4 |
--------------------------------------------------------------------------------
/__tests__/configs/ts-named-export/typed-scss-modules.config.ts:
--------------------------------------------------------------------------------
1 | export const config = {
2 | banner: "// ts-named-export",
3 | };
4 |
--------------------------------------------------------------------------------
/__tests__/core/alerts.test.ts:
--------------------------------------------------------------------------------
1 | import { alerts, setAlertsLogLevel } from "../../lib/core";
2 |
3 | describe("alerts", () => {
4 | let logSpy: jest.SpyInstance;
5 |
6 | beforeEach(() => {
7 | logSpy = jest.spyOn(console, "log").mockImplementation();
8 | });
9 |
10 | afterEach(() => {
11 | logSpy.mockRestore();
12 | });
13 |
14 | const TEST_ALERT_MSG = "TEST ALERT MESSAGE";
15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
16 | const EXPECTED = expect.stringContaining(TEST_ALERT_MSG);
17 |
18 | it("should print all messages with verbose log level", () => {
19 | setAlertsLogLevel("verbose");
20 |
21 | alerts.error(TEST_ALERT_MSG);
22 |
23 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
24 | //make sure each alert only calls console.log once
25 | expect(console.log).toHaveBeenCalledTimes(1);
26 |
27 | alerts.warn(TEST_ALERT_MSG);
28 |
29 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
30 | expect(console.log).toHaveBeenCalledTimes(2);
31 |
32 | alerts.notice(TEST_ALERT_MSG);
33 |
34 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
35 | expect(console.log).toHaveBeenCalledTimes(3);
36 |
37 | alerts.info(TEST_ALERT_MSG);
38 |
39 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
40 | expect(console.log).toHaveBeenCalledTimes(4);
41 |
42 | alerts.success(TEST_ALERT_MSG);
43 |
44 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
45 | expect(console.log).toHaveBeenCalledTimes(5);
46 | });
47 |
48 | it("should only print error messages with error log level", () => {
49 | setAlertsLogLevel("error");
50 |
51 | alerts.error(TEST_ALERT_MSG);
52 |
53 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
54 | expect(console.log).toHaveBeenCalledTimes(1);
55 |
56 | alerts.warn(TEST_ALERT_MSG);
57 | alerts.notice(TEST_ALERT_MSG);
58 | alerts.info(TEST_ALERT_MSG);
59 | alerts.success(TEST_ALERT_MSG);
60 |
61 | //shouldn't change
62 | expect(console.log).toHaveBeenCalledTimes(1);
63 | });
64 |
65 | it("should print all but warning messages with info log level", () => {
66 | setAlertsLogLevel("info");
67 |
68 | alerts.error(TEST_ALERT_MSG);
69 |
70 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
71 | expect(console.log).toHaveBeenCalledTimes(1);
72 |
73 | alerts.notice(TEST_ALERT_MSG);
74 |
75 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
76 | expect(console.log).toHaveBeenCalledTimes(2);
77 |
78 | alerts.info(TEST_ALERT_MSG);
79 |
80 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
81 | expect(console.log).toHaveBeenCalledTimes(3);
82 |
83 | alerts.success(TEST_ALERT_MSG);
84 |
85 | expect(console.log).toHaveBeenLastCalledWith(EXPECTED);
86 | expect(console.log).toHaveBeenCalledTimes(4);
87 |
88 | alerts.warn(TEST_ALERT_MSG);
89 |
90 | expect(console.log).toHaveBeenCalledTimes(4);
91 | });
92 |
93 | it("should print no messages with silent log level", () => {
94 | setAlertsLogLevel("silent");
95 |
96 | alerts.error(TEST_ALERT_MSG);
97 | alerts.warn(TEST_ALERT_MSG);
98 | alerts.notice(TEST_ALERT_MSG);
99 | alerts.info(TEST_ALERT_MSG);
100 | alerts.success(TEST_ALERT_MSG);
101 |
102 | expect(console.log).not.toHaveBeenCalled();
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/__tests__/core/generate.test.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { generate } from "../../lib/core";
3 | import { describeAllImplementations } from "../helpers";
4 |
5 | describeAllImplementations((implementation) => {
6 | describe("generate", () => {
7 | beforeEach(() => {
8 | // Only mock the write, so the example files can still be read.
9 | fs.writeFileSync = jest.fn();
10 | console.log = jest.fn(); // avoid console logs showing up
11 | });
12 |
13 | it("generates types for all files matching the pattern", async () => {
14 | const pattern = `${__dirname}/../dummy-styles/**/*.scss`;
15 |
16 | await generate(pattern, {
17 | banner: "",
18 | watch: false,
19 | ignoreInitial: false,
20 | exportType: "named",
21 | exportTypeName: "ClassNames",
22 | exportTypeInterface: "Styles",
23 | listDifferent: false,
24 | ignore: [],
25 | implementation,
26 | quoteType: "single",
27 | updateStaleOnly: false,
28 | logLevel: "verbose",
29 | outputFolder: null,
30 | allowArbitraryExtensions: false,
31 | });
32 |
33 | expect(fs.writeFileSync).toHaveBeenCalledTimes(6);
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/__tests__/core/list-different.test.ts:
--------------------------------------------------------------------------------
1 | import { listDifferent } from "../../lib/core";
2 | import { describeAllImplementations } from "../helpers";
3 |
4 | describeAllImplementations((implementation) => {
5 | describe("listDifferent", () => {
6 | let exit: jest.SpyInstance;
7 |
8 | beforeEach(() => {
9 | console.log = jest.fn();
10 | exit = jest.spyOn(process, "exit").mockImplementation();
11 | });
12 |
13 | afterEach(() => {
14 | exit.mockRestore();
15 | });
16 |
17 | it("logs invalid type definitions and exits with 1", async () => {
18 | const pattern = `${__dirname}/../**/*.scss`;
19 |
20 | await listDifferent(pattern, {
21 | banner: "",
22 | watch: false,
23 | ignoreInitial: false,
24 | exportType: "named",
25 | exportTypeName: "ClassNames",
26 | exportTypeInterface: "Styles",
27 | listDifferent: true,
28 | aliases: {
29 | "~fancy-import": "complex",
30 | "~another": "style",
31 | },
32 | aliasPrefixes: {
33 | "~": "nested-styles/",
34 | },
35 | ignore: [],
36 | implementation,
37 | quoteType: "single",
38 | updateStaleOnly: false,
39 | logLevel: "verbose",
40 | outputFolder: null,
41 | allowArbitraryExtensions: false,
42 | });
43 |
44 | expect(exit).toHaveBeenCalledWith(1);
45 | expect(console.log).toHaveBeenCalledWith(
46 | expect.stringContaining(`[INVALID TYPES] Check type definitions for`)
47 | );
48 | expect(console.log).toHaveBeenCalledWith(
49 | expect.stringContaining(`invalid.scss`)
50 | );
51 | });
52 |
53 | it("logs nothing and does not exit when formatted using Prettier", async () => {
54 | const pattern = `${__dirname}/list-different/formatted.scss`;
55 |
56 | await listDifferent(pattern, {
57 | banner: "",
58 | watch: false,
59 | ignoreInitial: false,
60 | exportType: "default",
61 | exportTypeName: "ClassNames",
62 | exportTypeInterface: "Styles",
63 | listDifferent: true,
64 | ignore: [],
65 | implementation,
66 | quoteType: "single",
67 | updateStaleOnly: false,
68 | logLevel: "verbose",
69 | nameFormat: ["kebab"],
70 | outputFolder: null,
71 | allowArbitraryExtensions: false,
72 | });
73 |
74 | expect(console.log).toHaveBeenCalledTimes(1);
75 | expect(console.log).toHaveBeenCalledWith(
76 | expect.stringContaining(`Only 1 file found for`)
77 | );
78 | expect(exit).not.toHaveBeenCalled();
79 | });
80 |
81 | it("logs nothing and does not exit if all files are valid", async () => {
82 | const pattern = `${__dirname}/../dummy-styles/**/style.scss`;
83 |
84 | await listDifferent(pattern, {
85 | banner: "",
86 | watch: false,
87 | ignoreInitial: false,
88 | exportType: "named",
89 | exportTypeName: "ClassNames",
90 | exportTypeInterface: "Styles",
91 | listDifferent: true,
92 | ignore: [],
93 | implementation,
94 | quoteType: "single",
95 | updateStaleOnly: false,
96 | logLevel: "verbose",
97 | outputFolder: null,
98 | allowArbitraryExtensions: false,
99 | });
100 |
101 | expect(exit).not.toHaveBeenCalled();
102 | expect(console.log).not.toHaveBeenCalled();
103 | });
104 |
105 | it("logs not generated type file and exits with 1", async () => {
106 | const pattern = `${__dirname}/list-different/no-generated.scss`;
107 |
108 | await listDifferent(pattern, {
109 | banner: "",
110 | watch: false,
111 | ignoreInitial: false,
112 | exportType: "named",
113 | exportTypeName: "ClassNames",
114 | exportTypeInterface: "Styles",
115 | listDifferent: true,
116 | ignore: [],
117 | implementation,
118 | quoteType: "single",
119 | updateStaleOnly: false,
120 | logLevel: "verbose",
121 | outputFolder: null,
122 | allowArbitraryExtensions: false,
123 | });
124 |
125 | expect(exit).toHaveBeenCalledWith(1);
126 | expect(console.log).toHaveBeenCalledWith(
127 | expect.stringContaining(
128 | `[INVALID TYPES] Type file needs to be generated for`
129 | )
130 | );
131 | expect(console.log).toHaveBeenCalledWith(
132 | expect.stringContaining(`no-generated.scss`)
133 | );
134 | });
135 |
136 | it("ignores ignored files", async () => {
137 | const pattern = `${__dirname}/list-different/no-generated.scss`;
138 |
139 | await listDifferent(pattern, {
140 | banner: "",
141 | watch: false,
142 | ignoreInitial: false,
143 | exportType: "named",
144 | exportTypeName: "ClassNames",
145 | exportTypeInterface: "Styles",
146 | listDifferent: true,
147 | ignore: ["**/no-generated.scss"],
148 | implementation,
149 | quoteType: "single",
150 | updateStaleOnly: false,
151 | logLevel: "verbose",
152 | outputFolder: null,
153 | allowArbitraryExtensions: false,
154 | });
155 |
156 | expect(exit).not.toHaveBeenCalled();
157 | expect(console.log).toHaveBeenCalledTimes(1);
158 | expect(console.log).toHaveBeenCalledWith(
159 | expect.stringContaining(`No files found`)
160 | );
161 | });
162 | });
163 | });
164 |
--------------------------------------------------------------------------------
/__tests__/core/list-different/formatted.scss:
--------------------------------------------------------------------------------
1 | .i {
2 | color: orange;
3 |
4 | &-am {
5 | &-kebab {
6 | &-cased {
7 | color: red;
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/__tests__/core/list-different/formatted.scss.d.ts:
--------------------------------------------------------------------------------
1 | export type Styles = {
2 | i: string;
3 | "i-am-kebab-cased": string;
4 | };
5 |
6 | export type ClassNames = keyof Styles;
7 |
8 | declare const styles: Styles;
9 |
10 | export default styles;
11 |
--------------------------------------------------------------------------------
/__tests__/core/list-different/no-generated.scss:
--------------------------------------------------------------------------------
1 | .no-generated {
2 | color: blue;
3 | }
4 |
--------------------------------------------------------------------------------
/__tests__/core/list-files-and-perform-sanity-check.test.ts:
--------------------------------------------------------------------------------
1 | import { ConfigOptions } from "../../lib/core";
2 | import { listFilesAndPerformSanityChecks } from "../../lib/core/list-files-and-perform-sanity-checks";
3 |
4 | const options: ConfigOptions = {
5 | banner: "",
6 | watch: false,
7 | ignoreInitial: false,
8 | exportType: "named",
9 | exportTypeName: "ClassNames",
10 | exportTypeInterface: "Styles",
11 | listDifferent: true,
12 | ignore: [],
13 | implementation: "sass",
14 | quoteType: "single",
15 | updateStaleOnly: false,
16 | logLevel: "verbose",
17 | outputFolder: null,
18 | allowArbitraryExtensions: false,
19 | };
20 |
21 | describe("listAllFilesAndPerformSanityCheck", () => {
22 | beforeEach(() => {
23 | console.log = jest.fn();
24 | });
25 |
26 | it("prints a warning if the pattern matches 0 files", () => {
27 | const pattern = `${__dirname}/list-different/test.txt`;
28 |
29 | listFilesAndPerformSanityChecks(pattern, options);
30 |
31 | expect(console.log).toHaveBeenCalledWith(
32 | expect.stringContaining("No files found.")
33 | );
34 | });
35 |
36 | it("prints a warning if the pattern matches 1 file", () => {
37 | const pattern = `${__dirname}/list-different/formatted.scss`;
38 |
39 | listFilesAndPerformSanityChecks(pattern, options);
40 |
41 | expect(console.log).toHaveBeenCalledWith(
42 | expect.stringContaining("Only 1 file found for")
43 | );
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/__tests__/core/remove-file.test.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import { alerts } from "../../lib/core/alerts";
4 | import { removeSCSSTypeDefinitionFile } from "../../lib/core/remove-file";
5 | import { DEFAULT_OPTIONS } from "../../lib/load";
6 |
7 | describe("removeFile", () => {
8 | const originalTestFile = path.resolve(__dirname, "..", "removable.scss");
9 | const existingFile = path.resolve(__dirname, "..", "style.scss");
10 | const existingTypes = path.join(
11 | process.cwd(),
12 | "__tests__/removable.scss.d.ts"
13 | );
14 | const outputFolderExistingTypes = path.resolve(
15 | process.cwd(),
16 | "__generated__/__tests__/removable.scss.d.ts"
17 | );
18 |
19 | let existsSpy: jest.SpyInstance;
20 | let unlinkSpy: jest.SpyInstance;
21 | let alertsSpy: jest.SpyInstance;
22 |
23 | beforeEach(() => {
24 | existsSpy = jest
25 | .spyOn(fs, "existsSync")
26 | .mockImplementation(
27 | (path) =>
28 | path === existingTypes ||
29 | path === existingFile ||
30 | path === outputFolderExistingTypes
31 | );
32 |
33 | unlinkSpy = jest.spyOn(fs, "unlinkSync").mockImplementation();
34 |
35 | alertsSpy = jest.spyOn(alerts, "success").mockImplementation();
36 | });
37 |
38 | afterEach(() => {
39 | jest.clearAllMocks();
40 | });
41 |
42 | it("does nothing if types file doesn't exist", () => {
43 | const nonExistingFile = path.resolve(__dirname, "..", "deleted.scss");
44 | const nonExistingTypes = path.join(
45 | process.cwd(),
46 | "__tests__/deleted.scss.d.ts"
47 | );
48 |
49 | removeSCSSTypeDefinitionFile(nonExistingFile, DEFAULT_OPTIONS);
50 |
51 | expect(existsSpy).toHaveBeenCalledWith(
52 | expect.stringMatching(nonExistingFile)
53 | );
54 | expect(existsSpy).toHaveBeenCalledWith(
55 | expect.stringMatching(nonExistingTypes)
56 | );
57 | expect(unlinkSpy).not.toHaveBeenCalled();
58 | expect(alertsSpy).not.toHaveBeenCalled();
59 | });
60 |
61 | it("removes *.scss.d.ts types file for *.scss", () => {
62 | removeSCSSTypeDefinitionFile(originalTestFile, DEFAULT_OPTIONS);
63 |
64 | expect(existsSpy).toHaveBeenCalledWith(
65 | expect.stringMatching(existingTypes)
66 | );
67 | expect(unlinkSpy).toHaveBeenCalled();
68 | expect(unlinkSpy).toHaveBeenCalledWith(
69 | expect.stringMatching(existingTypes)
70 | );
71 | expect(alertsSpy).toHaveBeenCalled();
72 | });
73 |
74 | describe("when outputFolder is passed", () => {
75 | it("removes the correct files", () => {
76 | removeSCSSTypeDefinitionFile(originalTestFile, {
77 | ...DEFAULT_OPTIONS,
78 | outputFolder: "__generated__",
79 | });
80 |
81 | expect(existsSpy).toHaveBeenCalledWith(
82 | expect.stringMatching(outputFolderExistingTypes)
83 | );
84 | expect(unlinkSpy).toHaveBeenCalled();
85 | expect(unlinkSpy).toHaveBeenCalledWith(
86 | expect.stringMatching(outputFolderExistingTypes)
87 | );
88 | expect(alertsSpy).toHaveBeenCalled();
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/__tests__/core/write-file.test.ts:
--------------------------------------------------------------------------------
1 | import fs, { PathOrFileDescriptor } from "fs";
2 | import path from "path";
3 | import { writeFile } from "../../lib/core";
4 | import { describeAllImplementations } from "../helpers";
5 |
6 | describeAllImplementations((implementation) => {
7 | describe("writeFile", () => {
8 | beforeEach(() => {
9 | // Only mock the write, so the example files can still be read.
10 | jest.spyOn(fs, "writeFileSync").mockImplementation();
11 |
12 | // Avoid creating new directories while running tests.
13 | jest.spyOn(fs, "mkdirSync").mockImplementation();
14 |
15 | // Test removing existing types.
16 | jest.spyOn(fs, "unlinkSync").mockImplementation();
17 |
18 | console.log = jest.fn();
19 | });
20 |
21 | it("writes the corresponding type definitions for a file and logs", async () => {
22 | const testFile = path.resolve(__dirname, "..", "dummy-styles/style.scss");
23 |
24 | await writeFile(testFile, {
25 | banner: "",
26 | watch: false,
27 | ignoreInitial: false,
28 | exportType: "named",
29 | exportTypeName: "ClassNames",
30 | exportTypeInterface: "Styles",
31 | listDifferent: false,
32 | ignore: [],
33 | implementation,
34 | quoteType: "single",
35 | updateStaleOnly: false,
36 | logLevel: "verbose",
37 | outputFolder: null,
38 | allowArbitraryExtensions: false,
39 | });
40 |
41 | const expectedPath = path.join(
42 | process.cwd(),
43 | "__tests__/dummy-styles/style.scss.d.ts"
44 | );
45 |
46 | expect(fs.writeFileSync).toHaveBeenCalledWith(
47 | expectedPath,
48 | "export declare const someClass: string;\n"
49 | );
50 | expect(console.log).toHaveBeenCalledWith(
51 | expect.stringContaining(`[GENERATED TYPES] ${expectedPath}`)
52 | );
53 | });
54 |
55 | it("writes the corresponding type definitions for a file and logs when allowArbitraryExtensions is set", async () => {
56 | const testFile = path.resolve(__dirname, "..", "dummy-styles/style.scss");
57 |
58 | await writeFile(testFile, {
59 | banner: "",
60 | watch: false,
61 | ignoreInitial: false,
62 | exportType: "named",
63 | exportTypeName: "ClassNames",
64 | exportTypeInterface: "Styles",
65 | listDifferent: false,
66 | ignore: [],
67 | implementation,
68 | quoteType: "single",
69 | updateStaleOnly: false,
70 | logLevel: "verbose",
71 | outputFolder: null,
72 | allowArbitraryExtensions: true,
73 | });
74 |
75 | const expectedPath = path.join(
76 | process.cwd(),
77 | "__tests__/dummy-styles/style.d.scss.ts"
78 | );
79 |
80 | expect(fs.writeFileSync).toHaveBeenCalledWith(
81 | expectedPath,
82 | "export declare const someClass: string;\n"
83 | );
84 | expect(console.log).toHaveBeenCalledWith(
85 | expect.stringContaining(`[GENERATED TYPES] ${expectedPath}`)
86 | );
87 | });
88 |
89 | it("skips files with no classes", async () => {
90 | const testFile = path.resolve(__dirname, "..", "dummy-styles/empty.scss");
91 |
92 | await writeFile(testFile, {
93 | banner: "",
94 | watch: false,
95 | ignoreInitial: false,
96 | exportType: "named",
97 | exportTypeName: "ClassNames",
98 | exportTypeInterface: "Styles",
99 | listDifferent: false,
100 | ignore: [],
101 | implementation,
102 | quoteType: "single",
103 | updateStaleOnly: false,
104 | logLevel: "verbose",
105 | outputFolder: null,
106 | allowArbitraryExtensions: false,
107 | });
108 |
109 | expect(fs.writeFileSync).not.toHaveBeenCalled();
110 | expect(console.log).toHaveBeenCalledWith(
111 | expect.stringContaining(`[NO GENERATED TYPES] ${testFile}`)
112 | );
113 | });
114 |
115 | describe("when a file already exists with type definitions", () => {
116 | const testFile = path.resolve(__dirname, "..", "dummy-styles/empty.scss");
117 | const existingTypes = path.join(
118 | process.cwd(),
119 | "__tests__/dummy-styles/empty.scss.d.ts"
120 | );
121 | const originalExistsSync = fs.existsSync;
122 |
123 | beforeEach(() => {
124 | jest
125 | .spyOn(fs, "existsSync")
126 | .mockImplementation((p) =>
127 | p === existingTypes ? true : originalExistsSync(p)
128 | );
129 | });
130 |
131 | afterEach(() => {
132 | (fs.existsSync as jest.Mock).mockRestore();
133 | });
134 |
135 | it("removes existing type definitions if no classes are found", async () => {
136 | await writeFile(testFile, {
137 | banner: "",
138 | watch: false,
139 | ignoreInitial: false,
140 | exportType: "named",
141 | exportTypeName: "ClassNames",
142 | exportTypeInterface: "Styles",
143 | listDifferent: false,
144 | ignore: [],
145 | implementation,
146 | quoteType: "single",
147 | updateStaleOnly: false,
148 | logLevel: "verbose",
149 | outputFolder: null,
150 | allowArbitraryExtensions: false,
151 | });
152 |
153 | expect(fs.unlinkSync).toHaveBeenCalledWith(existingTypes);
154 | expect(console.log).toHaveBeenCalledWith(
155 | expect.stringContaining(`[REMOVED] ${existingTypes}`)
156 | );
157 | });
158 | });
159 |
160 | describe("when outputFolder is passed", () => {
161 | it("should write to the correct path", async () => {
162 | const testFile = path.resolve(
163 | __dirname,
164 | "..",
165 | "dummy-styles/style.scss"
166 | );
167 |
168 | await writeFile(testFile, {
169 | banner: "",
170 | watch: false,
171 | ignoreInitial: false,
172 | exportType: "named",
173 | exportTypeName: "ClassNames",
174 | exportTypeInterface: "Styles",
175 | listDifferent: false,
176 | ignore: [],
177 | implementation,
178 | quoteType: "single",
179 | updateStaleOnly: false,
180 | logLevel: "verbose",
181 | outputFolder: "__generated__",
182 | allowArbitraryExtensions: false,
183 | });
184 |
185 | const expectedPath = path.join(
186 | process.cwd(),
187 | "__generated__/__tests__/dummy-styles/style.scss.d.ts"
188 | );
189 |
190 | expect(fs.writeFileSync).toHaveBeenCalledWith(
191 | expectedPath,
192 | "export declare const someClass: string;\n"
193 | );
194 | expect(console.log).toHaveBeenCalledWith(
195 | expect.stringContaining(`[GENERATED TYPES] ${expectedPath}`)
196 | );
197 | });
198 | });
199 |
200 | describe("when --updateStaleOnly is passed", () => {
201 | const originalReadFileSync = fs.readFileSync;
202 | const testFile = path.resolve(__dirname, "..", "dummy-styles/style.scss");
203 | const expectedPath = path.join(
204 | process.cwd(),
205 | "__tests__/dummy-styles/style.scss.d.ts"
206 | );
207 |
208 | beforeEach(() => {
209 | jest.spyOn(fs, "statSync");
210 | jest.spyOn(fs, "existsSync");
211 | jest.spyOn(fs, "readFileSync");
212 | (fs.existsSync as jest.Mock).mockImplementation(() => true);
213 | });
214 |
215 | afterEach(() => {
216 | (fs.statSync as jest.Mock).mockRestore();
217 | (fs.existsSync as jest.Mock).mockRestore();
218 | (fs.readFileSync as jest.Mock).mockRestore();
219 | });
220 |
221 | it("skips stale files", async () => {
222 | (fs.statSync as jest.Mock).mockImplementation((p) => ({
223 | mtime:
224 | p === expectedPath ? new Date(2020, 0, 2) : new Date(2020, 0, 1),
225 | }));
226 |
227 | await writeFile(testFile, {
228 | banner: "",
229 | watch: false,
230 | ignoreInitial: false,
231 | exportType: "named",
232 | exportTypeName: "ClassNames",
233 | exportTypeInterface: "Styles",
234 | listDifferent: false,
235 | ignore: [],
236 | implementation,
237 | quoteType: "single",
238 | updateStaleOnly: true,
239 | logLevel: "verbose",
240 | outputFolder: null,
241 | allowArbitraryExtensions: false,
242 | });
243 |
244 | expect(fs.writeFileSync).not.toHaveBeenCalled();
245 | });
246 |
247 | it("updates files that aren't stale", async () => {
248 | (fs.statSync as jest.Mock).mockImplementation(
249 | () => new Date(2020, 0, 1)
250 | );
251 |
252 | // Mock outdated file contents.
253 | (fs.readFileSync as jest.Mock).mockImplementation(
254 | (
255 | p: PathOrFileDescriptor,
256 | opts?: {
257 | encoding?: null | undefined;
258 | flag?: string | undefined;
259 | } | null
260 | ) => (p === expectedPath ? `` : originalReadFileSync(p, opts))
261 | );
262 |
263 | await writeFile(testFile, {
264 | banner: "",
265 | watch: false,
266 | ignoreInitial: false,
267 | exportType: "named",
268 | exportTypeName: "ClassNames",
269 | exportTypeInterface: "Styles",
270 | listDifferent: false,
271 | ignore: [],
272 | implementation,
273 | quoteType: "single",
274 | updateStaleOnly: true,
275 | logLevel: "verbose",
276 | outputFolder: null,
277 | allowArbitraryExtensions: false,
278 | });
279 |
280 | expect(fs.writeFileSync).toHaveBeenCalled();
281 | });
282 |
283 | it("skips files that aren't stale but type definition contents haven't changed", async () => {
284 | (fs.statSync as jest.Mock).mockImplementation(
285 | () => new Date(2020, 0, 1)
286 | );
287 |
288 | await writeFile(testFile, {
289 | banner: "",
290 | watch: false,
291 | ignoreInitial: false,
292 | exportType: "named",
293 | exportTypeName: "ClassNames",
294 | exportTypeInterface: "Styles",
295 | listDifferent: false,
296 | ignore: [],
297 | implementation,
298 | quoteType: "single",
299 | updateStaleOnly: true,
300 | logLevel: "verbose",
301 | outputFolder: null,
302 | allowArbitraryExtensions: false,
303 | });
304 |
305 | expect(fs.writeFileSync).not.toHaveBeenCalled();
306 | });
307 |
308 | it("doesn't attempt to access a non-existent file", async () => {
309 | (fs.existsSync as jest.Mock).mockImplementation(() => false);
310 |
311 | await writeFile(testFile, {
312 | banner: "",
313 | watch: false,
314 | ignoreInitial: false,
315 | exportType: "named",
316 | exportTypeName: "ClassNames",
317 | exportTypeInterface: "Styles",
318 | listDifferent: false,
319 | ignore: [],
320 | implementation,
321 | quoteType: "single",
322 | updateStaleOnly: true,
323 | logLevel: "verbose",
324 | outputFolder: null,
325 | allowArbitraryExtensions: false,
326 | });
327 |
328 | expect(fs.statSync).not.toHaveBeenCalledWith(testFile);
329 | });
330 | });
331 | });
332 | });
333 |
--------------------------------------------------------------------------------
/__tests__/dart-sass/dart-sass.test.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import slash from "slash";
3 | import { alerts } from "../../lib/core";
4 | import { main } from "../../lib/main";
5 |
6 | describe("dart-sass", () => {
7 | let writeFileSyncSpy: jest.SpyInstance;
8 |
9 | beforeEach(() => {
10 | // Only mock the writes, so the example files can still be read.
11 | writeFileSyncSpy = jest.spyOn(fs, "writeFileSync").mockImplementation();
12 |
13 | // Avoid creating directories while running tests.
14 | jest.spyOn(fs, "mkdirSync").mockImplementation();
15 |
16 | // Avoid console logs showing up.
17 | jest.spyOn(console, "log").mockImplementation();
18 |
19 | jest.spyOn(alerts, "error").mockImplementation();
20 | });
21 |
22 | afterEach(() => {
23 | writeFileSyncSpy.mockReset();
24 | });
25 |
26 | it("@use support", async () => {
27 | const pattern = `${__dirname}`;
28 |
29 | await main(pattern, {
30 | banner: "",
31 | watch: false,
32 | ignoreInitial: false,
33 | exportType: "named",
34 | exportTypeName: "ClassNames",
35 | exportTypeInterface: "Styles",
36 | listDifferent: false,
37 | ignore: [],
38 | implementation: "sass",
39 | quoteType: "single",
40 | updateStaleOnly: false,
41 | logLevel: "verbose",
42 | additionalData: "$global-red: red;",
43 | aliases: {
44 | "~fancy-import": "complex",
45 | "~another": "style",
46 | },
47 | aliasPrefixes: {
48 | "~": "nested-styles/",
49 | },
50 | });
51 |
52 | expect(alerts.error).not.toHaveBeenCalled();
53 | expect(fs.writeFileSync).toHaveBeenCalledTimes(1);
54 |
55 | const expectedDirname = slash(__dirname);
56 |
57 | expect(fs.writeFileSync).toHaveBeenCalledWith(
58 | `${expectedDirname}/use.scss.d.ts`,
59 | "export declare const foo: string;\n"
60 | );
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/__tests__/dart-sass/use.scss:
--------------------------------------------------------------------------------
1 | @use "variables";
2 |
3 | .foo {
4 | color: variables.$red;
5 | }
6 |
--------------------------------------------------------------------------------
/__tests__/dart-sass/variables.scss:
--------------------------------------------------------------------------------
1 | $red: red;
2 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/alias-prefixes.scss:
--------------------------------------------------------------------------------
1 | @import "~fancy-import";
2 | @import "~style.scss";
3 |
4 | .my-custom-class {
5 | color: green;
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/alias-prefixes.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const someStyles: string;
2 | export declare const nestedClass: string;
3 | export declare const nestedAnother: string;
4 | export declare const nestedStyles: string;
5 | export declare const myCustomClass: string;
6 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/aliases.scss:
--------------------------------------------------------------------------------
1 | @import "~fancy-import";
2 | @import "~another";
3 |
4 | .my-custom-class {
5 | color: green;
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/aliases.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const someStyles: string;
2 | export declare const nestedClass: string;
3 | export declare const nestedAnother: string;
4 | export declare const someClass: string;
5 | export declare const myCustomClass: string;
6 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/complex.scss:
--------------------------------------------------------------------------------
1 | .some-styles {
2 | color: red;
3 | }
4 |
5 | .number-1 {
6 | color: green;
7 | }
8 |
9 | .nested {
10 | &-class {
11 | background: green;
12 | }
13 |
14 | &-another {
15 | border: 1px;
16 | }
17 | }
18 |
19 | :where(.where-selector) {
20 | color: blue;
21 | }
22 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/complex.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const someStyles: string;
2 | export declare const nestedClass: string;
3 | export declare const nestedAnother: string;
4 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/composes.scss:
--------------------------------------------------------------------------------
1 | .composed-class {
2 | composes: nested-styles from "./nested-styles/style.scss";
3 | color: red;
4 | }
5 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/composes.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const composedClass: string;
2 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/dashes.scss:
--------------------------------------------------------------------------------
1 | .App {
2 | background: white;
3 |
4 | .Logo {
5 | width: 50%;
6 | }
7 | }
8 |
9 | .App-Header {
10 | font-size: 2em;
11 | }
12 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/dashes.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const app: string;
2 | export declare const logo: string;
3 | export declare const appHeader: string;
4 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/empty.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skovy/typed-scss-modules/e80c68e2d8369be28a17f605d298d2f2bdc16b28/__tests__/dummy-styles/empty.scss
--------------------------------------------------------------------------------
/__tests__/dummy-styles/global-variables.scss:
--------------------------------------------------------------------------------
1 | .globalStyle {
2 | background-color: $global-red;
3 | }
4 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/invalid.scss:
--------------------------------------------------------------------------------
1 | .random-class {
2 | color: pink;
3 | }
4 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/invalid.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const nope: string;
2 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/nested-styles/style.scss:
--------------------------------------------------------------------------------
1 | .nested-styles {
2 | background: purple;
3 | }
4 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/nested-styles/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const nestedStyles: string;
2 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/style.scss:
--------------------------------------------------------------------------------
1 | .some-class {
2 | color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const someClass: string;
2 |
--------------------------------------------------------------------------------
/__tests__/dummy-styles/typed-scss-modules.config.js:
--------------------------------------------------------------------------------
1 | export const config = {
2 | banner: "",
3 | watch: false,
4 | ignoreInitial: false,
5 | exportType: "named",
6 | exportTypeName: "ClassNames",
7 | exportTypeInterface: "Styles",
8 | listDifferent: false,
9 | ignore: [],
10 | quoteType: "single",
11 | updateStaleOnly: false,
12 | logLevel: "verbose",
13 | };
14 |
--------------------------------------------------------------------------------
/__tests__/helpers/index.ts:
--------------------------------------------------------------------------------
1 | import { Implementations, IMPLEMENTATIONS } from "../../lib/implementations";
2 |
3 | export const describeAllImplementations = (
4 | fn: (implementation: Implementations) => void
5 | ) => {
6 | IMPLEMENTATIONS.forEach((implementation) => {
7 | describe(`${implementation} implementation`, () => {
8 | fn(implementation);
9 | });
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/__tests__/implementations/get-default-implementation.test.ts:
--------------------------------------------------------------------------------
1 | import { getDefaultImplementation } from "../../lib/implementations";
2 |
3 | describe("getDefaultImplementation", () => {
4 | it("returns node-sass if it exists", () => {
5 | expect(getDefaultImplementation()).toBe("node-sass");
6 | });
7 |
8 | it("returns sass if node-sass does not exist", () => {
9 | const resolver = jest.fn((implementation) => {
10 | if (implementation === "node-sass") {
11 | throw new Error("Not Found");
12 | }
13 | }) as unknown as RequireResolve;
14 |
15 | expect(getDefaultImplementation(resolver)).toBe("sass");
16 | expect(resolver).toHaveBeenCalledTimes(2);
17 | expect(resolver).toHaveBeenNthCalledWith(1, "node-sass");
18 | expect(resolver).toHaveBeenNthCalledWith(2, "sass");
19 | });
20 |
21 | it("returns node-sass even if both sass and node-sass do not exist", () => {
22 | const resolver = jest.fn(() => {
23 | throw new Error("Not Found");
24 | }) as unknown as RequireResolve;
25 |
26 | expect(getDefaultImplementation(resolver)).toBe("node-sass");
27 | expect(resolver).toHaveBeenCalledTimes(2);
28 | expect(resolver).toHaveBeenNthCalledWith(1, "node-sass");
29 | expect(resolver).toHaveBeenNthCalledWith(2, "sass");
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/__tests__/implementations/get-implementation.test.ts:
--------------------------------------------------------------------------------
1 | import nodeSass from "node-sass";
2 | import sass from "sass";
3 | import { getImplementation } from "../../lib/implementations";
4 |
5 | describe("getImplementation", () => {
6 | it("returns the correct implementation when explicitly passed", () => {
7 | expect(getImplementation("node-sass")).toEqual(nodeSass);
8 | expect(getImplementation("sass")).toEqual(sass);
9 | });
10 |
11 | it("returns the correct default implementation if it is invalid", () => {
12 | expect(
13 | getImplementation(
14 | // @ts-expect-error invalid implementation
15 | "wat-sass"
16 | )
17 | ).toEqual(nodeSass);
18 | expect(getImplementation()).toEqual(nodeSass);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/load.test.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { DEFAULT_OPTIONS, loadConfig, mergeOptions } from "../lib/load";
3 |
4 | const CONFIG_CASES = [
5 | "js-default-export",
6 | "js-module-exports",
7 | "js-named-export",
8 | "ts-default-export",
9 | "ts-named-export",
10 | ];
11 |
12 | describe("#loadConfig", () => {
13 | it.each(CONFIG_CASES)(
14 | "should load the '%s' config file correctly",
15 | // Spoof the current working directory so when the config file is read
16 | // we can direct it to any path we want. This makes it easier to test
17 | // various kinds of configuration files as if they were in the root.
18 | async (configCaseName) => {
19 | jest
20 | .spyOn(process, "cwd")
21 | .mockReturnValue(path.resolve(`__tests__/configs/${configCaseName}`));
22 |
23 | const config = await loadConfig();
24 |
25 | expect(config).toHaveProperty("banner");
26 | expect(config).toEqual({ banner: `// ${configCaseName}` });
27 | }
28 | );
29 | });
30 |
31 | describe("#mergeOptions", () => {
32 | it("should return the default options by default", () => {
33 | expect(mergeOptions({}, {})).toEqual(DEFAULT_OPTIONS);
34 | });
35 |
36 | it("should allow overriding all default options via the CLI options", () => {
37 | expect(
38 | mergeOptions(
39 | {
40 | nameFormat: ["kebab"],
41 | implementation: "sass",
42 | exportType: "default",
43 | exportTypeName: "Classes",
44 | exportTypeInterface: "AllStyles",
45 | watch: true,
46 | ignoreInitial: true,
47 | listDifferent: true,
48 | ignore: ["path"],
49 | quoteType: "double",
50 | updateStaleOnly: true,
51 | logLevel: "silent",
52 | outputFolder: "__generated__",
53 | banner: "// override",
54 | allowArbitraryExtensions: true,
55 | },
56 | {}
57 | )
58 | ).toEqual({
59 | nameFormat: ["kebab"],
60 | implementation: "sass",
61 | exportType: "default",
62 | exportTypeName: "Classes",
63 | exportTypeInterface: "AllStyles",
64 | watch: true,
65 | ignoreInitial: true,
66 | listDifferent: true,
67 | ignore: ["path"],
68 | quoteType: "double",
69 | updateStaleOnly: true,
70 | logLevel: "silent",
71 | outputFolder: "__generated__",
72 | banner: "// override",
73 | allowArbitraryExtensions: true,
74 | });
75 | });
76 |
77 | it("should allow overriding all default options via the config options", () => {
78 | const importer = jest.fn();
79 |
80 | expect(
81 | mergeOptions(
82 | {},
83 | {
84 | nameFormat: ["kebab"],
85 | implementation: "sass",
86 | exportType: "default",
87 | exportTypeName: "Classes",
88 | exportTypeInterface: "AllStyles",
89 | watch: true,
90 | ignoreInitial: true,
91 | listDifferent: true,
92 | ignore: ["path"],
93 | quoteType: "double",
94 | updateStaleOnly: true,
95 | logLevel: "silent",
96 | banner: "// override",
97 | outputFolder: "__generated__",
98 | importer,
99 | allowArbitraryExtensions: true,
100 | }
101 | )
102 | ).toEqual({
103 | nameFormat: ["kebab"],
104 | implementation: "sass",
105 | exportType: "default",
106 | exportTypeName: "Classes",
107 | exportTypeInterface: "AllStyles",
108 | watch: true,
109 | ignoreInitial: true,
110 | listDifferent: true,
111 | ignore: ["path"],
112 | quoteType: "double",
113 | updateStaleOnly: true,
114 | logLevel: "silent",
115 | banner: "// override",
116 | outputFolder: "__generated__",
117 | importer,
118 | allowArbitraryExtensions: true,
119 | });
120 | });
121 |
122 | it("should give precedence to CLI options and still merge config-only options", () => {
123 | const importer = jest.fn();
124 |
125 | expect(
126 | mergeOptions(
127 | {
128 | nameFormat: ["kebab"],
129 | implementation: "sass",
130 | exportType: "default",
131 | exportTypeName: "Classes",
132 | exportTypeInterface: "AllStyles",
133 | watch: true,
134 | ignoreInitial: true,
135 | listDifferent: true,
136 | ignore: ["path"],
137 | quoteType: "double",
138 | updateStaleOnly: true,
139 | logLevel: "silent",
140 | banner: "// override",
141 | outputFolder: "__cli-generated__",
142 | allowArbitraryExtensions: true,
143 | },
144 | {
145 | nameFormat: ["param"],
146 | implementation: "node-sass",
147 | exportType: "named",
148 | exportTypeName: "Classnames",
149 | exportTypeInterface: "TheStyles",
150 | watch: false,
151 | ignoreInitial: false,
152 | listDifferent: false,
153 | ignore: ["another/path"],
154 | quoteType: "single",
155 | updateStaleOnly: false,
156 | logLevel: "info",
157 | banner: "// not override",
158 | outputFolder: "__generated__",
159 | importer,
160 | }
161 | )
162 | ).toEqual({
163 | nameFormat: ["kebab"],
164 | implementation: "sass",
165 | exportType: "default",
166 | exportTypeName: "Classes",
167 | exportTypeInterface: "AllStyles",
168 | watch: true,
169 | ignoreInitial: true,
170 | listDifferent: true,
171 | ignore: ["path"],
172 | quoteType: "double",
173 | updateStaleOnly: true,
174 | logLevel: "silent",
175 | banner: "// override",
176 | outputFolder: "__cli-generated__",
177 | importer,
178 | allowArbitraryExtensions: true,
179 | });
180 | });
181 |
182 | it("should give ignore undefined CLI options", () => {
183 | const importer = jest.fn();
184 |
185 | expect(
186 | mergeOptions(
187 | {
188 | aliases: undefined,
189 | aliasPrefixes: undefined,
190 | nameFormat: ["kebab"],
191 | implementation: "sass",
192 | exportType: "default",
193 | exportTypeName: "Classes",
194 | exportTypeInterface: "AllStyles",
195 | watch: true,
196 | ignoreInitial: true,
197 | listDifferent: true,
198 | ignore: ["path"],
199 | quoteType: "double",
200 | updateStaleOnly: true,
201 | logLevel: "silent",
202 | banner: undefined,
203 | outputFolder: "__cli-generated__",
204 | allowArbitraryExtensions: true,
205 | },
206 | {
207 | aliases: {},
208 | aliasPrefixes: {},
209 | nameFormat: ["param"],
210 | implementation: "node-sass",
211 | exportType: "named",
212 | exportTypeName: "Classnames",
213 | exportTypeInterface: "TheStyles",
214 | watch: false,
215 | ignoreInitial: false,
216 | listDifferent: false,
217 | ignore: ["another/path"],
218 | quoteType: "single",
219 | updateStaleOnly: false,
220 | logLevel: "info",
221 | banner: "// banner",
222 | outputFolder: "__generated__",
223 | importer,
224 | allowArbitraryExtensions: false,
225 | }
226 | )
227 | ).toEqual({
228 | aliases: {},
229 | aliasPrefixes: {},
230 | nameFormat: ["kebab"],
231 | implementation: "sass",
232 | exportType: "default",
233 | exportTypeName: "Classes",
234 | exportTypeInterface: "AllStyles",
235 | watch: true,
236 | ignoreInitial: true,
237 | listDifferent: true,
238 | ignore: ["path"],
239 | quoteType: "double",
240 | updateStaleOnly: true,
241 | logLevel: "silent",
242 | banner: "// banner",
243 | outputFolder: "__cli-generated__",
244 | importer,
245 | allowArbitraryExtensions: true,
246 | });
247 | });
248 | });
249 |
--------------------------------------------------------------------------------
/__tests__/main.test.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import slash from "slash";
4 | import { alerts } from "../lib/core";
5 | import { main } from "../lib/main";
6 | import { describeAllImplementations } from "./helpers";
7 |
8 | describeAllImplementations((implementation) => {
9 | describe("main", () => {
10 | let writeFileSyncSpy: jest.SpyInstance;
11 |
12 | beforeEach(() => {
13 | // Only mock the writes, so the example files can still be read.
14 | writeFileSyncSpy = jest.spyOn(fs, "writeFileSync").mockImplementation();
15 |
16 | // Avoid creating directories while running tests.
17 | jest.spyOn(fs, "mkdirSync").mockImplementation();
18 |
19 | // Avoid console logs showing up.
20 | jest.spyOn(console, "log").mockImplementation();
21 |
22 | jest.spyOn(alerts, "error").mockImplementation();
23 | });
24 |
25 | afterEach(() => {
26 | writeFileSyncSpy.mockReset();
27 | });
28 |
29 | it("generates types for all .scss files when the pattern is a directory", async () => {
30 | const pattern = `${__dirname}/dummy-styles`;
31 |
32 | await main(pattern, {
33 | banner: "",
34 | watch: false,
35 | ignoreInitial: false,
36 | exportType: "named",
37 | exportTypeName: "ClassNames",
38 | exportTypeInterface: "Styles",
39 | listDifferent: false,
40 | ignore: [],
41 | implementation,
42 | quoteType: "single",
43 | updateStaleOnly: false,
44 | logLevel: "verbose",
45 | additionalData: "$global-red: red;",
46 | aliases: {
47 | "~fancy-import": "complex",
48 | "~another": "style",
49 | },
50 | aliasPrefixes: {
51 | "~": "nested-styles/",
52 | },
53 | });
54 |
55 | expect(alerts.error).not.toHaveBeenCalled();
56 | expect(fs.writeFileSync).toHaveBeenCalledTimes(9);
57 |
58 | const expectedDirname = slash(path.join(__dirname, "dummy-styles"));
59 |
60 | expect(fs.writeFileSync).toHaveBeenCalledWith(
61 | `${expectedDirname}/complex.scss.d.ts`,
62 | "export declare const nestedAnother: string;\nexport declare const nestedClass: string;\nexport declare const number1: string;\nexport declare const someStyles: string;\nexport declare const whereSelector: string;\n"
63 | );
64 | expect(fs.writeFileSync).toHaveBeenCalledWith(
65 | `${expectedDirname}/style.scss.d.ts`,
66 | "export declare const someClass: string;\n"
67 | );
68 | });
69 |
70 | it("generates types for all .scss files and ignores files that match the ignore pattern", async () => {
71 | const pattern = `${__dirname}/dummy-styles`;
72 |
73 | await main(pattern, {
74 | banner: "",
75 | watch: false,
76 | ignoreInitial: false,
77 | exportType: "named",
78 | exportTypeName: "ClassNames",
79 | exportTypeInterface: "Styles",
80 | listDifferent: false,
81 | ignore: ["**/style.scss"],
82 | implementation,
83 | quoteType: "single",
84 | updateStaleOnly: false,
85 | logLevel: "verbose",
86 | additionalData: "$global-red: red;",
87 | aliases: {
88 | "~fancy-import": "complex",
89 | "~another": "style",
90 | },
91 | aliasPrefixes: {
92 | "~": "nested-styles/",
93 | },
94 | });
95 |
96 | expect(alerts.error).not.toHaveBeenCalled();
97 | expect(fs.writeFileSync).toHaveBeenCalledTimes(7);
98 |
99 | const expectedDirname = slash(path.join(__dirname, "dummy-styles"));
100 |
101 | expect(fs.writeFileSync).toHaveBeenCalledWith(
102 | `${expectedDirname}/complex.scss.d.ts`,
103 | "export declare const nestedAnother: string;\nexport declare const nestedClass: string;\nexport declare const number1: string;\nexport declare const someStyles: string;\nexport declare const whereSelector: string;\n"
104 | );
105 |
106 | // Files that should match the ignore pattern.
107 | expect(fs.writeFileSync).not.toHaveBeenCalledWith(
108 | `${expectedDirname}/style.scss.d.ts`,
109 | expect.anything()
110 | );
111 | expect(fs.writeFileSync).not.toHaveBeenCalledWith(
112 | `${expectedDirname}/nested-styles/style.scss.d.ts`,
113 | expect.anything()
114 | );
115 | });
116 |
117 | it("reads options from the configuration file", async () => {
118 | const pattern = `${__dirname}/dummy-styles`;
119 |
120 | jest.spyOn(process, "cwd").mockReturnValue(path.resolve(pattern));
121 |
122 | await main(pattern, {
123 | additionalData: "$global-red: red;",
124 | aliases: {
125 | "~fancy-import": "complex",
126 | "~another": "style",
127 | },
128 | aliasPrefixes: {
129 | "~": "nested-styles/",
130 | },
131 | exportType: "default",
132 | });
133 |
134 | expect(alerts.error).not.toHaveBeenCalled();
135 | expect(fs.writeFileSync).toHaveBeenCalledTimes(9);
136 |
137 | // Transform the calls into a more readable format for the snapshot.
138 | const contents = writeFileSyncSpy.mock.calls
139 | .map(([fullFilePath, contents]: [string, string]) => ({
140 | path: path.relative(__dirname, fullFilePath),
141 | contents,
142 | }))
143 | // Sort to avoid flakey snapshot tests if call order changes.
144 | .sort((a, b) => a.path.localeCompare(b.path));
145 |
146 | expect(contents).toMatchSnapshot();
147 | });
148 |
149 | it("outputs the correct files when outputFolder is passed", async () => {
150 | const pattern = path.resolve(__dirname, "dummy-styles");
151 |
152 | await main(pattern, {
153 | additionalData: "$global-red: red;",
154 | aliases: {
155 | "~fancy-import": "complex",
156 | "~another": "style",
157 | },
158 | aliasPrefixes: {
159 | "~": "nested-styles/",
160 | },
161 | outputFolder: "__generated__",
162 | });
163 |
164 | expect(alerts.error).not.toHaveBeenCalled();
165 | expect(fs.writeFileSync).toHaveBeenCalledTimes(9);
166 | expect(fs.mkdirSync).toHaveBeenCalledTimes(9);
167 |
168 | // Transform the calls into a more readable format for the snapshot.
169 | const contents = writeFileSyncSpy.mock.calls
170 | .map(([fullFilePath, contents]: [string, string]) => ({
171 | path: path.relative(__dirname, fullFilePath),
172 | contents,
173 | }))
174 | // Sort to avoid flakey snapshot tests if call order changes.
175 | .sort((a, b) => a.path.localeCompare(b.path));
176 |
177 | expect(contents).toMatchSnapshot();
178 | });
179 | });
180 | });
181 |
--------------------------------------------------------------------------------
/__tests__/prettier/__snapshots__/prettier.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`attemptPrettier should match snapshot 1`] = `
4 | "export type Styles = {
5 | nestedAnother: string;
6 | nestedClass: string;
7 | someStyles: string;
8 | };
9 |
10 | export type ClassNames = keyof Styles;
11 |
12 | declare const styles: Styles;
13 |
14 | export default styles;
15 | "
16 | `;
17 |
--------------------------------------------------------------------------------
/__tests__/prettier/prettier.test.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 | import prettier from "prettier";
3 | import { attemptPrettier } from "../../lib/prettier";
4 | import { classNamesToTypeDefinitions } from "../../lib/typescript";
5 |
6 | const file = join(__dirname, "test.d.ts");
7 | const input =
8 | "export type Styles = {'myClass': string;'yourClass': string;}; export type Classes = keyof Styles; declare const styles: Styles; export default styles;";
9 |
10 | describe("attemptPrettier", () => {
11 | it("should locate and apply prettier.format", async () => {
12 | const output = await attemptPrettier(file, input);
13 |
14 | expect(prettier.format(input, { parser: "typescript" })).toMatch(output);
15 | });
16 |
17 | it("should match snapshot", async () => {
18 | const typeDefinition = await classNamesToTypeDefinitions({
19 | banner: "",
20 | classNames: ["nestedAnother", "nestedClass", "someStyles"],
21 | file,
22 | exportType: "default",
23 | });
24 |
25 | if (!typeDefinition) {
26 | throw new Error("failed to collect typeDefinition");
27 | }
28 |
29 | const output = await attemptPrettier(file, typeDefinition);
30 |
31 | expect(output).toMatchSnapshot();
32 | });
33 | });
34 |
35 | describe("attemptPrettier - mock prettier", () => {
36 | beforeAll(() => {
37 | jest.mock("prettier", () => ({
38 | format: undefined,
39 | }));
40 | });
41 |
42 | it("should fail to recognize prettier and return input", async () => {
43 | const output = await attemptPrettier(file, input);
44 |
45 | expect(input).toMatch(output);
46 | });
47 | });
48 |
49 | describe("attemptPrettier - mock resolution check", () => {
50 | beforeAll(() => {
51 | jest.mock("../../lib/prettier/can-resolve");
52 | });
53 |
54 | it("should fail to resolve prettier and return input", async () => {
55 | const output = await attemptPrettier(file, input);
56 |
57 | expect(input).toMatch(output);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/__tests__/sass/file-to-class-names.test.ts:
--------------------------------------------------------------------------------
1 | import { fileToClassNames } from "../../lib/sass";
2 | import { describeAllImplementations } from "../helpers";
3 |
4 | describeAllImplementations((implementation) => {
5 | describe("fileToClassNames", () => {
6 | it("converts a file path to an array of class names (default camel cased)", async () => {
7 | const result = await fileToClassNames(
8 | `${__dirname}/../dummy-styles/complex.scss`
9 | );
10 |
11 | expect(result).toEqual([
12 | "nestedAnother",
13 | "nestedClass",
14 | "number1",
15 | "someStyles",
16 | "whereSelector",
17 | ]);
18 | });
19 |
20 | describe("nameFormat", () => {
21 | it("converts a file path to an array of class names with kebab as the name format", async () => {
22 | const result = await fileToClassNames(
23 | `${__dirname}/../dummy-styles/complex.scss`,
24 | {
25 | nameFormat: ["kebab"],
26 | implementation,
27 | }
28 | );
29 |
30 | expect(result).toEqual([
31 | "nested-another",
32 | "nested-class",
33 | "number-1",
34 | "some-styles",
35 | "where-selector",
36 | ]);
37 | });
38 |
39 | it("converts a file path to an array of class names with param as the name format", async () => {
40 | const result = await fileToClassNames(
41 | `${__dirname}/../dummy-styles/complex.scss`,
42 | {
43 | nameFormat: ["param"],
44 | implementation,
45 | }
46 | );
47 |
48 | expect(result).toEqual([
49 | "nested-another",
50 | "nested-class",
51 | "number-1",
52 | "some-styles",
53 | "where-selector",
54 | ]);
55 | });
56 |
57 | it("converts a file path to an array of class names with snake as the name format", async () => {
58 | const result = await fileToClassNames(
59 | `${__dirname}/../dummy-styles/complex.scss`,
60 | {
61 | nameFormat: ["snake"],
62 | implementation,
63 | }
64 | );
65 |
66 | expect(result).toEqual([
67 | "nested_another",
68 | "nested_class",
69 | "number_1",
70 | "some_styles",
71 | "where_selector",
72 | ]);
73 | });
74 |
75 | it("converts a file path to an array of class names where only classes with dashes in the names are altered", async () => {
76 | const result = await fileToClassNames(
77 | `${__dirname}/../dummy-styles/dashes.scss`,
78 | {
79 | nameFormat: ["dashes"],
80 | implementation,
81 | }
82 | );
83 |
84 | expect(result).toEqual(["App", "appHeader", "Logo"]);
85 | });
86 |
87 | it("does not change class names when nameFormat is set to none", async () => {
88 | const result = await fileToClassNames(
89 | `${__dirname}/../dummy-styles/dashes.scss`,
90 | {
91 | nameFormat: ["none"],
92 | implementation,
93 | }
94 | );
95 |
96 | expect(result).toEqual(["App", "App-Header", "Logo"]);
97 | });
98 |
99 | it("applies all transformers when is set to all", async () => {
100 | const result = await fileToClassNames(
101 | `${__dirname}/../dummy-styles/complex.scss`,
102 | {
103 | nameFormat: ["all"],
104 | implementation,
105 | }
106 | );
107 |
108 | expect(result).toEqual([
109 | "nested_another",
110 | "nested_class",
111 | "nested-another",
112 | "nested-class",
113 | "nestedAnother",
114 | "nestedClass",
115 | "number_1",
116 | "number-1",
117 | "number1",
118 | "some_styles",
119 | "some-styles",
120 | "someStyles",
121 | "where_selector",
122 | "where-selector",
123 | "whereSelector",
124 | ]);
125 | });
126 |
127 | it("applies multiple transformers when sent as an array", async () => {
128 | const result = await fileToClassNames(
129 | `${__dirname}/../dummy-styles/complex.scss`,
130 | {
131 | nameFormat: ["kebab", "snake"],
132 | implementation,
133 | }
134 | );
135 |
136 | expect(result).toEqual([
137 | "nested_another",
138 | "nested_class",
139 | "nested-another",
140 | "nested-class",
141 | "number_1",
142 | "number-1",
143 | "some_styles",
144 | "some-styles",
145 | "where_selector",
146 | "where-selector",
147 | ]);
148 | });
149 |
150 | it("handles only a string", async () => {
151 | const result = await fileToClassNames(
152 | `${__dirname}/../dummy-styles/complex.scss`,
153 | {
154 | nameFormat: "snake",
155 | implementation,
156 | }
157 | );
158 |
159 | expect(result).toEqual([
160 | "nested_another",
161 | "nested_class",
162 | "number_1",
163 | "some_styles",
164 | "where_selector",
165 | ]);
166 | });
167 | });
168 |
169 | describe("aliases", () => {
170 | it("converts a file that contains aliases", async () => {
171 | const result = await fileToClassNames(
172 | `${__dirname}/../dummy-styles/aliases.scss`,
173 | {
174 | aliases: {
175 | "~fancy-import": "complex",
176 | "~another": "style",
177 | },
178 | implementation,
179 | }
180 | );
181 |
182 | expect(result).toEqual([
183 | "myCustomClass",
184 | "nestedAnother",
185 | "nestedClass",
186 | "number1",
187 | "someClass",
188 | "someStyles",
189 | "whereSelector",
190 | ]);
191 | });
192 | });
193 |
194 | describe("aliasPrefixes", () => {
195 | it("converts a file that contains alias prefixes (but prioritizes aliases)", async () => {
196 | const result = await fileToClassNames(
197 | `${__dirname}/../dummy-styles/alias-prefixes.scss`,
198 | {
199 | aliases: {
200 | "~fancy-import": "complex",
201 | },
202 | aliasPrefixes: {
203 | "~": "nested-styles/",
204 | },
205 | implementation,
206 | }
207 | );
208 |
209 | expect(result).toEqual([
210 | "myCustomClass",
211 | "nestedAnother",
212 | "nestedClass",
213 | "nestedStyles",
214 | "number1",
215 | "someStyles",
216 | "whereSelector",
217 | ]);
218 | });
219 | });
220 |
221 | describe("composes", () => {
222 | it("converts a file that contains a composes dependency from another file", async () => {
223 | const result = await fileToClassNames(
224 | `${__dirname}/../dummy-styles/composes.scss`,
225 | {
226 | implementation,
227 | }
228 | );
229 |
230 | expect(result).toEqual(["composedClass"]);
231 | });
232 | });
233 |
234 | describe("additionalData", () => {
235 | it("adds additional data to enable adding any necessary context", async () => {
236 | const result = await fileToClassNames(
237 | `${__dirname}/../dummy-styles/global-variables.scss`,
238 | {
239 | implementation,
240 | additionalData: "$global-red: red;",
241 | }
242 | );
243 |
244 | expect(result).toEqual(["globalStyle"]);
245 | });
246 | });
247 | });
248 | });
249 |
--------------------------------------------------------------------------------
/__tests__/sass/importer.test.ts:
--------------------------------------------------------------------------------
1 | import { SyncContext } from "node-sass";
2 | import { LegacyImporterThis } from "sass";
3 | import { aliasImporter, customImporters } from "../../lib/sass/importer";
4 |
5 | // SASS importers receive two other arguments that this package doesn't care about.
6 | // Fake `this` which the type definitions both define for importers.
7 | const fakeImporterThis = {} as LegacyImporterThis & SyncContext;
8 | const fakePrev = "";
9 |
10 | describe("#aliasImporter", () => {
11 | it("should create an importer to replace aliases and otherwise return null", () => {
12 | const importer = aliasImporter({
13 | aliases: { input: "output", "~alias": "node_modules" },
14 | aliasPrefixes: {},
15 | });
16 |
17 | expect(importer.call(fakeImporterThis, "input", fakePrev)).toEqual({
18 | file: "output",
19 | });
20 | expect(importer.call(fakeImporterThis, "~alias", fakePrev)).toEqual({
21 | file: "node_modules",
22 | });
23 | expect(importer.call(fakeImporterThis, "output", fakePrev)).toBeNull();
24 | expect(
25 | importer.call(fakeImporterThis, "input-substring", fakePrev)
26 | ).toBeNull();
27 | expect(importer.call(fakeImporterThis, "other", fakePrev)).toBeNull();
28 | });
29 |
30 | it("should create an importer to replace alias prefixes and otherwise return null", () => {
31 | const importer = aliasImporter({
32 | aliases: {},
33 | aliasPrefixes: { "~": "node_modules/", abc: "def" },
34 | });
35 |
36 | expect(importer.call(fakeImporterThis, "abc-123", fakePrev)).toEqual({
37 | file: "def-123",
38 | });
39 | expect(importer.call(fakeImporterThis, "~package", fakePrev)).toEqual({
40 | file: "node_modules/package",
41 | });
42 | expect(importer.call(fakeImporterThis, "output~", fakePrev)).toBeNull();
43 | expect(
44 | importer.call(fakeImporterThis, "input-substring-abc", fakePrev)
45 | ).toBeNull();
46 | expect(importer.call(fakeImporterThis, "other", fakePrev)).toBeNull();
47 | });
48 | });
49 |
50 | describe("#customImporters", () => {
51 | beforeEach(() => {
52 | console.log = jest.fn(); // avoid console logs showing up
53 | });
54 |
55 | it("should return only an alias importer by default", () => {
56 | const importers = customImporters({
57 | aliases: { "~alias": "secret/path" },
58 | aliasPrefixes: { "~": "node_modules/" },
59 | });
60 |
61 | expect(importers).toHaveLength(1);
62 |
63 | const [aliasImporter] = importers;
64 |
65 | expect(aliasImporter.call(fakeImporterThis, "~package", fakePrev)).toEqual({
66 | file: "node_modules/package",
67 | });
68 | expect(aliasImporter.call(fakeImporterThis, "~alias", fakePrev)).toEqual({
69 | file: "secret/path",
70 | });
71 | expect(aliasImporter.call(fakeImporterThis, "other", fakePrev)).toBeNull();
72 | });
73 |
74 | it("should add additional importers if passed a function", () => {
75 | const importer = jest.fn();
76 |
77 | const importers = customImporters({
78 | aliases: {},
79 | aliasPrefixes: {},
80 | importer,
81 | });
82 |
83 | expect(importers).toHaveLength(2);
84 | expect(importers[1]).toEqual(importer);
85 | });
86 |
87 | it("should add multiple importers if passed an array", () => {
88 | const importer1 = jest.fn();
89 | const importer2 = jest.fn();
90 | const importer3 = jest.fn();
91 |
92 | const importers = customImporters({
93 | aliases: {},
94 | aliasPrefixes: {},
95 | importer: [importer1, importer2, importer3],
96 | });
97 |
98 | expect(importers).toHaveLength(4);
99 | expect(importers[1]).toEqual(importer1);
100 | expect(importers[2]).toEqual(importer2);
101 | expect(importers[3]).toEqual(importer3);
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/__tests__/typescript/class-names-to-type-definitions.test.ts:
--------------------------------------------------------------------------------
1 | import os from "os";
2 | import { join } from "path";
3 | import { classNamesToTypeDefinitions } from "../../lib/typescript";
4 |
5 | jest.mock("../../lib/prettier/can-resolve", () => ({
6 | canResolvePrettier: () => false,
7 | }));
8 | const file = join(__dirname, "test.d.ts");
9 |
10 | describe("classNamesToTypeDefinitions (without Prettier)", () => {
11 | beforeEach(() => {
12 | console.log = jest.fn();
13 | });
14 |
15 | describe("named", () => {
16 | it("converts an array of class name strings to type definitions", async () => {
17 | const definition = await classNamesToTypeDefinitions({
18 | banner: "",
19 | classNames: ["myClass", "yourClass"],
20 | exportType: "named",
21 | file,
22 | });
23 |
24 | expect(definition).toEqual(
25 | "export declare const myClass: string;\nexport declare const yourClass: string;\n"
26 | );
27 | });
28 |
29 | it("returns null if there are no class names", async () => {
30 | const definition = await classNamesToTypeDefinitions({
31 | banner: "",
32 | classNames: [],
33 | exportType: "named",
34 | file,
35 | });
36 |
37 | expect(definition).toBeNull();
38 | });
39 |
40 | it("prints a warning if a classname is a reserved keyword and does not include it in the type definitions", async () => {
41 | const definition = await classNamesToTypeDefinitions({
42 | banner: "",
43 | classNames: ["myClass", "if"],
44 | exportType: "named",
45 | file,
46 | });
47 |
48 | expect(definition).toEqual("export declare const myClass: string;\n");
49 | expect(console.log).toHaveBeenCalledWith(
50 | expect.stringContaining(`[SKIPPING] 'if' is a reserved keyword`)
51 | );
52 | });
53 |
54 | it("prints a warning if a classname is invalid and does not include it in the type definitions", async () => {
55 | const definition = await classNamesToTypeDefinitions({
56 | banner: "",
57 | classNames: ["myClass", "invalid-variable"],
58 | exportType: "named",
59 | file,
60 | });
61 |
62 | expect(definition).toEqual("export declare const myClass: string;\n");
63 | expect(console.log).toHaveBeenCalledWith(
64 | expect.stringContaining(`[SKIPPING] 'invalid-variable' contains dashes`)
65 | );
66 | });
67 | });
68 |
69 | describe("default", () => {
70 | it("converts an array of class name strings to type definitions", async () => {
71 | const definition = await classNamesToTypeDefinitions({
72 | banner: "",
73 | classNames: ["myClass", "yourClass"],
74 | exportType: "default",
75 | file,
76 | });
77 |
78 | expect(definition).toEqual(
79 | "export type Styles = {\n 'myClass': string;\n 'yourClass': string;\n};\n\nexport type ClassNames = keyof Styles;\n\ndeclare const styles: Styles;\n\nexport default styles;\n"
80 | );
81 | });
82 |
83 | it("returns null if there are no class names", async () => {
84 | const definition = await classNamesToTypeDefinitions({
85 | banner: "",
86 | classNames: [],
87 | exportType: "default",
88 | file,
89 | });
90 |
91 | expect(definition).toBeNull();
92 | });
93 | });
94 |
95 | describe("invalid export type", () => {
96 | it("returns null", async () => {
97 | const definition = await classNamesToTypeDefinitions({
98 | banner: "",
99 | classNames: ["myClass"],
100 | // @ts-expect-error -- invalid export type
101 | exportType: "invalid",
102 | file,
103 | });
104 |
105 | expect(definition).toBeNull();
106 | });
107 | });
108 |
109 | describe("quoteType", () => {
110 | it("uses double quotes for default exports when specified", async () => {
111 | const definition = await classNamesToTypeDefinitions({
112 | banner: "",
113 | classNames: ["myClass", "yourClass"],
114 | exportType: "default",
115 | quoteType: "double",
116 | file,
117 | });
118 |
119 | expect(definition).toEqual(
120 | 'export type Styles = {\n "myClass": string;\n "yourClass": string;\n};\n\nexport type ClassNames = keyof Styles;\n\ndeclare const styles: Styles;\n\nexport default styles;\n'
121 | );
122 | });
123 |
124 | it("does not affect named exports", async () => {
125 | const definition = await classNamesToTypeDefinitions({
126 | banner: "",
127 | classNames: ["myClass", "yourClass"],
128 | exportType: "named",
129 | quoteType: "double",
130 | file,
131 | });
132 |
133 | expect(definition).toEqual(
134 | "export declare const myClass: string;\nexport declare const yourClass: string;\n"
135 | );
136 | });
137 | });
138 |
139 | describe("exportType name and type attributes", () => {
140 | it("uses custom value for ClassNames type name", async () => {
141 | const definition = await classNamesToTypeDefinitions({
142 | banner: "",
143 | classNames: ["myClass", "yourClass"],
144 | exportType: "default",
145 | exportTypeName: "Classes",
146 | file,
147 | });
148 |
149 | expect(definition).toEqual(
150 | "export type Styles = {\n 'myClass': string;\n 'yourClass': string;\n};\n\nexport type Classes = keyof Styles;\n\ndeclare const styles: Styles;\n\nexport default styles;\n"
151 | );
152 | });
153 |
154 | it("uses custom value for Styles type name", async () => {
155 | const definition = await classNamesToTypeDefinitions({
156 | banner: "",
157 | classNames: ["myClass", "yourClass"],
158 | exportType: "default",
159 | exportTypeInterface: "IStyles",
160 | file,
161 | });
162 |
163 | expect(definition).toEqual(
164 | "export type IStyles = {\n 'myClass': string;\n 'yourClass': string;\n};\n\nexport type ClassNames = keyof IStyles;\n\ndeclare const styles: IStyles;\n\nexport default styles;\n"
165 | );
166 | });
167 | });
168 |
169 | describe("Banner support", () => {
170 | const firstLine = (str: string): string => str.split(os.EOL)[0];
171 |
172 | it("appends the banner to the top of the output file: default", async () => {
173 | const banner = "// Example banner";
174 | const definition = await classNamesToTypeDefinitions({
175 | banner,
176 | classNames: ["myClass", "yourClass"],
177 | exportType: "default",
178 | file,
179 | });
180 |
181 | expect(firstLine(definition!)).toBe(banner);
182 | });
183 |
184 | it("appends the banner to the top of the output file: named", async () => {
185 | const banner = "// Example banner";
186 | const definition = await classNamesToTypeDefinitions({
187 | banner,
188 | classNames: ["myClass", "yourClass"],
189 | exportType: "named",
190 | file,
191 | });
192 |
193 | expect(firstLine(definition!)).toBe(banner);
194 | });
195 | });
196 | });
197 |
--------------------------------------------------------------------------------
/__tests__/typescript/get-type-definition-path.test.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { DEFAULT_OPTIONS } from "../../lib/load";
3 | import { getTypeDefinitionPath } from "../../lib/typescript";
4 |
5 | describe("getTypeDefinitionPath", () => {
6 | const cssFilePath = path.resolve(process.cwd(), "some/path/style.scss");
7 |
8 | it("returns the type definition path", () => {
9 | const outputPath = getTypeDefinitionPath(cssFilePath, DEFAULT_OPTIONS);
10 |
11 | expect(outputPath).toEqual(`${cssFilePath}.d.ts`);
12 | });
13 |
14 | describe("when outputFolder is passed", () => {
15 | it("returns the type definition path", () => {
16 | const outputPath = getTypeDefinitionPath(cssFilePath, {
17 | ...DEFAULT_OPTIONS,
18 | outputFolder: "__generated__",
19 | });
20 |
21 | const generatedFilePath = path.resolve(
22 | process.cwd(),
23 | "__generated__/some/path/style.scss.d.ts"
24 | );
25 |
26 | expect(outputPath).toEqual(generatedFilePath);
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | presets: [
4 | ["@babel/preset-env", { targets: { node: "current" } }],
5 | "@babel/preset-typescript",
6 | ],
7 | plugins: ["babel-plugin-transform-import-meta"],
8 | };
9 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = { extends: ["@commitlint/config-conventional"] };
3 |
--------------------------------------------------------------------------------
/docs/typed-scss-modules-example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skovy/typed-scss-modules/e80c68e2d8369be28a17f605d298d2f2bdc16b28/docs/typed-scss-modules-example.gif
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic example
2 |
3 | This example contains:
4 |
5 | - Core variables (`core/variables.scss`) which contains things like colors, etc. To make the import of these variables easier, it's expected that this directory is included in the search path. This demonstrates the need for `includePaths`.
6 | - An alias. This is most common when using a [webpack alias](https://webpack.js.org/configuration/resolve/#resolve-alias). This demonstrates the need for `aliases`.
7 |
8 | The command to generate the proper type files would look like this (_in the root of this repository_):
9 |
10 | ```bash
11 | npm run typed-scss-modules "examples/basic/**/*.scss" -- --includePaths examples/basic/core --aliases.~alias variables --banner '// example banner'
12 | ```
13 |
14 | - The glob pattern is wrapped in quotes to pass it as a string and avoid executing.
15 | - `includePaths` with `examples/basic/core` so that `@import 'variables'` is found.
16 | - `aliases` with `~alias: variables` meaning any `@import '~alias'` resolves to `@import 'variables'`.
17 | - No file will be output for `variables.scss` since there are no classes.
18 |
--------------------------------------------------------------------------------
/examples/basic/core/variables.scss:
--------------------------------------------------------------------------------
1 | $blue: blue;
2 | $yellow: yellow;
3 |
--------------------------------------------------------------------------------
/examples/basic/feature-a/index.ts:
--------------------------------------------------------------------------------
1 | import styles from "./style.scss";
2 |
3 | console.log(styles.text);
4 | console.log(styles.textHighlighted);
5 | // console.log(styles.somethingElse) <- Invalid: Property 'somethingElse' does not exist
6 |
--------------------------------------------------------------------------------
/examples/basic/feature-a/style.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .text {
4 | color: $blue;
5 |
6 | &-highlighted {
7 | color: $yellow;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/basic/feature-a/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | // example banner
2 | export declare const text: string;
3 | export declare const textHighlighted: string;
4 |
--------------------------------------------------------------------------------
/examples/basic/feature-b/index.ts:
--------------------------------------------------------------------------------
1 | import styles from "./style.scss";
2 |
3 | console.log(styles.topBanner);
4 | // console.log(styles.somethingElse) <- Invalid: Property 'somethingElse' does not exist
5 |
--------------------------------------------------------------------------------
/examples/basic/feature-b/style.scss:
--------------------------------------------------------------------------------
1 | @import "~alias";
2 |
3 | .top-banner {
4 | background: $yellow;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/basic/feature-b/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | // example banner
2 | export declare const topBanner: string;
3 |
--------------------------------------------------------------------------------
/examples/config-file/README.md:
--------------------------------------------------------------------------------
1 | # Configuration file example
2 |
3 | This example contains:
4 |
5 | - A custom configuration file with a custom SASS importer. You should only need a custom importer if you also use one as part of your build tool.
6 |
7 | The command to generate the proper type files would look like this (_in the root of this example, not repository_):
8 |
9 | ```bash
10 | npm run typed-scss-modules "./**/*.scss"
11 | ```
12 |
--------------------------------------------------------------------------------
/examples/config-file/feature/a.scss:
--------------------------------------------------------------------------------
1 | @import "variables.json";
2 |
3 | .class-name {
4 | color: $color-red;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/config-file/feature/a.scss.d.ts:
--------------------------------------------------------------------------------
1 | // config file banner
2 | export type Styles = {
3 | "class-name": string;
4 | };
5 |
6 | export type ClassNames = keyof Styles;
7 |
8 | declare const styles: Styles;
9 |
10 | export default styles;
11 |
--------------------------------------------------------------------------------
/examples/config-file/feature/variables.json:
--------------------------------------------------------------------------------
1 | {
2 | "color-red": "#c33",
3 | "color-blue": "#33c"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/config-file/typed-scss-modules.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | // eslint-disable-next-line @typescript-eslint/no-var-requires
3 | const jsonImporter = require("node-sass-json-importer");
4 |
5 | export const config = {
6 | aliases: { "not-real": "test-value" },
7 | aliasPrefixes: { "also-not-real": "test-value" },
8 | banner: "// config file banner",
9 | nameFormat: "kebab",
10 | exportType: "default",
11 | importer: jsonImporter(),
12 | };
13 |
--------------------------------------------------------------------------------
/examples/default-export/README.md:
--------------------------------------------------------------------------------
1 | # Default Export Example
2 |
3 | This example contains:
4 |
5 | - Class names that are expected to be kebab (param) cased. Since variables cannot contain a `-` this can be achieved using a type with default export.
6 | - Class names that are TypeScript keywords (eg: `while`) that cannot be used as named constants.
7 |
8 | The command to generate the proper type files would look like this (_in the root of this repository_):
9 |
10 | ```bash
11 | npm run typed-scss-modules "examples/default-export/**/*.scss" -- --exportType default --nameFormat kebab --banner '// example banner'
12 | ```
13 |
--------------------------------------------------------------------------------
/examples/default-export/feature-a/index.ts:
--------------------------------------------------------------------------------
1 | import styles, { ClassNames, Styles } from "./style.scss";
2 |
3 | console.log(styles.i);
4 | console.log(styles["i-am-kebab-cased"]);
5 |
6 | // Using the ClassNames union type to assign class names.
7 | const className: ClassNames = "i-am-kebab-cased";
8 |
9 | // Using the Styles type for reconstructing a subset.
10 | export const classNames: Partial = {
11 | [className]: "something",
12 | i: "a-string",
13 | };
14 |
--------------------------------------------------------------------------------
/examples/default-export/feature-a/style.scss:
--------------------------------------------------------------------------------
1 | .i {
2 | color: orange;
3 |
4 | &-am {
5 | &-kebab {
6 | &-cased {
7 | color: red;
8 | }
9 | }
10 | }
11 | }
12 |
13 | .while {
14 | color: blue;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/default-export/feature-a/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | // example banner
2 | export type Styles = {
3 | i: string;
4 | "i-am-kebab-cased": string;
5 | while: string;
6 | };
7 |
8 | export type ClassNames = keyof Styles;
9 |
10 | declare const styles: Styles;
11 |
12 | export default styles;
13 |
--------------------------------------------------------------------------------
/examples/output-folder/README.md:
--------------------------------------------------------------------------------
1 | # Output folder example
2 |
3 | This example contains:
4 |
5 | - A custom configuration file with a custom output folder. All type definitions will be generated in this directory.
6 |
7 | The command to generate the proper type files would look like this (_in the root of this example, not repository_):
8 |
9 | ```bash
10 | yarn typed-scss-modules "./**/*.scss"
11 | ```
12 |
--------------------------------------------------------------------------------
/examples/output-folder/__generated__/examples/output-folder/feature-a/a.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const a: string;
2 |
--------------------------------------------------------------------------------
/examples/output-folder/__generated__/examples/output-folder/feature-b/b.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const b: string;
2 |
--------------------------------------------------------------------------------
/examples/output-folder/__generated__/examples/output-folder/feature-c/c.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const c: string;
2 |
--------------------------------------------------------------------------------
/examples/output-folder/__generated__/examples/output-folder/feature-c/nested/nested.scss.d.ts:
--------------------------------------------------------------------------------
1 | export declare const nested: string;
2 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-a/a.scss:
--------------------------------------------------------------------------------
1 | .a {
2 | color: aqua;
3 | color: $text-color;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-a/index.ts:
--------------------------------------------------------------------------------
1 | import styles from "./a.scss";
2 |
3 | console.log(styles.a);
4 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-b/b.scss:
--------------------------------------------------------------------------------
1 | .b {
2 | background: blue;
3 | color: $text-color;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-b/index.ts:
--------------------------------------------------------------------------------
1 | import styles from "./b.scss";
2 |
3 | console.log(styles.b);
4 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-c/c.scss:
--------------------------------------------------------------------------------
1 | .c {
2 | background: crimson;
3 | color: $text-color;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-c/index.ts:
--------------------------------------------------------------------------------
1 | import styles from "./c.scss";
2 |
3 | console.log(styles.c);
4 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-c/nested/index.ts:
--------------------------------------------------------------------------------
1 | import styles from "./nested.scss";
2 |
3 | console.log(styles.nested);
4 |
--------------------------------------------------------------------------------
/examples/output-folder/feature-c/nested/nested.scss:
--------------------------------------------------------------------------------
1 | .nested {
2 | background: navy;
3 | color: $text-color;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/output-folder/typed-scss-modules.config.js:
--------------------------------------------------------------------------------
1 | export const config = {
2 | // Note: you likely would only need `__generated`.
3 | // This example includes `examples/output-folder` because it's nested within a project.
4 | outputFolder: "__generated__/examples/output-folder",
5 | // Example of declaring additional data for every stylesheet.
6 | // This could also be an import to global variables or mixins.
7 | additionalData: "$text-color: black;",
8 | };
9 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | clearMocks: true,
3 | testMatch: ["**/__tests__/**/*.test.ts"],
4 | testPathIgnorePatterns: [
5 | "/dist/",
6 | "/node_modules/",
7 | "(.*).d.ts",
8 | ],
9 | transformIgnorePatterns: [
10 | "[/\\\\]node_modules[/\\\\](?!bundle-require).+\\.js$",
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/lib/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import yargs from "yargs";
4 | import { IMPLEMENTATIONS } from "./implementations";
5 | import { main } from "./main";
6 | import { Aliases, NAME_FORMATS } from "./sass";
7 | import { EXPORT_TYPES, LOG_LEVELS, QUOTE_TYPES } from "./typescript";
8 |
9 | const { _: patterns, ...rest } = yargs
10 | .usage(
11 | "Generate .scss.d.ts from CSS module .scss files.\nUsage: $0 [options]"
12 | )
13 | .example("$0 src", "All .scss files at any level in the src directory")
14 | .example(
15 | "$0 src/**/*.scss",
16 | "All .scss files at any level in the src directory"
17 | )
18 | .example(
19 | "$0 src/**/*.scss --watch",
20 | "Watch all .scss files at any level in the src directory that are added or changed"
21 | )
22 | .example(
23 | "$0 src/**/*.scss --includePaths src/core src/variables",
24 | 'Search the "core" and "variables" directory when resolving imports'
25 | )
26 | .example(
27 | "$0 src/**/*.scss --aliases.~name variables",
28 | 'Replace all imports for "~name" with "variables"'
29 | )
30 | .example(
31 | "$0 src/**/*.scss --aliasPrefixes.~ ./node_modules/",
32 | 'Replace the "~" prefix with "./node_modules/" for all imports beginning with "~"'
33 | )
34 | .example(
35 | "$0 src/**/*.scss --ignore **/secret.scss",
36 | 'Ignore any file names "secret.scss"'
37 | )
38 | .example(
39 | "$0 src/**/*.scss --implementation sass",
40 | "Use the Dart SASS package"
41 | )
42 | .example(
43 | "$0 src/**/*.scss -e default --quoteType double",
44 | "Use double quotes around class name definitions rather than single quotes."
45 | )
46 | .example("$0 src/**/*.scss --logLevel error", "Output only errors")
47 | .demandCommand(1)
48 | .option("additionalData", {
49 | string: true,
50 | alias: "d",
51 | describe: "Prepends the SCSS code before each file.",
52 | })
53 | .option("aliases", {
54 | coerce: (obj: Aliases): Aliases => obj,
55 | alias: "a",
56 | describe: "Alias any import to any other value.",
57 | })
58 | .option("aliasPrefixes", {
59 | coerce: (obj: Aliases): Aliases => obj,
60 | alias: "p",
61 | describe: "A prefix for any import to rewrite to another value.",
62 | })
63 | .option("nameFormat", {
64 | alias: "n",
65 | array: true,
66 | string: true,
67 | choices: NAME_FORMATS,
68 | describe: "The name format that should be used to transform class names.",
69 | })
70 | .option("implementation", {
71 | choices: IMPLEMENTATIONS,
72 | describe:
73 | "The SASS package to used to compile. This will default to the sass implementation you have installed.",
74 | })
75 | .option("exportType", {
76 | choices: EXPORT_TYPES,
77 | alias: "e",
78 | describe: "The type of export used for defining the type definitions.",
79 | })
80 | .option("exportTypeName", {
81 | string: true,
82 | describe:
83 | 'Set a custom type name for styles when --exportType is "default."',
84 | })
85 | .option("exportTypeInterface", {
86 | string: true,
87 | describe:
88 | 'Set a custom interface name for styles when --exportType is "default."',
89 | })
90 | .option("watch", {
91 | boolean: true,
92 | alias: "w",
93 | describe:
94 | "Watch for added or changed files and (re-)generate the type definitions.",
95 | })
96 | .option("ignoreInitial", {
97 | boolean: true,
98 | describe: "Skips the initial build when passing the watch flag.",
99 | })
100 | .option("listDifferent", {
101 | boolean: true,
102 | alias: "l",
103 | describe:
104 | "List any type definitions that are different than those that would be generated.",
105 | })
106 | .option("includePaths", {
107 | array: true,
108 | string: true,
109 | alias: "i",
110 | describe: "Additional paths to include when trying to resolve imports.",
111 | })
112 | .option("ignore", {
113 | string: true,
114 | array: true,
115 | describe: "Add a pattern or an array of glob patterns to exclude matches.",
116 | })
117 | .option("outputFolder", {
118 | string: true,
119 | alias: "o",
120 | describe:
121 | "Define a (relative) folder to output the generated type definitions. Note this requires adding the output folder to tsconfig.json `rootDirs`.",
122 | })
123 | .options("quoteType", {
124 | choices: QUOTE_TYPES,
125 | alias: "q",
126 | describe:
127 | "Specify the quote type so that generated files adhere to your TypeScript rules.",
128 | })
129 | .options("updateStaleOnly", {
130 | boolean: true,
131 | alias: "u",
132 | describe:
133 | "Overwrite generated files only if the source file has more recent changes.",
134 | })
135 | .option("logLevel", {
136 | choices: LOG_LEVELS,
137 | alias: "L",
138 | describe: "Verbosity level of console output",
139 | })
140 | .options("banner", {
141 | string: true,
142 | describe:
143 | "Inserts text at the top of every output file for documentation purposes.",
144 | })
145 | .options("allowArbitraryExtensions", {
146 | boolean: true,
147 | describe:
148 | 'Output filenames that will be compatible with the "arbitrary file extensions" feature that was introduced in TypeScript 5.0.',
149 | })
150 | .parseSync();
151 |
152 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
153 | main(patterns[0] as string, { ...rest });
154 |
--------------------------------------------------------------------------------
/lib/core/alerts.ts:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 |
3 | export const LOG_LEVELS = ["verbose", "error", "info", "silent"] as const;
4 | export type LogLevel = (typeof LOG_LEVELS)[number];
5 |
6 | export const logLevelDefault: LogLevel = "verbose";
7 |
8 | let currentLogLevel: LogLevel | undefined;
9 |
10 | export const setAlertsLogLevel = (logLevel: LogLevel) => {
11 | currentLogLevel = logLevel;
12 | };
13 |
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | type CbFunc = (...args: any[]) => void;
16 | type WrappedCbFunc = (
17 | ...args: Parameters
18 | ) => ReturnType | void;
19 | /**
20 | * wraps a callback and only calls it if currentLogLevel is undefined or included in permittedLogLevels
21 | * @param permittedLogLevels list of log levels. callbacks will only be called if current log level is listed here
22 | * @param cb callback
23 | */
24 | const withLogLevelsRestriction =
25 | (permittedLogLevels: LogLevel[], cb: T): WrappedCbFunc =>
26 | (...args: Parameters): ReturnType | void => {
27 | const shouldCall =
28 | !currentLogLevel || permittedLogLevels.includes(currentLogLevel);
29 |
30 | if (shouldCall) {
31 | return cb(...args);
32 | }
33 | };
34 |
35 | const error = withLogLevelsRestriction(
36 | ["verbose", "error", "info"],
37 | (message: string) => console.log(chalk.red(message))
38 | );
39 | const warn = withLogLevelsRestriction(["verbose"], (message: string) =>
40 | console.log(chalk.yellowBright(message))
41 | );
42 | const notice = withLogLevelsRestriction(
43 | ["verbose", "info"],
44 | (message: string) => console.log(chalk.gray(message))
45 | );
46 | const info = withLogLevelsRestriction(["verbose", "info"], (message: string) =>
47 | console.log(chalk.blueBright(message))
48 | );
49 | const success = withLogLevelsRestriction(
50 | ["verbose", "info"],
51 | (message: string) => console.log(chalk.green(message))
52 | );
53 |
54 | export const alerts = { error, warn, notice, info, success };
55 |
--------------------------------------------------------------------------------
/lib/core/generate.ts:
--------------------------------------------------------------------------------
1 | import { alerts } from "./alerts";
2 | import { listFilesAndPerformSanityChecks } from "./list-files-and-perform-sanity-checks";
3 | import { ConfigOptions } from "./types";
4 | import { writeFile } from "./write-file";
5 |
6 | /**
7 | * Given a file glob generate the corresponding types once.
8 | *
9 | * @param pattern the file pattern to generate type definitions for
10 | * @param options the CLI options
11 | */
12 | export const generate = async (
13 | pattern: string,
14 | options: ConfigOptions
15 | ): Promise => {
16 | const files = listFilesAndPerformSanityChecks(pattern, options);
17 |
18 | if (files.length === 0) {
19 | return;
20 | }
21 |
22 | alerts.success(
23 | `Found ${files.length} file${
24 | files.length === 1 ? `` : `s`
25 | }. Generating type definitions...`
26 | );
27 |
28 | // Wait for all the type definitions to be written.
29 | await Promise.all(files.map((file) => writeFile(file, options)));
30 | };
31 |
--------------------------------------------------------------------------------
/lib/core/index.ts:
--------------------------------------------------------------------------------
1 | export { alerts, setAlertsLogLevel } from "./alerts";
2 | export { generate } from "./generate";
3 | export { listDifferent } from "./list-different";
4 | export { CLIOptions, ConfigOptions } from "./types";
5 | export { watch } from "./watch";
6 | export { writeFile } from "./write-file";
7 |
--------------------------------------------------------------------------------
/lib/core/list-different.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { fileToClassNames } from "../sass";
3 | import {
4 | classNamesToTypeDefinitions,
5 | getTypeDefinitionPath,
6 | } from "../typescript";
7 | import { alerts } from "./alerts";
8 | import { listFilesAndPerformSanityChecks } from "./list-files-and-perform-sanity-checks";
9 | import { ConfigOptions } from "./types";
10 |
11 | export const listDifferent = async (
12 | pattern: string,
13 | options: ConfigOptions
14 | ): Promise => {
15 | const files = listFilesAndPerformSanityChecks(pattern, options);
16 |
17 | // Wait for all the files to be checked.
18 | const validChecks = await Promise.all(
19 | files.map((file) => checkFile(file, options))
20 | );
21 | if (validChecks.includes(false)) {
22 | process.exit(1);
23 | }
24 | };
25 |
26 | export const checkFile = async (
27 | file: string,
28 | options: ConfigOptions
29 | ): Promise => {
30 | try {
31 | const classNames = await fileToClassNames(file, options);
32 | const typeDefinition = await classNamesToTypeDefinitions({
33 | classNames: classNames,
34 | file,
35 | ...options,
36 | });
37 |
38 | if (!typeDefinition) {
39 | // Assume if no type defs are necessary it's fine
40 | return true;
41 | }
42 |
43 | const path = getTypeDefinitionPath(file, options);
44 | if (!fs.existsSync(path)) {
45 | alerts.error(
46 | `[INVALID TYPES] Type file needs to be generated for ${file} `
47 | );
48 | return false;
49 | }
50 |
51 | const content = fs.readFileSync(path, { encoding: "utf8" });
52 | if (content !== typeDefinition) {
53 | alerts.error(`[INVALID TYPES] Check type definitions for ${file}`);
54 | return false;
55 | }
56 |
57 | return true;
58 | } catch (error) {
59 | alerts.error(
60 | `An error occurred checking ${file}:\n${JSON.stringify(error)}`
61 | );
62 | return false;
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/lib/core/list-files-and-perform-sanity-checks.ts:
--------------------------------------------------------------------------------
1 | import glob from "glob";
2 | import { alerts } from "./alerts";
3 | import { ConfigOptions } from "./types";
4 |
5 | /**
6 | * Return the files matching the given pattern and alert the user if only 0 or 1
7 | * files matched.
8 | *
9 | * @param pattern the file pattern to generate type definitions for
10 | * @param options the CLI options
11 | */
12 | export function listFilesAndPerformSanityChecks(
13 | pattern: string,
14 | options: ConfigOptions
15 | ): string[] {
16 | // Find all the files that match the provided pattern.
17 | const files = glob.sync(pattern, { ignore: options.ignore });
18 |
19 | if (!files || !files.length) {
20 | alerts.error("No files found.");
21 | }
22 |
23 | // This case still works as expected but it's easy to do on accident so
24 | // provide a (hopefully) helpful warning.
25 | if (files.length === 1) {
26 | alerts.warn(
27 | `Only 1 file found for ${pattern}. If using a glob pattern (eg: dir/**/*.scss) make sure to wrap in quotes (eg: "dir/**/*.scss").`
28 | );
29 | }
30 |
31 | return files;
32 | }
33 |
--------------------------------------------------------------------------------
/lib/core/remove-file.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { ConfigOptions } from ".";
3 | import { getTypeDefinitionPath } from "../typescript";
4 | import { alerts } from "./alerts";
5 |
6 | /**
7 | * Given a single file remove the file
8 | *
9 | * @param file any file to remove
10 | */
11 |
12 | const removeFile = (file: string): void => {
13 | try {
14 | if (fs.existsSync(file)) {
15 | fs.unlinkSync(file);
16 | alerts.success(`[REMOVED] ${file}`);
17 | }
18 | } catch (error) {
19 | alerts.error(
20 | `An error occurred removing ${file}:\n${JSON.stringify(error)}`
21 | );
22 | }
23 | };
24 |
25 | /**
26 | * Given a single file remove the generated types if they exist
27 | *
28 | * @param file the SCSS file to generate types for
29 | */
30 | export const removeSCSSTypeDefinitionFile = (
31 | file: string,
32 | options: ConfigOptions
33 | ): void => {
34 | const path = getTypeDefinitionPath(file, options);
35 | removeFile(path);
36 | };
37 |
--------------------------------------------------------------------------------
/lib/core/types.ts:
--------------------------------------------------------------------------------
1 | import { SASSOptions } from "../sass";
2 | import { ExportType, LogLevel, QuoteType } from "../typescript";
3 |
4 | type CLIOnlyOptions = Extract;
5 |
6 | export interface CLIOptions extends Exclude {
7 | banner: string;
8 | ignore: string[];
9 | ignoreInitial: boolean;
10 | exportType: ExportType;
11 | exportTypeName: string;
12 | exportTypeInterface: string;
13 | listDifferent: boolean;
14 | quoteType: QuoteType;
15 | updateStaleOnly: boolean;
16 | watch: boolean;
17 | logLevel: LogLevel;
18 | outputFolder: string | null;
19 | allowArbitraryExtensions: boolean;
20 | }
21 |
22 | export interface ConfigOptions extends CLIOptions, SASSOptions {}
23 |
--------------------------------------------------------------------------------
/lib/core/watch.ts:
--------------------------------------------------------------------------------
1 | import chokidar from "chokidar";
2 | import { alerts } from "./alerts";
3 | import { listFilesAndPerformSanityChecks } from "./list-files-and-perform-sanity-checks";
4 | import { removeSCSSTypeDefinitionFile } from "./remove-file";
5 | import { ConfigOptions } from "./types";
6 | import { writeFile } from "./write-file";
7 |
8 | /**
9 | * Watch a file glob and generate the corresponding types.
10 | *
11 | * @param pattern the file pattern to watch for file changes or additions
12 | * @param options the CLI options
13 | */
14 | export const watch = (pattern: string, options: ConfigOptions): void => {
15 | listFilesAndPerformSanityChecks(pattern, options);
16 |
17 | alerts.success("Watching files...");
18 |
19 | chokidar
20 | .watch(pattern, {
21 | ignoreInitial: options.ignoreInitial,
22 | ignored: options.ignore,
23 | })
24 | .on("change", (path) => {
25 | alerts.info(`[CHANGED] ${path}`);
26 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
27 | writeFile(path, options);
28 | })
29 | .on("add", (path) => {
30 | alerts.info(`[ADDED] ${path}`);
31 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
32 | writeFile(path, options);
33 | })
34 | .on("unlink", (path) => {
35 | alerts.info(`[REMOVED] ${path}`);
36 | removeSCSSTypeDefinitionFile(path, options);
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/lib/core/write-file.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { SassError } from "node-sass";
3 | import path from "path";
4 | import { fileToClassNames } from "../sass";
5 | import {
6 | classNamesToTypeDefinitions,
7 | getTypeDefinitionPath,
8 | } from "../typescript";
9 | import { alerts } from "./alerts";
10 | import { removeSCSSTypeDefinitionFile } from "./remove-file";
11 | import { CLIOptions } from "./types";
12 |
13 | /**
14 | * Given a single file generate the proper types.
15 | *
16 | * @param file the SCSS file to generate types for
17 | * @param options the CLI options
18 | */
19 | export const writeFile = async (
20 | file: string,
21 | options: CLIOptions
22 | ): Promise => {
23 | try {
24 | const classNames = await fileToClassNames(file, options);
25 | const typeDefinition = await classNamesToTypeDefinitions({
26 | classNames,
27 | file,
28 | ...options,
29 | });
30 |
31 | const typesPath = getTypeDefinitionPath(file, options);
32 | const typesExist = fs.existsSync(typesPath);
33 |
34 | // Avoid outputting empty type definition files.
35 | // If the file exists and the type definition is now empty, remove the file.
36 | if (!typeDefinition) {
37 | if (typesExist) {
38 | removeSCSSTypeDefinitionFile(file, options);
39 | } else {
40 | alerts.notice(`[NO GENERATED TYPES] ${file}`);
41 | }
42 | return;
43 | }
44 |
45 | // Avoid re-writing the file if it hasn't changed.
46 | // First by checking the file modification time, then
47 | // by comparing the file contents.
48 | if (options.updateStaleOnly && typesExist) {
49 | const fileModified = fs.statSync(file).mtime;
50 | const typeDefinitionModified = fs.statSync(typesPath).mtime;
51 |
52 | if (fileModified < typeDefinitionModified) {
53 | return;
54 | }
55 |
56 | const existingTypeDefinition = fs.readFileSync(typesPath, "utf8");
57 | if (existingTypeDefinition === typeDefinition) {
58 | return;
59 | }
60 | }
61 |
62 | // Files can be written to arbitrary directories and need to
63 | // be nested to match the project structure so it's possible
64 | // there are multiple directories that need to be created.
65 | const dirname = path.dirname(typesPath);
66 | if (!fs.existsSync(dirname)) {
67 | fs.mkdirSync(dirname, { recursive: true });
68 | }
69 |
70 | fs.writeFileSync(typesPath, typeDefinition);
71 | alerts.success(`[GENERATED TYPES] ${typesPath}`);
72 | } catch (error) {
73 | const { message, file, line, column } = error as SassError;
74 | const location = file ? ` (${file}[${line}:${column}])` : "";
75 | alerts.error(`${message}${location}`);
76 | }
77 | };
78 |
--------------------------------------------------------------------------------
/lib/implementations/index.ts:
--------------------------------------------------------------------------------
1 | import nodeSass from "node-sass";
2 | import sass from "sass";
3 |
4 | /**
5 | * A list of all possible SASS package implementations that can be used to
6 | * perform the compilation and parsing of the SASS files. The expectation is
7 | * that they provide a nearly identical API so they can be swapped out but
8 | * all of the same logic can be reused.
9 | */
10 | export const IMPLEMENTATIONS = ["node-sass", "sass"] as const;
11 | export type Implementations = (typeof IMPLEMENTATIONS)[number];
12 |
13 | type Implementation = typeof nodeSass | typeof sass;
14 |
15 | /**
16 | * Determine which default implementation to use by checking which packages
17 | * are actually installed and available to use.
18 | *
19 | * @param resolver DO NOT USE - this is unfortunately necessary only for testing.
20 | */
21 | export const getDefaultImplementation = (
22 | resolver: RequireResolve = require.resolve
23 | ): Implementations => {
24 | let pkg: Implementations = "node-sass";
25 |
26 | try {
27 | resolver("node-sass");
28 | } catch (error) {
29 | try {
30 | resolver("sass");
31 | pkg = "sass";
32 | } catch (ignoreError) {
33 | pkg = "node-sass";
34 | }
35 | }
36 |
37 | return pkg;
38 | };
39 |
40 | /**
41 | * Retrieve the desired implementation.
42 | *
43 | * @param implementation the desired implementation.
44 | */
45 | export const getImplementation = (
46 | implementation?: Implementations
47 | ): Implementation => {
48 | if (implementation === "sass") {
49 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
50 | return require("sass");
51 | } else {
52 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
53 | return require("node-sass");
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { main as default } from "./main";
2 |
--------------------------------------------------------------------------------
/lib/load.ts:
--------------------------------------------------------------------------------
1 | import { bundleRequire } from "bundle-require";
2 | import JoyCon from "joycon";
3 | import path from "path";
4 | import { alerts, CLIOptions, ConfigOptions } from "./core";
5 | import { getDefaultImplementation } from "./implementations";
6 | import { nameFormatDefault } from "./sass";
7 | import {
8 | bannerTypeDefault,
9 | exportTypeDefault,
10 | exportTypeInterfaceDefault,
11 | exportTypeNameDefault,
12 | logLevelDefault,
13 | quoteTypeDefault,
14 | } from "./typescript";
15 |
16 | const VALID_CONFIG_FILES = [
17 | "typed-scss-modules.config.ts",
18 | "typed-scss-modules.config.js",
19 | ];
20 | const joycon = new JoyCon();
21 |
22 | /**
23 | * Load a custom config file in the project root directory with any options for this package.
24 | *
25 | * This supports config files in the following formats and order:
26 | * - Named `config` export: `export const config = {}`
27 | * - Default export: `export default {}`
28 | * - `module.exports = {}`
29 | */
30 | export const loadConfig = async (): Promise<
31 | Record | ConfigOptions
32 | > => {
33 | const CURRENT_WORKING_DIRECTORY = process.cwd();
34 |
35 | const configPath = await joycon.resolve(
36 | VALID_CONFIG_FILES,
37 | CURRENT_WORKING_DIRECTORY,
38 | path.parse(CURRENT_WORKING_DIRECTORY).root
39 | );
40 |
41 | if (configPath) {
42 | try {
43 | const configModule = await bundleRequire({
44 | filepath: configPath,
45 | });
46 |
47 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
48 | const config: ConfigOptions =
49 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
50 | configModule.mod.config || configModule.mod.default || configModule.mod;
51 |
52 | return config;
53 | } catch (error) {
54 | alerts.error(
55 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
56 | `An error occurred loading the config file "${configPath}":\n${error}`
57 | );
58 |
59 | return {};
60 | }
61 | }
62 |
63 | return {};
64 | };
65 |
66 | // Default values for all options that need defaults.
67 | export const DEFAULT_OPTIONS: CLIOptions = {
68 | nameFormat: [nameFormatDefault],
69 | implementation: getDefaultImplementation(),
70 | exportType: exportTypeDefault,
71 | exportTypeName: exportTypeNameDefault,
72 | exportTypeInterface: exportTypeInterfaceDefault,
73 | watch: false,
74 | ignoreInitial: false,
75 | listDifferent: false,
76 | ignore: [],
77 | quoteType: quoteTypeDefault,
78 | updateStaleOnly: false,
79 | logLevel: logLevelDefault,
80 | banner: bannerTypeDefault,
81 | outputFolder: null,
82 | allowArbitraryExtensions: false,
83 | };
84 |
85 | const removedUndefinedValues = >(
86 | obj: Obj
87 | ): Obj => {
88 | for (const key in obj) {
89 | if (obj[key] === undefined) {
90 | delete obj[key];
91 | }
92 | }
93 |
94 | return obj;
95 | };
96 |
97 | /**
98 | * Given both the CLI and config file options merge into a single options object.
99 | *
100 | * When possible, CLI options will override config file options.
101 | *
102 | * Some options are only available in the config file. For example, a custom function can't
103 | * be easily defined via the CLI so some complex options are only available in the config file.
104 | */
105 | export const mergeOptions = (
106 | cliOptions: Partial,
107 | configOptions: Partial
108 | ): ConfigOptions => {
109 | return {
110 | ...DEFAULT_OPTIONS,
111 | ...configOptions,
112 | ...removedUndefinedValues(cliOptions),
113 | };
114 | };
115 |
--------------------------------------------------------------------------------
/lib/main.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import slash from "slash";
4 | import {
5 | CLIOptions,
6 | generate,
7 | listDifferent,
8 | setAlertsLogLevel,
9 | watch,
10 | } from "./core";
11 | import { loadConfig, mergeOptions } from "./load";
12 |
13 | export const main = async (
14 | pattern: string,
15 | cliOptions: Partial
16 | ) => {
17 | const configOptions = await loadConfig();
18 | const options = mergeOptions(cliOptions, configOptions);
19 |
20 | setAlertsLogLevel(options.logLevel);
21 |
22 | // When the provided pattern is a directory construct the proper glob to find
23 | // all .scss files within that directory. Also, add the directory to the
24 | // included paths so any imported with a path relative to the root of the
25 | // project still works as expected without adding many include paths.
26 | if (fs.existsSync(pattern) && fs.lstatSync(pattern).isDirectory()) {
27 | if (Array.isArray(options.includePaths)) {
28 | options.includePaths.push(pattern);
29 | } else {
30 | options.includePaths = [pattern];
31 | }
32 |
33 | // When the pattern provide is a directory, assume all .scss files within.
34 | pattern = slash(path.resolve(pattern, "**/*.scss"));
35 | }
36 |
37 | if (options.listDifferent) {
38 | await listDifferent(pattern, options);
39 | return;
40 | }
41 |
42 | if (options.watch) {
43 | watch(pattern, options);
44 | } else {
45 | await generate(pattern, options);
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/lib/prettier/can-resolve.ts:
--------------------------------------------------------------------------------
1 | // this is only extracted to a module to mock in testing as require.resolve can't be mocked.
2 | // https://github.com/facebook/jest/issues/9543
3 | export function canResolvePrettier() {
4 | try {
5 | require.resolve("prettier");
6 | return true;
7 | } catch (_error) {
8 | // cannot resolve prettier
9 | return false;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/lib/prettier/index.ts:
--------------------------------------------------------------------------------
1 | import { format, resolveConfig } from "prettier";
2 | import { alerts } from "../core";
3 | import { canResolvePrettier } from "./can-resolve";
4 |
5 | interface Prettier {
6 | format: typeof format;
7 | resolveConfig: typeof resolveConfig;
8 | }
9 |
10 | const isPrettier = (t: unknown): t is Prettier =>
11 | !!t &&
12 | typeof t === "object" &&
13 | t !== null &&
14 | "format" in t &&
15 | typeof (t as Prettier).format === "function" &&
16 | "resolveConfig" in t &&
17 | typeof (t as Prettier).resolveConfig === "function";
18 |
19 | /**
20 | * Try to load prettier and config from project to format input,
21 | * fall back to input if prettier is not found or failed
22 | *
23 | * @param {file} file
24 | * @param {string} input
25 | */
26 | export const attemptPrettier = async (file: string, input: string) => {
27 | if (!canResolvePrettier()) {
28 | return input;
29 | }
30 |
31 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment
32 | const prettier = require("prettier");
33 | if (!isPrettier(prettier)) {
34 | // doesn't look like prettier
35 | return input;
36 | }
37 |
38 | try {
39 | const config = await prettier.resolveConfig(file, {
40 | editorconfig: true,
41 | });
42 | // try to return formatted output
43 | return prettier.format(input, { ...config, parser: "typescript" });
44 | } catch (error) {
45 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
46 | alerts.notice(`Tried using prettier, but failed with error: ${error}`);
47 | }
48 |
49 | // failed to format
50 | return input;
51 | };
52 |
--------------------------------------------------------------------------------
/lib/sass/file-to-class-names.ts:
--------------------------------------------------------------------------------
1 | import {
2 | camelCase,
3 | camelCaseTransformMerge,
4 | paramCase,
5 | snakeCase,
6 | } from "change-case";
7 | import fs from "fs";
8 | import { Implementations, getImplementation } from "../implementations";
9 | import { Aliases, SASSImporterOptions, customImporters } from "./importer";
10 | import { sourceToClassNames } from "./source-to-class-names";
11 |
12 | export { Aliases };
13 | export type ClassName = string;
14 | interface Transformer {
15 | (className: ClassName): string;
16 | }
17 |
18 | const transformersMap = {
19 | camel: (className: ClassName) =>
20 | camelCase(className, { transform: camelCaseTransformMerge }),
21 | dashes: (className: ClassName) =>
22 | /-/.test(className) ? camelCase(className) : className,
23 | kebab: (className: ClassName) => transformersMap.param(className),
24 | none: (className: ClassName) => className,
25 | param: (className: ClassName) => paramCase(className),
26 | snake: (className: ClassName) => snakeCase(className),
27 | } as const;
28 |
29 | type NameFormatWithTransformer = keyof typeof transformersMap;
30 | const NAME_FORMATS_WITH_TRANSFORMER = Object.keys(
31 | transformersMap
32 | ) as NameFormatWithTransformer[];
33 |
34 | export const NAME_FORMATS = [...NAME_FORMATS_WITH_TRANSFORMER, "all"] as const;
35 | export type NameFormat = (typeof NAME_FORMATS)[number];
36 |
37 | export interface SASSOptions extends SASSImporterOptions {
38 | additionalData?: string;
39 | includePaths?: string[];
40 | nameFormat?: string | string[];
41 | implementation: Implementations;
42 | }
43 | export const nameFormatDefault: NameFormatWithTransformer = "camel";
44 |
45 | export const fileToClassNames = async (
46 | file: string,
47 | {
48 | additionalData,
49 | includePaths = [],
50 | nameFormat: rawNameFormat,
51 | implementation,
52 | aliases,
53 | aliasPrefixes,
54 | importer,
55 | }: SASSOptions = {} as SASSOptions
56 | ) => {
57 | const { renderSync } = getImplementation(implementation);
58 |
59 | const nameFormat = (
60 | typeof rawNameFormat === "string" ? [rawNameFormat] : rawNameFormat
61 | ) as NameFormat[];
62 |
63 | const nameFormats: NameFormatWithTransformer[] = nameFormat
64 | ? nameFormat.includes("all")
65 | ? NAME_FORMATS_WITH_TRANSFORMER
66 | : (nameFormat as NameFormatWithTransformer[])
67 | : [nameFormatDefault];
68 |
69 | const data = fs.readFileSync(file).toString();
70 | const result = renderSync({
71 | file,
72 | data: additionalData ? `${additionalData}\n${data}` : data,
73 | includePaths,
74 | importer: customImporters({ aliases, aliasPrefixes, importer }),
75 | });
76 |
77 | const classNames = await sourceToClassNames(result.css, file);
78 | const transformers = nameFormats.map((item) => transformersMap[item]);
79 | const transformedClassNames = new Set([]);
80 | classNames.forEach((className: ClassName) => {
81 | transformers.forEach((transformer: Transformer) => {
82 | transformedClassNames.add(transformer(className));
83 | });
84 | });
85 |
86 | return Array.from(transformedClassNames).sort((a, b) => a.localeCompare(b));
87 | };
88 |
--------------------------------------------------------------------------------
/lib/sass/importer.ts:
--------------------------------------------------------------------------------
1 | import { SyncImporter } from "node-sass";
2 | import { LegacySyncImporter } from "sass";
3 |
4 | // Hacky way to merge both dart-sass and node-sass importer definitions.
5 | type Importer = LegacySyncImporter & SyncImporter;
6 |
7 | export { Importer };
8 |
9 | export interface Aliases {
10 | [index: string]: string;
11 | }
12 |
13 | interface AliasImporterOptions {
14 | aliases: Aliases;
15 | aliasPrefixes: Aliases;
16 | }
17 |
18 | /**
19 | * Construct a SASS importer to create aliases for imports.
20 | */
21 | export const aliasImporter =
22 | ({ aliases, aliasPrefixes }: AliasImporterOptions): Importer =>
23 | (url: string) => {
24 | if (url in aliases) {
25 | const file = aliases[url];
26 |
27 | return {
28 | file,
29 | };
30 | }
31 |
32 | const prefixMatch = Object.keys(aliasPrefixes).find((prefix) =>
33 | url.startsWith(prefix)
34 | );
35 |
36 | if (prefixMatch) {
37 | return {
38 | file: aliasPrefixes[prefixMatch] + url.substr(prefixMatch.length),
39 | };
40 | }
41 |
42 | return null;
43 | };
44 |
45 | export interface SASSImporterOptions {
46 | aliases?: Aliases;
47 | aliasPrefixes?: Aliases;
48 | importer?: Importer | Importer[];
49 | }
50 |
51 | /**
52 | * Construct custom SASS importers based on options.
53 | *
54 | * - Given aliases and alias prefix options, add a custom alias importer.
55 | * - Given custom SASS importer(s), append to the list of importers.
56 | */
57 | export const customImporters = ({
58 | aliases = {},
59 | aliasPrefixes = {},
60 | importer,
61 | }: SASSImporterOptions): Importer[] => {
62 | const importers: Importer[] = [aliasImporter({ aliases, aliasPrefixes })];
63 |
64 | if (typeof importer === "function") {
65 | importers.push(importer);
66 | } else if (Array.isArray(importer)) {
67 | importers.push(...importer);
68 | }
69 |
70 | return importers;
71 | };
72 |
--------------------------------------------------------------------------------
/lib/sass/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | Aliases,
3 | NAME_FORMATS,
4 | SASSOptions,
5 | fileToClassNames,
6 | nameFormatDefault,
7 | } from "./file-to-class-names";
8 |
--------------------------------------------------------------------------------
/lib/sass/source-to-class-names.ts:
--------------------------------------------------------------------------------
1 | import postcss from "postcss";
2 | import PostcssModulesPlugin from "postcss-modules";
3 |
4 | /**
5 | * Converts a CSS source string to a list of exports (class names, keyframes, etc.)
6 | */
7 | export const sourceToClassNames = async (
8 | source: { toString(): string },
9 | file: string
10 | ) => {
11 | let result: Record = {};
12 | await postcss([
13 | PostcssModulesPlugin({
14 | getJSON: (_, json) => {
15 | result = json;
16 | },
17 | }),
18 | ]).process(source, { from: file });
19 | return Object.keys(result);
20 | };
21 |
--------------------------------------------------------------------------------
/lib/typescript/class-names-to-type-definition.ts:
--------------------------------------------------------------------------------
1 | import { ClassName } from "lib/sass/file-to-class-names";
2 | import os from "os";
3 | import reserved from "reserved-words";
4 | import { alerts } from "../core";
5 | import { attemptPrettier } from "../prettier";
6 |
7 | export type ExportType = "named" | "default";
8 | export const EXPORT_TYPES: ExportType[] = ["named", "default"];
9 |
10 | export type QuoteType = "single" | "double";
11 | export const QUOTE_TYPES: QuoteType[] = ["single", "double"];
12 |
13 | export interface TypeDefinitionOptions {
14 | banner: string;
15 | classNames: ClassName[];
16 | file: string;
17 | exportType: ExportType;
18 | exportTypeName?: string;
19 | exportTypeInterface?: string;
20 | quoteType?: QuoteType;
21 | }
22 |
23 | export const exportTypeDefault: ExportType = "named";
24 | export const exportTypeNameDefault: string = "ClassNames";
25 | export const exportTypeInterfaceDefault: string = "Styles";
26 | export const quoteTypeDefault: QuoteType = "single";
27 | export const bannerTypeDefault: string = "";
28 |
29 | const classNameToNamedTypeDefinition = (className: ClassName) =>
30 | `export declare const ${className}: string;`;
31 |
32 | const classNameToType = (className: ClassName, quoteType: QuoteType) => {
33 | const quote = quoteType === "single" ? "'" : '"';
34 | return ` ${quote}${className}${quote}: string;`;
35 | };
36 |
37 | const isReservedKeyword = (className: ClassName) =>
38 | reserved.check(className, "es5", true) ||
39 | reserved.check(className, "es6", true);
40 |
41 | const isValidName = (className: ClassName) => {
42 | if (isReservedKeyword(className)) {
43 | alerts.warn(
44 | `[SKIPPING] '${className}' is a reserved keyword (consider renaming or using --exportType default).`
45 | );
46 | return false;
47 | } else if (/-/.test(className)) {
48 | alerts.warn(
49 | `[SKIPPING] '${className}' contains dashes (consider using 'camelCase' or 'dashes' for --nameFormat or using --exportType default).`
50 | );
51 | return false;
52 | }
53 |
54 | return true;
55 | };
56 |
57 | export const classNamesToTypeDefinitions = async (
58 | options: TypeDefinitionOptions
59 | ): Promise => {
60 | if (options.classNames.length) {
61 | const lines: string[] = [];
62 |
63 | const {
64 | exportTypeName: ClassNames = exportTypeNameDefault,
65 | exportTypeInterface: Styles = exportTypeInterfaceDefault,
66 | } = options;
67 |
68 | switch (options.exportType) {
69 | case "default":
70 | if (options.banner) lines.push(options.banner);
71 |
72 | lines.push(`export type ${Styles} = {`);
73 | lines.push(
74 | ...options.classNames.map((className) =>
75 | classNameToType(className, options.quoteType || quoteTypeDefault)
76 | )
77 | );
78 | lines.push(`};${os.EOL}`);
79 |
80 | lines.push(`export type ${ClassNames} = keyof ${Styles};${os.EOL}`);
81 | lines.push(`declare const styles: ${Styles};${os.EOL}`);
82 | lines.push(`export default styles;`);
83 |
84 | break;
85 | case "named":
86 | if (options.banner) lines.push(options.banner);
87 |
88 | lines.push(
89 | ...options.classNames
90 | .filter(isValidName)
91 | .map(classNameToNamedTypeDefinition)
92 | );
93 |
94 | break;
95 | }
96 |
97 | if (lines.length) {
98 | const typeDefinition = lines.join(`${os.EOL}`) + `${os.EOL}`;
99 | return await attemptPrettier(options.file, typeDefinition);
100 | } else {
101 | return null;
102 | }
103 | } else {
104 | return null;
105 | }
106 | };
107 |
--------------------------------------------------------------------------------
/lib/typescript/get-type-definition-path.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { ConfigOptions } from "../core";
3 |
4 | const CURRENT_WORKING_DIRECTORY = process.cwd();
5 |
6 | /**
7 | * Given a file path to a SCSS file, generate the corresponding type definition
8 | * file path.
9 | *
10 | * @param file the SCSS file path
11 | */
12 | export const getTypeDefinitionPath = (
13 | file: string,
14 | options: ConfigOptions
15 | ): string => {
16 | let resolvedPath = file;
17 |
18 | if (options.outputFolder) {
19 | const relativePath = path.relative(CURRENT_WORKING_DIRECTORY, file);
20 | resolvedPath = path.resolve(
21 | CURRENT_WORKING_DIRECTORY,
22 | options.outputFolder,
23 | relativePath
24 | );
25 | }
26 |
27 | if (options.allowArbitraryExtensions) {
28 | const resolvedDirname = path.dirname(resolvedPath);
29 | // Note: `ext` includes a leading period (e.g. '.scss')
30 | const { name, ext } = path.parse(resolvedPath);
31 | // @see https://www.typescriptlang.org/tsconfig/#allowArbitraryExtensions
32 | return path.join(resolvedDirname, `${name}.d${ext}.ts`);
33 | } else {
34 | return `${resolvedPath}.d.ts`;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/lib/typescript/index.ts:
--------------------------------------------------------------------------------
1 | export { LOG_LEVELS, LogLevel, logLevelDefault } from "../core/alerts";
2 | export {
3 | EXPORT_TYPES,
4 | ExportType,
5 | QUOTE_TYPES,
6 | QuoteType,
7 | bannerTypeDefault,
8 | classNamesToTypeDefinitions,
9 | exportTypeDefault,
10 | exportTypeInterfaceDefault,
11 | exportTypeNameDefault,
12 | quoteTypeDefault,
13 | } from "./class-names-to-type-definition";
14 | export { getTypeDefinitionPath } from "./get-type-definition-path";
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typed-scss-modules",
3 | "version": "0.0.0",
4 | "description": "TypeScript type definition generator for SCSS CSS Modules",
5 | "main": "dist/lib/index.js",
6 | "types": "dist/lib/index.d.ts",
7 | "author": "Spencer Miskoviak ",
8 | "license": "MIT",
9 | "engines": {
10 | "node": ">=16"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/skovy/typed-scss-modules.git"
15 | },
16 | "homepage": "https://github.com/skovy/typed-scss-modules.git#readme",
17 | "keywords": [
18 | "scss",
19 | "css modules",
20 | "cli",
21 | "typescript",
22 | "type generator",
23 | "scss modules"
24 | ],
25 | "files": [
26 | "dist/lib"
27 | ],
28 | "bin": "./dist/lib/cli.js",
29 | "scripts": {
30 | "test": "jest",
31 | "test:watch": "jest --watch",
32 | "typed-scss-modules": "ts-node ./lib/cli.ts",
33 | "clean": "rm -rf ./dist",
34 | "build": "npm run clean && tsc && chmod +x dist/lib/cli.js",
35 | "prepare": "npm run build && husky install",
36 | "check-types": "tsc --noEmit",
37 | "check-formatting": "prettier --check '**/*.{js,json,css,md,scss,tsx,ts}'",
38 | "check-linting": "eslint .",
39 | "commit": "commit",
40 | "semantic-release": "semantic-release"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.22.9",
44 | "@babel/preset-env": "^7.22.9",
45 | "@babel/preset-typescript": "^7.22.5",
46 | "@commitlint/cli": "^16.2.1",
47 | "@commitlint/config-conventional": "^16.2.1",
48 | "@commitlint/prompt-cli": "^16.2.1",
49 | "@types/glob": "^7.2.0",
50 | "@types/jest": "^29.5.3",
51 | "@types/node": "^17.0.18",
52 | "@types/node-sass": "^4.11.3",
53 | "@types/prettier": "^2.7.3",
54 | "@types/reserved-words": "^0.1.0",
55 | "@types/sass": "^1.16.0",
56 | "@types/yargs": "^17.0.8",
57 | "@typescript-eslint/eslint-plugin": "^6.3.0",
58 | "@typescript-eslint/parser": "^6.3.0",
59 | "babel-jest": "^29.6.2",
60 | "babel-plugin-transform-import-meta": "^2.2.1",
61 | "eslint": "^8.46.0",
62 | "eslint-plugin-jest": "^27.2.3",
63 | "eslint-plugin-jest-formatting": "^3.1.0",
64 | "eslint-plugin-promise": "^6.1.1",
65 | "husky": "^7.0.4",
66 | "jest": "^29.6.2",
67 | "lint-staged": "^12.3.4",
68 | "node-sass": "7.0.3",
69 | "node-sass-json-importer": "^4.3.0",
70 | "prettier": "^2.8.8",
71 | "prettier-plugin-organize-imports": "^3.2.3",
72 | "sass": "^1.49.7",
73 | "semantic-release": "^17.4.7",
74 | "ts-node": "^10.9.1",
75 | "typescript": "^5.1.6"
76 | },
77 | "peerDependencies": {
78 | "node-sass": "^7.0.3 || ^8.0.0",
79 | "sass": "^1.49.7"
80 | },
81 | "peerDependenciesMeta": {
82 | "node-sass": {
83 | "optional": true
84 | },
85 | "sass": {
86 | "optional": true
87 | }
88 | },
89 | "dependencies": {
90 | "bundle-require": "^4.0.4",
91 | "chalk": "4.1.2",
92 | "change-case": "^4.1.2",
93 | "chokidar": "^3.5.3",
94 | "esbuild": "^0.17.0",
95 | "glob": "^7.2.0",
96 | "joycon": "^3.1.1",
97 | "postcss": "^8.4.27",
98 | "postcss-modules": "^6.0.0",
99 | "reserved-words": "^0.1.2",
100 | "slash": "^3.0.0",
101 | "yargs": "^17.3.1"
102 | },
103 | "lint-staged": {
104 | "*.{js,json,css,md,scss,tsx,ts}": [
105 | "prettier --write"
106 | ]
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "strict": true,
6 | "esModuleInterop": true,
7 | "skipLibCheck": true,
8 | "baseUrl": ".",
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "declaration": true,
13 | "outDir": "./dist",
14 | "rootDirs": [".", "examples/output-folder/__generated__"]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------