preface
Why write this article? Because I've been working on strve recently JS ecology has learned a lot while building its own framework. So this article introduces a more convenient and flexible command line scaffold tool and how to publish it to NPM.
I have written similar articles on developing command-line tools before, but the core idea is to remotely pull the project template code from the Git warehouse through the code. Sometimes the pull fails due to network speed, and then the initialization project fails.
So, is there a better plan than this? So here comes this article.
Recently, many projects have been developed using Vite tools. I have to admire Mr. You's amazing code ability and created such a good development tool. The development experience is very smooth. Especially when you just initialize the project, you only need to execute one line of command without installing any tools globally. Then, customize and select the required template to initialize the project, and you're done! This operation really surprised me! I wonder if it would be Nice if I applied the idea of create Vite to my own scaffolding tools!
actual combat
So, without saying a word, open the ViteGitHub address.
https://github.com/vitejs
After searching for more than half a day, I finally found the core code of the command-line tool.
https://github.com/vitejs/vite/tree/main/packages/create-vite

What came into view were many folders beginning with template -. I opened several and looked at them. They were all framework project templates. Well, you can put it aside first.
Next, we'll open index JS file to see what content. I list the code, you can have a simple look, don't delve into it.
#!/usr/bin/env node // @ts-check const fs = require('fs') const path = require('path') // Avoids autoconversion to number of the project name by defining that the args // non associated with an option ( _ ) needs to be parsed as a string. See #4606 const argv = require('minimist')(process.argv.slice(2), { string: ['_'] }) // eslint-disable-next-line node/no-restricted-require const prompts = require('prompts') const { yellow, green, cyan, blue, magenta, lightRed, red } = require('kolorist') const cwd = process.cwd() const FRAMEWORKS = [ { name: 'vanilla', color: yellow, variants: [ { name: 'vanilla', display: 'JavaScript', color: yellow }, { name: 'vanilla-ts', display: 'TypeScript', color: blue } ] }, { name: 'vue', color: green, variants: [ { name: 'vue', display: 'JavaScript', color: yellow }, { name: 'vue-ts', display: 'TypeScript', color: blue } ] }, { name: 'react', color: cyan, variants: [ { name: 'react', display: 'JavaScript', color: yellow }, { name: 'react-ts', display: 'TypeScript', color: blue } ] }, { name: 'preact', color: magenta, variants: [ { name: 'preact', display: 'JavaScript', color: yellow }, { name: 'preact-ts', display: 'TypeScript', color: blue } ] }, { name: 'lit', color: lightRed, variants: [ { name: 'lit', display: 'JavaScript', color: yellow }, { name: 'lit-ts', display: 'TypeScript', color: blue } ] }, { name: 'svelte', color: red, variants: [ { name: 'svelte', display: 'JavaScript', color: yellow }, { name: 'svelte-ts', display: 'TypeScript', color: blue } ] } ] const TEMPLATES = FRAMEWORKS.map( (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name] ).reduce((a, b) => a.concat(b), []) const renameFiles = { _gitignore: '.gitignore' } async function init() { let targetDir = argv._[0] let template = argv.template || argv.t const defaultProjectName = !targetDir ? 'vite-project' : targetDir let result = {} try { result = await prompts( [ { type: targetDir ? null : 'text', name: 'projectName', message: 'Project name:', initial: defaultProjectName, onState: (state) => (targetDir = state.value.trim() || defaultProjectName) }, { type: () => !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm', name: 'overwrite', message: () => (targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`) + ` is not empty. Remove existing files and continue?` }, { type: (_, { overwrite } = {}) => { if (overwrite === false) { throw new Error(red('✖') + ' Operation cancelled') } return null }, name: 'overwriteChecker' }, { type: () => (isValidPackageName(targetDir) ? null : 'text'), name: 'packageName', message: 'Package name:', initial: () => toValidPackageName(targetDir), validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name' }, { type: template && TEMPLATES.includes(template) ? null : 'select', name: 'framework', message: typeof template === 'string' && !TEMPLATES.includes(template) ? `"${template}" isn't a valid template. Please choose from below: ` : 'Select a framework:', initial: 0, choices: FRAMEWORKS.map((framework) => { const frameworkColor = framework.color return { title: frameworkColor(framework.name), value: framework } }) }, { type: (framework) => framework && framework.variants ? 'select' : null, name: 'variant', message: 'Select a variant:', // @ts-ignore choices: (framework) => framework.variants.map((variant) => { const variantColor = variant.color return { title: variantColor(variant.name), value: variant.name } }) } ], { onCancel: () => { throw new Error(red('✖') + ' Operation cancelled') } } ) } catch (cancelled) { console.log(cancelled.message) return } // user choice associated with prompts const { framework, overwrite, packageName, variant } = result const root = path.join(cwd, targetDir) if (overwrite) { emptyDir(root) } else if (!fs.existsSync(root)) { fs.mkdirSync(root) } // determine template template = variant || framework || template console.log(`\nScaffolding project in ${root}...`) const templateDir = path.join(__dirname, `template-${template}`) const write = (file, content) => { const targetPath = renameFiles[file] ? path.join(root, renameFiles[file]) : path.join(root, file) if (content) { fs.writeFileSync(targetPath, content) } else { copy(path.join(templateDir, file), targetPath) } } const files = fs.readdirSync(templateDir) for (const file of files.filter((f) => f !== 'package.json')) { write(file) } const pkg = require(path.join(templateDir, `package.json`)) pkg.name = packageName || targetDir write('package.json', JSON.stringify(pkg, null, 2)) const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent) const pkgManager = pkgInfo ? pkgInfo.name : 'npm' console.log(`\nDone. Now run:\n`) if (root !== cwd) { console.log(` cd ${path.relative(cwd, root)}`) } switch (pkgManager) { case 'yarn': console.log(' yarn') console.log(' yarn dev') break default: console.log(` ${pkgManager} install`) console.log(` ${pkgManager} run dev`) break } console.log() } function copy(src, dest) { const stat = fs.statSync(src) if (stat.isDirectory()) { copyDir(src, dest) } else { fs.copyFileSync(src, dest) } } function isValidPackageName(projectName) { return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test( projectName ) } function toValidPackageName(projectName) { return projectName .trim() .toLowerCase() .replace(/\s+/g, '-') .replace(/^[._]/, '') .replace(/[^a-z0-9-~]+/g, '-') } function copyDir(srcDir, destDir) { fs.mkdirSync(destDir, { recursive: true }) for (const file of fs.readdirSync(srcDir)) { const srcFile = path.resolve(srcDir, file) const destFile = path.resolve(destDir, file) copy(srcFile, destFile) } } function isEmpty(path) { return fs.readdirSync(path).length === 0 } function emptyDir(dir) { if (!fs.existsSync(dir)) { return } for (const file of fs.readdirSync(dir)) { const abs = path.resolve(dir, file) // baseline is Node 12 so can't use rmSync :( if (fs.lstatSync(abs).isDirectory()) { emptyDir(abs) fs.rmdirSync(abs) } else { fs.unlinkSync(abs) } } } /** * @param {string | undefined} userAgent process.env.npm_config_user_agent * @returns object | undefined */ function pkgFromUserAgent(userAgent) { if (!userAgent) return undefined const pkgSpec = userAgent.split(' ')[0] const pkgSpecArr = pkgSpec.split('/') return { name: pkgSpecArr[0], version: pkgSpecArr[1] } } init().catch((e) => { console.error(e) })
Seeing so much code above, don't you want to read on? Don't panic! In fact, we just use a few places inside. We can continue to read at ease.
These codes are the core code of Create Vite. We will see that the constant FRAMEWORKS defines an array object. In addition, the array objects are some frames that we need to choose to install when initializing the project. So, we can start with the ViteGithub project Clone and try the effect.
Then, after cloning the project, we find the folder / packages / create vite. We will only focus on this folder now.

