Inside the Vue CLI Startup Process

From npm Script to CLI Execution

When the npm run dev command is executed, the npm CLI parses the package.json file located in the current working directory to find the corresponding script definition under the scripts field.

"scripts": {
  "start:dev": "vue-cli-service serve --mode dev",
  "build:prod": "vue-cli-service build --mode prod",
  "deploy": "rsync -av -e ssh ./dist/webapp admin@server:/var/www",
  "check": "vue-cli-service lint"
}

Consequently, running npm run dev translates to executing vue-cli-service serve --mode dev. The vue-cli-service executable is symlinked in the node_modules/.bin directory during the project setup phase.

Binary Resolution

The operating system invokes the appropriate binary wrapper, such as node_modules/.bin/vue-cli-service.cmd on Windows. This script resolves the Node.js executable path and delegates control to the main JavaScript entry point.

@ECHO off
SETLOCAL
SET "script_dir=%~dp0"
IF EXIST "%script_dir%\node.exe" (
    SET "node_exec=%script_dir%\node.exe"
) ELSE (
    SET "node_exec=node"
)
"%node_exec%" "%script_dir%\..\@vue\cli-service\bin\vue-cli-service.js" %*

This wrapper ensures that the script node_modules/@vue/cli-service/bin/vue-cli-service.js is executed using Node.

The CLI Entry Point

The primary JavaScript entry file validates the current Node.js version and instantiates the core Service class.

const { semver, logError } = require('@vue/cli-shared-utils');
const requiredNodeVersion = require('../package.json').engines.node;

if (!semver.satisfies(process.version, requiredNodeVersion, { includePrerelease: true })) {
  logError(`Node ${process.version} detected, but vue-cli-service requires Node ${requiredNodeVersion}. Please update Node.`);
  process.exit(1);
}

const AppService = require('../lib/Service');
const serviceInstance = new AppService(process.env.VUE_CLI_CONTEXT || process.cwd());

const cliArguments = process.argv.slice(2);
const parsedArgs = require('minimist')(cliArguments, {
  boolean: ['modern', 'report', 'inline-vue', 'watch', 'open', 'copy', 'https', 'verbose']
});

const targetCommand = parsedArgs._[0];

serviceInstance.execute(targetCommand, parsedArgs, cliArguments).catch(err => {
  logError(err);
  process.exit(1);
});

The script creates an AppService instance, parses command-line arguments using minimist, and triggers the execute method corresponding to the requested command.

The AppService Class

Initialization and Dependencies

const path = require('path');
const Config = require('webpack-chain');
const { merge } = require('webpack-merge');
const { warn, logError, isPlugin, sortPlugins } = require('@vue/cli-shared-utils');
const PluginAPI = require('./PluginAPI');
const { defaults } = require('./options');

class AppService {
  // ...
}

Constructor

constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
  process.VUE_CLI_SERVICE = this;
  this.isBootstrapped = false;
  this.context = context;
  this.chainModifiers = [];
  this.rawConfigModifiers = [];
  this.devServerHooks = [];
  this.commands = {};
  this.manifest = this.fetchProjectManifest(pkg);
  this.pluginList = this.assemblePlugins(plugins, useBuiltIn);
  this.skippedPlugins = new Set();
  this.environmentModes = this.pluginList.reduce((modes, { apply: { defaultModes } }) => {
    return Object.assign(modes, defaultModes);
  }, {});
}

fetchProjectManifest

This method retrieves the project configuration by reading package.json.

fetchProjectManifest (inlineManifest, context = this.context) {
  if (inlineManifest) return inlineManifest;
  const manifest = require('@vue/cli-shared-utils').resolvePkg(context);
  if (manifest.vuePlugins && manifest.vuePlugins.resolveFrom) {
    this.manifestContext = path.resolve(context, manifest.vuePlugins.resolveFrom);
    return this.fetchProjectManifest(null, this.manifestContext);
  }
  return manifest;
}

assemblePlugins

This method aggregates and orders internal built-in plugins, dependencies declared in package.json, and local custom plugins.

