feat(重写构建): 重写构建

This commit is contained in:
bac-joker 2020-11-14 17:25:30 +08:00
parent a49f328088
commit 28498a30bc
95 changed files with 3617 additions and 2614 deletions

View File

@ -1,6 +1,7 @@
# http://editorconfig.org
root = true
lib
[*]
charset = utf-8

View File

@ -3,7 +3,7 @@ import { join } from 'path';
// utils must build before core
// runtime must build before renderer-react
const headPkgs = ['fes-runtime', 'fes-core'];
const headPkgs = ['fes-runtime', 'fes-core', 'fes', 'fes-plugin-built-in'];
const tailPkgs = [];
// const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
// (pkg) =>

View File

@ -196,8 +196,6 @@ export default class Config {
return {};
}
// @ts-ignore
// @ts-ignore
addAffix(file, affix) {
const ext = extname(file);
return file.replace(new RegExp(`${ext}$`), `.${affix}${ext}`);

View File

@ -2,7 +2,7 @@ import assert from 'assert';
import * as utils from '@umijs/utils';
import { isValidPlugin, pathToObj } from './utils/pluginUtils';
import { EnableBy, PluginType, ServiceStage } from './enums';
import Logger from '../logger/logger';
// TODO
// 标准化 logger
export default class PluginAPI {
@ -11,6 +11,7 @@ export default class PluginAPI {
this.key = opts.key;
this.service = opts.service;
this.utils = utils;
this.logger = new Logger(`fes:plugin:${this.id || this.key}`);
}
// TODO: reversed keys

View File

@ -180,18 +180,7 @@ export default class Service extends EventEmitter {
// 1. merge default config
// 2. validate
this.setStage(ServiceStage.getConfig);
const defaultConfig = await this.applyPlugins({
key: 'modifyDefaultConfig',
type: this.ApplyPluginsType.modify,
initialValue: await this.configInstance.getDefaultConfig()
});
this.config = await this.applyPlugins({
key: 'modifyConfig',
type: this.ApplyPluginsType.modify,
initialValue: this.configInstance.getConfig({
defaultConfig
})
});
await this.setConfig();
// merge paths to keep the this.paths ref
this.setStage(ServiceStage.getPaths);
@ -209,6 +198,21 @@ export default class Service extends EventEmitter {
});
}
async setConfig() {
const defaultConfig = await this.applyPlugins({
key: 'modifyDefaultConfig',
type: this.ApplyPluginsType.modify,
initialValue: await this.configInstance.getDefaultConfig()
});
this.config = await this.applyPlugins({
key: 'modifyConfig',
type: this.ApplyPluginsType.modify,
initialValue: this.configInstance.getConfig({
defaultConfig
})
});
}
async initPlugins() {
this._extraPlugins = [];
this.setStage(ServiceStage.initPlugins);
@ -257,7 +261,8 @@ export default class Service extends EventEmitter {
'config',
'env',
'args',
'hasPlugins'
'hasPlugins',
'setConfig'
].includes(prop)
) {
return typeof this[prop] === 'function'
@ -422,13 +427,6 @@ export default class Service extends EventEmitter {
this.args = args;
await this.init();
// TODO 临时实现
await this.applyPlugins({
key: 'onGenerateFiles',
type: ApplyPluginsType.event
});
this.setStage(ServiceStage.run);
await this.applyPlugins({
key: 'onStart',
@ -437,11 +435,11 @@ export default class Service extends EventEmitter {
args
}
});
// TODO 执行命令
// return this.runCommand({
// name,
// args
// });
return this.runCommand({
name,
args
});
}
async runCommand({

View File

@ -73,7 +73,7 @@ export function pathToObj({ path, cwd }) {
} else {
id = winPath(path);
}
id = id.replace('@webank/fes-core/lib/plugins', '@@');
id = id.replace('@webank/fes-plugin-built-in/lib/plugins', '@@');
id = id.replace(/\.js$/, '');
const key = isPkgPlugin

View File

@ -3,9 +3,9 @@
import Config from './Config/Config';
import Service from './Service/Service';
import PluginAPI from './Service/PluginAPI';
import Logger from './logger/logger';
import { PluginType } from './Service/enums';
import { isPlugin } from './Service/utils/pluginUtils';
import ServiceWithBuiltIn from './ServiceWithBuiltIn';
export * from './route';
@ -15,5 +15,5 @@ export {
PluginAPI,
isPlugin,
PluginType,
ServiceWithBuiltIn
Logger
};

View File

@ -0,0 +1,75 @@
import {
createDebug,
chalk
} from '@umijs/utils';
export default class Logger {
LOG = chalk.black.bgBlue('LOG');
INFO = chalk.black.bgBlue('INFO');
WARN = chalk.black.bgHex('#faad14')('WARN');
ERROR = chalk.black.bgRed('ERROR');
PROFILE = chalk.black.bgCyan('PROFILE');
constructor(namespace) {
// TODO: get namespace filename accounding caller function
if (!namespace) {
throw new Error('logger needs namespace');
}
this.namespace = namespace;
this.profilers = {};
this.debug = createDebug(this.namespace);
}
log(...args) {
// TODO: node env production
console.log(this.LOG, ...args);
}
/**
* The {@link logger.info} function is an alias for {@link logger.log()}.
* @param args
*/
info(...args) {
console.log(this.INFO, ...args);
}
error(...args) {
console.error(this.ERROR, ...args);
}
warn(...args) {
console.warn(this.WARN, ...args);
}
formatTiming(timing) {
return timing < 60 * 1000
? `${Math.round(timing / 10) / 100}s`
: `${Math.round(timing / 600) / 100}m`;
}
profile(id, message) {
const time = Date.now();
const namespace = `${this.namespace}:${id}`;
// for test
let msg;
if (this.profilers[id]) {
const timeEnd = this.profilers[id];
delete this.profilers[id];
process.stderr.write(`${this.PROFILE} `);
msg = `${this.PROFILE} ${chalk.cyan(
`${namespace}`,
)} Completed in ${this.formatTiming(time - timeEnd)}`;
console.log(msg);
} else {
msg = `${this.PROFILE} ${chalk.cyan(`${namespace}`)} ${message || ''}`;
console.log(msg);
}
this.profilers[id] = time;
return msg;
}
}

View File

@ -1,15 +0,0 @@
// TODO 拆成独立的包作为内置插件包
export default [
// register methods
require.resolve('./registerMethods'),
// misc
require.resolve('./routes'),
// generate files
require.resolve('./generateFiles/core/plugin'),
require.resolve('./generateFiles/core/routes'),
require.resolve('./generateFiles/core/fesExports'),
require.resolve('./generateFiles/fes')
];

View File

@ -0,0 +1,3 @@
export default {
disableTypeCheck: false,
};

View File

@ -0,0 +1,36 @@
{
"name": "@webank/fes-plugin-built-in",
"version": "2.0.0",
"description": "@webank/fes-plugin-built-in",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib"
],
"repository": {
"type": "git",
"url": ""
},
"keywords": [
"fes"
],
"authors": [
""
],
"license": "MIT",
"bugs": "",
"homepage": "",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@umijs/utils": "^3.2.24",
"@umijs/bundler-webpack": "^3.2.23",
"@umijs/server": "^3.2.23",
"@vue/babel-plugin-jsx": "^1.0.0-rc.3",
"@webank/fes-core": "^2.0.0",
"cliui": "6.0.0",
"vue-loader": "^16.0.0-rc.1",
"html-webpack-plugin": "^3.2.0"
}
}

View File

@ -0,0 +1,53 @@
// TODO 拆成独立的包作为内置插件包
export default [
// register methods
require.resolve('./plugins/registerMethods'),
// misc
require.resolve('./plugins/routes'),
// generate files
require.resolve('./plugins/generateFiles/core/plugin'),
require.resolve('./plugins/generateFiles/core/routes'),
require.resolve('./plugins/generateFiles/core/fesExports'),
require.resolve('./plugins/generateFiles/fes'),
// bundle configs
require.resolve('./plugins/features/alias'),
require.resolve('./plugins/features/analyze'),
require.resolve('./plugins/features/autoprefixer'),
require.resolve('./plugins/features/base'),
require.resolve('./plugins/features/chainWebpack'),
require.resolve('./plugins/features/chunks'),
require.resolve('./plugins/features/cssLoader'),
require.resolve('./plugins/features/cssnano'),
require.resolve('./plugins/features/copy'),
require.resolve('./plugins/features/define'),
require.resolve('./plugins/features/devServer'),
require.resolve('./plugins/features/devtool'),
require.resolve('./plugins/features/externals'),
require.resolve('./plugins/features/extraBabelPlugins'),
require.resolve('./plugins/features/extraBabelPresets'),
require.resolve('./plugins/features/extraPostCSSPlugins'),
require.resolve('./plugins/features/hash'),
require.resolve('./plugins/features/html'),
require.resolve('./plugins/features/inlineLimit'),
require.resolve('./plugins/features/lessLoader'),
require.resolve('./plugins/features/mountElementId'),
require.resolve('./plugins/features/nodeModulesTransform'),
require.resolve('./plugins/features/outputPath'),
require.resolve('./plugins/features/plugins'),
require.resolve('./plugins/features/postcssLoader'),
require.resolve('./plugins/features/proxy'),
require.resolve('./plugins/features/publicPath'),
require.resolve('./plugins/features/styleLoader'),
require.resolve('./plugins/features/targets'),
require.resolve('./plugins/features/terserOptions'),
require.resolve('./plugins/features/theme'),
require.resolve('./plugins/features/vueLoader'),
// commands
require.resolve('./plugins/commands/build/build'),
require.resolve('./plugins/commands/dev/dev')
];

View File

@ -0,0 +1,73 @@
import { relative } from 'path';
import { existsSync } from 'fs';
import { Logger } from '@webank/fes-core';
import {
cleanTmpPathExceptCache,
getBundleAndConfigs,
printFileSizes
} from '../buildDevUtils';
import generateFiles from '../generateFiles';
const logger = new Logger('fes:plugin-built-in');
export default function (api) {
const {
paths,
utils: { rimraf }
} = api;
api.registerCommand({
name: 'build',
description: 'build application for production',
async fn() {
cleanTmpPathExceptCache({
absTmpPath: paths.absTmpPath
});
// generate files
await generateFiles({ api, watch: false });
// build
const {
bundler,
bundleConfigs,
bundleImplementor
} = await getBundleAndConfigs({ api });
try {
// clear output path before exec build
if (process.env.CLEAR_OUTPUT !== 'none') {
if (paths.absOutputPath && existsSync(paths.absOutputPath || '')) {
logger.debug(`Clear OutputPath: ${paths.absNodeModulesPath}`);
rimraf.sync(paths.absOutputPath);
}
}
const { stats } = await bundler.build({
bundleConfigs,
bundleImplementor
});
if (process.env.RM_TMPDIR !== 'none') {
rimraf.sync(paths.absTmpPath);
}
printFileSizes(stats, relative(process.cwd(), paths.absOutputPath));
await api.applyPlugins({
key: 'onBuildComplete',
type: api.ApplyPluginsType.event,
args: {
stats
}
});
} catch (err) {
await api.applyPlugins({
key: 'onBuildComplete',
type: api.ApplyPluginsType.event,
args: {
err
}
});
// throw build error
throw err;
}
}
});
}

View File

@ -0,0 +1,232 @@
import { Bundler as DefaultBundler } from '@umijs/bundler-webpack';
import { join, resolve } from 'path';
import { existsSync, readdirSync, readFileSync } from 'fs';
import { rimraf, chalk } from '@umijs/utils';
import zlib from 'zlib';
export async function getBundleAndConfigs({
api,
port
}) {
// bundler
const Bundler = await api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBundler',
initialValue: DefaultBundler
});
const bundleImplementor = await api.applyPlugins({
key: 'modifyBundleImplementor',
type: api.ApplyPluginsType.modify,
initialValue: undefined
});
const bundler = new Bundler({
cwd: api.cwd,
config: api.config
});
const bundlerArgs = {
env: api.env,
bundler: { id: Bundler.id, version: Bundler.version }
};
// get config
async function getConfig({ type }) {
const env = api.env === 'production' ? 'production' : 'development';
const getConfigOpts = await api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBundleConfigOpts',
initialValue: {
env,
type,
port,
hot: process.env.HMR !== 'none',
entry: {
umi: join(api.paths.absTmpPath, 'fes.js')
},
// @ts-ignore
bundleImplementor,
async modifyBabelOpts(opts) {
return api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBabelOpts',
initialValue: opts
});
},
async modifyBabelPresetOpts(opts) {
return api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBabelPresetOpts',
initialValue: opts
});
},
async chainWebpack(webpackConfig, opts) {
return api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'chainWebpack',
initialValue: webpackConfig,
args: {
...opts
}
});
}
},
args: {
...bundlerArgs,
type
}
});
return api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBundleConfig',
initialValue: await bundler.getConfig(getConfigOpts),
args: {
...bundlerArgs,
type
}
});
}
const bundleConfigs = await api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBundleConfigs',
initialValue: [await getConfig({ type: 'csr' })].filter(
Boolean,
),
args: {
...bundlerArgs,
getConfig
}
});
return {
bundleImplementor,
bundler,
bundleConfigs
};
}
export function cleanTmpPathExceptCache({
absTmpPath
}) {
if (!existsSync(absTmpPath)) return;
readdirSync(absTmpPath).forEach((file) => {
if (file === '.cache') return;
rimraf.sync(join(absTmpPath, file));
});
}
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 1.8 * 1024 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1 * 1024 * 1024;
export function printFileSizes(stats, dir) {
const ui = require('cliui')({ width: 80 });
const json = stats.toJson({
hash: false,
modules: false,
chunks: false
});
const filesize = (bytes) => {
bytes = Math.abs(bytes);
const radix = 1024;
const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let loop = 0;
// calculate
while (bytes >= radix) {
bytes /= radix;
++loop;
}
return `${bytes.toFixed(1)} ${unit[loop]}`;
};
const assets = json.assets
? json.assets
: json?.children?.reduce((acc, child) => acc.concat(child?.assets), []);
const seenNames = new Map();
const isJS = val => /\.js$/.test(val);
const isCSS = val => /\.css$/.test(val);
const orderedAssets = assets.map((a) => {
a.name = a.name.split('?')[0];
// These sizes are pretty large
const isMainBundle = a.name.indexOf('fes.') === 0;
const maxRecommendedSize = isMainBundle
? WARN_AFTER_BUNDLE_GZIP_SIZE
: WARN_AFTER_CHUNK_GZIP_SIZE;
const isLarge = maxRecommendedSize && a.size > maxRecommendedSize;
return {
...a,
suggested: isLarge && isJS(a.name)
};
})
.filter((a) => {
if (seenNames.has(a.name)) {
return false;
}
seenNames.set(a.name, true);
return isJS(a.name) || isCSS(a.name);
})
.sort((a, b) => {
if (isJS(a.name) && isCSS(b.name)) return -1;
if (isCSS(a.name) && isJS(b.name)) return 1;
return b.size - a.size;
});
function getGzippedSize(asset) {
const filepath = resolve(join(dir, asset.name));
if (existsSync(filepath)) {
const buffer = readFileSync(filepath);
return filesize(zlib.gzipSync(buffer).length);
}
return filesize(0);
}
function makeRow(a, b, c) {
return ` ${a}\t ${b}\t ${c}`;
}
ui.div(
`${makeRow(
chalk.cyan.bold('File'),
chalk.cyan.bold('Size'),
chalk.cyan.bold('Gzipped'),
)
}\n\n${
// eslint-disable-next-line
orderedAssets.map(asset => makeRow(/js$/.test(asset.name) ? (asset.suggested ? chalk.yellow(join(dir, asset.name)) : chalk.green(join(dir, asset.name))) : chalk.blue(join(dir, asset.name)),
filesize(asset.size),
getGzippedSize(asset),))
.join('\n')}`,
);
console.log(
`${ui.toString()}\n\n ${chalk.gray(
'Images and other types of assets omitted.',
)}\n`,
);
if (orderedAssets?.some(asset => asset.suggested)) {
// We'll warn for bundles exceeding them.
// TODO: use umi docs
console.log();
console.log(
chalk.yellow('The bundle size is significantly larger than recommended.'),
);
console.log(
chalk.yellow(
'Consider reducing it with code splitting: https://umijs.org/docs/load-on-demand',
),
);
console.log(
chalk.yellow(
'You can also analyze the project dependencies using ANALYZE=1',
),
);
console.log();
}
}

