typescript-plugin-css-modules
typescript-plugin-css-modules copied to clipboard
SCSS - Importing SCSS variables globally breaks exported TS type to `{}`.
Thanks for the awesome TS plugin! 💪
Describe the bug
There's certain cases when using SASS where you need the value of a variable and need to wrap it in #{} in order to get the variable output and not the variable name.
When adding #{} to a *.module.scss, the file's classNames etc are no longer exported and only shows {} in the TS file.
To Reproduce
Add the following to any *.module.scss file, and try and import it to a TS file.
container.module.scss
@use "variables" as *; // Which includes $breakpoint
.container {
@media (min-width: #{$breakpoint}) {
color: red;
}
}
Expected behavior
Using the #{} functionality shouldn't remove the classNames.
Desktop (please complete the following information):
- OS: iOS
- Browser: Chrome
- Version: 96.
I think it might be related to using sass variables in @media selectors, as I can see I get the same issue without the #{} syntax.
Works
.container {
@media (min-width: 768px) {
color: red;
}
}

Don't work
@use "variables" as *; // Which includes $breakpoint
.container {
@media (min-width: $breakpoint) {
color: red;
}
}

Both of these examples makes the imported object type be an empty object {}.
Is there anything I can do to help debug the issue and help fix it? Can you guide me in a direction?
I think it's related to https://github.com/mrmckeb/typescript-plugin-css-modules/issues/146.
Trying to add the variable directly to the scss fixed the error.
I have a SCSS configuration in my NextJS app which includes all my resources, and I think that needs to be set in the same way for the plugin.
const nextConfig = {
sassOptions: {
prependData: '@use "libs/theme/src/lib/resources.scss" as *;',
},
...
}
I will try and update the tsconfig plugin settings, and see if I can get it to work :)
I have a similar issue. I think the plugin is unable to resolve any SASS variables that are defined in additionalData or sassOptions.prependData inside webpack.config.js.
// src/css1.module.scss
// $class-prefix is defined in additionalData
// which I think `typescript-plugin-css-modules`
// is unable to resolve
.test {
font-size: 24px;
:global {
.#{$class-prefix} {
font-size: 24px;
}
}
}
// src/css2.module.scss
// $class-prefix is defined within the SASS files
// where `typescript-plugin-css-modules` can resolve its
// value
$class-prefix: 'hello';
.test {
:global {
.#{$class-prefix} {
font-size: 24px;
}
}
}
// src/Component.tsx
// This one will not work, throwing type errors underlined in css1.test:
// (alias) let css1: {}
import css1 from './css1.module.scss';
// This one works:
// (alias) let css2: {
// test: string;
// }
import css2 from './css2.module.scss';
const SomeComponent = () => {
return (
<div>
<div className={css1.test}><span className="hello">Hello!</div></div>
<div className={css2.test}><span className="hello">Hello!</div></div>
</div>
)
}
// webpack.config.js
module.exports = {
// ....
module: {
// ....
rules: [
// ....
{
test: /\.(scss|sass)$/,
use: [
{
loader: 'sass-loader',
options: {
additionalData: `$class-prefix: ${CLASS_PREFIX};`
}
},
]
}
// ....
],
// ....
},
// ....
}
I think it's related to #146.
Trying to add the variable directly to the
scssfixed the error. I have a SCSS configuration in myNextJSapp which includes all my resources, and I think that needs to be set in the same way for the plugin.const nextConfig = { sassOptions: { prependData: '@use "libs/theme/src/lib/resources.scss" as *;', }, ... }I will try and update the
tsconfigplugin settings, and see if I can get it to work :)
So far I haven't been able to find a solution which imports the resources.
Hello @mthines !
Did you find a solution for this issue?
@kirill-martynov No and I'm still frustrated by it.
I really want this plugin to work.
@mrmckeb 🙏
@kirill-martynov No and I'm still frustrated by it.
I really want this plugin to work.
@mrmckeb 🙏
me too, we have a lot of media query global variables on a project, but when we are using them, it's always showing empty type {}
With custom media global variables @media #{$new-tablet-only} {

Without custom media global variables

@mrmckeb please help us to fix this issue
So, I've tried another solutions for my purpose, specially media-quaries global variables.
I've removed sass variables from my project, switched to pure css modules and installed postcss-custom-media and others plugins
Created a file with custom media queries variables:
// src/styles/media.css
@custom-media --small-viewport (max-width: 30em);
Then I added a postcss.config.js
// postcss.config.js
const postcssCustomMedia = require('postcss-custom-media');
const postcssNested = require('postcss-nested');
module.exports = {
plugins: [
postcssNested,
postcssCustomMedia({
importFrom: './src/styles/media.css',
}),
],
};
Then I've used this variable in my css module file
// main.module.css
.app {
padding: 0;
@media (--small-viewport) {
padding: 10px;
}
}
And then everything works perfectly:

