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 });