Skip to content
Lucky Snail Logo Lucky Snail
中文

Building a Front-End Scaffold (Part 1)

/ 8 min read /
#前端工程化
Table of Contents 目录

When learning Vue or React, we’ve probably all used their scaffolding tools (create-vue, create-react-app). With scaffolding, we can quickly get a Vue or React project without spending effort on building the basic project skeleton, allowing us to develop features faster.

To make it easier to understand, I’ll try to explain the “ins and outs” of scaffolding step by step through a scenario. In the scenario, the protagonist — A Niu, a front-end developer who isn’t very good at writing code.

What is Scaffolding?

Before building our own scaffolding, let’s get to know it again! Scaffolding is: “A tool or framework used to quickly generate the basic structure, configuration files, and code templates of a project. It usually contains predefined directory structures, configuration files, dependency management tools, build tools, etc. The main purpose of scaffolding is to reduce repetitive work, improve development efficiency, and ensure consistency in project structure.”

In front-end development, scaffolding tools usually help us with the following tasks:

  • Initialize project directory structure
  • Configure development environment (e.g., Webpack, Babel, ESLint, etc.)
  • Integrate commonly used libraries and tools (e.g., Vue, React, TypeScript, etc.)
  • Provide code templates and best practices

Using scaffolding, we get an expected template project that already includes the infrastructure we need.

What Problems Does Scaffolding Solve?

  1. Tedious project initialization: Manually creating project directories, configuration files, installing dependencies, etc., is very time-consuming and error-prone. Scaffolding automates these steps, generating the project skeleton with one click.
  2. Unified development standards: Every team has its own code style, directory structure, and dependency management approach. Scaffolding can enforce these standards, ensuring team members work in a unified environment.
  3. Improved development efficiency: Through pre-configured development environments, build processes, and testing frameworks, scaffolding can greatly improve development efficiency, allowing developers to focus on implementing business logic.
  4. Reduced repetitive work: When using the same configurations and dependencies across multiple projects, scaffolding avoids repetitive work and increases code reuse.
  5. Lower learning cost: For new developers, scaffolding provides a clear project structure and best practices, lowering the learning curve.
  6. Team technical accumulation: If a team has multiple projects, and feature B implemented in project A is also needed in project C, accumulating it into scaffolding is a better way to promote it, reducing repetitive work and improving efficiency.
  7. Best practice accumulation: When we integrate configurations and capabilities into scaffolding, we have to consider whether they are the best implementation, whether they fit the team, code style issues, etc.

When We Don’t Need Scaffolding

  • Existing scaffolding already meets our needs.
  • The project is very simple; manually creating the project is a better choice.
  • The team is small, the tech stack is very unified, everyone is familiar with project development standards, so scaffolding is unnecessary — it only adds extra process.
  • Time and resources are limited: don’t build scaffolding at this time, as maintaining it also costs.

Quickly Running Through a Core Scaffolding Flow

Requirements Analysis

Our goal is to develop a scaffolding tool similar to vue cli, allowing us to download remote GitHub repository code using our own scaffolding tool. Let’s first experience create-vue:

Terminal window
npm create vue@latest
image-20250202165310697

Through interaction, we get a basic project template. A simple analysis reveals three main steps:

  1. The user runs npm create xxx in the terminal, then downloads a scaffolding package.
  2. After downloading, an interactive page runs in the terminal. Through step-by-step interaction, we finally get the package we want.
  3. After the user completes the interaction logic, the corresponding code is installed locally. We need to provide friendly download progress prompts and some post-installation tips.

Feature Development

Step 1

Let’s develop step by step according to our analysis, and then optimize at the end. First, develop step 1 (project name create-lucky, note that if we want to create a project via npm create, the project name must start with create-).

Terminal window
# Create folder and enter
mkdir create-lucky && cd create-lucky
# Initialize package.json and index.js
npm init -y && nano index.js
# Then enter the following code in index.js, note the first line must be #!/usr/bin/env node
#!/usr/bin/env node
console.log("Hello from create-lucky!");
# To make index.js executable, add execute permission, then run
chmod +x index.js
# Then modify package.json
"main": "src/index.js",
"bin": {
"create-lucky": "src/index.js"
},
# Local test, run npm link
npm link
# In any directory, run the following command to test the scaffolding
npm create lucky
# Publish 1, first check npm registry: https://registry.npmjs.org/
npm get registry
# Publish 2, if not registered for npm account, register first, follow the prompts to enter username, password, and email.
npm login
# Publish 3, publish to npm
npm publish
# Check if publish succeeded, there may be a ten-second delay, go to https://www.npmjs.com/~luckysnail, where luckysnail is your npm account name
# Run npm create lucky locally to verify
image-20250202173927965

Seeing this confirms step 1 is complete!

Step 2

In this step, we need to create an interaction in the terminal so users can install the desired project using the scaffolding.