I use the Yarn dependency management tool, so I first initialize the dependency with the command.
yarn

Then, we can first open package. In the root directory JSON file, you will find the following commands.
{ "bin": { "create-vite": "index.js", "cva": "index.js" } }
We can name our own template here. For example, we call it demo,
{ "bin": { "create-demo": "index.js", "cvd": "index.js" } }
Then, we first use the yarn link command here to make this command run locally.
Then run the create demo command.

Some interactive text will be displayed and you will find it very familiar, which is exactly what we saw when we created the Vite project. We said earlier that we want to implement our own project template, and now we have found the core. So start working!
We will see that there are many folders beginning with template - in the root directory. Let's open one and have a look. For example, template Vue.

The original templates are here! But these template files start with template -, is there any Convention? So we're going to look back at the index JS file.
// determine template template = variant || framework || template console.log(`\nScaffolding project in ${root}...`) const templateDir = path.join(__dirname, `template-${template}`)
If so, all templates must start with template -.
Then, we will create a template demo folder under the root directory and put an index JS file as a sample template.
When we execute the initialization project, we find that we need to select the corresponding template. Where do these options come from? We decided to go back and look at the index under the root directory JS file.

You will find such an array, which is the framework template we want to select.
const FRAMEWORKS = [ { name: 'vanilla', color: yellow, variants: [ { name: 'vanilla', display: 'JavaScript', color: yellow }, { name: 'vanilla-ts', display: 'TypeScript', color: blue } ] }, { name: 'vue', color: green, variants: [ { name: 'vue', display: 'JavaScript', color: yellow }, { name: 'vue-ts', display: 'TypeScript', color: blue } ] }, { name: 'react', color: cyan, variants: [ { name: 'react', display: 'JavaScript', color: yellow }, { name: 'react-ts', display: 'TypeScript', color: blue } ] }, { name: 'preact', color: magenta, variants: [ { name: 'preact', display: 'JavaScript', color: yellow }, { name: 'preact-ts', display: 'TypeScript', color: blue } ] }, { name: 'lit', color: lightRed, variants: [ { name: 'lit', display: 'JavaScript', color: yellow }, { name: 'lit-ts', display: 'TypeScript', color: blue } ] }, { name: 'svelte', color: red, variants: [ { name: 'svelte', display: 'JavaScript', color: yellow }, { name: 'svelte-ts', display: 'TypeScript', color: blue } ] } ]
Therefore, you can add another object after the array.
{ name: 'demo', color: red, variants: [ { name: 'demo', display: 'JavaScript', color: yellow } ] }
OK, you will find that I have a color attribute here, and an attribute value similar to the color value, which depends on the constant exported by kolorist. Kolorist is a small library that puts colors into standard input / standard output. We will see that the interactive text of those templates will display different colors, which is its credit.
const { yellow, green, cyan, blue, magenta, lightRed, red } = require('kolorist')
We have also added the template object to the array. Next, we execute the command to see the effect.

