akfun 5.2.12 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/module/index.js +0 -8
- package/package.json +36 -29
- package/src/loaders/README.md +1 -0
- package/src/loaders/component-scope-style-loader.js +429 -0
- package/src/plugin/babel-plugin-add-data-scope.js +399 -99
- package/src/rollup/rollup.config.js +1 -1
- package/src/utils/loadAsset.ts +88 -0
- package/src/utils/loadAsset.js +0 -47
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AKFun 前端脚手架
|
|
2
2
|
|
|
3
|
-
AKFun 是一个基于 Webpack 与 Rollup 的多场景前端打包工具,支持 Vue、React、React+TS 技术栈,致力于提供"零配置、开箱即用"
|
|
3
|
+
AKFun 是一个基于 Webpack 与 Rollup 的多场景前端打包工具,支持 Vue、React、React+TS 技术栈,致力于提供"零配置、开箱即用"的前端工程能力,让开发者专注于业务开发。
|
|
4
4
|
|
|
5
5
|
## 目录
|
|
6
6
|
|
|
@@ -20,10 +20,10 @@ AKFun 是一个基于 Webpack 与 Rollup 的多场景前端打包工具,支持
|
|
|
20
20
|
|
|
21
21
|
## 主要特性
|
|
22
22
|
|
|
23
|
-
- **零配置**:
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- **灵活可配**:
|
|
23
|
+
- **零配置**: 内置默认配置(前端工程最佳实践),开箱可用
|
|
24
|
+
- **多技术体系支持**: 支持 Vue、React、React+TS 的调试与构建
|
|
25
|
+
- **多种构建类型**: 本地开发(含热更新/代理)、生产构建、库构建(UMD/ESM)、Node 模块构建
|
|
26
|
+
- **灵活可配**: 支持入口、别名、代理、公共样式注入、ESLint/StyleLint、Babel/Loader/Plugin 扩展等配置
|
|
27
27
|
- **样式与规范**: 集成 Autoprefixer、Sass、PostCSS、ESLint、StyleLint
|
|
28
28
|
- **参数替换**: 支持基于 [params-replace-loader](https://www.npmjs.com/package/params-replace-loader) 的环境变量批量替换
|
|
29
29
|
- **模板支持**: 提供完整的 Vue/React 项目模板
|
package/module/index.js
CHANGED
|
@@ -33,7 +33,6 @@ let argv = yargs
|
|
|
33
33
|
'创建一个项目',
|
|
34
34
|
(yargs) => {
|
|
35
35
|
yargs
|
|
36
|
-
.reset()
|
|
37
36
|
.usage(titleTip('Usage') + ': $0 init [options]')
|
|
38
37
|
.option('type', {
|
|
39
38
|
alias: 't',
|
|
@@ -106,7 +105,6 @@ let argv = yargs
|
|
|
106
105
|
'初始化AKFun配置文件',
|
|
107
106
|
(yargs) => {
|
|
108
107
|
yargs
|
|
109
|
-
.reset()
|
|
110
108
|
.usage(titleTip('Usage') + ': $0 config init')
|
|
111
109
|
.alias('h', 'help');
|
|
112
110
|
},
|
|
@@ -119,7 +117,6 @@ let argv = yargs
|
|
|
119
117
|
'开启本地调试模式',
|
|
120
118
|
(yargs) => {
|
|
121
119
|
yargs
|
|
122
|
-
.reset()
|
|
123
120
|
.usage(titleTip('Usage') + ': $0 dev')
|
|
124
121
|
.alias('h', 'help');
|
|
125
122
|
},
|
|
@@ -132,7 +129,6 @@ let argv = yargs
|
|
|
132
129
|
'构建生产环境代码',
|
|
133
130
|
(yargs) => {
|
|
134
131
|
yargs
|
|
135
|
-
.reset()
|
|
136
132
|
.usage(titleTip('Usage') + ': $0 build')
|
|
137
133
|
.alias('h', 'help');
|
|
138
134
|
},
|
|
@@ -145,7 +141,6 @@ let argv = yargs
|
|
|
145
141
|
'构建 Library 库(UMD 模块)',
|
|
146
142
|
(yargs) => {
|
|
147
143
|
yargs
|
|
148
|
-
.reset()
|
|
149
144
|
.usage(titleTip('Usage') + ': $0 build2lib')
|
|
150
145
|
.alias('h', 'help');
|
|
151
146
|
},
|
|
@@ -158,7 +153,6 @@ let argv = yargs
|
|
|
158
153
|
'构建 ESM 模块',
|
|
159
154
|
(yargs) => {
|
|
160
155
|
yargs
|
|
161
|
-
.reset()
|
|
162
156
|
.usage(titleTip('Usage') + ': $0 build2esm')
|
|
163
157
|
.alias('h', 'help');
|
|
164
158
|
},
|
|
@@ -171,7 +165,6 @@ let argv = yargs
|
|
|
171
165
|
'构建 Node 模块',
|
|
172
166
|
(yargs) => {
|
|
173
167
|
yargs
|
|
174
|
-
.reset()
|
|
175
168
|
.usage(titleTip('Usage') + ': $0 build2node')
|
|
176
169
|
.alias('h', 'help');
|
|
177
170
|
},
|
|
@@ -184,7 +177,6 @@ let argv = yargs
|
|
|
184
177
|
'输出当前配置文件',
|
|
185
178
|
(yargs) => {
|
|
186
179
|
yargs
|
|
187
|
-
.reset()
|
|
188
180
|
.usage(titleTip('Usage') + ': $0 inspect')
|
|
189
181
|
.option('type', {
|
|
190
182
|
alias: 't',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akfun",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "前端脚手架:支持Vue技术栈和react技术栈",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"前端工程",
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@babel/cli": "^7.17.0",
|
|
53
53
|
"@babel/core": "^7.17.0",
|
|
54
|
-
"@babel/polyfill": "^7.10.1",
|
|
55
54
|
"@babel/eslint-parser": "^7.17.0",
|
|
56
55
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
|
57
56
|
"@babel/plugin-proposal-decorators": "^7.17.0",
|
|
@@ -59,41 +58,44 @@
|
|
|
59
58
|
"@babel/plugin-proposal-function-sent": "^7.16.7",
|
|
60
59
|
"@babel/plugin-proposal-json-strings": "^7.16.7",
|
|
61
60
|
"@babel/plugin-proposal-numeric-separator": "^7.16.7",
|
|
62
|
-
"@babel/plugin-transform-nullish-coalescing-operator": "^7.23.0",
|
|
63
|
-
"@babel/plugin-transform-optional-chaining": "^7.23.0",
|
|
64
61
|
"@babel/plugin-proposal-throw-expressions": "^7.16.7",
|
|
65
62
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
|
66
63
|
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
|
64
|
+
"@babel/plugin-transform-nullish-coalescing-operator": "^7.23.0",
|
|
65
|
+
"@babel/plugin-transform-optional-chaining": "^7.23.0",
|
|
67
66
|
"@babel/plugin-transform-runtime": "^7.27.1",
|
|
68
|
-
"@babel/
|
|
67
|
+
"@babel/polyfill": "^7.10.1",
|
|
69
68
|
"@babel/preset-env": "^7.16.11",
|
|
70
69
|
"@babel/preset-react": "^7.16.7",
|
|
71
70
|
"@babel/preset-typescript": "^7.16.7",
|
|
72
71
|
"@babel/register": "^7.17.0",
|
|
72
|
+
"@babel/runtime": "^7.27.1",
|
|
73
|
+
"@baiducloud/sdk": "^1.0.7",
|
|
73
74
|
"@mapbox/stylelint-processor-arbitrary-tags": "^0.4.0",
|
|
74
75
|
"@rollup/plugin-alias": "^5.1.1",
|
|
75
76
|
"@rollup/plugin-babel": "^6.1.0",
|
|
76
77
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
77
78
|
"@rollup/plugin-image": "^3.0.3",
|
|
78
|
-
"@svgr/rollup": "^8.1.0",
|
|
79
79
|
"@rollup/plugin-json": "^6.1.0",
|
|
80
80
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
81
|
-
"@rollup/plugin-typescript": "^12.3.0",
|
|
82
81
|
"@rollup/plugin-terser": "^0.4.4",
|
|
82
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
83
|
+
"@svgr/rollup": "^8.1.0",
|
|
83
84
|
"@svgr/webpack": "^6.2.1",
|
|
84
85
|
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
|
85
86
|
"@typescript-eslint/parser": "^5.10.2",
|
|
86
87
|
"@vue/compiler-sfc": "^3.2.29",
|
|
87
88
|
"@vue/eslint-config-typescript": "^10.0.0",
|
|
89
|
+
"ali-oss": "^6.17.0",
|
|
88
90
|
"autoprefixer": "^10.4.2",
|
|
89
91
|
"babel-loader": "^8.2.3",
|
|
90
92
|
"babel-plugin-import": "^1.13.3",
|
|
91
93
|
"chalk": "^4.0.0",
|
|
92
|
-
"compression-webpack-plugin": "^
|
|
94
|
+
"compression-webpack-plugin": "^11.1.0",
|
|
93
95
|
"connect-history-api-fallback": "^2.0.0",
|
|
94
|
-
"copy-webpack-plugin": "^
|
|
96
|
+
"copy-webpack-plugin": "^13.0.1",
|
|
95
97
|
"css-loader": "^6.6.0",
|
|
96
|
-
"css-minimizer-webpack-plugin": "^
|
|
98
|
+
"css-minimizer-webpack-plugin": "^7.0.4",
|
|
97
99
|
"cssnano": "^5.0.16",
|
|
98
100
|
"deepmerge": "^4.2.2",
|
|
99
101
|
"del": "^6.0.0",
|
|
@@ -113,35 +115,33 @@
|
|
|
113
115
|
"express": "^4.17.2",
|
|
114
116
|
"figlet": "^1.5.2",
|
|
115
117
|
"file-loader": "^6.2.0",
|
|
116
|
-
"git-clone": "0.
|
|
118
|
+
"git-clone": "^0.2.0",
|
|
117
119
|
"glob": "^7.2.0",
|
|
118
120
|
"html-loader": "^3.1.0",
|
|
119
121
|
"html-webpack-plugin": "^5.5.0",
|
|
120
122
|
"http-proxy-middleware": "^2.0.2",
|
|
121
|
-
"inquirer": "^
|
|
122
|
-
"
|
|
123
|
+
"inquirer": "^13.3.0",
|
|
124
|
+
"less": "^4.1.3",
|
|
125
|
+
"less-loader": "^11.0.0",
|
|
123
126
|
"mini-css-extract-plugin": "^2.5.3",
|
|
124
127
|
"open": "^8.4.0",
|
|
125
128
|
"ora": "^4.0.4",
|
|
126
129
|
"params-replace-loader": "^1.1.6",
|
|
127
130
|
"portfinder": "^1.0.28",
|
|
128
|
-
"postcss-loader": "^
|
|
131
|
+
"postcss-loader": "^7.3.4",
|
|
129
132
|
"postcss-nested": "^5.0.6",
|
|
130
133
|
"postcss-preset-env": "^7.3.1",
|
|
131
134
|
"postcss-simple-vars": "^6.0.3",
|
|
132
135
|
"progress-bar-webpack-plugin": "^2.1.0",
|
|
133
|
-
"qs": "^6.
|
|
136
|
+
"qs": "^6.14.2",
|
|
134
137
|
"rimraf": "^3.0.2",
|
|
135
|
-
"rollup": "^4.
|
|
138
|
+
"rollup": "^4.59.0",
|
|
139
|
+
"rollup-plugin-jsx": "^1.0.3",
|
|
136
140
|
"rollup-plugin-node-externals": "^8.1.2",
|
|
137
141
|
"rollup-plugin-postcss": "^4.0.2",
|
|
138
142
|
"rollup-plugin-vue": "^6.0.0",
|
|
139
143
|
"sass": "^1.94.2",
|
|
140
144
|
"sass-loader": "^12.4.0",
|
|
141
|
-
"less": "^4.1.3",
|
|
142
|
-
"less-loader": "^11.0.0",
|
|
143
|
-
"stylus": "^0.57.0",
|
|
144
|
-
"stylus-loader": "^7.0.0",
|
|
145
145
|
"sass-resources-loader": "^2.2.4",
|
|
146
146
|
"semver": "^7.3.5",
|
|
147
147
|
"shelljs": "^0.8.5",
|
|
@@ -150,32 +150,39 @@
|
|
|
150
150
|
"stylelint": "^14.3.0",
|
|
151
151
|
"stylelint-config-standard": "^24.0.0",
|
|
152
152
|
"stylelint-webpack-plugin": "^3.1.1",
|
|
153
|
+
"stylus": "^0.57.0",
|
|
154
|
+
"stylus-loader": "^7.0.0",
|
|
153
155
|
"ts-import-plugin": "^2.0.0",
|
|
154
156
|
"ts-loader": "^9.2.6",
|
|
155
157
|
"typescript": "^4.5.5",
|
|
156
158
|
"url-loader": "^4.1.1",
|
|
157
159
|
"vue-eslint-parser": "^8.2.0",
|
|
158
|
-
"vue-loader": "^15.
|
|
160
|
+
"vue-loader": "^15.11.1",
|
|
159
161
|
"vue-style-loader": "^4.1.3",
|
|
160
|
-
"vue-template-compiler": "^2.
|
|
161
|
-
"webpack": "^5.
|
|
162
|
+
"vue-template-compiler": "^2.7.16",
|
|
163
|
+
"webpack": "^5.105.3",
|
|
162
164
|
"webpack-bundle-analyzer": "^4.5.0",
|
|
163
|
-
"webpack-cli": "^
|
|
165
|
+
"webpack-cli": "^5.1.4",
|
|
164
166
|
"webpack-dev-middleware": "^5.3.1",
|
|
165
167
|
"webpack-hot-middleware": "^2.25.1",
|
|
166
168
|
"webpack-merge": "^5.8.0",
|
|
167
169
|
"webpack-node-externals": "^3.0.0",
|
|
168
|
-
"yargs": "^
|
|
169
|
-
"@baiducloud/sdk": "^1.0.7",
|
|
170
|
-
"ali-oss": "^6.17.0"
|
|
170
|
+
"yargs": "^17.7.2"
|
|
171
171
|
},
|
|
172
172
|
"devDependencies": {
|
|
173
|
-
"@commitlint/cli": "^
|
|
174
|
-
"@commitlint/config-conventional": "^
|
|
173
|
+
"@commitlint/cli": "^20.4.2",
|
|
174
|
+
"@commitlint/config-conventional": "^20.4.2",
|
|
175
175
|
"husky": "^4.3.8",
|
|
176
176
|
"lint-staged": "^12.3.3",
|
|
177
177
|
"prettier": "^2.5.1"
|
|
178
178
|
},
|
|
179
|
+
"overrides": {
|
|
180
|
+
"serialize-javascript": "^7.0.3",
|
|
181
|
+
"ajv": "^8.18.0",
|
|
182
|
+
"lodash": "^4.17.22",
|
|
183
|
+
"minimatch": "^3.1.3",
|
|
184
|
+
"diff": "^4.0.4"
|
|
185
|
+
},
|
|
179
186
|
"engines": {
|
|
180
187
|
"node": ">= 16.0.0",
|
|
181
188
|
"npm": ">= 8.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# 此处存放 webpack 自定义 loader
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const curConfig = require('../config/index'); // 获取当前项目根目录下的配置文件
|
|
3
|
+
|
|
4
|
+
const DEFAULT_COMPONENTS_DIR = curConfig.componentsDir || './src/components';
|
|
5
|
+
const DEFAULT_SCOPE_KEY = 'data-scope';
|
|
6
|
+
|
|
7
|
+
function normalizePath(targetPath) {
|
|
8
|
+
return targetPath ? path.normalize(targetPath) : '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 将驼峰命名转换为 kebab-case(短横线连接)
|
|
13
|
+
* @param {string} str - 输入字符串
|
|
14
|
+
* @returns {string} kebab-case 格式
|
|
15
|
+
*/
|
|
16
|
+
function camelToKebab(str) {
|
|
17
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 将驼峰命名转换为 snake_case(下划线连接)
|
|
22
|
+
* @param {string} str - 输入字符串
|
|
23
|
+
* @returns {string} snake_case 格式
|
|
24
|
+
*/
|
|
25
|
+
function camelToSnake(str) {
|
|
26
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 生成组件名的所有可能变体(包括大小写、命名风格、container 后缀等)
|
|
31
|
+
* @param {string} componentName - 组件名称,如 simpleCmp__c
|
|
32
|
+
* @returns {string[]} 所有可能的变体数组
|
|
33
|
+
*/
|
|
34
|
+
function generateComponentNameVariants(componentName) {
|
|
35
|
+
if (!componentName) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const variants = new Set();
|
|
40
|
+
|
|
41
|
+
// 原始名称
|
|
42
|
+
variants.add(componentName);
|
|
43
|
+
|
|
44
|
+
// 首字母大写
|
|
45
|
+
const capitalized = componentName.charAt(0).toUpperCase() + componentName.slice(1);
|
|
46
|
+
variants.add(capitalized);
|
|
47
|
+
|
|
48
|
+
// 全小写
|
|
49
|
+
variants.add(componentName.toLowerCase());
|
|
50
|
+
|
|
51
|
+
// 全大写
|
|
52
|
+
variants.add(componentName.toUpperCase());
|
|
53
|
+
|
|
54
|
+
// kebab-case(短横线连接)
|
|
55
|
+
const kebabCase = camelToKebab(componentName);
|
|
56
|
+
variants.add(kebabCase);
|
|
57
|
+
|
|
58
|
+
// snake_case(下划线连接)
|
|
59
|
+
const snakeCase = camelToSnake(componentName);
|
|
60
|
+
variants.add(snakeCase);
|
|
61
|
+
|
|
62
|
+
// 首字母大写的 kebab-case
|
|
63
|
+
const capitalizedKebab = kebabCase.charAt(0).toUpperCase() + kebabCase.slice(1);
|
|
64
|
+
variants.add(capitalizedKebab);
|
|
65
|
+
|
|
66
|
+
// 首字母大写的 snake_case
|
|
67
|
+
const capitalizedSnake = snakeCase.charAt(0).toUpperCase() + snakeCase.slice(1);
|
|
68
|
+
variants.add(capitalizedSnake);
|
|
69
|
+
|
|
70
|
+
// 生成带 container 后缀的变体
|
|
71
|
+
const containerSuffixes = ['-container', 'Container', '_container'];
|
|
72
|
+
const baseVariants = Array.from(variants);
|
|
73
|
+
baseVariants.forEach((variant) => {
|
|
74
|
+
containerSuffixes.forEach((suffix) => {
|
|
75
|
+
variants.add(variant + suffix);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return Array.from(variants);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 检查选择器是否匹配组件名(忽略大小写和命名风格)
|
|
84
|
+
* @param {string} selector - 选择器,如 .simpleCmp__c 或 #simpleCmp__c
|
|
85
|
+
* @param {string[]} componentNameVariants - 组件名的所有变体
|
|
86
|
+
* @returns {boolean} 是否匹配
|
|
87
|
+
*/
|
|
88
|
+
function isComponentRootSelector(selector, componentNameVariants) {
|
|
89
|
+
if (!selector || !componentNameVariants || componentNameVariants.length === 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const trimmedSelector = selector.trim();
|
|
94
|
+
|
|
95
|
+
// 提取类选择器或 ID 选择器的名称部分
|
|
96
|
+
// 匹配 .className 或 #idName 格式
|
|
97
|
+
const match = trimmedSelector.match(/^[.#]([a-zA-Z0-9_-]+)/);
|
|
98
|
+
if (!match) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const selectorName = match[1];
|
|
103
|
+
|
|
104
|
+
// 检查是否匹配任何组件名变体(忽略大小写)
|
|
105
|
+
return componentNameVariants.some((variant) => {
|
|
106
|
+
return selectorName.toLowerCase() === variant.toLowerCase();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 为样式选择器添加作用域前缀
|
|
112
|
+
* @param {string} selector - 选择器字符串
|
|
113
|
+
* @param {string} scopeSelector - 作用域选择器,如 [data-scope="componentName"]
|
|
114
|
+
* @param {string[]} componentNameVariants - 组件名的所有变体,用于判断是否是最外层选择器
|
|
115
|
+
* @returns {string} 添加了作用域的选择器
|
|
116
|
+
*/
|
|
117
|
+
function addScopeToSelector(selector, scopeSelector, componentNameVariants = []) {
|
|
118
|
+
if (!selector || !selector.trim()) {
|
|
119
|
+
return selector;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const trimmedSelector = selector.trim();
|
|
123
|
+
|
|
124
|
+
// 处理 :global() 语法,不添加作用域
|
|
125
|
+
if (trimmedSelector.includes(':global(')) {
|
|
126
|
+
return selector;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 如果选择器已经是作用域选择器,直接返回
|
|
130
|
+
if (trimmedSelector.startsWith('[') && trimmedSelector.includes(scopeSelector)) {
|
|
131
|
+
return selector;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 处理多个选择器(逗号分隔)
|
|
135
|
+
if (trimmedSelector.includes(',')) {
|
|
136
|
+
return trimmedSelector
|
|
137
|
+
.split(',')
|
|
138
|
+
.map((sel) => addScopeToSelector(sel.trim(), scopeSelector, componentNameVariants))
|
|
139
|
+
.join(', ');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 处理伪类和伪元素(如 :hover, ::before, :nth-child(2))
|
|
143
|
+
// 匹配格式:baseSelector:pseudo 或 baseSelector::pseudo
|
|
144
|
+
const pseudoMatch = trimmedSelector.match(
|
|
145
|
+
/^(.+?)(::?[a-zA-Z-]+(?:\([^)]*\))?(?:::?[a-zA-Z-]+(?:\([^)]*\))?)*)$/
|
|
146
|
+
);
|
|
147
|
+
if (pseudoMatch) {
|
|
148
|
+
const baseSelector = pseudoMatch[1].trim();
|
|
149
|
+
const pseudo = pseudoMatch[2];
|
|
150
|
+
// 为 baseSelector 添加作用域
|
|
151
|
+
const scopedBase = addScopeToSelector(baseSelector, scopeSelector, componentNameVariants);
|
|
152
|
+
return `${scopedBase}${pseudo}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 检查是否是最外层组件选择器(类选择器或 ID 选择器)
|
|
156
|
+
const isRootSelector = isComponentRootSelector(trimmedSelector, componentNameVariants);
|
|
157
|
+
|
|
158
|
+
// 处理普通选择器
|
|
159
|
+
if (trimmedSelector.startsWith('.') || trimmedSelector.startsWith('#')) {
|
|
160
|
+
// 如果是最外层组件选择器,直接拼接(无空格)
|
|
161
|
+
// 否则添加空格(包含在 scope 里面)
|
|
162
|
+
if (isRootSelector) {
|
|
163
|
+
return `${scopeSelector}${trimmedSelector}`;
|
|
164
|
+
} else {
|
|
165
|
+
return `${scopeSelector} ${trimmedSelector}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 如果选择器以 [ 开头(属性选择器),直接拼接
|
|
170
|
+
if (trimmedSelector.startsWith('[')) {
|
|
171
|
+
return `${scopeSelector}${trimmedSelector}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 处理标签选择器和其他选择器(添加空格)
|
|
175
|
+
return `${scopeSelector} ${trimmedSelector}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 解析样式文件并添加作用域
|
|
180
|
+
* @param {string} source - 原始样式内容
|
|
181
|
+
* @param {string} scopeSelector - 作用域选择器
|
|
182
|
+
* @param {string[]} componentNameVariants - 组件名的所有变体
|
|
183
|
+
* @returns {string} 添加了作用域的样式内容
|
|
184
|
+
*/
|
|
185
|
+
function addScopeToStyles(source, scopeSelector, componentNameVariants = []) {
|
|
186
|
+
if (!source || !source.trim()) {
|
|
187
|
+
return source;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let result = '';
|
|
191
|
+
let i = 0;
|
|
192
|
+
let depth = 0; // 大括号嵌套深度
|
|
193
|
+
let inString = false; // 是否在字符串中
|
|
194
|
+
let stringChar = ''; // 字符串的引号类型
|
|
195
|
+
let inComment = false; // 是否在注释中
|
|
196
|
+
let commentType = ''; // 注释类型:'//' 或 '/*'
|
|
197
|
+
let currentSelector = '';
|
|
198
|
+
let currentRule = '';
|
|
199
|
+
let braceStart = -1;
|
|
200
|
+
|
|
201
|
+
// 不需要添加作用域的 @ 规则(这些规则应该保持原样)
|
|
202
|
+
const skipScopeAtRules = ['@import', '@charset', '@namespace', '@font-face'];
|
|
203
|
+
// 需要递归处理的 @ 规则(内部的选择器也需要添加作用域)
|
|
204
|
+
const nestedAtRules = ['@media', '@supports', '@keyframes', '@page', '@viewport'];
|
|
205
|
+
let inAtRule = false;
|
|
206
|
+
let atRuleName = '';
|
|
207
|
+
let atRuleType = ''; // 'skip' 或 'nested'
|
|
208
|
+
|
|
209
|
+
while (i < source.length) {
|
|
210
|
+
const char = source[i];
|
|
211
|
+
const nextChar = source[i + 1] || '';
|
|
212
|
+
|
|
213
|
+
// 处理字符串
|
|
214
|
+
if (!inComment && (char === '"' || char === "'")) {
|
|
215
|
+
if (!inString) {
|
|
216
|
+
inString = true;
|
|
217
|
+
stringChar = char;
|
|
218
|
+
} else if (char === stringChar && source[i - 1] !== '\\') {
|
|
219
|
+
inString = false;
|
|
220
|
+
stringChar = '';
|
|
221
|
+
}
|
|
222
|
+
currentRule += char;
|
|
223
|
+
i++;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 处理注释
|
|
228
|
+
if (!inString) {
|
|
229
|
+
if (!inComment && char === '/' && nextChar === '/') {
|
|
230
|
+
inComment = true;
|
|
231
|
+
commentType = '//';
|
|
232
|
+
currentRule += char;
|
|
233
|
+
i++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!inComment && char === '/' && nextChar === '*') {
|
|
237
|
+
inComment = true;
|
|
238
|
+
commentType = '/*';
|
|
239
|
+
currentRule += char;
|
|
240
|
+
i++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (inComment && commentType === '//' && char === '\n') {
|
|
244
|
+
inComment = false;
|
|
245
|
+
commentType = '';
|
|
246
|
+
currentRule += char;
|
|
247
|
+
i++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (inComment && commentType === '/*' && char === '*' && nextChar === '/') {
|
|
251
|
+
inComment = false;
|
|
252
|
+
commentType = '';
|
|
253
|
+
currentRule += char + nextChar;
|
|
254
|
+
i += 2;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (inComment) {
|
|
258
|
+
currentRule += char;
|
|
259
|
+
i++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 处理 @ 规则
|
|
265
|
+
if (!inString && !inComment && char === '@' && depth === 0) {
|
|
266
|
+
const remaining = source.substring(i);
|
|
267
|
+
// 检查是否是跳过作用域的规则
|
|
268
|
+
for (const atRule of skipScopeAtRules) {
|
|
269
|
+
if (remaining.startsWith(atRule)) {
|
|
270
|
+
inAtRule = true;
|
|
271
|
+
atRuleName = atRule;
|
|
272
|
+
atRuleType = 'skip';
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// 检查是否是嵌套规则
|
|
277
|
+
if (!inAtRule) {
|
|
278
|
+
for (const atRule of nestedAtRules) {
|
|
279
|
+
if (remaining.startsWith(atRule)) {
|
|
280
|
+
inAtRule = true;
|
|
281
|
+
atRuleName = atRule;
|
|
282
|
+
atRuleType = 'nested';
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 处理左大括号
|
|
290
|
+
if (!inString && !inComment && char === '{') {
|
|
291
|
+
if (depth === 0) {
|
|
292
|
+
// 保存当前选择器
|
|
293
|
+
currentSelector = currentRule.trim();
|
|
294
|
+
currentRule = '';
|
|
295
|
+
braceStart = i;
|
|
296
|
+
}
|
|
297
|
+
depth++;
|
|
298
|
+
currentRule += char;
|
|
299
|
+
i++;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 处理右大括号
|
|
304
|
+
if (!inString && !inComment && char === '}') {
|
|
305
|
+
depth--;
|
|
306
|
+
currentRule += char;
|
|
307
|
+
|
|
308
|
+
if (depth === 0) {
|
|
309
|
+
// 处理完整的规则块
|
|
310
|
+
if (inAtRule) {
|
|
311
|
+
if (atRuleType === 'skip') {
|
|
312
|
+
// 跳过作用域的 @ 规则直接添加
|
|
313
|
+
result += currentSelector + currentRule;
|
|
314
|
+
} else if (atRuleType === 'nested') {
|
|
315
|
+
// 嵌套 @ 规则:为内部的选择器添加作用域
|
|
316
|
+
const innerContent = currentRule.slice(1, -1); // 去掉外层大括号
|
|
317
|
+
const scopedInnerContent = addScopeToStyles(
|
|
318
|
+
innerContent,
|
|
319
|
+
scopeSelector,
|
|
320
|
+
componentNameVariants
|
|
321
|
+
);
|
|
322
|
+
result += currentSelector + '{' + scopedInnerContent + '}';
|
|
323
|
+
} else {
|
|
324
|
+
result += currentSelector + currentRule;
|
|
325
|
+
}
|
|
326
|
+
inAtRule = false;
|
|
327
|
+
atRuleName = '';
|
|
328
|
+
atRuleType = '';
|
|
329
|
+
} else {
|
|
330
|
+
// 普通规则,为选择器添加作用域
|
|
331
|
+
const scopedSelector = addScopeToSelector(
|
|
332
|
+
currentSelector,
|
|
333
|
+
scopeSelector,
|
|
334
|
+
componentNameVariants
|
|
335
|
+
);
|
|
336
|
+
result += scopedSelector + currentRule;
|
|
337
|
+
}
|
|
338
|
+
currentSelector = '';
|
|
339
|
+
currentRule = '';
|
|
340
|
+
braceStart = -1;
|
|
341
|
+
}
|
|
342
|
+
i++;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 其他字符
|
|
347
|
+
currentRule += char;
|
|
348
|
+
i++;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 处理文件末尾可能剩余的内容
|
|
352
|
+
if (currentRule.trim()) {
|
|
353
|
+
if (depth === 0 && currentSelector) {
|
|
354
|
+
const scopedSelector = addScopeToSelector(
|
|
355
|
+
currentSelector,
|
|
356
|
+
scopeSelector,
|
|
357
|
+
componentNameVariants
|
|
358
|
+
);
|
|
359
|
+
result += scopedSelector + currentRule;
|
|
360
|
+
} else {
|
|
361
|
+
result += currentRule;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return result || source;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* 自动添加组件样式作用域
|
|
369
|
+
* @param {string} source - 原始样式内容
|
|
370
|
+
* @returns {string} 添加了作用域的样式内容
|
|
371
|
+
* 实现关键路径:
|
|
372
|
+
* 1. 判断是否是目标文件
|
|
373
|
+
* 2. 获取组件名
|
|
374
|
+
* 3. 生成组件名的所有可能变体
|
|
375
|
+
* 4. 为选择器添加作用域
|
|
376
|
+
* 5. 返回添加了作用域的样式内容
|
|
377
|
+
*
|
|
378
|
+
* 特别说明,组件根节点的样式 需要和组件目录名称 产生关联,不然其根节点的样式会失效。
|
|
379
|
+
*/
|
|
380
|
+
module.exports = function componentScopeStyleLoader(source) {
|
|
381
|
+
if (typeof this?.cacheable === 'function') {
|
|
382
|
+
this.cacheable();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const options =
|
|
386
|
+
(typeof this?.getOptions === 'function' && this.getOptions()) || this?.query || {};
|
|
387
|
+
const scopeKey = options.scopeKey || DEFAULT_SCOPE_KEY;
|
|
388
|
+
const componentsDir = options.componentsDir || DEFAULT_COMPONENTS_DIR;
|
|
389
|
+
|
|
390
|
+
const resourcePath = this.resourcePath || '';
|
|
391
|
+
const isTargetFile = /(index|style)\.(scss|less)$/i.test(resourcePath);
|
|
392
|
+
if (!isTargetFile) {
|
|
393
|
+
return source;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const componentsDirAbs = path.isAbsolute(componentsDir)
|
|
397
|
+
? componentsDir
|
|
398
|
+
: path.resolve(process.cwd(), componentsDir);
|
|
399
|
+
|
|
400
|
+
const normalizedResourcePath = normalizePath(resourcePath);
|
|
401
|
+
const normalizedCmpDir = normalizePath(componentsDirAbs + path.sep);
|
|
402
|
+
|
|
403
|
+
// 仅处理位于组件目录下的样式文件
|
|
404
|
+
if (!normalizedResourcePath.startsWith(normalizedCmpDir)) {
|
|
405
|
+
return source;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 组件目录的第一层子目录名即组件名
|
|
409
|
+
const relativePath = path.relative(componentsDirAbs, resourcePath);
|
|
410
|
+
const pathParts = relativePath.split(path.sep);
|
|
411
|
+
const cmpName = pathParts[0];
|
|
412
|
+
|
|
413
|
+
if (!cmpName) {
|
|
414
|
+
return source;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 非组件直接子目录下的样式也不处理
|
|
418
|
+
if (pathParts.length > 2) {
|
|
419
|
+
return source;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 生成组件名的所有可能变体
|
|
423
|
+
const componentNameVariants = generateComponentNameVariants(cmpName);
|
|
424
|
+
|
|
425
|
+
const scopeSelector = `[${scopeKey}="${cmpName}"]`;
|
|
426
|
+
const scopedContent = addScopeToStyles(source, scopeSelector, componentNameVariants);
|
|
427
|
+
|
|
428
|
+
return scopedContent;
|
|
429
|
+
};
|