Building an NPX Script for Project Setup

Stefan B.
Stefan B.
Published October 7, 2024
Cover image

Setting up a new project can be tedious and repetitive from multiple angles. As end-users, we are learning a new platform, new paradigms, and maybe a new framework to build our next dream project.

As a company, we want to get our potential users up and running as quickly as possible and make it easy for them to explore our platform's benefits without friction. Nothing frustrates users more than a complex setup process where things don't work out of the box. In the end, they will bounce and find a tool that works better, faster, and more reliable.

Therefore, an easy way to set up a new project can be a crucial aspect of a product strategy. For the web, npm is one of the standard package managers, and with that (at least since version 5.2) comes a tool for executing commands called npx.

In this post, we want to explore how to build a setup tool in JavaScript that can be called using npx. The principles can be adopted for all web projects (platforms, frameworks, SDKs). In our case, it will create a Next.js app powered by Stream's React Chat SDK. You can find the complete code for this project here.

Project structure

As we said before, the project's structure can be reused for all other web platforms, so we want to focus on the general structure before we go into the concrete details.

In our case, we will have the following steps in our script:

  • Ask for the project name
  • Clone a sample repository
  • Install the necessary dependencies
  • Cleanup and create a new git project
  • Customize the project with the user's secret and credentials

The project's baseline will be a JavaScript file that executes all the logic. It will first ask the user to input a project name and then execute the command to clone the GitHub template repository (if you’re curious to learn more about how to build with Stream, see here) into a folder with that name. We will use a repository built using the best practices to serve as a starter project. We will then install the dependencies and clean up the git repository with a new history.

For the last part, we will again request information from the user. These will be project-specific credentials. In our case, for access to the chat information, users will need to retrieve an API key and a secret from the Stream Dashboard. Since we can't get this information from anywhere else, we need to ask the user to explicitly input it.

We then create a new .env file with the credentials and add them to the project structure. Then, we change the user credentials inside a specific file. By requesting these few credentials, we can customize the repository to work perfectly for the given user.

Project setup

We start by initializing a new project using npm. Inside a newly created folder for our project, we run this command:

npm init -y

The -y parameter automatically answers all the setup questions asked during the initialization phase. With this, we will have a package.json file in our current folder that should look like this:

json
1
2
3
4
5
6
7
8
9
10
11
12
{ "name": "stream-nextjs-setup", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "description": "" }

We can fill out the remaining info, but will skip this since it doesn't add value to this article. However, we will add one property to this file manually. We can see it has a scripts field that many people know for executing e.g. build commands using npm run or yarn.

We want to add another field here called bin. This specifies a runnable command that we will later be able to execute using npx. First, we create an empty file called setup-script.js. Then, we add the bin field specifying the name of the command first (in our case, setup-script) and then specifying the file to execute. We add this to the package.json file:

json
1
2
3
"bin": { "setup-project": "./setup-script.js" }

That sets up the project, and we can start filling out the setup-script.js file.

Getting the Project Name

We want to allow the user to add the project name differently. First, a parameter called name is added to the command's execution. Second, if the user does not provide that information upon execution, we will ask them to input it before executing our script.

To get a named parameter from the execution (when the user enters setup-project --name my-project-name), we use a helper package called yargs. We can install it like this:

npm i yargs
#or
yarn add yargs

For prompting and collecting user information, we use the readline package that comes with Node.js. We create two helper functions. One that creates an interface to use (because we will need this again later). The second function is to ask a question, wait for user input, and return that. Inside setup-script.js, we add these two functions:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const readline = require('readline'); const createInterface = () => { return readline.createInterface({ input: process.stdin, output: process.stdout, }); }; const askQuestion = (rl, question) => { return new Promise((resolve) => { rl.question(question, (answer) => { resolve(answer); }); }); };

Then, we jump into the script and add the logic. To keep the code clean, we separate all logical steps into separate functions, and we call this one getProjectName. We check if the user provided an argument called name; if not, we prompt them to enter the project name.

Add the following code at the top of the file:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env node const yargs = require('yargs'); const getProjectName = async () => { let projectName = yargs.argv.name; if (!projectName) { const rl = createInterface(); projectName = await askQuestion(rl, 'Enter project name: '); rl.close(); } return projectName; };

Next, we want to define a main function where we call all the steps sequentially, starting with the getProjectName function and finally call that main function:

js
1
2
3
4
5
const main = async () => { const projectName = await getProjectName(); }; main().catch(console.error);

We have the project name and can continue with the next step.

Clone and Cleanup the Repository

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Next, we set up the repository, install the dependencies and do a cleanup. Like we have mentioned, we will use a repository using a Next.js project, powered by the Stream Chat React SDK, but this can be used for any arbitrary project.

We must execute commands on the user's machine to perform these actions. We will again create a helper function called execCommand to make this repetitive process easier. Add this code to the setup-script.js file:

js
1
2
3
4
5
6
7
8
9
10
const { execSync } = require('child_process'); const execCommand = (command, options = {}) => { try { execSync(command, { stdio: 'inherit', ...options }); } catch (error) { console.error(`Error executing command: ${command}`); process.exit(1); } };