You will find an additional demo template, which is exactly what we want.
Let's continue.

We will see that the demo1 folder has been successfully created in the root directory, and it is the demo template we want.
The Error shown in the figure above is because I did not create a package on the demo template JSON file, so it can be ignored here. You can create a package in your own template JSON file.
Although we have successfully created our own template locally, we can only create it locally. In other words, if you change a computer, you can't execute the command to create a template.
Therefore, we need to find a way to publish to the cloud. Here we publish to NPM.
First, we create a new project directory, delete other templates, and only keep our own templates. In addition, delete other template objects in the array and keep your own template.
I take my own template create strve app as an example.

Then, we open package JSON file, you need to modify some information.
Take create strve app as an example:
{ "name": "create-strve-app", "version": "1.3.3", "license": "MIT", "author": "maomincoding", "bin": { "create-strve-app": "index.js", "cs-app": "index.js" }, "files": [ "index.js", "template-*" ], "main": "index.js", "private": false, "keywords": ["strve","strvejs","dom","mvvm","virtual dom","html","template","string","create-strve","create-strve-app"], "engines": { "node": ">=12.0.0" }, "repository": { "type": "git", "url": "git+https://github.com/maomincoding/create-strve-app.git" }, "bugs": { "url": "https://github.com/maomincoding/create-strve-app/issues" }, "homepage": "https://github.com/maomincoding/create-strve-app#readme", "dependencies": { "kolorist": "^1.5.0", "minimist": "^1.2.5", "prompts": "^2.4.2" } }
Note that before each publishing, the version field must be different from before, otherwise the publishing fails.
Finally, we run the following commands in turn.
- Switch to npm source
npm config set registry=https://registry.npmjs.org
- Log in to NPM (ignore this step if you are already logged in)
npm login
- Publish NPM
npm publish
We can log in to NPM( https://www.npmjs.com/ )
View has been published successfully!

Later, we can directly run the command to download the custom template. This is very useful when we reuse templates. It can not only improve efficiency, but also avoid making many unnecessary mistakes.
epilogue
Thank you for reading this article. I hope it can help you. If you have any questions during operation, you can leave me a message.
In addition, the Create Strve App in this example is a set of fast build strve Command line tool for JS project. If you are interested, you can visit the following address to view the source code:
https://github.com/maomincoding/create-strve-app
Stay up and fight for more than two months, strve JS ecology has been preliminarily completed. The following is strve JS latest document address, welcome to browse.
https://maomincoding.github.io/strvejs-doc/