Quramy/typed-css-modules

WebPack loader

texastoland opened this issue ยท 38 comments

Have you consider making this a WebPack loader so it can work on any transformed CSS (we're using cssnext)?

Hi @AppShipIt . I don't plan it for now.

I created this tool to help TypeScript coding. It means that I need *.css.d.ts before bundling my app.

That's what we're doing too it's just not vanilla CSS. I'll close and get around to it myself eventually ๐Ÿ‘

Hi! FYI: I've written a simple webpack loader: https://www.npmjs.com/package/typed-css-modules-loader

I'll give it a try within the next few days ๐Ÿ™Œ

@AppShipIt did you ever get it working? I'm using css-next with TypeScript and this too.

olegstepura/typed-css-modules-loader#1 (comment): TL;DR I think typed-css-modules needs to be a PostCSS plugin (it's a dependency anyway) to play nice with other PostCSS plugins.

@AppShipIt Actually, I was able to make it work with PostCSS and any other loader.

What you want to do is put this loader between css-loader and all other loaders (postcss, sass etc.) That way the later loaders will process your files, generating plain CSS, then this loader will generate the .d.ts files, then css-loader will do its job.

Here's what my setup looks like:

// typed_css_module_loader.js

// Note: not the version from npm, slightly modified to operate on the `source` variable
// rather than the file itself. This way it operates on the CSS handed down to it by any
// preceding loaders.
var DtsCreator = require('typed-css-modules');
var creator = new DtsCreator();

module.exports = function(source, map) {
  this.cacheable && this.cacheable();
  var callback = this.async();

  // creator.create(..., source) tells the module to operate on the
  // source variable. Check API for more details.
  creator.create(this.resourcePath, source).then(function(content) {
    content.writeFile().then(function() {
      callback(null, source, map);
    });
  });
};
// webpack.js

var compiler = webpack({
  .
  .
  .
  module: {
    .
    .
    .
    loaders: [
        // Load CSS modules
        { test: /\.css$/,
          loader: ExtractTextPlugin.extract(
            "style",
            "css?modules&localIdentName=[name]--[local]&sourceMap!typed-css-modules!postcss"
          )
        }
    ]
    .
    .
    .
  }

  .
  .
  .
  resolveLoader: {
    root: path.resolve(__dirname, "node_modules"),
    alias: {
      "typed-css-modules": path.resolve(__dirname, "loaders/typed_css_modules_loader")
     }
  }
)};

The one issue I'm running into is that the file creation is asynchronous. The implication is that when you create a new file, its possible that the TypeScript compiler runs before the .d.ts files are generated, which makes the compiler complain. The next time webpack runs everything is ok, but there's an error the first time after a new file is created.

Actually, I'll take that back. The issue still holds, but probably isn't because of synchronous/asynchronous file generation. It probably has something to do with webpack's conception of the filesystem i.e. if you generate a file using a loader, subsequent loaders don't know about it until the next time webpack runs.

@athyuttamre

The one issue I'm running into is that the file creation is asynchronous. The implication is that when you create a new file, its possible that the TypeScript compiler runs before the .d.ts files are generated, which makes the compiler complain. The next time webpack runs everything is ok, but there's an error the first time after a new file is created.

I experience the same issue with https://github.com/olegstepura/typed-css-modules-loader
We all need a bulletproof solution ;)

I tried to solve that issue using my loader as a preloader, but it wasn't much helpful. I still need to reload project from time to time when I change/create css files.

Actually your comment has exact code that I have in my loader. I've changed it to use second argument in creator.create

Yep, looks like we're in the same boat @olegstepura. I'll post here if I figure it out though!

My plan is to see if I can hijack the _compiler or _compilation object and modify the filesystem in there. That way we can put our CSS loaders in preLoaders, which will add the new type definitions into the filesystem cache. Then in actual loaders when TS compiler runs, it'll see the files existing.

I still suspect this needs to a PostCSS plugin instead of wrapping it. I'd be happy to help and test.

@texastoland it should be pretty easy to make a PostCSS plugin out of this, but I don't follow why it must be one. In fact, if I want to run my PostCSS output through some other loaders (say, sass for example), and then generate type definitions, I wouldn't be able to do that if it was a PostCSS plugin. The current approach works better since you can put this loader at any point in your loader order.

