Repository: ProjectEvergreen/greenwood (js)

Issue:
support ejecting webpack configuration (and friends)
## Type of Change
- [x] New Feature Request
- [ ] Documentation / Website
- [ ] Improvement / Suggestion
- [ ] Bug
- [ ] Other (please clarify below)

## Summary
Related to #370, for users that want the most control over their project configuration, Greenwood should support ["ejecting"](https://create-react-app.dev/docs/available-scripts/#npm-run-eject) its Greenwood configuration, and defaulting to that if it's there.  With that, user's can do any sort of loader / plugin configurations they want.

## Details
1. This should be exposed as a new task, called `eject`, e.g. `greenwood eject` as _webpack.config.js_
1. Greeenwood CLI will need to know to know to use the user's webpack configuration
1. It should also support an `--all` flag that will also copy the remaining config files to the user's root
    - _babel.config.js_
    - _postcss.config.js_
    - _.browserslistrc_
1. Add documentation to the CLI section of the [Docs -> Overview](https://www.greenwoodjs.io/docs/) page
1. Add test case

Pull request description:
task/issue 372 Add support for ejecting config files including webpack, babel, postcss
## Related Issue
Resolves #372

## Summary of Changes
* Add new `greenwood eject` command with `--all` option
* Eject task that copies config files to project root directory

## TODO

* [x] Test case
* [x] Documentation

Patch:
diff --git a/README.md b/README.md
index caeedf9b..51544a65 100644
--- a/README.md
+++ b/README.md
@@ -30,12 +30,14 @@ Then in your _package.json_, you can run the CLI like so:
 ```javascript
 "scripts": {
   "build": "greenwood build",
-  "start": "greenwood develop"
+  "start": "greenwood develop",
+  "eject": "greenwood eject",
 }
 ```
 
 - `npm run build`: generates a static build of your project
 - `npm start`: starts a local development server for your project
+- `npm run eject`: ejects configurations to your working directory for additional customizations
 
 ## Documentation
 All of our documentation is on our [website](https://www.greenwoodjs.io/) (which itself is built by Greenwood!).  See our website documentation to learn more about:
diff --git a/packages/cli/src/config/webpack.config.common.js b/packages/cli/src/config/webpack.config.common.js
index 602c99fd..26b773d5 100644
--- a/packages/cli/src/config/webpack.config.common.js
+++ b/packages/cli/src/config/webpack.config.common.js
@@ -84,7 +84,7 @@ module.exports = ({ config, context }) => {
     resolve: {
       extensions: ['.js', '.json', '.gql', '.graphql'],
       alias: {
-        '@greenwood/cli/data': path.join(__dirname, '..', './data')
+        '@greenwood/cli/data': context.dataDir
       }
     },
 
diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js
index ea7a7de5..dd2fadcf 100755
--- a/packages/cli/src/index.js
+++ b/packages/cli/src/index.js
@@ -12,8 +12,10 @@ const program = require('commander');
 const generateCompilation = require('./lifecycles/compile');
 const runProdBuild = require('./tasks/build');
 const runDevServer = require('./tasks/develop');
+const ejectConfigFiles = require('./tasks/eject');
 const scriptPkg = require('../package.json');
-
+let compilation = {};
+let cmdOption = {};
 let MODE = '';
 
 console.log(`${chalk.rgb(175, 207, 71)('-------------------------------------------------------')}`);
@@ -38,6 +40,15 @@ program
     MODE = cmd._name;
   });
 
+program
+  .command('eject')
+  .option('-a, --all', 'eject all configurations including babel, postcss, browserslistrc')
+  .description('Eject greenwood configurations.')
+  .action((cmd) => {
+    MODE = cmd._name;
+    cmdOption.all = cmd.all;
+  });
+
 program.parse(process.argv);
 
 if (program.parse.length === 0) {
@@ -47,11 +58,12 @@ if (program.parse.length === 0) {
 const run = async() => {
   
   try {
-    const compilation = await generateCompilation();
     
     switch (MODE) {
-
+      
       case 'build':
+        compilation = await generateCompilation();
+
         console.log('Building project for production.'.yellow);
         
         await runProdBuild(compilation);
@@ -62,12 +74,22 @@ const run = async() => {
         
         break;
       case 'develop':
+        compilation = await generateCompilation();
+
         console.log('Starting local development server'.yellow);        
         
         await runDevServer(compilation);
         
         console.log('Development mode activiated'.green);
 
+        break;
+      case 'eject':
+        console.log('Ejecting configurations'.yellow);
+
+        await ejectConfigFiles(cmdOption.all);
+        
+        console.log(`Configurations ejected successfully to ${process.cwd()}`.green);
+
         break;
       default: 
         console.log('Error: missing command. try checking --help if you\'re encountering issues');
diff --git a/packages/cli/src/lifecycles/context.js b/packages/cli/src/lifecycles/context.js
index 7778502d..64dff40e 100644
--- a/packages/cli/src/lifecycles/context.js
+++ b/packages/cli/src/lifecycles/context.js
@@ -4,6 +4,7 @@ const defaultTemplatesDir = path.join(__dirname, '../templates/');
 const defaultConfigDir = path.join(__dirname, '../config');
 const scratchDir = path.join(process.cwd(), './.greenwood/');
 const publicDir = path.join(process.cwd(), './public');
+const dataDir = path.join(__dirname, '../data');
 
 module.exports = initContexts = async({ config }) => {
 
@@ -33,8 +34,9 @@ module.exports = initContexts = async({ config }) => {
       const userHasWorkspaceWebpackDevelop = await fs.exists(path.join(process.cwd(), webpackDev));
       const userHasWorkspaceBabel = await fs.exists(path.join(process.cwd(), babelConfig));
       const userHasWorkspacePostCSS = await fs.exists(path.join(process.cwd(), postcssConfig));
-
+      
       let context = {
+        dataDir,
         scratchDir,
         publicDir,
         pagesDir: userHasWorkspacePages ? userPagesDir : defaultTemplatesDir,
diff --git a/packages/cli/src/tasks/eject.js b/packages/cli/src/tasks/eject.js
new file mode 100644
index 00000000..c324856d
--- /dev/null
+++ b/packages/cli/src/tasks/eject.js
@@ -0,0 +1,26 @@
+const fs = require('fs');
+const path = require('path');
+
+module.exports = ejectConfigFiles = async (copyAllFiles) => {
+  return new Promise(async (resolve, reject) => {
+    try {
+
+      if (copyAllFiles) {
+        configFilePaths = fs.readdirSync(path.join(__dirname, '../config'));
+      } else {
+        configFilePaths = [
+          'webpack.config.common.js',
+          'webpack.config.develop.js',
+          'webpack.config.prod.js'
+        ];
+      }
+      configFilePaths.forEach(configFile => {
+        fs.copyFileSync(path.join(__dirname, '../config', configFile), path.join(process.cwd(), configFile));
+        console.log(`Ejecting ${configFile}`.blue);
+      });
+      resolve();
+    } catch (err) {
+      reject(err);
+    }
+  });
+};
\ No newline at end of file
diff --git a/www/pages/docs/build.md b/www/pages/docs/build.md
index 2c47c2aa..84f2d372 100644
--- a/www/pages/docs/build.md
+++ b/www/pages/docs/build.md
@@ -8,6 +8,7 @@ linkheadings: 3
 
 # Build Configurations
 
+A number of [core build configuration files](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/config) can be overridden by creating each file within the root path of your project. You can also automate this task in a single command [see eject configurations](#eject-configurations).
 
 ### Babel
 
@@ -36,4 +37,29 @@ By default, [this is the postcss.config.js](https://github.com/ProjectEvergreen/
 ### Webpack
 
 The webpack config for production(`webpack.config.prod.js`) and development(`webpack.config.develop.js`) can be overridden by providing your own custom configuration within your project's directory.  You can eject the default core configurations into your project using greenwood cli [eject](#eject-configurations) command and then edit them after, which is the simplest and recommended method for modifying webpack config.  If you wish to revert back to the default provided configuration, simply delete these 2 files from your project's root directory. 
-A number of core build configuration files can be overridden by creating each file within the root path of your project.
\ No newline at end of file
+A number of core build configuration files can be overridden by creating each file within the root path of your project.
+
+### Eject Configurations
+
+From greenwood CLI you can eject [core configuration files(webpack, postcss, babel, browserslistrc)](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/config) into your project's working directory which will make it easier to add your own customizations.  To do so, add the following to your package.json `scripts` object:
+
+**package.json**
+
+```js
+{
+  "scripts": {
+    "eject":  "greenwood eject --all",
+  }
+}
+
+```
+
+> Note: The `--all` option is to eject all config files. If you only want to eject webpack config files, remove the `--all`
+
+You can then run:
+
+```bash
+$ npm run eject
+```
+
+To run the eject task which copies all the configuration files into your project's working directory.


Test patch:
diff --git a/packages/cli/test/cases/eject.default/eject.default.spec.js b/packages/cli/test/cases/eject.default/eject.default.spec.js
new file mode 100644
index 00000000..40fe7b61
--- /dev/null
+++ b/packages/cli/test/cases/eject.default/eject.default.spec.js
@@ -0,0 +1,147 @@
+/*
+ * Use Case
+ * Run Greenwood eject command to copy core configuration files
+ *
+ * User Result
+ * Should eject configuration files to working directory
+ *
+ * User Command
+ * greenwood eject
+ */
+const fs = require('fs-extra');
+const path = require('path');
+const expect = require('chai').expect;
+const TestBed = require('../../../../../test/test-bed');
+
+describe('Eject Greenwood With: ', function() {
+  let setup;
+
+  before(async function() {
+    setup = new TestBed();
+    this.context = await setup.setupTestBed(__dirname);
+  });
+
+  describe('Default Eject Option', function() {
+
+    before(async function() {
+      await setup.runGreenwoodCommand('eject');
+    });
+
+    it('should output webpack config files to project working directory', function() {
+      let configFiles = fs.readdirSync(__dirname);
+      
+      configFiles = configFiles.filter((file) => file !== 'node_modules' && file !== 'eject.default.spec.js');
+      
+      /* 
+      * 'webpack.config.common.js',
+      * 'webpack.config.develop.js',
+      * 'webpack.config.prod.js'
+      */
+      expect(configFiles.length).to.equal(3);
+    });
+
+    it('should output webpack common config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'webpack.config.common.js'))).to.be.true;
+    });
+
+    it('should output webpack develop config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'webpack.config.develop.js'))).to.be.true;
+    });
+
+    it('should output webpack prod config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'webpack.config.prod.js'))).to.be.true;
+    });
+
+    after(function() {
+      // remove files
+      const configFiles = fs.readdirSync(__dirname);
+
+      configFiles.forEach(file => {
+        if (file !== 'eject.default.spec.js' && file !== 'node_modules') {
+          fs.remove(path.join(__dirname, file));
+        }
+      });
+    });
+  });
+
+  describe('Eject All Option', function() {
+
+    before(async function() {
+      await setup.runGreenwoodCommand('eject --all');
+    });
+
+    it('should output webpack, postcss, babel, browserlistrc config files to working directory', function() {
+      let configFiles = fs.readdirSync(__dirname);
+
+      configFiles = configFiles.filter((file) => file !== 'node_modules' && file !== 'eject.default.spec.js');
+
+      /* 
+      * '.browserslistrc',
+      * 'babel.config.js',
+      * 'postcss.config.js',
+      * 'webpack.config.common.js',
+      * 'webpack.config.develop.js',
+      * 'webpack.config.prod.js'
+      */
+      expect(configFiles.length).to.equal(6);
+    });
+
+    it('should output webpack common config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'webpack.config.common.js'))).to.be.true;
+    });
+
+    it('should output webpack develop config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'webpack.config.develop.js'))).to.be.true;
+    });
+
+    it('should output webpack prod config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'webpack.config.prod.js'))).to.be.true;
+    });
+
+    it('should output babel config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'babel.config.js'))).to.be.true;
+    });
+
+    it('should output postcss config', function() {
+      expect(fs.existsSync(path.join(__dirname, 'postcss.config.js'))).to.be.true;
+    });
+
+    it('should output browserslist', function() {
+      expect(fs.existsSync(path.join(__dirname, '.browserslistrc'))).to.be.true;
+    });
+
+    after(function() {
+      // remove files
+      const configFiles = fs.readdirSync(__dirname);
+
+      configFiles.forEach(file => {
+        if (file !== 'eject.default.spec.js' && file !== 'node_modules') {
+          fs.remove(path.join(__dirname, file));
+        }
+      });
+    });
+  });
+
+  describe('Eject and Build Ejected Config', function() {
+
+    before(async function() {
+      await setup.runGreenwoodCommand('eject');
+      await setup.runGreenwoodCommand('build');
+    });
+
+    runSmokeTest(['public', 'index', 'not-found', 'hello'], 'Eject and Build Ejected Config');
+  });
+
+  after(function() {
+    setup.teardownTestBed();
+
+    // remove ejected files
+    const configFiles = fs.readdirSync(__dirname);
+
+    configFiles.forEach(file => {
+      if (file !== 'eject.default.spec.js') {
+        fs.remove(path.join(__dirname, file));
+      }
+    });
+  });
+});
\ No newline at end of file
diff --git a/test/test-bed.js b/test/test-bed.js
index efa5b1f5..7d930efd 100644
--- a/test/test-bed.js
+++ b/test/test-bed.js
@@ -90,7 +90,8 @@ module.exports = class TestBed {
 
       const runner = os.platform() === 'win32' ? 'node.cmd' : 'node';
       const npm = spawn(runner, [cliPath, task], {
-        cwd: this.rootDir
+        cwd: this.rootDir,
+        shell: true
       });
 
       npm.on('close', code => {