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