View File

@ -0,0 +1,243 @@
import { Server } from '@umijs/server';
import {
delay
} from '@umijs/utils';
import assert from 'assert';
import {
cleanTmpPathExceptCache,
getBundleAndConfigs
} from '../buildDevUtils';
import generateFiles from '../generateFiles';
import {
watchPkg
} from './watchPkg';
export default (api) => {
const {
env,
paths,
utils: {
chalk,
portfinder
}
} = api;
const unwatchs = [];
let port;
let hostname;
let server;
function destroy() {
for (const unwatch of unwatchs) {
unwatch();
}
// eslint-disable-next-line
server?.listeningApp?.close();
}
api.registerCommand({
name: 'dev',
description: 'start a dev server for development',
async fn({ args = {} }) {
const defaultPort = process.env.PORT || args.port || api.config.devServer?.port;
port = await portfinder.getPortPromise({
port: defaultPort ? parseInt(String(defaultPort), 10) : 8000
});
hostname = process.env.HOST || api.config.devServer?.host || '0.0.0.0';
console.log(chalk.cyan('Starting the development server...'));
process.send({
type: 'UPDATE_PORT',
port
});
// enable https, HTTP/2 by default when using --https
const isHTTPS = process.env.HTTPS || args.https;
cleanTmpPathExceptCache({
absTmpPath: paths.absTmpPath
});
const watch = process.env.WATCH !== 'none';
// generate files
const unwatchGenerateFiles = await generateFiles({
api,
watch
});
if (unwatchGenerateFiles) unwatchs.push(unwatchGenerateFiles);
if (watch) {
// watch pkg changes
const unwatchPkg = watchPkg({
cwd: api.cwd,
onChange() {
console.log();
api.logger.info('Plugins in package.json changed.');
api.restartServer();
}
});
unwatchs.push(unwatchPkg);
// watch config change
const unwatchConfig = api.service.configInstance.watch({
userConfig: api.service.userConfig,
onChange: async ({
pluginChanged,
valueChanged
}) => {
if (pluginChanged.length) {
console.log();
api.logger.info(
`Plugins of ${pluginChanged
.map(p => p.key)
.join(', ')} changed.`,
);
api.restartServer();
}
if (valueChanged.length) {
let reload = false;
let regenerateTmpFiles = false;
const fns = [];
const reloadConfigs = [];
valueChanged.forEach(({
key,
pluginId
}) => {
const {
onChange
} = api.service.plugins[pluginId].config || {};
if (onChange === api.ConfigChangeType.regenerateTmpFiles) {
regenerateTmpFiles = true;
}
if (!onChange || onChange === api.ConfigChangeType.reload) {
reload = true;
reloadConfigs.push(key);
}
if (typeof onChange === 'function') {
fns.push(onChange);
}
});
if (reload) {
console.log();
api.logger.info(`Config ${reloadConfigs.join(', ')} changed.`);
api.restartServer();
} else {
api.service.userConfig = api.service.configInstance.getUserConfig();
await api.setConfig();
if (regenerateTmpFiles) {
await generateFiles({
api
});
} else {
fns.forEach(fn => fn());
}
}
}
}
});
unwatchs.push(unwatchConfig);
}
// delay dev server 启动,避免重复 compile
// https://github.com/webpack/watchpack/issues/25
// https://github.com/yessky/webpack-mild-compile
await delay(500);
// dev
const {
bundler,
bundleConfigs,
bundleImplementor
} = await getBundleAndConfigs({
api,
port
});
const opts = bundler.setupDevServerOpts({
bundleConfigs,
bundleImplementor
});
const beforeMiddlewares = await api.applyPlugins({
key: 'addBeforeMiddewares',
type: api.ApplyPluginsType.add,
initialValue: [],
args: {}
});
const middlewares = await api.applyPlugins({
key: 'addMiddewares',
type: api.ApplyPluginsType.add,
initialValue: [],
args: {}
});
server = new Server({
...opts,
compress: true,
https: !!isHTTPS,
headers: {
'access-control-allow-origin': '*'
},
proxy: api.config.proxy,
beforeMiddlewares,
afterMiddlewares: [
...middlewares
],
...(api.config.devServer || {})
});
const listenRet = await server.listen({
port,
hostname
});
return {
...listenRet,
destroy
};
}
});
api.registerMethod({
name: 'getPort',
fn() {
assert(
env === 'development',
'api.getPort() is only valid in development.',
);
return port;
}
});
api.registerMethod({
name: 'getHostname',
fn() {
assert(
env === 'development',
'api.getHostname() is only valid in development.',
);
return hostname;
}
});
api.registerMethod({
name: 'getServer',
fn() {
assert(
env === 'development',
'api.getServer() is only valid in development.',
);
return server;
}
});
api.registerMethod({
name: 'restartServer',
fn() {
console.log(chalk.gray('Try to restart dev server...'));
destroy();
process.send({
type: 'RESTART'
});
}
});
};

