I have written the vuE3 + TS enterprise development environment as a scaffold, which was supposed to be used to quickly build the development environment of the company’s various projects, and it is being integrated little by little. Here’s the effect:
[Article objective] :
- This section describes some toolkits and their application scenarios
- Scaffolding Principle (development idea)
- Implement a scaffold
For better understanding and more efficient learning, it is recommended to download this project Lu-cli first.
Here are some of the most common toolkits used in scaffolding, and I’ll explain the main uses of these in the following articles. Let’s take a look at some of the packages available:
commander
Command line interface complete solution.Portal πͺinquirer
Interactive command interface collection.Portal πͺglobby
Path matching tool.Portal πͺexeca
Child process management tool.Portal πͺfs-extra
Enhanced file system.Portal πͺdownload-git-repo
Download the code repository tool.Portal πͺejs
Template rendering.Portal πͺchalk
Terminal character styles.Portal πͺboxen
Terminal “box”.Portal πͺvue-codemod
Convert the contents of the file toAST.Portal πͺora
Terminal loading effect.Portal πͺfiglet
The terminal logo is displayed.Portal πͺopen
Cross-platform open links.Portal πͺ- .
There are more interesting bags to dig up.
Core knowledge collection (toolkit introduction and application scenarios)
I will not describe the details of the package application, recommended that you first NPM init a development project, you can first call these packages to see what they are used to do, what the effect is like, easy to understand, the official website address has been listed above for you.
1. Create a command
The Commander package is a complete command-line solution for creating scaffolding commands such as the Lucli Create app:
// index.js
#!/usr/bin/env node
const program = require("commander");
program
.version("0.0.1"."-v, --version") // Define the version
.command("create app") // Command name
.description("create an application") / / description
.action((source,destination) = >{ // Perform the callback
console.log('1',source);
console.log('2',destination);
// Execute some logic, such as the following interactive logic
})
// Parse the command line
program.parse();
Copy the code
Note: Be sure to execute program.parse(); Parse the command because otherwise you’ll end up being silly and the scaffolding collapses before it starts.
Test in a development environment with the following command:
node ./bin/index.js create app
Copy the code
When our scaffolding is developed, we do the mapping in the bin field of package.json. Such as:
// package.json
"bin": {"lucli":"./index.js"
}
Copy the code
By NPM, index.js will be mapped to global bins of Lucli after the user has installed our scaffolding tool globally, so that it can be executed on the command line:
lucli create app
Copy the code
Of course, we can also do the mapping manually with the NPM link command before the release.
Note: The command line permission is incorrect. Windows users are advised to run the command in administrator mode. MAC users recommend using sudo
npm link
or
sudo npm install
Copy the code
After that we can also run the lucli create app command.
2. Collect user interaction information
Use the Inquirer to interact with users.
const inquirer = require("inquirer")
inquirer.prompt([
{
name:"name".message:"the name of project: ".// Project name
type:"input".// Character type
default:"lucli-demo".// Default name
validate: (name) = > { // Verify that the name is correct
return validProjectName(name).errMessage || true; }}, {name:"framework".message:"project framework".// Project framework
type:"list".choices: [/ / options
{
name: "vue + ts".value: "vue"
},
{
name: "react".value: "react"
}
]
}
])
.then((answers) = > {
console.log('Result:',answers);
})
.catch((error) = > {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else went wrong}});Copy the code
3. Determine the target project path
We get the name of the project created by the user in the above code, and we use path to determine the project path.
const path = require("path");
// Remember the targetDir
const targetDir = path.join(process.cwd(), 'Project name');
Copy the code
TargetDir is the local absolute address of your target project, for example:
/Users/lucas/repository/study/lu-cli
Copy the code
4. Match the directory
Globby is used to read our template directory in the scaffold.
const globby = require("globby");
const files = await globby(["* * / *"] and {cwd: './template'.dot: true })
console.log(files)
Copy the code
The result is an array of paths based on all the files in the directory you specify.
An example is a directory like this:
β ββtemplate β ββ SRC β ββindex.js β ββrouter β ββindex.ts β ββtemplate β ββ SRC β ββrouter β ββindex.tsCopy the code
Take the file as the smallest unit. The specific role of this tool is more obvious in actual combat, and then look back.
5. Read and write files
Fs-extra is an enhanced version of the FS module, adding some new apis to the existing functionality.
const fs = require("fs-extra")
// Read the contents of the file
const content = fs.readFileSync('File path'.'utf-8');
// Write a file
fs.writeFileSync('File path'.'File contents')
Copy the code
In addition to the above methods, template files can also be downloaded from the code repository in the following ways.
6. Download the repository code
Git Clone the code from the repository via download-git-repo.
const download = require("download-git-repo");
download('https://www.xxx.. git'.'test/tmp'.function (err) {
console.log(err)
})
Copy the code
7. Template rendering
Using EJS usually requires rendering according to different conditions in the template. Such as:
package.json
According to whether the user needs to installbabel
Add aboutbabel
Some configuration of.main.js
According to the function of the file, rendering different code.xx.vue
Dynamic Settings in the template ofcss
Precompiled.- .
const ejs = require('ejs')
// demo-01
const template = (
`<%_ if (isTrue) { _%>`
+ 'content'
+ ` _ % > < % _} `
)
const newContent = ejs.render(template, {
isTrue: true,})//demo-02
const template = (
`<style lang="<%= cssPre %>">`
+`.redColor{`
+`color:red`
+`} `
+ `</style>`
)
const newContent = ejs.render(template, {
cssPre: 'less',})Copy the code
8. Child process execution manager
Execa is used to execute terminal commands, such as NPM install, and the tool can also set up the installation source.
const executeCommand = (command, args, cwd) = > {
return new Promise((resolve, reject) = > {
const child = execa(command, args, {
cwd,
stdio: ['inherit'.'pipe'.'inherit'],
})
child.stdout.on('data'.buffer= > {
const str = buffer.toString()
if (/warning/.test(str)) {
return
}
process.stdout.write(buffer)
})
child.on('close'.code= > {
if(code ! = =0) {
reject(new Error(`command failed: ${command}`))
return
}
resolve()
})
})
}
// This is the target project path, where NPM install is executed
await executeCommand('npm'['install'], targetDir)
Copy the code
9. Terminal string style.
Chalk is used to display different style strings in the terminal.
const chalk = require('chalk');
console.log(chalk.blue('Hello world! '));
Copy the code
10. The file content is converted toAST
In vue-CLI, vue-codemod converts the contents of the file into an AST, thus implementing the function of injecting code into the file and returning a string of file contents.
const { runTransformation } = require("vue-codemod")
const fileInfo = {
path: "src/main.js".source: "File contents"
}
// Parse the code to get the AST and insert the statement from the imports parameter
const 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)
// a tricky way to avoid blank line after the previous import
.forEach(({ node }) = > delete node.loc)
.insertAfter(importASTNodes)
} else {
// no pre-existing import declarationsroot.get().node.program.body.unshift(... importASTNodes) }return root.toSource()
}
const params = {
imports: [ "import { store, key } from './store';"]}const newContent = runTransformation(fileInfo, transformation, params)
Copy the code
Scaffolding Principle (development idea)
conclusion
The scaffolding principle is to read the template information we prepared in advance according to different customized needs after collecting user interaction information, and then make some differentiated updates to individual files. The core of the update is how to modify the contents of files, which can be done in three ways:
- using
vue-codemod
- Template rendering
- Regular match
This section will be explained in detail later, and finally the corresponding content will be written under our target project. The installation command is being executed.
There are many ways to read templates, but you can also download them directly from the remote repository code using the download-git-repo method. This depends on how you use it. If you download the complete project code without any configuration, you may have a lot of templates to prepare, which is relatively rigid. You can also reduce the package size by putting generic templates in the repository and downloading them in this way, rather than putting them in the project. Even when it comes to version updates, there are pros and cons. Usually we put the template in the project for easy maintenance.
The core principles are the same, and the more you need to think about how to organize your code, the better.
The following I more through the way of code structure to help you to comb out the specific how to develop scaffolding an idea.
The development train of thought
Create a new js file to create the scaffold command. You can debug it using node or using the NPM link in package.json.
In the callback after the command is executed we create the user interaction, wait for the user interaction to complete, and assume that we have this information:
{
name:"lucli-demo".// Project name
framework:"vue"./ / the vue framework
funcList: ["router"."vuex"."less"] // Feature list
}
Copy the code
Our goal is now clear, to build a vUE development environment that integrates Router, VUex, and less. The next idea is to download the template and inject the code into the corresponding file, such as adding the corresponding dependencies in package.json.
We divided the templates according to their functions. The template directory of vUE framework is as follows:
vue-plugins/default/ // Default template
vue-plugins/router/ // Routing template
vue-plugins/vuex/ / / vuex template
Copy the code
Downloading a template is simply reading the template content and generating a file:
// Write a file
fs.writeFileSync('File path'.'File contents')
Copy the code
Can we generate multiple files by iterating through an object? Based on that, let’s imagine putting the list of files we want to build into one object. The structure of the array is as follows:
{
'src/main.js':'content'.'src/App.vue':'content'.'src/theme.less':'content'.'package.json':'content'. }Copy the code
The next thing to do is to generate this object. First we create a Generator class where we will put the directory of the final files to render:
// src/Generator.js
class Generator{
constructor(){
this.files = {}; // File directory}}Copy the code
Using globby to read our template directory, and then traversing the object, using the file system (FS-extra) to read the corresponding file content, we use the Router template as an example.
// Generator.js
class Generator{
constructor(){
this.files = {}; // File directory
}
render(){
// Read the router function template
const files = await globby(["* * / *"] and {cwd: './src/vue-plugins/router'.dot: true })
for (const rawPath of files) {
// Read the contents of the file
const content = getFileContent(rawPath)
// Update the files object
this.files[rawPath] = content; }}}Copy the code
We create an index.js under each template directory and save the template information in the files object by calling the render method.
// src/vue-plugins/router/index.js
module.exports = (generator) = > {
// Render template
generator.render("vue-plugins/router/template");
}
Copy the code
After we get the list of features, we loop through the list, executing the render method in turn.
const generator = new Generator();
// The loop function loads the template
_funcsList.forEach(funcName= > {
require(`.. /src/${pluginName}/${funcName}/index.js`)(generator);
});
Copy the code
Of course, let’s not forget to load our default template, which is the main body of the project architecture.
So our files object is the directory to render the files to.
All that’s left is to do some differentiation in specific files, such as introducing the router in main.js.
There are three ways to insert code into a file:
vue-cli
In the use ofvue-codemod
This package converts the contents of the file intoASTAnd then inASTInsert the contents of the corresponding node, and thenASTConvert to file content.- The contents of the file are essentially read as a string, and the insert code just inserts the string in the right place. We can match the position by using regular expressions.
- If you are not familiar with template rendering, it is recommended to learn about EJS first. If you are not familiar with template rendering, it is recommended to learn about EJS first.
We do package.json separately, using simple object merges for related differentiation, and also consider template rendering for complex configurations.
The idea is the same, we create a codeInFiles object in the Generator to hold the file to be inserted and the contents to be inserted, and a PKG object to hold the package.json contents.
// src/Generator.js
class Generator{
constructor(){
this.pkg = {
name,
version: "1.0.0".description: "".scripts: {
dev: "vite --mode development".build: "vue-tsc --noEmit && vite build".prebuild: "vue-tsc --noEmit && vite build --mode staging".serve: "vite preview",}};// package.json
this.files = {}; // File directory
this.codeInFiles = { // The object to insert code into
'path':new Set()
};
}
// Update the codeInFiles object to insert code into
injectImports(path, source) {
const _imports = this.codeInFiles[path] || (this.codeInFiles[path] = new Set());
(Array.isArray(source) ? source : [source]).forEach(item= > {
_imports.add(item)
})
}
}
Copy the code
For example, in the router function template index.js, call:
// src/vue-plugins/router/index.js
module.exports = (generator) = > {
// Render template
generator.render("vue-plugins/router/template");
// Add dependencies
generator.extendPackage({
"dependencies": {
"vue-router": "^ 4.0.10",}})// Inject the code
generator.injectImports("src/main.ts"."import router from './router';");
}
Copy the code
Insert code by looping through the codeInFiles object, updating the files object with the new file contents. I’ll use vue-codemod as an example:
// Generator.js
// Handle package objects
extendPackage(obj) {
for (const key in obj) {
const value = obj[key];
if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
this.pkg[key] = Object.assign(this.pkg[key] || {}, value);
} else {
this.pkg[key] = value; }}}// Insert code into files
injectImports(){
Object.keys(_codeInFiles).forEach(file= > {
const imports = _codeInFiles[file] instanceof Set ? Array.from(_codeInFiles[file]) : [];
if (imports && imports.length) {
// Update the files object with the contents of the newly inserted file
_files[file] = runTransformation(
{ path: file, source: _files[file] },
injectImports,
{ imports },
)
}
})
}
Copy the code
Then according to the file directory files in turn to generate files.
// Generate package,json file
fs.writeFileSync('package.json'.JSON.stringify(this.pkg, null.2))
// Generate other files
Object.keys(files).forEach((name) = > {
fs.writeFileSync(filePath, files[name])
})
Copy the code
So our project architecture is basically set up.
Finally, install the dependencies through ExecA.
// Run the NPM install command to install dependency packages
await executeCommand('npm'['install'], targetDir)
Copy the code
Actual scaffolding
Note: in the actual combat process, the author will not say particularly fine, the code will not be very complete, in order to better learning efficiency:
I suggest you download this project lu- CLI, in actual combat can be used as a reference, and find the code.
Initialize the project
Initialize our scaffolding project with NPM init to improve the directory structure below:
β β cli - project// Project nameβ ββ Bass Exercises β ββ Index.js// Command fileβ β β the SRC/ / the source codeβ β β package. Json// Configuration fileβ β β the README, mdCopy the code
Create a command
// bin/index.js
#!/usr/bin/env node
const program = require("commander");
const handlePrompts = require(".. /src/create");
program
.version("0.0.1"."-v, --version")
.command("create app")
.description("create an application")
.action(() = > {
// Handle the interaction
handlePrompts();
})
// Parse the command line
program.parse();
Copy the code
Run the debug
node ./bin/index.js create app
Copy the code
Creating interactive
We created SRC/creation.js in the root directory to handle the interaction logic. I won’t go into this section too much. You can design the interaction as you like.
// src/create.js
const inquirer = require("inquirer")
const boxen = require('boxen');
const chalk = require('chalk');
const path = require("path");
const { promptTypes } = require("./enum");
const getPromptsByType = require("./getPrompts");
const Generator = require("./Generator");
const {
executeCommand,
validProjectName
} = require("./utils/index");
module.exports = () = > {
// Print our welcome message
console.log(chalk.green(boxen("Welcome to Lucli ~", { borderStyle: 'classic'.padding: 1.margin: 1 })));
inquirer.prompt([
{
name: "name".message: "the name of project: ".// Project name
type: "input".// Character type
default: "lucli-demo".// Default name
validate: (name) = > {
return validProjectName(name).errMessage || true; }}, {name: "framework".message: "project framework".// Project framework
type: "list".choices: [{name: "vue + ts".value: promptTypes.VUE
},
{
name: "react".value: promptTypes.REACT
}
]
}
]).then(answers= >{
Prompts are chosen according to the frame
const prompts = getPromptsByType(answers.framework);
if (prompts.length) {
// Select function
inquirer.prompt(prompts).then(async (funcs) => {
// Logic processing will code})}else {
console.log('Sorry, under development, stay tuned! '); }})}Copy the code
Create the Generator class
After processing the interaction, we have the corresponding information:
{
name:"lucli-demo".// Project name
framework:"vue"./ / the vue framework
funcList: ["router"."vuex"."less"] // Feature list
}
Copy the code
The next step is to generate the files file. First, create the Generator class and initialize our files, codeInFiles, PKG objects and some handler functions.
This class is actually in the development of a little bit of improvement, the author to lazy directly posted. You can try to write it yourself.
// src/Generator.js
const path = require("path");
const ejs = require('ejs');
const fs = require("fs-extra");
const { runTransformation } = require("vue-codemod")
const {
writeFileTree,
injectImports,
injectOptions,
isObject
} = require(".. /src/utils/index")
const {
isBinaryFileSync
} = require('isbinaryfile');
class Generator {
constructor({ name, targetDir }) {
this.targetDir = targetDir;
this.pkg = { // package.json
name,
version: "1.0.0".description: "".scripts: {
dev: "vite --mode development".build: "vue-tsc --noEmit && vite build".prebuild: "vue-tsc --noEmit && vite build --mode staging".serve: "vite preview",}};this.files = {}; // File directory
this.codeInFiles = {}; // The file to insert the code into
this.optionInFiles = {}; / / injection
this.middlewareFuns = []; // A list of functions that handle file directories
}
// Handle package objects
extendPackage(obj) {
for (const key in obj) {
const value = obj[key];
if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
this.pkg[key] = Object.assign(this.pkg[key] || {}, value);
} else {
this.pkg[key] = value; }}}// Update the object to insert code into
injectImports(path, source) {
const _imports = this.codeInFiles[path] || (this.codeInFiles[path] = new Set());
(Array.isArray(source) ? source : [source]).forEach(item= > {
_imports.add(item)
})
}
// Update the object of the option to be inserted
injectOptions(path, source) {
const _options = this.optionInFiles[path] || (this.optionInFiles[path] = new Set());
(Array.isArray(source) ? source : [source]).forEach(item= > {
_options.add(item)
})
}
// Parse the contents of the file
resolveFile(sourcePath) {
// Return the binary file directly
if (isBinaryFileSync(sourcePath)) {
return fs.readFileSync(sourcePath);
}
const template = fs.readFileSync(sourcePath, 'utf-8');
// This is not necessary, if you have a template rendering need to add
const content = ejs.render(template);
return content;
}
// Render method
async render(source) {
this.middlewareFuns.push(async() = > {const relativePath = `./src/${source}`;
const globby = require("globby");
// Get the file directory
const files = await globby(["* * / *"] and {cwd: relativePath, dot: true })
for (const rawPath of files) {
// Get the absolute address to read the file
const sourcePath = path.resolve(relativePath, rawPath)
const content = this.resolveFile(sourcePath)
// There is file content
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
this.files[rawPath] = content; }}})}// Execute the function
async generator() {
// Set the value of files
for (const middleawre of this.middlewareFuns) {
await middleawre();
}
const _files = this.files;
const _codeInFiles = this.codeInFiles;
const _optionsInFiles = this.optionInFiles;
// Insert code into files
Object.keys(_codeInFiles).forEach(file= > {
const imports = _codeInFiles[file] instanceof Set ? Array.from(_codeInFiles[file]) : [];
if (imports && imports.length) {
_files[file] = runTransformation(
{ path: file, source: _files[file] },
injectImports,
{ imports },
)
}
})
// Insert code into files
Object.keys(_optionsInFiles).forEach(file= > {
const injections = _optionsInFiles[file] instanceof Set ? Array.from(_optionsInFiles[file]) : [];
if(injections && injections.length) { _files[file] = injectOptions(_files[file], injections); }})await writeFileTree(this.targetDir, this.files)
// Generate a package.json file
await writeFileTree(this.targetDir, {
"package.json": JSON.stringify(this.pkg, null.2)}}}module.exports = Generator;
Copy the code
To load a template
Then we define our project path, instantiate a Generator class, loop through our list of functions, load the template file, and build our Files object. For specific template information, please refer to the template in the project.
// src/create.js
module.exports = () = >{... inquirer.prompt(prompts).then(async (funcs) => {
// Logic processing will code
// Project path
const targetDir = path.join(process.cwd(), answers.name);
// Create an instance
const generator = new Generator({ name: answers.name, targetDir });
const _funcsList = funcs.funcList;
// Select CSS precompile
if (_funcsList.includes("precompile")) {
const result = await inquirer.prompt([
{
name: "cssPrecle".message: "less or sass ?".type: "list".choices: [{name: "less".value: "less"
},
{
name: "sass".value: "sass"}}]]); _funcsList.pop();// Add precompiled dependencies
generator.extendPackage({
"devDependencies": {
[result.cssPrecle]: result.cssPrecle === "less" ? "^ 4.4.1" : "^ 1.35.2"}})}let pluginName = ' ';
// Define the frame template
switch (answers.framework) {
case promptTypes.VUE:
pluginName = 'vue-plugins'
break;
case promptTypes.REACT:
pluginName = 'vue-plugins'
break;
};
// Load the default template
require(`.. /src/${pluginName}/default/index.js`)(generator);
// Load the function template
_funcsList.forEach(funcName= > {
require(`.. /src/${pluginName}/${funcName}/index.js`)(generator); }); . })Copy the code
// src/vue-plugins/vuex/index.js
module.exports = (generator) = > {
// Add dependencies
generator.extendPackage({
"dependencies": {
"vuex": "^ 4.0.2." "}})// Inject the code
generator.injectImports("src/main.ts"."import { store, key } from './store';");
// The injection option
generator.injectOptions("src/main.ts".".use(store, key)");
// Render template
generator.render("vue-plugins/vuex/template");
}
Copy the code
You can use the Inquirer with flexibility, such as the CSS precompile option. You can also use type:expand to do this.
Generate the file
Then execute the Generator function to write the files object as a file, and finally install the dependencies in package.json:
//src/create.js
const {
executeCommand,
validProjectName
} = require("./utils/index"); .// Execute the render generated file
await generator.generator();
// Run the NPM install command to install dependency packages
await executeCommand('npm'['install'], targetDir)
console.log(chalk.green(boxen("Build success", { borderStyle: 'double'.padding: 1 })));
Copy the code
So we have developed a basic scaffolding.
The last
By mastering this project you can not only develop tools like scaffolding, you can write more tools like scaffolding to improve your productivity.
There are many things that can be optimized for this project, such as selecting the installation source when you install dependencies, checking if the folder already exists when you create the project, and configuring some development specifications. Interested partners can explore and submit to me Mr.
I’ll update this post as well if I have time.
thank you