feat: 插件开发文档和模板 (#135)

* feat: 插件开发文档和模板

* docs: 更新文档
This commit is contained in:
harrywan 2022-06-27 17:53:24 +08:00 committed by GitHub
parent d475753aeb
commit 3b8af8aacb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 447 additions and 1 deletions

View File

@ -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 自动注册为插件或插件集。

View File

@ -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
});
}
}

View File

@ -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();
}
};

View File

@ -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

View File

@ -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
}
};

View File

@ -0,0 +1,2 @@
node_modules
lib

View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "none"
}

View File

@ -0,0 +1,4 @@
module.exports = {
copy: ['runtime']
};

View File

@ -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"
}
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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];
};

View File

@ -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}`);
};

View File

@ -0,0 +1 @@
// 通过配置生成的代码

View File

@ -0,0 +1,5 @@
// 配置运行时插件
export function onAppCreated({ app }) {
console.log(app);
}