For cloning the template repository (which can be found here) we use the git command-line tool (assuming that it is installed on the user's machine). Then, we must switch to the project directory and run a simple npm install command to install dependencies. Finally, we remove the existing .git file and initialize a new git repository inside the project folder with an initial commit.

We define the function called setupRepository like this:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path'); const TEMPLATE_REPO_URL = 'https://github.com/GetStream/nextjs-chat-template'; const setupRepository = async (projectName) => { execCommand(`git clone --depth=1 ${TEMPLATE_REPO_URL} ${projectName}`); const projectPath = path.join(process.cwd(), projectName); execCommand('npm install', { cwd: projectPath }); fs.rmSync(path.join(projectPath, '.git'), { recursive: true }); execCommand('git init', { cwd: projectPath }); execCommand('git add .', { cwd: projectPath }); execCommand('git commit -m "Initial commit"', { cwd: projectPath }); };

Lastly, we add a call to the setupRepository function inside the main function:

js
1
2
3
4
5
const main = async () => { const projectName = await getProjectName(); await setupRepository(projectName); };

Customizing User Credentials

The project is set up like this, and we could stop here and help the user figure out how to configure the rest. However, we want to go the extra mile and help them with custom configurations requiring things like apiKey and secret.

For this, we again prompt the user to input the relevant information. The difference now is that we will create a specific file, an environment file, and change the code in another file to set the user-specific information.

We start by requesting the user input the apiKey and the Stream secret (which are both information they need to retrieve from the Stream Dashboard). We then create a new file called .env.local and fill it with the secrets. We know how to name them since we created the code in the repository ourselves.

We open up setup-script.js and add a new function that does this:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const customizeProject = async (projectName) => { const projectPath = path.join(process.cwd(), projectName); const rl = createInterface(); // Step 1: Tell the user where to get the relevant information console.log('You now need to provide your Stream Chat API key and secret.\n'); console.log('You can get your Stream Chat API key and secret from the Stream Dashboard at: https://dashboard.getstream.io/ \n\n'); // Step 2: Ask for Stream Chat API key and secret const apiKey = await askQuestion(rl, 'Enter your Stream Chat API key: '); const apiSecret = await askQuestion(rl, 'Enter your Stream Chat secret: '); // Step 3: Create .env.local file const envLocalPath = path.join(projectPath, '.env.local'); if (!fs.existsSync(envLocalPath)) { fs.appendFileSync( envLocalPath, `NEXT_PUBLIC_STREAM_API_KEY=${apiKey}\n`, 'utf8' ); fs.appendFileSync(envLocalPath, `STREAM_SECRET=${apiSecret}\n`, 'utf8'); } }

Next, upon running the project, we want to initialize a user and load their data. For that, we require an existing user in the project, so we ask the user to create one in the dashboard and ask them to enter the credentials afterward.

Then, we change the code inside the newly set up project to replace it with the information they provide for us. For that, we again create a helper function to update a certain portion of a file and replace it with other content:

js
1
2
3
4
5
6
7
const fs = require('fs'); const updateFile = (filePath, searchValue, replaceValue) => { const content = fs.readFileSync(filePath, 'utf8'); const updatedContent = content.replace(searchValue, replaceValue); fs.writeFileSync(filePath, updatedContent, 'utf8'); };

And with that, we can fill up the rest of the customizeProject function, asking the user for a userId and a userName and then changing the respective lines inside the page.tsx file using the updateFile function we just created:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const customizeProject = async (projectName) => { /* previous code */ // Step 4: Ask for userId and userName const userId = await askQuestion(rl, 'Enter the user ID: '); const userName = await askQuestion(rl, 'Enter the user name: '); rl.close(); // Step 5: Update page.tsx updateFile( path.join(projectPath, 'app/page.tsx'), 'const userId = undefined;', `const userId = "${userId}";` ); updateFile( path.join(projectPath, 'app/page.tsx'), 'const userName = undefined;', `const userName = "${userName}";` ); }

With that, we can call the customizeProject inside the main function:

js
1
2
3
4
5
6
7
const main = async () => { const projectName = await getProjectName(); await setupRepository(projectName); await customizeProject(projectName); };

We have added all the logic we need, now, we only need to run the script to validate it.

Running the Script

We have two options for running the script. For testing purposes, we can simply call the script from the terminal like this:

bash
1
./setup-script.js

While this works and we can use it we want to see if we can also run it as a npx command. The way this works globally would be to upload the package to the npm registry and make it available there.

However, for testing locally, we can manually link the package on our machine and make our script available like this. For that, we navigate to the repository where the script is located and run the following command:

bash
1
npm link

After that, we can run the command on our machine just like we would if it were globally available:

bash
1
npx setup-project

Feel free to test running this command and see the magic of the script we've created running on your machine.

Summary

In this post, we've examined how to build our own setup npx script. We have requested user input and used it inside our script. Then, we cloned a repository and cleaned it up, giving our user a blank slate from which to start. Finally, we've added the user's custom configuration to provide them with a great developer experience and the ability to start their project immediately.

Again, we want to stress the importance of giving users the option to get started with our Chat SDKs and products as frictionless as possible. The principles discussed in this post can be applied to many other projects with minimal adoption.

We hope you enjoyed this article and let us know on X what kind of great scripts you build yourselves.

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->