Compare commits

..

No commits in common. "v4.0.0-beta.1" and "master" have entirely different histories.

596 changed files with 19093 additions and 19150 deletions

3
.gitignore vendored
View File

@ -1,10 +1,7 @@
.DS_Store
.idea
.history
.cache
.turbo
.temp
.hound
.fes

3
.npmrc
View File

@ -1,4 +1,3 @@
registry=https://registry.npmmirror.com
link-workspace-packages=true
prefer-workspace-packages=true
auto-install-peers=true
shamefully-hoist=true

View File

@ -1,22 +1,3 @@
# [4.0.0-beta.1](https://github.com/WeBankFinTech/fes.js/compare/v4.0.0-beta.0...v4.0.0-beta.1) (2025-09-22)
# [4.0.0-beta.0](https://github.com/WeBankFinTech/fes.js/compare/v3.4.12...v4.0.0-beta.0) (2025-09-16)
### Bug Fixes
* 优化模版样式 ([effd137](https://github.com/WeBankFinTech/fes.js/commit/effd1378b44dfdfab47ff46ccf1b7e6e0d4d7e66))
* 使用 pathToFileURL 确保 Windows 下路径导入正确 ([ed26dfb](https://github.com/WeBankFinTech/fes.js/commit/ed26dfb39b753f7d28ab1e3d6bd43fb90c972427))
### Features
* esm ([c4c081a](https://github.com/WeBankFinTech/fes.js/commit/c4c081ae3a279ec891236460386b48cf7128d737))
## [3.4.12](https://github.com/WeBankFinTech/fes.js/compare/v3.4.11...v3.4.12) (2025-06-24)

View File

@ -56,10 +56,12 @@ It mainly has the following functions:
| [@fesjs/plugin-access](http://fesjs.mumblefe.cn/reference/plugin/plugins/access.html) | Provides the ability to control the permissions of page resources |
| [@fesjs/plugin-enums](http://fesjs.mumblefe.cn/reference/plugin/plugins/enums.html#%E4%BB%8B%E7%BB%8D) | Provide unified enumeration access and rich functions to handle enumeration |
| [@fesjs/plugin-icon](http://fesjs.mumblefe.cn/reference/plugin/plugins/icon.html#%E4%BB%8B%E7%BB%8D) | svg file is automatically registered as a component |
| [@fesjs/plugin-jest](http://fesjs.mumblefe.cn/reference/plugin/plugins/jest.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | Based on `Jest`, provide unit testing and coverage testing capabilities |
| [@fesjs/plugin-layout](http://fesjs.mumblefe.cn/reference/plugin/plugins/layout.html) | Simple configuration to have a layout, including navigation and sidebar |
| [@fesjs/plugin-locale](http://fesjs.mumblefe.cn/reference/plugin/plugins/locale.html#%E4%BB%8B%E7%BB%8D) | Based on `Vue I18n`, providing internationalization capabilities |
| [@fesjs/plugin-model](http://fesjs.mumblefe.cn/reference/plugin/plugins/model.html#%E4%BB%8B%E7%BB%8D) | Simple data management solution |
| [@fesjs/plugin-request](http://fesjs.mumblefe.cn/reference/plugin/plugins/request.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | Based on the request encapsulated by `Axios`, built-in functions such as preventing repeated requests, request throttling, and error handling |
| [@fesjs/plugin-vuex](http://fesjs.mumblefe.cn/reference/plugin/plugins/vuex.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | Based on `Vuex`, provide state management capabilities |
| [@fesjs/plugin-qiankun](http://fesjs.mumblefe.cn/reference/plugin/plugins/qiankun.html#%E4%BB%8B%E7%BB%8D) | Based on `qiankun`, provide microservice capabilities |
| [@fesjs/plugin-sass](http://fesjs.mumblefe.cn/reference/plugin/plugins/sass.html#%E4%BB%8B%E7%BB%8D) | Style support sass |
| [@fesjs/plugin-monaco-editor](http://fesjs.mumblefe.cn/reference/plugin/plugins/editor.html#%E4%BB%8B%E7%BB%8D) | Provide code editor capability, based on `monaco-editor` (code editor used by VS Code) |

30
build.config.js Normal file
View File

@ -0,0 +1,30 @@
export default {
pkgs: [
'create-fes-app',
'fes',
'fes-compiler',
'fes-preset-built-in',
'fes-builder-vite',
'fes-builder-webpack',
'fes-runtime',
'fes-utils',
'fes-plugin-access',
'fes-plugin-enums',
'fes-plugin-icon',
'fes-plugin-jest',
'fes-plugin-layout',
'fes-plugin-locale',
'fes-plugin-model',
'fes-plugin-monaco-editor',
'fes-plugin-qiankun',
'fes-plugin-request',
'fes-plugin-sass',
'fes-plugin-vuex',
'fes-plugin-pinia',
'fes-plugin-windicss',
'fes-plugin-watermark',
'fes-plugin-login',
'fes-plugin-swc',
],
copy: [],
};

View File

@ -1,18 +0,0 @@
# 从 3.x 迁移到 4.x
构建模块从 commonjs 切换到 esm
## 需调整内容
- package.json 添加 `"type": "module"`
## 版本 4.x 的 break
1. [@fesjs/builder-vite]: vite5 升级到 [vite7](https://cn.vitejs.dev/guide/migration.html)
2. [@fesjs/plugin-pinia]: pinia 2.x > [3.x](https://github.com/vuejs/pinia/releases/tag/v3.0.0)
## 插件
- 移除插件[@fesjs/plugin-vuex]
- 移除插件[@fesjs/plugin-windicss]
- 移除插件[@fesjs/plugin-jest]

View File

@ -2,11 +2,11 @@
import antfu from '@antfu/eslint-config';
export default await antfu({
// TODO: 使用 ignore 代替 cli 命令中的配置
stylistic: {
indent: 4,
quotes: 'single',
semi: 'always',
ignores: ['*.yaml'],
},
typescript: true,
vue: true,

8
jest.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
testPathIgnorePatterns: [
'/node_modules/',
'fes-template',
'fes-template-h5'
]
};

View File

@ -1,9 +1,9 @@
{
"name": "fes.js",
"type": "module",
"version": "4.0.0-beta.1",
"version": "3.4.12",
"private": true,
"packageManager": "pnpm@10.14.0",
"packageManager": "pnpm@8.6.6",
"description": "一个好用的前端管理台快速开发框架",
"preferGlobal": true,
"workspaces": [
@ -19,8 +19,8 @@
],
"scripts": {
"bootstrap": "pnpm i",
"dev": "turbo run watch",
"build": "turbo run build",
"dev": "node scripts/build.mjs --watch",
"build": "node scripts/build.mjs",
"release": "node scripts/release.mjs",
"clean": "rm -rf ./node_modules & rm -rf packages/**/node_modules & rm -rf packages/**/.cache",
"docs:dev": "vitepress dev docs",
@ -32,30 +32,29 @@
"hooks:sync": "simple-git-hooks"
},
"dependencies": {
"consola": "^3.4.2",
"conventional-changelog-cli": "^5.0.0",
"chalk": "^5.0.1",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.3.6",
"execa": "^6.1.0",
"minimist": "^1.2.6",
"picocolors": "^1.1.1",
"semver": "^7.3.6",
"tsup": "^8.5.0",
"turbo": "^2.5.6"
"semver": "^7.3.6"
},
"devDependencies": {
"@antfu/eslint-config": "^5.2.2",
"@antfu/eslint-config": "^3.8.0",
"@commitlint/cli": "^18.4.4",
"@commitlint/config-conventional": "^18.4.4",
"chokidar": "^3.5.3",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"deepmerge": "^4.2.2",
"eslint": "^9.34.0",
"fs-extra": "^11.3.1",
"eslint": "^9.13.0",
"fs-extra": "^11.1.1",
"lint-staged": "^15.2.0",
"simple-git-hooks": "^2.9.0",
"typescript": "^5.9.2",
"typescript": "^5.6.3",
"vitepress": "1.0.0-alpha.73",
"vue": "^3.5.21"
"vue": "^3.3.4",
"yargs-parser": "^21.1.1"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged",

View File

@ -1,40 +0,0 @@
import type { IPluginAPI } from '@fesjs/shared';
import type { InlineConfig } from 'vite';
import { existsSync } from 'node:fs';
import process from 'node:process';
import { build } from 'vite';
import getBuildConfig from './getBuildConfig';
export default function (api: IPluginAPI) {
const {
paths,
utils: { rimraf },
} = api;
api.registerCommand({
command: 'build',
description: 'build application for production',
async fn() {
rimraf.sync(paths.absTmpPath);
// generate files
await api.applyPlugins({
key: 'onGenerateFiles',
type: api.ApplyPluginsType.event,
});
const bundleConfig: InlineConfig = await getBuildConfig(api);
// clear output path before exec build
if (process.env.CLEAR_OUTPUT !== 'none') {
if (paths.absOutputPath && existsSync(paths.absOutputPath)) {
rimraf.sync(paths.absOutputPath);
}
}
await build(bundleConfig);
if (process.env.RM_TMPDIR !== 'none') {
rimraf.sync(paths.absTmpPath);
}
},
});
}

View File

@ -1,26 +0,0 @@
import type { Connect } from 'vite';
import { join } from 'node:path';
import historyFallback from 'connect-history-api-fallback';
import { pathExistsSync } from 'fs-extra/esm';
interface ViteConfig {
publicDir: string;
}
interface HistoryParams {
[key: string]: any;
}
function proxyMiddleware(viteConfig: ViteConfig, params: HistoryParams): Connect.NextHandleFunction {
return (req: any, res: any, next: any) => {
const fileName = join(viteConfig.publicDir, req.url);
if (req.url.length > 1 && req.url.startsWith('/') && pathExistsSync(fileName)) {
return next();
}
const history = historyFallback(params);
history(req, res, next);
};
}
export default proxyMiddleware;

View File

@ -1,22 +0,0 @@
import type { IPluginAPI } from '@fesjs/shared';
export default (api: IPluginAPI) => {
api.describe({
key: 'viteOption',
config: {
schema(joi: any) {
return joi.object();
},
default: {},
},
});
api.describe({
key: 'vite',
config: {
schema(joi: any) {
return joi.object();
},
default: {},
},
});
};

View File

@ -1,30 +0,0 @@
import { join } from 'node:path';
import { OWNER_DIR, ViteBuildConfig } from './shared';
interface BuilderPlugin {
plugins: string[];
}
export { ViteBuildConfig };
export default function (): BuilderPlugin {
return {
plugins: [
join(OWNER_DIR, 'dist/registerBuilder.mjs'),
join(OWNER_DIR, 'dist/registerMethods.mjs'),
join(OWNER_DIR, 'dist/registerType.mjs'),
// bundle configs
join(OWNER_DIR, 'dist/features/viteHtml.mjs'),
join(OWNER_DIR, 'dist/features/viteOption.mjs'),
join(OWNER_DIR, 'dist/features/viteVueJsx.mjs'),
join(OWNER_DIR, 'dist/features/viteVuePlugin.mjs'),
join(OWNER_DIR, 'dist/features/viteAnalyze.mjs'),
join(OWNER_DIR, 'dist/features/viteLegacy.mjs'),
// commands
join(OWNER_DIR, 'dist/commands/build/index.mjs'),
join(OWNER_DIR, 'dist/commands/dev/index.mjs'),
],
};
}

View File

@ -1,7 +0,0 @@
import type { IPluginAPI } from '@fesjs/shared';
export default function (api: IPluginAPI) {
api.registerBuilder({
name: 'vite',
});
}

View File

@ -1,19 +0,0 @@
import type { Options as PolyfillOptions } from '@vitejs/plugin-legacy';
import type { Options } from '@vitejs/plugin-vue';
import type createPlugin from '@vitejs/plugin-vue-jsx';
import type { HTMLOptions, UserConfig } from 'vite';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
export const OWNER_DIR: string = join(dirname(fileURLToPath(import.meta.url)), '..');
export interface ViteBuildConfig {
viteOption?: UserConfig;
vite?: UserConfig;
viteVuePlugin?: Options;
viteVueJsx?: Parameters<typeof createPlugin>[0];
viteLegacy?: PolyfillOptions;
viteHtml?: HTMLOptions;
viteAnalyze?: any;
viteOptionConfig?: UserConfig;
}

View File

@ -1,9 +0,0 @@
{
"extends": ["@fesjs/typescript-config/base.json"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

File diff suppressed because one or more lines are too long

View File

@ -1,24 +0,0 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: [
'src/index.ts',
'src/registerBuilder.ts',
'src/registerMethods.ts',
'src/registerType.ts',
'src/features/viteHtml.ts',
'src/features/viteOption.ts',
'src/features/viteVueJsx.ts',
'src/features/viteVuePlugin.ts',
'src/features/viteAnalyze.ts',
'src/features/viteLegacy.ts',
'src/commands/build/index.ts',
'src/commands/dev/index.ts',
],
splitting: false,
sourcemap: false,
clean: true,
dts: true,
shims: true,
format: ['esm'],
});

View File

@ -1,21 +0,0 @@
import type { ServerOptions } from 'vite';
// eslint-disable-next-line antfu/no-import-dist
import type { ViteBuildConfig } from './dist/index.d.mjs';
declare module '@fesjs/fes' {
interface PluginBuildConfig extends ViteBuildConfig {
}
interface FesConfig {
terserOptions?: any;
inlineLimit?: number;
outputPath?: string;
proxy?: ServerOptions['proxy'];
title?: string;
mountElementId?: string;
publicPath?: string;
alias?: Record<string, string>;
autoprefixer?: any;
}
}

View File

@ -1,46 +0,0 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { WebpackBuildConfig } from './shared';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export {
WebpackBuildConfig,
};
export default function () {
return {
plugins: [
join(__dirname, './plugins/registerBuilder.mjs'),
// register methods
join(__dirname, './plugins/registerMethods.mjs'),
join(__dirname, './plugins/registerType.mjs'),
// bundle configs
join(__dirname, './plugins/features/analyze.mjs'),
join(__dirname, './plugins/features/chainWebpack.mjs'),
join(__dirname, './plugins/features/cssLoader.mjs'),
join(__dirname, './plugins/features/copy.mjs'),
join(__dirname, './plugins/features/devServer.mjs'),
join(__dirname, './plugins/features/devtool.mjs'),
join(__dirname, './plugins/features/externals.mjs'),
join(__dirname, './plugins/features/exportStatic.mjs'),
join(__dirname, './plugins/features/extraBabelPlugins.mjs'),
join(__dirname, './plugins/features/extraBabelPresets.mjs'),
join(__dirname, './plugins/features/extraPostCSSPlugins.mjs'),
join(__dirname, './plugins/features/html.mjs'),
join(__dirname, './plugins/features/lessLoader.mjs'),
join(__dirname, './plugins/features/postcssLoader.mjs'),
join(__dirname, './plugins/features/nodeModulesTransform.mjs'),
join(__dirname, './plugins/features/vueLoader.mjs'),
join(__dirname, './plugins/features/extraCSS.mjs'),
// commands
join(__dirname, './plugins/commands/build/index.mjs'),
join(__dirname, './plugins/commands/dev/index.mjs'),
join(__dirname, './plugins/commands/webpack/index.mjs'),
],
};
}

View File

@ -1,9 +0,0 @@
const pitcher = (code: string): string => code;
export function pitch(this: any) {
if (/&blockType=config/.test(this.resourceQuery)) {
return '';
}
}
export default pitcher;

View File

@ -1,28 +0,0 @@
import type Config from 'webpack-5-chain';
import type { WebpackBuildConfig } from '../../../shared';
import { resolveRuntimeEnv, stringifyObjValue } from '@fesjs/utils';
import webpack from 'webpack';
interface CreateDefineWebpackConfigOptions {
config: WebpackBuildConfig;
publicPath?: string;
webpackConfig: Config;
}
export default function createDefineWebpackConfig({ config, publicPath, webpackConfig }: CreateDefineWebpackConfigOptions) {
const env = stringifyObjValue(resolveRuntimeEnv(publicPath || ''));
const define = stringifyObjValue({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
...(config as any).define,
});
webpackConfig.plugin('define').use(webpack.DefinePlugin, [
{
'process.env': env,
...define,
},
]);
}

View File

@ -1,107 +0,0 @@
import type { IPluginAPI } from '@fesjs/shared';
import type { WebpackBuildConfig } from '../../../shared';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveRuntimeEnv, winPath } from '@fesjs/utils';
import { esmResolve } from '../../../shared';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
interface CreateHtmlWebpackConfigOptions {
api: IPluginAPI<WebpackBuildConfig>;
cwd: string;
config: WebpackBuildConfig;
webpackConfig: any;
headScripts?: () => Promise<Array<{ src: string }>>;
isProd: boolean;
publicPath?: string;
}
interface Route {
path: string;
meta?: {
title?: string;
};
children?: Route[];
}
export default async function createHtmlWebpackConfig({ api, cwd, config, webpackConfig, headScripts, isProd, publicPath }: CreateHtmlWebpackConfigOptions) {
const htmlOptions: any = {
filename: '[name].html',
...config.html,
templateParameters: {
title: (api.config as any).title || config.html?.title || 'fes.js',
...resolveRuntimeEnv(publicPath || ''),
mountElementId: (config as any).mountElementId,
},
};
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
},
});
}
const htmlPath = join(cwd, 'index.html');
const defaultHtmlPath = join(__dirname, '../index-default.html');
const publicCopyIgnore: string[] = [];
// default, single page setup.
htmlOptions.template = existsSync(htmlPath) ? htmlPath : defaultHtmlPath;
publicCopyIgnore.push(winPath(htmlOptions.template));
webpackConfig.plugin('html').use(esmResolve('html-webpack-plugin'), [htmlOptions]);
// 如果需要导出html则根据路由生成对应的html文件
if ((config as any).exportStatic) {
const routes: Route[] = await (api as any).getRoutes();
const addHtml = (_routes: Route[] | undefined) => {
if (Array.isArray(_routes)) {
_routes.forEach((route) => {
const _fileName = `${route.path.slice(1) || 'index'}.html`;
if (_fileName !== 'index.html') {
const _htmlOptions = {
...config.html,
filename: _fileName,
templateParameters: {
title: route?.meta?.title || config.html?.title || (api.config as any).title || 'fes.js',
...resolveRuntimeEnv(publicPath!),
mountElementId: (config as any).mountElementId,
},
};
webpackConfig.plugin(_fileName).use(esmResolve('html-webpack-plugin'), [_htmlOptions]);
}
if (route.children && route.children.length) {
addHtml(route.children);
}
});
}
};
addHtml(routes);
}
if (headScripts) {
const headScriptsMap = await headScripts();
webpackConfig.plugin('html-tags').use(esmResolve('html-webpack-tags-plugin'), [
{
append: false,
scripts: headScriptsMap.map(script => ({
path: script.src,
})),
},
]);
}
return {
publicCopyIgnore,
};
}

View File

@ -1,35 +0,0 @@
import type Config from 'webpack-5-chain';
import type { WebpackBuildConfig } from '../../../shared';
import { join } from 'node:path';
import { esmRequire, esmResolve, OWNER_DIR } from '../../../shared';
interface CreateVueWebpackConfigOptions {
config: WebpackBuildConfig;
webpackConfig: Config;
}
export default function createVueWebpackConfig({ config, webpackConfig }: CreateVueWebpackConfigOptions) {
webpackConfig.module
.rule('vue')
.test(/\.vue$/)
.use('vue-loader')
.loader(esmResolve('vue-loader'))
.options({
babelParserPlugins: ['jsx', 'classProperties', 'decorators-legacy'],
...(config as any).vueLoader || {},
})
.end();
webpackConfig.module
.rule('vue-custom')
.resourceQuery((query: string) => {
if (!query) {
return false;
}
return query.startsWith('?vue&type=custom');
})
.use('vue-custom-loader')
.loader(join(OWNER_DIR, './pitcher.mjs'));
webpackConfig.plugin('vue-loader-plugin').use(esmRequire('vue-loader').VueLoaderPlugin);
}

View File

@ -1,7 +0,0 @@
import type { IPluginAPI } from '@fesjs/shared';
export default function (api: IPluginAPI) {
api.registerBuilder({
name: 'webpack',
});
}

View File

@ -1,72 +0,0 @@
import type HtmlWebpackPlugin from 'html-webpack-plugin';
import type { LoaderOptions, PluginOptions } from 'mini-css-extract-plugin';
import type Config from 'webpack-5-chain';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
export interface CopyFileType {
from: string;
to: string;
}
export const OWNER_DIR: string = join(dirname(fileURLToPath(import.meta.url)), '..');
export const esmRequire = createRequire(import.meta.url);
export function esmResolve(specifier: string) {
const esmRequire = createRequire(import.meta.url);
return esmRequire.resolve(specifier);
}
export interface WebpackBuildConfig {
analyze?: {
analyzerMode?: 'server' | 'static' | 'disabled';
analyzerHost?: string;
analyzerPort?: number | 'auto';
openAnalyzer?: boolean;
generateStatsFile?: boolean;
statsFilename?: string;
logLevel?: 'info' | 'warn' | 'error' | 'silent';
defaultSizes?: 'stat' | 'parsed' | 'gzip';
};
chainWebpack?: (memo: Config, args: any) => void;
copy?: CopyFileType | CopyFileType[];
cssLoader?: {
url?: boolean | ((url: string, resourcePath: string) => boolean);
import?: boolean | { filter: (url: string, media: string, resourcePath: string) => boolean };
modules?: boolean | string | object;
sourceMap?: boolean;
importLoaders?: number;
onlyLocals?: boolean;
esModule?: boolean;
localsConvention?: 'asIs' | 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly';
};
devServer?: {
port?: number;
host?: string;
https?: boolean;
headers?: object;
[key: string]: any;
};
devtool?: Config.DevTool;
exportStatic?: {
htmlSuffix?: boolean;
dynamicRoot?: boolean;
};
externals?: string | ((data: any) => any);
extraBabelPlugins?: [];
extraBabelPresets?: [];
extraPostCSSPlugins?: [];
html?: HtmlWebpackPlugin.Options;
lessLoader?: Record<string, any>;
nodeModulesTransform?: {
exclude: string[];
};
postcssLoader?: Record<string, any>;
vueLoader?: object;
extraCSS?: {
loader?: LoaderOptions;
plugin?: PluginOptions;
};
}

View File

@ -1,9 +0,0 @@
{
"extends": ["@fesjs/typescript-config/base.json"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

File diff suppressed because one or more lines are too long

View File

@ -1,41 +0,0 @@
import { copySync } from 'fs-extra/esm';
import { defineConfig } from 'tsup';
export default defineConfig({
entry: [
'src/index.ts',
'src/plugins/commands/pitcher.ts',
'src/plugins/registerBuilder.ts',
'src/plugins/registerMethods.ts',
'src/plugins/registerType.ts',
'src/plugins/features/analyze.ts',
'src/plugins/features/chainWebpack.ts',
'src/plugins/features/cssLoader.ts',
'src/plugins/features/copy.ts',
'src/plugins/features/devServer.ts',
'src/plugins/features/devtool.ts',
'src/plugins/features/externals.ts',
'src/plugins/features/exportStatic.ts',
'src/plugins/features/extraBabelPlugins.ts',
'src/plugins/features/extraBabelPresets.ts',
'src/plugins/features/extraPostCSSPlugins.ts',
'src/plugins/features/html.ts',
'src/plugins/features/lessLoader.ts',
'src/plugins/features/postcssLoader.ts',
'src/plugins/features/nodeModulesTransform.ts',
'src/plugins/features/vueLoader.ts',
'src/plugins/features/extraCSS.ts',
'src/plugins/commands/build/index.ts',
'src/plugins/commands/dev/index.ts',
'src/plugins/commands/webpack/index.ts',
],
splitting: false,
sourcemap: false,
clean: true,
dts: true,
shims: true,
format: ['esm'],
onSuccess() {
copySync('src/plugins/commands/index-default.html', 'dist/plugins/commands/index-default.html');
},
});

View File

@ -1,6 +0,0 @@
// eslint-disable-next-line antfu/no-import-dist
import type { WebpackBuildConfig } from './dist/index.d.mts';
declare module '@fesjs/fes' {
interface PluginBuildConfig extends WebpackBuildConfig {}
}

View File

@ -1,28 +0,0 @@
import type { UserConfig } from '../../types';
import { lodash } from '@fesjs/utils';
interface UpdateUserConfigWithKeyOptions {
key: string;
value: any;
userConfig: UserConfig;
}
interface GetUserConfigWithKeyOptions {
key: string;
userConfig: UserConfig;
}
export function updateUserConfigWithKey({
key,
value,
userConfig,
}: UpdateUserConfigWithKeyOptions): void {
lodash.set(userConfig, key, value);
}
export function getUserConfigWithKey({
key,
userConfig,
}: GetUserConfigWithKeyOptions): any {
return lodash.get(userConfig, key);
}

View File

@ -1,18 +0,0 @@
import { lodash } from '@fesjs/utils';
function funcToStr(obj: any): any {
if (typeof obj === 'function') {
return obj.toString();
}
if (lodash.isPlainObject(obj)) {
return Object.keys(obj).reduce((memo: Record<string, any>, key: string) => {
memo[key] = funcToStr(obj[key]);
return memo;
}, {});
}
return obj;
}
export default function isEqual(a: any, b: any): boolean {
return lodash.isEqual(funcToStr(a), funcToStr(b));
}

View File

@ -1,12 +0,0 @@
import Config from './config';
import Service from './service';
import { PluginType } from './service/enums';
import { isPluginOrPreset } from './service/utils/pluginUtils';
export { Config, isPluginOrPreset, PluginType, Service };
export type {
ConfigInstance,
PluginAPIInstance,
ServiceInstance,
} from './types';

View File

@ -1,49 +0,0 @@
/**
*
*/
export enum PluginType {
preset = 'preset',
plugin = 'plugin',
builder = 'builder',
}
/**
*
*/
export enum ServiceStage {
uninitialized = 0,
constructor = 1,
init = 2,
initPresets = 3,
initPlugins = 4,
initHooks = 5,
pluginReady = 6,
getConfig = 7,
getPaths = 8,
run = 9,
}
/**
*
*/
export enum ConfigChangeType {
reload = 'reload',
regenerateTmpFiles = 'regenerateTmpFiles',
}
/**
*
*/
export enum ApplyPluginsType {
add = 'add',
modify = 'modify',
event = 'event',
}
/**
*
*/
export enum EnableBy {
register = 'register',
config = 'config',
}

View File

@ -1,39 +0,0 @@
import type { Paths, UserConfig } from '../types';
import { existsSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { lodash, winPath } from '@fesjs/utils';
interface GetServicePathsOptions {
cwd: string;
config: UserConfig;
env: string;
}
function isDirectoryAndExist(path: string): boolean {
return existsSync(path) && statSync(path).isDirectory();
}
function normalizeWithWinPath(obj: Record<string, string>): Record<string, string> {
return lodash.mapValues(obj, value => winPath(value));
}
export default function getServicePaths({ cwd, config, env }: GetServicePathsOptions): Paths {
let absSrcPath = cwd;
if (isDirectoryAndExist(join(cwd, 'src'))) {
absSrcPath = join(cwd, 'src');
}
const absPagesPath = config.singular ? join(absSrcPath, 'page') : join(absSrcPath, 'pages');
const tmpDir = ['.fes', env !== 'development' && env].filter(Boolean).join('-');
const paths = {
tmpDir,
cwd,
absNodeModulesPath: join(cwd, 'node_modules'),
absOutputPath: join(cwd, (config.outputPath as string) || './dist'),
absSrcPath,
absPagesPath,
absTmpPath: join(absSrcPath, tmpDir),
};
return normalizeWithWinPath(paths) as unknown as Paths;
}

View File

@ -1,12 +0,0 @@
/**
* Promise
* @param obj
* @returns Promise true false
*/
export default function isPromise(obj: any): obj is Promise<any> {
return (
!!obj
&& (typeof obj === 'object' || typeof obj === 'function')
&& typeof obj.then === 'function'
);
}

View File

@ -1,217 +0,0 @@
import type { Plugin } from '../../types';
import { basename, dirname, extname, join, relative } from 'node:path';
import process from 'node:process';
import { pathToFileURL } from 'node:url';
import { chalk, compatESModuleRequire, lodash, resolve, winPath } from '@fesjs/utils';
import { readJSONSync } from 'fs-extra/esm';
import { packageUp } from 'package-up';
import { OWNER_DIR } from '../../shared';
import { PluginType } from '../enums';
interface FilterBuilderOptions {
pkg: Record<string, any>;
builder?: string;
}
interface FilterPluginAndPresetOptions {
pkg: Record<string, any>;
builder?: string;
}
interface GetPluginsOrPresetsOptions {
presets?: string[];
plugins?: string[];
userConfigPresets?: string[];
userConfigPlugins?: string[];
pkg: Record<string, any>;
cwd: string;
builder?: string;
}
interface PathToObjOptions {
path: string;
type: PluginType;
cwd: string;
}
interface ResolvePresetsOptions {
presets?: string[];
userConfigPresets?: string[];
builder?: string;
pkg: Record<string, any>;
cwd: string;
}
interface ResolvePluginsOptions {
plugins?: string[];
userConfigPlugins?: string[];
builder?: string;
pkg: Record<string, any>;
cwd: string;
}
const RE: Record<PluginType, RegExp> = {
[PluginType.plugin]: /^(@fesjs\/|@webank\/fes-|fes-)plugin-(.+)$/,
[PluginType.preset]: /^(@fesjs\/|@webank\/fes-|fes-)preset-(.+)$/,
[PluginType.builder]: /^(@fesjs\/|@webank\/fes-|fes-)builder-(.+)$/,
};
export function isPluginOrPreset(type: PluginType, name: string): boolean {
const hasScope = name.charAt(0) === '@';
const re = RE[type];
if (hasScope) {
return re.test(name.split('/')[1]) || re.test(name);
}
return re.test(name);
}
function filterBuilder(opts: FilterBuilderOptions): string[] {
const builders = Object.keys(opts.pkg.devDependencies || {})
.concat(Object.keys(opts.pkg.dependencies || {}))
.filter(isPluginOrPreset.bind(null, PluginType.builder))
.filter(builder => builder.includes(opts.builder || ''));
if (builders.length > 1) {
// eslint-disable-next-line no-console
console.log(chalk.yellow(`提示您使用了多个builder默认使用第一个${builders[0]}`));
return [builders[0]];
}
return builders;
}
function filterPluginAndPreset(type: PluginType, opts: FilterPluginAndPresetOptions): string[] {
const base = Object.keys(opts.pkg.devDependencies || {})
.concat(Object.keys(opts.pkg.dependencies || {}))
.filter(isPluginOrPreset.bind(null, type));
if (type === PluginType.preset) {
return base.concat(filterBuilder(opts));
}
if (type === PluginType.plugin) {
return base.concat(join(OWNER_DIR, './dist/service/plugins/builder.mjs'));
}
return base;
}
export function getPluginsOrPresets(type: PluginType, opts: GetPluginsOrPresetsOptions): string[] {
const upperCaseType = type.toUpperCase();
return [
// opts
...(opts[type === PluginType.preset ? 'presets' : 'plugins'] || []),
// env
...(process.env[`FES_${upperCaseType}S`] || '').split(',').filter(Boolean),
...filterPluginAndPreset(type, opts),
// user config
...(opts[type === PluginType.preset ? 'userConfigPresets' : 'userConfigPlugins'] || []),
].map(path =>
resolve.sync(path, {
basedir: opts.cwd,
extensions: ['.js', '.ts'],
}),
);
}
// e.g.
// initial-state -> initialState
// webpack.css-loader -> webpack.cssLoader
function nameToKey(name: string): string {
return name
.split('.')
.map(part => lodash.camelCase(part))
.join('.');
}
function pkgNameToKey(pkgName: string, type: PluginType): string {
if (pkgName.charAt(0) === '@' && !pkgName.startsWith('@fesjs/')) {
pkgName = pkgName.split('/')[1];
}
return nameToKey(pkgName.replace(RE[type], ''));
}
export async function pathToObj({ path, type, cwd }: PathToObjOptions): Promise<Plugin> {
let pkg: Record<string, any>;
let isPkgPlugin = false;
const pkgJSONPath = await packageUp({ cwd: path });
if (pkgJSONPath) {
pkg = readJSONSync(pkgJSONPath);
isPkgPlugin = winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js')) === winPath(path);
}
let id: string;
if (isPkgPlugin) {
id = pkg!.name;
}
else if (winPath(path).startsWith(winPath(cwd))) {
id = `./${winPath(relative(cwd, path))}`;
}
else if (pkgJSONPath) {
id = winPath(join(pkg!.name, relative(dirname(pkgJSONPath!), path)));
}
else {
id = winPath(path);
}
id = id.replace('@fesjs/preset-built-in/dist/plugins', '@@');
id = id.replace(/\.js$/, '');
const key = isPkgPlugin ? pkgNameToKey(pkg!.name, type) : nameToKey(basename(path, extname(path)));
return {
id,
key,
path: winPath(path),
async apply() {
try {
// 使用 pathToFileURL 确保在 Windows 下路径格式正确
const fileUrl = pathToFileURL(path).href;
const ret = await import(fileUrl);
// use the default member for es modules
return compatESModuleRequire(ret);
}
catch (e: any) {
throw new Error(`Register ${path} failed, since ${e.message}`);
}
},
defaultConfig: null,
};
}
export async function resolvePresets(opts: ResolvePresetsOptions): Promise<Plugin[]> {
const type = PluginType.preset;
const presets = await Promise.all([...getPluginsOrPresets(type, opts)].map(path =>
pathToObj({
type,
path,
cwd: opts.cwd,
}),
));
return presets
.sort((a, b) => {
if (a.id === '@fesjs/preset-built-in') {
return -1;
}
if (b.id === '@fesjs/preset-built-in') {
return 1;
}
if (/^(?:@fesjs\/|@webank\/fes-|fes-)builder-/.test(a.id)) {
return -1;
}
if (/^(?:@fesjs\/|@webank\/fes-|fes-)builder-/.test(b.id)) {
return 1;
}
return 0;
});
}
export async function resolvePlugins(opts: ResolvePluginsOptions): Promise<Plugin[]> {
const type = PluginType.plugin;
const plugins = await Promise.all([...getPluginsOrPresets(type, opts)].map(path =>
pathToObj({
type,
path,
cwd: opts.cwd,
}),
));
return plugins;
}
export function isValidPlugin(plugin: any): plugin is Plugin {
return plugin && plugin.id && plugin.key && typeof plugin.apply === 'function';
}

View File

@ -1,7 +0,0 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
/**
*
*/
export const OWNER_DIR: string = join(dirname(fileURLToPath(import.meta.url)), '..');

View File

@ -1,94 +0,0 @@
import type Config from './config';
import type Service from './service';
import type { ApplyPluginsType, EnableBy } from './service/enums';
import type PluginAPI from './service/pluginAPI';
export type ConfigInstance = InstanceType<typeof Config>;
export type ServiceInstance = InstanceType<typeof Service>;
export type PluginAPIInstance = InstanceType<typeof PluginAPI>;
// Enums
export type { ApplyPluginsType, ConfigChangeType, EnableBy, PluginType, ServiceStage } from './service/enums';
// Utility types
export interface UserConfig {
[key: string]: any;
}
export interface Paths {
tmpDir: string;
cwd: string;
absNodeModulesPath: string;
absOutputPath: string;
absSrcPath: string;
absPagesPath: string;
absTmpPath: string;
}
export interface Plugin {
id: string;
key: string;
path: string;
apply: () => Promise<any> | any;
config?: PluginConfig;
enableBy?: EnableBy | (() => boolean);
isPreset?: boolean;
defaultConfig: any;
}
export interface PluginConfig {
schema?: (joi: any) => any;
default?: any;
}
export interface Hook {
key: string;
fn: (...args: any[]) => any;
pluginId?: string;
stage?: number;
before?: string;
}
export interface CommandOption {
command: string;
description: string;
options?: CommandOptionConfig[];
fn?: (args: CommandArgs) => Promise<void> | void;
}
export interface CommandOptionConfig {
name: string;
description: string;
default?: any;
choices?: string[];
}
export interface CommandArgs {
rawArgv: Record<string, any>;
args: Record<string, any>;
options: Record<string, any>;
program: any;
}
export interface ApplyPluginsOptions {
key: string;
type: ApplyPluginsType;
initialValue?: any;
args?: any;
}
export interface ResolvePresetsOptions {
presets?: string[];
userConfigPresets?: string[];
builder?: string;
pkg: Record<string, any>;
cwd: string;
}
export interface ResolvePluginsOptions {
plugins?: string[];
userConfigPlugins?: string[];
builder?: string;
pkg: Record<string, any>;
cwd: string;
}

View File

@ -1,9 +0,0 @@
{
"extends": ["@fesjs/typescript-config/base.json"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -1,11 +0,0 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts', 'src/service/plugins/builder.ts'],
splitting: false,
sourcemap: false,
clean: true,
dts: true,
shims: true,
format: ['esm'],
});

View File

@ -1 +1 @@
Used in bin/create-fes-app.mjs to determine if it is in the local debug state.
Used in bin/create-fes-app.js to determine if it is in the local debug state.

View File

@ -35,13 +35,16 @@ Fes.js 是一个好用的前端应用解决方案。提供覆盖编译构建到
| [@fesjs/plugin-access](http://fesjs.mumblefe.cn/reference/plugin/plugins/access.html) | 提供对页面资源的权限控制能力 |
| [@fesjs/plugin-enums](http://fesjs.mumblefe.cn/reference/plugin/plugins/enums.html#%E4%BB%8B%E7%BB%8D) | 提供统一的枚举存取及丰富的函数来处理枚举 |
| [@fesjs/plugin-icon](http://fesjs.mumblefe.cn/reference/plugin/plugins/icon.html#%E4%BB%8B%E7%BB%8D) | svg 文件自动注册为组件 |
| [@fesjs/plugin-jest](http://fesjs.mumblefe.cn/reference/plugin/plugins/jest.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | 基于 `Jest`,提供单元测试、覆盖测试能力 |
| [ @fesjs/plugin-layout](http://fesjs.mumblefe.cn/reference/plugin/plugins/layout.html) | 简单的配置即可拥有布局,包括导航以及侧边栏 |
| [@fesjs/plugin-locale](http://fesjs.mumblefe.cn/reference/plugin/plugins/locale.html#%E4%BB%8B%E7%BB%8D) | 基于 `Vue I18n`,提供国际化能力 |
| [@fesjs/plugin-model](http://fesjs.mumblefe.cn/reference/plugin/plugins/model.html#%E4%BB%8B%E7%BB%8D) | 简易的数据管理方案 |
| [@fesjs/plugin-request](http://fesjs.mumblefe.cn/reference/plugin/plugins/request.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | 基于 `Axios` 封装的 request内置防止重复请求、请求节流、错误处理等功能 |
| [@fesjs/plugin-vuex](http://fesjs.mumblefe.cn/reference/plugin/plugins/vuex.html#%E5%90%AF%E7%94%A8%E6%96%B9%E5%BC%8F) | 基于 `Vuex`, 提供状态管理能力 |
| [@fesjs/plugin-qiankun](http://fesjs.mumblefe.cn/reference/plugin/plugins/qiankun.html#%E4%BB%8B%E7%BB%8D) | 基于 `qiankun`,提供微服务能力 |
| [@fesjs/plugin-sass](http://fesjs.mumblefe.cn/reference/plugin/plugins/sass.html#%E4%BB%8B%E7%BB%8D) | 样式支持 sass |
| [@fesjs/plugin-monaco-editor](http://fesjs.mumblefe.cn/reference/plugin/plugins/editor.html#%E4%BB%8B%E7%BB%8D) | 提供代码编辑器能力, 基于`monaco-editor`VS Code 使用的代码编辑器) |
| [@fesjs/plugin-windicss](http://fesjs.mumblefe.cn/reference/plugin/plugins/windicss.html) | 基于 `windicss`,提供原子化 CSS 能力 |
| [@fesjs/plugin-pinia](http://fesjs.mumblefe.cn/reference/plugin/plugins/pinia.html) | pinia状态处理 |
| [@fesjs/plugin-watermark](http://fesjs.mumblefe.cn/reference/plugin/plugins/watermark.html) | 水印 |

View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../lib/cli');

View File

@ -1,5 +0,0 @@
#!/usr/bin/env node
import { runMain } from '../dist/index.mjs';
runMain();

View File

@ -1,50 +1,39 @@
{
"name": "@fesjs/create-fes-app",
"version": "4.0.0-beta.0",
"version": "3.0.7",
"description": "create a app base on fes.js",
"author": "qlin",
"license": "MIT",
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"main": "lib/index.js",
"files": [
"lib",
"bin",
"templates/**/*"
],
"bin": {
"create-fes-app": "bin/create-fes-app.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
"directory": "packages/create-fes-app"
},
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"keywords": [
"fes"
],
"sideEffects": false,
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"bin": {
"create-fes-app": "bin/create-fes-app.mjs"
},
"files": [
"bin",
"dist",
"templates/**/*"
],
"scripts": {
"watch": "tsup --watch",
"build": "tsup"
"author": "qlin",
"license": "MIT",
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.2",
"fs-extra": "^11.3.1",
"glob": "^11.0.3",
"mustache": "^4.2.0",
"ora": "^8.2.0",
"semver": "^7.7.2",
"validate-npm-package-name": "^6.0.2"
},
"devDependencies": {
"@types/validate-npm-package-name": "^4.0.2"
"@fesjs/utils": "^3.0.3",
"fs-extra": "^11.1.1",
"inquirer": "^7.3.3",
"readline": "^1.3.0",
"validate-npm-package-name": "^3.0.0"
}
}

View File

@ -0,0 +1,49 @@
import { chalk, yParser } from '@fesjs/utils';
import { existsSync } from 'fs';
import { join } from 'path';
const args = yParser(process.argv.slice(2), {
alias: {
version: ['v'],
help: ['h'],
force: ['f'],
merge: ['m'],
proxy: ['x']
},
boolean: ['version', 'help', 'merge', 'force']
});
if (args._.length > 1) {
console.log(chalk.yellow('\n Warning: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'));
}
if (args.version && !args._[0]) {
args._[0] = 'version';
const local = existsSync(join(__dirname, '../.local'))
? chalk.cyan('@local')
: '';
const { name, version } = require('../package.json');
console.log(`${name}@${version}${local}`);
} else if (args.help && !args._[0]) {
console.log(`
Usage: create-fes-app <name>
Options:
-v, --version Output the current version
-h, --help Display help for command
-f, --force Overwrite target directory if it exists
-m, --merge Merge target directory if it exists
-x, --proxy <proxyUrl> Use specified proxy when creating project
`);
} else {
require('.')
.default({
cwd: process.cwd(),
args
})
.catch((err) => {
console.error(`Create failed, ${err.message}`);
console.error(err);
});
}

View File

@ -0,0 +1,22 @@
import { Generator } from '@fesjs/utils';
export default class AppGenerator extends Generator {
constructor({ cwd, args, path, targetDir }) {
super({
cwd,
args,
});
this.path = path;
this.targetDir = targetDir;
}
async writing() {
this.copyDirectory({
context: {
version: require('../../package.json').version,
},
path: this.path,
target: this.targetDir,
});
}
}

View File

@ -0,0 +1,24 @@
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

@ -0,0 +1,117 @@
import path from 'path';
import { chalk } from '@fesjs/utils';
import validateProjectName from 'validate-npm-package-name';
import fs from 'fs-extra';
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) {
process.env.HTTP_PROXY = args.proxy;
}
const projectName = args._[0];
const inCurrent = projectName === '.';
const name = inCurrent ? path.relative('../', cwd) : projectName;
const targetDir = path.resolve(cwd, projectName || '.');
const result = validateProjectName(name);
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`));
result.errors &&
result.errors.forEach((err) => {
console.error(chalk.red.dim(`Error: ${err}`));
});
result.warnings &&
result.warnings.forEach((warn) => {
console.error(chalk.red.dim(`Warning: ${warn}`));
});
throw new Error('Process exited');
}
if (fs.pathExistsSync(targetDir) && !args.merge) {
if (args.force) {
await fs.remove(targetDir);
} else if (inCurrent) {
clearConsole();
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: 'Generate project in current directory?',
},
]);
if (!ok) {
return null;
}
} else {
clearConsole();
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Merge', value: 'merge' },
{ name: 'Cancel', value: false },
],
},
]);
if (!action) {
return null;
}
if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`);
await fs.remove(targetDir);
}
}
}
clearConsole();
const { template } = await inquirer.prompt([
{
name: 'template',
type: 'list',
message: 'Pick an template:',
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 === 'pc' || template === 'h5') {
const generator = new AppGenerator({
cwd,
args,
targetDir,
path: path.join(__dirname, `../templates/app/${template}`),
});
await generator.run();
console.log();
console.log(chalk.green(`project ${projectName} created successfully, please execute the following command to use:`));
console.log(`$ cd ${projectName}`);
console.log('$ pnpm i');
console.log('$ pnpm 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('$ pnpm i');
console.log('$ pnpm dev');
console.log();
}
};

View File

@ -1,2 +0,0 @@
export { main } from './main';
export { runMain } from './run';

View File

@ -1,116 +0,0 @@
import { join, relative, resolve } from 'node:path';
import process from 'node:process';
import { defineCommand } from 'citty';
import consola from 'consola';
import ora from 'ora';
import validate from 'validate-npm-package-name';
import pkg from '../package.json' assert { type: 'json' };
import { getWorkPath } from './shared';
import { setupGlobalConsole } from './utils/console';
import { checkEngines } from './utils/engines';
import { copyDirectory } from './utils/gen';
export const main = defineCommand({
meta: {
name: pkg.name,
version: pkg.version,
description: pkg.description,
},
args: {
name: {
type: 'positional',
description: '项目名称',
required: true,
},
proxy: {
type: 'string',
description: '代理地址',
alias: ['-p'],
},
merge: {
type: 'boolean',
description: '是否合并',
alias: ['m'],
},
force: {
type: 'boolean',
description: '是否强制覆盖',
alias: ['f'],
},
},
async setup() {
setupGlobalConsole();
await checkEngines();
},
async run({ args }) {
const inCurrent = args.name === '.';
const cwd = getWorkPath();
const projectName = inCurrent ? relative('../', cwd) : args.name;
const result = validate(projectName);
if (!result.validForNewPackages) {
consola.error(`Invalid project name: "${projectName}"`);
result.errors
&& result.errors.forEach((err) => {
consola.error(`Error: ${err}`);
});
result.warnings
&& result.warnings.forEach((warn) => {
consola.warn(`Warning: ${warn}`);
});
process.exit(0);
}
const template = await consola.prompt('Pick an template:', {
type: 'select',
options: [{
label: 'PC, suitable for management desk front-end applications',
value: 'pc',
}, {
label: 'H5, suitable for mobile applications',
value: 'h5',
}, {
label: 'Plugin, suitable for fes plugin',
value: 'plugin',
}, {
label: 'Cancel',
value: 'cancel',
}],
});
const targetDir = resolve(cwd, projectName || '.');
if (template === 'pc' || template === 'h5') {
const spinner = ora('项目生成中加载中...').start();
copyDirectory({
context: {
version: pkg.version,
},
path: join(__dirname, `../templates/app/${template}`),
target: targetDir,
});
spinner.succeed('项目创建成功');
consola.box([
`cd ${projectName}`,
'pnpm i',
'pnpm dev',
].join('\n'));
}
else if (template === 'plugin') {
copyDirectory({
context: {
version: pkg.version,
name: projectName,
},
path: join(__dirname, '../templates/plugin'),
target: targetDir,
});
consola.success(`plugin ${projectName} created successfully, please execute the following command to use:`);
consola.box([
`cd ${projectName}`,
'pnpm i',
'pnpm dev',
].join('\n'));
}
},
});

View File

@ -1,5 +0,0 @@
import { runMain as _runMain } from 'citty';
import { main } from './main';
export const runMain = () => _runMain(main);

View File

@ -1,27 +0,0 @@
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
export const OWNER_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
export function getWorkPath() {
return process.env.PWD || process.cwd();
}
export function removeSync(dir: string) {
rmSync(dir, {
recursive: true,
force: true,
});
}
export function readJsonSync(filePath: string) {
const content = readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
export function writeJSONSync(filePath: string, data: Record<string, any>) {
writeFileSync(filePath, JSON.stringify(data, null, 2));
}

View File

@ -0,0 +1,13 @@
import readline from 'readline';
export const clearConsole = (title) => {
if (process.stdout.isTTY) {
const blank = '\n'.repeat(process.stdout.rows);
console.log(blank);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
if (title) {
console.log(title);
}
}
};

View File

@ -1,17 +0,0 @@
import process from 'node:process';
import { consola } from 'consola';
export function setupGlobalConsole(opts: { dev?: boolean } = {}) {
// Wrap all console logs with consola for better DX
if (opts.dev) {
consola.wrapAll();
}
else {
consola.wrapConsole();
}
process.on('unhandledRejection', err => consola.error('[unhandledRejection]', err));
process.on('uncaughtException', err => consola.error('[uncaughtException]', err));
}

View File

@ -1,17 +0,0 @@
import process from 'node:process';
import { logger } from './logger';
export async function checkEngines() {
const satisfies = await import('semver/functions/satisfies.js').then(
r => r.default || (r as any as typeof import('semver/functions/satisfies.js')),
); // npm/node-semver#381
const currentNode = process.versions.node;
const nodeRange = '>= 18.0.0';
if (!satisfies(currentNode, nodeRange)) {
logger.warn(
`Current version of Node.js (\`${currentNode}\`) is unsupported and might cause issues.\n Please upgrade to a compatible version \`${nodeRange}\`.`,
);
}
}

View File

@ -1,46 +0,0 @@
import { readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { copySync, outputFileSync } from 'fs-extra/esm';
import { globSync } from 'glob';
import Mustache from 'mustache';
import { getWorkPath } from '../shared';
function copyTpl(opts: {
templatePath: string;
target: string;
context: Record<string, any>;
}): void {
const tpl = readFileSync(opts.templatePath, 'utf-8');
const content = Mustache.render(tpl, opts.context);
outputFileSync(opts.target, content, 'utf-8');
}
export function copyDirectory(opts: {
path: string;
target: string;
context: Record<string, any>;
}): void {
const files = globSync('**/*', {
cwd: opts.path,
dot: true,
ignore: ['**/node_modules/**'],
});
files.forEach((file) => {
const absFile = join(opts.path, file);
if (statSync(absFile).isDirectory()) {
return;
}
if (file.endsWith('.tpl')) {
return copyTpl({
templatePath: absFile,
target: join(opts.target, file.replace(/\.tpl$/, '')),
context: opts.context,
});
}
const absTarget = join(opts.target, file);
copySync(absFile, absTarget);
});
}

View File

@ -1,3 +0,0 @@
import { consola } from 'consola';
export const logger = consola.withTag('fes');

View File

@ -12,18 +12,17 @@
"access": "public"
},
"dependencies": {
"@fesjs/builder-webpack": "^4.0.0",
"@fesjs/fes": "^4.0.0",
"@fesjs/plugin-icon": "^5.0.0",
"@fesjs/plugin-request": "^5.0.0",
"@fesjs/builder-webpack": "^3.1.0",
"@fesjs/fes": "^3.1.17",
"@fesjs/plugin-icon": "^4.0.0",
"@fesjs/plugin-request": "^4.0.1",
"core-js": "^3.43.0",
"lodash-es": "^4.17.21",
"vue": "^3.5.17"
},
"devDependencies": {
"@antfu/eslint-config": "^5.2.2",
"eslint": "^9.35.0",
"@antfu/eslint-config": "4.16.1",
"eslint": "9.29.0",
"postcss-px-to-viewport-8-plugin": "^1.2.5",
"typescript": "^5.9.2"
"typescript": "5.8.3"
}
}

View File

@ -0,0 +1,44 @@
import { defineRuntimeConfig } from '@fesjs/fes';
export default defineRuntimeConfig({
request: {
// API 前缀
baseURL: '',
dataHandler(data, response) {
// 处理响应内容异常
if (data.code !== '0') {
if (data.code === '20000') {
console.log('hello world');
}
throw new Error(response);
}
// 响应数据格式化
return data?.result ? data.result : data;
},
// http 异常,和插件异常
errorHandler(error) {
if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// `error.request` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
console.log(error.request);
} else if (error.type) {
// 插件异常
console.log(error.msg);
} else {
// 发送请求时出了点问题
console.log('Error', error.message);
}
console.log(error.config);
},
// 请求拦截器
requestInterceptors: [],
// 响应拦截器
responseInterceptors: [],
},
});

View File

@ -1,42 +0,0 @@
import { defineRuntimeConfig } from '@fesjs/fes'
import { isPlainObject } from 'lodash-es'
export default defineRuntimeConfig({
request: {
baseURL: '',
timeout: 10000, // 默认 10s
method: 'POST', // 默认 post
mergeRequest: false, // 是否合并请求
responseType: null, // 可选 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData',默认根据 content-type 处理
credentials: 'include', // 默认 include, 'include' | 'same-origin' | 'omit'
headers: {}, // 传给服务器的 header
cacheData: false, // 是否缓存
requestInterceptor: (config: Config) => Config,
responseInterceptor: (response: RequestResponse) => RequestResponse,
transformData(data, response) {
// 处理响应内容异常
if (isPlainObject(data)) {
if (data.code === '10000') {
return Promise.reject(data)
}
return data?.result ? data.result : data
}
return data
},
// http 异常,和插件异常
errorHandler(error) {
// 处理业务异常,例如上述 transformData 抛出的异常
if (error.code) {
console.log(error.msg)
}
else if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(`服务异常:${error.response.status}`)
}
else {
// 请求异常
console.log(error.msg || error.message || `请求失败`)
}
},
},
})

View File

@ -0,0 +1,64 @@
// TODO
// 时间格式化
// js 数字精度计算
// 手机号、身份证号 等的校验
// 数字分割
export function resetContainerHeight(dom) {
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight;
window.onresize = function () {
const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
if (resizeHeight < originalHeight) {
// 恢复内容区域高度
const container = document.querySelector(dom);
container.style.height = originalHeight;
}
};
}
export function resetInputBlur() {
const isWechat = window.navigator.userAgent.match(/MicroMessenger\/([\d.]+)/i);
if (!isWechat) return;
const wechatVersion = isWechat[1];
const version = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
// 如果设备类型为iOS 12+ 和wechat 6.7.4+,恢复成原来的视口
if (+wechatVersion.replace(/\./g, '') >= 674 && +version[1] >= 12) {
window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight));
}
}
export function getQueryString(name) {
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
const r = window.location.search.substr(1).match(reg);
if (r != null) {
return decodeURIComponent(r[2]);
}
return null;
}
export function simpleRequest(options) {
const xhr = new XMLHttpRequest();
xhr.timeout = 3000;
if (options.type === 'GET') {
xhr.open(options.type, options.url, options.async || true);
xhr.send(null);
} else if (options.type === 'POST') {
xhr.open(options.type, options.url, options.async || true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(options.data || {}));
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
options.successed(xhr.responseText);
} else {
options.failed && options.failed(xhr);
}
}
};
xhr.ontimeout = function () {
options.failed && options.failed(xhr);
};
}

View File

@ -1,61 +0,0 @@
export function resetContainerHeight(dom) {
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight
window.onresize = function () {
const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight
if (resizeHeight < originalHeight) {
// 恢复内容区域高度
const container = document.querySelector(dom)
container.style.height = originalHeight
}
}
}
export function resetInputBlur() {
const isWechat = window.navigator.userAgent.match(/MicroMessenger\/([\d.]+)/i)
if (!isWechat)
return
const wechatVersion = isWechat[1]
const version = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/)
// 如果设备类型为iOS 12+ 和wechat 6.7.4+,恢复成原来的视口
if (+wechatVersion.replace(/\./g, '') >= 674 && +version[1] >= 12) {
window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight))
}
}
export function getQueryString(name) {
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
const r = window.location.search.substr(1).match(reg)
if (r != null) {
return decodeURIComponent(r[2])
}
return null
}
export function simpleRequest(options) {
const xhr = new XMLHttpRequest()
xhr.timeout = 3000
if (options.type === 'GET') {
xhr.open(options.type, options.url, options.async || true)
xhr.send(null)
}
else if (options.type === 'POST') {
xhr.open(options.type, options.url, options.async || true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify(options.data || {}))
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
options.successed(xhr.responseText)
}
else {
options.failed && options.failed(xhr)
}
}
}
xhr.ontimeout = function () {
options.failed && options.failed(xhr)
}
}

View File

@ -1,37 +1,43 @@
<script setup lang="ts">
import { defineRouteMeta, useRequest, useRouter } from '@fesjs/fes'
import { onMounted, ref } from 'vue'
<template>
<div class="onepiece">
fes & 拉夫德鲁 <br />
<fes-icon :spin="true" class="one-icon" type="smile" @click="clickIcon" />
<div v-if="loading" class="loading">loading</div>
<div v-else class="data">{{ data }}</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useRouter, useRequest, defineRouteMeta } from '@fesjs/fes';
defineRouteMeta({
title: '首页',
})
title: '首页',
});
const fes = ref('fes upgrade to vue3')
const rotate = ref(90)
const router = useRouter()
onMounted(() => {
console.log(router)
console.log('mounted1!!')
})
function clickIcon() {
console.log('click Icon')
}
const { loading, data } = useRequest('api')
export default {
setup() {
const fes = ref('fes upgrade to vue3');
const rotate = ref(90);
const router = useRouter();
onMounted(() => {
console.log(router);
console.log('mounted1!!');
});
const clickIcon = () => {
console.log('click Icon');
};
const { loading, data } = useRequest('api');
return {
loading,
data,
fes,
rotate,
clickIcon,
};
},
};
</script>
<template>
<div class="onepiece">
fes & 拉夫德鲁 <br>
<fes-icon :spin="true" class="one-icon" type="smile" @click="clickIcon" />
<div v-if="loading" class="loading">
loading
</div>
<div v-else class="data">
{{ data }}
</div>
</div>
</template>
<style lang="less" scoped>
@import '~@/styles/mixins/hairline';
@import '~@/styles/mixins/hover';

View File

@ -1,14 +1,20 @@
<script setup lang="ts">
import { defineRouteMeta } from '@fesjs/fes'
import { ref } from 'vue'
<template>
<div>{{ fes }}</div>
</template>
<script>
import { defineRouteMeta } from '@fesjs/fes';
import { ref } from 'vue';
defineRouteMeta({
title: 'one piece',
})
title: 'one piece',
});
const fes = ref('fes upgrade to vue3')
export default {
setup() {
const fes = ref('fes upgrade to vue3');
return {
fes,
};
},
};
</script>
<template>
<div>{{ fes }}</div>
</template>

View File

@ -1,34 +1,32 @@
{
"name": "@fesjs/template",
"version": "3.0.0",
"private": true,
"description": "fes项目模版",
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
"test:unit": "fes test:unit"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/builder-vite": "^5.0.0",
"@fesjs/fes": "^4.0.0",
"@fesjs/fes-design": "^0.8.82",
"@fesjs/plugin-access": "^4.0.0",
"@fesjs/plugin-enums": "^4.0.0",
"@fesjs/plugin-layout": "^6.0.0",
"@fesjs/plugin-model": "^4.0.0",
"core-js": "^3.43.0",
"lodash-es": "^4.17.21",
"vue": "^3.5.17"
},
"devDependencies": {
"@antfu/eslint-config": "^5.2.2",
"eslint": "^9.35.0",
"postcss-px-to-viewport-8-plugin": "^1.2.5",
"typescript": "^5.9.2"
}
"name": "@fesjs/template",
"version": "3.0.0",
"private": true,
"description": "fes项目模版",
"scripts": {
"build": "fes build",
"prod": "FES_ENV=prod fes build",
"analyze": "ANALYZE=1 fes build",
"dev": "fes dev",
"test:unit": "fes test:unit"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fesjs/builder-webpack": "^3.1.0",
"@fesjs/fes": "^3.1.17",
"@fesjs/fes-design": "^0.8.82",
"@fesjs/plugin-access": "^3.1.9",
"@fesjs/plugin-enums": "^3.0.1",
"@fesjs/plugin-layout": "^5.4.6",
"@fesjs/plugin-model": "^3.0.3",
"core-js": "^3.43.0",
"vue": "^3.5.17"
},
"devDependencies": {
"@antfu/eslint-config": "4.16.1",
"eslint": "9.29.0",
"typescript": "5.8.3"
}
}

View File

@ -0,0 +1,25 @@
import { access, defineRuntimeConfig } from '@fesjs/fes';
import PageLoading from '@/components/pageLoading.vue';
import UserCenter from '@/components/userCenter.vue';
export default defineRuntimeConfig({
beforeRender: {
loading: <PageLoading />,
action() {
const { setRole } = access;
return new Promise((resolve) => {
setTimeout(() => {
setRole('admin');
// useModel('@@initialState') @/components/UserCenter
resolve({
userName: '李雷',
});
}, 1000);
});
},
},
layout: {
renderCustom: () => <UserCenter />,
},
});

View File

@ -1,61 +0,0 @@
import { access, defineRuntimeConfig } from '@fesjs/fes'
import { isPlainObject } from 'lodash-es'
import PageLoading from '@/components/pageLoading.vue'
import UserCenter from '@/components/userCenter.vue'
export default defineRuntimeConfig({
beforeRender: {
loading: <PageLoading />,
action() {
const { setRole } = access
return new Promise((resolve) => {
setTimeout(() => {
setRole('admin')
// 初始化应用的全局状态,可以通过 useModel('@@initialState') 获取,具体用法看@/components/UserCenter 文件
resolve({
userName: '李雷',
})
}, 1000)
})
},
},
request: {
baseURL: '',
timeout: 10000, // 默认 10s
method: 'POST', // 默认 post
mergeRequest: false, // 是否合并请求
responseType: null, // 可选 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData',默认根据 content-type 处理
credentials: 'include', // 默认 include, 'include' | 'same-origin' | 'omit'
headers: {}, // 传给服务器的 header
cacheData: false, // 是否缓存
transformData(data, response) {
// 处理响应内容异常
if (isPlainObject(data)) {
if (data.code === '10000') {
return Promise.reject(data)
}
return data?.result ? data.result : data
}
return data
},
// http 异常,和插件异常
errorHandler(error) {
// 处理业务异常,例如上述 transformData 抛出的异常
if (error.code) {
console.log(error.msg)
}
else if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(`服务异常:${error.response.status}`)
}
else {
// 请求异常
console.log(error.msg || error.message || `请求失败`)
}
},
},
layout: {
renderCustom: () => <UserCenter />,
},
})

View File

@ -1,13 +1,20 @@
<script setup lang="ts">
import { FSpin } from '@fesjs/fes-design'
</script>
<template>
<div class="page-loading">
<FSpin size="large" stroke="#5384ff" />
</div>
<div class="page-loading">
<f-spin size="large" stroke="#5384ff" />
</div>
</template>
<script>
import { FSpin } from '@fesjs/fes-design';
export default {
components: {
FSpin,
},
setup() {
return {};
},
};
</script>
<style>
.page-loading {
position: fixed;

View File

@ -1,15 +1,18 @@
<script setup lang="ts">
import { useModel } from '@fesjs/fes'
const initialState = useModel('@@initialState')
</script>
<template>
<div class="right">
{{ initialState.userName }}
</div>
<div class="right">{{ initialState.userName }}</div>
</template>
<script>
import { useModel } from '@fesjs/fes';
export default {
setup() {
const initialState = useModel('@@initialState');
return {
initialState,
};
},
};
</script>
<style scope>
.right {
text-align: right;

View File

@ -1,14 +1,12 @@
<script setup lang="ts">
import { defineRouteMeta } from '@fesjs/fes'
<template>
<div style="padding: 32px">hello world</div>
</template>
<script setup>
import { defineRouteMeta } from '@fesjs/fes';
defineRouteMeta({
name: 'index',
title: '首页',
})
name: 'index',
title: '首页',
});
</script>
<template>
<div style="padding: 32px">
hello world
</div>
</template>

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

@ -1,8 +1,2 @@
.DS_Store
node_modules
lib
dist
npm-debug.log
lib

View File

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

View File

@ -1 +0,0 @@
# Fes.js 插件

View File

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

View File

@ -1,36 +0,0 @@
// eslint.config.js
import antfu from '@antfu/eslint-config';
export default await antfu({
stylistic: {
indent: 4,
quotes: 'single',
semi: 'always',
ignores: ['*.yaml'],
},
typescript: true,
vue: true,
rules: {
'curly': ['error', 'multi-line'],
'vue/block-order': [
'error',
{
order: ['template', 'script', 'style'],
},
],
'style/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'semi',
requireLast: true,
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
multilineDetection: 'brackets',
},
],
},
});

View File

@ -1,17 +1,17 @@
{
"name": "fes-plugin-{{{name}}}",
"version": "1.0.0",
"version": "3.0.0",
"description": "一个fes.js插件",
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"main": "lib/index.js",
"files": [
"dist",
"lib",
"README.md",
"types.d.ts"
],
"scripts": {
"watch": "tsup --watch",
"build": "tsup"
"dev": "node scripts/build.js --watch",
"build": "node scripts/build.js",
"lint": "eslint -c ./.eslintrc.js --ext .js,.jsx,.vue,.ts"
},
"license": "MIT",
"keywords": [
@ -19,14 +19,31 @@
"dependencies": {
},
"devDependencies": {
"@antfu/eslint-config": "^5.2.2",
"tsup": "^8.5.0",
"fs-extra": "^11.3.1",
"eslint": "^9.34.0",
"typescript": "^5.9.2"
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@webank/eslint-config-webank": "1.2.7",
"chalk": "^4.1.2",
"chokidar": "^3.5.2",
"deepmerge": "^4.2.2",
"fs-extra": "^11.1.1",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"yargs-parser": "^20.2.9"
},
"peerDependencies": {
"@fesjs/fes": "^4.0.0",
"vue": "^3.5.20",
}
"@fesjs/fes": "^3.0.0",
"vue": "^3.2.47"
},
"lint-staged": {
"*.{js,jsx,vue,ts}": [
"eslint --format=codeframe"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"typings": "./types.d.ts"
}

View File

@ -0,0 +1,144 @@
// 关闭 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,52 @@
// 关闭 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: '64' },
},
],
],
});
}
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

@ -1,5 +1,5 @@
import { join } from 'node:path';
import { readFileSync } from 'node:fs';
import { join } from 'path';
import { readFileSync } from 'fs';
import { name } from '../package.json';
const namespace = 'plugin-{{{name}}}';

View File

@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"rootDir": "./src",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -1,15 +0,0 @@
import { copySync } from 'fs-extra/esm';
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: false,
clean: true,
dts: false,
shims: true,
format: ['esm'],
onSuccess() {
copySync('src/runtime', 'dist/runtime');
},
});

View File

@ -0,0 +1,10 @@
import {} from '@fesjs/fes';
declare module "@fesjs/fes" {
interface PluginBuildConfig {
}
interface PluginRuntimeConfig {
}
}

View File

@ -1,9 +0,0 @@
{
"extends": ["@fesjs/typescript-config/base.json"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -1,11 +0,0 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: false,
clean: true,
dts: false,
shims: true,
format: ['esm'],
});

View File

@ -1,6 +1,6 @@
{
"name": "@fesjs/builder-vite",
"version": "5.0.0-beta.0",
"version": "4.0.5",
"description": "@fesjs/builder-vite",
"author": "qlin",
"license": "MIT",
@ -8,7 +8,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
"directory": "packages/builder-vite"
"directory": "packages/fes-builder-vite"
},
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
@ -16,49 +16,44 @@
"keywords": [
"fes"
],
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"main": "lib/index.js",
"files": [
"dist",
"lib",
"types.d.ts"
],
"scripts": {
"watch": "tsup --watch",
"build": "tsup"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"@fesjs/fes": "^4.0.0-beta.0",
"core-js": "^3.45.1"
"@fesjs/fes": "^3.1.12",
"core-js": "^3.29.1"
},
"dependencies": {
"@fesjs/shared": "^4.0.0-beta.0",
"@fesjs/utils": "^4.0.0-beta.0",
"@babel/core": "^7.23.3",
"@fesjs/utils": "^3.0.3",
"@rollup/pluginutils": "^5.1.0",
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"autoprefixer": "^10.4.21",
"@vitejs/plugin-basic-ssl": "^1.0.2",
"@vitejs/plugin-legacy": "^5.2.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.4",
"colorette": "^2.0.16",
"connect-history-api-fallback": "^2.0.0",
"consola": "^3.4.2",
"consola": "^2.15.3",
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.2",
"ejs": "^3.1.6",
"fast-glob": "^3.2.11",
"fs-extra": "^11.3.1",
"html-minifier-terser": "^7.2.0",
"fs-extra": "^10.0.1",
"html-minifier-terser": "^6.1.0",
"less": "^4.2.0",
"node-html-parser": "^5.3.3",
"pathe": "^0.2.0",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-safe-parser": "^6.0.0",
"rollup-plugin-visualizer": "^6.0.3",
"rollup-plugin-visualizer": "^5.9.3",
"terser": "^5.24.0",
"vite": "^7.1.4"
"vite": "^5.0.3"
},
"typings": "./types.d.ts"
}

View File

@ -1,14 +1,9 @@
import type { IPluginAPI } from '@fesjs/shared';
import type { InlineConfig, UserConfig } from 'vite';
import type { ViteBuildConfig } from '../../shared';
import { splitVendorChunkPlugin } from 'vite';
import legacy from '@vitejs/plugin-legacy';
import autoprefixer from 'autoprefixer';
import postcssFlexbugsFixes from 'postcss-flexbugs-fixes';
import postcssSafeParser from 'postcss-safe-parser';
import { getInnerCommonConfig } from '../../common/getConfig';
function getEsbuildTarget(targets: any): string[] {
const result: string[] = [];
function getEsbuildTarget(targets) {
const result = [];
['chrome', 'edge', 'firefox', 'hermes', 'ios', 'node', 'opera', 'rhino', 'safari'].forEach((key) => {
if (targets[key]) {
result.push(`${key}${targets[key]}`);
@ -17,20 +12,20 @@ function getEsbuildTarget(targets: any): string[] {
return result;
}
export default async (api: IPluginAPI<ViteBuildConfig>): Promise<InlineConfig> => {
export default async (api) => {
const { deepmerge, getTargetsAndBrowsersList } = api.utils;
const { build = {} } = api.config.vite || api.config.viteOption as UserConfig;
const { build = {} } = api.config.viteOption;
const { targets, browserslist } = getTargetsAndBrowsersList({ config: api.config });
const bundleConfig: InlineConfig = deepmerge(getInnerCommonConfig(api), {
const bundleConfig = deepmerge(getInnerCommonConfig(api), {
mode: 'production',
css: {
postcss: {
plugins: [
postcssFlexbugsFixes,
postcssSafeParser,
autoprefixer({
require('postcss-flexbugs-fixes'),
require('postcss-safe-parser'),
require('autoprefixer')({
...api.config.autoprefixer,
overrideBrowserslist: browserslist,
}),
@ -44,10 +39,12 @@ export default async (api: IPluginAPI<ViteBuildConfig>): Promise<InlineConfig> =
targets,
...api.config.viteLegacy,
}),
splitVendorChunkPlugin(),
],
build: {
...build,
terserOptions: build.terserOptions || api.config.terserOptions,
target: build.target || getEsbuildTarget(targets),
outDir: build.outDir || api.config.outputPath || 'dist',
assetsDir: build.assetsDir || 'static',
assetsInlineLimit: build.assetsInlineLimit || api.config.inlineLimit || 8192,

View File

@ -0,0 +1,42 @@
import { build } from 'vite';
import { existsSync } from 'fs';
import getBuildConfig from './getBuildConfig';
export default function (api) {
const {
paths,
utils: { rimraf },
} = api;
api.registerCommand({
command: 'build',
description: 'build application for production',
async fn() {
rimraf.sync(paths.absTmpPath);
// generate files
await api.applyPlugins({
key: 'onGenerateFiles',
type: api.ApplyPluginsType.event,
});
const bundleConfig = await getBuildConfig(api);
try {
// clear output path before exec build
if (process.env.CLEAR_OUTPUT !== 'none') {
if (paths.absOutputPath && existsSync(paths.absOutputPath)) {
rimraf.sync(paths.absOutputPath);
}
}
await build(bundleConfig);
if (process.env.RM_TMPDIR !== 'none') {
rimraf.sync(paths.absTmpPath);
}
} catch (err) {
// throw build error
throw err;
}
},
});
}

View File

@ -1,27 +1,16 @@
import type { IPluginAPI } from '@fesjs/shared';
import type { InlineConfig } from 'vite';
import process from 'node:process';
import basicSsl from '@vitejs/plugin-basic-ssl';
import { getInnerCommonConfig } from '../../common/getConfig';
import viteMiddlewarePlugin from './viteMiddlewarePlugin';
interface Args {
port?: string | number;
https?: boolean;
[key: string]: any;
}
export default async (api: IPluginAPI, args: Args): Promise<InlineConfig> => {
export default async (api, args) => {
const { deepmerge, getPort, changePort, getHostName } = api.utils;
const viteOption = api.config.vite || api.config.viteOption || {};
const port = await getPort(process.env.PORT || args.port || viteOption.server?.port);
const port = await getPort(process.env.PORT || args.port || api.config.viteOption.server?.port);
changePort(port);
const hostname = getHostName(viteOption.server?.host);
const hostname = getHostName(api.config.viteOption.server?.host);
const { server } = viteOption;
const { server } = api.config.viteOption;
const beforeMiddlewares = await api.applyPlugins({
key: 'addBeforeMiddlewares',
@ -36,9 +25,9 @@ export default async (api: IPluginAPI, args: Args): Promise<InlineConfig> => {
args: {},
});
const isHTTPS = !!(process.env.HTTPS || args.https || viteOption.server?.https);
const isHTTPS = !!(process.env.HTTPS || args.https || api.config.viteOption.server?.https);
const bundleConfig: InlineConfig = deepmerge(getInnerCommonConfig(api), {
const bundleConfig = deepmerge(getInnerCommonConfig(api), {
mode: 'development',
plugins: [viteMiddlewarePlugin(beforeMiddlewares, middlewares), isHTTPS && basicSsl()].filter(Boolean),
server: {

View File

@ -1,28 +1,16 @@
import type { IPluginAPI } from '@fesjs/shared';
import type { ViteDevServer } from 'vite';
import process from 'node:process';
import { createServer } from 'vite';
import getDevConfig from './getDevConfig';
interface Args {
args?: Record<string, any>;
rawArgv?: Record<string, any>;
options?: Record<string, any>;
program?: any;
}
export default (api: IPluginAPI) => {
export default (api) => {
const {
paths,
utils: { chalk, rimraf },
} = api;
let server: ViteDevServer | undefined;
let server;
function destroy() {
if (server) {
server.close().catch(() => {});
}
server?.close();
}
api.registerCommand({
@ -38,7 +26,7 @@ export default (api: IPluginAPI) => {
description: 'whether to turn on the https service',
},
],
async fn({ args = {} }: Args) {
async fn({ args = {} }) {
rimraf.sync(paths.absTmpPath);
await api.applyPlugins({
@ -62,14 +50,11 @@ export default (api: IPluginAPI) => {
api.registerMethod({
name: 'restartServer',
fn() {
// eslint-disable-next-line no-console
console.log(chalk.gray('Try to restart dev server...'));
destroy();
if (typeof process !== 'undefined' && process.send) {
process.send({
type: 'RESTART',
});
}
process.send({
type: 'RESTART',
});
},
});
};

Some files were not shown because too many files have changed in this diff Show More