assemblePlugins (inlinePlugins, useBuiltIn) {
  const formatPluginEntry = (pluginId, absPath) => ({
    id: pluginId.replace(/^.\//, 'built-in:'),
    apply: require(absPath || pluginId)
  });

  const corePlugins = [
    './commands/serve',
    './commands/build',
    './commands/inspect',
    './commands/help',
    './config/base',
    './config/assets',
    './config/css',
    './config/prod',
    './config/app'
  ].map(formatPluginEntry);

  let finalPlugins;
  if (inlinePlugins) {
    finalPlugins = useBuiltIn !== false ? corePlugins.concat(inlinePlugins) : inlinePlugins;
  } else {
    const projectPlugins = Object.keys(this.manifest.devDependencies || {})
      .concat(Object.keys(this.manifest.dependencies || {}))
      .filter(isPlugin)
      .map(pluginId => formatPluginEntry(pluginId, require.resolve(pluginId)));
    finalPlugins = corePlugins.concat(projectPlugins);
  }

  if (this.manifest.vuePlugins && this.manifest.vuePlugins.service) {
    const customFiles = this.manifest.vuePlugins.service;
    finalPlugins = finalPlugins.concat(customFiles.map(f => ({
      id: `local:${f}`,
      apply: require(`./${f}`)
    })));
  }

  return sortPlugins(finalPlugins);
}

execute

Resolves the execution mode, triggers the bootstrap sequence, and runs the matched command.

async execute (name, args = {}, rawArgs = []) {
  const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.environmentModes[name]);
  this.setSkippedPlugins(args, rawArgs);
  await this.bootstrap(mode);

  args._ = args._ || [];
  let targetCmd = this.commands[name];
  if (!targetCmd && name) {
    logError(`Command "${name}" is not registered.`);
    process.exit(1);
  }
  if (!targetCmd || args.help || args.h) {
    targetCmd = this.commands.help;
  } else {
    args._.shift();
    rawArgs.shift();
  }
  return targetCmd.fn(args, rawArgs);
}

bootstrap

The bootstrap mechanism loads environment variables, processes vue.config.js, applies all plugin logic, and registers Webpack configuration modifiers.

bootstrap (mode = process.env.VUE_CLI_MODE) {
  if (this.isBootstrapped) return;
  this.isBootstrapped = true;
  this.mode = mode;

  if (mode) this.parseEnvFiles(mode);
  this.parseEnvFiles();

  const userConfig = this.loadUserConfig();
  const applyConfig = (resolvedConfig) => {
    this.mergedOptions = require('lodash.defaultsdeep')(resolvedConfig, defaults());

    this.pluginList.forEach(({ id, apply }) => {
      if (this.skippedPlugins.has(id)) return;
      apply(new PluginAPI(id, this), this.mergedOptions);
    });

    if (this.mergedOptions.chainWebpack) this.chainModifiers.push(this.mergedOptions.chainWebpack);
    if (this.mergedOptions.configureWebpack) this.rawConfigModifiers.push(this.mergedOptions.configureWebpack);
  };

  return userConfig && typeof userConfig.then === 'function'
    ? userConfig.then(applyConfig)
    : applyConfig(userConfig);
}

parseEnvFiles

Reads .env and .env.[mode] files, populating process.env and configuring NODE_ENV alongside BABEL_ENV.

parseEnvFiles (mode) {
  const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`);
  const localPath = `${basePath}.local`;

  const load = envPath => {
    try {
      const envConfig = require('dotenv').config({ path: envPath, debug: process.env.DEBUG });
      require('dotenv-expand')(envConfig);
    } catch (err) {
      if (err.toString().indexOf('ENOENT') < 0) logError(err);
    }
  };

  load(localPath);
  load(basePath);

  if (mode) {
    const defaultNodeEnv = (mode === 'production' || mode === 'test') ? mode : 'development';
    if (process.env.NODE_ENV == null) process.env.NODE_ENV = defaultNodeEnv;
    if (process.env.BABEL_ENV == null) process.env.BABEL_ENV = defaultNodeEnv;
  }
}

The PluginAPI Bridge

The PluginAPI class acts as an interface provided to each plugin. It allows plugins to interact with the AppService instance to register new commands, inject Webpack chain configurations, or modify the dev server setup.

The Serve Command Implementation

The built-in serve plugin registers the development server command through the PluginAPI.

module.exports = (api, options) => {
  api.registerCommand('serve', {
    description: 'Start the local development server',
    usage: 'vue-cli-service serve [options] [entry]',
    options: {
      '--open': 'Launch browser on startup',
      '--port': 'Specify port (default: 8080)',
      '--mode': 'Specify environment mode (default: development)'
    }
  }, async function startServer (args) {
    // ...
  })
}

Internally, the startServer handler compiles the project using Webpack and spins up a webpack-dev-server instance to serve the built assets with hot module replacement.

const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');

const compiler = webpack(webpackConfig);
const devServer = new WebpackDevServer(serverConfig, compiler);
devServer.start().catch(err => { throw err });

Tags: Vue CLI webpack Node.js Frontend Tooling javascript

Posted on Thu, 21 May 2026 17:19:23 +0000 by faswad