View File

@ -0,0 +1,55 @@
import { join } from 'path';
import { chokidar, winPath, lodash } from '@umijs/utils';
import { existsSync, readFileSync } from 'fs';
import { isPlugin, PluginType } from '@webank/fes-core';
function getFesPlugins(opts) {
return Object.keys({
...opts.pkg.dependencies,
...opts.pkg.devDependencies
}).filter(name => (
isPlugin(PluginType.plugin, name)
));
}
function getFesPluginsFromPkgPath(opts) {
let pkg = {};
if (existsSync(opts.pkgPath)) {
try {
pkg = JSON.parse(readFileSync(opts.pkgPath, 'utf-8'));
} catch (e) {
// ignore
}
}
return getFesPlugins({ pkg });
}
export function watchPkg(opts) {
const pkgPath = join(opts.cwd, 'package.json');
const plugins = getFesPluginsFromPkgPath({ pkgPath });
const watcher = chokidar.watch(pkgPath, {
ignoreInitial: true
});
watcher.on('all', () => {
const newPlugins = getFesPluginsFromPkgPath({ pkgPath });
if (!lodash.isEqual(plugins, newPlugins)) {
// 已经重启了,只处理一次就够了
opts.onChange();
}
});
return () => {
watcher.close();
};
}
export function watchPkgs(opts) {
const unwatchs = [watchPkg({ cwd: opts.cwd, onChange: opts.onChange })];
if (winPath(opts.cwd) !== winPath(process.cwd())) {
unwatchs.push(watchPkg({ cwd: process.cwd(), onChange: opts.onChange }));
}
return () => {
unwatchs.forEach((unwatch) => {
unwatch();
});
};
}

View File

@ -0,0 +1,57 @@
import { chokidar, lodash, winPath } from '@umijs/utils';
import { join } from 'path';
export default async ({ api, watch }) => {
const { paths } = api;
async function generate() {
api.logger.debug('generate files');
await api.applyPlugins({
key: 'onGenerateFiles',
type: api.ApplyPluginsType.event
});
}
const watchers = [];
await generate();
function unwatch() {
watchers.forEach((watcher) => {
watcher.close();
});
}
function createWatcher(path) {
const watcher = chokidar.watch(path, {
// ignore .dotfiles and _mock.js
ignored: /(^|[/\\])(_mock.js$|\..)/,
ignoreInitial: true
});
watcher.on(
'all',
lodash.throttle(async () => {
await generate();
}, 100),
);
}
if (watch) {
const watcherPaths = await api.applyPlugins({
key: 'addTmpGenerateWatcherPaths',
type: api.ApplyPluginsType.add,
initialValue: [
paths.absPagesPath,
join(paths.absSrcPath, api.config?.singular ? 'layout' : 'layouts'),
join(paths.absSrcPath, 'app.js')
]
});
lodash
.uniq(watcherPaths.map(p => winPath(p)))
.forEach((p) => {
createWatcher(p);
});
}
return unwatch;
};

View File

@ -0,0 +1,60 @@
import { dirname } from 'path';
import { winPath, resolve } from '@umijs/utils';
export default (api) => {
const { paths, pkg, cwd } = api;
api.describe({
key: 'alias',
config: {
schema(joi) {
return joi.object();
},
default: {
}
}
});
function getUserLibDir({ library }) {
if (
(pkg.dependencies && pkg.dependencies[library])
|| (pkg.devDependencies && pkg.devDependencies[library])
// egg project using `clientDependencies` in ali tnpm
|| (pkg.clientDependencies && pkg.clientDependencies[library])
) {
return winPath(
dirname(
// 通过 resolve 往上找,可支持 lerna 仓库
// lerna 仓库如果用 yarn workspace 的依赖不一定在 node_modules可能被提到根目录并且没有 link
resolve.sync(`${library}/package.json`, {
basedir: cwd
}),
),
);
}
return null;
}
// 另一种实现方式:
// 提供 projectFirstLibraries 的配置方式,但是不通用,先放插件层实现
api.chainWebpack(async (memo) => {
const libraries = await api.applyPlugins({
key: 'addProjectFirstLibraries',
type: api.ApplyPluginsType.add,
initialValue: [
]
});
libraries.forEach((library) => {
memo.resolve.alias.set(
library.name,
getUserLibDir({ library: library.name }) || library.path,
);
});
// 选择在 chainWebpack 中进行以上 alias 的初始化,是为了支持用户使用 modifyPaths API 对 paths 进行改写
memo.resolve.alias.set('@', paths.absSrcPath);
memo.resolve.alias.set('@@', paths.absTmpPath);
return memo;
});
};

View File

@ -0,0 +1,44 @@
export default (api) => {
api.describe({
key: 'analyze',
config: {
schema(joi) {
return joi
.object({
analyzerMode: joi.string().valid('server', 'static', 'disabled'),
analyzerHost: joi.string(),
analyzerPort: joi.alternatives(joi.number(), 'auto'),
openAnalyzer: joi.boolean(),
generateStatsFile: joi.boolean(),
statsFilename: joi.string(),
logLevel: joi.string().valid('info', 'warn', 'error', 'silent'),
defaultSizes: joi.string().valid('stat', 'parsed', 'gzip')
})
.unknown(true);
},
default: {
analyzerMode: process.env.ANALYZE_MODE || 'server',
analyzerPort: process.env.ANALYZE_PORT || 8888,
openAnalyzer: process.env.ANALYZE_OPEN !== 'none',
// generate stats file while ANALYZE_DUMP exist
generateStatsFile: !!process.env.ANALYZE_DUMP,
statsFilename: process.env.ANALYZE_DUMP || 'stats.json',
logLevel: process.env.ANALYZE_LOG_LEVEL || 'info',
defaultSizes: 'parsed' // stat // gzip
}
},
enableBy: () => !!(process.env.ANALYZE || process.env.ANALYZE_SSR)
});
api.chainWebpack((webpackConfig, opts) => {
const { type } = opts;
if (type === 'csr' && !process.env.ANALYZE_SSR) {
webpackConfig
.plugin('bundle-analyzer')
.use(require('umi-webpack-bundle-analyzer').BundleAnalyzerPlugin, [
api.config?.analyze || {}
]);
}
return webpackConfig;
});
};

View File

@ -0,0 +1,16 @@
export default (api) => {
api.describe({
key: 'autoprefixer',
config: {
default: {
flexbox: 'no-2009'
},
schema(joi) {
return joi
.object()
.description('postcss autoprefixer, default flexbox: no-2009');
}
}
});
};

View File

