timarney / react-app-rewired

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

Change build output path #201

Closed jorisre closed 6 years ago

jorisre commented 6 years ago

Hi !

What's the best way to change the build output path ? I'm trying to modify the config.output.path var but i'm getting this error :

Error: ENOENT: no such file or directory

Some code :

const { injectBabelPlugin } = require('react-app-rewired');
const rewireLess = require('react-app-rewire-less');

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

     config.output.path += '/sub-build';  // <= Doesn't work

     config = injectBabelPlugin(['import', { libraryName: 'antd', style: true }], config);
     config = rewireLess.withLoaderOptions({
         modifyVars: {
            // Less vars...
         },
     })(config, env);

     return config;
};

My aim is to generate multiples builds.

Thanks for help

dawnmist commented 6 years ago

First verify that the config.output.path value is defined.

Then use:

const path = require('path');
config.output.path = path.join(config.output.path, 'sub-build');

That has the benefit that it'll use the right path separators regardless of whether you're on Win/Mac/Linux.

jorisre commented 6 years ago

@dawnmist thanks for your reply.

I just tried your solution but it seems not work.

favicon.ico and manifest.json aren't in sub-build;

screenshot : https://cl.ly/1l1y1b3b0G1m

And i'm getting this error :

Error: ENOENT: no such file or directory, open '/path/to/my/project-react/build/static/js/main.946f3fc2.js'

Thanks for help

dawnmist commented 6 years ago

Sorry about that.

I have taken a look at the build script in create-react-app, and the problem is that some of the work is being done directly inside the build script itself instead of being done by webpack, so changes to the path inside the webpack configuration will not affect the parts being done outside webpack.

This includes copying the favicon.ico file, and checking the file size for the generated files (where it will be miscalculating the path to the output file, which I think is causing the "no such file" error you are getting).

Take a look at the script here: https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/scripts/build.js

Any place using paths.appBuild will be referring to the original/default build path rather than the one that you are trying to set.

I don't think that we have the capability to override their config/paths file. It is the same reason as for why it is difficult to override the "entry" file from src/index.js to a different filename - the normal rewire process gets bypassed in the build script.

jorisre commented 6 years ago

Thanks for your help

Finally, I've add a project-config and clone my project.

nahuely commented 6 years ago

Hi Joris, how did you fix it at the end? because i want to change the build directory, but it get the same error as you, did you manage to fix it? cheers

jorisre commented 6 years ago

Hi @nahuely, My aim was to build two products on the same project. Finally I'm using a .env file supported by react, If I change an option in my .env then build will be different.

stephanoapiolaza commented 5 years ago

What is the option on .env?

axedre commented 5 years ago

Curious about it too... Trying to simply change the output directory from build to dist but to no avail. Since it appears to be a problem in CRA's build script, can we perhaps get @gaearon's eyes on this?

EDIT For full disclosure, this solution proposed by Dan Abramov himself is not valid in my use case, since I need to launch two builds in parallel, each with a different value for a certain env variable, i.e.

  1. FOO=true yarn build
  2. FOO=false yarn build

and my config-overrides.js is rigged to change the output path conditionally based on that very value, i.e.:

module.exports = function override(config, env) {
    if(/^(?:true|1)$/i.test(process.env.FOO)) {
        config.output.path = path.resolve(__dirname, 'build-foo');
    }
    // Otherwise, `build/` is fine
    return config;
}
dawnmist commented 5 years ago

I've managed to achieve this in the past by using a different npm package called patch-package to apply the modification directly to the react-scripts config/paths.js file. Using patch-package means that the patch is re-applied every time any package is installed/removed/etc, so npm/yarn don't clobber your change.

Because the paths defined in the config/paths.js file are used in several places, and not all of those places can be modified by react-app-rewired, it is not a problem that react-app-rewired was able to solve. By modifying that file directly, the modification flows through to all the places where the value was being used properly.

For the case of wanting to use multiple alternative build directories, setting the value of the appBuild variable in config/paths.js to an environment variable (with a default value if the env variable was not specified) might work (I haven't tested that bit). Note that ensuring that the appBuild variable is never able to be null, undefined, an empty string, 'src', or 'node_modules' is important - I can see the potential for things to go horribly wrong otherwise (like the first step deleting your project files because it thinks its cleaning the build directory...). If you change it to a dynamic value, be very careful to sanity check that value & protect yourself from a major oops if someone forgot to set it or sets it to a dangerous value! I suspect that potential for error causing serious problems for the user is probably a large part of why create-react-app don't make this a configurable value by default.

axedre commented 5 years ago

Thanks @dawnmist that's extremely helpful, I was not aware of this library but I'll look into it asap. Cheers. :)

