脚手架
命令
vue-cli2
vue init webpack hello
vue-cli4(5)
vue create hello-world
cra
npx create-react-app my-app
vite
yarn create vite
umi
yarn create umi appName
umi
mkdir myapp && cd myapp npx @umijs/create-umi-app
2. vue-cli2
2.1 初始化项目
npm install -g vue-cli@2 --force
vue init webpack hello-vue-cli2
2.2 基本流程
downloading template...
? Project name
vue-cli · Generated "hello-vue-cli2" .
Installing project dependencies ...
2.3 基本思路
2.4 简单实现
mkdir 1. vue-cli2 && cd 1. vue-cli2
npm init -y
npm install commander chalk ora inquirer download-git-repo metalsmith handlebars consolidate async minimatch
const download = require ('download-git-repo' )
const program = require ('commander' )
const ora = require ('ora' )
const inquirer = require ('inquirer' )
const chalk = require ('chalk' )
const Metalsmith = require ('metalsmith' )
const Handlebars = require ('handlebars' )
const render = require ('consolidate' ).handlebars .render
const async = require ('async' )
const match = require ('minimatch' )
"bin" : {
"vue-init" : "bin/vue-init" ,
vue-init.js
program
.usage ('<template-name> [project-name]' )
program.parse (process.argv )
const template = program.args [0 ]
const rawName = program.args [1 ]
const tmp = path.join (home, '.vue-templates' , template.replace (/[\/:]/g , '-' ))
const spinner = ora ('downloading template' )
spinner.start ()
const officialTemplate = 'vuejs-templates/' + template
download (officialTemplate, tmp, { clone }, err => {
spinner.stop ()
generate (rawName, tmp, to, err => {
logger.success ('Generated "%s".' , name)
function generate (name, src, dest, done ) {
const js = path.join (src, "meta.js" );
const req = require (path.resolve (js));
const opts = req;
const metalsmith = Metalsmith (path.join (src, "template" ));
const data = Object .assign (metalsmith.metadata (), {
destDirName : name,
inPlace : dest === process.cwd (),
noEscape : true ,
metalsmith.use (askQuestions (opts.prompts ))
.use (filterFiles (opts.filters ))
metalsmith
.clean (false )
.source ("." )
.destination (dest)
.build ((err, files ) => {
done (err);
if (typeof opts.complete === 'function' ) {
opts.complete (data, {chalk})
return data;
metalsmith
.use ((files, metalsmith, done ) => {
async .eachSeries (
Object .keys (opts.prompts ),
(key, next ) => {
prompt (metalsmith.metadata (), key, opts.prompts [key], next);
metalsmith.use ((files, metalsmith, done ) => {
const fileNames = Object .keys (files);
Object .keys (opts.filters ).forEach ((glob ) => {
fileNames.forEach ((file ) => {
if (match (file, glob, { dot : true })) {
const condition = opts.filters [glob];
const fn = new Function (
"data" ,
"with (data) { return " + condition + "}"
if (!fn (metalsmith.metadata ())) {
delete files[file];
done ();
meta.js
module .exports = {
metalsmith : {
before : addTestAnswers
prompts : {},
filters : {}
complete : function (data, { chalk } ) {
const green = chalk.green
sortDependencies (data, green)
const cwd = path.join (process.cwd (), data.destDirName )
installDependencies (cwd, data.autoInstall , green)
.then (() => {
return runLintFix (cwd, data, green)
.then (() => {
printMessage (data, green)
.catch (e => {
console .log (chalk.red ('Error:' ), e)
if (exists (templatePath)) {
generate ()
5. gitee
`` `js
// 如果我们需要自己下载git仓库的模板 gitee的api文档
const gitUrl = ` https :
npm install zdex-downloadgitrepo
2.5 npm包发布
npm link
ibox-vue-init webpack hello
npm login (npm whoami)
npm publish
npm publish --access public
3. vite
3.1 初始化项目
yarn create vite
3.2 基本流程
3.3 基本思路
template-vue-ts
template-vue
3.4 简单实现
初始化项目
npm init -y
"bin" : {
"create-box-vite" : "index.js" ,
"ibox-cva" : "index.js"
npm install minimist prompts kolorist
const argv = require ("minimist" )(process.argv .slice (2 ));
const prompts = require ("prompts" );
const { blue } = require ("kolorist" );
index.js
const FRAMEWORKS = [
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
const TEMPLATES = FRAMEWORKS .map (
(f ) => (f.variants && f.variants .map ((v ) => v.name )) || [f.name ]
).reduce ((a, b ) => a.concat (b), [])
async function init () {
let result = await prompts ([])
const { framework, packageName, variant } = result
template = variant || framework || template
const templateDir = path.join (__dirname, `template-${template} ` )
const files = fs.readdirSync (templateDir)
for (const file of files.filter ((f ) => f !== 'package.json' )) {
write (file)
template文件
3.5 npm包发布
4. cra
4.1 初始化项目
npx create-react-app react-demo
4.2 基本流程
Creating a new React app in /Users /liu/my-app.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...
yarn add v1.22 .10
[1 /4 ] 🔍 Resolving packages...
[2 /4 ] 🚚 Fetching packages...
[3 /4 ] 🔗 Linking dependencies...
[4 /4 ] 🔨 Building fresh packages...
success Saved lockfile.
├─ cra-template@1.1 .2
├─ react-dom@17.0 .2
├─ react-scripts@4.0 .3
└─ react@17.0 .2
info All dependencies
├─ cra-template@1.1 .2
├─ immer@8.0 .1
├─ react-dev-utils@11.0 .4
├─ react-dom@17.0 .2
├─ react-scripts@4.0 .3
├─ react@17.0 .2
└─ scheduler@0.20 .2
✨ Done in 20. 65s.
Initialized a git repository.
Installing template dependencies using yarnpkg...
yarn add v1.22 .10
[1 /4 ] 🔍 Resolving packages...
[2 /4 ] 🚚 Fetching packages...
[3 /4 ] 🔗 Linking dependencies...
[4 /4 ] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 17 new dependencies.
Removing template package using yarnpkg...
yarn remove v1.22 .10
[1 /2 ] 🗑 Removing module cra-template...
[2 /2 ] 🔨 Regenerating lockfile and installing missing dependencies...
success Uninstalled packages.
✨ Done in 7. 42s.
Created git commit.
Success ! Created my-app at /Users /liu/my-app
Inside that directory, you can run several commands :
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
We suggest that you begin by typing :
cd my-app
yarn start
Happy hacking!
4.3 实现基本思路
4.4 简单实现
项目初始化
lerna init
lerna create @i-box/create-ibox-react-app
lerna create @i-box/ibox-cra-template
lerna create @i-box/ibox-react-scripts
"workspaces" : [
"packages/*"
cra源码调试
"version" : "0.2.0" ,
"configurations" : [
"name" : "Launch via NPM" ,
"request" : "launch" ,
"runtimeArgs" : ["run-script" , "create" ],
"runtimeExecutable" : "npm" ,
"skipFiles" : ["<node_internals>/**" ],
"type" : "pwa-node"
"create" : "node ./packages/create-react-app/index.js hello-world" ,
create-ibox-react-app
cra-template
template目录和template.json 文件
"files" : [
"template" ,
"template.json"
react-script
4.5 create-ibox-react-app
项目初始化
"bin" : {
"ibox-react-app" : "./index.js" ,
"ibox-cra" : "./index.js"
yarn workspace @i-box/create-ibox-react-app add chalk commander fs-extra cross-spawn
index.js
#!/usr/bin/env node
const { init } = require ("./createReactApp" );
init ();
createReactApp
function init () {
const program = new commander.Command (packageJson.name )
.version (packageJson.version )
.arguments ("<project-directory>" )
.usage (`${chalk.green("<project-directory>" )} [options]` )
.action ((name ) => {
projectName = name;
.parse (process.argv );
createApp (
projectName,
program.verbose ,
program.scriptsVersion ,
program.template ,
program.useNpm ,
program.usePnp
createApp
async function createApp (name, verbose, version, template, useNpm, usePnp ) {
const root = path.resolve (name);
const appName = path.basename (root);
fs.ensureDirSync (name);
console .log (`Creating a new React app in ${chalk.green(root)} .` );
const packageJson = {
name : appName,
version : '0.1.0' ,
private : true ,
fs.writeFileSync (
path.join (root, 'package.json' ),
JSON .stringify (packageJson, null , 2 ) + os.EOL
const originalDirectory = process.cwd ();
process.chdir (root);
await run (root, appName, originalDirectory);
async function run (root, appName, originalDirectory ) {
const scriptName = "react-scripts" ;
const templateName = "cra-template" ;
const allDependencies = ["react" , "react-dom" , scriptName, templateName];
console .log ("Installing packages. This might take a couple of minutes." );
console .log (
`Installing ${chalk.cyan("react" )} , ${chalk.cyan(
"react-dom"
)} , and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)} `
await install (root, allDependencies);
let data = [root, appName, true , originalDirectory, templateName];
let source = `
var init = require('react-scripts/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
await executeNodeScript ({ cwd : process.cwd () }, data, source);
process.exit (0 );
async function executeNodeScript ({ cwd }, data, source ) {
return new Promise ((resolve ) => {
const child = spawn (
process.execPath ,
["-e" , source, "--" , JSON .stringify (data)],
{ cwd, stdio : "inherit" }
child.on ("close" , resolve);
install
function install (root, allDependencies ) {
return new Promise ((resolve ) => {
const command = "yarnpkg" ;
const args = ["add" , "--exact" , ...allDependencies, "--cwd" , root];
const child = spawn (command, args, { stdio : "inherit" });
child.on ("close" , resolve);
4.6 react-scripts
初始化项目
yarn workspace @i-box/ibox-react-scripts add react react-dom
yarn workspace @i-box/ibox-react-scripts add cross-spawn fs-extra chalk webpack webpack-dev-server babel-loader babel-preset-react-app html-webpack-plugin open
"bin" : {
"rs" : "./bin/react-scripts.js"
"files" : [
"bin" ,
"scripts"
"scripts" : {
"build" : "node ./bin/react-scripts build" ,
"start" : "node ./bin/react-scripts start"
scripts中的init.js方法
module .exports = function (
appPath,
appName,
verbose,
originalDirectory,
templateName
const appPackage = require (path.join (appPath, "package.json" ));
const templatePath = path.dirname (
require .resolve (`${templateName} /package.json` , { paths : [appPath] })
appPackage.scripts = Object .assign ({
start : "react-scripts start" ,
build : "react-scripts build" ,
fs.writeFileSync (
path.join (appPath, "package.json" ),
JSON .stringify (appPackage, null , 2 ) + os.EOL
const templateDir = path.join (templatePath, "template" );
fs.copySync (templateDir, appPath);
const data = fs.readFileSync (path.join (appPath, "gitignore" ));
fs.appendFileSync (path.join (appPath, ".gitignore" ), data);
fs.unlinkSync (path.join (appPath, "gitignore" ));
execSync ("git init" , { stdio : "ignore" });
console .log ("Initialized a git repository." );
let command = "yarnpkg" ,
remove = "remove" ,
args = ["add" ];
console .log (`Installing template dependencies using ${command} ...` );
const proc = spawn.sync (command, args, { stdio : "inherit" });
console .log (`Removing template package using ${command} ...` );
const proc1 = spawn.sync (command, [remove, templateName], {
stdio : "inherit" ,
execSync ("git add -A" , { stdio : "ignore" });
execSync ('git commit -m "Initialize project using Create React App"' , {
stdio : "ignore" ,
console .log (`Success! Created ${appName} at ${appPath} ` );
4.7 发布
lerna publish
5. vue-cli4
1. 初始化项目
vue create hello-world
2. 基本流程
? Please pick a preset : Manually select features
? Check the features needed for your project :
Choose Vue version, Babel , Router , Vuex , Linter
? Choose a version of Vue .js that you want to start the project with 3. x
✨ Creating project in /Users /xueshuai.liu /hello-world.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while ...
🚀 Invoking generators...
📦 Installing additional dependencies...
⚓ Running completion hooks...
🚀 Generating README .md ...
🎉 Successfully created project hello-world.
👉 Get started with the following commands :
3. 简单实现
初始化项目
lerna init
"workspaces" : ["packages/*" ]
"useWorkspaces" : true ,
lerna create @i-box/cli
lerna create @i-box/cli-service
lerna create @i-box/cli-shared-utils
lerna create @i-box/cli-plugin-vuex
yarn 用来处理依赖
lerna用来初始化和发布
3.1 cli
cd packages/cli
"bin" : {
"ibox-vue" : "bin/vue.js"
yarn workspace @i-box/cli add minimist commander fs-extra inquirer isbinaryfile ejs vue-codemod
1. vue.js
program.command ("create <app-name>" ).action ((name, options ) => {
require ("../lib/create.js" )(name, options);
program.parse (process.argv );
2. create.js
async function create (projectName, options ) {
const cwd = options.cwd || process.cwd ()
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative ('../' , cwd) : projectName
const targetDir = path.resolve (cwd, projectName || '.' )
let promptModules = getPromptModules ();
const creator = new Creator (name, targetDir, promptModules)
await creator.create (options)
2.1 getPromptModules
const getPromptModules = () => {
return [
'vueVersion' ,
'babel' ,
'typescript' ,
'pwa' ,
'router' ,
'vuex' ,
'cssPreprocessors' ,
'linter' ,
'unit' ,
'e2e'
].map (file => require (`../promptModules/${file} ` ))
3. Creator
初始化 new Creator
const isManualMode = answers => answers.preset === '__manual__'
module .exports = class Creator extends EventEmitter {
constructor (name, context, prompModules ) {
super ()
this .name = name
this .context = process.env .VUE_CLI_CONTEXT = context
const { presetPrompt, featurePrompt } = this .resolveIntroPrompts ()
this .outroPrompts = this .resolveOutroPrompts ()
this .presetPrompt = presetPrompt
this .featurePrompt = featurePrompt
this .injectedPrompts = []
this .promptCompleteCbs = []
const promptAPI = new PromptModuleAPI (this )
promptModules.forEach ((m ) => m (promptAPI));
async create (cliOptions = {}, preset = null ) {}
resolveIntroPrompts 获取预设和特性
function resolveIntroPrompts () {
const defaultPreset = {
useConfigFiles : false ,
cssPreprocessor : undefined ,
plugins : {
'@vue/cli-plugin-babel' : {},
'@vue/cli-plugin-eslint' : {
config : 'base' ,
lintOn : ['save' ]
const presets = {
'default' : Object .assign ({ vueVersion : '2' }, defaultPreset),
'__default_vue_3__' : Object .assign ({ vueVersion : '3' }, defaultPreset)
const presetChoices = Object .entries (presets).map (([name, preset] ) => {
let displayName = name
if (name === 'default' ) {
displayName = 'Default'
} else if (name === '__default_vue_3__' ) {
displayName = 'Default (Vue 3)'
return {
name : `${displayName} ` ,
value : name
const presetPrompt = {
name : 'preset' ,
type : 'list' ,
message : `Please pick a preset:` ,
choices : [
...presetChoices,
name : 'Manually select features' ,
value : '__manual__'
const featurePrompt = {
name : 'features' ,
when : isManualMode,
type : 'checkbox' ,
message : 'Check the features needed for your project:' ,
choices : [],
pageSize : 10
return {
presetPrompt,
featurePrompt
resolveOutroPrompts
function resolveOutroPrompts () {
return [
name : 'useConfigFiles' ,
when : isManualMode,
type : 'list' ,
message : 'Where do you prefer placing config for Babel, ESLint, etc.?' ,
choices : [
name : 'In dedicated config files' ,
value : 'files'
name : 'In package.json' ,
value : 'pkg'
name : 'save' ,
when : isManualMode,
type : 'confirm' ,
message : 'Save this as a preset for future projects?' ,
default : false
name : 'saveName' ,
when : answers => answers.save ,
type : 'input' ,
message : 'Save preset as:'
PromptModuleAPI
class PromptModuleAPI {
constructor (creator ) {
this .creator = creator
injectFeature (feature) {
this .creator .featurePrompt .choices .push (feature)
injectPrompt (prompt) {
this .creator .injectedPrompts .push (prompt)
injectOptionForPrompt (name, option) {
this .creator .injectedPrompts .find (f => {
return f.name === name
}).choices .push (option)
onPromptComplete (cb) {
this .creator .promptCompleteCbs .push (cb)
4. create
async create (cliOptions = {}, preset = null ) {
let preset = await this .promptAndResolvePreset ();
preset.plugins ["@vue/cli-service" ] = Object .assign (
projectName : name,
preset
log (`✨ Creating project in ${chalk.yellow(context)} .` );
const pkg = {
name,
version : "0.1.0" ,
private : true ,
devDependencies : {},
const deps = Object .keys (preset.plugins );
deps.forEach ((dep ) => {
pkg.devDependencies [dep] = "latest" ;
await writeFileTree (context, {
"package.json" : JSON .stringify (pkg, null , 2 ),
log (`🗃 Initializing git repository...` );
await this .run ("git init" );
log (`⚙\u{fe0f} Installing CLI plugins. This might take a while...` );
await run ("npm install" );
log (`🚀 Invoking generators...` );
const plugins = await this .resolvePlugins (preset.plugins , pkg);
const generator = new Generator (context, {
plugins,
afterInvokeCbs,
afterAnyInvokeCbs,
await generator.generate ({
extractConfigFiles : preset.useConfigFiles ,
await writeFileTree (context, { "README.md" : "README.md" });
await run ("git add -A" );
log (`🎉 Successfully created project ${chalk.yellow(name)} .` );
promptAndResolvePreset
function promptAndResolvePreset (answers = null ) {
if (!answers) {
this .injectedPrompts .forEach (prompt => {
const originalWhen = prompt.when || (() => true )
prompt.when = answers => {
return isManualMode (answers) && originalWhen (answers)
let prompts = [
this .presetPrompt ,
this .featurePrompt ,
...this .injectedPrompts ,
...this .outroPrompts
answers = await inquirer.prompt (prompts)
let preset
if (answers.preset && answers.preset !== '__manual__' ) {
preset = defaults.presets
} else {
preset = {
useConfigFiles : answers.useConfigFiles === 'files' ,
plugins : {}
answers.features = answers.features || []
this .promptCompleteCbs .forEach (cb => cb (answers, preset))
return preset
resolvePlugins
async resolvePlugins (rawPlugins, pkg ) {
rawPlugins = sortObject (rawPlugins, ['@vue/cli-service' ], true )
const plugins = []
for (const id of Object .keys (rawPlugins)) {
const apply = loadModule (`${id} /generator` , this .context ) || (() => {})
let options = rawPlugins[id] || {}
if (options.prompts ) {
let pluginPrompts = loadModule (`${id} /prompts` , this .context )
if (pluginPrompts) {
const prompt = inquirer.createPromptModule ()
if (typeof pluginPrompts === 'function' ) {
pluginPrompts = pluginPrompts (pkg, prompt)
if (typeof pluginPrompts.getPrompts === 'function' ) {
pluginPrompts = pluginPrompts.getPrompts (pkg, prompt)
log ()
log (`${chalk.cyan(options._isPreset ? `Preset options:` : id)} ` )
options = await prompt (pluginPrompts)
plugins.push ({ id, apply, options })
return plugins
5. Generator
class Generator {
constructor (context, { pkg = {}, plugins = [], files = {} } = {} ) {
this .pkg = pkg
this .files = {}
this .fileMiddlewares = [];
this .allPlugins = this .resolveAllPlugins ()
const cliService = plugins.find ((p ) => p.id === "@vue/cli-service" );
this .rootOptions = rootOptions;
resolveAllPlugins
function resolveAllPlugins () {
const allPlugins = [];
Object .keys (this .pkg .dependencies || {})
.concat (Object .keys (this .pkg .devDependencies || {}))
.forEach ((id ) => {
if (!isPlugin (id)) return ;
const pluginGenerator = loadModule (`${id} /generator` , this .context );
if (!pluginGenerator) return ;
allPlugins.push ({ id, apply : pluginGenerator });
return sortPlugins (allPlugins);
generate方法
async generate ({ extractConfigFiles = false , checkExisting = false } = {}) {
await this .initPlugins ()
const initialFiles = Object .assign ({}, this .files )
await this .resolveFiles ()
this .files ['package.json' ] = JSON .stringify (this .pkg , null , 2 ) + '\n'
await writeFileTree (this .context , this .files , initialFiles, this .filesModifyRecord )
initPlugins
function initPlugins () {
const { rootOptions } = this
const pluginIds = this .plugins .map (p => p.id )
for (const plugin of this .plugins ) {
const { id, apply } = plugin;
const api = new GeneratorAPI (id, this , {}, rootOptions);
await apply (api, options, rootOptions, invoking)
GeneratorAPI
class GeneratorAPI {
constructor (id, generator, options, rootOptions ) {
this .id = id;
this .generator = generator;
this .options = options;
this .rootOptions = rootOptions;
this .pluginsData = generator.plugins
.filter (({ id } ) => id !== `@vue/cli-service` )
.map (({ id } ) => ({
name : toShortPluginId (id),
this ._entryFile = undefined ;
get entryFile () {
if (this ._entryFile ) return this ._entryFile ;
const file = fs.existsSync (this .resolve ("src/main.ts" ))
? "src/main.ts"
: "src/main.js" ;
return (this ._entryFile = file);
extendPackage (fields ) {
const pkg = this .generator .pkg ;
const toMerge = isFunction (fields) ? fields (pkg) : fields;
for (const key in toMerge) {
const value = toMerge[key];
const existing = pkg[key];
isObject (value) &&
(key === "dependencies" || key === "devDependencies" )
pkg[key] = mergeDeps (existing || {}, value);
} else {
pkg[key] = value;
render (source, additionalData = {} ) {
const baseDir = extractCallDir ();
if (isString (source)) {
source = path.resolve (baseDir, source);
this ._injectFileMiddleware (async (files) => {
const data = this ._resolveData (additionalData);
const globby = require ("globby" );
const _files = await globby (["**/*" ], { cwd : source, dot : true });
for (const rawPath of _files) {
const targetPath = rawPath
.split ("/" )
.map ((filename ) => {
if (filename.charAt (0 ) === "_" && filename.charAt (1 ) !== "_" ) {
return `.${filename.slice(1 )} ` ;
return filename;
.join ("/" );
const sourcePath = path.resolve (source, rawPath);
const content = renderFile (sourcePath, data);
if (Buffer .isBuffer (content) || /[^\s]/ .test (content)) {
files[targetPath] = content;
} else if (isObject (source)) {
} else if (isFunction (source)) {
this ._injectFileMiddleware (source);
_injectFileMiddleware (middleware ) {
this .generator .fileMiddlewares .push (middleware);
hasPlugin (id ) {
return this .generator .hasPlugin (id);
_resolveData (additionalData ) {
return Object .assign (
options : this .options ,
rootOptions : this .rootOptions ,
plugins : this .pluginsData ,
additionalData
injectImports (file, imports ) {
const _imports =
this .generator .imports [file] ||
(this .generator .imports [file] = new Set ());
(Array .isArray (imports) ? imports : [imports]).forEach ((imp ) => {
_imports.add (imp);
transformScript (file, codemod, options ) {
const normalizedPath = this ._normalizePath (file);
this ._injectFileMiddleware ((files ) => {
if (typeof files[normalizedPath] === "undefined" ) {
error (`Cannot find file ${normalizedPath} ` );
return ;
files[normalizedPath] = runTransformation (
path : this .resolve (normalizedPath),
source : files[normalizedPath],
codemod,
options
injectRootOptions () {
const _options =
this .generator .rootOptions [file] ||
(this .generator .rootOptions [file] = new Set ());
(Array .isArray (options) ? options : [options]).forEach ((opt ) => {
_options.add (opt);
postProcessFiles (cb ) {
this .generator .postProcessFilesCbs .push (cb);
_normalizePath (p ) {
if (path.isAbsolute (p)) {
p = path.relative (this .generator .context , p);
return p.replace (/\\/g , "/" );
resolve (..._paths ) {
return path.resolve (this .generator .context , ..._paths);
resolveFiles
async resolveFiles () {
const files = this .files
for (const middleware of this .fileMiddlewares ) {
await middleware (files, ejs.render )
Object .keys (files).forEach (file => {
let imports = this .imports [file]
imports = imports instanceof Set ? Array .from (imports) : imports
if (imports && imports.length > 0 ) {
files[file] = runTransformation (
{ path : file, source : files[file] },
require ('./util/codemods/injectImports' ),
{ imports }
let injections = this .rootOptions [file]
injections = injections instanceof Set ? Array .from (injections) : injections
if (injections && injections.length > 0 ) {
files[file] = runTransformation (
{ path : file, source : files[file] },
require ('./util/codemods/injectOptions' ),
{ injections }
injectImports
function injectImports (fileInfo, api, { imports }) {
const j = api.jscodeshift
const root = j (fileInfo.source )
const toImportAST = i => j (`${i} \n` ).nodes ()[0 ].program .body [0 ]
const toImportHash = node => JSON .stringify ({
specifiers : node.specifiers .map (s => s.local .name ),
source : node.source .raw
const declarations = root.find (j.ImportDeclaration )
const importSet = new Set (declarations.nodes ().map (toImportHash))
const nonDuplicates = node => !importSet.has (toImportHash (node))
const importASTNodes = imports.map (toImportAST).filter (nonDuplicates)
if (declarations.length ) {
declarations
.at (-1 )
.forEach (({ node } ) => delete node.loc )
.insertAfter (importASTNodes)
} else {
root.get ().node .program .body .unshift (...importASTNodes)
return root.toSource ()
3.4 cli-shared-utils
yarn workspace @i-box/cli-shared-utils add chalk execa semver chalk strip-ansi readline events ora module
3.3 vue-cli-service
1. 作为核心插件使用
module .exports = (api.options ) => {
api.render ('./template' , {})
api.extendPackage ({
scripts : {
'serve' : 'vue-cli-service serve' ,
'build' : 'vue-cli-service build'
dependencies : {
vue : "^3.0.4" ,
devDependencies : {
"@vue/compiler-sfc" : "^3.0.4" ,
2. 作为命令使用
"bin" : {
"vue-cli-service" : "bin/vue-cli-service.js"
Service
class Service {
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {} ) {
checkWebpack (context)
this .pkg = this .resolvePkg (pkg);
this .plugins = this .resolvePlugins (plugins, useBuiltIn)
this .webpackChainFns = [];
this .webpackRawConfigFns = [];
this .commands = {};
async run () {}
async init (){}
resolveWebpackConfig (){}
resolvePkg (inlinePkg, context = this .context ) {
Module .createRequire (path.resolve (context, "package.json" )).resolve (context)
resolvePlugins (inlinePlugins ) {
const idToPlugin = (id, absolutePath ) => ({
id : id.replace (/^.\// , "built-in:" ),
apply : require (absolutePath || id),
let plugins
const builtInPlugins = [
"./commands/serve" , "./commands/build" , "./commands/inspect" ,
"./config/base" ,"./config/assets" , "./config/css" ,"./config/prod" ,
].map ((id ) => idToPlugin (id))
const projectPlugins = Object .keys (this .pkg .devDependencies || {})
.concat (Object .keys (this .pkg .dependencies || {}))
.filter (isPlugin)
.map ((id ) => idToPlugin (id, resolveModule (id, this .pkgContext )));
return builtInPlugins.concat (projectPlugins)
async run () {
await this .init (mode);
let command = this .commands [name];
const { fn } = command;
return fn (args, rawArgv)
async init () {
this .loadEnv ();
const userOptions = this .loadUserOptions ()
const loadedCallback = (loadedUserOptions ) => {
this .plugins .forEach (({ id, apply } ) => {
apply (new PluginAPI (id, this ), this .projectOptions );
this .webpackChainFns .push (this .projectOptions .chainWebpack )
this .webpackRawConfigFns .push (this .projectOptions .configureWebpack )
return loadedCallback (userOptions)
resolveWebpackConfig ()
PluginAPI
class PluginAPI {
registerCommand (name, opts, fn ) {
this .service .commands [name] = { fn, opts : opts || {} };
chainWebpack () {
this .service .webpackChainFns .push (fn);
configureWebpack () { }
configureDevServer (fn ) {}
resolveWebpackConfig () {}
resolveChainableWebpackConfig (){}
4. 插件
program.command ("add <plugin> [pluginOptions]" ).action ((plugin ) => {
require ("../lib/add" )(plugin, minimist (process.argv .slice (3 )));
function add (pluginToAdd, options = {}, context = process.cwd ()) {
if (!(await confirmIfGitDirty (context))) { return }
const packageName = resolvePluginId (pluginName)
await pm.add (`${packageName} @${pluginVersion} ` )
const generatorPath = resolveModule (`${packageName} /generator` , context)
invoke (pluginName, options, context)
实现一个简单的插件
module .exports = (api, options = {}, rootOptions = {} ) => {
api.injectImports (api.entryFile , `import native from './plugins/native-ui';` );
api.transformScript (api.entryFile , require ("./injectUseNativeUi" ));
api.extendPackage ({
dependencies : {
"naive-ui" : "^2.11.4" ,
api.render ("./template" , {});
6. create-umi
1. 初始化项目
2. 基本流程
$ yarn create umi
? Select the boilerplate type (Use arrow keys)
ant-design-pro - Create project with a layout-only ant-design-pro boilerplate, use together with umi block.
❯ app - Create project with a simple boilerplate, support typescript.
plugin - Create a umi plugin.
? Do you want to use typescript? (y/N)
? What functionality do you want to enable? (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ antd
◯ dva
◯ code splitting
◯ dll
create abc/package.json
create abc/.gitignore
create abc/.editorconfig
create abc/.env
create abc/.eslintrc
create abc/.prettierignore
create abc/.prettierrc
create abc/.umirc .js
create abc/mock/.gitkeep
create abc/src/assets/yay.jpg
create abc/src/global .css
create abc/src/layouts/index.css
create abc/src/layouts/index.tsx
create abc/src/pages/index.css
create abc/src/pages/index.tsx
create abc/tsconfig.json
create abc/typings.d .ts
📋 Copied to clipboard, just use Ctrl +V
✨ File Generate Done
3. 基本流程
4. 简单实现
初始化项目
npm init -y
"files" : [
"cli.js" ,
"index.js" ,
"lib"
"bin" : {
"create-umi2" : "cli.js"
npm install chalk inquirer mkdirp execa glob fs-extra semver typescript yargs-parser yeoman-environment yeoman-generator
cli.js
const args = require ('yargs-parser' )(process.argv .slice (2 ))
const run = require ('./lib/run' );
const name = args._ [0 ] || ''
const {type} = args
(async () => {
await run (name, type, args)
run.js
const run = async (config ) => {
let { type } = config;
const answers = await inquirer.prompt ([
name : "type" ,
message : "Select the boilerplate type" ,
type : "list" ,
choices : generators,
type = answers.type ;
return runGenerator (`./generators/${type} ` , config);
generators
const generators = fs
.readdirSync (`${__dirname} /generators` )
.filter (f => !f.startsWith ('.' ))
.map (f => {
return {
name : `${f.padEnd(15 )} - ${chalk.gray(require (`./generators/${f} /meta.json` ).description)} ` ,
value : f,
short : f,
runGenerator
const runGenerator = async (generatorPath, { name = '' , cwd = process.cwd(), args = {} } ) => {
return new Promise (resolve => {
if (name) {
mkdirp.sync (name);
cwd = path.join (cwd, name);
const Generator = require (generatorPath);
const env = yeoman.createEnv ([], {
const generator = new Generator ({
name,
resolved : require .resolve (generatorPath),
args,
return generator.run (() => {
resolve (true )
basic
const Generator = require ("yeoman-generator" );
class BasicGenerator extends Generator {
constructor (opts ) {
super (opts);
this .opts = opts;
this .name = basename (opts.env .cwd );
initianlizing () {
prompting () {
configuring () {
default () {
writing () {
conflicts () {
install () {
end () {
prompt (questions ) {
process.send && process.send ({ type : 'prompt' });
process.emit ('message' , { type : 'prompt' });
return super .prompt (questions);
isTsFile (f ) {
return f.endsWith ('.ts' ) || f.endsWith ('.tsx' ) || !!/(tsconfig\.json)/g .test (f);
writeFiles ({ context, filterFiles = noop } ) {
glob.sync ('**/*' , { cwd : this .templatePath (), dot : true ,})
.filter (filterFiles)
.filter (file => !file.includes ('welcomeImgs' ))
.forEach (file => {
const filePath = this .templatePath (file);
if (statSync (filePath).isFile ()) {
this .fs .copyTpl (
this .templatePath (filePath),
this .destinationPath (file.replace (/^_/ , '.' )),
context,
ant-design-pro
class AntDesignProGenerator extends BasicGenerator {
prompting () {
const prompts = [
name : 'language' ,
type : 'list' ,
message : '🤓 Which language do you want to use?' ,
choices : ['TypeScript' , 'JavaScript' ],
default : 'TypeScript' ,
name : 'allBlocks' ,
type : 'list' ,
message : '🚀 Do you need all the blocks or a simple scaffold?' ,
choices : ['simple' , 'complete' ],
default : 'simple' ,
return this .prompt (prompts).then (props => {
this .prompts = props;
async writing () {
const projectName = this .opts .name || this .opts .env .cwd ;
const githubUrl = `https://gitee.com/ant-design/ant-design-pro`
const gitArgs = [`clone` , githubUrl, `--depth=1` , projectName];
await exec (`git` ,gitArgs)
class Generator extends BasicGenerator {
prompting () {
const prompts = [
name : "isTypeScript" ,
type : "confirm" ,
message : "Do you want to use typescript?" ,
default : false ,
name : "reactFeatures" ,
message : "What functionality do you want to enable?" ,
type : "checkbox" ,
choices : [
{ name : "antd" , value : "antd" },
{ name : "dva" , value : "dva" },
{ name : "code splitting" , value : "dynamicImport" },
{ name : "dll" , value : "dll" },
{ name : "internationalization" , value : "locale" },
return this .prompt (prompts).then ((props ) => {
this .prompts = props;
writing () {
this .writeFiles ({
context : {
name : this .name ,
...this .prompts ,
filterFiles : f => {
const { isTypeScript, reactFeatures } = this .prompts ;
if (isTypeScript) {
if (f.endsWith ('.js' )) return false ;
if (!reactFeatures.includes ('dva' )) {
if (f.startsWith ('src/models' ) || f === 'src/app.ts' ) return false ;
if (!reactFeatures.includes ('locale' )) {
if (f.startsWith ('src/locales' ) || f.includes ('umi-plugin-locale' )) return false ;
} else {
if (this .isTsFile (f)) return false ;
if (!reactFeatures.includes ('dva' )) {
if (f.startsWith ('src/models' ) || f === 'src/app.js' ) return false ;
if (!reactFeatures.includes ('locale' )) {
if (f.startsWith ('src/locales' ) || f.includes ('umi-plugin-locale' )) return false ;
return true ;
7. create-umi-app
1. 初始化项目
mkdir myapp && cd myapp
npx @umijs/create-umi-app
2. 简单实现
项目初始化
lerna init
lerna create @i-box/create-ibox-umi-app
"workspaces" : [
"packages/*"
"useWorkspaces" : true ,
yarn workspace @i-box/create-ibox-umi-app add yargs-parser yeoman-generator yeoman-environment glob
"bin" : {
"create-ibox-umi-app" : "bin/create-umi-app.js"
"files" : [
"lib" ,
"bin" ,
"templates"
create-umi-app.js
#!/usr/bin/env node
require ('../lib/cli' );
const args = require ('yargs-parser' )(process.argv .slice (2 ));
const yeoman = require ("yeoman-environment" );
const env = yeoman.createEnv ([], {
cwd : process.cwd (),
require ('./' ).default ({
cwd : process.cwd (),
args,
const AppGenerator = require ('./AppGenerator' )
module .exports = async ({cwd, args}) => {
const generator = new AppGenerator ({
args,
await generator.run ();
AppGenerator
class AppGenerator extends Generator {
async writing () {
this .copyDirectory ({
context : {
version : require ('../package' ).version ,
conventionRoutes : this .args .conventionRoutes ,
path : join (__dirname, '../templates' ),
target : this .cwd ,
复制代码
429
萌萌哒草头将军
Vue.js
React.js
472
dream_99517
VIP.4 融会贯通