• Column address: Front-end compilation and engineering
  • Series: Babel and all
  • Author: Miomio

background

Recently want to do a similar vUE – CLI vue serve function, can directly specify an entry file for rapid preview development requirements.

Vue serve MyComponent. Vue previews the component effect by typing vue serve MyComponent.

Function points:

  1. The ability to specify a portal, quickly launch a development server for preview, and have hot updates

  2. Some templates can be built in, such as support for built-in custom component libraries, so that different scenarios can use different templates to start

Initialize the project

mkdir code-start

cd code-start

npm init -y
Copy the code

Initialize the new directory, and then use typescript with TSUP here to develop our CLI tools.

// package.json
{
    / /... Omit some other parameters and dependency packages
    "scripts": {
        "dev": "tsup-node src/cli.ts --watch src"."build": "tsup-node src/cli.ts --format esm,cjs"
    },

    "devDependencies": {
        "tsup": "^ 4.14.0"}}Copy the code

After initializing and installing the required dependencies, we can use the dev command to develop the functionality we need.

Cli tool

Node.js provides process.argv to read command line arguments, and if the command line requires some complicated arguments, we need to parse them ourselves. However, there is already a library for parsing parameters in the community, so use Commander to handle this.

// src/cli.ts

#!/usr/bin/env node

import { program } from 'commander';

program
    .command('serve <entry>', {
        isDefault: true,
    })
    .option('-t, --template <value>'.'Specify the template to run'.'vue')
    .option('-p, --port <value>'.'Specify the port to run on'.'2333')
    .action(async (entry, options) => {
        run(options.template, entry, {
            port: options.port,
        });
    });

program.parse(process.argv);
Copy the code

#! On the first line The/usr/bin/env node, #! /usr/bin/env indicates where to find the parser, and node is the name of the parser (indicating that the file is run by Node.js).

Here we create a serve command that accepts the entry argument and uses it as the default command. It also supports passing -t, -p to specify the template to run and the port to run.

// package.json
{
    / /...
    "bin": {
        "code-start": "./dist/cli.js"}}Copy the code

Then add an entry to package.json so that we can simulate installing the local module using the NPM link command.

At this point, type code-start-h and our command line program will take effect.

code-start -h

Usage: cli <command> [options]

code start

Options:
  -V, --version            output the version number
  -h, --help               display help for command

Commands:
  serve [options] <entry>
  help [command]           display help for command
Copy the code

Development server

Vite was chosen here for the development server, which is obviously the best choice for a quick start.

For a basic vue2 initialization project, create a new vue2 directory in the outermost templates directory. The directory structure looks something like this:

# templates/vue2├─ SRC │ ├─ ├─ index.htmlCopy the code
// main.ts
import App from '~entry';
import Vue from 'vue';

new Vue({
    render: h= > h(App),
}).$mount('#app');
Copy the code

Then in main.ts we start with a ~entry placeholder, then replace it with the real entry.

// src/index.ts
import path from 'path';
import { createServer, InlineConfig } from 'vite';
import { findExisting } from './utils';
import entryPlugin from './plugins/entry';

/ * * * *@param {string} Template Specifies the template to use@param {string} Entry Entry file *@param {RunOptions} [options] Other configurations */
export async function run(
    template: string,
    entry: string, options? : { port? :number;
    },
) {
    const templateRoot = path.resolve(__dirname, `.. /templates/${template}`);

    // Check whether the template root directory has a vite configuration file
    let file = findExisting(templateRoot, ['vite.config.ts'.'vite.config.js']);

    const viteConfig: InlineConfig = {
        // Use the Vite configuration file if there is one in the template
        configFile: file ? path.join(templateRoot, `. /${file}`) : false.// Parse the entry plug-in for the previous' ~entry 'placeholder
        plugins: [entryPlugin(entry)],
        server: {
            host: true.port: options? .port ||2333,}};// Create a development server listener
    const server = await createServer(viteConfig);
    await server.listen();
}
Copy the code

Then create a viteDevServer listener with the parameters passed in from the command line.

// src/plugins/entry.ts
import type { Plugin } from 'vite';
import path from 'path';