FWIW, I have this working with TypeScript + webpack + CSS modules + PostCSS (including cssnext and other plugins.) Let me know if I can clarify something! :)

FWIW, I have this working with TypeScript + webpack + CSS modules + PostCSS (including cssnext and other plugins.)

I see what you mean! Do you have a fork I can plug into our build (same setup) at work? WIP is okay.

Hi, guys! I just found some other approach by @Jimdo https://github.com/Jimdo/typings-for-css-modules-plugin
Didn't test it yet, but seems like it's more than a simple plugin.

I'll look ASAP thanks for looking out @olegstepura ๐Ÿ™Œ

Very interesting, great find @olegstepura! I'll try it first thing tomorrow morning.

Perhaps the author @timse could let us know if our issues can be solved by their plugin.

timse commented

Howdy! :)

Honestly I'm not sure if my solution is in any way a better idea.
And i must admit i was looking into the typed-css-modules but did not look here and therefore did not try the loader.

How the plugin works is that is actually expects css-loader to be in the loader chain and then uses the output of css-loader (which relies on post-css internally) to produce the CSS module and then generates typings from that, this way I can have whatever loader before css-loader and do not have to care about how many loaders may be before that or can only work on vanilla css.

As there is no proper way (or at least i did not find one) to write files to the system it creates the .d.ts files on the fly after the compiler step of webpack which then actually retriggers a rerun of webpack (due to new files in the watched tree) that then solves the problem of getting it linted right away (but is very ugly in its own regard).

That being said, I would definitly look into having the whole thing as a loader rather than a plugin would I have to write it again or will look into the loader mentioned here.

Another motivation however was that i dont like how typed-css-modules generates the typings,
as it generates single constants which makes it impossible to have all allowed classes as per spec so instead of going for

/* styles.css.d.ts */
export const primary: string;
export const myClass: string;

the typings-for-css-modules-plugin generates

export interface IStylesCss {
  'some-class': string;
  'someOtherClass': string;
  'some-class-sayWhat': string;
}
declare const styles: IStylesCss;

export default styles;

which allows whatever way a class is written.

Then again I could have just opened a pull-request but since I wanted to hook into the build step and did not see the loader mentioned here I went for a all new approach.

Not sure if this really answers any question, either way its all not too satisfactory especially for one issue in particular:

As long as a style-file is not required its not part of the build and thus not noticed by either a loader or a plugin. However the only way to make typescript import anything but a ts/tsx file is by using commonjs style require
-> const styles = require('somestyles.css');

only after that you can switch to
-> import styles from 'somestyles.css';

as only after the initial compile step the d.ts files exist and typescript knows how to interpret that import.
This actually has to be done with every new files when first introduced and feels very wrong.
(well at least thats how far i got) either way im kind of interested in your approaches and issues to that and would be happy to help!

@timse

As long as a style-file is not required its not part of the build and thus not noticed by either a loader or a plugin. However the only way to make typescript import anything but a ts/tsx file is by using commonjs style require
-> const styles = require('somestyles.css');

only after that you can switch to
-> import styles from 'somestyles.css';

this seem to be the same issue we're experiencing with the loader approach. For me new .css file does not get a .d.ts companion until I reload the project.

that then solves the problem of getting it linted right away (but is very ugly in its own regard).

I didn't understand the linting problem?

  declare const styles: IStylesCss;
  export default styles;

Great idea!

As long as a style-file is not required its not part of the build and thus not noticed by either a loader or a plugin.

I don't see any workaround around this since WebPack needs to know all files when it starts. @TheLarkInn @kentcdodds?

However the only way to make typescript import anything but a ts/tsx file is by using commonjs style require

@olegstepura Were you able to work around using require on initial build?

@olegstepura Were you able to work around using require on initial build?

@texastoland I didn't test it yet. Doesn't seem to be a good solution. In my setup I just need to press enter in console where app is running to fully restart it. So in my case it maybe even faster to restart then to write require and then replace it with import.

