How to implement a create-vite-like scaffolding and publish it to npm

Foreword

Recently I was working on an electron ecology-related project. Since I had to do some project initialization functions, I wrote a scaffolding to do this. Then I learned about and practiced the scaffolding-related functions in detail, and finally succeeded in making it. I think If you need the scaffolding, share your relevant experiences here.

Let’s take a look at Vite’s official website first.

image.png

The goal we want to achieve is the same. yarn create electron-prokit myapp can quickly build an electron project directly.

What is npm create

Just run it from the command line and you’ll know

npm create --help

npm create.png

In other words, npm create is actually an alias of npm init

This method can be used to build the app when node version >=6.10

npm will splice create- in front of the initial item you provide and then use the npx tool to download and execute the method, that is

npm create vite
// Equivalent to
npm init vite
// Equivalent to
npx create-vite
// Equivalent to
npm install create-vite -g & amp; & amp; create-vite

So npm create vite means using the create-vite scaffolding to create a vite project.

The same goes for yarn: https://classic.yarnpkg.com/en/docs/cli/create

Once you understand this, you can start designing the scaffolding.

Scaffolding function

Our scaffolding is named create-electron-prokit. As the name suggests, it is a generator for the electron-prokit series of projects. Its main function is to produce electron-prokit Related projects, split details, our function points are as follows.

  • Receives the project name, description, etc. input by the user, and is used to determine the directory name and modify the package.json file.
  • Receive user input to customize project content (such as selection of frameworks).
  • Download the electron-prokit template code locally.
  • Give feedback on the creation progress and creation results.

Technical selection

Now that we know the function, we need to make some technical selections. After reading the source code of create-vite, we can also learn from related technical tools.

  • Development language tools: typescript, ts-node
  • Processing command: commander
  • Handle interaction: inquirer
  • Download the git warehouse template: git-clone
  • Semantic template: handlebars
  • Command line beautification: ora
  • File-related plug-ins: fs-extra

Development steps

Now we will start to talk about how to develop in detail, which is divided into the following 6 steps.

Initialization project

Run from command line

npm i -g pnpm
pnpm init

Then add the necessary information, where main is the entry file, and bin is used to introduce a global command, mapped to dist/index.js. With the bin field, we can directly run create-electron-prokit command, no need for node dist/index.js anymore.

{<!-- -->
  "name": "create-electron-prokit",
  "version": "0.0.1",
  "description": "A cli to create an electron prokit project",
  "main": "dist/index.js",
  "type": "module",
  "bin": {<!-- -->
    "create-electron-prokit": "dist/index.js"
  },
  "keywords": [
    "Electron",
    "electron",
    "electron-prokit",
    "electron prokit",
    "Electron Prokit",
    "Prokit",
    "prokit",
    "create-electron-prokit"
  ],
  "author": "Xutaotaotao",
  "license": "MIT",
}

Let the project support TS
Install typescript and @types/node.

pnpm add typescript @types/node -D

Initialize tsconfig.json