const PLACE_HOLDER_ENTRY = '~entry';
export default function entryPlugin(entry: string) :Plugin {
    const context = process.cwd();
    const realEntry = path.resolve(context, entry);

    return {
        name: 'entryPlugin'.resolveId(id) {
            if(id ! == PLACE_HOLDER_ENTRY) {return;
            }

            returnrealEntry; }}; }Copy the code

In entryPlugin, treat the ~entry placeholder as the real entry we pass in from the command line. This step could have been handled directly with alias, but there are other things we need to do later, so we use the plug-in method to solve the problem.

So we have a simple handler, but some of the template dependencies we installed in our CLI program, such as the vue2 template above, will rely on some vue, vite-plugin-vue2, etc.

Depend on the processing

In the template directory above, add a package.json to declare the required dependency versions.

# templates/vue├─ SRC │ ├─ ├─ ├─ download.htmlCopy the code
program
    .command('serve <entry>', {
        isDefault: true,
    })
    .option('-t, --template <value>'.'Specify the template to run'.'sfv')
    .option('-p, --port <value>'.'Specify the port to run on'.'2333')
    .option('--choose-version'.'Select dependent Version')
    .action(async (entry, options) => {
        if (options.chooseVersion) {
            await chooseDepsVersion(options.template);
        }
        await installDeps(options.template);
        run(options.template, entry, {
            port: options.port,
        });
    });
Copy the code

Then add a configuration function for selecting a version dependency to the CLI program. If the –choose-version parameter is passed, an interactive operation is provided for the user to specify the version number of the installation dependency.

import { createHash } from 'crypto';
import shell from 'shelljs';
import inquirer from 'inquirer';
import semver from 'semver';
import fs from 'fs-extra';
import path from 'path';

