Quramy / typed-css-modules

Creates .d.ts files from CSS Modules .css files
MIT License
1.03k stars 68 forks source link

WebPack loader #2

Closed texastoland closed 8 years ago

texastoland commented 8 years ago

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

Quramy commented 8 years ago

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.

texastoland commented 8 years ago

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

olegstepura commented 8 years ago

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

texastoland commented 8 years ago

I'll give it a try within the next few days 🙌

athyuttamre commented 8 years ago

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

texastoland commented 8 years ago

https://github.com/olegstepura/typed-css-modules-loader/issues/1#issuecomment-223379638: 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.

athyuttamre commented 8 years ago

@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")
     }
  }
)};
athyuttamre commented 8 years ago

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.

athyuttamre commented 8 years ago

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.

olegstepura commented 8 years ago

@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

athyuttamre commented 8 years ago

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.

texastoland commented 8 years ago

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

athyuttamre commented 8 years ago

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

texastoland commented 8 years ago

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.

olegstepura commented 8 years ago

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.

texastoland commented 8 years ago

I'll look ASAP thanks for looking out @olegstepura 🙌

athyuttamre commented 8 years ago

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 8 years ago

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!

olegstepura commented 8 years ago

@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.

texastoland commented 8 years ago

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?

texastoland commented 8 years ago

Relevant issues: https://github.com/Microsoft/TypeScript/issues/6508#issuecomment-224428750 https://github.com/Microsoft/TypeScript/issues/9017

olegstepura commented 8 years ago

@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.

olegstepura commented 8 years ago

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

I cannot understand. What's the benefit?

olegstepura commented 8 years ago

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

texastoland commented 8 years ago

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.

athyuttamre commented 8 years ago

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.

texastoland commented 8 years ago

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

timse commented 8 years ago

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 ?

athyuttamre commented 8 years ago

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.

AlexGalays commented 8 years ago

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

athyuttamre commented 8 years ago

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

timse commented 7 years ago

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

AlexGalays commented 7 years ago

@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 7 years ago

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?

mohsen1 commented 7 years ago

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.

athyuttamre commented 7 years ago

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

ryandrewjohnson commented 7 years ago

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.

eikawata commented 6 years ago

@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.