@ -0,0 +1,12 @@
export default (api) => {
api.describe({
key: 'base',
config: {
default: '/',
schema(joi) {
return joi.string();
}
}
});
};

View File

@ -0,0 +1,31 @@
import { join } from 'path';
import { existsSync } from 'fs';
import { winPath } from '@umijs/utils';
export default (api) => {
api.describe({
key: 'chainWebpack',
config: {
schema(joi) {
return joi.function();
}
}
});
api.chainWebpack((webpackConfig) => {
const cwd = api.cwd;
const prefix = existsSync(join(cwd, 'src')) ? join(cwd, 'src') : cwd;
// 添加 .vue 后缀
webpackConfig.resolve.extensions.merge([
'.vue'
]);
webpackConfig.module
.rule('js-in-node_modules').use('babel-loader').tap((options) => {
console.log(options);
options.cacheDirectory = winPath(`${prefix}/.fes/.cache/babel-loader`);
return options;
});
return webpackConfig;
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'chunks',
config: {
schema(joi) {
return joi.array().items(joi.string());
}
}
});
};

View File

@ -0,0 +1,19 @@
export default (api) => {
api.describe({
key: 'copy',
config: {
schema(joi) {
return joi.array().items(
joi.alternatives(
joi.object({
from: joi.string(),
to: joi.string()
}),
joi.string(),
),
);
}
}
});
};

View File

@ -0,0 +1,36 @@
export default (api) => {
api.describe({
key: 'cssLoader',
config: {
schema(joi) {
return joi
.object({
url: joi.alternatives(joi.boolean(), joi.function()),
import: joi.alternatives(joi.boolean(), joi.function()),
modules: joi.alternatives(
joi.boolean(),
joi.string(),
joi.object(),
),
sourceMap: joi.boolean(),
importLoaders: joi.number(),
onlyLocals: joi.boolean(),
esModule: joi.boolean(),
localsConvention: joi
.string()
.valid(
'asIs',
'camelCase',
'camelCaseOnly',
'dashes',
'dashesOnly',
)
})
.description(
'more css-loader options see https://webpack.js.org/loaders/css-loader/#options',
);
}
}
});
};

View File

