npm init vite
Need to install the following packages:
create-vite
Ok to proceed? (y) y
√ Project name: ... vite-project
√ Select a framework: » react
√ Select a variant: » react
Scaffolding project in C:\aprepare\t1\vite-project...
Done. Now run:
cd vite-project
npm install
npm run dev
git clone https://github.com/vitejs/vite.git
cd vite
yarn install
packages\create-vite\index.js
.vscode\launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\packages\\create-vite\\index.js",
"args": ["create","vite-project"]
}
]
}
node
命令 create-react-appgitub
和gitee
仓库动态读取mkdir vite100
cd vite100
lerna init
node_modules
下,节省磁盘空间,且给了yarn更大的依赖优化空间{
"packages": [
"packages/*"
],
"version": "0.0.0",
+ "npmClient": "yarn",
+ "useWorkspaces": true
}
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^4.0.0"
},
+ "workspaces": [
+ "packages/*"
+ ]
}
lerna create @vite100/config -y //配置项
lerna create @vite100/create -y //创建项目
lerna create vite100 -y //核心命令
lerna create @vite100/settings -y //常量定义
lerna create @vite100/utils -y //工具方法
lerna create @vite100/cli-plugin-router -y //工具方法
packages\config\package.json
{
"dependencies": {
"@vite100/settings": "^0.0.0",
"@vite100/utils": "^0.0.0",
"fs-extra": "^10.0.0",
"userhome": "^1.0.0"
}
}
packages\create\package.json
{
"dependencies": {
"@vite100/settings": "^0.0.0",
"@vite100/utils": "^0.0.0",
"chalk": "^4.1.2",
"clone-git-repo": "^0.0.2",
"ejs": "^3.1.6",
"execa": "^5.1.1",
"fs-extra": "^10.0.0",
"glob": "^7.1.7",
"inquirer": "^8.1.2",
"isbinaryfile": "^4.0.8",
"vue-codemod": "^0.0.5"
}
}
packages\utils\package.json
{
"dependencies": {
"@vite100/settings": "^0.0.0",
"axios": "^0.21.2",
"cross-spawn": "^7.0.3",
"userhome": "^1.0.0",
"npmlog": "^5.0.1",
"ora": "^6.0.0",
"userhome": "^1.0.0"
}
}
packages\vite100\package.json
{
"dependencies": {
"@vite100/config": "^0.0.0",
"@vite100/create": "^0.0.0"
}
}
{
"publishConfig": {
"access": "public",
"registry": "http://registry.npmjs.org"
}
}
packages\vite100\package.json
{
"name": "vite100",
"version": "0.0.0",
"dependencies": {
"@vite100/config":"^0.0.0",
"@vite100/create":"^0.0.0"
},
+ "bin":{
+ "vite100": "index.js"
+ }
}
packages\vite100\index.js
#!/usr/bin/env node
async function main() {
let argv = process.argv.slice(2);
console.log(argv);
}
main().catch((err) => {
console.error(err);
});
#!/usr/bin/env node
再link,否则会用文本编辑器打开vite100\packages\vite100
目录中执行yarn unlink
,再重新linkyarn link
yarn global bin
C:\Users\zhangrenyang\AppData\Local\Yarn\bin
C:\Users\zhangrenyang\AppData\Local\Yarn\Data\link\vite100
npm bin -g
C:\Users\zhangrenyang\AppData\Roaming\npm
vite100 create vite-project
yarn workspace @vite100/config add userhome fs-extra
yarn workspace @vite100/utils add cross-spawn userhome fs-extra
packages\settings\index.js
//执行命令脚本
exports.COMMAND_SOURCE = `
const args = JSON.parse(process.argv[1]);
const factory = require('.');
factory(args);
`
//配置文件名称
exports.RC_NAME = ".vite100rc";
packages\utils\config.js
const userhome = require("userhome");
const fs = require("fs-extra");
const { RC_NAME } = require("@vite100/settings");
const configPath = userhome(RC_NAME);
let config = {};
if (fs.existsSync(configPath)) {
config = fs.readJSONSync(configPath);
}
config.configPath=configPath;
module.exports = config;
packages\utils\executeNodeScript.js
const spawn = require("cross-spawn");
async function executeNodeScript({ cwd }, source, args) {
return new Promise((resolve) => {
const childProcess = spawn(
process.execPath,
["-e", source, "--", JSON.stringify(args)],
{ cwd, stdio: "inherit" }
);
childProcess.on("close", resolve);
});
}
module.exports = executeNodeScript;
packages\utils\log.js
const log = require('npmlog');
log.heading = 'vite100';
module.exports = log;
packages\utils\index.js
exports.log = require('./log');
exports.executeNodeScript = require('./executeNodeScript');
exports.config = require('./config');
packages\config\command.js
const {executeNodeScript} = require('@vite100/utils');
const {COMMAND_SOURCE} = require('@vite100/settings');
const command = {
command: "config [key] [value]",
describe: "设置或查看配置项,比如GIT_TYPE设置仓库类型,ORG_NAME设置组织名",
builder: (yargs) => {},
handler:async function(argv){
//await executeNodeScript({ cwd: __dirname }, COMMAND_SOURCE,argv);
require('.')(argv);
},
};
module.exports = command;
packages\config\index.js
const fs = require("fs-extra");
const { log ,config} = require("@vite100/utils");
async function factory(argv) {
const { key, value } = argv;
if (key && value) {
config[key] = value;
await fs.writeJSON(config.configPath, config, { spaces: 2 });
log.info("vite100","(%s=%s)配置成功保存至%s", key, value, config.configPath);
}else if(key){
console.log('%s=%s',key, config[key]);
}else{
console.log(config);
}
}
module.exports = factory;
packages\vite100\index.js
#!/usr/bin/env node
const yargs = require("yargs/yargs");
const configCmd = require("@vite100/config/command");
async function main() {
const cli = yargs();
cli
.usage(`Usage: vite100 <command> [options]`)
.demandCommand(1, "至少需要一个命令")
.strict()
.recommendCommands()
+ .command(configCmd)
.parse(process.argv.slice(2));
}
main().catch((err) => {
console.error(err);
});
packages\create\command.js
const {COMMAND_SOURCE} = require('@vite100/settings');
const {executeNodeScript} = require('@vite100/utils');
const command = {
command: "create <name>",
describe: "创建项目",
builder: (yargs) => {
yargs.positional("name", {
type: "string",
describe: "项目名称",
});
},
handler:async function(argv){
let args = {projectName:argv.name,workingDirectory:process.cwd()};
//await executeNodeScript({ cwd: __dirname }, COMMAND_SOURCE,args);
require('.')(args);
}
};
module.exports = command;
packages\create\index.js
const path = require("path");
const fs = require("fs-extra");
const execa = require("execa");
const { red } = require("chalk");
const { config, log } = require("@vite100/utils");
async function create(argv) {
const { workingDirectory, projectName } = argv;
const { GIT_TYPE, ORG_NAME } = config;
if (!GIT_TYPE) {
throw new Error(red("X") + " 尚未配置仓库类型!");
}
if (!ORG_NAME) {
throw new Error(red("X") + " 尚未配置组织名称!");
}
const projectDir = path.join(workingDirectory, projectName);
log.info("vite100", "创建的项目目录为%s", projectDir);
}
module.exports = (...args) => {
return create(...args).catch(err => {
console.error(err);
});
};
packages\vite100\index.js
#!/usr/bin/env node
const yargs = require("yargs/yargs");
const configCmd = require("@vite100/config/command");
+const createCmd = require("@vite100/create/command");
async function main() {
const cli = yargs();
cli
.usage(`Usage: vite100 <command> [options]`)
.demandCommand(1, "至少需要一个命令")
.strict()
.recommendCommands()
+ .command(createCmd)
.command(configCmd)
.parse(process.argv.slice(2));
}
main().catch((err) => {
console.error(err);
});
packages\create\lib\promptModules\router.js
module.exports = cli => {
cli.injectFeature({
name: 'Router',
value: 'router',
description: '请选择路由模式',
link: 'https://www.npmjs.com/package/react-router-dom'
})
cli.injectPrompt({
name: 'historyMode',
when: answers => answers.features.includes('router'),
message: '请选择history的模式',
type: 'list',
choices: [
{
name: 'hash',
value: 'hash'
},
{
name: 'browser',
value: 'browser'
}
],
default: 'browser'
})
cli.injectPrompt({
name: 'appTitle',
when: answers => answers.features.includes('router'),
message: '请输入根组件的标题',
type: 'text',
default: 'AppTitle'
})
cli.onPromptComplete((answers, projectOptions) => {
if (answers.features.includes('router')) {
projectOptions.historyMode =answers.historyMode;
projectOptions.appTitle=answers.appTitle;
}
})
}
packages\create\lib\getPromptModules.js
function getPromptModules() {
return ['router'].map(file => require(`./promptModules/${file}`));
}
module.exports = getPromptModules;
packages\create\index.js
const path = require("path");
const { red } = require("chalk");
const { config, log } = require("@vite100/utils");
+const getPromptModules = require('./lib/getPromptModules');
async function create(argv) {
const { workingDirectory, projectName } = argv;
const { GIT_TYPE, ORG_NAME } = config;
if (!GIT_TYPE) {
throw new Error(red("X") + " 尚未配置仓库类型!");
}
if (!ORG_NAME) {
throw new Error(red("X") + " 尚未配置组织名称!");
}
const projectDir = path.join(workingDirectory, projectName);
log.info("vite100", "创建的项目目录为%s", projectDir);
+ let promptModules = getPromptModules();
+ console.info("选择项promptModules", promptModules);
}
module.exports = (...args) => {
return create(...args).catch(err => {
console.error(err);
});
};
packages\create\lib\PromptModuleAPI.js
class PromptModuleAPI {
constructor(creator) {
this.creator = creator;
}
//插入特性
injectFeature(feature) {
this.creator.featurePrompt.choices.push(feature)
}
//插入选项
injectPrompt(prompt) {
this.creator.injectedPrompts.push(prompt)
}
//选择完成后的回调
onPromptComplete(cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
module.exports = PromptModuleAPI;
packages\create\lib\Creator.js
const { prompt } = require("inquirer");
const PromptModuleAPI = require("./PromptModuleAPI");
const defaultFeaturePrompt = {
name: "features",
type: "checkbox",
message: "请选择项目特性:",
choices: [],
}
class Creator {
constructor(projectName, projectDir, promptModules) {
this.projectName = projectName;//项目名称
this.projectDir = projectDir;//项目路径
this.featurePrompt = defaultFeaturePrompt;//默认选项框
this.injectedPrompts = [];//插入插入的选择框
this.promptCompleteCbs = [];//选择结束之后的回调
const promptModuleAPI = new PromptModuleAPI(this);
promptModules.forEach((module) => module(promptModuleAPI));
}
async create() {
//获取选择项
const projectOptions = (this.projectOptions = await this.promptAndResolve());
console.log('projectOptions',projectOptions);
//{historyMode: 'browser',appTitle: 'AppTitle'}
}
async promptAndResolve() {
let prompts = [this.featurePrompt, ...this.injectedPrompts];
let answers = await prompt(prompts);
let projectOptions = {plugins: {},};
this.promptCompleteCbs.forEach((cb) => cb(answers, projectOptions));
return projectOptions;
}
}
module.exports = Creator;
packages\create\index.js
const path = require("path");
const { red } = require("chalk");
const { config, log } = require("@vite100/utils");
const getPromptModules = require('./lib/getPromptModules');
+const Creator = require('./lib/Creator');
async function create(argv) {
const { workingDirectory, projectName } = argv;
const { GIT_TYPE, ORG_NAME } = config;
if (!GIT_TYPE) {
throw new Error(red("X") + " 尚未配置仓库类型!");
}
if (!ORG_NAME) {
throw new Error(red("X") + " 尚未配置组织名称!");
}
const projectDir = path.join(workingDirectory, projectName);
log.info("vite100", "创建的项目目录为%s", projectDir);
let promptModules = getPromptModules();
console.info("选择项promptModules", promptModules);
+ let creator = new Creator(projectName, projectDir, promptModules);
+ await creator.create();
}
module.exports = (...args) => {
return create(...args).catch(err => {
console.error(err);
});
};
packages\create\lib\Creator.js
const { prompt } = require("inquirer");
+const fs = require("fs-extra");
+const { red } = require("chalk");
const PromptModuleAPI = require("./PromptModuleAPI");
+const { log } = require("@vite100/utils");
const defaultFeaturePrompt = {
name: "features",
type: "checkbox",
message: "请选择项目特性:",
choices: [],
}
class Creator {
constructor(projectName, projectDir, promptModules) {
this.projectName = projectName;//项目名称
this.projectDir = projectDir;//项目路径
this.featurePrompt = defaultFeaturePrompt;//默认选项框
this.injectedPrompts = [];//插入插入的选择框
this.promptCompleteCbs = [];//选择结束之后的回调
const promptModuleAPI = new PromptModuleAPI(this);
promptModules.forEach((module) => module(promptModuleAPI));
}
async create() {
//获取选择项
const projectOptions = (this.projectOptions = await this.promptAndResolve());
console.log('projectOptions', projectOptions);
//{historyMode: 'browser',appTitle: 'AppTitle'}
+ //准备项目目录
+ await this.prepareProjectDir();
}
+ async prepareProjectDir() {
+ let { projectDir } = this;
+ try {
+ await fs.access(projectDir);
+ const files = await fs.readdir(projectDir);
+ if (files.length > 0) {
+ const { overwrite } = await prompt({
+ type: "confirm",
+ name: "overwrite",
+ message: `目标目录非空,是否要移除存在的文件并继续?`,
+ });
+ if (overwrite) {
+ await fs.emptyDir(projectDir);
+ } else {
+ throw new Error(red("X") + " 操作被取消");
+ }
+ }
+ } catch (error) {
+ await fs.mkdirp(projectDir);
+ }
+ log.info("vite100", "%s目录已经准备就绪", projectDir);
+ }
async promptAndResolve() {
let prompts = [this.featurePrompt, ...this.injectedPrompts];
let answers = await prompt(prompts);
let projectOptions = { plugins: {}, };
this.promptCompleteCbs.forEach((cb) => cb(answers, projectOptions));
return projectOptions;
}
}
module.exports = Creator;
packages\utils\request.js
const axios = require("axios");
const { GIT_TYPE } = require("./config");
const GITEE = "https://gitee.com/api/v5";
const GITHUB = "https://api.github.com";
const BASE_URL = GIT_TYPE === "gitee" ? GITEE : GITHUB;
const request = axios.create({
baseURL: BASE_URL,
timeout: 5000,
});
request.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
return Promise.reject(error);
}
);
module.exports = request;
packages\utils\withLoading.js
async function withLoading(message, fn, ...args) {
const ora = await import("ora");
const spinner = ora.default(message);
spinner.start();
const result = await fn(...args);
spinner.succeed();
return result;
}
module.exports = withLoading;
packages\utils\index.js
exports.log = require('./log');
exports.executeNodeScript = require('./executeNodeScript');
exports.config = require('./config');
+exports.withLoading = require('./withLoading');
+exports.request = require('./request');
packages\settings\index.js
//执行命令脚本
exports.COMMAND_SOURCE = `
const args = JSON.parse(process.argv[1]);
const factory = require('.');
factory(args);
`
//配置文件名称
exports.RC_NAME = ".vite100rc";
+//模板存放名称
+exports.TEMPLATES = ".vite100_templates";
packages\create\lib\Creator.js
const { prompt } = require("inquirer");
const fs = require("fs-extra");
const { red } = require("chalk");
+const userhome = require("userhome");
+const {promisify} = require('util');
+const clone = promisify(require('clone-git-repo'));
const PromptModuleAPI = require("./PromptModuleAPI");
+const { log,config,withLoading ,request} = require("@vite100/utils");
+const { TEMPLATES } = require("@vite100/settings");
const defaultFeaturePrompt = {
name: "features",
type: "checkbox",
message: "请选择项目特性:",
choices: [],
}
class Creator {
constructor(projectName, projectDir, promptModules) {
this.projectName = projectName;//项目名称
this.projectDir = projectDir;//项目路径
this.featurePrompt = defaultFeaturePrompt;//默认选项框
this.injectedPrompts = [];//插入插入的选择框
this.promptCompleteCbs = [];//选择结束之后的回调
const promptModuleAPI = new PromptModuleAPI(this);
promptModules.forEach((module) => module(promptModuleAPI));
}
async create() {
//获取选择项
const projectOptions = (this.projectOptions = await this.promptAndResolve());
console.log('projectOptions', projectOptions);
//{historyMode: 'browser',appTitle: 'AppTitle'}
//准备项目目录
await this.prepareProjectDir();
+ //下载模板,给templateDir赋值
+ await this.downloadTemplate();
}
+ async downloadTemplate() {
+ const { GIT_TYPE, ORG_NAME } = config;
+ let repos = await withLoading("读取模板列表", async () =>
+ request.get(`/orgs/${ORG_NAME}/repos`)
+ );
+ let { repo } = await prompt({
+ name: "repo",
+ type: "list",
+ message: "请选择模板",
+ choices: repos.map((repo) => repo.name)
+ });
+ let tags = await withLoading("读取标签列表", async () =>
+ request.get(`/repos/${ORG_NAME}/${repo}/tags`)
+ );
+ let { tag } = await prompt({
+ name: "tag",
+ type: "list",
+ message: "请选择版本",
+ choices: tags,
+ });
+ let repository = GIT_TYPE + `:${ORG_NAME}/${repo}`;
+ if (tag) repository += `#${tag}`;
+ const downloadDirectory = userhome(TEMPLATES);
+ let templateDir = (this.templateDir = `${downloadDirectory}/${repo}/${tag}`);
+ log.info("vite3", "准备下载模板到%s", templateDir);
+ try {
+ await fs.access(templateDir);
+ } catch (error) {
+ log.info("vite100", "从仓库下载%s", repository);
+ await clone(repository, templateDir, { clone: true });
+ }
+ }
async prepareProjectDir() {
let { projectDir } = this;
try {
await fs.access(projectDir);
const files = await fs.readdir(projectDir);
if (files.length > 0) {
const { overwrite } = await prompt({
type: "confirm",
name: "overwrite",
message: `目标目录非空,是否要移除存在的文件并继续?`,
});
if (overwrite) {
await fs.emptyDir(projectDir);
} else {
throw new Error(red("X") + " 操作被取消");
}
}
} catch (error) {
await fs.mkdirp(projectDir);
}
log.info("vite100", "%s目录已经准备就绪", projectDir);
}
async promptAndResolve() {
let prompts = [this.featurePrompt, ...this.injectedPrompts];
let answers = await prompt(prompts);
let projectOptions = { plugins: {}, };
this.promptCompleteCbs.forEach((cb) => cb(answers, projectOptions));
return projectOptions;
}
}
module.exports = Creator;
packages\create\lib\Creator.js
const { prompt } = require("inquirer");
const fs = require("fs-extra");
const { red } = require("chalk");
const userhome = require("userhome");
const {promisify} = require('util');
+const execa = require("execa");
const clone = promisify(require('clone-git-repo'));
const PromptModuleAPI = require("./PromptModuleAPI");
const { log,config,withLoading ,request} = require("@vite100/utils");
const { TEMPLATES } = require("@vite100/settings");
const defaultFeaturePrompt = {
name: "features",
type: "checkbox",
message: "请选择项目特性:",
choices: [],
}
class Creator {
constructor(projectName, projectDir, promptModules) {
this.projectName = projectName;//项目名称
this.projectDir = projectDir;//项目路径
this.featurePrompt = defaultFeaturePrompt;//默认选项框
this.injectedPrompts = [];//插入插入的选择框
this.promptCompleteCbs = [];//选择结束之后的回调
const promptModuleAPI = new PromptModuleAPI(this);
promptModules.forEach((module) => module(promptModuleAPI));
}
async create() {
//获取选择项
const projectOptions = (this.projectOptions = await this.promptAndResolve());
console.log('projectOptions', projectOptions);
//{historyMode: 'browser',appTitle: 'AppTitle'}
//准备项目目录
await this.prepareProjectDir();
//下载模板,给templateDir赋值
await this.downloadTemplate();
//把项目拷贝到模板中
+ await fs.copy(this.templateDir, this.projectDir);
+ //初始化git仓库
+ await execa("git", ["init"], { cwd: this.projectDir, stdio: "inherit" });
+ log.info("vite100", "在%s安装依赖", this.projectDir);
+ await execa("npm", ["install"], { cwd: this.projectDir, stdio: "inherit" });
}
async downloadTemplate() {
const { GIT_TYPE, ORG_NAME } = config;
let repos = await withLoading("读取模板列表", async () =>
request.get(`/orgs/${ORG_NAME}/repos`)
);
let { repo } = await prompt({
name: "repo",
type: "list",
message: "请选择模板",
choices: repos.map((repo) => repo.name)
});
let tags = await withLoading("读取标签列表", async () =>
request.get(`/repos/${ORG_NAME}/${repo}/tags`)
);
let { tag } = await prompt({
name: "tag",
type: "list",
message: "请选择版本",
choices: tags,
});
let repository = GIT_TYPE + `:${ORG_NAME}/${repo}`;
if (tag) repository += `#${tag}`;
const downloadDirectory = userhome(TEMPLATES);
let templateDir = (this.templateDir = `${downloadDirectory}/${repo}/${tag}`);
log.info("vite3", "准备下载模板到%s", templateDir);
try {
await fs.access(templateDir);
} catch (error) {
log.info("vite100", "从仓库下载%s", repository);
await clone(repository, templateDir, { clone: true });
}
}
async prepareProjectDir() {
let { projectDir } = this;
try {
await fs.access(projectDir);
const files = await fs.readdir(projectDir);
if (files.length > 0) {
const { overwrite } = await prompt({
type: "confirm",
name: "overwrite",
message: `目标目录非空,是否要移除存在的文件并继续?`,
});
if (overwrite) {
await fs.emptyDir(projectDir);
} else {
throw new Error(red("X") + " 操作被取消");
}
}
} catch (error) {
await fs.mkdirp(projectDir);
}
log.info("vite100", "%s目录已经准备就绪", projectDir);
}
async promptAndResolve() {
let prompts = [this.featurePrompt, ...this.injectedPrompts];
let answers = await prompt(prompts);
let projectOptions = { plugins: {}, };
this.promptCompleteCbs.forEach((cb) => cb(answers, projectOptions));
return projectOptions;
}
}
module.exports = Creator;
packages\create\index.js
const path = require("path");
const { red } = require("chalk");
+const execa = require("execa");
const { config, log } = require("@vite100/utils");
const getPromptModules = require('./lib/getPromptModules');
const Creator = require('./lib/Creator');
async function create(argv) {
const { workingDirectory, projectName } = argv;
const { GIT_TYPE, ORG_NAME } = config;
if (!GIT_TYPE) {
throw new Error(red("X") + " 尚未配置仓库类型!");
}
if (!ORG_NAME) {
throw new Error(red("X") + " 尚未配置组织名称!");
}
const projectDir = path.join(workingDirectory, projectName);
log.info("vite100", "创建的项目目录为%s", projectDir);
let promptModules = getPromptModules();
console.info("选择项promptModules", promptModules);
let creator = new Creator(projectName, projectDir, promptModules);
await creator.create();
+ log.info("vite100", "启动服务");
+ await execa("npm", ["run", "dev"], { cwd: projectDir, stdio: "inherit" });
}
module.exports = (...args) => {
return create(...args).catch(err => {
console.error(err);
});
};
packages\create\lib\promptModules\router.js
module.exports = cli => {
cli.injectFeature({
name: 'Router',
value: 'router',
description: '请选择路由模式',
link: 'https://www.npmjs.com/package/react-router-dom'
})
cli.injectPrompt({
name: 'historyMode',
when: answers => answers.features.includes('router'),
message: '请选择history的模式',
type: 'list',
choices: [
{
name: 'hash',
value: 'hash'
},
{
name: 'browser',
value: 'browser'
}
],
default: 'browser'
})
cli.injectPrompt({
name: 'appTitle',
when: answers => answers.features.includes('router'),
message: '请输入根组件的标题',
type: 'text',
default: 'AppTitle'
})
cli.onPromptComplete((answers, projectOptions) => {
if (answers.features.includes('router')) {
+ projectOptions.plugins['cli-plugin-router'] = {
+ historyMode: answers.historyMode
+ }
projectOptions.historyMode =answers.historyMode;
projectOptions.appTitle=answers.appTitle;
}
})
}
packages\create\lib\Creator.js
const { prompt } = require("inquirer");
+const path = require("path");
const fs = require("fs-extra");
const { red } = require("chalk");
const userhome = require("userhome");
const {promisify} = require('util');
const execa = require("execa");
const clone = promisify(require('clone-git-repo'));
const PromptModuleAPI = require("./PromptModuleAPI");
const { log,config,withLoading ,request} = require("@vite100/utils");
const { TEMPLATES } = require("@vite100/settings");
const defaultFeaturePrompt = {
name: "features",
type: "checkbox",
message: "请选择项目特性:",
choices: [],
}
class Creator {
constructor(projectName, projectDir, promptModules) {
this.projectName = projectName;//项目名称
this.projectDir = projectDir;//项目路径
this.featurePrompt = defaultFeaturePrompt;//默认选项框
this.injectedPrompts = [];//插入插入的选择框
this.promptCompleteCbs = [];//选择结束之后的回调
+ this.plugins = [];//插件
const promptModuleAPI = new PromptModuleAPI(this);
promptModules.forEach((module) => module(promptModuleAPI));
}
async create() {
//获取选择项
const projectOptions = (this.projectOptions = await this.promptAndResolve());
console.log('projectOptions', projectOptions);
//{historyMode: 'browser',appTitle: 'AppTitle'}
//准备项目目录
await this.prepareProjectDir();
//下载模板,给templateDir赋值
await this.downloadTemplate();
//把项目拷贝到模板中
await fs.copy(this.templateDir, this.projectDir);
+ const pkgPath = path.join(this.projectDir, 'package.json');
+ let pkg = (this.pkg = await fs.readJSON(pkgPath));
+ const deps = Reflect.ownKeys(projectOptions.plugins);
+ deps.forEach(dep => pkg.devDependencies[dep] = `latest`);
+ await fs.writeJSON(pkgPath,pkg,{spaces:2});
//初始化git仓库
await execa("git", ["init"], { cwd: this.projectDir, stdio: "inherit" });
log.info("vite100", "在%s安装依赖", this.projectDir);
await execa("npm", ["install"], { cwd: this.projectDir, stdio: "inherit" });
}
async downloadTemplate() {
const { GIT_TYPE, ORG_NAME } = config;
let repos = await withLoading("读取模板列表", async () =>
request.get(`/orgs/${ORG_NAME}/repos`)
);
let { repo } = await prompt({
name: "repo",
type: "list",
message: "请选择模板",
choices: repos.map((repo) => repo.name)
});
let tags = await withLoading("读取标签列表", async () =>
request.get(`/repos/${ORG_NAME}/${repo}/tags`)
);
let { tag } = await prompt({
name: "tag",
type: "list",
message: "请选择版本",
choices: tags,
});
let repository = GIT_TYPE + `:${ORG_NAME}/${repo}`;
if (tag) repository += `#${tag}`;
const downloadDirectory = userhome(TEMPLATES);
let templateDir = (this.templateDir = `${downloadDirectory}/${repo}/${tag}`);
log.info("vite3", "准备下载模板到%s", templateDir);
try {
await fs.access(templateDir);
} catch (error) {
log.info("vite100", "从仓库下载%s", repository);
await clone(repository, templateDir, { clone: true });
}
}
async prepareProjectDir() {
let { projectDir } = this;
try {
await fs.access(projectDir);
const files = await fs.readdir(projectDir);
if (files.length > 0) {
const { overwrite } = await prompt({
type: "confirm",
name: "overwrite",
message: `目标目录非空,是否要移除存在的文件并继续?`,
});
if (overwrite) {
await fs.emptyDir(projectDir);
} else {
throw new Error(red("X") + " 操作被取消");
}
}
} catch (error) {
await fs.mkdirp(projectDir);
}
log.info("vite100", "%s目录已经准备就绪", projectDir);
}
async promptAndResolve() {
let prompts = [this.featurePrompt, ...this.injectedPrompts];
let answers = await prompt(prompts);
let projectOptions = { plugins: {}, };
this.promptCompleteCbs.forEach((cb) => cb(answers, projectOptions));
return projectOptions;
}
}
module.exports = Creator;
packages\utils\mergeDeps.js
function mergeDeps(sourceDeps, depsToInject){
let result = Object.assign({}, sourceDeps);
for (const depName in depsToInject) {
result[depName] = depsToInject[depName];
}
return result;
}
module.exports = mergeDeps;
packages\utils\loadModule.js
const path = require('path');
const Module = require('module');
function loadModule(request,context){
return Module.createRequire(path.resolve(context,'package.json'))(request);
}
module.exports = loadModule;
packages\utils\extractCallDir.js
const path = require('path');
function extractCallDir(){
const obj = {}
Error.captureStackTrace(obj)
const callSite = obj.stack.split('\n')[3]
const namedStackRegExp = /\s\((.*):\d+:\d+\)$/
let matchResult = callSite.match(namedStackRegExp)
const fileName = matchResult[1]
return path.dirname(fileName)
}
module.exports = extractCallDir;
packages\utils\writeFileTree.js
const path = require('path');
const fs = require('fs-extra');
async function writeFileTree(projectDir, files) {
Object.keys(files).forEach(file => {
let content = files[file];
if (file.endsWith('.ejs')) file = file.slice(0, -4);
const filePath = path.join(projectDir, file);
fs.ensureDirSync(path.dirname(filePath));//先确保文件所在的目录 是存在
fs.writeFileSync(filePath,content);
});
}
module.exports = writeFileTree;
packages\utils\index.js
+const path = require('path');
exports.log = require('./log');
exports.executeNodeScript = require('./executeNodeScript');
exports.config = require('./config');
exports.withLoading = require('./withLoading');
exports.request = require('./request');
+exports.loadModule = require('./loadModule');
+exports.mergeDeps = require('./mergeDeps');
+exports.extractCallDir = require('./extractCallDir');
+exports.writeFileTree = require('./writeFileTree');
+exports.isObject = val => typeof val === 'object';
+exports.isString = val => typeof val === 'string';
packages\create\lib\GeneratorAPI.js
const fs = require('fs');
const ejs = require('ejs');
const path = require('path');
const { promisify } = require('util');
const glob = promisify(require('glob'));
const { isBinaryFile } = require('isbinaryfile');
const { runTransformation } = require('vue-codemod')
const { isObject, isString, extractCallDir, mergeDeps } = require("@vite100/utils");
class GeneratorAPI {
constructor(id, creator, projectOptions) {
this.id = id;
this.creator = creator;
this.projectOptions = projectOptions;
}
//插入文件处理中间件
async _injectFileMiddleware(middleware) {
this.creator.fileMiddlewares.push(middleware);
}
//渲染拷贝文件
render(source) {
const baseDir = extractCallDir();
if (isString(source)) {
source = path.resolve(baseDir, source)
this._injectFileMiddleware(async (files, projectOptions) => {
const templateFiles = await glob('**/*', { cwd: source, nodir: true });
for (let i = 0; i < templateFiles.length; i++) {
let templateFile = templateFiles[i];
files[templateFile] = await renderFile(path.resolve(source, templateFile), projectOptions);
}
});
}
}
//扩展依赖包
extendPackage(toMerge) {
const pkg = this.creator.pkg;
for (const key in toMerge) {
const value = toMerge[key];
let existing = pkg[key];
if (isObject(value) && (key === 'dependencies' || key === 'devDependencies')) {
pkg[key] = mergeDeps(existing || {}, value);
} else {
pkg[key] = value;
}
}
}
//转译脚本
transformScript(file, codemod, projectOptions = {}) {
this._injectFileMiddleware((files) => {
files[file] = runTransformation(
{
path: file,
source: files[file]
},
codemod,
projectOptions
)
})
}
//插入导入语句
injectImport(file, newImport) {
const imports = (this.creator.imports[file] = this.creator.imports[file] || []);
imports.push(newImport)
}
//入口文件的路径
get entryFile() {
return 'src/index.js';
}
}
//渲染文件
async function renderFile(templatePath, projectOptions) {
if (await isBinaryFile(templatePath)) {
return fs.readFileSync(templatePath);
}
let template = fs.readFileSync(templatePath, 'utf8');
return ejs.render(template, projectOptions);
}
module.exports = GeneratorAPI;
packages\create\lib\codemods\injectImports.js
function injectImports(fileInfo, api, { imports }) {
const jscodeshift = api.jscodeshift
const root = jscodeshift(fileInfo.source)
const declarations = root.find(jscodeshift.ImportDeclaration)
const toImportAST = imp => jscodeshift(`${imp}\n`).nodes()[0].program.body[0]
const importASTNodes = imports.map(toImportAST);
if (declarations.length) {
declarations.at(-1).insertAfter(importASTNodes)
} else {
root.get().node.program.body.unshift(...importASTNodes)
}
return root.toSource()
}
module.exports = injectImports;
packages\create\lib\Creator.js
const { prompt } = require("inquirer");
const path = require("path");
const fs = require("fs-extra");
const { red } = require("chalk");
const userhome = require("userhome");
const {promisify} = require('util');
const execa = require("execa");
+const glob = promisify(require('glob'));
const clone = promisify(require('clone-git-repo'));
+const { runTransformation } = require('vue-codemod')
const PromptModuleAPI = require("./PromptModuleAPI");
+const GeneratorAPI = require('./GeneratorAPI');
+const { isBinaryFile } = require('isbinaryfile');
+const { log,config,withLoading ,request,loadModule,writeFileTree} = require("@vite100/utils");
const { TEMPLATES } = require("@vite100/settings");
const defaultFeaturePrompt = {
name: "features",
type: "checkbox",
message: "请选择项目特性:",
choices: [],
}
class Creator {
constructor(projectName, projectDir, promptModules) {
this.projectName = projectName;//项目名称
this.projectDir = projectDir;//项目路径
this.featurePrompt = defaultFeaturePrompt;//默认选项框
this.injectedPrompts = [];//插入插入的选择框
this.promptCompleteCbs = [];//选择结束之后的回调
this.plugins = [];//插件
+ this.fileMiddlewares = [];//文件中间件
+ this.files = {};//最终输出的文件列表
+ this.pkg = {};//我描述内容
+ this.imports={};//额外的导入语句
const promptModuleAPI = new PromptModuleAPI(this);
promptModules.forEach((module) => module(promptModuleAPI));
}
async create() {
//获取选择项
const projectOptions = (this.projectOptions = await this.promptAndResolve());
console.log('projectOptions', projectOptions);
//{historyMode: 'browser',appTitle: 'AppTitle'}
//准备项目目录
await this.prepareProjectDir();
//下载模板,给templateDir赋值
await this.downloadTemplate();
//把项目拷贝到模板中
await fs.copy(this.templateDir, this.projectDir);
const pkgPath = path.join(this.projectDir, 'package.json');
let pkg = (this.pkg = await fs.readJSON(pkgPath));
const deps = Reflect.ownKeys(projectOptions.plugins);
deps.forEach(dep => pkg.devDependencies[dep] = `latest`);
await fs.writeJSON(pkgPath,pkg,{spaces:2});
//初始化git仓库
await execa("git", ["init"], { cwd: this.projectDir, stdio: "inherit" });
log.info("vite100", "在%s安装依赖", this.projectDir);
await execa("npm", ["install"], { cwd: this.projectDir, stdio: "inherit" });
+ //解析插件,拿到插件的generator方法
+ const resolvedPlugins = await this.resolvePlugins(projectOptions.plugins);
+ //应用插件
+ await this.applyPlugins(resolvedPlugins);
+ await this.initFiles();
+ //准备文件内容
+ await this.renderFiles();
+ //删除插件的依赖
+ deps.forEach(dep => delete pkg.devDependencies[dep]);
+ this.files['package.json'] = JSON.stringify(pkg,null,2);
+ //把文件写入硬盘
+ await writeFileTree(this.projectDir, this.files);
+ //重新安装额外的依赖
+ await execa("npm", ["install"], { cwd: this.projectDir, stdio: "inherit" });
}
+ async initFiles(){
+ const projectFiles = await glob('**/*', { cwd: this.projectDir, nodir: true });
+ for (let i = 0; i < projectFiles.length; i++) {
+ let projectFile = projectFiles[i];
+ let projectFilePath = path.join(this.projectDir,projectFile);
+ let content;
+ if (await isBinaryFile(projectFilePath)) {
+ content = await fs.readFile(projectFilePath);
+ }else{
+ content = await fs.readFile(projectFilePath,'utf8');
+ }
+ this.files[projectFile] = content;
+ }
+ }
+ async renderFiles() {
+ const {files,projectOptions} = this;
+ for (const middleware of this.fileMiddlewares) {
+ await middleware(files,projectOptions);
+ }
+ Object.keys(files).forEach(file => {
+ let imports = this.imports[file]
+ if (imports && imports.length > 0) {
+ files[file] = runTransformation(
+ { path: file, source: files[file] },
+ require('./codemods/injectImports'),
+ { imports }
+ )
+ }
+ })
+ }
+ async resolvePlugins(rawPlugins) {
+ const plugins = [];
+ for (const id of Reflect.ownKeys(rawPlugins)) {
+ const apply = loadModule(`${id}/generator`, this.projectDir);
+ let options = rawPlugins[id];
+ plugins.push({ id, apply, options });
+ }
+ return plugins;
+ }
+ async applyPlugins(plugins) {
+ for (const plugin of plugins) {
+ const { id, apply, options } = plugin;
+ const generatorAPI = new GeneratorAPI(id, this, options);
+ await apply(generatorAPI, options);
+ }
+ }
async downloadTemplate() {
const { GIT_TYPE, ORG_NAME } = config;
let repos = await withLoading("读取模板列表", async () =>
request.get(`/orgs/${ORG_NAME}/repos`)
);
let { repo } = await prompt({
name: "repo",
type: "list",
message: "请选择模板",
choices: repos.map((repo) => repo.name)
});
let tags = await withLoading("读取标签列表", async () =>
request.get(`/repos/${ORG_NAME}/${repo}/tags`)
);
let { tag } = await prompt({
name: "tag",
type: "list",
message: "请选择版本",
choices: tags,
});
let repository = GIT_TYPE + `:${ORG_NAME}/${repo}`;
if (tag) repository += `#${tag}`;
const downloadDirectory = userhome(TEMPLATES);
let templateDir = (this.templateDir = `${downloadDirectory}/${repo}/${tag}`);
log.info("vite3", "准备下载模板到%s", templateDir);
try {
await fs.access(templateDir);
} catch (error) {
log.info("vite100", "从仓库下载%s", repository);
await clone(repository, templateDir, { clone: true });
}
}
async prepareProjectDir() {
let { projectDir } = this;
try {
await fs.access(projectDir);
const files = await fs.readdir(projectDir);
if (files.length > 0) {
const { overwrite } = await prompt({
type: "confirm",
name: "overwrite",
message: `目标目录非空,是否要移除存在的文件并继续?`,
});
if (overwrite) {
await fs.emptyDir(projectDir);
} else {
throw new Error(red("X") + " 操作被取消");
}
}
} catch (error) {
await fs.mkdirp(projectDir);
}
log.info("vite100", "%s目录已经准备就绪", projectDir);
}
async promptAndResolve() {
let prompts = [this.featurePrompt, ...this.injectedPrompts];
let answers = await prompt(prompts);
let projectOptions = { plugins: {}, };
this.promptCompleteCbs.forEach((cb) => cb(answers, projectOptions));
return projectOptions;
}
}
module.exports = Creator;
packages\cli-plugin-router\template\src\App.js.ejs
import React from 'react';
function App(){
return <div><%=appTitle%></div>
}
export default App;
packages\cli-plugin-router\template\src\routesConfig.js
import App from './App';
export default [
{
path: '/',
component: App
}
]
packages\cli-plugin-router\generator.js
module.exports = async (api, options) => {
api.render('./template');
api.injectImport(api.entryFile,
`import {${options.historyMode === 'hash' ? 'HashRouter' : 'BrowserRouter'} as Router,Route} from 'react-router-dom'`);
api.injectImport(api.entryFile,
`import routesConfig from './routesConfig'`);
api.injectImport(api.entryFile,
`import { renderRoutes } from "react-router-config"`);
api.transformScript(api.entryFile, require('./injectRouter'))
api.extendPackage({
dependencies: {
'react-router-dom': 'latest',
'react-router-config': 'latest'
}
});
}
packages\cli-plugin-router\injectRouter.js
module.exports = (file, api) => {
const jscodeshift = api.jscodeshift
const root = jscodeshift(file.source)
const appImportDeclaration = root.find(jscodeshift.ImportDeclaration, (node) => {
if(node.specifiers[0].local.name === 'App'){
return true
}
})
if(appImportDeclaration)
appImportDeclaration.remove();
const appJSXElement = root.find(jscodeshift.JSXElement, (node) => {
if (node.openingElement.name.name === 'App') {
return true
}
})
if(appJSXElement)
appJSXElement.replaceWith(({ node }) => {
return jscodeshift.jsxElement(
jscodeshift.jsxOpeningElement(jscodeshift.jsxIdentifier('Router')), jscodeshift.jsxClosingElement(jscodeshift.jsxIdentifier('Router')), [
jscodeshift.jsxExpressionContainer(
jscodeshift.callExpression(jscodeshift.identifier('renderRoutes'),[jscodeshift.identifier('routesConfig')])
)
], false
);
})
return root.toSource()
}
package.json
{
"publishConfig": {
"access": "public",
"registry": "http://registry.npmjs.org"
}
}
npm whoami
npm login
zhangrenyang2000
lerna publish
命令 | 功能 |
---|---|
lerna bootstrap | 安装依赖 |
lerna clean | 删除各个包下的node_modules |
lerna init | 创建新的lerna库 |
lerna list | 查看本地包列表 |
lerna changed | 显示自上次release tag以来有修改的包, 选项通 list |
lerna diff | 显示自上次release tag以来有修改的包的差异, 执行 git diff |
lerna exec | 在每个包目录下执行任意命令 |
lerna run | 执行每个包package.json中的脚本命令 |
lerna add | 添加一个包的版本为各个包的依赖 |
lerna import | 引入package |
lerna link | 链接互相引用的库 |
lerna create | 新建package |
lerna publish | 发布 |
命令 | 说明 |
---|---|
yarn -v | 查看yarn版本 |
yarn config list | 查看yarn的所有配置 |
yarn config set registry https://registry.npm.taobao.org/ | 修改yarn的源镜像为淘宝源 |
yarn config set global-folder "D:\RTE\Yarn\global" | 修改全局安装目录, 先创建好目录(global), 我放在了Yarn安装目录下(D:\RTE\Yarn\global) |
yarn config set prefix "D:\RTE\Yarn\global\" | 修改全局安装目录的bin目录位置 |
yarn config set cache-folder "D:\RTE\Yarn\cache" | 修改全局缓存目录, 先创建好目录(cache), 和global放在同一层目录下 |
yarn config list | 查看所有配置 |
yarn global bin | 查看当前yarn的bin的位置 |
yarn global dir | 查看当前yarn的全局安装位置 |
作用 | 命令 |
---|---|
查看工作空间信息 | yarn workspaces info |
给所有的空间添加依赖 | yarn workspaces run add lodash |
给根空间添加依赖 | yarn add -W -D typescript jest |
给某个项目添加依赖 | yarn workspace create-react-app3 add commander |
删除所有的 node_modules | lerna clean 等于 yarn workspaces run clean |
安装和link所有的名 | yarn install 等于 lerna bootstrap --npm-client yarn --use-workspaces |
重新获取所有的 node_modules | yarn install --force |
查看缓存目录 | yarn cache dir |
清除本地缓存 | yarn cache clean |
在所有package中运行指定的命令 | yarn workspaces run |
const yargs = require("yargs/yargs");
const cli = yargs();
cli
.usage(`Usage: vite100 <command> [options]`)
.demandCommand(1, "至少需要一个命令")
.strict()
.recommendCommands()
.command({
command: "create <name>",
describe: "创建项目",
builder: (yargs) => {
yargs.positional("name", {
type: "string",
describe: "项目名称",
});
},
handler: async function (argv) {
console.log(argv);
//{ _: [ 'create' ], '$0': 'doc\\1.yargs.js', name: 'p1' }
}
})
.parse(process.argv.slice(2));
stdion: 'inherit'
,当执行代码时,子进程将会继承主进程的stdin、stdout和stderrnode -e "console.log(process.argv)" -- a b
node -e "console.log(JSON.parse(process.argv[1]))" -- "{\"name\":\"zhufeng\"}"
node -e "console.log(process.cwd())"
const spawn = require("cross-spawn");
async function executeNodeScript({ cwd }, source, args) {
return new Promise((resolve) => {
const childProcess = spawn(
process.execPath,
["-e", source, "--", JSON.stringify(args)],
{ cwd, stdio: "inherit" }
);
childProcess.on("close", resolve);
});
}
module.exports = executeNodeScript;
const clone = require('clone-git-repo');
let repository = 'gitee:zhufengtemplate/template-react#v1.0';
clone(repository,'./output', {clone:true},function (err) {
console.log(err);
})
let jscodeshift = require('jscodeshift');
const ast = jscodeshift(`import ReactDOM from "react-dom"`);
console.log(ast.nodes());
console.log(ast.nodes()[0]);
console.log(ast.nodes()[0].program);
console.log(ast.nodes()[0].program.body[0]);
const { runTransformation } = require('vue-codemod')
let file = 'index.js';
let source = `
import React from 'react';
`;
let imports = ['import ReactDOM from "react-dom"'];
let transformed = runTransformation(
{ path: file, source },
injectImports,
{ imports }
)
console.log(transformed);
function injectImports(fileInfo, api, { imports }) {
const jscodeshift = api.jscodeshift
const root = jscodeshift(fileInfo.source)
const declarations = root.find(jscodeshift.ImportDeclaration)
const toImportAST = imp => jscodeshift(`${imp}\n`).nodes()[0].program.body[0]
const importASTNodes = imports.map(toImportAST);
if (declarations.length) {
declarations.at(-1).insertAfter(importASTNodes)
} else {
root.get().node.program.body.unshift(...importASTNodes)
}
return root.toSource()
}