axedre commented 5 years ago

I've managed to accomplish this successfully with patch-package by reading appBuild from an environment variable (which is sanitized beforehand to ensure its value is not among the blacklisted ., .., node_modules, src etc.).

Thanks again for your invaluable insight on this. Cheers!

jeanpaulangelle-taulia commented 5 years ago

@axedre Can you share with me some code snippets of what worked for you.

I changed the appBuild path in config/paths.js in the react-scripts node module. Created the patch with yarn patch-package react-scripts then applied the patch with yarn patch-package. Then tested with yarn build. Thought that would be enough. Not working still. Outputting to /build not /dist.

dawnmist commented 5 years ago

@jeanpaulangelle-taulia Did you change all 3 instances for appBuild in that file, or only one? If only one - which one?

Patch for react-scripts 3.0.1 should look something like:

diff --git a/node_modules/react-scripts/config/paths.js b/node_modules/react-scripts/config/paths.js
index e5a3e0b..48a5aa8 100644
--- a/node_modules/react-scripts/config/paths.js
+++ b/node_modules/react-scripts/config/paths.js
@@ -77,7 +77,7 @@ const resolveModule = (resolveFn, filePath) => {
 module.exports = {
   dotenv: resolveApp('.env'),
   appPath: resolveApp('.'),
-  appBuild: resolveApp('build'),
+  appBuild: resolveApp('dist'),
   appPublic: resolveApp('public'),
   appHtml: resolveApp('public/index.html'),
   appIndexJs: resolveModule(resolveApp, 'src/index'),
@@ -100,7 +100,7 @@ const resolveOwn = relativePath => path.resolve(__dirname, '..', relativePath);
 module.exports = {
   dotenv: resolveApp('.env'),
   appPath: resolveApp('.'),
-  appBuild: resolveApp('build'),
+  appBuild: resolveApp('dist'),
   appPublic: resolveApp('public'),
   appHtml: resolveApp('public/index.html'),
   appIndexJs: resolveModule(resolveApp, 'src/index'),
@@ -135,7 +135,7 @@ if (
   module.exports = {
     dotenv: resolveOwn('template/.env'),
     appPath: resolveApp('.'),
-    appBuild: resolveOwn('../../build'),
+    appBuild: resolveOwn('../../dist'),
     appPublic: resolveOwn('template/public'),
     appHtml: resolveOwn('template/public/index.html'),
     appIndexJs: resolveModule(resolveOwn, 'template/src/index'),

I think it is the second of the 3 places that is critical for changing the output directory, but changing all 3 makes sure that you've covered them all (and if you do later eject ensures that the ejected config contains your modified path).

jeanpaulangelle-taulia commented 5 years ago

@dawnmist Ah gosh. Wow. Thanks, this fixed it. Works now 👍

axedre commented 5 years ago

@jeanpaulangelle-taulia Sorry I only read your comment today. So in fact I've resorted to setting the appBuild folder using an env var, which falls back to 'build' if undefined or empty; plus I've done a little bit of sanitizing to prevent accidental assignment of "protected" paths, as @dawnmist suggested. Here is my code:

--- a/node_modules/react-scripts/config/paths.js
+++ b/node_modules/react-scripts/config/paths.js
@@ -73,11 +73,31 @@ const resolveModule = (resolveFn, filePath) => {
   return resolveFn(`${filePath}.js`);
 };

+// Ensure appBuild is 'safe' (i.e. not `src/`, `node_modules/`, './', etc)
+const appBuild = (envAppBuild => {
+    const blacklistedPaths = [
+        '.',
+        '..',
+        '.git',
+        'node_modules',
+        'patches',
+        'public',
+        'src'
+    ];
+    if(Boolean(envAppBuild) && !blacklistedPaths.includes(envAppBuild)) {
+        return envAppBuild;
+    }
+    return 'build';
+})(process.env.BUILD_DIR);
+
 // config after eject: we're in ./config/
 module.exports = {
   dotenv: resolveApp('.env'),
   appPath: resolveApp('.'),
-  appBuild: resolveApp('build'),
+  appBuild: resolveApp(appBuild),
   appPublic: resolveApp('public'),
   appHtml: resolveApp('public/index.html'),
   appIndexJs: resolveModule(resolveApp, 'src/index'),
@@ -100,7 +120,7 @@ const resolveOwn = relativePath => path.resolve(__dirname, '..', relativePath);
 module.exports = {
   dotenv: resolveApp('.env'),
   appPath: resolveApp('.'),
-  appBuild: resolveApp('build'),
+  appBuild: resolveApp(appBuild),
   appPublic: resolveApp('public'),
   appHtml: resolveApp('public/index.html'),
   appIndexJs: resolveModule(resolveApp, 'src/index'),
@@ -135,7 +155,7 @@ if (
   module.exports = {
     dotenv: resolveOwn('template/.env'),
     appPath: resolveApp('.'),
-    appBuild: resolveOwn('../../build'),
+    appBuild: resolveOwn(`../../${appBuild}`),
     appPublic: resolveOwn('template/public'),
     appHtml: resolveOwn('template/public/index.html'),
     appIndexJs: resolveModule(resolveOwn, 'template/src/index'),

HTH :)

jeanpaulangelle-taulia commented 5 years ago

Thank you so much!

Vivomo commented 5 years ago

I have another solution.

It will rename your buildPath dirname after npm run build

rename.js code:

const fs = require('fs');
const path = require('path');

function removeDir(dir) {
    let files = fs.readdirSync(dir);
    for (let i = 0; i < files.length; i++) {
        let newPath = path.join(dir, files[i]);
        let stat = fs.statSync(newPath);
        if (stat.isDirectory()) {
            removeDir(newPath);
        } else {
            fs.unlinkSync(newPath);
        }
    }
    fs.rmdirSync(dir);
}

const newRoot = './dist';

if (fs.existsSync(newRoot)) {
    removeDir(newRoot);
}

fs.renameSync('./build', newRoot);
Qubad786 commented 4 years ago

Override paths.appBuild instead. Yourconfig-overrides.js will become:

const path = require('path');

module.exports = {
    paths: function (paths, env) {        
        paths.appBuild = path.join(paths.appBuild, 'sub-build');
        return paths;
    },
}
krystof-k commented 3 years ago

You can also use an environment variable, e.g. like this:

const path = require('path');

module.exports = {
  paths: function (paths, env) {        
    paths.appBuild = path.join(paths.appBuild, process.env.BUILD_PATH || 'build');
    return paths;
  },
};
cliffordfajardo commented 3 years ago

My config-overrides.js file looks like this:

module.exports = function override(config, env) {
   // support mono repo folder outside of `/src`
   aliasDangerous({...configPaths('tsconfig.paths.json'),})(config)
   return paths
};

I tried modifying it like this

module.exports = function override(config, env) {
   // support mono repo folder outside of `/src`
   aliasDangerous({...configPaths('tsconfig.paths.json'),})(config)

   // inspired by the code in the previous comments (but doesnt work)
   config.paths = (paths, env) => {
       paths.appBuild = path.join(paths.appBuild, 'sub-build');
       return paths
    }
   return paths
};

and I get the following error:

Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
 - configuration has an unknown property 'paths'. These properties are valid:
   object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, externals?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, serve?, stats?, target?, watch?, watchOptions? }
   For typos: please correct them.
   For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.
     Loaders should be updated to allow passing options via loader options in module.rules.
     Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:
     plugins: [
       new webpack.LoaderOptionsPlugin({
         // test: /\.xxx$/, // may apply this only for some modules
         options: {
           paths: …
         }
       })
     ]

Using this export function style above (instead of exporting an object), how can I change the build output path of webpack?

dawnmist commented 3 years ago

@cliffordfajardo You cannot - it is not possible to properly modify the output path through the webpack config due to the way that create-react-app's paths are set.

The export function style only modifies the webpack config. In order to modify the output paths properly, there are additional places that have to be modified - it can't just be done by the webpack config.

You will need to migrate to the object export format in order to modify the output path. Migrating is actually very simple - the function that you currently output in the export function style just gets assigned to the 'webpack' field of the object. Then you create the path field as a new function that modifies and returns the paths.

yakyn0103 commented 2 years ago

Add in your .env-cmdrc file variable: "BUILD_PATH": "./newpatch",

Pinwheeler commented 1 year ago

For anyone who is still struggling with this, I'm guessing that you're using create-react-app which offers a solution: https://stackoverflow.com/questions/41495658/use-custom-build-output-folder-when-using-create-react-app

// package.json
  "scripts": {
    ...
    "build": "BUILD_PATH='./site' react-app-rewired build"
  },