@ -0,0 +1,16 @@
export default (api) => {
api.describe({
// https://cssnano.co/optimisations/
key: 'cssnano',
config: {
default: {
mergeRules: false,
minifyFontValues: { removeQuotes: false }
},
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'define',
config: {
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,29 @@
export default (api) => {
api.describe({
key: 'devServer',
config: {
default: {},
schema(joi) {
return joi
.object({
port: joi.number().description('devServer port, default 8000'),
host: joi.string(),
https: joi.alternatives(
joi
.object({
key: joi.string(),
cert: joi.string()
})
.unknown(),
joi.boolean(),
),
headers: joi.object(),
writeToDisk: joi.alternatives(joi.boolean(), joi.function())
})
.description('devServer configs')
.unknown(true);
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'devtool',
config: {
schema(joi) {
return joi.string();
}
}
});
};

View File

@ -0,0 +1,12 @@
export default (api) => {
api.describe({
key: 'externals',
config: {
schema(joi) {
// https://webpack.js.org/configuration/externals/#externals
return joi.alternatives(joi.object(), joi.string(), joi.function());
}
}
});
};

View File

@ -0,0 +1,25 @@
import { join } from 'path';
import { existsSync } from 'fs';
import { winPath } from '@umijs/utils';
export default (api) => {
api.describe({
key: 'extraBabelPlugins',
config: {
schema(joi) {
return joi.array();
}
}
});
api.modifyBabelOpts((babelOpts) => {
const cwd = api.cwd;
const prefix = existsSync(join(cwd, 'src')) ? join(cwd, 'src') : cwd;
babelOpts.cacheDirectory = process.env.BABEL_CACHE !== 'none'
? winPath(`${prefix}/.fes/.cache/babel-loader`)
: false;
babelOpts.plugins.push(require.resolve('@vue/babel-plugin-jsx'));
return babelOpts;
});
};

View File

@ -0,0 +1,23 @@
export default (api) => {
api.describe({
key: 'extraBabelPresets',
config: {
schema(joi) {
return joi.array();
}
}
});
api.modifyBabelPresetOpts(opts => Object.assign({}, opts, {
typescript: false,
env: {
useBuiltIns: 'entry',
corejs: 3,
modules: false
},
react: false,
reactRemovePropTypes: false,
reactRequire: false,
svgr: false
}));
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'extraPostCSSPlugins',
config: {
schema(joi) {
return joi.array();
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'hash',
config: {
schema(joi) {
return joi.boolean();
}
}
});
};

View File

@ -0,0 +1,66 @@
import { resolve, join } from 'path';
import { existsSync } from 'fs';
export default (api) => {
api.describe({
key: 'vueLoader',
config: {
schema(joi) {
return joi
.object({})
.description(
'more vue-loader options see https://vue-loader.vuejs.org/',
);
}
}
});
api.chainWebpack((webpackConfig) => {
const isProd = api.env === 'production';
const htmlOptions = {
title: api.service.pkg.name,
templateParameters: (compilation, assets, pluginOptions) => {
// enhance html-webpack-plugin's built in template params
let stats;
return Object.assign({
// make stats lazy as it is expensive
get webpack() {
// eslint-disable-next-line
return stats || (stats = compilation.getStats().toJson());
},
compilation,
webpackConfig: compilation.options,
htmlWebpackPlugin: {
files: assets,
options: pluginOptions
}
}, api.config.html);
}
};
if (isProd) {
Object.assign(htmlOptions, {
minify: {
removeComments: true,
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeScriptTypeAttributes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
}
});
}
// resolve HTML file(s)
const htmlPath = join(api.paths.cwd, 'public/index.html');
const defaultHtmlPath = resolve(__dirname, 'index-default.html');
htmlOptions.template = existsSync(htmlPath)
? htmlPath
: defaultHtmlPath;
webpackConfig
.plugin('html')
.use(require('html-webpack-plugin'), [htmlOptions]);
});
};

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Fes App</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'inlineLimit',
config: {
schema(joi) {
return joi.number();
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'lessLoader',
config: {
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,12 @@
export default (api) => {
api.describe({
key: 'mountElementId',
config: {
default: '#app',
schema(joi) {
return joi.string().allow('');
}
}
});
};

View File

@ -0,0 +1,18 @@
export default (api) => {
api.describe({
key: 'nodeModulesTransform',
config: {
default: {
type: 'all',
exclude: []
},
schema(joi) {
return joi.object({
type: joi.string().valid('all', 'none'),
exclude: joi.array().items(joi.string())
});
}
}
});
};

View File

@ -0,0 +1,14 @@
export default (api) => {
api.describe({
key: 'outputPath',
config: {
default: 'dist',
schema(joi) {
return joi
.string()
.not('src', 'public', 'pages', 'mock', 'config')
.allow('');
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'plugins',
config: {
schema(joi) {
return joi.array().items(joi.string());
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'postcssLoader',
config: {
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,18 @@
export default (api) => {
api.describe({
key: 'proxy',
config: {
onChange: () => {
const server = api.getServer();
if (server) {
// refrest proxy service
server.setupProxy(api.config.proxy, true);
}
},
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,14 @@
export default (api) => {
api.describe({
key: 'publicPath',
config: {
default: '/',
schema(joi) {
return joi
.string()
.regex(/\/$/)
.error(new Error('config.publicPath must end with /.'));
}
}
});
};

View File

@ -0,0 +1,10 @@
export default (api) => {
api.describe({
key: 'styleLoader',
config: {
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,19 @@
export default (api) => {
api.describe({
key: 'targets',
config: {
default: {
node: true,
chrome: 49,
firefox: 64,
safari: 10,
edge: 13,
ios: 10
},
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'terserOptions',
config: {
schema(joi) {
return joi.object();
}
}
});
};

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'theme',
config: {
schema(joi) {
return joi.object().pattern(joi.string(), joi.string());
}
}
});
};

View File

@ -0,0 +1,35 @@
export default (api) => {
api.describe({
key: 'vueLoader',
config: {
schema(joi) {
return joi
.object({})
.description(
'more vue-loader options see https://vue-loader.vuejs.org/',
);
}
}
});
api.chainWebpack((webpackConfig) => {
// 添加 .vue 后缀
webpackConfig.module
.rule('vue')
.test(/\.vue$/)
.use('vue-loader')
.loader(require.resolve('vue-loader'))
.options({
babelParserPlugins: ['jsx', 'classProperties', 'decorators-legacy']
})
.end()
.end();
webpackConfig
.plugin('vue-loader')
.use(require('vue-loader').VueLoaderPlugin);
return webpackConfig;
});
};

View File

@ -3,7 +3,7 @@ import { readFileSync } from 'fs';
import {
join
} from 'path';
import { routesToJSON } from '../../../route';
import { routesToJSON } from '@webank/fes-core';
export default function (api) {
const {

View File

@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
export default function (api) {
[
'onExit',
'onGenerateFiles',
'addFesExports',
'addRuntimePluginKey',
@ -12,7 +13,18 @@ export default function (api) {
'addEntryImports',
'addEntryCodeAhead',
'addEntryCode',
'modifyRoutes'
'addBeforeMiddewares',
'addMiddewares',
'modifyRoutes',
'modifyBundler',
'modifyBundleImplementor',
'modifyBundleConfigOpts',
'modifyBundleConfig',
'modifyBundleConfigs',
'modifyBabelOpts',
'modifyBabelPresetOpts',
'chainWebpack',
'addTmpGenerateWatcherPaths'
].forEach((name) => {
api.registerMethod({ name });
});

View File

@ -1,4 +1,4 @@
import { getRoutes } from '../route';
import { getRoutes } from '@webank/fes-core';
export default function (api) {
api.describe({

View File

@ -30,11 +30,9 @@
"license": "MIT",
"devDependencies": {
"@vue/compiler-sfc": "^3.0.0",
"@webank/eslint-config-webank": "^0.1.4",
"csp-html-webpack-plugin": "^4.0.0"
"@webank/eslint-config-webank": "^0.1.4"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.11.2",
"vue": "^3.0.2",
"@webank/fes": "^2.0.0"
}

View File

@ -0,0 +1,6 @@
export default {
cjs: { type: 'babel', lazy: true },
esm: { type: 'rollup' },
disableTypeCheck: false,
extraExternals: ['@@/core/fesExports'],
};

13
packages/fes/bin/fes.js Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env node
const resolveCwd = require('resolve-cwd');
const { name, bin } = require('../package.json');
const localCLI = resolveCwd.silent(`${name}/${bin.fes}`);
if (!process.env.USE_GLOBAL_UMI && localCLI && localCLI !== __filename) {
// eslint-disable-next-line
require(localCLI);
} else {
require('../lib/cli');
}

View File

@ -1,54 +0,0 @@
#!/usr/bin/env node
const commander = require('commander');
const pkg = require('../package.json');
const generateConfig = require('../build/helpers/config');
const log = require('../build/helpers/log');
commander.usage('<command> [options]')
.version(pkg.version, '-v, --vers')
.option('-e, --env <env>', '配置环境 local(本地) | sit(测试) | prod(生产)')
.description(pkg.description);
commander.command('init [name]')
.description('创建项目')
.action(async (name) => {
const projectInit = require('../build/tasks/init');
const config = generateConfig('init');
await projectInit(config, name);
});
commander.command('update')
.description('将 fes2 项目升级到 fes3')
.action(() => {
const update = require('../build/tasks/update');
const config = generateConfig('update');
update(config);
});
commander.command('dev')
.description('开发调试, 默认 local')
.action(() => {
const dev = require('../build/tasks/dev');
const config = generateConfig('dev', commander.env || 'local');
dev(config);
});
commander.command('build')
.description('打包压缩,默认 prod')
.action(() => {
const build = require('../build/tasks/build');
const config = generateConfig('build', commander.env || 'prod');
build(config);
});
commander.parse(process.argv);
if (!process.argv.slice(2).length) {
commander.outputHelp((text) => {
log.message(text);
return '';
});
}

View File

@ -1,8 +0,0 @@
const autoprefixer = require('autoprefixer');
const browsers = require('../helpers/browser');
module.ex = {
plugins: [
autoprefixer({ browsers })
]
};

View File

@ -1,486 +0,0 @@
const path = require('path');
const fs = require('fs');
const merge = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const FriendlyErrorsPlugin = require('@soda/friendly-errors-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlPlugin = require('html-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const autoprefixer = require('autoprefixer');
const browsers = require('../helpers/browser');
function handleGzipCompress(compress) {
if (!compress) return false;
if (typeof compress === 'boolean') {
return {};
}
return compress;
}
module.exports = function webpackConfig(configs, webpack, mode) {
let template = path.resolve(
configs.folders.PROJECT_DIR,
'./publish/index.html'
);
if (!fs.existsSync(template)) {
template = path.resolve(configs.folders.FES_DIR, './src/index.html');
}
const isDev = mode === 'dev';
const isBuild = mode === 'build';
const gzipCompress = handleGzipCompress(configs.compress);
const presets = [
[
require.resolve('@babel/preset-env')
]
];
const plugins = [
[require.resolve('@vue/babel-plugin-jsx')],
[
require.resolve('@babel/plugin-transform-runtime'), {
corejs: 3
}
],
require.resolve('@babel/plugin-proposal-object-rest-spread'),
require.resolve('@babel/plugin-syntax-dynamic-import')
];
const cssloaders = [
isDev
? {
loader: require.resolve('vue-style-loader'),
options: {
sourceMap: false,
shadowMode: false
}
}
: {
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../'
}
},
{
loader: require.resolve('css-loader'),
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: [
autoprefixer({ browsers })
]
},
sourceMap: false
}
}
];
const baseConfig = {
mode: isDev ? 'development' : 'production',
context: path.resolve(configs.folders.PROJECT_DIR),
entry: {
app: [
path.resolve(configs.folders.PROJECT_DIR, './src/.fes/fes.js')
]
},
resolve: {
extensions: ['.js', '.jsx', '.vue', '.json'],
alias: {
projectRoot: configs.folders.PROJECT_DIR,
'@': path.resolve(configs.folders.PROJECT_DIR, 'src'),
assets: path.resolve(configs.folders.PROJECT_DIR, './src/assets/')
}
},
output: {
globalObject: 'this',
filename: isDev ? 'js/[name].js' : 'js/[name].[contenthash:8].js',
chunkFilename: isDev ? 'js/[name].chunk.js' : 'js/[name].[contenthash:8].js',
path: configs.folders.PROJECT_DIST_DIR,
publicPath: isDev ? '/' : './'
},
module: {
// noParse: /^(vue|vue-router|vuex|vuex-router-sync|axios)$/,
// noParse: /fes-runtime/,
rules: [
/* config.module.rule('vue') */
{
test: /\.vue$/,
use: [
{
loader: require.resolve('cache-loader'),
options: {
cacheDirectory: path.resolve(configs.folders.PROJECT_DIR, 'node_modules/.cache/vue-loader')
}
},
{
loader: require.resolve('vue-loader'),
options: {
shadowMode: true,
cacheDirectory: path.resolve(configs.folders.PROJECT_DIR, 'node_modules/.cache/vue-loader'),
babelParserPlugins: ['jsx', 'classProperties', 'decorators-legacy']
}
}
]
},
/* config.module.rule('images') */
{
test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
use: [
{
loader: require.resolve('url-loader'),
options: {
limit: 4096,
fallback: {
loader: require.resolve('file-loader'),
options: {
name: isDev ? 'img/[name].[ext]' : 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
/* config.module.rule('svg') */
{
test: /\.(svg)(\?.*)?$/,
use: [
{
loader: require.resolve('file-loader'),
options: {
name: isDev ? 'img/[name].[ext]' : 'img/[name].[hash:8].[ext]'
}
}
]
},
/* config.module.rule('media') */
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use: [
{
loader: require.resolve('url-loader'),
options: {
limit: 4096,
fallback: {
loader: require.resolve('file-loader'),
options: {
name: isDev ? 'media/[name].[ext]' : 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
/* config.module.rule('fonts') */
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use: [
{
loader: require.resolve('url-loader'),
options: {
limit: 4096,
fallback: {
loader: require.resolve('file-loader'),
options: {
name: isDev ? 'fonts/[name].[ext]' : 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
/* config.module.rule('css') */
{
test: /\.css$/,
use: cssloaders
},
/* config.module.rule('postcss') */
{
test: /\.p(ost)?css$/,
use: cssloaders
},
/* config.module.rule('less') */
{
test: /\.less$/,
use: cssloaders.concat([
{
loader: require.resolve('less-loader'),
options: {
sourceMap: false,
javascriptEnabled: true
}
}
])
},
/* config.module.rule('stylus') */
{
test: /\.styl(us)?$/,
use: cssloaders.concat([
{
loader: require.resolve('stylus-loader'),
options: {
sourceMap: false,
preferPathResolver: 'webpack'
}
}
])
},
/* config.module.rule('js') */
{
test: /\.m?jsx?$/,
include(filePath) {
if (filePath.startsWith(path.resolve(process.cwd(), 'src'))) {
return true;
}
if (/fes-core.?src/.test(filePath)) {
return true;
}
if (/fes-plugin-[a-z-]+.?(src|index)/.test(filePath)) {
return true;
}
return false;
},
use: [
{
loader: require.resolve('cache-loader'),
options: {
cacheDirectory: path.resolve(configs.folders.PROJECT_DIR, 'node_modules/.cache/babel-loader')
}
},
{
loader: require.resolve('thread-loader')
},
{
loader: require.resolve('babel-loader'),
options: {
presets,
plugins
}
}
]
}
]
},
devtool: isDev && 'cheap-module-eval-source-map',
plugins: [
/* config.plugin('progress') */
new webpack.ProgressPlugin(),
/* config.plugin('vue-loader') */
new VueLoaderPlugin(),
/* config.plugin('define') */
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
'process.env': {
// NODE_ENV: isDev ? 'development' : 'production',
env: JSON.stringify(configs.env),
command: JSON.stringify(configs.command)
}
}),
/* config.plugin('clean dist') */
isBuild && new CleanWebpackPlugin(),
/* config.plugin('extract-css') */
isBuild
&& new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css'
}),
/* config.plugin('Copy static') */
isBuild
&& new CopyPlugin([
{
from: configs.folders.PROJECT_STATIC_DIR,
to: path.resolve(
configs.folders.PROJECT_DIST_DIR,
'static'
)
}
]),
/* config.plugin('optimize-css') */
isBuild
&& new OptimizeCssnanoPlugin({
sourceMap: false,
cssnanoOptions: {
preset: [
'default',
{
mergeLonghand: false,
cssDeclarationSorter: false
}
]
}
}),
/* config.plugin('hash-module-ids') */
isBuild
&& new webpack.HashedModuleIdsPlugin({
hashDigest: 'hex'
}),
/* config.plugin('固定一下 chunk id') */
isBuild
&& new webpack.NamedChunksPlugin((chunk) => {
if (chunk.name) {
return chunk.name;
}
// eslint-disable-next-line
const hash = require('hash-sum');
const joinedHash = hash(
Array.from(chunk.modulesIterable, m => m.id).join('_')
);
return `chunk-${joinedHash}`;
}),
// /* config.plugin('Copyright') */
// isBuild
// && new webpack.BannerPlugin(''),
/* config.plugin('case-sensitive-paths') */
new CaseSensitivePathsPlugin(),
/* config.plugin('friendly-errors') */
new FriendlyErrorsPlugin(),
isBuild && gzipCompress && new CompressionWebpackPlugin({ // gzip 压缩
filename: '[path][base].gz',
test: /\.js$|\.html$|\.css/,
threshold: 10240,
minRatio: 0.8,
...gzipCompress
}),
/* config.plugin('index.html') */
new HtmlPlugin({
template,
minify: isBuild && {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
collapseBooleanAttributes: true,
removeScriptTypeAttributes: true
}
})
]
};
if (isBuild) {
baseConfig.optimization = {
minimizer: [
new TerserPlugin({
test: /\.m?js(\?.*)?$/i,
chunkFilter: () => true,
warningsFilter: () => true,
extractComments: false,
sourceMap: true,
cache: true,
cacheKeys: defaultCacheKeys => defaultCacheKeys,
parallel: true,
include: undefined,
exclude: undefined,
minify: undefined,
terserOptions: {
output: {
comments: /^\**!|@preserve|@license|@cc_on/i
},
compress: {
arrows: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
inline: false,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
switches: false,
toplevel: false,
typeofs: false,
booleans: true,
if_return: true,
sequences: true,
unused: true,
conditionals: true,
dead_code: true,
evaluate: true
},
mangle: {
safari10: true
}
}
})
],
splitChunks: {
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
},
runtimeChunk: true
};
}
baseConfig.plugins = baseConfig.plugins.filter(plu => plu !== false);
let advancedConfig = {};
const projectWebpackConfigFile = path.resolve(configs.folders.PROJECT_DIR, 'webpack.config.js');
if (fs.existsSync(projectWebpackConfigFile)) {
console.log('[init] 加载项目个性webpack配置文件');
// eslint-disable-next-line
advancedConfig = require(projectWebpackConfigFile)(mode, configs, webpack);
}
return merge(baseConfig, advancedConfig);
};

View File

@ -1,8 +0,0 @@
module.exports = [
'Chrome >= 46',
'Firefox >= 45',
'Safari >= 10',
'Edge >= 13',
'iOS >= 10',
'Electron >= 0.36'
];

View File

@ -1,41 +0,0 @@
const path = require('path');
const fs = require('fs');
function generateConfig(command, env) {
// cli目录
const CLI_DIR = path.dirname(path.dirname(fs.realpathSync(process.argv[1])));
// 解决git-bash目录问题
const PROJECT_DIR = process.env.PWD || process.cwd();
const FES_DIR = path.resolve(PROJECT_DIR, './node_modules/@webank/fes-core');
const PROJECT_DIST_DIR = path.resolve(PROJECT_DIR, 'dist');
const PROJECT_TMP_DIR = path.resolve(PROJECT_DIR, './.fes');
const PROJECT_PAGE_DIR = path.resolve(PROJECT_DIR, './src/pages');
const PROJECT_CPN_DIR = path.resolve(PROJECT_DIR, './src/components');
const PROJECT_STATIC_DIR = path.join(PROJECT_DIR, './src/static');
const projectName = path.basename(PROJECT_DIR);
const fesConfigFile = path.join(PROJECT_DIR, 'fes.config.js');
const config = {
command,
env,
port: 5000,
projectName,
folders: {
CLI_DIR,
FES_DIR,
PROJECT_DIR,
PROJECT_STATIC_DIR,
PROJECT_DIST_DIR,
PROJECT_TMP_DIR,
PROJECT_PAGE_DIR,
PROJECT_CPN_DIR
}
};
// eslint-disable-next-line
const fesCofig = require(fesConfigFile);
return Object.assign({}, config, fesCofig);
}
module.exports = generateConfig;

View File

@ -1,49 +0,0 @@
const
http = require('http');
const webpack = require('webpack');
const express = require('express');
const open = require('open');
const path = require('path');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackDevMiddleware = require('webpack-dev-middleware');
const initMock = require('../mock/init.js');
module.exports = function createDevServer(port, defaultConfig) {
const hotMiddlewarePath = require.resolve('webpack-hot-middleware');
defaultConfig.entry.app.unshift(`${hotMiddlewarePath.replace(path.basename(hotMiddlewarePath), '')}client?reload=true`);
defaultConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
defaultConfig.plugins.push(new webpack.NamedModulesPlugin());
const app = express();
const compiler = webpack(defaultConfig);
// devServer 自带支持,添加自定义插件。
app.use(webpackDevMiddleware(compiler, {
lazy: false,
logLevel: 'silent',
watchOptions: {
aggregateTimeout: 300,
poll: 1000
},
stats: {
colors: true,
chunks: false,
timings: true
},
publicPath: defaultConfig.output.publicPath
}));
app.use(webpackHotMiddleware(compiler, {
log: false
}));
app.use('/static', express.static('src/static'));
// 初始化Mock数据
initMock(app);
defaultConfig.open && open(`http://localhost:${port}`);
http.createServer(app).listen(port);
};

View File

@ -1,25 +0,0 @@
const net = require('net');
function checkout(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
resolve(checkout(port + 1));
}
});
server.once('listening', () => {
server.close(() => {
resolve(port);
});
});
server.listen(port);
});
}
module.exports = function getPort(basePort) {
basePort = basePort || 5000;
return checkout(basePort);
};

View File

@ -1,13 +0,0 @@
const chalk = require('chalk');
module.exports = {
error(msg) {
return console.log(chalk.red(msg));
},
warn(msg) {
return console.log(chalk.yellow(msg));
},
message(msg) {
return console.log(chalk.cyan(msg));
}
};

View File

@ -1,142 +0,0 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const httpProxy = require('http-proxy');
const url = require('url');
const util = require('./util');
const proxy = httpProxy.createProxyServer();
global.router = express.Router();
/**
* 数据模拟函数
*/
function cgiMock() {
// eslint-disable-next-line
const option = getOption(arguments);
if (!option.url || !option.result) {
return;
}
// option.method is one of ['get','post','delete','put'...]
const method = option.method || 'use';
global.router[method.toLowerCase()](option.url, (req, res) => {
setTimeout(() => {
// set header
res.set(option.headers);
// set Content-Type
option.type && res.type(option.type);
// set status code
res.status(option.statusCode);
// set cookie
util.each(option.cookies, (item) => {
const name = item.name;
const value = item.value;
delete item.name;
delete item.value;
res.cookie(name, value, item);
});
// do result
if (util.isFunction(option.result)) {
option.result(req, res);
} else if (util.isArray(option.result) || util.isObject(option.result)) {
!option.type && res.type('json');
res.json(option.result);
} else {
!option.type && res.type('text');
res.send(option.result.toString());
}
}, option.timeout);
});
}
// 根据参数个数获取配置
function getOption(arg) {
const len = arg.length;
// 默认配置
const option = {
headers: {
'Cache-Control': 'no-cache'
},
statusCode: 200,
cookies: [],
timeout: 0
};
if (len === 0) {
return cgiMock;
} if (len === 1) {
const newOption = arg[0];
if (util.isObject(newOption)) {
util.each(newOption, (value, key) => {
if (key === 'headers') {
util.each(newOption.headers, (headervalue, headerkey) => {
option.headers[headerkey] = newOption.headers[headerkey];
});
} else {
option[key] = newOption[key];
}
});
}
} else {
option.url = arg[0];
option.result = arg[1];
}
return option;
}
// 把基于 cgiMockfile 的相对绝对转成绝对路径
function parsePath(value) {
return path.join(global.cgiMockFilePath, value);
}
// log proxy data
proxy.on('open', (proxySocket) => {
proxySocket.on('data', (chunk) => {
console.log(chunk.toString());
});
});
proxy.on('proxyRes', (proxyRes) => {
console.log('RAW Response from the target', JSON.stringify(proxyRes.headers, true, 2));
const cookie = proxyRes.headers['set-cookie'];
if (cookie && cookie.length > 0) {
for (let i = 0; i < cookie.length; i++) {
cookie[i] = cookie[i].replace('Secure', '');
}
}
});
proxy.on('error', (e) => {
console.log(e);
});
// 规则之外的请求转发
cgiMock.proxy = function (host) {
process.nextTick(() => {
global.router.use((req, res) => {
proxy.web(req, res, {
target: host,
secure: false
});
});
});
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('Host', url.parse(host).host);
});
};
// 读取文件内容
cgiMock.file = function (file) {
return fs.readFileSync(parsePath(file));
};
cgiMock.prefix = '/';
module.exports = cgiMock;

View File

@ -1,100 +0,0 @@
const Mock = require('mockjs');
const faker = require('faker');
const path = require('path');
const fs = require('fs');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const onFinished = require('on-finished');
const util = require('./util');
const cgiMock = require('./cgiMock');
const log = require('../helpers/log');
const main = {
init(app, argv, cwd) {
const defaultCgiMockFile = path.join(process.cwd(), 'mock.js');
if (fs.existsSync(defaultCgiMockFile)) {
this.app = app;
this.argv = argv;
this.cwd = cwd;
app.use(logger('dev'));
app.use(
bodyParser.urlencoded({
extended: false
})
);
app.use(cookieParser());
this.customRoute();
}
},
customRoute() {
const argv = this.argv;
const defaultCgiMockFile = path.join(process.cwd(), 'mock.js');
const home = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'];
let cgiMockFile;
if (argv) {
if (argv.f) {
if (process.platform === 'win32') {
cgiMockFile = path.resolve(this.cwd, this.argv.f);
} else if (argv.f[0] === '~') {
cgiMockFile = path.resolve(
home,
argv.f.replace(/^~\//, '')
);
} else {
cgiMockFile = path.resolve(this.cwd, this.argv.f);
}
} else {
cgiMockFile = defaultCgiMockFile;
}
} else {
cgiMockFile = defaultCgiMockFile;
}
global.cgiMockFilePath = path.resolve(cgiMockFile, '..');
const loadRouteConfig = function () {
util.cleanCache(cgiMockFile);
try {
if (!fs.existsSync(cgiMockFile)) {
log.error('[WARN] 不存在mock.js文件');
} else {
// eslint-disable-next-line
const projectMock = require(cgiMockFile);
if (util.isFunction(projectMock)) {
global.router.stack = [];
projectMock(cgiMock, Mock, faker);
log.message('[SUCCESS] mock.js 加载成功');
} else {
log.error(
`[ERROR] mock.js cannot be ${typeof projectMock}`
);
}
}
} catch (e) {
log.error('[ERROR] mock.js 有误,请检查');
log.error(JSON.stringify(e));
}
};
loadRouteConfig();
this.app.use(cgiMock.prefix, (req, res, next) => {
onFinished(res, () => {
loadRouteConfig();
});
global.router(req, res, next);
});
util.watchFile(cgiMockFile, () => {
log.message('[INFO] mock.js 发生变化');
loadRouteConfig();
});
}
};
module.exports = main.init.bind(main);

View File

@ -1,17 +0,0 @@
const express = require('express');
const argv = require('yargs').argv;
const port = argv.p || 8888;
const cwd = process.cwd();
const app = express();
const init = require('../init');
init(app, argv, cwd);
app.set('port', port);
app.listen(port, () => {
console.log(`cgiMock server listening on ${port}`);
});

View File

@ -1,64 +0,0 @@
module.exports = function mock(cgiMock, Mock) {
const Random = Mock.Random;
// 前缀,全局(可选)
cgiMock.prefix = '/prefix';
// 返回一个数字
cgiMock('/number', 123);
// 返回一个json
cgiMock({
url: '/json',
result: {
code: '400101', msg: "不合法的请求:Missing cookie 'wb_app_id' for method parameter of type String", transactionTime: '20170309171146', success: false
}
});
// 利用mock.js 产生随机文本
cgiMock('/text', Random.cparagraph());
// 返回一个字符串 利用mock.js 产生随机字符
cgiMock('/string', Mock.mock({
'string|1-10': '★'
}));
// 正则匹配url, 返回一个字符串
cgiMock(/\/abc|\/xyz/, 'regexp test!');
// option.result 参数如果是一个函数, 可以实现自定义返回内容, 接收的参数是是经过 express 封装的 req 和 res 对象.
cgiMock(/\/function$/, (req, res) => {
res.send('function test');
});
// 返回文本 fs.readFileSync
cgiMock('/file', cgiMock.file('./test.json'));
// 更复杂的规则配置
cgiMock({
url: /\/who/,
method: 'GET',
result(req, res) {
if (req.query.name === 'kwan') {
res.json({ kwan: '孤独患者' });
} else {
res.send('Nooooooooooo');
}
},
headers: {
'Content-Type': 'text/plain',
'Content-Length': '123',
ETag: '12345'
},
cookies: [
{
name: 'myname', value: 'kwan', maxAge: 900000, httpOnly: true
}
],
// 接口随机延迟
timeout: Mock.mock({
'number|1000-5000': 1000
}).number
});
};

View File

@ -1 +0,0 @@
file test

View File

@ -1,62 +0,0 @@
const fs = require('fs');
const toString = Object.prototype.toString;
module.exports = {
isArray(value) {
return toString.call(value) === '[object Array]';
},
isObject(value) {
return toString.call(value) === '[object Object]';
},
isFunction(value) {
return toString.call(value) === '[object Function]';
},
each(val, callback) {
if (this.isArray(val)) {
val.forEach(callback);
}
if (this.isObject(val)) {
Object.keys(val).forEach((key) => {
callback(val[key], key);
});
}
},
watchFile(filename, callback) {
const isWin = (process.platform === 'win32');
if (isWin) {
return fs.watch(filename, (event) => {
if (event === 'change') {
return callback(filename);
}
return null;
});
}
return fs.watchFile(filename, {
interval: 200
}, (curr, prev) => {
if (curr.mtime > prev.mtime) {
return callback(filename);
}
return null;
});
},
unwatchFile(watcher, filename) {
if (watcher) {
watcher.close && watcher.close();
} else {
fs.unwatchFile(filename);
}
},
cleanCache(modulePath) {
if (require.cache[modulePath]) {
delete require.cache[modulePath];
}
}
};

View File

@ -1,23 +0,0 @@
const webpack = require('webpack');
const log = require('../helpers/log');
const createProdConfig = require('../configs/webpack.config');
const generateRoute = require('./route');
function startBuild(config) {
try {
generateRoute(config);
const webpackConfig = createProdConfig(config, webpack, 'build');
webpack(webpackConfig, (err) => {
if (err) {
log.error(err);
return;
}
console.log('[build] success');
});
} catch (e) {
log.error(e);
}
}
module.exports = startBuild;

View File

@ -1,40 +0,0 @@
const webpack = require('webpack');
const { ServiceWithBuiltIn } = require('@webank/fes-core');
const { yParser } = require('@umijs/utils');
const getPkg = require('../helpers/getPkg');
const getCwd = require('../helpers/getCwd');
const createDevServer = require('../helpers/createDevServer');
const getPort = require('../helpers/getPort');
const log = require('../helpers/log');
const createDevConfig = require('../configs/webpack.config');
const args = yParser(process.argv.slice(2));
// TODO 监听 pages 等文件变更重新编译
async function startDev(config) {
const service = new ServiceWithBuiltIn({
cwd: getCwd(),
pkg: getPkg(process.cwd())
});
await service.run({
name: 'dev',
args
});
const webpackConfig = createDevConfig(config, webpack, 'dev');
if (!webpackConfig) return;
getPort(config.port)
.then((port) => {
log.message(`------------ find port success. port: ${port}`);
createDevServer(port, webpackConfig);
}).catch((err) => {
log.message('------------ build error.');
log.error(err);
});
}
module.exports = startDev;

View File

@ -1,13 +0,0 @@
const init = require('./init.js');
const route = require('./route.js');
const build = require('./build.js');
const dev = require('./dev.js');
const update = require('./update.js');
module.exports = {
init,
route,
build,
dev,
update
};

View File

@ -1,55 +0,0 @@
const path = require('path');
const fs = require('fs-extra');
const prompts = require('prompts');
const tar = require('tar');
const { execSync } = require('child_process');
const log = require('../helpers/log');
function createProject(config, projectName) {
log.message('正在初始化项目...');
const projectDir = path.resolve(config.folders.PROJECT_DIR, projectName);
if (fs.pathExistsSync(projectDir)) {
log.error('该项目已存在,请重新输入!');
return Promise.reject();
}
return new Promise((resolve) => {
const productDir = `${config.folders.PROJECT_DIR}/${projectName}`;
const stdout = execSync('npm pack @webank/fes-template', { encoding: 'utf8', stdio: [null] });
const filePath = path.resolve(config.folders.PROJECT_DIR, stdout.replace('\n', ''));
fs.mkdirSync(projectDir);
fs.createReadStream(filePath).pipe(
tar.x({
strip: 1,
C: productDir // alias for cwd:'some-dir', also ok
})
);
fs.unlinkSync(filePath);
log.message(`项目 ${projectName} 创建完成,请执行下面的命令进行使用:`);
log.message(`$ cd ${projectName}`);
log.message('$ npm i');
log.message('$ npm run dev');
resolve();
});
}
async function initProject(config, projectName) {
if (projectName) {
await createProject(config, projectName);
} else {
const response = await prompts([
{
type: 'text',
name: 'name',
message: '请输入项目名称: '
}
]);
if (!response.name) {
await initProject(config, projectName);
} else {
await createProject(config, response.name);
}
}
}
module.exports = initProject;

View File

@ -1,15 +0,0 @@
const { exec } = require('child_process');
const log = require('../helpers/log');
function update(config) {
log.message('安装@webank/fes-core @webank/fes-ui...');
exec(`cd ${config.folders.PROJECT_DIR} && npm i @webank/fes-core @webank/fes-ui --save && npm i`, (err) => {
if (err) {
console.error(err);
return;
}
log.message('升级完毕');
});
}
module.exports = update;

View File

@ -1,4 +1,5 @@
module.exports = {
foo: () => {}
module.exports = function () {
// TODO 模块导出
console.log('Hello fes');
};

View File

@ -6,10 +6,16 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"files": [
"lib",
"dist",
"bin"
],
"bin": {
"fes": "./bin/index.js"
"fes": "./bin/fes.js"
},
"main": "index.js",
"module": "dist/index.esm.js",
"author": "harrywan,qlin",
"repository": {
"type": "git",
@ -24,88 +30,10 @@
"strong"
],
"dependencies": {
"@babel/core": "^7.12.3",
"@babel/helper-module-imports": "^7.12.1",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-decorators": "^7.12.1",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/runtime": "^7.12.1",
"@babel/runtime-corejs3": "^7.12.1",
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
"@soda/friendly-errors-webpack-plugin": "^1.7.1",
"@umijs/utils": "^3.2.24",
"@vue/babel-plugin-jsx": "^1.0.0-rc.3",
"@vue/compiler-sfc": "^3.0.2",
"@webank/fes-plugin-built-in": "^2.0.0",
"@webank/fes-core": "^2.0.0",
"@webank/fes-runtime": "^2.0.0",
"autoprefixer": "^8.1.0",
"babel-loader": "^8.0.6",
"body-parser": "^1.5.2",
"cache-loader": "^4.1.0",
"case-sensitive-paths-webpack-plugin": "^2.2.0",
"chalk": "^4.1.0",
"chokidar": "^1.7.0",
"clean-webpack-plugin": "^3.0.0",
"commander": "^4.1.0",
"compression-webpack-plugin": "^6.0.3",
"cookie-parser": "^1.4.3",
"copy-webpack-plugin": "^5.0.4",
"cross-spawn": "^2.1.0",
"css-loader": "^3.1.0",
"execa": "^0.8.0",
"express": "^4.14.0",
"express-http-proxy": "^0.10.0",
"express-session": "^1.7.2",
"faker": "^4.1.0",
"file-loader": "^4.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"fs": "0.0.2",
"fs-extra": "^8.1.0",
"hash-sum": "^2.0.0",
"html-webpack-plugin": "^3.2.0",
"http-proxy": "^1.12.0",
"json-templater": "^1.2.0",
"less": "^3.12.2",
"less-loader": "^7.0.2",
"lodash": "^4.17.4",
"mini-css-extract-plugin": "^0.8.0",
"mockjs": "^1.1.0",
"morgan": "^1.2.2",
"mustache": "^4.0.1",
"node-plus-string": "^1.0.1",
"normalize-path": "^1.0.0",
"on-finished": "^2.3.0",
"open": "^7.3.0",
"path": "^0.12.7",
"pkg-up": "^3.1.0",
"postcss": "^7.0.35",
"postcss-loader": "^4.0.4",
"prompts": "^2.3.0",
"request": "^2.81.0",
"require-dir": "^0.3.0",
"resolve": "^1.18.1",
"shelljs": "^0.5.3",
"string-replace-loader": "^2.2.0",
"strip-indent": "^2.0.0",
"style-loader": "^1.3.0",
"stylus": "^0.54.8",
"stylus-loader": "^3.0.2",
"tar": "^6.0.5",
"tar-fs": "^1.16.0",
"terser-webpack-plugin": "^2.2.1",
"thread-loader": "^2.1.3",
"url-loader": "^2.2.0",
"vue-loader": "^16.0.0-beta.8",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.2",
"yargs": "^3.31.0"
"@umijs/utils": "3.2.24",
"resolve-cwd": "^3.0.0"
}
}

69
packages/fes/src/cli.js Normal file
View File

@ -0,0 +1,69 @@
import {
chalk,
yParser
} from '@umijs/utils';
import {
Service
} from './serviceWithBuiltIn';
import fork from './utils/fork';
import getCwd from './utils/getCwd';
import getPkg from './utils/getPkg';
// process.argv: [node, fes.js, command, args]
const args = yParser(process.argv.slice(2), {
alias: {
version: ['v'],
help: ['h']
},
boolean: ['version']
});
// TODO version 命令
if (args.version && !args._[0]) {
args._[0] = 'version';
console.log(`fes@${require('../package.json').version}`);
} else if (!args._[0]) {
// TODO 帮助命令
args._[0] = 'help';
}
(async () => {
try {
switch (args._[0]) {
case 'dev':
// eslint-disable-next-line
const child = fork({
scriptPath: require.resolve('./forkedDev')
});
// ref:
// http://nodejs.cn/api/process/signal_events.html
process.on('SIGINT', () => {
child.kill('SIGINT');
process.exit(1);
});
process.on('SIGTERM', () => {
child.kill('SIGTERM');
process.exit(1);
});
break;
default:
// eslint-disable-next-line
const name = args._[0];
if (name === 'build') {
process.env.NODE_ENV = 'production';
}
await new Service({
cwd: getCwd(),
pkg: getPkg(process.cwd())
}).run({
name,
args
});
break;
}
} catch (e) {
console.error(chalk.red(e.message));
console.error(e.stack);
process.exit(1);
}
})();

View File

@ -0,0 +1,48 @@
import { chalk, yParser } from '@umijs/utils';
import { Service } from './serviceWithBuiltIn';
import getCwd from './utils/getCwd';
import getPkg from './utils/getPkg';
const args = yParser(process.argv.slice(2));
let closed = false;
function onSignal(signal, service) {
if (closed) return;
closed = true;
// 退出时触发插件中的onExit事件
service.applyPlugins({
key: 'onExit',
type: service.ApplyPluginsType.event,
args: {
signal
}
});
process.exit(0);
}
(async () => {
try {
process.env.NODE_ENV = 'development';
const service = new Service({
cwd: getCwd(),
pkg: getPkg(process.cwd())
});
await service.run({
name: 'dev',
args
});
// kill(2) Ctrl-C
process.once('SIGINT', () => onSignal('SIGINT', service));
// kill(3) Ctrl-\
process.once('SIGQUIT', () => onSignal('SIGQUIT', service));
// kill(15) default
process.once('SIGTERM', () => onSignal('SIGTERM', service));
} catch (e) {
console.error(chalk.red(e.message));
console.error(e.stack);
process.exit(1);
}
})();

15
packages/fes/src/index.js Normal file
View File

@ -0,0 +1,15 @@
export {
useRoute,
useRouter,
onBeforeRouteUpdate,
onBeforeRouteLeave,
RouterLink,
useLink,
createWebHashHistory,
createRouter,
Plugin,
ApplyPluginsType
} from '@webank/fes-runtime';
// @ts-ignore
export * from '@@/core/fesExports';

View File

@ -1,12 +1,12 @@
import { dirname } from 'path';
import CoreService from './Service/Service';
import innerPlugins from './plugins';
import { Service as CoreService } from '@webank/fes-core';
import innerPlugins from '@webank/fes-plugin-built-in';
// TODO 迁移到 fes 目录
class ServiceWithBuiltIn extends CoreService {
class Service extends CoreService {
constructor(opts) {
process.env.FES_VERSION = require('../package').version;
process.env.FES_DIR = dirname(require.resolve('../package'));
process.env.UMI_DIR = dirname(require.resolve('../package'));
super({
...opts,
@ -15,4 +15,4 @@ class ServiceWithBuiltIn extends CoreService {
}
}
export default ServiceWithBuiltIn;
export { Service };

View File

@ -0,0 +1,60 @@
import {
fork
} from 'child_process';
const usedPorts = [];
let CURRENT_PORT;
export default function start({
scriptPath
}) {
const execArgv = process.execArgv.slice(0);
const inspectArgvIndex = execArgv.findIndex(argv => argv.includes('--inspect-brk'),);
if (inspectArgvIndex > -1) {
const inspectArgv = execArgv[inspectArgvIndex];
execArgv.splice(
inspectArgvIndex,
1,
inspectArgv.replace(/--inspect-brk=(.*)/, (match, s1) => {
let port;
try {
port = parseInt(s1, 10) + 1;
} catch (e) {
port = 9230; // node default inspect port plus 1.
}
if (usedPorts.includes(port)) {
port += 1;
}
usedPorts.push(port);
return `--inspect-brk=${port}`;
}),
);
}
// set port to env when current port has value
if (CURRENT_PORT) {
// @ts-ignore
process.env.PORT = CURRENT_PORT;
}
const child = fork(scriptPath, process.argv.slice(2), {
execArgv
});
child.on('message', (data) => {
const type = (data && data.type) || null;
if (type === 'RESTART') {
child.kill();
start({
scriptPath
});
} else if (type === 'UPDATE_PORT') {
// set current used port
CURRENT_PORT = data.port;
}
process.send && process.send(data);
});
return child;
}

View File

@ -1,6 +1,6 @@
const { join, isAbsolute } = require('path');
import { join, isAbsolute } from 'path';
module.exports = () => {
export default () => {
const cwd = process.cwd();
if (process.env.APP_ROOT) {
// avoid repeat cwd path

View File

@ -1,7 +1,7 @@
const { join } = require('path');
const getCwd = require('./getCwd');
import { join } from 'path';
import getCwd from './getCwd';
module.exports = (dir) => {
export default (dir) => {
try {
// eslint-disable-next-line
return require(join(getCwd(), 'package.json'));

3099
yarn.lock

File diff suppressed because it is too large Load Diff