type-css-modules
v1.0.13
Published
Generate declaration files for CSS modules
Downloads
70
Readme
type-css-modules
Generate declaration files for CSS modules
Why use this package?
The type definition from @types/css-modules
is easy for humans to understand, but not specific enough for programs:
declare module '*.css' {
const styles: Record<string, string>;
export default styles;
}
First, you will run into poor developer experience (DX) when noPropertyAccessFromIndexSignature
is enabled.
{{! app/components/ui/page.hbs }}
{{! This should work, but results in an error. }}
<div class={{this.styles.container}}>
{{!-- ↳ Property 'container' comes from an index signature, so it must be accessed with {{get ... 'container'}}. --}}
</div>
{{! A workaround }}
<div class={{get this.styles "container"}}>
</div>
/* app/components/ui/page.gts */
import styles from './page.css';
<template>
// This should work, but results in an error.
<div class={{styles.container}}>
// ↳ Property 'container' comes from an index signature, so it must be accessed with ['container'].
</div>
// A workaround
<div class={{styles['container']}}>
</div>
</template>
Second, the loose definition may be incompatible with libraries that provide types (e.g. qunit-dom
). You will overuse the non-null assertion operator !
.
/* tests/integration/components/ui/page-test.ts */
import styles from 'app/components/ui/page.css';
// This should work, but results in an error.
assert
.dom('[data-test-container]')
.hasClass(styles.container);
// ↳ Argument of type 'string | undefined' is not assignable to parameter of type 'string | RegExp'.
// Type 'undefined' is not assignable to 'string | RegExp'.
// A workaround
assert
.dom('[data-test-container]')
.hasClass(styles['container']!);
When you provide accurate types, libraries (e.g. Glint
, embroider-css-modules
) improve your DX in return. You can catch typos and type issues early.
{{! app/components/ui/page.hbs }}
<div class={{local this.styles "ontainer"}}> {{! ⚠️ Property 'ontainer' is missing }}
<h1 class={{this.styles.head}}> {{! ⚠️ Property 'head' does not exist }}
{{@title}}
</h1>
<div class={{local this.style "body"}}> {{! ⚠️ Did you mean 'styles'? }}
{{yield}}
</div>
</div>
How to use this package?
Install type-css-modules
as a development dependency. Ensure that CSS declaration files exist before checking types; for example, you can write a pre-script.
/* package.json */
{
"scripts": {
"prelint:types": "type-css-modules <arguments>",
"lint:types": "tsc --noEmit" // or "glint"
},
"devDependencies": {
"type-css-modules": "...",
"typescript": "..."
}
}
Arguments
You must pass --src
to indicate the location(s) of your CSS files.
# One source directory
type-css-modules --src app
# Multiple source directories
type-css-modules --src app/components app/controllers
Pass --root
to run the codemod on a project somewhere else (i.e. not in the current directory).
type-css-modules --root <path/to/your/project>
Use Prettier?
type-css-modules
adds quotation marks in declaration files. This way, the names of CSS class selectors can always be used as object keys.
To separate formatting concerns, configure Prettier to handle *.css.d.ts
files differently.
/* .prettierrc.js */
module.exports = {
overrides: [
{
files: '*.css.d.ts',
options: {
quoteProps: 'preserve',
},
},
],
};
Can I use the file extension *.module.css?
Yes! You may use *.module.css
to indicate the stylesheets that are for CSS modules. type-css-modules
will create declaration files with the extension *.module.css.d.ts
.
The Prettier configuration (shown above) can remain as is.
Limitations
To reduce complexity, type-css-modules
expects you to follow the conventions of embroider-css-modules
:
- Give the local scope to the styles that you own1
- Avoid nesting styles2
- Use the default import to import styles
Here are some examples that meet the syntax requirements.
/* app/components/ui/page.css */
.container {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - 3em);
overflow-y: auto;
padding: 1.5rem 1rem;
scrollbar-gutter: stable;
}
.header {
grid-area: header;
}
.body {
grid-area: body;
}
{{! app/components/ui/page.hbs }}
<div class={{local this.styles "container"}}>
<h1 class={{this.styles.header}}>
{{@title}}
</h1>
<div class="{{this.styles.body}}">
{{yield}}
</div>
</div>
/* app/components/ui/page.ts */
import Component from '@glimmer/component';
import styles from './page.css';
export default class UiPageComponent extends Component {
styles = styles;
}
/* app/components/ui/page.gts */
import { local } from 'embroider-css-modules';
import styles from './page.css';
<template>
<div class={{local styles "container"}}>
<h1 class={{styles.header}}>
{{@title}}
</h1>
<div class="{{styles.body}}">
{{yield}}
</div>
</div>
</template>
And some counterexamples (what not to do):
/* app/components/ui/page.css */
:local(.container) {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - 3em);
overflow-y: auto;
padding: 1.5rem 1rem;
scrollbar-gutter: stable;
}
:local(.header) {
grid-area: header;
}
:local(.body) {
grid-area: body;
}
/* app/components/ui/page.css */
.container {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - 3em);
overflow-y: auto;
padding: 1.5rem 1rem;
scrollbar-gutter: stable;
.header {
grid-area: header;
}
.body {
grid-area: body;
}
}
/* app/components/ui/page.gts */
import { container, header, body } from './page.css';
<template>
<div class={{container}}>
<h1 class={{header}}>
{{@title}}
</h1>
<div class="{{body}}">
{{yield}}
</div>
</div>
</template>
1. With webpack
, for example, you can configure mode
to be a function that returns 'local'
or 'global'
. In stylesheets, you can use the :global()
pseudo-class selector to refer to "things from outside."
2. CSS nesting is in spec. To reduce maintenance cost, type-css-modules
will leave it up to css-tree
to parse nested styles (see issue #210).
Compatibility
- Node.js v18 or above
Contributing
See the Contributing guide for details.
License
This project is licensed under the MIT License.