Automatize tailwind installation on create-react-app with Node.js
Published: 2021-04-26 13:56:50

## Projects configuration might be complicated and boring.
&nbsp;
I recently went into the process of learning how to automatize some annoying tasks I often do in my projects using Node.js.

I picked tailwind and create-react-app as an example, but this is mainly about showing Node.js scripting possibilities to save time. The final result is to type :

```bash
node myscript.js appname
```
instead of :
```bash
npx create-react-app appname
```

And our normal CRA is generated with tailwind fully installed according to tailwind documentation.

Before jumping into the next part, I advise you to be comfortable with javascript promises and to have a quick look to node.js fs and child process documentation if you don't know them yet.

&nbsp;
## Script overview :
&nbsp;
Have a look at the tailwind documentation to see how to install it on a CRA.
https://tailwindcss.com/docs/guides/create-react-app
&nbsp;
Basically, we want to code each step of the installation process explained in the documentation so our script has to handle these things :

- execute the npx create-react-app appname command.
- Install tailwind packages.
- Install a package called craco.
- Modify the script section of the package.json file.
- Create a craco.config.js file at the root of the project.
- Generate a tailwindcss config file.
- And finally import tailwind css in our index.css file.

We identified each part of the mission, this is just a simple algorithm to code now.
&nbsp;
## The script :
&nbsp;
### Preparation :
Firstly create a javascript file, we want to convert the above steps in js code.
Child process exec method allows us to execute commands and fs to manipulate files.
Sadly their methods work with a callback so we do a quick adaptation to be able to use them with async/await thanks to the "util" module..
Include at the top of your script the following import :
```js
const util = require('util');
const { exec } = util.promisify(require('child_process'));
const { readFile, writeFile, appendFile } = util.promisify(require('fs'));
```
&nbsp;
### Code :
- Execute the npx create-react-app appname command :
```js
const installation = async () => {
    await exec(`npx create-react-app ${process.argv[2]}`); 
}
```
Installation is the main function of our script, and we execute our first command here !
process.argv[2] is the argument given when we launch the script such as :
```bash
 node index.js appname
```
Here process.argv[2] is equal to "appname".
- The next step is to install packages :
```js
const installation = async () => {
    await exec(`npx create-react-app ${process.argv[2]}`);
    await exec(`cd ${process.argv[2]} && npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 && npm install @craco/craco`)
}
```
- Now we need to get the package.json file to modify the script section including craco script as mentioned in the tailwind doc. 
At this point the creacte-react-app has been generated and so we have to change our location to be inside the project and import package.json to manipulate it as a javascript object, finally we overwrite the file with the new modification.
This is where the fs module come to help us !

We create a new function in charge of doing the package.json modification :
```js
const modifyPackageJson = async () => {
  const data = await readFile(`${process.argv[2]}/package.json`);
  packageJson = JSON.parse(data);
  packageJson.scripts = {
    start: 'craco start',
    build: 'craco build',
    test: 'craco test',
    eject: 'react-scripts eject',
  };
  await writeFile(`${process.argv[2]}/package.json`, JSON.stringify(packageJson));
};
```
- Create the craco.config.js file.
We will proceed in the same way, create a new function and write the file with the require data :
```js
const addCracoConfig = async () => {
  const cracoConfig = `module.exports = {
	style: {
		postcss: {
			plugins: [
				require('tailwindcss'),
				require('autoprefixer'),
			],
		},
	},
}`;
  await writeFile(`${process.argv[2]}/craco.config.js`, cracoConfig);
};
```

Add the 2 last functions we created to the main function :

```js
const installation = async () => {
    await exec(`npx create-react-app ${process.argv[2]}`);
    await exec(`npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 && npm install @craco/craco`)
    await modifyPackageJson();
    await addCracoConfig();
}
```
- Generate the tailwindcss config file :
```js
const installation = async () => {
    await exec(`npx create-react-app ${process.argv[2]}`);
    await exec(`npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 && npm install @craco/craco`)
    await modifyPackageJson();
    await addCracoConfig();
    await exec(`cd ${process.argv[2]} && npx tailwindcss init`);
}
```
- Finally we need to include tailwind in the index.css.
Again we use fs but we need to write the new code at the **top** of the index.css file. So this is going to be a read->write->append operation.

I create a new function called "includeTailwindCss" :
```js
const includeTailwindCss= async () => {
  const tailwindImport = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`;
  const data = await readFile(`${process.argv[2]}/src/index.css`);
  await writeFile(`${process.argv[2]}/src/index.css`, tailwindImport);
  await appendFile(`${process.argv[2]}/src/index.css`, data);
};
```
We extract the index.css code in a data variable so that we can overwrite index.css with the new data. Then we append to the index.css the old data stored in the data variable, in this way the tailwind import lines are at the top of our css file.

Add this last function to our installation function and I think we are good now !

The final code should looks like this :

```js
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const readFile = util.promisify(require('fs').readFile);
const writeFile = util.promisify(require('fs').writeFile);
const appendFile = util.promisify(require('fs').appendFile);

const installation = async () => {
  await exec(`npx create-react-app ${process.argv[2]}`);
  await exec(`cd ${process.argv[2]} && npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 && npm install @craco/craco`);
  await modifyPackageJson();
  await addCracoConfig();
  await exec(`cd ${process.argv[2]} && npx tailwindcss init`);
  await includeTailwindCss();
};

const modifyPackageJson = async () => {
  const data = await readFile(`${process.argv[2]}/package.json`);
  packageJson = JSON.parse(data);
  packageJson.scripts = {
    start: 'craco start',
    build: 'craco build',
    test: 'craco test',
    eject: 'react-scripts eject',
  };
  await writeFile(`${process.argv[2]}/package.json`, JSON.stringify(packageJson));
};

const addCracoConfig = async () => {
  const cracoConfig = `module.exports = {
	style: {
		postcss: {
			plugins: [
				require('tailwindcss'),
				require('autoprefixer'),
			],
		},
	},
}`;
  await writeFile(`${process.argv[2]}/craco.config.js`, cracoConfig);
};

const includeTailwindCss = async () => {
  const tailwindImport = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`;
  const data = await readFile(`${process.argv[2]}/src/index.css`);
  await writeFile(`${process.argv[2]}/src/index.css`, tailwindImport);
  await appendFile(`${process.argv[2]}/src/index.css`, data);
};

installation();
```

Now launch your script in the folder where you want to create your react-app :

```bash
node myscript.js appname
```

And that's all !

Finally you can go further and add real command logs results using node.js EventEmitter, errors handling, publish your script as a npm package etc. This was just an example with tailwind but there are a lot of possibilities everywhere and things to improve.

We can always find things to optimize.

What do you think about my method ? Have you a cleaner way to do this ?

Thanks for reading and have a good day.