tsc --init
{<!-- -->
  "compilerOptions": {<!-- -->
    "target": "es2016",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "sourceMap": true,
    "outDir": "./dist",
    "importHelpers": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist/**/*"],
}

npm link local debugging

Let’s write a hello world in src/index.ts and test whether ts compilation is normal.

  • src/index.ts
#!/usr/bin/env node --experimental-specifier-resolution=node
const msg: string = 'Hello World'
console.log(msg)

Add the dev option to the scripts of the package.json file

"dev": "node --experimental-specifier-resolution=node --loader ts-node/esm src/index.ts"

Run npm run dev and you can see that Hello World is half the battle. With the above preparations, we can debug locally. But to achieve the same effect as the command line, npm link is needed.

Remember we had a bin configuration in package.json earlier. If we execute the npm link command in the project, you can Run the create-electron-prokit command. But this command points to the file dist/index.js, which is obviously a compiled file, so we need to add some scripts to package.json option makes my development smoother!

"scripts": {
    "dev": "node --experimental-specifier-resolution=node --loader ts-node/esm src/index.ts",
    "build": "tsc",
    "start": "node --experimental-specifier-resolution=node dist/index.js"
  },

npm run build Then run the create-electron-prokit command and you will see hello world! Are you very happy? You have completed half of this project. Everything is difficult at the beginning, and the rest is the development of some logical functions.

Command processing function development

Let’s start with the simplest one, receiving command line parameters.

  • src/index.ts
#!/usr/bin/env node --experimental-specifier-resolution=node

const name = process.argv[2];
if (!name) {<!-- -->
  log.warn("The project name cannot be empty!");
  process.exit(1);
} else {<!-- -->
  init(name);
}

function init(name: string) {<!-- -->
    console.log(name)
}

It’s that simple. Our first function has been developed. The following is to expand the init function.

Development of interactive processing functions

At this step we need to print the log, then ask the user for the corresponding opinions, and then obtain the user’s input and selections.
Install inquirer, ora, fs-extra.

pnpm add inquirer ora fs-extra

Added project description and author input query as well as framework selection

#!/usr/bin/env node --experimental-specifier-resolution=node
import type {<!-- --> QuestionCollection } from "inquirer";
import inquirer from "inquirer";
import ora from "ora";
import fs from "fs-extra";

const log = ora("modify");

async function init(name: string) {<!-- -->
  const InitPrompts: QuestionCollection = [
    {<!-- -->
      name: "description",
      message: "please input description",
      default: "",
    },
    {<!-- -->
      name: "author",
      message: "please input author",
      default: "",
    },
  ];

  const FrameworkOptions: QuestionCollection = {<!-- -->
    type: "list",
    name: "framework",
    message: "Select a framework",
    choices: [
      {<!-- -->
        name: "React",
        value: "React",
      },
      {<!-- -->
        name: "Vue",
        value: "Vue",
      },
    ],
  };
  if (fs.existsSync(name)) {<!-- -->
    log.warn(`Has the same name project,please create another project!`);
    return;
  }
  log.info(`Start init create-electron-prokit project: ${<!-- -->name}`);
  const initOptions = await inquirer.prompt(InitPrompts);
  const frameworkOptions = await inquirer.prompt(FrameworkOptions);
}

function main() {<!-- -->
  const name = process.argv[2];
  if (!name) {<!-- -->
    log.warn("The project name cannot be empty!");
    process.exit(1);
  } else {<!-- -->
    init(name);
  }
}

main()

Here we have optimized and integrated the code to make it clearer. We use ora to beautify the console output, fs-extra to detect whether the folder exists, and inquirer to receive user input and selections. In this step, we obtain the most basic user’s input, and then download the corresponding template through the user’s input, and then change some template information.

image.png

Download template function development

Install git-clone

pnpm add git-clone

Implement download logic in src/download.ts

  • src/download.ts
import path from "path"
import gitclone from "git-clone"
import fs from "fs-extra"
import ora from "ora"

export const downloadTemplate = (
  templateGitUrl: string,
  downloadPath: string
):Promise<any> => {<!-- -->
  const loading = ora("Downloadimg template")
  return new Promise((resolve, reject) => {<!-- -->
    loading.start("Start download template")
    gitclone(templateGitUrl, downloadPath, {<!-- -->
      checkout: "master",
      shallow: true,
    },(error:any) => {<!-- -->
      if (error) {<!-- -->
        loading.stop()
        loading.fail("Download fail")
        reject(error)
      } else {<!-- -->
        fs.removeSync(path.join(downloadPath, ".git"))
        loading.succeed("Download success")
        loading.stop()
        resolve("Download success")
      }
    })
  })
}

Very simple, achieved. Let’s quote it in the init method and define the corresponding template address.

#!/usr/bin/env node --experimental-specifier-resolution=node
import * as tslib from "tslib";
import type {<!-- --> QuestionCollection } from "inquirer";
import inquirer from "inquirer";
import ora from "ora";
import fs from "fs-extra";
import {<!-- --> downloadTemplate } from "./download";

const log = ora("modify");

async function init(name: string) {<!-- -->
  const ReactTemplateGitUrl =
    "https://github.com/Xutaotaotao/ep-vite-react-electron-template";

  const VueTemplateGitUrl =
    "https://github.com/Xutaotaotao/ep-vite-vue3-electron-template";

  const InitPrompts: QuestionCollection = [
    {<!-- -->
      name: "description",
      message: "please input description",
      default: "",
    },
    {<!-- -->
      name: "author",
      message: "please input author",
      default: "",
    },
  ];

  const FrameworkOptions: QuestionCollection = {<!-- -->
    type: "list",
    name: "framework",
    message: "Select a framework",
    choices: [
      {<!-- -->
        name: "React",
        value: "React",
      },
      {<!-- -->
        name: "Vue",
        value: "Vue",
      },
    ],
  };
  if (fs.existsSync(name)) {<!-- -->
    log.warn(`Has the same name project,please create another project!`);
    return;
  }
  log.info(`Start init create-electron-prokit project: ${<!-- -->name}`);
  const initOptions = await inquirer.prompt(InitPrompts);
  const frameworkOptions = await inquirer.prompt(FrameworkOptions);
  const templateGitUrl =
    frameworkOptions.framework === "React"
      ?ReactTemplateGitUrl
      : VueTemplateGitUrl;
  try {<!-- -->
    const downloadPath = `./${<!-- -->name}`;
    // download
    await downloadTemplate(templateGitUrl, downloadPath);
  } catch (error) {<!-- -->
    console.error(error);
  }
}

function main() {<!-- -->
  const name = process.argv[2];
  if (!name) {<!-- -->
    log.warn("The project name cannot be empty!");
    process.exit(1);
  } else {<!-- -->
    init(name);
  }
}

main()

Wow! We are only one step away from success, which is to modify package.json.

Modify package.json function development

Before replacing, we need to modify the package.json of the template and add some slots to facilitate subsequent replacement.

{
  "name": "{<!-- -->{name}}",
  "version": "1.0.0",
  "description": "{<!-- -->{description}}",
  "author": "{<!-- -->{author}}"
}

Installhandlebars

pnpm add handlebars

Implement modification logic in src/modify.ts

  • src/modify.ts
import path from "path"
import fs from "fs-extra"
import handlebars from "handlebars"
import ora from "ora"

const log = ora("modify")

export const modifyPackageJson = function (downloadPath: string, options: any):void {<!-- -->
  const packagePath = path.join(downloadPath, "package.json")
  log.start("start modifying package.json")
  if (fs.existsSync(packagePath)) {<!-- -->
    const content = fs.readFileSync(packagePath).toString()
    const template = handlebars.compile(content)

    const param = {<!-- -->
      name: options.name,
      description: options.description,
      author: options.author,
    }

    const result = template(param)
    fs.writeFileSync(packagePath, result)
    log.stop()
    log.succeed("This project has been successfully created! ")
    log.info(`Install dependencies:

      cd ${<!-- -->downloadPath} & amp; & amp; yarn install
    `)
    log.info(`Run project:

      yarn run dev
    `)
  } else {<!-- -->
    log.stop()
    log.fail("modify package.json fail")
    throw new Error("no package.json")
  }
}

Here we have completed the function to modify the logic, and then imported and used it in the init function.

 try {
    const downloadPath = `./${name}`;
    await downloadTemplate(templateGitUrl, downloadPath);
    modifyPackageJson(downloadPath, { name, ...initOptions } as Options);
  } catch (error) {
    console.error(error);
  }

OK, here we are done! Next release NPM

Publish NPM

Publishing NPM locally is very simple. There are three steps: build, log in to npm, and then publish

Build

Build and run directly

npm run build

Login & amp;publish

First register an account on the npm official website.

In the project root directory, log in to your npm account and enter your username, password, and email address.

npm login

After successful login, just execute npm publish directly.

You can read my article on building a Vue UI component library from scratch (3) – releasing the first npm package

Verification

After we publish successfully, we can verify it locally. We can run it directly.

yarn create electron-prokit my-app

or

npm create electron-prokit my-app

You can see the effect!

image.png

Conclusion

This article uses the simplest way to build a scaffold. The functions in the middle can actually be enriched, but the core process has been implemented, and subsequent functional expansions are only logical additions and changes, mainly to allow everyone to get started quickly! ! !

Project source code: https://github.com/Xutaotaotao/electron-prokit

If this article is helpful to you, please give a small star to my project?