(Node tool enhancement) Generate specific components from template files

Introduction to the

background

Each time you need to create a Pages in a folder, create the Component, create the corresponding SCSS file, and copy the code to the Ant Design Pro Component if the required page is similar to the previous one. And then today when you’re doing requirements you want to reference a widget in your project that automatically generates components from a template

I feel that doing this kind of small tool is definitely more than doing demand cool ah ~ first talk about what this article can bring?

  • Node simple applications
  • The FS module reads and writes files
  • Command line query? Similar to vue-CLI
  • Using JS prototype inheritance to template file information encapsulation
  • Simple Buffer data acquisition and use

Take a look at the results

Ask which template to use

Has been created

Completed files and directories

This actually saves me from having to create a folder first, then create component files and style files, and I don’t have to go to the official UI library document to copy and paste code

The overall train of thought

The overall idea is very simple, as follows:

  1. Create several template files (commonly used in projects)
  2. Ask the user for the required component type => Component name => Component location
  3. The template file created in step (1) is available for user selection
  4. Create a file information constructor to hold user input and derive state
  5. Generate files based on stored status information
  6. Simple processing of template files, output user final files

Isn’t it easy to do all this in 100 lines of code

code

1. Create a template file

For example, when we write a React component, we often create a new folder in the Pages directory and create an index. JSX file as the entry of the module. We just extract the index. JSX file into a generic template file. JSX files and SCSS files are automatically created according to our configuration information by running only one command at a time

We’ll create a component-generate directory with index.js as our core code file and the rest as template files

<! -- component-generate folder -->.├ ─ ├─ ├─ ├─ ├.jsx // ├─ ├─ index.jsx // / ├─ index.jsx // //Copy the code

2. Use the CLI to interact with users

The first question to consider is, how do I make node.js CLI programs interactive? Yarn Generate, for example, later asks users to interact with them

As of version 7, Node.js provides the readline module

If you are interested, you can check it out, but I also recommend using inquirer, a third-party library

Example code is as follows:

const inquirer = require("inquirer");

var questions = [
  {
    type: "input".name: "name".message: "What's your name?",},]; inquirer.prompt(questions).then((answers) = > {
  console.log(Hi `${answers["name"]}! `);
});
Copy the code

Based on the Inquirer library, we create our own CLI interrogators

const inquirer = require("inquirer");
const GENERATE = {
  TYPE: "type".PATH: "path".FILE_NAME: "fileName"};const questions = [
  {
    type: "list".name: GENERATE.TYPE,
    message: "What components do you want to generate?".choices: getTypeList(), // Get the custom template in step 3
  },
  {
    type: "input".name: GENERATE.FILE_NAME,
    message: "Component name? JSX (default index. JSX)}, {type: "input".name: GENERATE.PATH,
    message: Where do you want your components to be generated (optional, in the component root by default)${DEFAULT_PATH}Under, the default stitching path)? `,},]; inquirer.prompt(questions).then(async (answers) => {
  // ...
  console.log(answers); // { type: '', path: '', fileName: '' }
});
Copy the code

3. Select the template based on the template file

We use Node’s FS module to traverse the current template folder, but exclude our core files, and consider everything else to be template files

const COMPONENT_PATH = "./component-generate"; // Our template folder path
function getTypeList() {
  const response = fs.readdirSync(COMPONENT_PATH);
  // Exclude the index.js core file
  const list = response.filter((item) = >item ! = ="index.js");
  // Remove all suffix names to display the template type
  return list.map((item) = > {
    return item.substring(0, item.lastIndexOf("."));
  });
}
Copy the code

Options automatically generated from the template

4. Create a file information constructor to save user input and derive state

Now that we have the user interaction data, it’s time to save it for later use, I don’t know why (maybe a refresher on prototyping?). , consciously use this way to do ~

// File information constructor
function FileInfo(info) {
  this.path = info.path || "/";
  this.type = info.type;
  this.fileName = info.fileName || "index.jsx";
}

// Get the concatenated PATH
FileInfo.prototype.getPath = function() {
  this.fileName =
    Array.prototype.shift.call(
      String.prototype.split.call(this.fileName, ".")) +".jsx";
  return `.${DEFAULT_PATH}The ${this.path}/The ${this.fileName}`;
};

// Get the corresponding code according to the template type selected by the user
FileInfo.prototype.getCodeByType = async function() {
  const response = await fs.readFileSync(`${COMPONENT_PATH}/The ${this.type}.jsx`, {
    encoding: "utf-8"});const fileContent = response.replace(
    "DEFAULT_NAME".// Specifies the default name of the upper-level directory
    this.fileName === "index.jsx"
      ? this.path.substring(1.this.path.length)
      : this.fileName.substring(0.this.fileName.lastIndexOf(".")));// eslint-disable-next-line no-undef
  const bufferBytes = Buffer.from(fileContent);
  return bufferBytes;
};

// Get the file name
FileInfo.prototype.getFileName = function() {
  return this.fileName;
};
Copy the code

5. Generate a file based on the saved status information

Our data is already stored in the FileInfo constructor, which generates files based on the user’s data

The idea is to know where the user needs to create the file and what the name of the file to create is.

One problem is that Node does not support creating a new file in an existing path. In this case, you have to create the corresponding path and then create the corresponding file

The core code is as follows:

function recursiveWriteFile(path, buffer, callback) {
  const lastPath = path.substring(0, path.lastIndexOf('/'))
  // Create a directory first
  fs.mkdir(lastPath, { recursive: true }, err= > {
    if (err) {
      return callback(err)
    }
    // Check whether the file exists. If the file does not exist, the cli prompts the user to replace it
    fs.access(path, fs.constants.F_OK, err= > {
      if (err) {
        // File does not exist, can be written according to the template content
        fs.writeFile(path, buffer, err= > {
          if (err) {
            return callback(err)
          }

          callback(undefined)})return
      }

      console.log('File already exists, please rename or delete file and try again')})})}Copy the code

So that’s how you create this recursively, and then you pass in parameters based on what the user typed in, right

inquirer.prompt(questions).then(async answers => {
  const fileInfo = new FileInfo(answers)
  const generatePath = fileInfo.getPath()
  const generateCode = await fileInfo.getCodeByType()

  recursiveWriteFile(generatePath, generateCode, err= > {
    if (err) {
      throw err
    }

    console.log('Component created successfully')

    recursiveWriteFile(generatePath.replace('.jsx'.'.scss'), ' '.err= > {
      if (err) {
        throw err
      }

      console.log('Component style file created successfully')})})})Copy the code

Now you’re done! Finally, a little bit about template handling

6. Simply process template files and output user final files

One small detail here is that if the user chooses the default index.jsx file name, I will take his parent directory name and implement the component naming convention

const fileContent = response.replace(
  'DEFAULT_NAME'.// Specifies the default name of the upper-level directory
  this.fileName === 'index.jsx'
    ? this.path.substring(1.this.path.length)
    : this.fileName.substring(0.this.fileName.lastIndexOf('. ')))Copy the code

Finally, there is the Buffer, which actually stores the content in base 2 and displays it in hexadecimal. I don’t know if I’m wrong. I started by getting the contents of the component directly, and then writing to it would get an error saying that I had to use either a string or a Buffer for writeFile

const bufferBytes = Buffer.from(fileContent)
Copy the code

The above is a small Demo that automatically generates corresponding components based on the template, and we will continue to improve it later. First, we will use the development requirements in this way

Requirements haven’t been written yet, this has been written all day ~ who makes programmers lazy? Dismissed!