-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebpack.base.ts
More file actions
273 lines (245 loc) · 8.28 KB
/
Copy pathwebpack.base.ts
File metadata and controls
273 lines (245 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
const { DefinePlugin } = webpack;
// 参数定义 - 使用自定义解析支持位置参数
const optionDefinitions = [
{
name: 'client' as const,
type: String,
defaultOption: true,
multiple: true,
},
{
name: 'watch' as const,
alias: 'w',
type: Boolean,
defaultValue: false,
},
{
name: 'user-rules' as const,
alias: 'u',
type: String,
defaultValue: '',
}
];
// 解析参数
const args = commandLineArgs(optionDefinitions) as TypedArgs<typeof optionDefinitions>;
// 从位置参数中提取 client 和 mode
// multiple: true 会将所有位置参数收集为数组
const positionalArgs = Array.isArray(args.client) ? args.client : [];
// 验证并提取参数
let clientArg: string | undefined;
let modeArg: string | undefined;
const validClients = ['cfw', 'cvr', 'clash-party'];
const validModes = ['global-proxy', 'auto-routing'];
positionalArgs.forEach(arg => {
if (validClients.includes(arg)) {
clientArg = arg;
} else if (validModes.includes(arg)) {
modeArg = arg;
}
});
// 验证必填参数
if (!clientArg || !modeArg) {
console.error('错误: 必须指定 client 和 mode 参数');
console.error('用法: npm run build <cfw|cvr> <global-proxy|auto-routing>');
console.error('示例: npm run build cvr global-proxy');
console.error('示例: npm run build cfw auto-routing');
process.exit(1);
}
console.log(`构建配置: client=${clientArg}, mode=${modeArg}`);
// 异步获取编译时规则
// global-proxy 模式不需要规则数据,只有 auto-routing 模式才需要
const compileTimeRules = modeArg === 'auto-routing' ? await fetchRules() : {};
const path = await import('path');
const { fileURLToPath } = await import('url');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 编译时规则直接通过 DefinePlugin 注入,无需写入文件
console.log(`规则数据大小: ${(JSON.stringify(compileTimeRules).length / 1024).toFixed(2)} KB`);
// 加载用户自定义规则
let userCustomRules = { rules3D: [], simpleRules: { prepend: [], append: [] }, groups: [] };
if (args['user-rules']) {
try {
const userRulesPath = path.resolve(args['user-rules']);
console.log(`尝试加载用户自定义规则: ${userRulesPath}`);
// Windows 需要将绝对路径转换为 file:// URL
const isWindows = process.platform === 'win32';
const importPath = isWindows
? `file:///${userRulesPath.replace(/\\/g, '/')}`
: userRulesPath;
const userRulesModule = await import(importPath);
userCustomRules = userRulesModule.userCustomRules || userRulesModule.default || {};
const rules3DCount = userCustomRules.rules3D?.length || 0;
const prependCount = userCustomRules.simpleRules?.prepend?.length || 0;
const appendCount = userCustomRules.simpleRules?.append?.length || 0;
const groupsCount = userCustomRules.groups?.length || 0;
console.log(`✅ 用户自定义规则加载成功: rules3D=${rules3DCount}, simpleRules(prepend=${prependCount}, append=${appendCount}), groups=${groupsCount}`);
} catch (error) {
console.warn(`⚠️ 加载用户自定义规则失败: ${error.message}`);
console.warn(' 将使用空配置(不加载任何用户规则)');
}
} else {
console.log('💡 未指定用户自定义规则文件,使用 --user-rules 参数加载');
}
// 获取客户端的短前缀用于文件名
function clientPrefix(client: string): string {
return client === 'clash-party' ? 'cparty' : client;
}
const conf: Configuration = {
mode: 'production',
entry: {
[`${clientArg}-${modeArg}`]: {
import: clientArg === 'cfw'
? './src/clients/clash-for-windows/main.ts'
: clientArg === 'cvr'
? './src/clients/clash-verge/main.ts'
: './src/clients/clash-party/main.ts',
// 为不同客户端生成可区分的文件前缀
filename: `${clientArg}/${clientPrefix(clientArg)}-${modeArg}.js`,
// CFW 需要 commonjs2 导出,CVR 和 Clash Party 不配置 library,直接在源码中导出
...(clientArg === 'cfw' ? { library: { type: 'commonjs2' } } : {})
}
},
output: {
filename: `[name].js`,
iife: false,
// CVR 和 Clash Party 使用 var 库类型,将 main 导出为顶层变量
...(clientArg !== 'cfw' ? { library: { type: 'var', name: 'main' }, libraryExport: 'main' } : {}),
},
watch: args.watch,
stats: 'minimal',
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
},
module: {
rules: [
{
test: /\.(t|j)sx?$/i,
use: {
loader: 'babel-loader',
options: babelConf,
},
},
],
},
performance: {
maxEntrypointSize: 10000000,
maxAssetSize: 30000000,
hints: false
},
optimization: {
mangleExports: false,
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
keep_fnames: true,
keep_classnames: true,
format: {
comments: false,
},
},
extractComments: false,
})
],
},
plugins: [
new DefinePlugin({
__MAIN__: `this['main']=main;`,
__ROUTING_MODE__: JSON.stringify(modeArg),
__CompileTime_Rules__: JSON.stringify(compileTimeRules),
__USER_CUSTOM_RULES__: JSON.stringify(userCustomRules),
}),
// CVR 和 Clash Party:在文件头部添加 var main 声明,严格模式下不报错
...(clientArg !== 'cfw' ? [new webpack.BannerPlugin({
banner: 'var main;',
raw: true,
})] : [])
]
} as Configuration;
webpack(conf , (err , stats) => {
if ( err ) {
console.log(err , 111111111111111);
}
if ( stats.hasErrors() ) {
console.log(stats);
}
console.log(stats.toString({ colors : true }));
});
import {} from 'cmd-ts';
import { fetchRules } from './CompileTimeScripts/fetch-rules';
import commandLineArgs , {type OptionDefinition} from 'command-line-args';
import webpack from 'webpack';
import type { Configuration } from 'webpack';
import TerserPlugin from 'terser-webpack-plugin';
import babelConf from './babel.config.mjs';
/**
* 辅助类型:将 union 转为 intersection(用于合并每个 option 的属性)
*/
type UnionToIntersection<U> =
(U extends unknown ? (x: U) => void : never) extends (x: infer I) => void
? I
: never;
/**
* 解析 type 字段对应的 TS 类型
* - 未指定 type → string(库默认行为)
* - Boolean → boolean
* - Number → number
* - String → string
* - 自定义 parser 函数 → 自动推断其返回值
*/
type GetParserType<T> =
T extends undefined | null | void ? string :
T extends BooleanConstructor ? boolean :
T extends NumberConstructor ? number :
T extends StringConstructor ? string :
T extends (...args: unknown[]) => infer R ? R :
unknown;
/**
* 判断是否为数组类型(multiple 或 lazyMultiple)
*/
type IsMultiple<D> =
D extends { multiple: true } | { lazyMultiple: true } ? true : false;
/**
* 基础值类型(不包含 undefined)
*/
type BaseValue<D extends OptionDefinition> = IsMultiple<D> extends true
? GetParserType<D['type']>[]
: GetParserType<D['type']>;
/**
* 单个 option 对应的属性类型(核心逻辑)
* - 如果定义中写了 defaultValue → 属性必选(required)
* - 否则 → 属性可选(可选 + | undefined,与运行时“键不存在”行为一致)
*/
type OptionProp<D extends OptionDefinition> =
D['name'] extends infer Name extends PropertyKey
? 'defaultValue' extends keyof D
? { [K in Name]: BaseValue<D> } // 有 defaultValue → 必选
: { [K in Name]?: BaseValue<D> } // 无 defaultValue → 可选
: never;
/**
* 主类型:TypedArgs<typeof optionDefinitions>
*
* 支持 command-line-args 库的大部分常用 case:
* - Boolean / String / Number / 自定义 parser
* - multiple / lazyMultiple → 数组
* - defaultValue → 必选属性
* - defaultOption(位置参数)也自动支持(类型由 multiple 决定)
* - name 为 literal 类型(as const)时,输出键为精确 literal
* - 未指定的 type 默认 string
*
* 注意:
* - 键不存在时访问返回 undefined(与库行为一致)
* - 如果你使用了 parseOptions.camelCase,键名会被转换,需要自行处理
* - _unknown 等特殊字段未包含(因为 commandLineArgs(optionDefinitions) 默认不返回)
*/
export type TypedArgs<Defs extends readonly OptionDefinition[]> =
Defs extends readonly OptionDefinition[]
? UnionToIntersection<
Defs[number] extends infer D
? D extends OptionDefinition
? OptionProp<D>
: never
: never
>
: never;