timarney / react-app-rewired

Override create-react-app webpack configs without ejecting
MIT License
9.79k stars 425 forks source link

Drop into an existing CRA project #1

Closed geelen closed 7 years ago

geelen commented 7 years ago

I really like the idea behind this project, but it still kinda feels like a boilerplate — you have to decide to use react-app-rewired instead of create-react-app at the beginning, right?

Would it be possible to wrap this up in a different way, so that you could take a perfectly normal CRA app and then, instead of ejecting, do something like this:

npm i -D react-app-rewired
--- a/package.json
+++ b/package.json
@@ -3,7 +3,8 @@
   "devDependencies": {
-    "react-scripts": "0.7.0"
+    "react-scripts": "0.7.0",
+    "react-app-rewired": "^1.0.0"
   },
@@ -12,9 +13,9 @@
   "scripts": {
-    "start": "react-scripts start",
-    "build": "react-scripts build",
-    "test": "react-scripts test --env=jsdom",
-    "eject": "react-scripts eject"
+    "start": "react-app-rewired start",
+    "build": "react-app-rewired build",
+    "test": "react-app-rewired test --env=jsdom",
+    "eject": "react-app-rewired eject"
   }
 }

Then suddenly your magic config-overrides.js file gets picked up and you're all good. It feels like this API should be totally possible with the work you've already done, but let me know if I'm missing something.

I really want something like this to exist for people to be able to use babel-plugin-styled-components-named without ejecting. Reckon that's possible?

timarney commented 7 years ago

Hi @geelen

First thanks for your interest in this project - I've been seeking some better use cases and babel-plugin-styled-components-named would be another good one.

I do think what your saying will be possible. To date I've been waiting on CRA to stabilize to see if this idea was feasible.

Last week I updated CRA from 0.2.1 to 0.7.0 no issues... so unless something major changes I think we're good on that.

I will invest some time next week to see about setting this up properly via an npm install to make things as automatic as possible.

The current process:

No it's not another boilerplate, it's just not very well explained and very manual at this point.

Given a new app created via CRA.


  1. Copy the scripts folder from this project - these pull (override) the react-scripts configs
  2. Updated your package.json

The scripts section from the CRA app package.json need to repoint to the scripts folder you copied in.

"scripts": {
    "default": "node node_modules/react-scripts/scripts/build.js",
    "start": "node scripts/start",
    "build": "node scripts/build"
  },

You also need to install 2 existing npm packages

You'll need 2 existing npm packages

