preface

Why write this article? It’s because I’ve been working on the Strve.js ecosystem lately and have learned a lot while tinkering with the framework myself. This article introduces a more convenient and flexible command line scaffolding tool and how to publish it to NPM.

I’ve written about developing similar command-line tools before, but the idea is to remotely pull project template code from a Git repository. Sometimes pull fails due to network speed, which in turn causes initialization projects to fail.

So, is there a better solution? So here comes this piece.

Recently, many projects have been developed using Vite tools. You have to admire the amazing code ability, to create such a good development tool, development experience is very smooth. Especially when you’re initializing a project, you only need to execute a single command, and you don’t need to install any tools globally. Then, customize the template you need to initialize the project, and you’re done! This operation really surprised me! I wonder if it would be Nice if I applied the create-Vite approach to my own scaffolding tools!

In actual combat

So, without another word, hurry up and open ViteGitHub address.

https://github.com/vitejs
Copy the code

After searching for a long time, I finally found the core code of the command line tool.

https://github.com/vitejs/vite/tree/main/packages/create-vite
Copy the code

There are a lot of folders that start with template-. If YOU open a few of them, they are all framework project templates. So, you can put that aside.

Next, we’ll open the index.js file and see what’s in it. I’m going to show you the code, so you can look at it, but you don’t have to dig 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

  constdefaultProjectName = ! 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.The '-')
    .replace(/ / ^. [_].' ')
    .replace(/[^a-z0-9-~]+/g.The '-')}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)
})

Copy the code

Don’t you want to read any more after all this code? Don’t panic! We only use a few of them, so we can read on.

This code is the core of the Create Vite code, and we see that the constant FRAMEWORKS define an array object, and the array objects are some of the FRAMEWORKS we need to install when initializing the project. So, we can first ViteGithub Clone down, try the effect.

Then, after we Clone the project, we find the /packages/create-vite folder, which we will focus on for now.

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

yarn 
Copy the code

We can then open the package.json file in the root directory and find the following command.

{
  "bin": {
    "create-vite": "index.js"."cva": "index.js"}}Copy the code

We could call our own template here, let’s call it demo,

{
  "bin": {
    "create-demo": "index.js"."cvd": "index.js"}}Copy the code

We then use the YARN link command here first to make this command run locally.

Then run create-demo.

Some interactive text will display and you’ll find it very familiar, which is exactly what we saw when we created the Vite project. We said earlier that we wanted to implement a project template of our own, and now we’ve found the core. So get to work!

We’ll see a lot of template-starting folders in the root directory, so let’s open one and have a look. Such as the template – vue.

Here are the templates! But these template files start with template-. Is there any convention? So, we’re going to go back to the index.js file.

// determine template
template = variant || framework || template

console.log(`\nScaffolding project in ${root}. `)

const templateDir = path.join(__dirname, `template-${template}`)
Copy the code

Sure enough, all templates must start with template-.

So, we’ll create a template-demo folder under the root directory with an index.js file as the sample template.

When we initialize the project, we find that we need to select the corresponding template, so where do these options come from? We decided to go back to the index.js file in the root directory.

You’ll find an array that contains exactly the framework template you 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
      }
    ]
  }
]
Copy the code

So, you can add an object after the array.

{
    name: 'demo'.color: red,
    variants: [{name: 'demo'.display: 'JavaScript'.color: yellow
      }
    ]
}
Copy the code

Okay, you’ll notice that I’m going to have a color property here, and I’m going to have a property value that looks like a color value, which is a constant that kolorist exports. Kolorist is a small library that puts colors into standard input/standard output. The template interaction text that we saw in the previous section shows different colors because of this.

const {
  yellow,
  green,
  cyan,
  blue,
  magenta,
  lightRed,
  red
} = require('kolorist')
Copy the code

We’ve also added the template object to the array, so next we’ll run the command to see what happens.

You’ll find an extra demo template, which is exactly what you want.

We go ahead and execute.

We will see that the demo folder has been successfully created in the root directory, and it contains the demo template we want.

The Error shown above is because I did not create a package.json file on the demo template, so it can be ignored here. You can create a package.json file in your own template.

Although we successfully created one of our templates locally, we can only create it locally. That means you can’t execute the template creation command on a different computer.

So, we had to figure out how to publish to the cloud, and here we publish to NPM.

First, we create a new project directory, removing all the other templates and keeping only our own. In addition, delete the other template objects in the array and keep a template of your own.

I use my own template, create-Strve-app, as an example.

Next, we open the package.json file and 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}}Copy the code

Note that the Version field must be different before each release or the release will fail.

Finally, we run the following commands in sequence.

  1. Switch to the NPM source
npm config set registry=https://registry.npmjs.org
Copy the code
  1. Log in to NPM (skip this step if already logged in)
npm login
Copy the code
  1. Release NPM
npm publish
Copy the code

We can log on to NPM (www.npmjs.com/)

Check published successfully!

Later, we can run commands directly to download custom templates. This is very useful when we reuse templates, not only to increase efficiency, but also to avoid making many unnecessary mistakes.

conclusion

Thank you for reading this article. I hope it helps you. If you have any questions during operation, please leave a message to me.

In addition, the example created Strve App is a command line tool for quickly building strve.js projects. If you are interested, you can visit the following address to view the source code:

https://github.com/maomincoding/create-strve-app
Copy the code

After staying up for more than two months, strve.js ecology has been completed. The following is the latest document address of Strve.js, welcome to browse.

https://maomincoding.github.io/strvejs-doc/
Copy the code