From 3b8af8aacba7afe370635f745e0a95f4784d491b Mon Sep 17 00:00:00 2001 From: harrywan <445436867@qq.com> Date: Mon, 27 Jun 2022 17:53:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=92=E4=BB=B6=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=92=8C=E6=A8=A1=E6=9D=BF=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 插件开发文档和模板 * docs: 更新文档 --- docs/reference/plugin/dev/README.md | 27 ++++ .../create-fes-app/src/generator/Plugin.js | 26 ++++ packages/create-fes-app/src/index.js | 19 ++- .../templates/plugin/.editorconfig | 16 ++ .../templates/plugin/.eslintrc.js | 24 +++ .../templates/plugin/.gitignore | 2 + .../templates/plugin/.prettierrc | 4 + .../templates/plugin/build.config.js | 4 + .../templates/plugin/package.json.tpl | 47 ++++++ .../templates/plugin/scripts/build.js | 145 ++++++++++++++++++ .../templates/plugin/scripts/compiler.js | 48 ++++++ .../templates/plugin/scripts/randomColor.js | 35 +++++ .../templates/plugin/src/index.js.tpl | 45 ++++++ .../templates/plugin/src/runtime/core.tpl.tpl | 1 + .../templates/plugin/src/runtime/runtime.js | 5 + 15 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 packages/create-fes-app/src/generator/Plugin.js create mode 100644 packages/create-fes-app/templates/plugin/.editorconfig create mode 100644 packages/create-fes-app/templates/plugin/.eslintrc.js create mode 100644 packages/create-fes-app/templates/plugin/.gitignore create mode 100644 packages/create-fes-app/templates/plugin/.prettierrc create mode 100644 packages/create-fes-app/templates/plugin/build.config.js create mode 100644 packages/create-fes-app/templates/plugin/package.json.tpl create mode 100644 packages/create-fes-app/templates/plugin/scripts/build.js create mode 100644 packages/create-fes-app/templates/plugin/scripts/compiler.js create mode 100644 packages/create-fes-app/templates/plugin/scripts/randomColor.js create mode 100644 packages/create-fes-app/templates/plugin/src/index.js.tpl create mode 100644 packages/create-fes-app/templates/plugin/src/runtime/core.tpl.tpl create mode 100644 packages/create-fes-app/templates/plugin/src/runtime/runtime.js diff --git a/docs/reference/plugin/dev/README.md b/docs/reference/plugin/dev/README.md index 964ea9c4..0564dcd6 100644 --- a/docs/reference/plugin/dev/README.md +++ b/docs/reference/plugin/dev/README.md @@ -36,6 +36,33 @@ API 对象是构建流程管理 Service 类的实例,api 提供一些有用的 - **enableBy**, 是否开启插件,可配置某些场景下禁用插件。 +## 创建插件 + +##### 第一步:安装`create-fes-app` +```bash +npm i -g @fesjs/create-fes-app +``` + + +##### 第二步:创建插件项目 + +```bash +create-fes-app pluginName +``` +在询问`Pick an template`时选择`Plugin`! + +##### 第三步:进入插件目录 & 安装依赖 +```bash +cd pluginName & yarn +``` + +##### 第四步:启动编译 +```bash +yarn dev +``` + +##### 第五步:使用插件API完成你的插件!(可以参考其他插件理解api用法和场景) + ## 发布到 npm 以 `@fesjs/preset-`、`@fesjs/plugin-`、`@webank/fes-preset-`、`@webank/fes-plugin-`、`fes-preset-` 和 `fes-plugin-` 开头的依赖会被 Fes.js 自动注册为插件或插件集。 diff --git a/packages/create-fes-app/src/generator/Plugin.js b/packages/create-fes-app/src/generator/Plugin.js new file mode 100644 index 00000000..5ac20d5d --- /dev/null +++ b/packages/create-fes-app/src/generator/Plugin.js @@ -0,0 +1,26 @@ +import { Generator } from '@fesjs/utils'; + +export default class AppGenerator extends Generator { + constructor({ + cwd, args, path, targetDir, name + }) { + super({ + cwd, + args + }); + this.path = path; + this.targetDir = targetDir; + this.name = name; + } + + async writing() { + this.copyDirectory({ + context: { + version: require('../../package.json').version, + name: this.name + }, + path: this.path, + target: this.targetDir + }); + } +} diff --git a/packages/create-fes-app/src/index.js b/packages/create-fes-app/src/index.js index b7ece75e..476de238 100644 --- a/packages/create-fes-app/src/index.js +++ b/packages/create-fes-app/src/index.js @@ -6,6 +6,7 @@ import inquirer from 'inquirer'; import { clearConsole } from './utils'; import AppGenerator from './generator/App'; +import PluginGenerator from './generator/Plugin'; export default async ({ cwd, args }) => { if (args.proxy) { @@ -75,12 +76,13 @@ export default async ({ cwd, args }) => { choices: [ { name: 'PC, suitable for management desk front-end applications', value: 'pc' }, { name: 'H5, suitable for mobile applications', value: 'h5' }, + { name: 'Plugin, suitable for fes plugin', value: 'plugin' }, { name: 'Cancel', value: false } ] } ]); - if (template) { + if (template === 'pc' || template === 'h5') { const generator = new AppGenerator({ cwd, args, @@ -94,5 +96,20 @@ export default async ({ cwd, args }) => { console.log('$ yarn'); console.log('$ yarn dev'); console.log(); + } else if (template === 'plugin') { + const generator = new PluginGenerator({ + cwd, + args, + targetDir, + path: path.join(__dirname, '../templates/plugin'), + name + }); + await generator.run(); + console.log(); + console.log(chalk.green(`plugin ${projectName} created successfully, please execute the following command to use:`)); + console.log(`$ cd ${projectName}`); + console.log('$ yarn'); + console.log('$ yarn dev'); + console.log(); } }; diff --git a/packages/create-fes-app/templates/plugin/.editorconfig b/packages/create-fes-app/templates/plugin/.editorconfig new file mode 100644 index 00000000..4160ac31 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/.editorconfig @@ -0,0 +1,16 @@ +# http://editorconfig.org + +root = true +lib + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/packages/create-fes-app/templates/plugin/.eslintrc.js b/packages/create-fes-app/templates/plugin/.eslintrc.js new file mode 100644 index 00000000..4b7aabb3 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/.eslintrc.js @@ -0,0 +1,24 @@ + +module.exports = { + extends: [ + '@webank/eslint-config-webank/vue.js' + ], + globals: { + // 这里填入你的项目需要的全局变量 + // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: + // + // Vue: false + __DEV__: false + }, + rules: { + 'vue/comment-directive': 'off', + 'global-require': 'off', + 'import/no-unresolved': 'off', + 'no-restricted-syntax': 'off', + 'no-undefined': 'off', + 'vue/valid-template-root': 'off' + }, + env: { + jest: true + } +}; diff --git a/packages/create-fes-app/templates/plugin/.gitignore b/packages/create-fes-app/templates/plugin/.gitignore new file mode 100644 index 00000000..9b26ed04 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/.gitignore @@ -0,0 +1,2 @@ +node_modules +lib \ No newline at end of file diff --git a/packages/create-fes-app/templates/plugin/.prettierrc b/packages/create-fes-app/templates/plugin/.prettierrc new file mode 100644 index 00000000..29ed0c41 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "none" +} \ No newline at end of file diff --git a/packages/create-fes-app/templates/plugin/build.config.js b/packages/create-fes-app/templates/plugin/build.config.js new file mode 100644 index 00000000..828db044 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/build.config.js @@ -0,0 +1,4 @@ + +module.exports = { + copy: ['runtime'] +}; diff --git a/packages/create-fes-app/templates/plugin/package.json.tpl b/packages/create-fes-app/templates/plugin/package.json.tpl new file mode 100644 index 00000000..d445b07c --- /dev/null +++ b/packages/create-fes-app/templates/plugin/package.json.tpl @@ -0,0 +1,47 @@ +{ + "name": "fes-plugin-{{{name}}}", + "version": "2.0.0", + "description": "一个fes.js插件", + "main": "lib/index.js", + "files": [ + "lib", + "README.md" + ], + "scripts": { + "dev": "node scripts/build.js --watch", + "build": "node scripts/build.js", + "lint": "eslint -c ./.eslintrc.js --ext .js,.jsx,.vue,.ts" + }, + "license": "MIT", + "keywords": [ + ], + "dependencies": { + }, + "devDependencies": { + "@babel/core": "^7.15.0", + "@babel/preset-env": "^7.15.0", + "@webank/eslint-config-webank": "0.3.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "husky": "^4.3.0", + "lint-staged": "^10.4.0", + "yargs-parser": "^20.2.9" + }, + "peerDependencies": { + "@fesjs/fes": "^2.0.0", + "vue": "^3.0.5" + }, + "lint-staged": { + "*.{js,jsx,vue,ts}": [ + "eslint --format=codeframe" + ] + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + } +} \ No newline at end of file diff --git a/packages/create-fes-app/templates/plugin/scripts/build.js b/packages/create-fes-app/templates/plugin/scripts/build.js new file mode 100644 index 00000000..53434e87 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/scripts/build.js @@ -0,0 +1,145 @@ +// 关闭 import 规则 +/* eslint import/no-extraneous-dependencies: 0 */ + +const fs = require('fs'); +const fse = require('fs-extra'); +const path = require('path'); +const merge = require('deepmerge'); +const chokidar = require('chokidar'); +const chalk = require('chalk'); +const argv = require('yargs-parser')(process.argv.slice(2)); + +const compiler = require('./compiler'); +const randomColor = require('./randomColor'); +const pkg = require('../package.json'); + + +const ESM_OUTPUT_DIR = 'es'; +const NODE_CJS_OUTPUT_DIR = 'lib'; +const SOURCE_DIR = 'src'; +const CONFIG_FILE_NAME = 'build.config.js'; +const GLOBAL_CONFIG_PATH = path.join(process.cwd(), CONFIG_FILE_NAME); +const DEFAULT_CONFIG = { + target: 'node' +}; + +function genLog(pkgName) { + return (msg) => { + console.log(`${randomColor(pkgName)}: ${msg}`); + }; +} + +function genShortPath(filePath) { + const codePath = filePath.split(`/${SOURCE_DIR}/`)[1]; + return `${SOURCE_DIR}/${codePath}`; +} + +function getPkgSourcePath() { + return path.join(process.cwd(), SOURCE_DIR); +} + +function getOutputPath(config) { + if (config.target === 'browser') { + return path.join(process.cwd(), ESM_OUTPUT_DIR); + } + + return path.join(process.cwd(), NODE_CJS_OUTPUT_DIR); +} + +function getGlobalConfig() { + if (fs.existsSync(GLOBAL_CONFIG_PATH)) { + const userConfig = require(GLOBAL_CONFIG_PATH); + return merge(DEFAULT_CONFIG, userConfig); + } + return DEFAULT_CONFIG; +} + +function cleanBeforeCompilerResult(log) { + const esmOutputDir = path.join(process.cwd(), ESM_OUTPUT_DIR); + const cjsOutputDir = path.join(process.cwd(), NODE_CJS_OUTPUT_DIR); + if (fs.existsSync(esmOutputDir)) { + log(chalk.gray(`Clean ${ESM_OUTPUT_DIR} directory`)); + fse.removeSync(esmOutputDir); + } + if (fs.existsSync(cjsOutputDir)) { + log(chalk.gray(`Clean ${NODE_CJS_OUTPUT_DIR} directory`)); + fse.removeSync(cjsOutputDir); + } +} + +function transformFile(filePath, outputPath, config, log) { + if (/\.[jt]sx?$/.test(path.extname(filePath))) { + try { + const code = fs.readFileSync(filePath, 'utf-8'); + const shortFilePath = genShortPath(filePath); + const transformedCode = compiler(code, config); + + const type = config.target === 'browser' ? ESM_OUTPUT_DIR : NODE_CJS_OUTPUT_DIR; + log(`Transform to ${type} for ${config.target === 'browser' ? chalk.yellow(shortFilePath) : chalk.blue(shortFilePath)}`); + fse.outputFileSync(outputPath, transformedCode); + } catch (error) { + console.error(error); + } + } else { + fse.copySync(filePath, outputPath); + } +} + +function compilerPkg(codeDir, outputDir, config, log) { + const files = fs.readdirSync(codeDir); + files.forEach((file) => { + const filePath = path.join(codeDir, file); + const outputFilePath = path.join(outputDir, file); + const fileStats = fs.lstatSync(filePath); + if (config.copy.includes(file)) { + fse.copySync(filePath, outputFilePath); + } else if (fileStats.isDirectory(filePath) && !/__tests__/.test(file)) { + fse.ensureDirSync(outputFilePath); + compilerPkg(filePath, outputFilePath, config, log); + } else if (fileStats.isFile(filePath)) { + transformFile(filePath, outputFilePath, config, log); + } + }); +} + +function watchFile(dir, outputDir, config, log) { + chokidar + .watch(dir, { + ignoreInitial: true + }) + .on('all', (event, changeFile) => { + // 修改的可能是一个目录,一个文件,一个需要 copy 的文件 or 目录 + const shortChangeFile = genShortPath(changeFile); + const outputPath = changeFile.replace(dir, outputDir); + const stat = fs.lstatSync(changeFile); + log(`[${event}] ${shortChangeFile}`); + if (config.resolveCopy.some(item => changeFile.startsWith(item))) { + fse.copySync(changeFile, outputPath); + } else if (stat.isFile()) { + transformFile(changeFile, outputPath, config, log); + } else if (stat.isDirectory()) { + compilerPkg(changeFile, outputPath, config); + } + }); +} + +function main() { + const sourceCodeDir = getPkgSourcePath(); + const pkgName = pkg.name; + if (fs.existsSync(sourceCodeDir)) { + const log = genLog(pkgName); + const config = getGlobalConfig(); + const outputDir = getOutputPath(config); + + cleanBeforeCompilerResult(log); + const type = config.target === 'browser' ? ESM_OUTPUT_DIR : NODE_CJS_OUTPUT_DIR; + log(chalk.white(`Build ${type} with babel`)); + compilerPkg(sourceCodeDir, outputDir, config, log); + if (argv.watch) { + log(chalk.magenta(`Start watch ${SOURCE_DIR} directory...`)); + watchFile(sourceCodeDir, outputDir, config, log); + } + } +} + +main(); diff --git a/packages/create-fes-app/templates/plugin/scripts/compiler.js b/packages/create-fes-app/templates/plugin/scripts/compiler.js new file mode 100644 index 00000000..c5499cd9 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/scripts/compiler.js @@ -0,0 +1,48 @@ +// 关闭 import 规则 +/* eslint import/no-extraneous-dependencies: 0 */ + +const babel = require('@babel/core'); + + +function transform(code, options) { + const result = babel.transformSync(code, options); + return result.code; +} + +function transformNodeCode(code) { + return transform(code, { + presets: [ + ['@babel/preset-env', { + modules: 'cjs', + targets: { node: '12' } + }] + ] + }); +} + +function transformBrowserCode(code) { + // 因为 fes.js 在生产打包的时候,会处理所有的 node_modules 下的文件,确保不会丢失必要 polyfill + // 因此这里不对 polyfill 进行处理,避免全局污染 + return transform(code, { + presets: [ + ['@babel/preset-env', { + modules: false, + useBuiltIns: false, + targets: { chrome: '51' } + }] + ] + }); +} + +function compiler(code, config) { + if (!config.target || config.target === 'node') { + return transformNodeCode(code); + } + if (config.target === 'browser') { + return transformBrowserCode(code); + } + throw new Error(`config target error: ${config.target}, only can use 'node' and 'browser'`); +} + + +module.exports = compiler; diff --git a/packages/create-fes-app/templates/plugin/scripts/randomColor.js b/packages/create-fes-app/templates/plugin/scripts/randomColor.js new file mode 100644 index 00000000..2d4d9af8 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/scripts/randomColor.js @@ -0,0 +1,35 @@ +/* eslint import/no-extraneous-dependencies: 0 */ +const chalk = require('chalk'); + +const colors = [ + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'gray', + 'redBright', + 'greenBright', + 'yellowBright', + 'blueBright', + 'magentaBright', + 'cyanBright' +]; + +let index = 0; +const cache = {}; + +module.exports = function (pkg) { + if (!cache[pkg]) { + const color = colors[index]; + const str = chalk[color].bold(pkg); + cache[pkg] = str; + if (index === colors.length - 1) { + index = 0; + } else { + index += 1; + } + } + return cache[pkg]; +}; diff --git a/packages/create-fes-app/templates/plugin/src/index.js.tpl b/packages/create-fes-app/templates/plugin/src/index.js.tpl new file mode 100644 index 00000000..7e84db59 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/src/index.js.tpl @@ -0,0 +1,45 @@ +import { join } from 'path'; +import { readFileSync } from 'fs'; + +const namespace = 'plugin-{{{name}}}'; + +export default (api) => { + api.describe({ + key: '{{{name}}}', + config: { + schema(joi) { + return joi.object(); + }, + default: {} + } + }); + + const { + utils: { Mustache } + } = api; + + const absoluteFilePath = join(namespace, 'core.js'); + + const absRuntimeFilePath = join(namespace, 'runtime.js'); + + api.onGenerateFiles(() => { + // 运行时执行的代码全部copy到临时目录,此时不需要编译,稍后webpack会编译临时目录代码 + api.copyTmpFiles({ + namespace, + path: join(__dirname, 'runtime'), + ignore: ['.tpl'] + }); + + // 有些运行时代码通过配置生成,则通过tpl写入 + api.writeTmpFile({ + path: absoluteFilePath, + content: Mustache.render( + readFileSync(join(__dirname, 'runtime/core.tpl'), 'utf-8'), + { + } + ) + }); + }); + + api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`); +}; diff --git a/packages/create-fes-app/templates/plugin/src/runtime/core.tpl.tpl b/packages/create-fes-app/templates/plugin/src/runtime/core.tpl.tpl new file mode 100644 index 00000000..37af994d --- /dev/null +++ b/packages/create-fes-app/templates/plugin/src/runtime/core.tpl.tpl @@ -0,0 +1 @@ +// 通过配置生成的代码 \ No newline at end of file diff --git a/packages/create-fes-app/templates/plugin/src/runtime/runtime.js b/packages/create-fes-app/templates/plugin/src/runtime/runtime.js new file mode 100644 index 00000000..f9e50c70 --- /dev/null +++ b/packages/create-fes-app/templates/plugin/src/runtime/runtime.js @@ -0,0 +1,5 @@ +// 配置运行时插件 + +export function onAppCreated({ app }) { + console.log(app); +}