npm install rewire --save-dev npm install rimraf --save-dev (I think I'll be able to remove this dep)

  1. Copy in or create the config-overrides.js file. Update this file as need be.

I'll see what I can do for a more automatic setup and we'll go from there.

geelen commented 7 years ago

Hmm if that's the case I think this should be really straightforward. I might have some time later this week to have a crack at it, but if you get there first then 👍

Btw I think what you want is basically the same as any "command line" NPM project. I've never released one, but I found an article on the NPM blog that makes it sound pretty easy :)

This would also be relevant to CSS Modules, too, since there are a lot of people bugging CRA to include support for that. All it requires is adding ?modules=true to the css-loader but CRA doesn't really expose any way for that to be done.

timarney commented 7 years ago

Cool - I've got actually got a good start on this today. The way I'm setting it up should be very flexible i.e. we can add loaders etc..

I'll keep rolling and let you know once I have something to work with that you can install via NPM.

timarney commented 7 years ago

@geelen

I'm doing some testing.

I haven't worked with CSS modules yet ...

Given the existing loader lines from the Dev and Pro Webpack configs how would they need to end up like

CRA Dev

{
    test: /\.css$/,
    loader: 'style!css?importLoaders=1!postcss'
},

CRA Prod

{
   test: /\.css$/,
   loader: ExtractTextPlugin.extract('style', 'css?importLoaders=1&-autoprefixer!postcss') 
},

Like this?

loader: 'style!css?importLoaders=1&modules=true!postcss'

loader: ExtractTextPlugin.extract('style', 'css?importLoaders=1&modules=true&-autoprefixer!postcss') 
geelen commented 7 years ago

I believe so yep! On Sun., 13 Nov. 2016 at 11:58 am Tim Arney notifications@github.com wrote:

@geelen https://github.com/geelen

I'm doing some testing.

I haven't worked with CSS modules yet ...

Given the existing loader lines from the Dev and Pro Webpack configs how would they need to end up like

CRA Dev

{ test: /.css$/, loader: 'style!css?importLoaders=1!postcss' },

CRA Prod

{ test: /.css$/, loader: ExtractTextPlugin.extract('style', 'css?importLoaders=1&-autoprefixer!postcss') },

Like this?

loader: 'style!css?importLoaders=1&modules=true!postcss'

loader: ExtractTextPlugin.extract('style', 'css?importLoaders=1&modules=true&-autoprefixer!postcss')

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/timarney/react-app-rewired/issues/1#issuecomment-260159258, or mute the thread https://github.com/notifications/unsubscribe-auth/AABa4JPkABMg9VQ8SWJm3WNyXvAgBk_Xks5q9mCrgaJpZM4KwTWV .

timarney commented 7 years ago

@geelen If you want to toy around with this at some point you can now run

create-react-app your-app-name --scripts-version rewire-react-scripts

That will create a fresh CRA project with a config-overrides.js file in it. Like the existing repo but via npm install.

screen shot 2016-11-12 at 9 20 37 pm

With that you can mess with the configs from CRA.

That's a start.

The babel-plugin-styled-components-named plugin do you know if that can loaded via webpack or only via a .babelrc file?

timarney commented 7 years ago

FYI - just pushed 0.0.6 the sample config-overrides.js file works for CSS Modules

create-react-app your-app-name --scripts-version rewire-react-scripts

Modifiy App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import styles from './App.css';

class App extends Component {
  render() {
    return (
      <div className={styles.App}>
        <div className={styles['App-header']}>
          <img src={logo} className={styles['App-logo']} alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className={styles['App-intro']}>
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}
timarney commented 7 years ago

@geelen can you confirm with that plugin installed -> babel-plugin-styled-components-named

The result in Dev Tools would be:

babel-plugin-styled-components-named

If yes than this thing works.

config-overrides.js

module.exports = function override (config,env) {

  config.module.loaders[0].query.plugins =  ['styled-components-named'];

  return config;
}
geelen commented 7 years ago

Yep! If you wrote const Title = styled.h1 then that should be the output! 😄

geelen commented 7 years ago

So if you've got an existing CRA project what are the instructions to switch it to this one?

timarney commented 7 years ago

It's just a matter of updating the devDependencies

CRA
"devDependencies": {
    "react-scripts": "0.7.0"
  },

Rewired

"devDependencies": {
    "rewire-react-scripts": "0.0.6"
  },

Outside of that for existing apps at this point you would need to create a config-overrides.js.

I should be able to setup a little script to take care of that part.

geelen commented 7 years ago

Hmm, personally I think it'd be better if it ran alongside react-scripts rather than replaced it. That feels like a less invasive change, and would mean that rewire-react-scripts could be much smaller, containing just the scripts needed rather than a fork of CRA itself.

Would that work?

timarney commented 7 years ago

Ya it's not ideal especially given all I'm doing is wrapping webpack kickoff scripts with a function (init) so I can get a hook - https://github.com/timarney/create-react-app/commit/adf82c34aff39c3c9227fe0dfbe2cb964708c194#diff-8d717038a105d8c7993616152faa1a38

My original 'hack' didn't use a fork but given there's really no hooks the kickoff wasn't clean... The original used rewire to gain access to the configs etc... but given the setup of those scripts i.e. for the build script 'recursive' ran when it was included... than I would just run it again with the rewired version.

Forking the entire repo to make those alterations seems to be best practice at least so far https://github.com/facebookincubator/create-react-app/issues/682 CRA is setup to look for a fork as a replacement so it's fairly seamless / built in create-react-app my-app --scripts-version my-react-scripts-fork

I'm looking to avoid duplicating / maintaining those entire files (build and start) into a new package to keep the maintenance down.

Given the minimal amount of changes needed to get those hooks working (a few lines really) a fork seemed like the best option to me. The part that gets published to npm is just the react scripts package - I don't plan on altering anything else in CRA but I do want to keep it in sync with upstream to ensure the webpack / build scripts remain up to date.

Do you see any paths without duplicating / maintaining those entire files?

geelen commented 7 years ago

Hahah ok I see the problem. This just took me a while, and I really didn't think it was going to work, but it does!

/* start.js */
var fs = require('fs');
var path = require('path');

var config = require('react-scripts/config/webpack.config.dev')
var override = require(path.resolve(fs.realpathSync(process.cwd()) + '/config-overrides'));

require.cache[require.resolve('react-scripts/config/webpack.config.dev')].exports =
  override(config, process.env.NODE_ENV || 'development');

require('react-scripts/scripts/start')

You load the config, then run your overrides on it, then patch the require.cache so that when you start react-scripts and it loads the config it gets your patched config.

Neat, hey! I had a read through proxyquire which I've used in testing before and it seems like this is a legit butchery of require.cache. But maybe there's something I'm missing...

For the record, I also did a version with two files in case patching require.cache[...].export is bad for some reason:

/* start.js */
var config = require('./dev');

require.cache[require.resolve('react-scripts/config/webpack.config.dev')] =
  require.cache[require.resolve('./dev')];

require('react-scripts/scripts/start')
/* dev.js */
var fs = require('fs');
var path = require('path');

var config = require('react-scripts/config/webpack.config.dev')
var override = require(path.resolve(fs.realpathSync(process.cwd()) + '/config-overrides'));

module.exports = override(config, process.env.NODE_ENV);

This also works, but I prefer the single-file approach.

Oh also your config-overrides template example is invalid, I had to change it to this:

/* config-overrides.js */
module.exports = function override(config, env) {
  var loader = '';
  if (env === 'development') {
    loader = 'style!css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss';
  } else {
    loader = 'style!css?modules&-autoprefixer&importLoaders=1!postcss';
  }
  // Find the right loader then patch its 'loader' property
  config.module.loaders.forEach(l => {
    if (String(l.test) == String(/\.css$/)) l.loader = loader
  })
  // default override
  return config;
}

What do you think?

timarney commented 7 years ago

Nice that looks ideal - if you like I can add you as a collaborator on this repo get a new branch kicked off with that code.

I'll checkout that proxyquire repo.

timarney commented 7 years ago

Brilliant just gave it a go - seems to work flawless. Super clean.

timarney commented 7 years ago

Here's an init script I had started on to write entries to the package.json file + copy over the config-ovverides.js file

var fs = require('fs-extra');
var path = require('path');
var pathExists = require('path-exists');
var appPath = fs.realpathSync(process.cwd())
var appPackage = require(path.join(appPath, 'package.json'));

// Setup the script rules
if(!appPackage.scripts) return;
appPackage.scripts['rewire-start'] =  'react-app-rewired/start';
appPackage.scripts['rewire-build'] =  'react-app-rewired/build';

fs.writeFileSync(
  path.join(appPath, 'package.json'),
  JSON.stringify(appPackage, null, 2)
);

var overrideExists = pathExists.sync(path.join(appPath, 'config-overrides.js'));
  if (!overrideExists) {
    // Copy the files for the user
    fs.copySync(path.resolve(__dirname, 'config-overrides.js'), 'config-overrides.js');
  }

Think that will work or better idea?

Also

How's this looks for the build script?

/* build.js */
process.env.NODE_ENV = 'production';
var fs = require('fs');
var path = require('path');

var config = require('react-scripts/config/webpack.config.prod')
var override = require(path.resolve(fs.realpathSync(process.cwd()) + '/config-overrides'));

require.cache[require.resolve('react-scripts/config/webpack.config.prod')].exports =
override(config, process.env.NODE_ENV);

require('react-scripts/scripts/build')
geelen commented 7 years ago

I don't think you need to do it, personally. I think you just have manual instructions to npm install react-app-rewired, edit package.json and create config-overrides.js in your local directory. If the example file is quite small and readable it's useful for people to see it and get a sense of what it can do. Something like this maybe?

export default (config, env, { changeLoader, addBabelTransform }) => {
  let cssLoader
  if (env === 'development') {
    addBabelTransform('babel-plugin-styled-components-named')
    cssLoader = 'style!css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss'
  } else {
    cssLoader = 'style!css?modules&-autoprefixer&importLoaders=1!postcss'
  }
  changeLoader({
    test: /^.css$/
    loader: cssLoader
  })
}

Or how about:

export const dev = ({ changeLoader, addBabelTransform }) => {
  addBabelTransform('babel-plugin-styled-components-named')
  changeLoader({
    test: /^.css$/
    loader: 'style!css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss'
  })
}

export const production = ({ changeLoader }) => {
  changeLoader({
    test: /^.css$/
    loader: 'style!css?modules&-autoprefixer&importLoaders=1!postcss'
  })
}

Feels like an ok way to pass in helpers...

Probably worth raising another issue for this though, it might be best to not reinvent the wheel and build off webpack-config instead.

timarney commented 7 years ago

Okay pushed up a new branch if you can take a peak (just the start and build files for now) - https://github.com/timarney/react-app-rewired/tree/usecache

timarney commented 7 years ago

Ya I like the idea of building off webpack-config can flesh that out further in another issue.

Sounds good on the setup stuff it's pretty basic now. Given some good sample config-overrides I think people will get it (CSS Modules maybe Sass and a few others).

geelen commented 7 years ago

👍 looking good. I'll start raising issues for stuff as I think of it, I'll have some time later this week to dive in a bit deeper than the issue tracker :)

timarney commented 7 years ago

Cool sounds good - really happy with this solution. It's ultimately the vision I had for this repo. No eject and no fork. Clean and simple.

Feel free to mod things as you see fit. I'll do what I can next week as well to push things forward.

timarney commented 7 years ago

@geelen closing this issue as I think I have all discussed back into master ... as you mentioned we can open a new issue for looking at webpack-config.