declare const styles: IStylesCss; export default styles;
Great idea!

I cannot understand. What's the benefit?

@texastoland how's your test of Jimdo/typings-for-css-modules-plugin ?
Did you find it as a better approach?

I decided not to test because retyping imports wouldn't work for our team and sequential builds wouldn't work with CI. IStyles would enable you to import all styles into one variable instead of all into global namespace.

Hmm, I don't seem to have to retype imports at all.

If you use TypeScript with --target es5 --module commonjs, then TypeScript resolves the ES6 module imports into standard require imports. webpack seems to detect these and build the dependency graph to CSS files directly.

So in my TSX files, I simply write import * as styles from './test_button.css'. When webpack runs, it detects this dependency, and builds the .d.ts file. I also don't ever have to restart webpack.

I neglected to mention I'm using nightly to output ES6 modules for tree shaking.

timse commented

Hmm, I don't seem to have to retype imports at all.

If you use TypeScript with --target es5 --module commonjs, then TypeScript resolves the ES6 module imports into standard require imports. webpack seems to detect these and build the dependency graph to CSS files directly.

So in my TSX files, I simply write import * as styles from './test_button.css'. When webpack runs, it detects this dependency, and builds the .d.ts file. I also don't ever have to restart webpack.

does this refer to the loader or the plugin :) @athyuttamre ?

The loader!

On Sun, Jul 3, 2016 at 12:11 PM -0700, "timse" notifications@github.com wrote:

Hmm, I don't seem to have to retype imports at all.

If you use TypeScript with --target es5 --module commonjs, then TypeScript resolves the ES6 module imports into standard require imports. webpack seems to detect these and build the dependency graph to CSS files directly.

So in my TSX files, I simply write import * as styles from './test_button.css'. When webpack runs, it detects this dependency, and builds the .d.ts file. I also don't ever have to restart webpack.

does this refer to the loader or the plugin :) @athyuttamre ?

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

Tried that loader, it kind of works... But webpack no longer recompiles css files when updating them in watch mode.

Edit: Nevermind, it only does it if the class is not yet referenced explicitely by anything (just importing the file doesn't watch the class changes).

@AlexGalays yep! That tripped me up for a bit, but you're right, you need to reference the React component.

timse commented

Just as the plugin was mentioned here, i deprecated it in favor of a loader approach: https://github.com/Jimdo/typings-for-css-modules-loader

again the reasoning behind not using typed-css-modules for me is to be able to have things like sass etc. in case that solves anyones problems :)

@timse Cool! Does that loader end up generating/updating the styles' d.ts files in the same cycle as webpack compile the app, thus resulting in only one bundle file write ?

timse commented

If you mean if it triggers a second webpack building then no, this doesn't solve that @AlexGalays.

It is kind of impossible to achieve this, the only way to make webpack aware of the fact that a loader/plugin is creating a new file will not write to disk but only into webpacks memory fs, but typescript is not able to access that, thus its not possible to get the typings.

I have no idea if I have overseen a proper solution, but for now i would clame its impossible to have this and not retrigger a webpack build.

However given that one usually doesnt change sass all that often compared to code (a very non css-guy kind of view on the world i guess :P) it doesnt really hurt too much does it?

Can we solve this issue by declaring a global matcher module that was added TS 2.0?

declare module '*.css' {
  const classes: {[className: string]: string};
  export = classes;
}

If in your code you import a CSS file with relative path, TypeScript will try finding that file first and if it fails the global matcher will be used.

@mohsen1, this resolves the initial errors, but still triggers a second Webpack build due to the creation of new files.

Although this isn't really a great solve it does get around the error that is thrown on initial webpack build. I added an npm prestart command that runs tcm on scss files in my case.

In my package.json I have:

"prestart": "tcm -p src/**/*.scss -c",
"start": "webpack-dev-server --env.dev --hot --inline --open",

This ensures that the initial *.d.ts files are created before the webpack build begins.

@ryandrewjohnson, The problem with that approach is that if the *.scss files contain SASS-specific syntax, tcm will not be able to compile them.

For example. if the *.scss files use the string interpolation syntax, width: "#{$some-height}px", then tcm won't work.