Terminal window
# Install inquirer: a library for command-line user interaction
npm i inquirer
# Update src/index.js code
#!/usr/bin/env node
const inquirer = require("inquirer");
async function main() {
const answers = await inquirer.default.prompt([
{
type: "list",
name: "template",
message: "Please select the template you want to use:",
choices: ["Vue", "React", "React19", "NextJs"],
},
{
type: "input",
name: "customUrl",
message: "Please enter the project name:",
},
]);
let packageUrl = "";
if (answers.template === "custom-app") {
packageUrl = answers.customUrl;
} else {
packageUrl = `https://github.com/your-org/${answers.template}`;
}
console.log(`You selected the package URL: ${packageUrl}`);
}
main();
# Re-run npm link, test, then publish and test again

More inquirer usage: https://www.npmjs.com/package/inquirer

Step 3

Finally, we need to generate the corresponding code based on the user’s selection.

Terminal window
# Install required libraries, chalk: terminal font styles; ora: display loading animations; download-git-repo: download and extract git repositories
npm i chalk ora download-git-repo
# src/constants.js define constants
/** List of projects */
export const PROJECT_LIST = [
{
name: "vue",
desc: "vue3 + pinia + vue-router",
value: "https://github.com/axin-s-Template/vue-light-starter.git",
},
{
name: "React19",
desc: "React 19 + Tailwind 3 + Biome + RSBuild + shadui/cn lightweight starter template",
value: "https://github.com/axin-s-Template/react19-light-starter",
},
{
name: "React",
desc: "React lightweight template (suitable for simple apps), using Vite + TypeScript + TailwindCss + shadcn/ui + Zustand + react-router-dom simple SPA starter template",
value: "https://github.com/axin-s-Template/react-light-starter",
},
{
name: "NextJs",
desc: "Next.js 14+ basic starter template",
value: "https://github.com/axin-s-Template/Nextjs-Boilerplate",
},
];
# Set package.json's "type": "module",
# Update src/index.js
#!/usr/bin/env node
import inquirer from "inquirer";
import chalk from "chalk";
import ora from "ora";
import { promisify } from "util";
import downloadGitRepo from "download-git-repo";
import { PROJECT_LIST } from "./constants.js";
const download = promisify(downloadGitRepo);
async function main() {
const answers = await inquirer.prompt([
{
type: "list",
name: "template",
message: "Please select the template you want to use:",
choices: PROJECT_LIST.map((item) => ({
name: `${item.name} - ${item.desc}`,
value: item.name.toLowerCase(),
})),
},
{
type: "input",
name: "projectName",
message: "Please enter the project name:",
validate: (input) => {
if (!input.trim()) {
return "Project name cannot be empty";
}
return true;
},
},
]);
const selectedTemplate = PROJECT_LIST.find((item) => item.name.toLowerCase() === answers.template);
if (!selectedTemplate) {
console.log(chalk.red("No matching template found"));
return;
}
const spinner = ora("Downloading template...").start();
try {
const repoUrl = selectedTemplate.value.replace("https://github.com/", "").replace(".git", "");
await download(repoUrl, answers.projectName);
spinner.succeed(chalk.green("Template downloaded successfully!"));
console.log(chalk.blue("\nGetting started:"));
console.log(chalk.cyan(` cd ${answers.projectName}`));
console.log(chalk.cyan(" npm install"));
console.log(chalk.cyan(" npm run dev\n"));
} catch (err) {
spinner.fail(chalk.red("Download failed: " + err.message));
process.exit(1);
}
}
main().catch((err) => {
console.error(chalk.red(err));
process.exit(1);
});

Here, we run node ./src/index.js. If you see this, it means everything is fine!

image-20250204150625035

At this point, our basic scaffolding is complete! We can publish it to the npm registry and try it again. Remember to run npm unlink create-lucky first. Then run npm create lucky. Remember to modify the version in package.json each time you publish.

Optimizations

The current version of this scaffolding tool still has many shortcomings:

  • It uses JS, which is hard to maintain. Scaffolding is usually needed by teams, so the scaffolding code should be standardized.
  • It’s cumbersome to test: using npm link for testing, then needing npm unlink to cancel the link.
  • It lacks command parameter capabilities, e.g., -v, --help, etc., which are essential for a command-line tool.
  • It doesn’t support dynamically generating project information, e.g., initializing basic project info (name, description, etc.) when creating from a template.
  • The terminal isn’t visually appealing or cool — which is actually important since the first impression of your scaffolding is the terminal experience.

Therefore, we can use monorepo + @clack/prompt to optimize for the next version.

  • Use monorepo for better code maintenance and validation.
  • Use @clack/prompt to provide more beautiful and cool terminal interactions.

I’ll write another post about the optimizations!

Summary

First, thank you for reading this far. If you’ve read this, you probably think building your own scaffolding is valuable. In my opinion, scaffolding has two main values:

  1. For yourself: I think scaffolding brings the biggest benefit to the individual. During the development of scaffolding, you come up with your own best practices, you build your own development standards, and this cultivates a personal engineering mindset — a pursuit of perfection.
  2. For others: Others can quickly set up a project skeleton using your scaffolding. They benefit from your best practices, saving them time. Therefore, after development, we need to write documentation to promote our scaffolding.