mirror of
https://github.com/WeBankFinTech/fes.js.git
synced 2025-10-13 18:22:13 +08:00
Compare commits
21 Commits
master
...
v4.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
6868e85a9b | ||
|
3a04f3c792 | ||
|
4aeecb773e | ||
|
30b6b0d7ac | ||
|
42b2f38c2b | ||
|
6db74bfbbc | ||
|
0e07b43306 | ||
|
c1fa59fcf8 | ||
|
720296fc82 | ||
|
4158a1aee2 | ||
|
8297d91004 | ||
|
77b651c50b | ||
|
ad860c8f20 | ||
|
fd811c5269 | ||
|
f29492ebe0 | ||
|
3c77c9a2b6 | ||
|
ed26dfb39b | ||
|
2c69c96642 | ||
|
7c33e3e3ab | ||
|
8448e38c37 | ||
|
c4c081ae3a |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,7 +1,10 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.history
|
||||
|
||||
.cache
|
||||
.turbo
|
||||
|
||||
.temp
|
||||
.hound
|
||||
.fes
|
||||
|
3
.npmrc
3
.npmrc
@ -1,3 +1,4 @@
|
||||
registry=https://registry.npmmirror.com
|
||||
auto-install-peers=true
|
||||
link-workspace-packages=true
|
||||
prefer-workspace-packages=true
|
||||
shamefully-hoist=true
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
@ -1,3 +1,22 @@
|
||||
# [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)
|
||||
|
||||
|
||||
|
@ -56,12 +56,10 @@ 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) |
|
||||
|
@ -1,30 +0,0 @@
|
||||
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: [],
|
||||
};
|
18
docs/guide/upgrade4.md
Normal file
18
docs/guide/upgrade4.md
Normal file
@ -0,0 +1,18 @@
|
||||
# 从 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]
|
@ -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,
|
||||
|
@ -1,8 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'fes-template',
|
||||
'fes-template-h5'
|
||||
]
|
||||
};
|
29
package.json
29
package.json
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "fes.js",
|
||||
"type": "module",
|
||||
"version": "3.4.12",
|
||||
"version": "4.0.0-beta.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@8.6.6",
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"description": "一个好用的前端管理台快速开发框架",
|
||||
"preferGlobal": true,
|
||||
"workspaces": [
|
||||
@ -19,8 +19,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"bootstrap": "pnpm i",
|
||||
"dev": "node scripts/build.mjs --watch",
|
||||
"build": "node scripts/build.mjs",
|
||||
"dev": "turbo run watch",
|
||||
"build": "turbo run build",
|
||||
"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,29 +32,30 @@
|
||||
"hooks:sync": "simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.0.1",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"consola": "^3.4.2",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"execa": "^6.1.0",
|
||||
"minimist": "^1.2.6",
|
||||
"semver": "^7.3.6"
|
||||
"picocolors": "^1.1.1",
|
||||
"semver": "^7.3.6",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.8.0",
|
||||
"@antfu/eslint-config": "^5.2.2",
|
||||
"@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.13.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"eslint": "^9.34.0",
|
||||
"fs-extra": "^11.3.1",
|
||||
"lint-staged": "^15.2.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vitepress": "1.0.0-alpha.73",
|
||||
"vue": "^3.3.4",
|
||||
"yargs-parser": "^21.1.1"
|
||||
"vue": "^3.5.21"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx lint-staged",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fesjs/builder-vite",
|
||||
"version": "4.0.5",
|
||||
"version": "5.0.0-beta.0",
|
||||
"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/fes-builder-vite"
|
||||
"directory": "packages/builder-vite"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/WeBankFinTech/fes.js/issues"
|
||||
@ -16,44 +16,49 @@
|
||||
"keywords": [
|
||||
"fes"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"main": "dist/index.mjs",
|
||||
"module": "dist/index.mjs",
|
||||
"files": [
|
||||
"lib",
|
||||
"dist",
|
||||
"types.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"watch": "tsup --watch",
|
||||
"build": "tsup"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fesjs/fes": "^3.1.12",
|
||||
"core-js": "^3.29.1"
|
||||
"@fesjs/fes": "^4.0.0-beta.0",
|
||||
"core-js": "^3.45.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.3",
|
||||
"@fesjs/utils": "^3.0.3",
|
||||
"@fesjs/shared": "^4.0.0-beta.0",
|
||||
"@fesjs/utils": "^4.0.0-beta.0",
|
||||
"@rollup/pluginutils": "^5.1.0",
|
||||
"@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",
|
||||
"@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",
|
||||
"colorette": "^2.0.16",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"consola": "^2.15.3",
|
||||
"consola": "^3.4.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv-expand": "^8.0.2",
|
||||
"ejs": "^3.1.6",
|
||||
"fast-glob": "^3.2.11",
|
||||
"fs-extra": "^10.0.1",
|
||||
"html-minifier-terser": "^6.1.0",
|
||||
"fs-extra": "^11.3.1",
|
||||
"html-minifier-terser": "^7.2.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": "^5.9.3",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"terser": "^5.24.0",
|
||||
"vite": "^5.0.3"
|
||||
"vite": "^7.1.4"
|
||||
},
|
||||
"typings": "./types.d.ts"
|
||||
}
|
@ -1,9 +1,14 @@
|
||||
import { splitVendorChunkPlugin } from 'vite';
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { InlineConfig, UserConfig } from 'vite';
|
||||
import type { ViteBuildConfig } from '../../shared';
|
||||
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) {
|
||||
const result = [];
|
||||
function getEsbuildTarget(targets: any): string[] {
|
||||
const result: string[] = [];
|
||||
['chrome', 'edge', 'firefox', 'hermes', 'ios', 'node', 'opera', 'rhino', 'safari'].forEach((key) => {
|
||||
if (targets[key]) {
|
||||
result.push(`${key}${targets[key]}`);
|
||||
@ -12,20 +17,20 @@ function getEsbuildTarget(targets) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export default async (api) => {
|
||||
export default async (api: IPluginAPI<ViteBuildConfig>): Promise<InlineConfig> => {
|
||||
const { deepmerge, getTargetsAndBrowsersList } = api.utils;
|
||||
|
||||
const { build = {} } = api.config.viteOption;
|
||||
const { build = {} } = api.config.vite || api.config.viteOption as UserConfig;
|
||||
const { targets, browserslist } = getTargetsAndBrowsersList({ config: api.config });
|
||||
|
||||
const bundleConfig = deepmerge(getInnerCommonConfig(api), {
|
||||
const bundleConfig: InlineConfig = deepmerge(getInnerCommonConfig(api), {
|
||||
mode: 'production',
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
require('postcss-safe-parser'),
|
||||
require('autoprefixer')({
|
||||
postcssFlexbugsFixes,
|
||||
postcssSafeParser,
|
||||
autoprefixer({
|
||||
...api.config.autoprefixer,
|
||||
overrideBrowserslist: browserslist,
|
||||
}),
|
||||
@ -39,12 +44,10 @@ export default async (api) => {
|
||||
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,
|
40
packages/builder-vite/src/commands/build/index.ts
Normal file
40
packages/builder-vite/src/commands/build/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
@ -1,16 +1,27 @@
|
||||
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';
|
||||
|
||||
export default async (api, args) => {
|
||||
interface Args {
|
||||
port?: string | number;
|
||||
https?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default async (api: IPluginAPI, args: Args): Promise<InlineConfig> => {
|
||||
const { deepmerge, getPort, changePort, getHostName } = api.utils;
|
||||
|
||||
const port = await getPort(process.env.PORT || args.port || api.config.viteOption.server?.port);
|
||||
const viteOption = api.config.vite || api.config.viteOption || {};
|
||||
|
||||
const port = await getPort(process.env.PORT || args.port || viteOption.server?.port);
|
||||
changePort(port);
|
||||
|
||||
const hostname = getHostName(api.config.viteOption.server?.host);
|
||||
const hostname = getHostName(viteOption.server?.host);
|
||||
|
||||
const { server } = api.config.viteOption;
|
||||
const { server } = viteOption;
|
||||
|
||||
const beforeMiddlewares = await api.applyPlugins({
|
||||
key: 'addBeforeMiddlewares',
|
||||
@ -25,9 +36,9 @@ export default async (api, args) => {
|
||||
args: {},
|
||||
});
|
||||
|
||||
const isHTTPS = !!(process.env.HTTPS || args.https || api.config.viteOption.server?.https);
|
||||
const isHTTPS = !!(process.env.HTTPS || args.https || viteOption.server?.https);
|
||||
|
||||
const bundleConfig = deepmerge(getInnerCommonConfig(api), {
|
||||
const bundleConfig: InlineConfig = deepmerge(getInnerCommonConfig(api), {
|
||||
mode: 'development',
|
||||
plugins: [viteMiddlewarePlugin(beforeMiddlewares, middlewares), isHTTPS && basicSsl()].filter(Boolean),
|
||||
server: {
|
@ -1,16 +1,28 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { ViteDevServer } from 'vite';
|
||||
import process from 'node:process';
|
||||
import { createServer } from 'vite';
|
||||
import getDevConfig from './getDevConfig';
|
||||
|
||||
export default (api) => {
|
||||
interface Args {
|
||||
args?: Record<string, any>;
|
||||
rawArgv?: Record<string, any>;
|
||||
options?: Record<string, any>;
|
||||
program?: any;
|
||||
}
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
const {
|
||||
paths,
|
||||
utils: { chalk, rimraf },
|
||||
} = api;
|
||||
|
||||
let server;
|
||||
let server: ViteDevServer | undefined;
|
||||
|
||||
function destroy() {
|
||||
server?.close();
|
||||
if (server) {
|
||||
server.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
api.registerCommand({
|
||||
@ -26,7 +38,7 @@ export default (api) => {
|
||||
description: 'whether to turn on the https service',
|
||||
},
|
||||
],
|
||||
async fn({ args = {} }) {
|
||||
async fn({ args = {} }: Args) {
|
||||
rimraf.sync(paths.absTmpPath);
|
||||
|
||||
await api.applyPlugins({
|
||||
@ -50,11 +62,14 @@ export default (api) => {
|
||||
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',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -1,6 +1,12 @@
|
||||
export default (beforeMiddlewares = [], afterMiddlewares = []) => ({
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
interface Middleware {
|
||||
(req: any, res: any, next: any): void;
|
||||
}
|
||||
|
||||
export default (beforeMiddlewares: Middleware[] = [], afterMiddlewares: Middleware[] = []): Plugin => ({
|
||||
name: 'server-middleware-plugin',
|
||||
configureServer(server) {
|
||||
configureServer(server: any) {
|
||||
beforeMiddlewares.forEach((middleware) => {
|
||||
server.middlewares.use(middleware);
|
||||
});
|
@ -1,8 +1,10 @@
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
export default {
|
||||
name: 'sfc-config',
|
||||
transform(code, id) {
|
||||
transform(code: string, id: string) {
|
||||
if (/vue&type=config/.test(id)) {
|
||||
return `export default ''`;
|
||||
}
|
||||
},
|
||||
};
|
||||
} as Plugin;
|
26
packages/builder-vite/src/common/connectHistoryMiddleware.ts
Normal file
26
packages/builder-vite/src/common/connectHistoryMiddleware.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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;
|
@ -1,23 +1,15 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { InlineConfig } from 'vite';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync } from 'node:fs';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import { createHtmlPlugin } from './vite-plugin-html';
|
||||
import SFCConfigBlockPlugin from './SFCConfigBlockPlugin';
|
||||
import getDefine from './getDefine';
|
||||
import SFCConfigBlockPlugin from './SFCConfigBlockPlugin';
|
||||
import { createHtmlPlugin } from './vite-plugin-html';
|
||||
|
||||
function getPostcssConfig(api) {
|
||||
// TODO 支持其他 postcss 配置文件类型
|
||||
const configPath = `${api.paths.cwd}/postcss.config.js`;
|
||||
if (existsSync(configPath))
|
||||
return require(`${api.paths.cwd}/postcss.config.js`);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getInnerCommonConfig(api) {
|
||||
export function getInnerCommonConfig(api: IPluginAPI): InlineConfig {
|
||||
const { deepmerge, resolveRuntimeEnv } = api.utils;
|
||||
const { base, ...otherViteOption } = api.config.viteOption;
|
||||
const { base, ...otherViteOption } = (api.config.vite || api.config.viteOption);
|
||||
|
||||
const publicPath = base || api.config.publicPath || '/';
|
||||
|
||||
@ -27,11 +19,6 @@ export function getInnerCommonConfig(api) {
|
||||
configFile: false,
|
||||
define: getDefine(api, publicPath),
|
||||
cacheDir: join(api.cwd, '.cache'),
|
||||
css: {
|
||||
postcss: {
|
||||
...getPostcssConfig(api),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(api.config.viteVuePlugin || {}),
|
||||
SFCConfigBlockPlugin,
|
@ -1,17 +1,19 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { ViteBuildConfig } from '../shared';
|
||||
import { resolveRuntimeEnv, stringifyObjValue } from '@fesjs/utils';
|
||||
|
||||
export default (api, publicPath) => {
|
||||
const viteOption = api.config.viteOption;
|
||||
export default (api: IPluginAPI<ViteBuildConfig>, publicPath: string): Record<string, any> => {
|
||||
const viteOption = api.config.vite || api.config.viteOption;
|
||||
const env = resolveRuntimeEnv(publicPath);
|
||||
|
||||
const define = stringifyObjValue({
|
||||
...api.config.define,
|
||||
...viteOption.define,
|
||||
...(viteOption ? viteOption.define : {}),
|
||||
});
|
||||
const formatEnv = Object.keys(env).reduce((acc, cur) => {
|
||||
acc[`process.env.${cur}`] = JSON.stringify(env[cur]);
|
||||
return acc;
|
||||
}, {});
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
return {
|
||||
...formatEnv,
|
@ -1,34 +1,52 @@
|
||||
import type { ConfigEnv, Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
||||
import process from 'node:process';
|
||||
import { render } from 'ejs';
|
||||
import { expand } from 'dotenv-expand';
|
||||
import dotenv from 'dotenv';
|
||||
import path, { dirname, join } from 'pathe';
|
||||
import fse from 'fs-extra';
|
||||
import { normalizePath } from 'vite';
|
||||
import { parse } from 'node-html-parser';
|
||||
import fg from 'fast-glob';
|
||||
import consola from 'consola';
|
||||
import { dim } from 'colorette';
|
||||
import { minify } from 'html-minifier-terser';
|
||||
import { createFilter } from '@rollup/pluginutils';
|
||||
import { dim } from 'colorette';
|
||||
import consola from 'consola';
|
||||
import dotenv from 'dotenv';
|
||||
import { expand } from 'dotenv-expand';
|
||||
import { render } from 'ejs';
|
||||
import fg from 'fast-glob';
|
||||
import fse from 'fs-extra';
|
||||
import { minify } from 'html-minifier-terser';
|
||||
import { parse } from 'node-html-parser';
|
||||
import path, { dirname, join } from 'pathe';
|
||||
import { normalizePath } from 'vite';
|
||||
import history from './connectHistoryMiddleware';
|
||||
|
||||
function lookupFile(dir, formats, pathOnly = false) {
|
||||
for (const format of formats) {
|
||||
const fullPath = join(dir, format);
|
||||
if (fse.pathExistsSync(fullPath) && fse.statSync(fullPath).isFile())
|
||||
return pathOnly ? fullPath : fse.readFileSync(fullPath, 'utf-8');
|
||||
}
|
||||
const parentDir = dirname(dir);
|
||||
if (parentDir !== dir)
|
||||
return lookupFile(parentDir, formats, pathOnly);
|
||||
interface Env {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
function loadEnv(mode, envDir, prefix = '') {
|
||||
if (mode === 'local')
|
||||
throw new Error(`"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.`);
|
||||
interface ParsedUrl {
|
||||
pathname: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const env = {};
|
||||
interface Rewrites {
|
||||
from: RegExp;
|
||||
to: (params: { parsedUrl: ParsedUrl }) => string;
|
||||
}
|
||||
|
||||
function lookupFile(dir: string, formats: string[], pathOnly = false): string | undefined {
|
||||
for (const format of formats) {
|
||||
const fullPath = join(dir, format);
|
||||
if (fse.pathExistsSync(fullPath) && fse.statSync(fullPath).isFile()) {
|
||||
return pathOnly ? fullPath : fse.readFileSync(fullPath, 'utf-8');
|
||||
}
|
||||
}
|
||||
const parentDir = dirname(dir);
|
||||
if (parentDir !== dir) {
|
||||
return lookupFile(parentDir, formats, pathOnly);
|
||||
}
|
||||
}
|
||||
|
||||
function loadEnv(mode: string, envDir: string, prefix = ''): Env {
|
||||
if (mode === 'local') {
|
||||
throw new Error(`"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.`);
|
||||
}
|
||||
|
||||
const env: Env = {};
|
||||
const envFiles = [`.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env`];
|
||||
for (const file of envFiles) {
|
||||
const _path = lookupFile(envDir, [file], true);
|
||||
@ -39,18 +57,20 @@ function loadEnv(mode, envDir, prefix = '') {
|
||||
ignoreProcessEnv: true,
|
||||
});
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
if (key.startsWith(prefix) && env[key] === undefined)
|
||||
if (key.startsWith(prefix) && env[key] === undefined) {
|
||||
env[key] = value;
|
||||
}
|
||||
|
||||
else if (key === 'NODE_ENV')
|
||||
else if (key === 'NODE_ENV') {
|
||||
process.env.VITE_USER_NODE_ENV = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
async function isDirEmpty(dir) {
|
||||
async function isDirEmpty(dir: string): Promise<boolean> {
|
||||
return fse.readdir(dir).then(files => files.length === 0);
|
||||
}
|
||||
|
||||
@ -58,19 +78,44 @@ const DEFAULT_TEMPLATE = 'index.html';
|
||||
const ignoreDirs = ['.', '', '/'];
|
||||
const bodyInjectRE = /<\/body>/;
|
||||
|
||||
function createPlugin(userOptions = {}) {
|
||||
interface UserOptions {
|
||||
entry?: string;
|
||||
template?: string;
|
||||
pages?: Page[];
|
||||
verbose?: boolean;
|
||||
injectOptions?: InjectOptions;
|
||||
_minify?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Page {
|
||||
filename?: string;
|
||||
template?: string;
|
||||
entry?: string;
|
||||
injectOptions?: InjectOptions;
|
||||
}
|
||||
|
||||
interface InjectOptions {
|
||||
data?: Record<string, any>;
|
||||
ejsOptions?: any;
|
||||
tags?: any[];
|
||||
}
|
||||
|
||||
function createPlugin(userOptions: UserOptions = {}): Plugin {
|
||||
const { entry, template = DEFAULT_TEMPLATE, pages = [], verbose = false } = userOptions;
|
||||
let viteConfig;
|
||||
let env = {};
|
||||
let viteConfig: ResolvedConfig;
|
||||
let env: Env = {};
|
||||
return {
|
||||
name: 'vite:html',
|
||||
order: 'pre',
|
||||
configResolved(resolvedConfig) {
|
||||
viteConfig = resolvedConfig;
|
||||
enforce: 'pre',
|
||||
configResolved(resolvedConfig: ResolvedConfig) {
|
||||
viteConfig = resolvedConfig!;
|
||||
if (viteConfig.mode && viteConfig.root) {
|
||||
env = loadEnv(viteConfig.mode, viteConfig.root, '');
|
||||
}
|
||||
},
|
||||
config(conf) {
|
||||
const input = createInput(userOptions, conf);
|
||||
config(config: any, env: ConfigEnv) {
|
||||
const input = createInput(userOptions, env);
|
||||
if (input) {
|
||||
return {
|
||||
build: {
|
||||
@ -81,9 +126,9 @@ function createPlugin(userOptions = {}) {
|
||||
};
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
let _pages = [];
|
||||
const rewrites = [];
|
||||
configureServer(server: ViteDevServer) {
|
||||
const _pages: Page[] = [];
|
||||
const rewrites: Rewrites[] = [];
|
||||
if (!isMpa(viteConfig)) {
|
||||
const template2 = userOptions.template || DEFAULT_TEMPLATE;
|
||||
const filename = DEFAULT_TEMPLATE;
|
||||
@ -93,24 +138,25 @@ function createPlugin(userOptions = {}) {
|
||||
});
|
||||
}
|
||||
else {
|
||||
_pages = pages.map(page => ({
|
||||
_pages.push(...pages.map(page => ({
|
||||
filename: page.filename || DEFAULT_TEMPLATE,
|
||||
template: page.template || DEFAULT_TEMPLATE,
|
||||
}));
|
||||
})));
|
||||
}
|
||||
const proxy = viteConfig.server?.proxy ?? {};
|
||||
const baseUrl = viteConfig.base ?? '/';
|
||||
const keys = Object.keys(proxy);
|
||||
let indexPage = null;
|
||||
let indexPage: Page | null = null;
|
||||
for (const page of _pages) {
|
||||
if (page.filename !== 'index.html')
|
||||
rewrites.push(createRewire(page.template, page, baseUrl, keys));
|
||||
|
||||
else
|
||||
indexPage = page;
|
||||
if (page.filename !== 'index.html') {
|
||||
rewrites.push(createRewire(page.template || '', page, baseUrl, keys));
|
||||
}
|
||||
if (indexPage)
|
||||
|
||||
else { indexPage = page; }
|
||||
}
|
||||
if (indexPage) {
|
||||
rewrites.push(createRewire('', indexPage, baseUrl, keys));
|
||||
}
|
||||
|
||||
server.middlewares.use(
|
||||
history(viteConfig, {
|
||||
@ -122,7 +168,7 @@ function createPlugin(userOptions = {}) {
|
||||
},
|
||||
transformIndexHtml: {
|
||||
order: 'pre',
|
||||
async handler(html, ctx) {
|
||||
handler: async (html: string, ctx: any) => {
|
||||
const url = ctx.filename;
|
||||
const base = viteConfig.base;
|
||||
const excludeBaseUrl = url.replace(base, '/');
|
||||
@ -144,19 +190,23 @@ function createPlugin(userOptions = {}) {
|
||||
},
|
||||
},
|
||||
async closeBundle() {
|
||||
const outputDirs = [];
|
||||
const outputDirs: string[] = [];
|
||||
if (isMpa(viteConfig) || pages.length) {
|
||||
for (const page of pages) {
|
||||
if (page.template) {
|
||||
const dir = path.dirname(page.template);
|
||||
if (!ignoreDirs.includes(dir))
|
||||
if (!ignoreDirs.includes(dir)) {
|
||||
outputDirs.push(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const dir = path.dirname(template);
|
||||
if (!ignoreDirs.includes(dir))
|
||||
if (!ignoreDirs.includes(dir)) {
|
||||
outputDirs.push(dir);
|
||||
}
|
||||
}
|
||||
const cwd = path.resolve(viteConfig.root, viteConfig.build.outDir);
|
||||
const htmlFiles = await fg(
|
||||
outputDirs.map(dir => `${dir}/*.html`),
|
||||
@ -176,41 +226,45 @@ function createPlugin(userOptions = {}) {
|
||||
await Promise.all(
|
||||
htmlDirs.map(async (item) => {
|
||||
const isEmpty = await isDirEmpty(item);
|
||||
if (isEmpty)
|
||||
if (isEmpty) {
|
||||
return fse.remove(item);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createInput({ pages = [], template = DEFAULT_TEMPLATE }, viteConfig) {
|
||||
const input = {};
|
||||
function createInput({ pages = [], template = DEFAULT_TEMPLATE }: UserOptions, viteConfig: any) {
|
||||
const input: Record<string, string> = {};
|
||||
if (isMpa(viteConfig) || pages?.length) {
|
||||
const templates = pages.map(page => page.template);
|
||||
templates.forEach((temp) => {
|
||||
templates?.forEach((temp) => {
|
||||
if (temp) {
|
||||
let dirName = path.dirname(temp);
|
||||
const file = path.basename(temp);
|
||||
dirName = dirName.replace(/\s+/g, '').replace(/\//g, '-');
|
||||
const key = dirName === '.' || dirName === 'public' || !dirName ? file.replace(/\.html/, '') : dirName;
|
||||
input[key] = path.resolve(viteConfig.root, temp);
|
||||
input[key] = path.resolve(viteConfig?.root || '', temp);
|
||||
}
|
||||
});
|
||||
return input;
|
||||
}
|
||||
const dir = path.dirname(template);
|
||||
if (ignoreDirs.includes(dir))
|
||||
const dir = path.dirname(template || DEFAULT_TEMPLATE);
|
||||
if (ignoreDirs.includes(dir)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const file = path.basename(template);
|
||||
const file = path.basename(template || DEFAULT_TEMPLATE);
|
||||
const key = file.replace(/\.html/, '');
|
||||
return {
|
||||
[key]: path.resolve(viteConfig.root, template),
|
||||
[key]: path.resolve(viteConfig?.root || '', template || DEFAULT_TEMPLATE),
|
||||
};
|
||||
}
|
||||
|
||||
async function renderHtml(html, config) {
|
||||
async function renderHtml(html: string, config: any): Promise<string> {
|
||||
const { injectOptions, viteConfig, env, entry, verbose } = config;
|
||||
const { data, ejsOptions } = injectOptions;
|
||||
const { data, ejsOptions } = injectOptions || {};
|
||||
const ejsData = {
|
||||
...(viteConfig?.env ?? {}),
|
||||
...(viteConfig?.define ?? {}),
|
||||
@ -225,43 +279,45 @@ async function renderHtml(html, config) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getPage({ pages = [], entry, template = DEFAULT_TEMPLATE, inject = {} }, name, viteConfig) {
|
||||
function getPage(userOptions: UserOptions, name: string, viteConfig: ResolvedConfig | undefined) {
|
||||
const { pages = [], entry, template = DEFAULT_TEMPLATE, inject = {} } = userOptions;
|
||||
let page;
|
||||
if (isMpa(viteConfig) || pages?.length)
|
||||
if (isMpa(viteConfig) || pages?.length) {
|
||||
page = getPageConfig(name, pages, DEFAULT_TEMPLATE);
|
||||
}
|
||||
|
||||
else
|
||||
page = createSpaPage(entry, template, inject);
|
||||
else { page = createSpaPage(entry, template, inject); }
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
function isMpa(viteConfig) {
|
||||
function isMpa(viteConfig: ResolvedConfig | undefined): boolean {
|
||||
const input = viteConfig?.build?.rollupOptions?.input ?? undefined;
|
||||
return typeof input !== 'string' && Object.keys(input || {}).length > 1;
|
||||
}
|
||||
|
||||
function removeEntryScript(html, verbose = false) {
|
||||
if (!html)
|
||||
function removeEntryScript(html: string, verbose = false): string {
|
||||
if (!html) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const root = parse(html);
|
||||
const scriptNodes = root.querySelectorAll('script[type=module]') || [];
|
||||
const removedNode = [];
|
||||
const removedNode: string[] = [];
|
||||
scriptNodes.forEach((item) => {
|
||||
removedNode.push(item.toString());
|
||||
item.parentNode.removeChild(item);
|
||||
});
|
||||
verbose
|
||||
&& removedNode.length
|
||||
&& consola.warn(`vite-plugin-html: Since you have already configured entry, ${dim(
|
||||
if (verbose && removedNode.length) {
|
||||
consola.warn(`vite-plugin-html: Since you have already configured entry, ${dim(
|
||||
removedNode.toString(),
|
||||
)} is deleted. You may also delete it from the index.html.
|
||||
`);
|
||||
}
|
||||
return root.toString();
|
||||
}
|
||||
|
||||
function createSpaPage(entry, template, inject = {}) {
|
||||
function createSpaPage(entry: string | undefined, template: string, inject: InjectOptions = {}): Page {
|
||||
return {
|
||||
entry,
|
||||
filename: 'index.html',
|
||||
@ -270,7 +326,7 @@ function createSpaPage(entry, template, inject = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function getPageConfig(htmlName, pages, defaultPage) {
|
||||
function getPageConfig(htmlName: string, pages: Page[], defaultPage: string) {
|
||||
const defaultPageOption = {
|
||||
filename: defaultPage,
|
||||
template: `./${defaultPage}`,
|
||||
@ -279,15 +335,16 @@ function getPageConfig(htmlName, pages, defaultPage) {
|
||||
return page ?? defaultPageOption ?? undefined;
|
||||
}
|
||||
|
||||
function createRewire(reg, page, baseUrl, proxyUrlKeys) {
|
||||
function createRewire(reg: string, page: Page, baseUrl: string, proxyUrlKeys: string[]): Rewrites {
|
||||
return {
|
||||
from: new RegExp(`^/${reg}*`),
|
||||
to({ parsedUrl }) {
|
||||
to({ parsedUrl }: { parsedUrl: ParsedUrl }) {
|
||||
const pathname = parsedUrl.pathname;
|
||||
const excludeBaseUrl = pathname.replace(baseUrl, '/');
|
||||
const template = path.resolve(baseUrl, page.template);
|
||||
if (excludeBaseUrl === '/')
|
||||
const template = path.resolve(baseUrl, page.template || '');
|
||||
if (excludeBaseUrl === '/') {
|
||||
return template;
|
||||
}
|
||||
|
||||
const isApiUrl = proxyUrlKeys.some(item => pathname.startsWith(path.resolve(baseUrl, item)));
|
||||
return isApiUrl ? parsedUrl.path : template;
|
||||
@ -297,7 +354,7 @@ function createRewire(reg, page, baseUrl, proxyUrlKeys) {
|
||||
|
||||
const htmlFilter = createFilter(['**/*.html']);
|
||||
|
||||
function getOptions(_minify) {
|
||||
function getOptions(_minify: boolean) {
|
||||
return {
|
||||
collapseWhitespace: _minify,
|
||||
keepClosingSlash: _minify,
|
||||
@ -310,37 +367,41 @@ function getOptions(_minify) {
|
||||
};
|
||||
}
|
||||
|
||||
async function minifyHtml(html, minify$1) {
|
||||
if (typeof minify$1 === 'boolean' && !minify$1)
|
||||
async function minifyHtml(html: string, minifyOptions: boolean | any): Promise<string> {
|
||||
if (typeof minifyOptions === 'boolean' && !minifyOptions) {
|
||||
return html;
|
||||
}
|
||||
|
||||
let minifyOptions = minify$1;
|
||||
if (typeof minify$1 === 'boolean' && minify$1)
|
||||
minifyOptions = getOptions(minify$1);
|
||||
let minifyConfig = minifyOptions;
|
||||
if (typeof minifyOptions === 'boolean' && minifyOptions) {
|
||||
minifyConfig = getOptions(minifyOptions);
|
||||
}
|
||||
|
||||
const res = await minify(html, minifyOptions);
|
||||
const res = await minify(html, minifyConfig);
|
||||
return res;
|
||||
}
|
||||
|
||||
function createMinifyHtmlPlugin({ _minify = true } = {}) {
|
||||
function createMinifyHtmlPlugin({ _minify = true } = {}): Plugin {
|
||||
return {
|
||||
name: 'vite:minify-html',
|
||||
order: 'post',
|
||||
apply: 'build',
|
||||
async generateBundle(_, outBundle) {
|
||||
if (_minify) {
|
||||
for (const bundle of Object.values(outBundle)) {
|
||||
if (bundle.type === 'asset' && htmlFilter(bundle.fileName) && typeof bundle.source === 'string')
|
||||
if (bundle.type === 'asset' && htmlFilter(bundle.fileName) && typeof bundle.source === 'string') {
|
||||
bundle.source = await minifyHtml(bundle.source, _minify);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
consola.wrapConsole();
|
||||
|
||||
function createHtmlPlugin(userOptions = {}) {
|
||||
function createHtmlPlugin(userOptions: UserOptions = {}): Plugin[] {
|
||||
return [createPlugin(userOptions), createMinifyHtmlPlugin(userOptions)];
|
||||
}
|
||||
|
||||
export { createHtmlPlugin };
|
||||
export type { InjectOptions, Page, UserOptions };
|
@ -1,8 +1,12 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import process from 'node:process';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
export default (api: IPluginAPI<{ viteAnalyze: Record<string, any> }>) => {
|
||||
api.describe({
|
||||
key: 'viteAnalyze',
|
||||
config: {
|
||||
schema(joi) {
|
||||
schema(joi: any) {
|
||||
return joi.object();
|
||||
},
|
||||
default: {},
|
||||
@ -10,14 +14,14 @@ export default (api) => {
|
||||
enableBy: () => !!process.env.ANALYZE,
|
||||
});
|
||||
|
||||
api.modifyBundleConfig((memo) => {
|
||||
api.modifyBundleConfig((memo: any) => {
|
||||
memo.plugins.push(
|
||||
require('rollup-plugin-visualizer').visualizer({
|
||||
visualizer({
|
||||
filename: './.cache/visualizer/stats.html',
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
...api.viteAnalyze,
|
||||
...api.config.viteAnalyze,
|
||||
}),
|
||||
);
|
||||
|
@ -1,8 +1,11 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import process from 'node:process';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'viteHtml',
|
||||
config: {
|
||||
schema(joi) {
|
||||
schema(joi: any) {
|
||||
return joi.object();
|
||||
},
|
||||
default: {},
|
@ -1,8 +1,11 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import process from 'node:process';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'viteLegacy',
|
||||
config: {
|
||||
schema(joi) {
|
||||
schema(joi: any) {
|
||||
return joi.object();
|
||||
},
|
||||
default: {},
|
22
packages/builder-vite/src/features/viteOption.ts
Normal file
22
packages/builder-vite/src/features/viteOption.ts
Normal file
@ -0,0 +1,22 @@
|
||||
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: {},
|
||||
},
|
||||
});
|
||||
};
|
@ -1,8 +1,10 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'viteVueJsx',
|
||||
config: {
|
||||
schema(joi) {
|
||||
schema(joi: any) {
|
||||
return joi.object();
|
||||
},
|
||||
default: {},
|
@ -1,8 +1,10 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'viteVuePlugin',
|
||||
config: {
|
||||
schema(joi) {
|
||||
schema(joi: any) {
|
||||
return joi.object();
|
||||
},
|
||||
default: {},
|
30
packages/builder-vite/src/index.ts
Normal file
30
packages/builder-vite/src/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
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'),
|
||||
],
|
||||
};
|
||||
}
|
7
packages/builder-vite/src/registerBuilder.ts
Normal file
7
packages/builder-vite/src/registerBuilder.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default function (api: IPluginAPI) {
|
||||
api.registerBuilder({
|
||||
name: 'vite',
|
||||
});
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
export default function (api) {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default function (api: IPluginAPI) {
|
||||
['modifyBundleConfig'].forEach((name) => {
|
||||
api.registerMethod({ name });
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import { name } from '../package.json';
|
||||
|
||||
export default function (api) {
|
||||
export default function (api: IPluginAPI) {
|
||||
api.addConfigType(() => ({
|
||||
source: name,
|
||||
}));
|
19
packages/builder-vite/src/shared.ts
Normal file
19
packages/builder-vite/src/shared.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
9
packages/builder-vite/tsconfig.json
Normal file
9
packages/builder-vite/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": ["@fesjs/typescript-config/base.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
1
packages/builder-vite/tsconfig.tsbuildinfo
Normal file
1
packages/builder-vite/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
24
packages/builder-vite/tsup.config.ts
Normal file
24
packages/builder-vite/tsup.config.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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'],
|
||||
});
|
21
packages/builder-vite/types.d.ts
vendored
Normal file
21
packages/builder-vite/types.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fesjs/builder-webpack",
|
||||
"version": "3.1.0",
|
||||
"version": "4.0.0-beta.0",
|
||||
"description": "@fesjs/builder-webpack",
|
||||
"author": "qlin",
|
||||
"license": "MIT",
|
||||
@ -8,7 +8,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
|
||||
"directory": "packages/fes-builder-webpack"
|
||||
"directory": "packages/builder-webpack"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/WeBankFinTech/fes.js/issues"
|
||||
@ -16,46 +16,51 @@
|
||||
"keywords": [
|
||||
"fes"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"types": "types.d.ts",
|
||||
"main": "dist/index.mjs",
|
||||
"module": "dist/index.mjs",
|
||||
"files": [
|
||||
"lib",
|
||||
"dist",
|
||||
"types.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"watch": "tsup --watch",
|
||||
"build": "tsup"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fesjs/fes": "^3.1.17",
|
||||
"core-js": "^3.29.1"
|
||||
"@fesjs/fes": "^4.0.0-beta.0",
|
||||
"core-js": "^3.45.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.22.5",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.22.17",
|
||||
"@babel/plugin-proposal-function-bind": "^7.22.5",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.22.15",
|
||||
"@babel/plugin-transform-runtime": "^7.23.2",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/preset-typescript": "^7.23.2",
|
||||
"@fesjs/utils": "^3.0.3",
|
||||
"@vue/babel-plugin-jsx": "^1.2.2",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.28.3",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.27.1",
|
||||
"@babel/plugin-proposal-function-bind": "^7.27.1",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.28.3",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@fesjs/shared": "^4.0.0-beta.0",
|
||||
"@fesjs/utils": "^4.0.0-beta.0",
|
||||
"@vue/babel-plugin-jsx": "^1.5.0",
|
||||
"ajv": "^8.12.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-loader": "^10.0.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cliui": "^8.0.1",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"get-folder-size": "^2.0.1",
|
||||
"fs-extra": "^11.3.1",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"html-webpack-tags-plugin": "^3.0.2",
|
||||
"less": "^4.1.3",
|
||||
"less-loader": "^11.1.0",
|
||||
"mini-css-extract-plugin": "^2.8.1",
|
||||
"mini-css-extract-plugin": "^2.9.4",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"postcss-loader": "^7.1.0",
|
||||
@ -63,8 +68,8 @@
|
||||
"style-loader": "^3.3.2",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"vue-loader": "^17.4.2",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-5-chain": "^8.0.1",
|
||||
"webpack": "^5.101.3",
|
||||
"webpack-5-chain": "^8.0.2",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-dev-server": "^5.1.0",
|
||||
"webpackbar": "^7.0.0"
|
46
packages/builder-webpack/src/index.ts
Normal file
46
packages/builder-webpack/src/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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'),
|
||||
],
|
||||
};
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import webpack from 'webpack';
|
||||
|
||||
export async function build({ bundleConfig }) {
|
||||
export async function build(bundleConfig: webpack.Configuration): Promise<{ stats?: webpack.Stats }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const compiler = webpack(bundleConfig);
|
||||
compiler.run((err, stats) => {
|
||||
compiler!.run((err, stats) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return reject(new Error('build failed'));
|
@ -1,13 +1,12 @@
|
||||
/**
|
||||
* @copy 该文件代码大部分出自 umi,有需要请参考:
|
||||
* https://github.com/umijs/umi/blob/master/packages/preset-built-in/src/plugins/commands/build/build.ts
|
||||
*/
|
||||
|
||||
import { relative } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { relative } from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { cleanTmpPathExceptCache, getBundleAndConfigs, printFileSizes } from '../../common/buildDevUtils';
|
||||
import { build } from './build';
|
||||
|
||||
export default function (api) {
|
||||
export default function (api: IPluginAPI<WebpackBuildConfig>) {
|
||||
const {
|
||||
paths,
|
||||
utils: { rimraf, logger },
|
||||
@ -17,8 +16,6 @@ export default function (api) {
|
||||
command: 'build',
|
||||
description: 'build application for production',
|
||||
async fn() {
|
||||
const { build } = require('./build');
|
||||
|
||||
cleanTmpPathExceptCache({
|
||||
absTmpPath: paths.absTmpPath,
|
||||
});
|
||||
@ -40,11 +37,13 @@ export default function (api) {
|
||||
}
|
||||
}
|
||||
|
||||
const { stats } = await build({ bundleConfig });
|
||||
const { stats } = await build(bundleConfig);
|
||||
if (process.env.RM_TMPDIR !== 'none') {
|
||||
rimraf.sync(paths.absTmpPath);
|
||||
}
|
||||
printFileSizes(stats, relative(process.cwd(), paths.absOutputPath));
|
||||
if (stats) {
|
||||
printFileSizes({ stats, dir: relative(process.cwd(), paths.absOutputPath) });
|
||||
}
|
||||
await api.applyPlugins({
|
||||
key: 'onBuildComplete',
|
||||
type: api.ApplyPluginsType.event,
|
||||
@ -52,7 +51,8 @@ export default function (api) {
|
||||
stats,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err) {
|
||||
await api.applyPlugins({
|
||||
key: 'onBuildComplete',
|
||||
type: api.ApplyPluginsType.event,
|
@ -1,14 +1,16 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import { extname } from 'node:path';
|
||||
import historyFallback from 'connect-history-api-fallback';
|
||||
|
||||
const ASSET_EXT_NAMES = ['.ico', '.png', '.jpg', '.jpeg', '.gif', '.svg'];
|
||||
|
||||
function proxyMiddleware(api) {
|
||||
return (req, res, next) => {
|
||||
function proxyMiddleware(api: IPluginAPI<WebpackBuildConfig>) {
|
||||
return (req: any, res: any, next: any) => {
|
||||
const proxyConfig = api.config.proxy;
|
||||
if (proxyConfig) {
|
||||
if (Array.isArray(proxyConfig)) {
|
||||
if (proxyConfig.some(item => item.context.some(path => path && req.url.startsWith(path)))) {
|
||||
if (proxyConfig.some(item => item.context.some((path: string) => path && req.url.startsWith(path)))) {
|
||||
return next();
|
||||
}
|
||||
}
|
@ -1,8 +1,20 @@
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import { chalk } from '@fesjs/utils';
|
||||
import webpack from 'webpack';
|
||||
import WebpackDevServer from 'webpack-dev-server';
|
||||
|
||||
function formatProxy(proxy) {
|
||||
interface StartDevServerOptions {
|
||||
webpackConfig: webpack.Configuration;
|
||||
host: string;
|
||||
port: number;
|
||||
proxy: any;
|
||||
https: boolean | { key: string; cert: string };
|
||||
beforeMiddlewares: any[];
|
||||
afterMiddlewares: any[];
|
||||
customerDevServerConfig?: WebpackBuildConfig['devServer'];
|
||||
}
|
||||
|
||||
function formatProxy(proxy: any) {
|
||||
if (!proxy) {
|
||||
return [];
|
||||
}
|
||||
@ -19,8 +31,11 @@ function formatProxy(proxy) {
|
||||
});
|
||||
}
|
||||
|
||||
export function startDevServer({ webpackConfig, host, port, proxy, https, beforeMiddlewares, afterMiddlewares, customerDevServerConfig }) {
|
||||
const options = {
|
||||
export function startDevServer({ webpackConfig, host, port, proxy, https, beforeMiddlewares, afterMiddlewares, customerDevServerConfig }: StartDevServerOptions) {
|
||||
const headers: Record<string, string> = {
|
||||
'access-control-allow-origin': '*',
|
||||
};
|
||||
const options: WebpackDevServer.Configuration = {
|
||||
hot: true,
|
||||
allowedHosts: 'all',
|
||||
server: https ? 'https' : 'http',
|
||||
@ -39,15 +54,17 @@ export function startDevServer({ webpackConfig, host, port, proxy, https, before
|
||||
|
||||
return middlewares;
|
||||
},
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
},
|
||||
// @ts-expect-error 不知道这里为啥异常
|
||||
headers,
|
||||
...(customerDevServerConfig || {}),
|
||||
port,
|
||||
host,
|
||||
proxy: formatProxy(proxy),
|
||||
};
|
||||
const compiler = webpack(webpackConfig);
|
||||
if (!compiler) {
|
||||
throw new Error('Failed to create webpack compiler');
|
||||
}
|
||||
const server = new WebpackDevServer(options, compiler);
|
||||
if (options.host === '0.0.0.0') {
|
||||
// eslint-disable-next-line no-console
|
@ -1,38 +1,40 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { removeSync } from 'fs-extra/esm';
|
||||
import getFolderSize from 'get-folder-size';
|
||||
import { cleanTmpPathExceptCache, getBundleAndConfigs } from '../../common/buildDevUtils';
|
||||
import connectHistoryMiddleware from './connectHistoryMiddleware';
|
||||
import { startDevServer } from './devServer';
|
||||
|
||||
async function handleCacheClean(cwd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
async function handleCacheClean(cwd: string) {
|
||||
return new Promise((resolve) => {
|
||||
const cachePath = path.join(cwd, '.cache/webpack');
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return resolve();
|
||||
}
|
||||
require('get-folder-size')(cachePath, (err, size) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
return resolve(0);
|
||||
}
|
||||
// 大于 5G 清除缓存,修复 webpack 缓存无限增长问题
|
||||
// https://github.com/webpack/webpack/issues/13291
|
||||
getFolderSize.loose(cachePath).then((size) => {
|
||||
if (size > 5 * 1024 * 1024 * 1024) {
|
||||
require('fs-extra').removeSync(cachePath);
|
||||
removeSync(cachePath);
|
||||
}
|
||||
resolve(size);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default (api) => {
|
||||
export default (api: IPluginAPI<WebpackBuildConfig>) => {
|
||||
const {
|
||||
paths,
|
||||
utils: { chalk, getPort, getHostName, changePort, logger },
|
||||
} = api;
|
||||
|
||||
let port;
|
||||
let hostname;
|
||||
let server;
|
||||
let port: number;
|
||||
let hostname: string;
|
||||
let server: any;
|
||||
|
||||
async function destroy() {
|
||||
await server?.stop();
|
||||
@ -90,7 +92,6 @@ export default (api) => {
|
||||
args: {},
|
||||
});
|
||||
|
||||
const { startDevServer } = require('./devServer');
|
||||
server = startDevServer({
|
||||
webpackConfig: bundleConfig,
|
||||
host: hostname,
|
||||
@ -113,7 +114,7 @@ export default (api) => {
|
||||
fn() {
|
||||
logger.info(chalk.gray('Try to restart dev server...'));
|
||||
destroy();
|
||||
process.send({
|
||||
process.send?.({
|
||||
type: 'RESTART',
|
||||
});
|
||||
},
|
9
packages/builder-webpack/src/plugins/commands/pitcher.ts
Normal file
9
packages/builder-webpack/src/plugins/commands/pitcher.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const pitcher = (code: string): string => code;
|
||||
|
||||
export function pitch(this: any) {
|
||||
if (/&blockType=config/.test(this.resourceQuery)) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default pitcher;
|
@ -1,6 +1,10 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import { assert } from 'node:console';
|
||||
import { highlight } from 'cli-highlight';
|
||||
import { getBundleAndConfigs } from '../../common/buildDevUtils';
|
||||
|
||||
export default function (api) {
|
||||
export default function (api: IPluginAPI<WebpackBuildConfig>) {
|
||||
api.registerCommand({
|
||||
command: 'webpack',
|
||||
description: 'inspect webpack configurations',
|
||||
@ -26,26 +30,27 @@ export default function (api) {
|
||||
description: 'show full function definitions in output',
|
||||
},
|
||||
],
|
||||
async fn({ options }) {
|
||||
const assert = require('assert');
|
||||
const { toString } = require('webpack-5-chain');
|
||||
const { highlight } = require('cli-highlight');
|
||||
async fn({ options }: { options: any }) {
|
||||
const { bundleConfig } = await getBundleAndConfigs({ api });
|
||||
|
||||
let config = bundleConfig;
|
||||
let config: any = bundleConfig;
|
||||
assert(config, 'No valid config found with fes entry.');
|
||||
|
||||
if (options.rule) {
|
||||
config = config.module.rules.find((r) => r.__ruleNames[0] === options.rule);
|
||||
} else if (options.plugin) {
|
||||
config = config.plugins.find((p) => p.__pluginName === options.plugin);
|
||||
} else if (options.rules) {
|
||||
config = config.module.rules.map((r) => r.__ruleNames[0]);
|
||||
} else if (options.plugins) {
|
||||
config = config.plugins.map((p) => p.__pluginName || p.constructor.name);
|
||||
config = config.module.rules.find((r: any) => r.__ruleNames[0] === options.rule);
|
||||
}
|
||||
else if (options.plugin) {
|
||||
config = config.plugins.find((p: any) => p.__pluginName === options.plugin);
|
||||
}
|
||||
else if (options.rules) {
|
||||
config = config.module.rules.map((r: any) => r.__ruleNames[0]);
|
||||
}
|
||||
else if (options.plugins) {
|
||||
config = config.plugins.map((p: any) => p.__pluginName || p.constructor.name);
|
||||
}
|
||||
|
||||
console.log(highlight(toString(config, { verbose: options.verbose }), { language: 'js' }));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(highlight(config.toString({ verbose: options.verbose }), { language: 'js' }));
|
||||
},
|
||||
});
|
||||
}
|
@ -1,18 +1,25 @@
|
||||
/**
|
||||
* @copy 该文件代码大部分出自 umi,有需要请参考:
|
||||
* https://github.com/umijs/umi/blob/master/packages/preset-built-in/src/plugins/commands/buildDevUtils.ts
|
||||
*/
|
||||
|
||||
import { join, resolve } from 'path';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import zlib from 'zlib';
|
||||
import { rimraf, chalk } from '@fesjs/utils';
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type webpack from 'webpack';
|
||||
import type { WebpackBuildConfig } from '../../shared';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import zlib from 'node:zlib';
|
||||
import { chalk, rimraf } from '@fesjs/utils';
|
||||
import UI from 'cliui';
|
||||
import getConfig from './webpackConfig';
|
||||
|
||||
export async function getBundleAndConfigs({ api }) {
|
||||
interface GetBundleAndConfigsOptions {
|
||||
api: IPluginAPI<WebpackBuildConfig>;
|
||||
}
|
||||
|
||||
interface GetBundleAndConfigsResult {
|
||||
bundleConfig: webpack.Configuration;
|
||||
}
|
||||
|
||||
export async function getBundleAndConfigs({ api }: GetBundleAndConfigsOptions): Promise<GetBundleAndConfigsResult> {
|
||||
// get config
|
||||
const env = api.env === 'production' ? 'production' : 'development';
|
||||
const getConfigOpts = await api.applyPlugins({
|
||||
const getConfigOpts: any = await api.applyPlugins({
|
||||
type: api.ApplyPluginsType.modify,
|
||||
key: 'modifyBundleConfigOpts',
|
||||
initialValue: {
|
||||
@ -22,22 +29,21 @@ export async function getBundleAndConfigs({ api }) {
|
||||
entry: {
|
||||
index: join(api.paths.absTmpPath, 'fes.js'),
|
||||
},
|
||||
// @ts-ignore
|
||||
async modifyBabelOpts(opts) {
|
||||
async modifyBabelOpts(opts: any) {
|
||||
return api.applyPlugins({
|
||||
type: api.ApplyPluginsType.modify,
|
||||
key: 'modifyBabelOpts',
|
||||
initialValue: opts,
|
||||
});
|
||||
},
|
||||
async modifyBabelPresetOpts(opts) {
|
||||
async modifyBabelPresetOpts(opts: any) {
|
||||
return api.applyPlugins({
|
||||
type: api.ApplyPluginsType.modify,
|
||||
key: 'modifyBabelPresetOpts',
|
||||
initialValue: opts,
|
||||
});
|
||||
},
|
||||
async chainWebpack(webpackConfig, opts) {
|
||||
async chainWebpack(webpackConfig: any, opts: any) {
|
||||
return api.applyPlugins({
|
||||
type: api.ApplyPluginsType.modify,
|
||||
key: 'chainWebpack',
|
||||
@ -64,7 +70,7 @@ export async function getBundleAndConfigs({ api }) {
|
||||
args: {},
|
||||
});
|
||||
|
||||
const bundleConfig = await api.applyPlugins({
|
||||
const bundleConfig: webpack.Configuration = await api.applyPlugins({
|
||||
type: api.ApplyPluginsType.modify,
|
||||
key: 'modifyBundleConfig',
|
||||
initialValue: await getConfig({ api, ...getConfigOpts }),
|
||||
@ -74,7 +80,11 @@ export async function getBundleAndConfigs({ api }) {
|
||||
return { bundleConfig };
|
||||
}
|
||||
|
||||
export function cleanTmpPathExceptCache({ absTmpPath }) {
|
||||
interface CleanTmpPathExceptCacheOptions {
|
||||
absTmpPath: string;
|
||||
}
|
||||
|
||||
export function cleanTmpPathExceptCache({ absTmpPath }: CleanTmpPathExceptCacheOptions) {
|
||||
rimraf.sync(absTmpPath);
|
||||
}
|
||||
|
||||
@ -82,15 +92,20 @@ export function cleanTmpPathExceptCache({ absTmpPath }) {
|
||||
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({
|
||||
interface PrintFileSizesOptions {
|
||||
stats: webpack.Stats;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
export function printFileSizes({ stats, dir }: PrintFileSizesOptions) {
|
||||
const ui = UI({ width: 80 });
|
||||
const json: any = stats.toJson({
|
||||
hash: false,
|
||||
modules: false,
|
||||
chunks: false,
|
||||
});
|
||||
|
||||
const filesize = (bytes) => {
|
||||
const filesize = (bytes: number) => {
|
||||
bytes = Math.abs(bytes);
|
||||
const radix = 1024;
|
||||
const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
@ -104,14 +119,14 @@ export function printFileSizes(stats, dir) {
|
||||
return `${bytes.toFixed(1)} ${unit[loop]}`;
|
||||
};
|
||||
|
||||
const assets = json.assets ? json.assets : json?.children?.reduce((acc, child) => acc.concat(child?.assets), []);
|
||||
const assets: any[] = json.assets ? json.assets : json?.children?.reduce((acc: any, child: any) => acc.concat(child?.assets), []);
|
||||
|
||||
const seenNames = new Map();
|
||||
const isJS = (val) => /\.js$/.test(val);
|
||||
const isCSS = (val) => /\.css$/.test(val);
|
||||
const isJS = (val: string) => /\.js$/.test(val);
|
||||
const isCSS = (val: string) => /\.css$/.test(val);
|
||||
|
||||
const orderedAssets = assets
|
||||
.map((a) => {
|
||||
const orderedAssets: any[] = assets
|
||||
.map((a: any) => {
|
||||
a.name = a.name.split('?')[0];
|
||||
// These sizes are pretty large
|
||||
const isMainBundle = a.name.indexOf('fes.') === 0;
|
||||
@ -122,20 +137,24 @@ export function printFileSizes(stats, dir) {
|
||||
suggested: isLarge && isJS(a.name),
|
||||
};
|
||||
})
|
||||
.filter((a) => {
|
||||
.filter((a: any) => {
|
||||
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;
|
||||
.sort((a: any, b: any) => {
|
||||
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) {
|
||||
function getGzippedSize(asset: any) {
|
||||
const filepath = resolve(join(dir, asset.name));
|
||||
if (existsSync(filepath)) {
|
||||
const buffer = readFileSync(filepath);
|
||||
@ -144,15 +163,15 @@ export function printFileSizes(stats, dir) {
|
||||
return filesize(0);
|
||||
}
|
||||
|
||||
function makeRow(a, b, c) {
|
||||
function makeRow(a: string, b: string, c: string) {
|
||||
return ` ${a}\t ${b}\t ${c}`;
|
||||
}
|
||||
|
||||
ui.div(
|
||||
`${makeRow(chalk.cyan.bold('File'), chalk.cyan.bold('Size'), chalk.cyan.bold('Gzipped'))}\n\n${orderedAssets
|
||||
.map((asset) =>
|
||||
.map((asset: any) =>
|
||||
makeRow(
|
||||
/js$/.test(asset.name)
|
||||
asset.name.endsWith('js')
|
||||
? asset.suggested
|
||||
? chalk.yellow(join(dir, asset.name))
|
||||
: chalk.green(join(dir, asset.name))
|
||||
@ -164,13 +183,19 @@ export function printFileSizes(stats, dir) {
|
||||
.join('\n')}`,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${ui.toString()}\n\n ${chalk.gray('Images and other types of assets omitted.')}\n`);
|
||||
|
||||
if (orderedAssets?.some((asset) => asset.suggested)) {
|
||||
if (orderedAssets?.some((asset: any) => asset.suggested)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.yellow('The bundle size is significantly larger than recommended.'));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.yellow('Consider reducing it with code splitting'));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.yellow('You can also analyze the project dependencies using ANALYZE=1'));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log();
|
||||
}
|
||||
}
|
@ -1,33 +1,41 @@
|
||||
// css less post-css mini-css css 压缩
|
||||
// extraPostCSSPlugins
|
||||
// postcssLoader
|
||||
// lessLoader
|
||||
// css-loader
|
||||
// 支持 热加载
|
||||
// 性能优化
|
||||
// css 压缩 https://github.com/webpack-contrib/css-minimizer-webpack-plugin
|
||||
// 根据 entry 进行代码块拆分
|
||||
// 根据 entry 将文件输出到不同的文件夹
|
||||
import type Config from 'webpack-5-chain';
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import { deepmerge } from '@fesjs/utils';
|
||||
import { esmRequire, esmResolve } from '../../../shared';
|
||||
|
||||
function createRules({ isDev, webpackConfig, config, lang, test, loader, options, browserslist, styleLoaderOption }) {
|
||||
function applyLoaders(rule, cssLoaderOption = {}) {
|
||||
interface CreateRulesOptions {
|
||||
isDev: boolean;
|
||||
webpackConfig: Config;
|
||||
config: WebpackBuildConfig;
|
||||
lang: string;
|
||||
test: RegExp;
|
||||
loader?: string;
|
||||
options?: any;
|
||||
browserslist: string[];
|
||||
styleLoaderOption?: any;
|
||||
}
|
||||
|
||||
interface ApplyLoadersOptions {
|
||||
modules?: {
|
||||
localIdentName: string;
|
||||
};
|
||||
}
|
||||
|
||||
function createRules({ isDev, webpackConfig, config, lang, test, loader, options, browserslist, styleLoaderOption }: CreateRulesOptions) {
|
||||
function applyLoaders(rule: any, cssLoaderOption: ApplyLoadersOptions = {}) {
|
||||
if (isDev || !config.extraCSS) {
|
||||
rule.use('extra-css-loader').loader(require.resolve('style-loader')).options(Object.assign({}, styleLoaderOption));
|
||||
rule.use('extra-css-loader').loader(esmResolve('style-loader')).options(Object.assign({}, styleLoaderOption));
|
||||
}
|
||||
else {
|
||||
const loaderOptions = config.extraCSS?.loader ?? {};
|
||||
|
||||
if (!loaderOptions.publicPath && config.publicPath.startsWith('./')) {
|
||||
loaderOptions.publicPath = '../';
|
||||
}
|
||||
rule.use('extra-css-loader')
|
||||
.loader(require('mini-css-extract-plugin').loader)
|
||||
.loader(esmRequire('mini-css-extract-plugin').loader)
|
||||
.options(loaderOptions);
|
||||
}
|
||||
|
||||
rule.use('css-loader')
|
||||
.loader(require.resolve('css-loader'))
|
||||
.loader(esmResolve('css-loader'))
|
||||
.options(
|
||||
deepmerge(
|
||||
{
|
||||
@ -39,16 +47,16 @@ function createRules({ isDev, webpackConfig, config, lang, test, loader, options
|
||||
);
|
||||
|
||||
rule.use('postcss-loader')
|
||||
.loader(require.resolve('postcss-loader'))
|
||||
.loader(esmResolve('postcss-loader'))
|
||||
.options(
|
||||
deepmerge(
|
||||
{
|
||||
postcssOptions: () => ({
|
||||
plugins: [
|
||||
// https://github.com/luisrudge/postcss-flexbugs-fixes
|
||||
require('postcss-flexbugs-fixes'),
|
||||
require('postcss-safe-parser'),
|
||||
[require('autoprefixer'), { ...config.autoprefixer, overrideBrowserslist: browserslist }],
|
||||
esmRequire('postcss-flexbugs-fixes'),
|
||||
esmRequire('postcss-safe-parser'),
|
||||
[esmRequire('autoprefixer'), { overrideBrowserslist: browserslist }],
|
||||
...(config.extraPostCSSPlugins ? config.extraPostCSSPlugins : []),
|
||||
],
|
||||
}),
|
||||
@ -58,7 +66,7 @@ function createRules({ isDev, webpackConfig, config, lang, test, loader, options
|
||||
);
|
||||
|
||||
if (loader) {
|
||||
rule.use(loader).loader(require.resolve(loader)).options(options);
|
||||
rule.use(loader).loader(esmResolve(loader)).options(options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +79,14 @@ function createRules({ isDev, webpackConfig, config, lang, test, loader, options
|
||||
applyLoaders(rule.oneOf('css'));
|
||||
}
|
||||
|
||||
export default function createCssWebpackConfig({ isDev, config, webpackConfig, browserslist }) {
|
||||
interface CreateCssWebpackConfigOptions {
|
||||
isDev: boolean;
|
||||
config: WebpackBuildConfig;
|
||||
webpackConfig: Config;
|
||||
browserslist: string[];
|
||||
}
|
||||
|
||||
export default function createCssWebpackConfig({ isDev, config, webpackConfig, browserslist }: CreateCssWebpackConfigOptions) {
|
||||
createRules({
|
||||
isDev,
|
||||
webpackConfig,
|
||||
@ -98,7 +113,7 @@ export default function createCssWebpackConfig({ isDev, config, webpackConfig, b
|
||||
});
|
||||
|
||||
if (!isDev && config.extraCSS) {
|
||||
webpackConfig.plugin('extra-css').use(require.resolve('mini-css-extract-plugin'), [
|
||||
webpackConfig.plugin('extra-css').use(esmResolve('mini-css-extract-plugin'), [
|
||||
Object.assign(
|
||||
{
|
||||
filename: 'static/[name].[contenthash:8].css',
|
||||
@ -110,16 +125,16 @@ export default function createCssWebpackConfig({ isDev, config, webpackConfig, b
|
||||
}
|
||||
|
||||
if (!isDev) {
|
||||
webpackConfig.optimization.minimizer('css').use(require.resolve('css-minimizer-webpack-plugin'), [{}]);
|
||||
webpackConfig.optimization.minimizer('css').use(esmResolve('css-minimizer-webpack-plugin'), [{}]);
|
||||
}
|
||||
|
||||
return (options) => {
|
||||
return (options: Partial<CreateRulesOptions>) => {
|
||||
createRules({
|
||||
isDev,
|
||||
config,
|
||||
webpackConfig,
|
||||
browserslist,
|
||||
...options,
|
||||
});
|
||||
} as CreateRulesOptions);
|
||||
};
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
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,
|
||||
},
|
||||
]);
|
||||
}
|
@ -1,14 +1,28 @@
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import process from 'node:process';
|
||||
import { winPath } from '@fesjs/utils';
|
||||
import { esmRequire, esmResolve } from '../../../shared';
|
||||
|
||||
function getBabelOpts({ cwd, targets, config, presetOpts }) {
|
||||
const presets = [
|
||||
interface PresetOpts {
|
||||
transformRuntime: Record<string, any>;
|
||||
}
|
||||
|
||||
interface GetBabelOptsOptions {
|
||||
cwd: string;
|
||||
targets: Record<string, string>;
|
||||
config: WebpackBuildConfig;
|
||||
presetOpts: PresetOpts;
|
||||
}
|
||||
|
||||
function getBabelOpts({ cwd, targets, config, presetOpts }: GetBabelOptsOptions) {
|
||||
const presets: any[] = [
|
||||
[
|
||||
require.resolve('@babel/preset-env'),
|
||||
esmResolve('@babel/preset-env'),
|
||||
{
|
||||
targets,
|
||||
useBuiltIns: 'usage',
|
||||
corejs: {
|
||||
version: require('core-js/package.json').version,
|
||||
version: esmRequire('core-js/package.json').version,
|
||||
proposals: true,
|
||||
},
|
||||
modules: false,
|
||||
@ -16,7 +30,7 @@ function getBabelOpts({ cwd, targets, config, presetOpts }) {
|
||||
],
|
||||
[
|
||||
// FEATURE 实现类型安全检查
|
||||
require('@babel/preset-typescript').default,
|
||||
esmRequire('@babel/preset-typescript').default,
|
||||
{
|
||||
// https://babeljs.io/docs/en/babel-plugin-transform-typescript#impartial-namespace-support
|
||||
allowNamespaces: true,
|
||||
@ -26,24 +40,24 @@ function getBabelOpts({ cwd, targets, config, presetOpts }) {
|
||||
],
|
||||
...(config.extraBabelPresets || []),
|
||||
];
|
||||
const plugins = [
|
||||
require('@babel/plugin-proposal-export-default-from').default,
|
||||
const plugins: any[] = [
|
||||
esmRequire('@babel/plugin-proposal-export-default-from').default,
|
||||
[
|
||||
require('@babel/plugin-proposal-pipeline-operator').default,
|
||||
esmRequire('@babel/plugin-proposal-pipeline-operator').default,
|
||||
{
|
||||
proposal: 'minimal',
|
||||
},
|
||||
],
|
||||
require('@babel/plugin-proposal-do-expressions').default,
|
||||
require('@babel/plugin-proposal-function-bind').default,
|
||||
esmRequire('@babel/plugin-proposal-do-expressions').default,
|
||||
esmRequire('@babel/plugin-proposal-function-bind').default,
|
||||
[
|
||||
require.resolve('@babel/plugin-transform-runtime'),
|
||||
esmResolve('@babel/plugin-transform-runtime'),
|
||||
{
|
||||
useESModules: true,
|
||||
...presetOpts.transformRuntime,
|
||||
},
|
||||
],
|
||||
require.resolve('@vue/babel-plugin-jsx'),
|
||||
esmResolve('@vue/babel-plugin-jsx'),
|
||||
...(config.extraBabelPlugins || []),
|
||||
];
|
||||
return {
|
||||
@ -61,8 +75,16 @@ function getBabelOpts({ cwd, targets, config, presetOpts }) {
|
||||
};
|
||||
}
|
||||
|
||||
export default async ({ cwd, config, modifyBabelOpts, modifyBabelPresetOpts, targets }) => {
|
||||
let presetOpts = {
|
||||
interface ExportDefaultOptions {
|
||||
cwd: string;
|
||||
config: WebpackBuildConfig;
|
||||
modifyBabelOpts?: (opts: any) => Promise<any>;
|
||||
modifyBabelPresetOpts?: (opts: PresetOpts) => Promise<PresetOpts>;
|
||||
targets: Record<string, string>;
|
||||
}
|
||||
|
||||
export default async ({ cwd, config, modifyBabelOpts, modifyBabelPresetOpts, targets }: ExportDefaultOptions) => {
|
||||
let presetOpts: PresetOpts = {
|
||||
transformRuntime: {},
|
||||
};
|
||||
if (modifyBabelPresetOpts) {
|
@ -0,0 +1,107 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { platform } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import webpack from 'webpack';
|
||||
import Config from 'webpack-5-chain';
|
||||
import pkg from '../../../../package.json' assert { type: 'json' };
|
||||
import { esmResolve } from '../../../shared';
|
||||
import createCssWebpackConfig from './css';
|
||||
import createDefineWebpackConfig from './define';
|
||||
import getBabelOpts from './getBabelOpts';
|
||||
@ -22,32 +27,54 @@ const DEFAULT_EXCLUDE_NODE_MODULES = [
|
||||
'html-entities',
|
||||
];
|
||||
|
||||
function genTranspileDepRegex(exclude) {
|
||||
function genTranspileDepRegex(exclude: (string | RegExp)[]) {
|
||||
exclude = exclude.concat(DEFAULT_EXCLUDE_NODE_MODULES);
|
||||
const deps = exclude.map((dep) => {
|
||||
if (typeof dep === 'string') {
|
||||
const depPath = join('node_modules', dep, '/');
|
||||
return require('node:os').platform().startsWith('win') ? depPath.replace(/\\/g, '\\\\') : depPath;
|
||||
return platform().startsWith('win') ? depPath.replace(/\\/g, '\\\\') : depPath;
|
||||
}
|
||||
if (dep instanceof RegExp) {
|
||||
return dep.source;
|
||||
}
|
||||
if (dep instanceof RegExp) { return dep.source; }
|
||||
|
||||
throw new Error('exclude only accepts an array of string or regular expressions');
|
||||
});
|
||||
return deps.length ? new RegExp(deps.join('|')) : null;
|
||||
}
|
||||
|
||||
function handleAlias({ api, webpackConfig }) {
|
||||
interface HandleAliasOptions {
|
||||
api: IPluginAPI<WebpackBuildConfig>;
|
||||
webpackConfig: Config;
|
||||
}
|
||||
|
||||
function handleAlias({ api, webpackConfig }: HandleAliasOptions) {
|
||||
const config = api.config;
|
||||
if (config.alias) {
|
||||
Object.keys(config.alias).forEach((key) => {
|
||||
webpackConfig.resolve.alias.set(key, config.alias[key]);
|
||||
webpackConfig.resolve.alias.set(key, config.alias![key]);
|
||||
});
|
||||
}
|
||||
webpackConfig.resolve.alias.set('@', api.paths.absSrcPath);
|
||||
webpackConfig.resolve.alias.set('@@', api.paths.absTmpPath);
|
||||
}
|
||||
|
||||
export default async function getConfig({ api, cwd, config, env, entry = {}, modifyBabelOpts, modifyBabelPresetOpts, chainWebpack, headScripts, publicPath }) {
|
||||
interface GetConfigOptions {
|
||||
api: IPluginAPI<WebpackBuildConfig>;
|
||||
cwd: string;
|
||||
config: IPluginAPI<WebpackBuildConfig>['config'];
|
||||
env: 'development' | 'production';
|
||||
entry?: Record<string, string>;
|
||||
modifyBabelOpts?: (opts: any) => Promise<any>;
|
||||
modifyBabelPresetOpts?: (opts: any) => Promise<any>;
|
||||
chainWebpack?: (webpackConfig: Config, opts: any) => Promise<void>;
|
||||
headScripts?: () => Promise<any[]>;
|
||||
publicPath?: string;
|
||||
}
|
||||
|
||||
export default async function getConfig(
|
||||
{ api, cwd, config, env, entry = {}, modifyBabelOpts, modifyBabelPresetOpts, chainWebpack, headScripts, publicPath }: GetConfigOptions,
|
||||
): Promise<webpack.Configuration> {
|
||||
const isDev = env === 'development';
|
||||
const isProd = env === 'production';
|
||||
const webpackConfig = new Config();
|
||||
@ -56,12 +83,15 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
webpackConfig.mode(env);
|
||||
webpackConfig.stats('errors-only');
|
||||
webpackConfig.externals(config.externals || {});
|
||||
webpackConfig.devtool(isDev ? config.devtool || 'cheap-module-source-map' : config.devtool);
|
||||
const devtool = isDev ? config.devtool || 'cheap-module-source-map' : config.devtool;
|
||||
if (devtool) {
|
||||
webpackConfig.devtool(devtool);
|
||||
}
|
||||
|
||||
// --------------- cache -----------
|
||||
webpackConfig.cache({
|
||||
type: 'filesystem',
|
||||
version: require('../../../../package.json').version,
|
||||
version: pkg.version,
|
||||
cacheDirectory: join(cwd, '.cache/webpack'),
|
||||
});
|
||||
|
||||
@ -74,7 +104,7 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
// --------------- output -----------
|
||||
webpackConfig.output
|
||||
.path(absoluteOutput)
|
||||
.publicPath(publicPath)
|
||||
.publicPath(publicPath || '')
|
||||
.filename('static/[name].[contenthash:8].js')
|
||||
.chunkFilename('static/[name].[contenthash:8].chunk.js')
|
||||
.assetModuleFilename('static/[name][hash:8][ext]');
|
||||
@ -131,21 +161,23 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
.rule('js')
|
||||
.test(/\.(js|mjs|jsx|ts|tsx)$/)
|
||||
.exclude
|
||||
.add((filepath) => {
|
||||
.add((filepath: string) => {
|
||||
// always transpile js in vue files
|
||||
if (/(\.vue|\.jsx)$/.test(filepath)) { return false; }
|
||||
if (/\.vue$|\.jsx$/.test(filepath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't transpile node_modules
|
||||
return /node_modules/.test(filepath);
|
||||
})
|
||||
.end()
|
||||
.use('babel-loader')
|
||||
.loader(require.resolve('babel-loader'))
|
||||
.loader(esmResolve('babel-loader'))
|
||||
.options(babelOpts);
|
||||
|
||||
// 为了避免第三方依赖包编译不充分导致线上问题,默认对 node_modules 也进行全编译,只在生产构建的时候进行
|
||||
if (isProd) {
|
||||
const transpileDepRegex = genTranspileDepRegex(config.nodeModulesTransform.exclude);
|
||||
const transpileDepRegex = genTranspileDepRegex(config.nodeModulesTransform?.exclude || []);
|
||||
webpackConfig.module
|
||||
.rule('js-in-node_modules')
|
||||
.test(/\.(js|mjs)$/)
|
||||
@ -153,14 +185,16 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
.add(/node_modules/)
|
||||
.end()
|
||||
.exclude
|
||||
.add((filepath) => {
|
||||
if (transpileDepRegex && transpileDepRegex.test(filepath)) { return true; }
|
||||
.add((filepath: string) => {
|
||||
if (transpileDepRegex && transpileDepRegex.test(filepath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.end()
|
||||
.use('babel-loader')
|
||||
.loader(require.resolve('babel-loader'))
|
||||
.loader(esmResolve('babel-loader'))
|
||||
.options(babelOpts);
|
||||
}
|
||||
|
||||
@ -190,22 +224,27 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
});
|
||||
|
||||
// --------------- copy -----------
|
||||
const copyFiles = Array.isArray(config.copy) ? config.copy : config.copy ? [config.copy] : [];
|
||||
const copyPatterns = [
|
||||
existsSync(join(cwd, 'public')) && {
|
||||
from: join(cwd, 'public'),
|
||||
filter: (resourcePath) => {
|
||||
if (resourcePath.includes('.DS_Store')) { return false; }
|
||||
filter: (resourcePath: string) => {
|
||||
if (resourcePath.includes('.DS_Store')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (publicCopyIgnore.includes(resourcePath)) { return false; }
|
||||
if (publicCopyIgnore.includes(resourcePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
to: absoluteOutput,
|
||||
},
|
||||
...(config.copy || []).map((item) => {
|
||||
...copyFiles.map((item: any) => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
from: join(cwd, item.from),
|
||||
from: join(cwd, item),
|
||||
to: absoluteOutput,
|
||||
};
|
||||
}
|
||||
@ -217,14 +256,14 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
].filter(Boolean);
|
||||
// const publicCopyIgnore = ['.DS_Store'];
|
||||
if (copyPatterns.length) {
|
||||
webpackConfig.plugin('copy').use(require.resolve('copy-webpack-plugin'), [
|
||||
webpackConfig.plugin('copy').use(esmResolve('copy-webpack-plugin'), [
|
||||
{
|
||||
patterns: copyPatterns,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
webpackConfig.plugin('progress').use(require.resolve(require.resolve('webpackbar')));
|
||||
webpackConfig.plugin('progress').use(esmResolve('webpackbar'));
|
||||
|
||||
// --------------- define -----------
|
||||
createDefineWebpackConfig({
|
||||
@ -282,7 +321,7 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
});
|
||||
}
|
||||
|
||||
const memo = webpackConfig.toConfig();
|
||||
const memo: webpack.Configuration = webpackConfig.toConfig();
|
||||
memo.infrastructureLogging = {
|
||||
level: 'error',
|
||||
...memo.infrastructureLogging,
|
@ -1,4 +1,8 @@
|
||||
import type Config from 'webpack-5-chain';
|
||||
import type { WebpackBuildConfig } from '../../../shared';
|
||||
import process from 'node:process';
|
||||
import { deepmerge } from '@fesjs/utils';
|
||||
import { esmResolve } from '../../../shared';
|
||||
|
||||
const defaultTerserOptions = {
|
||||
compress: {
|
||||
@ -37,14 +41,22 @@ const defaultTerserOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
const terserOptions = (config) => ({
|
||||
terserOptions: deepmerge(defaultTerserOptions, config.terserOptions || {}),
|
||||
function terserOptions(config: WebpackBuildConfig) {
|
||||
return {
|
||||
terserOptions: deepmerge(defaultTerserOptions, (config as any).terserOptions || {}),
|
||||
extractComments: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default function createMinimizerWebpackConfig({ isProd, config, webpackConfig }) {
|
||||
interface CreateMinimizerWebpackConfigOptions {
|
||||
isProd: boolean;
|
||||
config: WebpackBuildConfig;
|
||||
webpackConfig: Config;
|
||||
}
|
||||
|
||||
export default function createMinimizerWebpackConfig({ isProd, config, webpackConfig }: CreateMinimizerWebpackConfigOptions) {
|
||||
if (isProd) {
|
||||
webpackConfig.optimization.minimizer('terser').use(require.resolve('terser-webpack-plugin'), [terserOptions(config)]);
|
||||
webpackConfig.optimization.minimizer('terser').use(esmResolve('terser-webpack-plugin'), [terserOptions(config)]);
|
||||
}
|
||||
if (process.env.FES_ENV === 'test') {
|
||||
webpackConfig.optimization.minimize(false);
|
@ -0,0 +1,35 @@
|
||||
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);
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import type Config from 'webpack-5-chain';
|
||||
import type { WebpackBuildConfig } from '../../shared';
|
||||
import process from 'node:process';
|
||||
import { esmRequire } from '../../shared';
|
||||
|
||||
export default (api: IPluginAPI<WebpackBuildConfig>) => {
|
||||
api.describe({
|
||||
key: 'analyze',
|
||||
config: {
|
||||
@ -29,8 +35,8 @@ export default (api) => {
|
||||
},
|
||||
enableBy: () => !!process.env.ANALYZE,
|
||||
});
|
||||
api.chainWebpack((webpackConfig) => {
|
||||
webpackConfig.plugin('bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [api.config?.analyze || {}]);
|
||||
(api as any).chainWebpack((webpackConfig: Config) => {
|
||||
webpackConfig.plugin('bundle-analyzer').use(esmRequire('webpack-bundle-analyzer').BundleAnalyzerPlugin, [api.config?.analyze || {}]);
|
||||
return webpackConfig;
|
||||
});
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'chainWebpack',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'copy',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'cssLoader',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'devServer',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'devtool',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'exportStatic',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'externals',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'extraBabelPlugins',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'extraBabelPresets',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'extraCSS',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'extraPostCSSPlugins',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'html',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'lessLoader',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'nodeModulesTransform',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'postcssLoader',
|
||||
config: {
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default (api: IPluginAPI) => {
|
||||
api.describe({
|
||||
key: 'vueLoader',
|
||||
config: {
|
7
packages/builder-webpack/src/plugins/registerBuilder.ts
Normal file
7
packages/builder-webpack/src/plugins/registerBuilder.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default function (api: IPluginAPI) {
|
||||
api.registerBuilder({
|
||||
name: 'webpack',
|
||||
});
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
export default function (api) {
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
|
||||
export default function (api: IPluginAPI) {
|
||||
[
|
||||
'addHTMLHeadScripts',
|
||||
'modifyBundleConfigOpts',
|
@ -1,6 +1,7 @@
|
||||
import type { IPluginAPI } from '@fesjs/shared';
|
||||
import { name } from '../../package.json';
|
||||
|
||||
export default function (api) {
|
||||
export default function (api: IPluginAPI) {
|
||||
api.addConfigType(() => ({
|
||||
source: name,
|
||||
}));
|
72
packages/builder-webpack/src/shared.ts
Normal file
72
packages/builder-webpack/src/shared.ts
Normal file
@ -0,0 +1,72 @@
|
||||
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;
|
||||
};
|
||||
}
|
@ -1,12 +1,30 @@
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
import assert from 'node:assert';
|
||||
import path from 'node:path';
|
||||
import { lodash, winPath } from '@fesjs/utils';
|
||||
|
||||
interface SpecifierObject {
|
||||
local: string;
|
||||
exported: string;
|
||||
}
|
||||
|
||||
type Specifier = string | SpecifierObject;
|
||||
|
||||
interface Item {
|
||||
source: string;
|
||||
exportAll?: boolean;
|
||||
specifiers?: Specifier[];
|
||||
}
|
||||
|
||||
interface GenerateExportsOptions {
|
||||
item: Item;
|
||||
fesExportsHook: Record<string, boolean>;
|
||||
}
|
||||
|
||||
const reserveLibrarys = ['fes']; // reserve library
|
||||
// todo 插件导出内容冲突问题待解决
|
||||
const reserveExportsNames = ['Link', 'NavLink', 'Redirect', 'dynamic', 'withRouter', 'Route'];
|
||||
|
||||
export default function generateExports(basePath, { item, fesExportsHook }) {
|
||||
export default function generateExports(basePath: string, { item, fesExportsHook }: GenerateExportsOptions): string {
|
||||
assert(item.source, 'source should be supplied.');
|
||||
const source = path.relative(path.basename(basePath), item.source);
|
||||
assert(item.exportAll || item.specifiers, 'exportAll or specifiers should be supplied.');
|
||||
@ -14,8 +32,8 @@ export default function generateExports(basePath, { item, fesExportsHook }) {
|
||||
if (item.exportAll) {
|
||||
return `export * from '${winPath(source)}';`;
|
||||
}
|
||||
assert(Array.isArray(item.specifiers), `specifiers should be Array, but got ${item.specifiers.toString()}.`);
|
||||
const specifiersStrArr = item.specifiers.map((specifier) => {
|
||||
assert(Array.isArray(item.specifiers), `specifiers should be Array, but got ${item.specifiers?.toString()}.`);
|
||||
const specifiersStrArr = item.specifiers!.map((specifier) => {
|
||||
if (typeof specifier === 'string') {
|
||||
assert(!reserveExportsNames.includes(specifier), `${specifier} is reserve name, you can use 'exported' to set alias.`);
|
||||
assert(!fesExportsHook[specifier], `${specifier} is Defined, you can use 'exported' to set alias.`);
|
||||
@ -23,8 +41,8 @@ export default function generateExports(basePath, { item, fesExportsHook }) {
|
||||
return specifier;
|
||||
}
|
||||
assert(lodash.isPlainObject(specifier), `Configure item context should be Plain Object, but got ${specifier}.`);
|
||||
assert(specifier.local && specifier.exported, 'local and exported should be supplied.');
|
||||
return `${specifier.local} as ${specifier.exported}`;
|
||||
assert((specifier as SpecifierObject).local && (specifier as SpecifierObject).exported, 'local and exported should be supplied.');
|
||||
return `${(specifier as SpecifierObject).local} as ${(specifier as SpecifierObject).exported}`;
|
||||
});
|
||||
return `export { ${specifiersStrArr.join(', ')} } from '${winPath(source)}';`;
|
||||
}
|
9
packages/builder-webpack/tsconfig.json
Normal file
9
packages/builder-webpack/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": ["@fesjs/typescript-config/base.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
1
packages/builder-webpack/tsconfig.tsbuildinfo
Normal file
1
packages/builder-webpack/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
41
packages/builder-webpack/tsup.config.ts
Normal file
41
packages/builder-webpack/tsup.config.ts
Normal file
@ -0,0 +1,41 @@
|
||||
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');
|
||||
},
|
||||
});
|
6
packages/builder-webpack/types.d.ts
vendored
Normal file
6
packages/builder-webpack/types.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// eslint-disable-next-line antfu/no-import-dist
|
||||
import type { WebpackBuildConfig } from './dist/index.d.mts';
|
||||
|
||||
declare module '@fesjs/fes' {
|
||||
interface PluginBuildConfig extends WebpackBuildConfig {}
|
||||
}
|
@ -35,16 +35,13 @@ 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) | 水印 |
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fesjs/compiler",
|
||||
"version": "3.0.6",
|
||||
"version": "4.0.0-beta.0",
|
||||
"description": "@fesjs/compiler",
|
||||
"author": "qlin",
|
||||
"license": "MIT",
|
||||
@ -8,7 +8,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
|
||||
"directory": "packages/fes-compiler"
|
||||
"directory": "packages/compiler"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/WeBankFinTech/fes.js/issues"
|
||||
@ -16,21 +16,31 @@
|
||||
"keywords": [
|
||||
"fes"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "dist/index.d.mts",
|
||||
"files": [
|
||||
"lib"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"watch": "tsup --watch",
|
||||
"build": "tsup"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/register": "^7.22.15",
|
||||
"@fesjs/utils": "^3.0.3",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@fesjs/utils": "^4.0.0-beta.0",
|
||||
"commander": "^7.0.0",
|
||||
"dotenv": "8.2.0",
|
||||
"fs-extra": "^11.3.1",
|
||||
"joi": "17.3.0",
|
||||
"package-up": "^5.0.0",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4"
|
||||
}
|
||||
}
|
@ -1,71 +1,92 @@
|
||||
/**
|
||||
* @copy 该文件代码大部分出自 umi,有需要请参考:
|
||||
* https://github.com/umijs/umi/tree/master/packages/core
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { extname, join } from 'path';
|
||||
import assert from 'assert';
|
||||
import { chalk, chokidar, compatESModuleRequire, deepmerge, cleanRequireCache, lodash, parseRequireDeps, winPath } from '@fesjs/utils';
|
||||
import type { ServiceInstance, UserConfig } from '../types';
|
||||
import assert from 'node:assert';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { extname, join } from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { chalk, chokidar, compatESModuleRequire, deepmerge, lodash, winPath } from '@fesjs/utils';
|
||||
import joi from 'joi';
|
||||
import { ServiceStage } from '../service/enums';
|
||||
import { getUserConfigWithKey, updateUserConfigWithKey } from './utils/configUtils';
|
||||
import isEqual from './utils/isEqual';
|
||||
import mergeDefault from './utils/mergeDefault';
|
||||
|
||||
const CONFIG_FILES = ['.fes.js'];
|
||||
interface ConfigOptions {
|
||||
cwd?: string;
|
||||
service: ServiceInstance;
|
||||
localConfig?: boolean;
|
||||
}
|
||||
|
||||
interface WatchOptions {
|
||||
userConfig: UserConfig;
|
||||
onChange: (params: {
|
||||
userConfig: UserConfig;
|
||||
pluginChanged: Array<{ key: string; pluginId: string }>;
|
||||
valueChanged: Array<{ key: string; pluginId: string }>;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const CONFIG_FILES: string[] = ['.fes.js'];
|
||||
|
||||
export default class Config {
|
||||
cwd;
|
||||
cwd: string;
|
||||
|
||||
service;
|
||||
service: ServiceInstance;
|
||||
|
||||
config;
|
||||
config: any;
|
||||
|
||||
localConfig;
|
||||
localConfig: boolean;
|
||||
|
||||
configFile;
|
||||
configFile: string[];
|
||||
|
||||
constructor(opts) {
|
||||
constructor(opts: ConfigOptions) {
|
||||
this.cwd = opts.cwd || process.cwd();
|
||||
this.service = opts.service;
|
||||
this.localConfig = opts.localConfig;
|
||||
this.localConfig = opts.localConfig || false;
|
||||
this.configFile = [];
|
||||
this.config = null;
|
||||
}
|
||||
|
||||
async getDefaultConfig() {
|
||||
async getDefaultConfig(): Promise<Record<string, any>> {
|
||||
const pluginIds = Object.keys(this.service.plugins);
|
||||
|
||||
// collect default config
|
||||
const defaultConfig = pluginIds.reduce((memo, pluginId) => {
|
||||
const defaultConfig = pluginIds.reduce((memo: Record<string, any>, pluginId: string) => {
|
||||
const { key, config = {} } = this.service.plugins[pluginId];
|
||||
if ('default' in config) memo[key] = config.default;
|
||||
if ('default' in config) {
|
||||
memo[key] = config.default;
|
||||
}
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
getConfig({ defaultConfig }) {
|
||||
async getConfig(defaultConfig: Record<string, any>): Promise<UserConfig> {
|
||||
assert(this.service.stage >= ServiceStage.pluginReady, 'Config.getConfig() failed, it should not be executed before plugin is ready.');
|
||||
|
||||
const userConfig = this.getUserConfig();
|
||||
const userConfig = await this.getUserConfig();
|
||||
// 用于提示用户哪些 key 是未定义的
|
||||
// TODO: 考虑不排除 false 的 key
|
||||
const userConfigKeys = Object.keys(userConfig).filter((key) => userConfig[key] !== false);
|
||||
const userConfigKeys = Object.keys(userConfig).filter(key => userConfig[key] !== false);
|
||||
|
||||
// get config
|
||||
const pluginIds = Object.keys(this.service.plugins);
|
||||
pluginIds.forEach((pluginId) => {
|
||||
pluginIds.forEach((pluginId: string) => {
|
||||
const { key, config = {} } = this.service.plugins[pluginId];
|
||||
// recognize as key if have schema config
|
||||
if (!config.schema) return;
|
||||
if (!config.schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = getUserConfigWithKey({
|
||||
key,
|
||||
userConfig,
|
||||
});
|
||||
// 不校验 false 的值,此时已禁用插件
|
||||
if (value === false) return;
|
||||
if (value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do validate
|
||||
const schema = config.schema(joi);
|
||||
@ -105,39 +126,33 @@ export default class Config {
|
||||
return userConfig;
|
||||
}
|
||||
|
||||
getUserConfig() {
|
||||
async getUserConfig(): Promise<UserConfig> {
|
||||
const configFile = this.getConfigFile();
|
||||
this.configFile = configFile;
|
||||
if (configFile.length > 0) {
|
||||
// clear require cache and set babel register
|
||||
const requireDeps = configFile.reduce((memo, file) => {
|
||||
memo = memo.concat(parseRequireDeps(file));
|
||||
return memo;
|
||||
}, []);
|
||||
requireDeps.forEach(cleanRequireCache);
|
||||
this.service.babelRegister.setOnlyMap({
|
||||
key: 'config',
|
||||
value: requireDeps,
|
||||
});
|
||||
|
||||
// require config and merge
|
||||
return this.mergeConfig(...this.requireConfigs(configFile));
|
||||
const configs = await this.requireConfigs(configFile);
|
||||
return this.mergeConfig(configs);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
addAffix(file, affix) {
|
||||
addAffix(file: string, affix: string): string {
|
||||
const ext = extname(file);
|
||||
return file.replace(new RegExp(`${ext}$`), `.${affix}${ext}`);
|
||||
}
|
||||
|
||||
requireConfigs(configFiles) {
|
||||
// eslint-disable-next-line
|
||||
return configFiles.map((f) => compatESModuleRequire(require(f)));
|
||||
async requireConfigs(configFiles: string[]): Promise<any[]> {
|
||||
const models = await Promise.all(configFiles.map((f) => {
|
||||
// 使用 pathToFileURL 确保在 Windows 下路径格式正确
|
||||
const fileUrl = pathToFileURL(f).href;
|
||||
// 避免命中模块缓存
|
||||
return import(`${fileUrl}?t=${Date.now()}`);
|
||||
}));
|
||||
return models.map(m => compatESModuleRequire(m));
|
||||
}
|
||||
|
||||
mergeConfig(...configs) {
|
||||
let ret = {};
|
||||
mergeConfig(configs: any[]): UserConfig {
|
||||
let ret: UserConfig = {};
|
||||
for (const config of configs) {
|
||||
// TODO: 精细化处理,比如处理 dotted config key
|
||||
ret = deepmerge(ret, config);
|
||||
@ -145,12 +160,14 @@ export default class Config {
|
||||
return ret;
|
||||
}
|
||||
|
||||
getConfigFile() {
|
||||
getConfigFile(): string[] {
|
||||
// TODO: support custom config file
|
||||
let configFile = CONFIG_FILES.find((f) => existsSync(join(this.cwd, f)));
|
||||
if (!configFile) return [];
|
||||
let configFile = CONFIG_FILES.find(f => existsSync(join(this.cwd, f)));
|
||||
if (!configFile) {
|
||||
return [];
|
||||
}
|
||||
configFile = winPath(configFile);
|
||||
let envConfigFile;
|
||||
let envConfigFile: string | undefined;
|
||||
// 潜在问题:
|
||||
// .local 和 .env 的配置必须有 configFile 才有效
|
||||
if (process.env.FES_ENV) {
|
||||
@ -160,45 +177,46 @@ export default class Config {
|
||||
}
|
||||
}
|
||||
const files = [configFile, envConfigFile, this.localConfig && this.addAffix(configFile, 'local')]
|
||||
.filter((f) => !!f)
|
||||
.map((f) => join(this.cwd, f))
|
||||
.filter((f) => existsSync(f));
|
||||
return files;
|
||||
.filter(f => !!f)
|
||||
.map(f => join(this.cwd, f as string))
|
||||
.filter(f => existsSync(f));
|
||||
return files as string[];
|
||||
}
|
||||
|
||||
getWatchFilesAndDirectories() {
|
||||
getWatchFilesAndDirectories(): string[] {
|
||||
const fesEnv = process.env.FES_ENV;
|
||||
const configFiles = lodash.clone(CONFIG_FILES);
|
||||
CONFIG_FILES.forEach((f) => {
|
||||
if (this.localConfig) configFiles.push(this.addAffix(f, 'local'));
|
||||
if (fesEnv) configFiles.push(this.addAffix(f, fesEnv));
|
||||
if (this.localConfig) {
|
||||
configFiles.push(this.addAffix(f, 'local'));
|
||||
}
|
||||
if (fesEnv) {
|
||||
configFiles.push(this.addAffix(f, fesEnv as string));
|
||||
}
|
||||
});
|
||||
|
||||
const configDir = winPath(join(this.cwd, 'config'));
|
||||
|
||||
const files = configFiles
|
||||
.reduce((memo, f) => {
|
||||
.reduce((memo: string[], f: string) => {
|
||||
const file = winPath(join(this.cwd, f));
|
||||
if (existsSync(file)) {
|
||||
memo = memo.concat(parseRequireDeps(file));
|
||||
} else {
|
||||
memo.push(file);
|
||||
}
|
||||
return memo;
|
||||
}, [])
|
||||
.filter((f) => !f.startsWith(configDir));
|
||||
.filter(f => !f.startsWith(configDir));
|
||||
|
||||
return [configDir].concat(files);
|
||||
}
|
||||
|
||||
watch(opts) {
|
||||
watch(opts: WatchOptions): () => void {
|
||||
let paths = this.getWatchFilesAndDirectories();
|
||||
let userConfig = opts.userConfig;
|
||||
const watcher = chokidar.watch(paths, {
|
||||
ignoreInitial: true,
|
||||
cwd: this.cwd,
|
||||
});
|
||||
watcher.on('all', (event, path) => {
|
||||
watcher.on('all', async (event, path) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.green(`[${event}] ${path}`));
|
||||
const newPaths = this.getWatchFilesAndDirectories();
|
||||
const diffs = lodash.difference(newPaths, paths);
|
||||
@ -207,13 +225,15 @@ export default class Config {
|
||||
paths = paths.concat(diffs);
|
||||
}
|
||||
|
||||
const newUserConfig = this.getUserConfig();
|
||||
const pluginChanged = [];
|
||||
const valueChanged = [];
|
||||
Object.keys(this.service.plugins).forEach((pluginId) => {
|
||||
const newUserConfig = await this.getUserConfig();
|
||||
const pluginChanged: Array<{ key: string; pluginId: string }> = [];
|
||||
const valueChanged: Array<{ key: string; pluginId: string }> = [];
|
||||
Object.keys(this.service.plugins).forEach((pluginId: string) => {
|
||||
const { key, config = {} } = this.service.plugins[pluginId];
|
||||
// recognize as key if have schema config
|
||||
if (!config.schema) return;
|
||||
if (!config.schema) {
|
||||
return;
|
||||
}
|
||||
if (!isEqual(newUserConfig[key], userConfig[key])) {
|
||||
const changed = {
|
||||
key,
|
||||
@ -221,7 +241,8 @@ export default class Config {
|
||||
};
|
||||
if (newUserConfig[key] === false || userConfig[key] === false) {
|
||||
pluginChanged.push(changed);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
valueChanged.push(changed);
|
||||
}
|
||||
}
|
28
packages/compiler/src/config/utils/configUtils.ts
Normal file
28
packages/compiler/src/config/utils/configUtils.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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);
|
||||
}
|
18
packages/compiler/src/config/utils/isEqual.ts
Normal file
18
packages/compiler/src/config/utils/isEqual.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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));
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
import { deepmerge, lodash } from '@fesjs/utils';
|
||||
|
||||
export default ({ defaultConfig, config }) => {
|
||||
interface MergeDefaultOptions {
|
||||
defaultConfig: any;
|
||||
config: any;
|
||||
}
|
||||
|
||||
export default function mergeDefault({ defaultConfig, config }: MergeDefaultOptions): any {
|
||||
if (lodash.isPlainObject(defaultConfig) && lodash.isPlainObject(config)) {
|
||||
return deepmerge(defaultConfig, config);
|
||||
}
|
||||
return typeof config !== 'undefined' ? config : defaultConfig;
|
||||
};
|
||||
}
|
12
packages/compiler/src/index.ts
Normal file
12
packages/compiler/src/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
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';
|
49
packages/compiler/src/service/enums.ts
Normal file
49
packages/compiler/src/service/enums.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 插件类型枚举
|
||||
*/
|
||||
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',
|
||||
}
|
39
packages/compiler/src/service/getPaths.ts
Normal file
39
packages/compiler/src/service/getPaths.ts
Normal file
@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
@ -1,77 +1,121 @@
|
||||
/**
|
||||
* @copy 该文件代码大部分出自 umi,有需要请参考:
|
||||
* https://github.com/umijs/umi/tree/master/packages/core
|
||||
*/
|
||||
import { join } from 'path';
|
||||
import { EventEmitter } from 'events';
|
||||
import assert from 'assert';
|
||||
import { existsSync } from 'fs';
|
||||
import { AsyncSeriesWaterfallHook } from 'tapable';
|
||||
import { lodash, chalk } from '@fesjs/utils';
|
||||
import type commander from 'commander';
|
||||
import type {
|
||||
ApplyPluginsOptions,
|
||||
CommandOption,
|
||||
ConfigInstance,
|
||||
Hook,
|
||||
Paths,
|
||||
Plugin,
|
||||
ResolvePluginsOptions,
|
||||
ResolvePresetsOptions,
|
||||
UserConfig,
|
||||
} from '../types';
|
||||
import assert from 'node:assert';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { chalk, lodash } from '@fesjs/utils';
|
||||
import { Command, Option } from 'commander';
|
||||
import { readJSONSync } from 'fs-extra/esm';
|
||||
import { AsyncSeriesWaterfallHook } from 'tapable';
|
||||
import Config from '../config';
|
||||
import { getUserConfigWithKey } from '../config/utils/configUtils';
|
||||
import { resolvePresets, pathToObj, resolvePlugins } from './utils/pluginUtils';
|
||||
import loadDotEnv from './utils/loadDotEnv';
|
||||
import isPromise from './utils/isPromise';
|
||||
import BabelRegister from './babelRegister';
|
||||
import PluginAPI from './pluginAPI';
|
||||
import { ApplyPluginsType, ConfigChangeType, EnableBy, PluginType, ServiceStage } from './enums';
|
||||
import getPaths from './getPaths';
|
||||
import PluginAPI from './pluginAPI';
|
||||
import isPromise from './utils/isPromise';
|
||||
import loadDotEnv from './utils/loadDotEnv';
|
||||
import { pathToObj, resolvePlugins, resolvePresets } from './utils/pluginUtils';
|
||||
|
||||
interface ServiceOptions {
|
||||
cwd?: string;
|
||||
pkg?: Record<string, any>;
|
||||
env?: string;
|
||||
fesPkg?: Record<string, any>;
|
||||
presets?: string[];
|
||||
plugins?: string[];
|
||||
}
|
||||
|
||||
interface SetupOptions {
|
||||
presets?: string[];
|
||||
plugins?: string[];
|
||||
}
|
||||
|
||||
interface ApplyAPIOptions {
|
||||
apply: () => Promise<any> | any;
|
||||
api: any;
|
||||
}
|
||||
|
||||
interface InitPresetResult {
|
||||
presets?: string[];
|
||||
plugins?: string[];
|
||||
}
|
||||
|
||||
interface ApplyPluginsOptionsExtended extends ApplyPluginsOptions {
|
||||
args?: any;
|
||||
}
|
||||
|
||||
interface RunOptions {
|
||||
rawArgv?: Record<string, any>;
|
||||
args?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface RunCommandOptions {
|
||||
rawArgv?: Record<string, any>;
|
||||
args?: Record<string, any>;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// 1. duplicated key
|
||||
export default class Service extends EventEmitter {
|
||||
cwd;
|
||||
cwd: string;
|
||||
|
||||
pkg;
|
||||
pkg: Record<string, any>;
|
||||
|
||||
skipPluginIds = new Set();
|
||||
skipPluginIds: Set<string> = new Set();
|
||||
|
||||
// lifecycle stage
|
||||
stage = ServiceStage.uninitialized;
|
||||
stage: ServiceStage = ServiceStage.uninitialized;
|
||||
|
||||
// registered commands
|
||||
commands = {};
|
||||
commands: Record<string, CommandOption> = {};
|
||||
|
||||
// including plugins
|
||||
plugins = {};
|
||||
plugins: Record<string, Plugin> = {};
|
||||
|
||||
// 构建
|
||||
builder = {};
|
||||
builder: Record<string, any> = {};
|
||||
|
||||
// plugin methods
|
||||
pluginMethods = {};
|
||||
pluginMethods: Record<string, () => void> = {};
|
||||
|
||||
// initial presets and plugins from arguments, config, process.env, and package.json
|
||||
initialPresets = [];
|
||||
initialPresets: Plugin[] = [];
|
||||
|
||||
// initial plugins from arguments, config, process.env, and package.json
|
||||
initialPlugins = [];
|
||||
initialPlugins: Plugin[] = [];
|
||||
|
||||
_extraPresets = [];
|
||||
_extraPresets: Plugin[] = [];
|
||||
|
||||
_extraPlugins = [];
|
||||
_extraPlugins: Plugin[] = [];
|
||||
|
||||
// user config
|
||||
userConfig;
|
||||
userConfig: UserConfig;
|
||||
|
||||
configInstance;
|
||||
configInstance: ConfigInstance;
|
||||
|
||||
config = null;
|
||||
|
||||
// babel register
|
||||
babelRegister;
|
||||
config: UserConfig | null = null;
|
||||
|
||||
// hooks
|
||||
hooksByPluginId = {};
|
||||
hooksByPluginId: Record<string, Hook[]> = {};
|
||||
|
||||
hooks = {};
|
||||
hooks: Record<string, Hook[]> = {};
|
||||
|
||||
// paths
|
||||
paths = {};
|
||||
paths: Paths;
|
||||
|
||||
env;
|
||||
env: string;
|
||||
|
||||
ApplyPluginsType = ApplyPluginsType;
|
||||
|
||||
@ -81,31 +125,35 @@ export default class Service extends EventEmitter {
|
||||
|
||||
ServiceStage = ServiceStage;
|
||||
|
||||
args;
|
||||
args: Record<string, any> | undefined;
|
||||
|
||||
constructor(opts) {
|
||||
fesPkg: Record<string, any>;
|
||||
|
||||
program: commander.Command;
|
||||
|
||||
ready: Promise<boolean>;
|
||||
|
||||
constructor(opts: ServiceOptions) {
|
||||
super();
|
||||
this.cwd = opts.cwd || process.cwd();
|
||||
// repoDir should be the root dir of repo
|
||||
this.pkg = opts.pkg || this.resolvePackage();
|
||||
this.env = opts.env || process.env.NODE_ENV;
|
||||
this.env = opts.env || process.env.NODE_ENV || 'development';
|
||||
this.fesPkg = opts.fesPkg || {};
|
||||
this.userConfig = {};
|
||||
|
||||
assert(existsSync(this.cwd), `cwd ${this.cwd} does not exist.`);
|
||||
|
||||
// register babel before config parsing
|
||||
this.babelRegister = new BabelRegister();
|
||||
this.program = this.initCommand();
|
||||
|
||||
// load .env or .local.env
|
||||
this.loadEnv();
|
||||
|
||||
// get user config without validation
|
||||
this.configInstance = new Config({
|
||||
cwd: this.cwd,
|
||||
service: this,
|
||||
localConfig: this.env === 'development',
|
||||
});
|
||||
this.userConfig = this.configInstance.getUserConfig();
|
||||
|
||||
// get paths
|
||||
this.paths = getPaths({
|
||||
@ -114,41 +162,47 @@ export default class Service extends EventEmitter {
|
||||
env: this.env,
|
||||
});
|
||||
|
||||
this.program = this.initCommand();
|
||||
|
||||
// setup initial plugins
|
||||
const baseOpts = {
|
||||
pkg: this.pkg,
|
||||
cwd: this.cwd,
|
||||
};
|
||||
this.initialPresets = resolvePresets({
|
||||
...baseOpts,
|
||||
presets: opts.presets || [],
|
||||
userConfigPresets: this.userConfig.presets || [],
|
||||
builder: this.userConfig.builder,
|
||||
});
|
||||
this.initialPlugins = resolvePlugins({
|
||||
...baseOpts,
|
||||
plugins: opts.plugins || [],
|
||||
userConfigPlugins: this.userConfig.plugins || [],
|
||||
builder: this.userConfig.builder,
|
||||
});
|
||||
this.ready = this.setup(opts);
|
||||
}
|
||||
|
||||
setStage(stage) {
|
||||
async setup(opts: SetupOptions): Promise<boolean> {
|
||||
// get user config without validation
|
||||
|
||||
this.userConfig = await this.configInstance.getUserConfig();
|
||||
|
||||
// setup initial plugins
|
||||
const baseOpts: ResolvePresetsOptions & ResolvePluginsOptions = {
|
||||
pkg: this.pkg,
|
||||
cwd: this.cwd,
|
||||
builder: this.userConfig.builder as string,
|
||||
};
|
||||
this.initialPresets = await resolvePresets({
|
||||
...baseOpts,
|
||||
presets: opts.presets || [],
|
||||
userConfigPresets: (this.userConfig.presets as string[]) || [],
|
||||
});
|
||||
this.initialPlugins = await resolvePlugins({
|
||||
...baseOpts,
|
||||
plugins: opts.plugins || [],
|
||||
userConfigPlugins: (this.userConfig.plugins as string[]) || [],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
setStage(stage: ServiceStage): void {
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
resolvePackage() {
|
||||
resolvePackage(): Record<string, any> {
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
return require(join(this.cwd, "package.json"));
|
||||
} catch (e) {
|
||||
return readJSONSync(join(this.cwd, 'package.json'));
|
||||
}
|
||||
catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
loadEnv() {
|
||||
loadEnv(): void {
|
||||
const basePath = join(this.cwd, '.env');
|
||||
const localPath = `${basePath}.local`;
|
||||
loadDotEnv(basePath);
|
||||
@ -158,16 +212,16 @@ export default class Service extends EventEmitter {
|
||||
loadDotEnv(localPath);
|
||||
}
|
||||
|
||||
async init() {
|
||||
async init(): Promise<void> {
|
||||
this.setStage(ServiceStage.init);
|
||||
await this.initPresetsAndPlugins();
|
||||
|
||||
// hooksByPluginId -> hooks
|
||||
// hooks is mapped with hook key, prepared for applyPlugins()
|
||||
this.setStage(ServiceStage.initHooks);
|
||||
Object.keys(this.hooksByPluginId).forEach((id) => {
|
||||
Object.keys(this.hooksByPluginId).forEach((id: string) => {
|
||||
const hooks = this.hooksByPluginId[id];
|
||||
hooks.forEach((hook) => {
|
||||
hooks.forEach((hook: Hook) => {
|
||||
const { key } = hook;
|
||||
hook.pluginId = id;
|
||||
this.hooks[key] = (this.hooks[key] || []).concat(hook);
|
||||
@ -190,51 +244,47 @@ export default class Service extends EventEmitter {
|
||||
// merge paths to keep the this.paths ref
|
||||
this.setStage(ServiceStage.getPaths);
|
||||
// config.outputPath may be modified by plugins
|
||||
if (this.config.outputPath) {
|
||||
this.paths.absOutputPath = join(this.cwd, this.config.outputPath);
|
||||
if (this.config?.outputPath) {
|
||||
this.paths.absOutputPath = join(this.cwd, this.config.outputPath as string);
|
||||
}
|
||||
const paths = await this.applyPlugins({
|
||||
key: 'modifyPaths',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: this.paths,
|
||||
});
|
||||
Object.keys(paths).forEach((key) => {
|
||||
this.paths[key] = paths[key];
|
||||
});
|
||||
}) as Paths;
|
||||
Object.assign(this.paths, paths);
|
||||
}
|
||||
|
||||
async setConfig() {
|
||||
async setConfig(): Promise<void> {
|
||||
const defaultConfig = await this.applyPlugins({
|
||||
key: 'modifyDefaultConfig',
|
||||
type: this.ApplyPluginsType.modify,
|
||||
initialValue: await this.configInstance.getDefaultConfig(),
|
||||
});
|
||||
}) as Record<string, any>;
|
||||
const initConfig = await this.configInstance.getConfig(defaultConfig);
|
||||
this.config = await this.applyPlugins({
|
||||
key: 'modifyConfig',
|
||||
type: this.ApplyPluginsType.modify,
|
||||
initialValue: this.configInstance.getConfig({
|
||||
defaultConfig,
|
||||
}),
|
||||
initialValue: initConfig,
|
||||
});
|
||||
}
|
||||
|
||||
async initPresetsAndPlugins() {
|
||||
async initPresetsAndPlugins(): Promise<void> {
|
||||
this.setStage(ServiceStage.initPresets);
|
||||
this._extraPlugins = [];
|
||||
while (this.initialPresets.length) {
|
||||
// eslint-disable-next-line
|
||||
await this.initPreset(this.initialPresets.shift());
|
||||
await this.initPreset(this.initialPresets.shift()!);
|
||||
}
|
||||
|
||||
this.setStage(ServiceStage.initPlugins);
|
||||
this._extraPlugins.push(...this.initialPlugins);
|
||||
while (this._extraPlugins.length) {
|
||||
// eslint-disable-next-line
|
||||
await this.initPlugin(this._extraPlugins.shift());
|
||||
const plugin = this._extraPlugins.shift();
|
||||
await this.initPlugin(plugin!);
|
||||
}
|
||||
}
|
||||
|
||||
getPluginAPI(opts) {
|
||||
getPluginAPI(opts: { id: string; key: string; service: Service }): PluginAPI {
|
||||
const pluginAPI = new PluginAPI(opts);
|
||||
|
||||
// register built-in methods
|
||||
@ -246,17 +296,18 @@ export default class Service extends EventEmitter {
|
||||
});
|
||||
|
||||
return new Proxy(pluginAPI, {
|
||||
get: (target, prop) => {
|
||||
get: (target, prop: string) => {
|
||||
// 由于 pluginMethods 需要在 register 阶段可用
|
||||
// 必须通过 proxy 的方式动态获取最新,以实现边注册边使用的效果
|
||||
if (this.pluginMethods[prop]) return this.pluginMethods[prop];
|
||||
if (this.pluginMethods[prop]) {
|
||||
return this.pluginMethods[prop];
|
||||
}
|
||||
if (
|
||||
[
|
||||
'applyPlugins',
|
||||
'ApplyPluginsType',
|
||||
'EnableBy',
|
||||
'ConfigChangeType',
|
||||
'babelRegister',
|
||||
'stage',
|
||||
'ServiceStage',
|
||||
'paths',
|
||||
@ -273,22 +324,26 @@ export default class Service extends EventEmitter {
|
||||
'builder',
|
||||
].includes(prop)
|
||||
) {
|
||||
// @ts-expect-error ignore property
|
||||
return typeof this[prop] === 'function' ? this[prop].bind(this) : this[prop];
|
||||
}
|
||||
// @ts-expect-error ignore property
|
||||
return target[prop];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async applyAPI(opts) {
|
||||
let ret = opts.apply()(opts.api);
|
||||
async applyAPI(opts: ApplyAPIOptions): Promise<any> {
|
||||
const module = await opts.apply();
|
||||
let ret = module(opts.api);
|
||||
|
||||
if (isPromise(ret)) {
|
||||
ret = await ret;
|
||||
}
|
||||
return ret || {};
|
||||
}
|
||||
|
||||
async initPreset(preset) {
|
||||
async initPreset(preset: Plugin): Promise<void> {
|
||||
const { id, key, apply } = preset;
|
||||
preset.isPreset = true;
|
||||
|
||||
@ -299,22 +354,23 @@ export default class Service extends EventEmitter {
|
||||
const { presets, plugins } = await this.applyAPI({
|
||||
api,
|
||||
apply,
|
||||
});
|
||||
}) as InitPresetResult;
|
||||
|
||||
// register extra presets and plugins
|
||||
if (presets) {
|
||||
assert(Array.isArray(presets), `presets returned from preset ${id} must be Array.`);
|
||||
// 插到最前面,下个 while 循环优先执行
|
||||
this._extraPresets.splice(
|
||||
0,
|
||||
0,
|
||||
...presets.map((path) =>
|
||||
const _presets = await Promise.all(presets.map(path =>
|
||||
pathToObj({
|
||||
type: PluginType.preset,
|
||||
path,
|
||||
cwd: this.cwd,
|
||||
}),
|
||||
),
|
||||
));
|
||||
this._extraPresets.splice(
|
||||
0,
|
||||
0,
|
||||
..._presets,
|
||||
);
|
||||
}
|
||||
|
||||
@ -322,25 +378,25 @@ export default class Service extends EventEmitter {
|
||||
const extraPresets = lodash.clone(this._extraPresets);
|
||||
this._extraPresets = [];
|
||||
while (extraPresets.length) {
|
||||
// eslint-disable-next-line
|
||||
await this.initPreset(extraPresets.shift());
|
||||
await this.initPreset(extraPresets.shift()!);
|
||||
}
|
||||
|
||||
if (plugins) {
|
||||
assert(Array.isArray(plugins), `plugins returned from preset ${id} must be Array.`);
|
||||
this._extraPlugins.push(
|
||||
...plugins.map((path) =>
|
||||
const _plugins = await Promise.all(plugins.map(path =>
|
||||
pathToObj({
|
||||
type: PluginType.plugin,
|
||||
path,
|
||||
cwd: this.cwd,
|
||||
}),
|
||||
),
|
||||
));
|
||||
this._extraPlugins.push(
|
||||
..._plugins,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async initPlugin(plugin) {
|
||||
async initPlugin(plugin: Plugin): Promise<void> {
|
||||
const { id, key, apply } = plugin;
|
||||
|
||||
const api = this.getPluginAPI({
|
||||
@ -357,25 +413,34 @@ export default class Service extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
getPluginOptsWithKey(key) {
|
||||
getPluginOptsWithKey(key: string): any {
|
||||
return getUserConfigWithKey({
|
||||
key,
|
||||
userConfig: this.userConfig,
|
||||
});
|
||||
}
|
||||
|
||||
registerPlugin(plugin) {
|
||||
registerPlugin(plugin: Plugin): void {
|
||||
this.plugins[plugin.id] = plugin;
|
||||
}
|
||||
|
||||
isPluginEnable(pluginId) {
|
||||
isPluginEnable(pluginId: string): boolean {
|
||||
// api.skipPlugins() 的插件
|
||||
if (this.skipPluginIds.has(pluginId)) return false;
|
||||
if (this.skipPluginIds.has(pluginId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { key, enableBy } = this.plugins[pluginId];
|
||||
const plugin = this.plugins[pluginId];
|
||||
if (!plugin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { key, enableBy } = plugin;
|
||||
|
||||
// 手动设置为 false
|
||||
if (this.userConfig[key] === false) return false;
|
||||
if (this.userConfig[key] === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 配置开启
|
||||
if (enableBy === this.EnableBy.config && !(key in this.userConfig)) {
|
||||
@ -391,77 +456,74 @@ export default class Service extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
hasPresets(presetIds) {
|
||||
hasPresets(presetIds: string[]): boolean {
|
||||
return presetIds.every((presetId) => {
|
||||
const preset = this.plugins[presetId];
|
||||
return preset && preset.isPreset && this.isPluginEnable(presetId);
|
||||
});
|
||||
}
|
||||
|
||||
hasPlugins(pluginIds) {
|
||||
hasPlugins(pluginIds: string[]): boolean {
|
||||
return pluginIds.every((pluginId) => {
|
||||
const plugin = this.plugins[pluginId];
|
||||
return plugin && !plugin.isPreset && this.isPluginEnable(pluginId);
|
||||
});
|
||||
}
|
||||
|
||||
async applyPlugins(opts) {
|
||||
async applyPlugins<T>(opts: ApplyPluginsOptionsExtended): Promise<T> {
|
||||
const hooks = this.hooks[opts.key] || [];
|
||||
switch (opts.type) {
|
||||
case ApplyPluginsType.add:
|
||||
if ('initialValue' in opts) {
|
||||
assert(Array.isArray(opts.initialValue), 'applyPlugins failed, opts.initialValue must be Array if opts.type is add.');
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
const tAdd = new AsyncSeriesWaterfallHook(["memo"]);
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const tAdd = new AsyncSeriesWaterfallHook(['memo']);
|
||||
for (const hook of hooks) {
|
||||
if (!this.isPluginEnable(hook.pluginId)) {
|
||||
if (!this.isPluginEnable(hook.pluginId!)) {
|
||||
continue;
|
||||
}
|
||||
tAdd.tapPromise(
|
||||
{
|
||||
name: hook.pluginId,
|
||||
name: hook.pluginId!,
|
||||
stage: hook.stage || 0,
|
||||
// @ts-ignore
|
||||
before: hook.before,
|
||||
},
|
||||
async (memo) => {
|
||||
const items = await hook.fn(opts.args);
|
||||
return memo.concat(items);
|
||||
return (memo as any[]).concat(items);
|
||||
},
|
||||
);
|
||||
}
|
||||
return tAdd.promise(opts.initialValue || []);
|
||||
return tAdd.promise(opts.initialValue || []) as Promise<T>;
|
||||
case ApplyPluginsType.modify:
|
||||
// eslint-disable-next-line
|
||||
const tModify = new AsyncSeriesWaterfallHook(["memo"]);
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const tModify = new AsyncSeriesWaterfallHook(['memo']);
|
||||
for (const hook of hooks) {
|
||||
if (!this.isPluginEnable(hook.pluginId)) {
|
||||
if (!this.isPluginEnable(hook.pluginId!)) {
|
||||
continue;
|
||||
}
|
||||
tModify.tapPromise(
|
||||
{
|
||||
name: hook.pluginId,
|
||||
name: hook.pluginId!,
|
||||
stage: hook.stage || 0,
|
||||
// @ts-ignore
|
||||
before: hook.before,
|
||||
},
|
||||
async (memo) => hook.fn(memo, opts.args),
|
||||
async (memo: any) => hook.fn(memo, opts.args),
|
||||
);
|
||||
}
|
||||
return tModify.promise(opts.initialValue);
|
||||
return tModify.promise(opts.initialValue) as Promise<T>;
|
||||
case ApplyPluginsType.event:
|
||||
// eslint-disable-next-line
|
||||
const tEvent = new AsyncSeriesWaterfallHook(["_"]);
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const tEvent = new AsyncSeriesWaterfallHook(['_']);
|
||||
for (const hook of hooks) {
|
||||
if (!this.isPluginEnable(hook.pluginId)) {
|
||||
if (!this.isPluginEnable(hook.pluginId!)) {
|
||||
continue;
|
||||
}
|
||||
tEvent.tapPromise(
|
||||
{
|
||||
name: hook.pluginId,
|
||||
name: hook.pluginId!,
|
||||
stage: hook.stage || 0,
|
||||
// @ts-ignore
|
||||
before: hook.before,
|
||||
},
|
||||
async () => {
|
||||
@ -469,22 +531,22 @@ export default class Service extends EventEmitter {
|
||||
},
|
||||
);
|
||||
}
|
||||
return tEvent.promise();
|
||||
return tEvent.promise(true) as Promise<T>;
|
||||
default:
|
||||
throw new Error(`applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`);
|
||||
throw new Error(`applyPlugin failed, type is not defined or is not matched, got ${(opts as any).type}.`);
|
||||
}
|
||||
}
|
||||
|
||||
initCommand() {
|
||||
initCommand(): commander.Command {
|
||||
const command = new Command();
|
||||
command
|
||||
.usage('<command> [options]')
|
||||
.version(`@fesjs/fes ${this.fesPkg.version}`, '-v, --vers', 'output the current version')
|
||||
.version(`@fesjs/fes ${this.fesPkg.version || ''}`, '-v, --vers', 'output the current version')
|
||||
.description(chalk.cyan('一个好用的前端应用解决方案'));
|
||||
return command;
|
||||
}
|
||||
|
||||
async run({ rawArgv = {}, args = {} }) {
|
||||
async run({ rawArgv = {}, args = {} }: RunOptions): Promise<any> {
|
||||
await this.init();
|
||||
|
||||
this.setStage(ServiceStage.run);
|
||||
@ -499,9 +561,9 @@ export default class Service extends EventEmitter {
|
||||
return this.runCommand({ rawArgv, args });
|
||||
}
|
||||
|
||||
async runCommand({ rawArgv = {}, args = {} }) {
|
||||
async runCommand({ rawArgv = {}, args = {} }: RunCommandOptions): Promise<any> {
|
||||
assert(this.stage >= ServiceStage.init, 'service is not initialized.');
|
||||
Object.keys(this.commands).forEach((command) => {
|
||||
Object.keys(this.commands).forEach((command: string) => {
|
||||
const commandOptionConfig = this.commands[command];
|
||||
const program = this.program;
|
||||
let c = program.command(command).description(commandOptionConfig.description);
|
||||
@ -519,7 +581,7 @@ export default class Service extends EventEmitter {
|
||||
}
|
||||
if (commandOptionConfig.fn) {
|
||||
c.action(async () => {
|
||||
await commandOptionConfig.fn({
|
||||
await commandOptionConfig.fn!({
|
||||
rawArgv,
|
||||
args,
|
||||
options: c.opts(),
|
||||
@ -532,13 +594,19 @@ export default class Service extends EventEmitter {
|
||||
return this.parseCommand();
|
||||
}
|
||||
|
||||
async parseCommand() {
|
||||
async parseCommand(): Promise<any> {
|
||||
this.program.on('--help', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` Run ${chalk.cyan('fes <command> --help')} for detailed usage of given command.`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log();
|
||||
});
|
||||
this.program.commands.forEach((c) => c.on('--help', () => console.log()));
|
||||
this.program.commands.forEach(c => c.on('--help', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log();
|
||||
}));
|
||||
return this.program.parseAsync(process.argv);
|
||||
}
|
||||
}
|
@ -1,17 +1,36 @@
|
||||
/**
|
||||
* @copy 该文件代码大部分出自 umi,有需要请参考:
|
||||
* https://github.com/umijs/umi/tree/master/packages/core
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import type {
|
||||
CommandOption,
|
||||
Hook,
|
||||
Plugin,
|
||||
PluginConfig,
|
||||
ServiceInstance,
|
||||
} from '../types';
|
||||
import assert from 'node:assert';
|
||||
import * as utils from '@fesjs/utils';
|
||||
import { isValidPlugin, pathToObj } from './utils/pluginUtils';
|
||||
import { EnableBy, PluginType, ServiceStage } from './enums';
|
||||
import { isValidPlugin, pathToObj } from './utils/pluginUtils';
|
||||
|
||||
interface PluginAPIOptions {
|
||||
id: string;
|
||||
key: string;
|
||||
service: ServiceInstance;
|
||||
}
|
||||
|
||||
interface DescribeOptions {
|
||||
id?: string;
|
||||
key?: string;
|
||||
config?: PluginConfig;
|
||||
enableBy?: EnableBy | (() => boolean);
|
||||
}
|
||||
|
||||
// TODO
|
||||
// 标准化 logger
|
||||
export default class PluginAPI {
|
||||
constructor(opts) {
|
||||
id: string;
|
||||
key: string;
|
||||
service: ServiceInstance;
|
||||
utils: typeof utils;
|
||||
logger: typeof utils.logger;
|
||||
|
||||
constructor(opts: PluginAPIOptions) {
|
||||
this.id = opts.id;
|
||||
this.key = opts.key;
|
||||
this.service = opts.service;
|
||||
@ -20,7 +39,7 @@ export default class PluginAPI {
|
||||
}
|
||||
|
||||
// TODO: reversed keys
|
||||
describe({ id, key, config, enableBy } = {}) {
|
||||
describe({ id, key, config, enableBy }: DescribeOptions = {}): void {
|
||||
const { plugins } = this.service;
|
||||
// this.id and this.key is generated automatically
|
||||
// so we need to diff first
|
||||
@ -46,13 +65,13 @@ export default class PluginAPI {
|
||||
plugins[this.id].enableBy = enableBy || EnableBy.register;
|
||||
}
|
||||
|
||||
register(hook) {
|
||||
register(hook: Hook): void {
|
||||
assert(hook.key && typeof hook.key === 'string', `api.register() failed, hook.key must supplied and should be string, but got ${hook.key}.`);
|
||||
assert(hook.fn && typeof hook.fn === 'function', `api.register() failed, hook.fn must supplied and should be function, but got ${hook.fn}.`);
|
||||
this.service.hooksByPluginId[this.id] = (this.service.hooksByPluginId[this.id] || []).concat(hook);
|
||||
}
|
||||
|
||||
registerCommand(commandOption) {
|
||||
registerCommand(commandOption: CommandOption): void {
|
||||
const { command, fn } = commandOption;
|
||||
assert(!this.service.commands[command], `api.registerCommand() failed, the command ${command} is exists.`);
|
||||
assert(typeof command === 'string', 'api.registerCommand() failed, the command must be string.');
|
||||
@ -61,67 +80,69 @@ export default class PluginAPI {
|
||||
}
|
||||
|
||||
// 在 preset 初始化阶段放后面,在插件注册阶段放前面
|
||||
registerPlugins(plugins) {
|
||||
async registerPlugins(plugins: (string | Plugin)[]): Promise<void> {
|
||||
assert(
|
||||
this.service.stage === ServiceStage.initPresets || this.service.stage === ServiceStage.initPlugins,
|
||||
'api.registerPlugins() failed, it should only be used in registering stage.',
|
||||
);
|
||||
assert(Array.isArray(plugins), 'api.registerPlugins() failed, plugins must be Array.');
|
||||
const extraPlugins = plugins.map((plugin) =>
|
||||
const extraPlugins = await Promise.all(plugins.map(plugin =>
|
||||
isValidPlugin(plugin)
|
||||
? plugin
|
||||
: pathToObj({
|
||||
type: PluginType.plugin,
|
||||
path: plugin,
|
||||
path: plugin as string,
|
||||
cwd: this.service.cwd,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
if (this.service.stage === ServiceStage.initPresets) {
|
||||
this.service._extraPlugins.push(...extraPlugins);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.service._extraPlugins.splice(0, 0, ...extraPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
registerPresets(presets) {
|
||||
async registerPresets(presets: (string | Plugin)[]): Promise<void> {
|
||||
assert(this.service.stage === ServiceStage.initPresets, 'api.registerPresets() failed, it should only used in presets.');
|
||||
assert(Array.isArray(presets), 'api.registerPresets() failed, presets must be Array.');
|
||||
const extraPresets = presets.map((preset) =>
|
||||
const extraPresets = await Promise.all(presets.map(preset =>
|
||||
isValidPlugin(preset)
|
||||
? preset
|
||||
? preset as Plugin
|
||||
: pathToObj({
|
||||
type: PluginType.preset,
|
||||
path: preset,
|
||||
path: preset as string,
|
||||
cwd: this.service.cwd,
|
||||
}),
|
||||
);
|
||||
));
|
||||
// 插到最前面,下个 while 循环优先执行
|
||||
this.service._extraPresets.splice(0, 0, ...extraPresets);
|
||||
}
|
||||
|
||||
registerMethod({ name, fn, exitsError = true }) {
|
||||
registerMethod({ name, fn, exitsError = true }: { name: string; fn?: (...args: any[]) => any; exitsError: boolean }): void {
|
||||
if (this.service.pluginMethods[name]) {
|
||||
if (exitsError) {
|
||||
throw new Error(`api.registerMethod() failed, method ${name} is already exist.`);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.service.pluginMethods[name] =
|
||||
fn ||
|
||||
this.service.pluginMethods[name]
|
||||
= fn
|
||||
// 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI
|
||||
// 否则 pluginId 会不会,导致不能正确 skip plugin
|
||||
function (hookFn) {
|
||||
const hook = {
|
||||
|| function (hookFn: any) {
|
||||
const hook: Partial<Hook> = {
|
||||
key: name,
|
||||
...(utils.lodash.isPlainObject(hookFn) ? hookFn : { fn: hookFn }),
|
||||
};
|
||||
// @ts-ignore
|
||||
this.register(hook);
|
||||
// @ts-expect-error this
|
||||
this.register(hook as Hook);
|
||||
};
|
||||
}
|
||||
|
||||
registerBuilder(builder) {
|
||||
registerBuilder(builder: Record<string, any>): void {
|
||||
assert(typeof builder === 'object', 'api.registerBuilder() failed, the builder must be object.');
|
||||
// const { name } = builder;
|
||||
// assert(typeof name === 'string', 'api.registerBuilder() failed, the builder.name must be string.');
|
||||
@ -129,7 +150,7 @@ export default class PluginAPI {
|
||||
this.service.builder = builder;
|
||||
}
|
||||
|
||||
skipPlugins(pluginIds) {
|
||||
skipPlugins(pluginIds: string[]): void {
|
||||
pluginIds.forEach((pluginId) => {
|
||||
this.service.skipPluginIds.add(pluginId);
|
||||
});
|
@ -1,4 +1,6 @@
|
||||
export default (api) => {
|
||||
import type { PluginAPIInstance } from '../../types';
|
||||
|
||||
export default function builderPlugin(api: PluginAPIInstance): void {
|
||||
api.describe({
|
||||
key: 'builder',
|
||||
config: {
|
||||
@ -8,4 +10,4 @@ export default (api) => {
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
12
packages/compiler/src/service/utils/isPromise.ts
Normal file
12
packages/compiler/src/service/utils/isPromise.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 判断对象是否为 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'
|
||||
);
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import process from 'node:process';
|
||||
import { parse } from 'dotenv';
|
||||
|
||||
/**
|
||||
* dotenv wrapper
|
||||
* @param envPath string
|
||||
* @param envPath 环境变量文件路径
|
||||
*/
|
||||
export default function loadDotEnv(envPath) {
|
||||
export default function loadDotEnv(envPath: string): void {
|
||||
if (existsSync(envPath)) {
|
||||
const parsed = parse(readFileSync(envPath, 'utf-8')) || {};
|
||||
Object.keys(parsed).forEach((key) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
process.env[key] = parsed[key];
|
||||
});
|
||||
}
|
217
packages/compiler/src/service/utils/pluginUtils.ts
Normal file
217
packages/compiler/src/service/utils/pluginUtils.ts
Normal file
@ -0,0 +1,217 @@
|
||||
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';
|
||||
}
|
7
packages/compiler/src/shared.ts
Normal file
7
packages/compiler/src/shared.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* 获取编译器所有者的目录路径
|
||||
*/
|
||||
export const OWNER_DIR: string = join(dirname(fileURLToPath(import.meta.url)), '..');
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user