Front end scaffolding sharing
Recently, the team needed to unify the scaffolding, and spent some time to understand how to build it. In fact, the principle is not complicated
Step one, first we need to know what we need, and then we need to create what
-
We first need an entry file that can be executed by Node
-
Templates to generate files
-
Use the Node FS module to write the template to the directory we specified
That’s the simplest idea, so we can do it
-
First create a folder, then execute NPM init within the folder to initialize a package.json file
-
Then create an entry file index.js and write
#! /usr/bin/env node console.log('hello,cli! ');Copy the code
Executing Node index.js will print Hello,cil!
-
Create another template file, template.js, and add the string
hello,template! Copy the code
-
We can then modify it in index.js, and get the current node execution path from process.cwd(). We can read a file and write it to a directory
#! /usr/bin/env node console.log('hello,cli! '); const fs = require("fs"); const path = require("path"); const folderName = path.join(process.cwd(), "/cli"); Const mkdirFile = (name) => {try {if (! fs.existsSync(name)) { fs.mkdirSync(name); } } catch (err) { console.error(err); }}; fs.readFile("./template.js", "utf-8", (err, data) => { if (err) { console.log(err); return; } else { console.log(data); mkdirFile(folderName); fs.writeFileSync(`${folderName}/template.js`, data); }});Copy the code
-
The foundation of the first step is complete. Now let’s think about it
How do you create scaffolds that need to interact on the console when you need to install third-party libraries
- Commander can automatically parse commands and parameters to process commands entered by users
- Inquirer User input/selection interaction
- Ora console progress animation prompt
Js create the project name, and then go to the project type selection. When the project type is selected, the corresponding project folder will be created. And create template.js in it
#! /usr/bin/env node // console.log("hello,cli!" ); const fs = require("fs"); const path = require("path"); const ora = require("ora"); const inquirer = require("inquirer"); const program = require("commander"); Const mkdirFile = (name) => {try {if (! fs.existsSync(name)) { fs.mkdirSync(name); } } catch (err) { console.error(err); }}; const doFs = (name) => { fs.readFile("./template.js", "utf-8", (err, data) => { if (err) { console.log(err); return; } else { mkdirFile(name); fs.writeFileSync(`${name}/template.js`, data); }}); }; Program //.version('1.0.0').command("create <app-name>").description("create a new project").action(async (name) => { const cwd = process.cwd(); Const questions = [{type: "list", message: "Please select item type: ", name: "type", choices: [{name:" Web item ", value: "web" }], }, ]; const { type } = await inquirer.prompt(questions); const proce = ora("Start creating..." ); proce.start(); const folderName = path.join(cwd, name); doFs(folderName); proce.succeed("succeed done!" ); }); program.parse(process.argv);Copy the code
So at this point, our basic idea is done, and we need to reorganize it.
-
First, we create a bin folder and add the tarl. js file as our entry point to import index.js
-
Create a SRC folder to hold all of our source code and add a SRC /core folder
-
Since the following template definition and class definition will be used, we need to execute ts files directly, so we need to add a third-party component, NPM I-S TS-node @types/node. For specific use, please refer to the official document, and then modify tar.js
#! /usr/bin/env node const tsNode = require("ts-node/dist/bin"); const path = require("path"); (async () => { const argv = process.argv.slice(2); const dir = path.join(__dirname, ".. /src"); tsNode.main(["index.ts", ...argv], { "--dir": dir }); }) ();Copy the code
-
Change index.js to index.ts and add type definitions to all method arguments. Then execute node bin/tara directly, throwing Cannot find module ‘typescript’ error. The TS library NPM I-S typescript needs to be installed
-
You can also use the remote template Download-git-repo to download repositories from Github.
npm install --save download-git-repo
Copy the code
Download () the first parameter is the repository address, see the official documentation for more details
-
With local templates, the idea is that you can generate a uniform template configuration object and then generate files one by one based on the configuration to generate a complete project directory
-
The first step is to create a template class called Project.ts that defines the properties to generate package.json
The init method is an abstract method used by subclasses to customize some operations
import Package from "./Package"; abstract class Project { name: string; private package: Package; constructor(parameters) {} init() {} protected abstract _init_(): void; } export default Project; Copy the code
- Create a Package class
class Package { constructor(parameters) {} } export default Package; Copy the code
-
-
The next step is to add properties and methods to the Package, name, version, scripts, dependencies, devDependencies, etc., and add get/set methods. KeyValue {[T: string]: any; }
import { KeyValue } from "./Interface"; Const INIT_VERSION = "0.1.0 from"; const MIT = "MIT"; function pick(object: KeyValue, props: string[]): KeyValue { const result: KeyValue = {}; props.forEach((prop) => { if (object[prop] ! == undefined) { result[prop] = object[prop]; }}); return result; } function toJSON(map: Map<string, string>): KeyValue { const json: KeyValue = {}; const keys: string[] = Array.from(map.keys()); keys.sort(); keys.forEach((key) => { json[key] = map.get(key) || ""; }); return json; } class Package { [x: string]: any; private name: string; private version: string = INIT_VERSION; private license: string = MIT; private scripts: Map<string, string> = new Map(); private dependencies: Map<string, string> = new Map(); private devDependencies: Map<string, string> = new Map(); constructor(name: string) { this.name = name; } addExtra(key: string, value: string | string[] | KeyValue) { this[key] = value; } addScript(name: string, script: string) { this.scripts.set(name, script); } addDependency(name: string, version: string) { this.dependencies.set(name, version); } addDependencies(dependencies: { [K: string]: string }) { Object.keys(dependencies).forEach((key) => { this.addDependency(key, dependencies[key]); }); } addDevDependency(name: string, version: string) { this.devDependencies.set(name, version); } addDevDependencies(dependencies: KeyValue) { Object.keys(dependencies).forEach((key) => { this.addDevDependency(key, dependencies[key]); }); } toJSON(): KeyValue { const pkg: KeyValue = pick(this, [ "name", "private", "version", "license", "main", "bin", "files", "lint-staged", ]); if (this.scripts.size ! == 0) { pkg.scripts = toJSON(this.scripts); } if (this.dependencies.size ! == 0) { pkg.dependencies = toJSON(this.dependencies); } if (this.devDependencies.size ! == 0) { pkg.devDependencies = toJSON(this.devDependencies); } return pkg; } } export default Package;Copy the code
Then modify project.ts so that when initializing a Project, the configuration of the Package is initialized and the operation methods of the Package are exposed in the Project
import Package from "./Package"; import { KeyValue } from "./Interface"; abstract class Project { name: string; private package: Package; Private flags: {[K: string]: Boolean} = {}; constructor(name: string, dir: string) { this.name = name; this.package = new Package(name); } init() { this._init_(); } protected abstract _init_(): void; setFlag(flag: string) { this.flags[flag] = true; } isFalg(flag: string): boolean { return this.flags[flag]; } attachPackage(key: string, value: string | string[] | KeyValue) { this.package.addExtra(key, value); } addScript(name: string, script: string) { this.package.addScript(name, script); } addDependency(name: string, version: string) { this.package.addDependency(name, version); } addDependencies(dependencies: { [K: string]: string }) { Object.keys(dependencies).forEach((key) => { this.package.addDependency(key, dependencies[key]); }); } addDevDependency(name: string, version: string) { this.package.addDevDependency(name, version); } addDevDependencies(dependencies: KeyValue) { Object.keys(dependencies).forEach((key) => { this.package.addDevDependency(key, dependencies[key]); }); } getPackage(): Package { return this.package; } } export default Project;Copy the code
- We can create a new WebProject, inherit Project, and then ininitAdd a few dependencies to the package.josn method, then modify index.ts, and we can initially create the package.josn we need
WebProject.ts
import Project from ".. /core/Project"; Class WebProject extends Project {_init_() {this.adddependencies ({"@types/node": "^15.12.5", commander: "^" 8.0.0 inquirer, "^" 8.1.1, ora: "^ 5.4.1", "ts - node" : "^" 10.0.0, typescript: "^ 4.3.5",}); } } export default WebProject;Copy the code
index.ts
Need to install a new FS dependency NPM i-s fs-extra@types /fs-extra
#! /usr/bin/env node import WebProject from "./src/projects/WebProject"; import * as fs from "fs-extra"; import Package from "./src/core/Package"; const path = require("path"); const ora = require("ora"); const inquirer = require("inquirer"); const program = require("commander"); Const mkdirFile = (name: string) => {// try {// if (! fs.existsSync(name)) { // fs.mkdirSync(name); // } // } catch (err) { // console.error(err); / / / /}}; // const doFs = (name: string) => { // fs.readFile("./template.js", "utf-8", (err: any, data: any) => { // if (err) { // console.log(err); // return; // } else { // mkdirFile(name); // fs.writeFileSync(`${name}/template.js`, data); / / / /}}); / /}; const writePackage = (folderName: string, data: Package) => { // mkdirFile(name); fs.ensureDir(folderName); const file = path.join(folderName, "package.json"); fs.ensureFile(file); fs.writeFile(file, JSON.stringify(data, null, 2)); }; Program //.version('1.0.0').command("create <app-name>").description("create a new project").action(async (name: string) => { const cwd = process.cwd(); Const questions = [{type: "list", message: "Please select item type: ", name: "type", choices: [{name:" Web item ", value: "web" }], }, ]; const { type } = await inquirer.prompt(questions); const proce = ora("Start creating..." ); proce.start(); const folderName = path.join(cwd, name); const fileName = path.join(folderName, "package.json"); // doFs(folderName); const project = new WebProject(name, cwd); project.init(); writePackage(folderName, project.getPackage()); proce.succeed("succeed done!" ); }); program.parse(process.argv);Copy the code
- Node bin/tara create myApp generates a myapp folder and package.json, which allows you to freely add properties and configurations to package.json in your Project
{" name ":" myapp ", "version" : "0.1.0 from", "license" : "MIT", "dependencies" : {" @ types/node ":" ^ 15.12.5 ", "commander" : "^ 8.0.0 inquirer", ""," ^ 8.1.1 ", "ora" : "^ 5.4.1", "ts - node" : "^ 10.0.0", "typescript" : "^ 4.3.5"}}Copy the code
Now we need to think about generating files other than package.json. The idea is to store a tree structure object, with the first layer storing the file name of the stub directory, the next layer storing its word folders/subfiles, and then the subfolders/subfiles in the next layer, and so on until all files are recorded. We then create folders/files layer by layer following this storage structure and write template characters to the files
-
The first step is to modify project. ts and add a dirTree. The type of the dirTree is Directory.
AddFile is to add a file to the Directory object and its template string. AddDirectory is to add a folder to the current Directory. AddByPath is to create a folder based on its path. Get a multi-level Directory object, where this is the initial value
import { KeyValue } from "./Interface"; class Directory { name: string; / / the first file name, the second for the template string or return a string of Promise method files: Map < string, the string | (() = > Promise < string >) > = new Map (); // Children: Directory[] = []; constructor(name: string) { this.name = name; } addFile(name: string, content: string | (() => Promise<string>) = "") { this.files.set(name, content); } addFiles(KeyValue: KeyValue) { Object.keys(KeyValue).forEach((key) => { this.addFile(key, KeyValue[key]); }); } addDirectory(directoryName: string): Directory { let dir = this.getDirectory(directoryName); If (dir) {return dir; } dir = new Directory(directoryName); this.children.push(dir); return dir; } addDirectories(directorys: string[]) { directorys.forEach((directory) => { this.addDirectory(directory); }); } getDirectory(name: string): Directory | undefined { return this.children.find((dir) => dir.name === name); } addByPath(path: string): Directory { const parts = path.split("/"); return parts.reduce<Directory>((dir, name) => dir.addDirectory(name), this); } getByPath(path: string): Directory | null | undefined { const parts = path.split("/"); return parts.reduce<Directory | null>((dir, name) => { if (dir) { const child = dir.getDirectory(name); if (child) { return child; } } return null; }, this); } } export default Directory;Copy the code
- At this point, we can continue to Project. Ts, adding and modifying corresponding methods and properties
. import * as path from "path"; import Directory from "./Directory"; abstract class Project { dirTree: Directory; constructor(name: string, dir: string) { this.name = name; this.dirTree = new Directory(path.resolve(dir, name)); this.package = new Package(name); }... addFile( fileName: string, content: string | (() => Promise<string>), path? : string ) { if (path) { const dir = this.dirTree.addByPath(path); dir.addFile(fileName, content); } else { this.dirTree.addFile(fileName, content); } } addDirectory(path: string): void { this.dirTree.addByPath(path); } addDirectories(dirs: string[]) { dirs.forEach((dir) => { this.addDirectory(dir); }); } } export default Project;Copy the code
- Then how to use the Directory class to complete the initialization of the Directory class object, at this time we can think of if we put the template and Directory class configuration in the Project to do, not only will make the Project class a lot of code, but also more complex. So we can make a unified class, pass in the Project object, configure all the properties in the Project, and then add a property in the Project that contains the object of the class, and that will simplify the Project, because that’s all we need to know. We create a feature. ts class that implements most of the Project methods
import * as fs from "fs-extra"; import Project from "./Project"; import { KeyValue } from "./Interface"; abstract class Feature { abstract name: string; protected project: Project; constructor(project: Project) { this.project = project; } // All feacture must implement its own __init method. public init(): void { this._init_(); } addFile( name: string, filePath: string | (() => Promise<string>) = "", path? : string ) { if (typeof filePath === "string") { try { this.project.addFile(name, fs.readFileSync(filePath, "utf-8"), path); } catch (error) { this.project.addFile(name, filePath, path); } } else { this.project.addFile(name, filePath, path); } } addScript(name: string, script: string) { this.project.addScript(name, script); } addScripts(keyValue: KeyValue): void { Object.keys(keyValue).forEach((it) => { this.addScript(it, keyValue[it]); }); } addDependency(name: string, version: string) { this.project.addDependency(name, version); } addDependencies(dependencies: { [K: string]: string }) { this.project.addDependencies(dependencies); } addDevDependency(name: string, version: string) { this.project.addDevDependency(name, version); } addDevDependencies(dependencies: KeyValue) { this.project.addDevDependencies(dependencies); } } export default Feature;Copy the code
- So modify project.ts again and add the following code
. import Feature from "./Feature"; abstract class Project { ...... private features: Feature[] = []; . init() { this._init_(); this.features.forEach((feature) => feature.init()); }... hasFeature(pattern: string): boolean { return !! this.features.find((f) => f.name === pattern); } addFeature(feature: Feature): void { if (this.hasFeature(feature.name)) { return; } this.features.push(feature); } } export default Project;Copy the code
- At this point we can configure our template
- Create a template folder SRC /core/features
- Create a new vuefeature.ts
- You can initialize a custom vue project and copy main.js, app. vue, index.html, index.js, home.vue into the template folder
import Feature from ".. /Feature"; import path from "path"; class VueFeature extends Feature { name = "VueFeature"; protected _init_(): void { this.initScript(); this.initTemplate(); this.initDependencies(); this.initDevDependencies(); } initScript() { this.addScripts({ start: "vue-cli-service serve", serve: "vue-cli-service serve", build: "vue-cli-service build", lint: "vue-cli-service lint", }); this.addScript("build:staging", "vue-cli-service build --mode staging"); this.addScript("build:dev", "vue-cli-service build --mode development"); } initTemplate() { this.addFile("main.js", path.join(__dirname, "./template/main.js"), "src"); this.addFile("App.vue", path.join(__dirname, "./template/App.vue"), "src"); this.addFile( "index.html", path.join(__dirname, "./template/index.html"), "public" ); this.addFile( `index.js`, path.join(__dirname, "./template/router.js"), "src/router" ); this.addFile( "Home.vue", path.join(__dirname, "./template/Home.vue"), "src/views" ); (1) {} initDependencies enclosing addDependencies ({" core - js ":" ^ 3.6.5 ", vue: "^ 3.0.0", "vue - the router" : "^ 4.0.0-0", vuex: "^ 4.0.0-0"}); } initDevDependencies() { // this.addDevDependency("", ""); AddDevDependencies ({"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-service": "~ 4.5.0 @", "vue/compiler - the SFC" : "^ 3.0.0", "@ vue/eslint - config - prettier" : "^ 6.0.0", "Babel - eslint" : "^ 10.1.0", eslint: "^ 6.7.2 eslint - plugin -", "prettier" : "^ 3.3.1", "eslint - plugin - vue" : "^" 7.0.0, prettier: "^ 2.2.1",}); } } export default VueFeature;Copy the code
- Modify project. ts to add VueFeature at the beginning of init method
import VueFeature from "./features/VueFeature"; . init() { this.addFeature(new VueFeature(this)); this._init_(); this.features.forEach((feature) => feature.init()); }...Copy the code
- Node bin/tara create myapp to print project and get the project object we need
Please select project type: Web Project ⠋ Start Creating... project <ref *1> WebProject { features: [ VueFeature { project: [Circular *1], name: 'VueFeature' } ], flags: {}, name: 'myapp', dirTree: Directory { files: Map(0) {}, children: [ [Directory], [Directory] ], name: '/usr/webapp/cli-test/myapp'}, package: package {version: '0.1.0', license: 'MIT', scripts: Map(6) { 'start' => 'vue-cli-service serve', 'serve' => 'vue-cli-service serve', 'build' => 'vue-cli-service build', 'lint' => 'vue-cli-service lint', 'build:staging' => 'vue-cli-service build --mode staging', 'build:dev' => 'vue-cli-service build --mode development' }, dependencies: The Map (10) {' @ types/node '= >' ^ 15.12.5 ', 'commander' = > '^ 8.0.0', 'inquirer' = > '^ 8.1.1', 'ora' = > '^ 5.4.1', 'ts - node' = > '^ 10.0.0', 'typescript' = > '^ 4.3.5', 'core - js' = >' ^ 3.6.5 ', 'vue' = > '^ 3.0.0', 'vue - the router' = > '^ 4.0.0-0', 'vuex' => '^4.0.0-0'} Map (10) {' @ vue/cli - plugin - Babel '= >' ~ 4.5.0 ', '@ vue/cli - plugin - eslint' = > '~ 4.5.0', '@ vue/cli - service' = > '~ 4.5.0. '@ vue/compiler - SFC' = > '^ 3.0.0', '@ vue/eslint - config - prettier' = > '^ 6.0.0', 'Babel - eslint' = > '^ 10.1.0', 'eslint' = > '^ 6.7.2', 'eslint - plugin - prettier' = > '^ 3.3.1', 'eslint - plugin - vue' = > '^ 7.0.0', 'prettier' = > '^ 2.2.1'}, Name: 'myapp'}} succeed done!Copy the code
-
The next step is to generate the template
Create a build. Ts
The installation depends on NPM i-s mkdirp ejs@types/ejS
const path = require("path"); const mkdirp = require("mkdirp"); import Package from "./Package"; import Project from "./Project"; import Directory from "./Directory"; import * as ejs from "ejs"; import * as fs from "fs-extra"; function pipeAsyncFunctions( ... fns: ((arg: any) => any)[] ): (arg: any) => Promise<any> { return (arg: any) => fns.reduce((p, f) => p.then(f), Promise.resolve(arg)); } async function renderFile(file: string, template: string, project: Project) { const content: string = ejs.render(template, { project }, { async: false }); await fs.writeFile(file, content); } async function createDirectory( current: Directory, project: Project, parent? : string ): Promise<void> { const dir = path.join(parent || "", current.name); await fs.ensureDir(dir); // log(' create directory: %s', dir) if (current.files.size! == 0) { const all = Array.from(current.files).map((item) => async () => { const file = path.join(dir, item[0]); // log(" create file: %s", file); await fs.ensureFile(file); await renderFile( file, typeof item[1] === "string" ? item[1] : await item[1](), project ); }); await pipeAsyncFunctions(... all)(true); } if (current.children.length ! == 0) { const all = current.children.map((child) => () => createDirectory(child, project, dir) ); await pipeAsyncFunctions(... all)(true); }} Async function createPackage(dir: string, PKG: Package) {console.log(" Create file: package.json"); const file = path.join(dir, "package.json"); await fs.ensureFile(file); await fs.writeFile(file, JSON.stringify(pkg, null, 2)); } async function createFolder(name: string) { const pwd = path.join(process.cwd(), name); Mkdirp (PWD). Then (() = > {the console. The log (" folder to create "the PWD); }); } async function build(project: Project): Promise<void> { await createFolder(project.name); await createDirectory(project.dirTree, project); await createPackage(project.dirTree.name, project.getPackage()); } export default build ;Copy the code
- Then modify index.js, pass the project object into the build method, execute, and generate the following structure
- myapp
- public
- index.html
- src
- router
- index.js
- router
- views
- Home.vue
- App.vue
- main.js
- package.json
- public
- myapp
#! /usr/bin/env node import WebProject from "./src/projects/WebProject"; import * as fs from "fs-extra"; import Package from "./src/core/Package"; import build from './src/core/build' // const fs = require("fs-extra"); const path = require("path"); const ora = require("ora"); const inquirer = require("inquirer"); const program = require("commander"); Program //.version('1.0.0').command("create <app-name>").description("create a new project").action(async (name: string) => { const cwd = process.cwd(); Const questions = [{type: "list", message: "Please select item type: ", name: "type", choices: [{name:" Web item ", value: "web" }], }, ]; const { type } = await inquirer.prompt(questions); const proce = ora("Start creating..." ); proce.start(); const folderName = path.join(cwd, name); const fileName = path.join(folderName, "package.json"); // doFs(folderName); const webProject = new WebProject(name, cwd); webProject.init(); // console.log("project",webProject) // writePackage(folderName, project.getPackage()); await build(webProject); proce.succeed("succeed done!" ); }); program.parse(process.argv);Copy the code
At this point, we can use Node to generate a template for our configuration. We can also add more controls, such as project configuration files, whether to support TS, using EJS in template files for conditional judgment, different VUE version control, different template selection generation
The optimization will follow, uploading the project to NPM, which can be installed and executed directly from NPM
Specific Demo code, I put in Git, you can go to pull down
Git address