typescript-plugin-css-modules icon indicating copy to clipboard operation
typescript-plugin-css-modules copied to clipboard

SCSS - Importing SCSS variables globally breaks exported TS type to `{}`.

Open mthines opened this issue 3 years ago • 9 comments

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.

mthines avatar Jan 10 '22 14:01 mthines

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;
  }
}

image

Don't work

@use "variables" as *; // Which includes $breakpoint

.container {
  @media (min-width: $breakpoint) {
    color: red;
  }
}

image

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?

mthines avatar Jan 13 '22 07:01 mthines

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 :)

mthines avatar Jan 13 '22 07:01 mthines

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};`
                }
              },
         ]
      }
      // ....
    ],
    // ....
  },
  // ....
}

woodysee avatar Jan 15 '22 17:01 woodysee

I think it's related to #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 :)

So far I haven't been able to find a solution which imports the resources.

mthines avatar Jan 25 '22 14:01 mthines

Hello @mthines !

Did you find a solution for this issue?

kirill-martynov avatar Jul 01 '22 10:07 kirill-martynov

@kirill-martynov No and I'm still frustrated by it.

I really want this plugin to work.

@mrmckeb 🙏

mthines avatar Jul 01 '22 10:07 mthines

@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} { 176881718-b12741af-9858-4183-8d29-7dfc965564cc

Without custom media global variables Screenshot 2022-07-01 at 14 03 32

@mrmckeb please help us to fix this issue

kirill-martynov avatar Jul 01 '22 11:07 kirill-martynov

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: Screenshot 2022-07-01 at 15 18 58

kirill-martynov avatar Jul 01 '22 12:07 kirill-martynov

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 :)

mthines avatar Jul 07 '22 08:07 mthines

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 avatar Dec 04 '22 01:12 mrmckeb

@mrmckeb I will investigate and close if that's the case :)

mthines avatar Dec 09 '22 07:12 mthines

@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 avatar Dec 09 '22 12:12 mthines

@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

ErAz7 avatar Jan 03 '23 14:01 ErAz7

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

ErAz7 avatar Jan 03 '23 17:01 ErAz7

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!

zineanteoh avatar Jul 09 '23 13:07 zineanteoh

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!

holahoon avatar Aug 30 '23 04:08 holahoon