Thanks for the suggestion @kirill-martynov! I didn't know that postcss plugin, so I will be starting to use that, as I've been frustrated with CSS variables not working for media queries.
As must as I like your solution, it still doesn't fix the issue regarding not being able to import CSS files so I still believe we need to find a solution for that :)
Possible workaround!
I have the same configuration like @mthines with NextJS and @import all mixins in every scss file so we don't have write this import in each file. This results with {} as a type whenever we use @include or use mixins. I could add this @import everywhere but since we have something like customRenderer why not use it. Unfortunately this means I have to recreate rendering SASS files like it is in the source code:
customRenderer.js
const path = require('path');
const sass = require('sass');
const fs = require('fs');
const createMatchPath = require('tsconfig-paths');
const getFilePath = (fileName) => path.dirname(fileName);
/**
* Creates a sass importer which resolves Webpack-style tilde-imports.
*/
const sassTildeImporter = (rawImportPath, source) => {
// We only care about tilde-prefixed imports that do not look like paths.
if (!rawImportPath.startsWith('~') || rawImportPath.startsWith('~/')) {
return null;
}
// Create subpathsWithExts such that it has entries of the form
// node_modules/@foo/bar/baz.(scss|sass)
// for an import of the form ~@foo/bar/baz(.(scss|sass))?
const nodeModSubpath = path.join('node_modules', rawImportPath.substring(1));
const subpathsWithExts = [];
if (
nodeModSubpath.endsWith('.scss') ||
nodeModSubpath.endsWith('.sass') ||
nodeModSubpath.endsWith('.css')
) {
subpathsWithExts.push(nodeModSubpath);
} else {
// Look for .scss first.
subpathsWithExts.push(
`${nodeModSubpath}.scss`,
`${nodeModSubpath}.sass`,
`${nodeModSubpath}.css`
);
}
// Support index files.
subpathsWithExts.push(`${nodeModSubpath}/_index.scss`, `${nodeModSubpath}/_index.sass`);
// Support sass partials by including paths where the file is prefixed by an underscore.
const basename = path.basename(nodeModSubpath);
if (!basename.startsWith('_')) {
const partials = subpathsWithExts.map((file) => file.replace(basename, `_${basename}`));
subpathsWithExts.push(...partials);
}
// Climbs the filesystem tree until we get to the root, looking for the first
// node_modules directory which has a matching module and filename.
let prevDir = '';
let dir = path.dirname(source);
while (prevDir !== dir) {
const searchPaths = subpathsWithExts.map((subpathWithExt) =>
path.join(dir, subpathWithExt)
);
for (const searchPath of searchPaths) {
if (fs.existsSync(searchPath)) {
return { file: searchPath };
}
}
prevDir = dir;
dir = path.dirname(dir);
}
// Returning null is not itself an error, it tells sass to instead try the
// next import resolution method if one exists
return null;
};
module.exports = (css, { fileName, compilerOptions }) => {
const { baseUrl, paths } = compilerOptions;
const matchPath = baseUrl && paths ? createMatchPath(path.resolve(baseUrl), paths) : null;
const aliasImporter = (url) => {
const newUrl = matchPath !== null ? matchPath(url) : undefined;
return newUrl ? { file: newUrl } : null;
};
const importers = [aliasImporter, sassTildeImporter];
const filePath = getFilePath(fileName);
return sass
.renderSync({
data: "@import 'src/styles/core.scss';\n\n" + css, // here I add my import at the beginning of sass source code
includePaths: [filePath, 'node_modules'],
importer: importers
})
.css.toString();
};
I tried to import sassTildeImporter but I couldn't figure it out so instead I copied the code here as well, hence this is so big. If anyone can improve it by importing it I'd really appreciate it.
Instead of rendering SASS from file I use input data because I want to prepend my import.
@kyvg's believes that #161 (merged via #178) should resolve this. Can anyone confirm? Thanks!
@mrmckeb I will investigate and close if that's the case :)
@mrmckeb I can confirm that the following code is now working in my NX monorepo setup.
Thanks for your help and work 💪
Here's the code if it helps others doing the same setup:
NX Directory structure
...
apps
|-- web
`-- web-e2e
libs
|-- theme
|-- ui
`-- utils
tsconfig.base.json
tsconfig.base.json
{
"compilerOptions": {
...,
"plugins": [
{
"name": "typescript-plugin-css-modules",
"options": {
"rendererOptions": {
"sass": {
"includePaths": ["libs/**/*.scss"]
}
}
}
}
],
},
}
apps/web/components/header.tsx
The JSX component including the CSS module files. The styles.header is now correctly shown.
import styles from './header.module.scss';
export const Header = () => {
return (
<header className={styles.header}>
<p>I'm the header</p>
</header>
);
};
apps/web/components/header.module.scss
The SCSS file loading the libs/theme.
@use 'libs/theme' as *;
.header {
.inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding-right: var(--sz-content-medium);
padding-left: var(--sz-content-medium);
}
@media (min-width: $bp-m) {
.inner {
padding-right: var(--sz-content-large);
padding-left: var(--sz-content-large);
}
}
}
libs/theme/index.scss
This includes all the shared SCSS configuration, variables, mixins and functions
$bp-m: 500px;
@mthines may I see your full tsconfig file? becasue I do exact same as you but still cant get it to work (styles is defined as {}) when I use absolute path and only relative path (in .scss imports) works fine
Doubt if this is because I use baseDir in tsconfig but tried many different scenarios and none worked
finally could solve it by using _ before partial imports. Basically sass doesn't need to include the underscore when importing partials but typescript-plugin-css-modules seems not to understand it and requires to include it.
Examples
This doesn't work:
@use 'styles/common` as *;
But this works fine:
@use 'styles/_common` as *;
as a result, _index.scss files also cannot be detected automatically by this plugin and you need to include the complete file path
CC @mrmckeb
This doesn't work:
@use 'styles/common` as *;But this works fine:
@use 'styles/_common` as *;
Adding the underscore before the file name fixed it for me -- thank you!
Wow, I've been searching this solution for hours. Though I don't quite enjoy how I need to @use "..." in every module.scss files, it at least doesn't throw type errors XD. Thank you!