// Get the hash value
export function getDepHash(root: string) :string {
    // lookupFile reads the contents of the file in turn
    let lock = lookupFile(root, ['package-lock.json'.'yarn.lock'.'pnpm-lock.yaml') | |' ';
    let pkg = lookupFile(root, ['package.json') | |' ';
    return createHash('sha256')
        .update(pkg + lock)
        .digest('hex')
        .substr(0.8);
}

// Install dependencies
export async function installDeps(template: string) {
    const root = path.resolve(__dirname, `.. /templates/${template}`);
    const metaPath = path.join(__dirname, '.. /_metadata.json');

    // Generate hash values from package.json and lock files
    const hash = getDepHash(root);
    let prevData: any = {};
    try {
        prevData = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) || {};
    } catch (e) {}

    // If the hash has changed, skip it
    if(prevData[template] ! == hash) {// Install dependencies through the shell
        const cmd = `yarn install --cwd ${root}`;
        shell.exec(cmd, {
            silent: true}); prevData[template] = hash;// Write files persistently for subsequent hash comparisonfs.writeJSONSync(metaPath, prevData); }}// Select the dependent version
export async function chooseDepsVersion(template: string) {
    const root = path.resolve(__dirname, `.. /templates/${template}`);
    const pkg = lookupFile(root, ['package.json') | |' ';
    if (pkg) {
        const pkgObj = JSON.parse(pkg) || {};
        const deps = Object.keys(pkgObj.dependencies || {});

        // Create an interactive program with inquirer
        // Specify the version of each dependency. The version number is checked by semver
        const questions = deps.map((dep): inquirer.Question => {
            const currentVersion = pkgObj.dependencies[dep];
            return {
                type: 'input'.name: dep,
                message: ` input${dep}Version number (Current version:${currentVersion}) : `.filter: input= > {
                    if(! input) {return input;
                    }

                    return semver.valid(input) || input;
                },
                validate: input= > {
                    if (input === ' ') {
                        return true;
                    }

                    return!!!!! semver.valid(input); }}; });const answers = await inquirer.prompt(questions);
        const writes: Record<string.string> = {};

        Object.keys(answers).forEach(dep= > {
            if(answers[dep]) { writes[dep] = answers[dep]; }});if (Object.keys(writes).length) {
            const targetFile = path.join(root, 'package.json');
            await fs.writeJson(
                path.join(root, 'package.json'),
                Object.assign(pkgObj, {
                    dependencies: {... pkgObj.dependencies, ... writes }, }), {spaces: 2,}); }}}Copy the code
  1. Select dependent versions to create a command line interaction with inquirer, enter the version number of each package, and write the input, if valid, to package.json.

  2. Dependency installation check Before running the template, check whether the dependency is installed properly and check whether the dependency is changed by comparing the old and new hash values of package.json and lock files. If the dependency is changed, run the YARN install command to install the dependency.

This completes some of the dependency versions of each template, such as specifying component library versions for your own custom templates, or wanting to build in some tool libraries, etc.

Extended multiple entry support

If you want to specify a directory in which all files can be accessed, or specify a remote address as an entry point, you can simply extend the entryPlugin to the entry point.

import type { Plugin } from 'vite';
import path from 'path';
import fs from 'fs-extra';
import glob from 'fast-glob';
import got from 'got';

const PLACE_HOLDER_ENTRY = '~entry';
const ROUTER_MOD = '__router_mod__';
const REMOTE_MOD = '__remote_mod__.vue';

function genRoutes(realEntry: string) {
    const pattern = `${realEntry.replace(/[\/\\]/g.'/').replace(/ / / $/.' ')}/**/*.vue`;
    const routes = await glob(pattern)
        .map(file= > {
            return `
            {
                path: '/${path.relative(realEntry, file).replace(/\.vue$/.' ')}',
                component: () => import('${file}` ')};
        })
        .join(', ');

    return `export default [
                ${routes}
            ]
        `;
}

interfacePluginOptions { pattern? :string;
}

export default function entryPlugin(entry: string, options? : PluginOptions) :Plugin {
    let realEntry = ' ';
    let isDirectory = false;
    let isRemoteEntry = isRemote(entry);

    if(! isRemoteEntry) {const context = process.cwd();
        realEntry = path.resolve(context, entry);
        isDirectory = fs.statSync(realEntry).isDirectory();
    }

    return {
        name: 'entryPlugin'.resolveId(id) {
            if(id ! == PLACE_HOLDER_ENTRY) {return;
            }

            If it is a remote address, return a placeholder mod
            if (isRemoteEntry) {
                return REMOTE_MOD;
            }

            // If it is a directory, use routing mode
            if (isDirectory) {
                return ROUTER_MOD;
            }

            return realEntry;
        },

        async load(id) {
            // In routing mode, scan all. Vue files in the directory to generate the exported routing table
            if (id === ROUTER_MOD) {
                return genRoutes(realEntry);
            }

            // For the remote address, fetch returns the text of the specific address
            if (id === REMOTE_MOD) {
                returngot(entry).text(); }}}; }Copy the code

If the entry is a remote address starting with HTTP or HTTPS, then the vite Plugin’s ability to retrieve the specific code returned.

If the entry is a directory, then scan all. Vue files in the directory to generate the exported routing table, and then make a judgment when processing main.ts in our template. If the return is an array, then use vue-router to initialize it.

// templates/vue2/src/main.ts
function initEntry() {
    import('~entry').then(async ({ default: mod }) => {
        const Vue = (await import('vue')).default;

        // If entry does not export an array, it is processed as an entry
        if (!Array.isArray(mod)) {
            new Vue({
                render: h= > h(mod),
            }).$mount('#app');
            return;
        }

        // If the export is an array, generate the route navigation
        const Router = (await import('vue-router')).default;
        const router = new Router({
            mode: 'hash'.routes: [
                ...mod,

                // We can simply generate a list page for navigation
                / / {
                // path: '*',
                // component: () => {},
                // },]}); Vue.use(Router);new Vue({
            render: h= > h('router-view'),
            router,
        }).$mount('#app');
    });
}
initEntry();
Copy the code

conclusion

At this point, our command-line tool is almost complete. As a front-end developer, you can use the powerful capabilities of Node.js to develop some applicable functions for your small needs. There are many useful command-line tools available in the community.

  • Chalk: beautify the command line module
  • Debug: indicates the log printing module
  • Shelljs: Runs the shell command
  • Inquirer: command line interaction
  • Semver: NPM semantic versioning
  • Commander: Command line tool
  • Fast-glob: a fast and efficient glob parsing library
  • Got: A library of HTTP requests