@rexnow/rslib-plugin 3.0.1 → 3.1.1
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 +9 -0
- package/dist/encrypt.d.ts +5 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +75 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
- 🔧 **自动类型生成**:根据 `WidgetMetadata` 自动生成 TypeScript 类型定义
|
|
13
13
|
- 📦 **构建优化**:清除 Rex Widget 不支持的导出声明
|
|
14
|
+
- 🔐 **模块加密**:可选调用 Rex 官方加密服务,将产物写成 `REXENC` 格式
|
|
14
15
|
- 🛠️ **开发体验**:提供完整的类型支持和智能提示
|
|
15
16
|
- 🔄 **热更新**:开发模式下自动重新生成类型定义
|
|
16
17
|
|
|
@@ -56,6 +57,7 @@ export default defineConfig({
|
|
|
56
57
|
plugins: [
|
|
57
58
|
pluginRexWidget({
|
|
58
59
|
typesFilePath: 'src/custom-types.d.ts', // 自定义类型文件路径
|
|
60
|
+
encrypt: true, // 生产构建后加密 JS 产物
|
|
59
61
|
}),
|
|
60
62
|
],
|
|
61
63
|
lib: [
|
|
@@ -73,6 +75,7 @@ export default defineConfig({
|
|
|
73
75
|
| 选项 | 类型 | 默认值 | 描述 |
|
|
74
76
|
|------|------|--------|------|
|
|
75
77
|
| `typesFilePath` | `string` | `'src/rex-widget-env.d.ts'` | 生成的类型定义文件路径 |
|
|
78
|
+
| `encrypt` | `boolean` | `false` | 生产构建后调用 [Rex 模块加密](https://rexnow.tv/encrypt/) 服务,将 JS 产物写成 `REXENC` 格式。`watch` 开发监听时不会加密。 |
|
|
76
79
|
|
|
77
80
|
## 💡 工作原理
|
|
78
81
|
|
|
@@ -154,6 +157,12 @@ type SearchContentType = typeof searchContent;
|
|
|
154
157
|
|
|
155
158
|
插件会自动清除构建输出中的导出声明,因为 Rex Widget 不支持脚本有导出声明。
|
|
156
159
|
|
|
160
|
+
### 4. 模块加密
|
|
161
|
+
|
|
162
|
+
设置 `encrypt: true` 后,插件会在类型生成完成之后,把构建出的 `.js` 发送到 `https://api.rexnow.tv/api/widgets/encrypt`,并用返回的 `REXENC` 信封覆盖产物。协议与 [rexnow.tv/encrypt](https://rexnow.tv/encrypt/) 一致:`AES-256-GCM`(`A256GCM` / `rex-1`),加密结果仅供 Rex 使用。
|
|
163
|
+
|
|
164
|
+
请保留未加密的源码。加密不会在 `rslib build --watch` 时执行,以免开发服务器读到密文。构建机需要能访问 `api.rexnow.tv`;加密失败会使生产构建失败。大文件可能需要 1–2 分钟;超过 2 MiB 或已经是 `REXENC` / `FWENC` 的文件会被拒绝。不要同时发布未加密的 source map。
|
|
165
|
+
|
|
157
166
|
## 📚 完整示例
|
|
158
167
|
|
|
159
168
|
### 项目结构
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const ENCRYPT_ENDPOINT = "https://api.rexnow.tv/api/widgets/encrypt";
|
|
2
|
+
export declare const ENCRYPT_MAX_BYTES: number;
|
|
3
|
+
export declare function isEncryptedWidget(source: string): boolean;
|
|
4
|
+
export declare function validateEncryptEnvelope(text: string): void;
|
|
5
|
+
export declare function encryptWidgetSource(source: string): Promise<string>;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ interface RexWidgetPluginOptions {
|
|
|
10
10
|
* @default 8000
|
|
11
11
|
*/
|
|
12
12
|
devPort?: number;
|
|
13
|
+
/**
|
|
14
|
+
* 生产构建后调用 Rex 加密服务,将 JS 产物写成 REXENC 格式。
|
|
15
|
+
* watch / 开发监听时不会加密。
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
encrypt?: boolean;
|
|
13
19
|
}
|
|
14
|
-
export declare const pluginRexWidget: ({ typesFilePath, devPort, }?: RexWidgetPluginOptions) => RsbuildPlugin;
|
|
20
|
+
export declare const pluginRexWidget: ({ typesFilePath, devPort, encrypt, }?: RexWidgetPluginOptions) => RsbuildPlugin;
|
|
15
21
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -6058,7 +6058,7 @@ const videoItemChildSchema = schemas_object({
|
|
|
6058
6058
|
previewUrl: schemas_string().optional(),
|
|
6059
6059
|
trailers: schemas_array(trailerItemSchema).optional(),
|
|
6060
6060
|
videoUrl: schemas_string().optional(),
|
|
6061
|
-
link: schemas_string(),
|
|
6061
|
+
link: schemas_string().optional(),
|
|
6062
6062
|
episode: schemas_number().optional(),
|
|
6063
6063
|
description: schemas_string().optional(),
|
|
6064
6064
|
playerType: schemas_union([
|
|
@@ -6137,6 +6137,58 @@ const widgetMetadataSchema = schemas_object({
|
|
|
6137
6137
|
params: schemas_array(widgetModuleParamSchema)
|
|
6138
6138
|
}).optional()
|
|
6139
6139
|
});
|
|
6140
|
+
const ENCRYPT_ENDPOINT = 'https://api.rexnow.tv/api/widgets/encrypt';
|
|
6141
|
+
const ENCRYPT_MAX_BYTES = 2097152;
|
|
6142
|
+
const ENCRYPT_TIMEOUT_MS = 150000;
|
|
6143
|
+
const HTTP_ERRORS = {
|
|
6144
|
+
400: '服务无法识别这个模块,请检查源码。',
|
|
6145
|
+
413: '文件过大,请缩小后重试。',
|
|
6146
|
+
422: '源码无法完成混淆,请检查模块语法。',
|
|
6147
|
+
429: '操作过于频繁,请稍后再试。',
|
|
6148
|
+
503: '加密服务暂时繁忙,请稍后再试。'
|
|
6149
|
+
};
|
|
6150
|
+
function isEncryptedWidget(source) {
|
|
6151
|
+
return /^(?:FWENC|REXENC)/.test(source.trimStart());
|
|
6152
|
+
}
|
|
6153
|
+
function validateEncryptEnvelope(text) {
|
|
6154
|
+
const lines = text.split('\n');
|
|
6155
|
+
if (3 !== lines.length || 'REXENC' !== lines[0]) throw new Error('服务返回了不兼容的格式,请稍后重试。');
|
|
6156
|
+
try {
|
|
6157
|
+
const header = JSON.parse(lines[1] ?? '');
|
|
6158
|
+
if (2 !== header.v || 'rex-1' !== header.mkv || 'A256GCM' !== header.alg || 'string' != typeof header.iv || 'string' != typeof header.kb || 12 !== Buffer.from(header.iv, 'base64').length || 60 !== Buffer.from(header.kb, 'base64').length || Buffer.from((lines[2] ?? '').trim(), 'base64').length <= 16) throw new Error();
|
|
6159
|
+
} catch {
|
|
6160
|
+
throw new Error('加密结果不完整,请重试。');
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
async function encryptWidgetSource(source) {
|
|
6164
|
+
if (!source.trim()) throw new Error('请选择文件,或粘贴模块源码。');
|
|
6165
|
+
if (Buffer.byteLength(source) > ENCRYPT_MAX_BYTES) throw new Error('文件超过 2 MiB,请缩小后重试。');
|
|
6166
|
+
if (isEncryptedWidget(source)) throw new Error('这是已加密的模块。请使用原始 JavaScript 源码。');
|
|
6167
|
+
const controller = new AbortController();
|
|
6168
|
+
const timer = setTimeout(()=>controller.abort(), ENCRYPT_TIMEOUT_MS);
|
|
6169
|
+
try {
|
|
6170
|
+
const response = await fetch(ENCRYPT_ENDPOINT, {
|
|
6171
|
+
method: 'POST',
|
|
6172
|
+
headers: {
|
|
6173
|
+
'Content-Type': 'text/plain; charset=utf-8'
|
|
6174
|
+
},
|
|
6175
|
+
body: source,
|
|
6176
|
+
signal: controller.signal,
|
|
6177
|
+
credentials: 'omit',
|
|
6178
|
+
cache: 'no-store'
|
|
6179
|
+
});
|
|
6180
|
+
if (!response.ok) throw new Error(HTTP_ERRORS[response.status] ?? `加密失败(HTTP ${response.status}),请稍后重试。`);
|
|
6181
|
+
const output = await response.text();
|
|
6182
|
+
validateEncryptEnvelope(output);
|
|
6183
|
+
return output;
|
|
6184
|
+
} catch (error) {
|
|
6185
|
+
if (error instanceof Error && 'AbortError' === error.name) throw new Error('等待已超时,请稍后重试。');
|
|
6186
|
+
if (error instanceof TypeError) throw new Error('无法连接加密服务,请检查网络后重试。');
|
|
6187
|
+
throw error;
|
|
6188
|
+
} finally{
|
|
6189
|
+
clearTimeout(timer);
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6140
6192
|
const toPascalCase = (str)=>upperFirst(camelCase(str));
|
|
6141
6193
|
function getParamTypeString(param) {
|
|
6142
6194
|
switch(param.type){
|
|
@@ -6339,6 +6391,9 @@ function generateVideoModuleInterface(nameSpaceName, sourceFile, module) {
|
|
|
6339
6391
|
});
|
|
6340
6392
|
generateModuleFunctionType(sourceFile, module);
|
|
6341
6393
|
}
|
|
6394
|
+
function toExecutableWidgetMetadataSource(content) {
|
|
6395
|
+
return content.replace(/\b(?:export\s+)?(?:const|let|var)\s+WidgetMetadata\b/g, 'WidgetMetadata');
|
|
6396
|
+
}
|
|
6342
6397
|
function safeParseWidgetMetadataFactory(api) {
|
|
6343
6398
|
return (content)=>{
|
|
6344
6399
|
try {
|
|
@@ -6347,7 +6402,7 @@ function safeParseWidgetMetadataFactory(api) {
|
|
|
6347
6402
|
};
|
|
6348
6403
|
const func = new Function("sandbox", `
|
|
6349
6404
|
let WidgetMetadata;
|
|
6350
|
-
${content};
|
|
6405
|
+
${toExecutableWidgetMetadataSource(content)};
|
|
6351
6406
|
sandbox.WidgetMetadata = WidgetMetadata;
|
|
6352
6407
|
`);
|
|
6353
6408
|
func(sandbox);
|
|
@@ -6449,7 +6504,17 @@ async function addSpaceToWidgetMetadata(distPath) {
|
|
|
6449
6504
|
});
|
|
6450
6505
|
await sourceFile.save();
|
|
6451
6506
|
}
|
|
6452
|
-
async function
|
|
6507
|
+
async function encryptOutputFiles(api, outputFiles) {
|
|
6508
|
+
const jsFiles = outputFiles.filter((file)=>file.endsWith(".js"));
|
|
6509
|
+
for (const file of jsFiles){
|
|
6510
|
+
const source = await node_fs.promises.readFile(file, "utf-8");
|
|
6511
|
+
api.logger.info(`正在加密 ${node_path.basename(file)}…`);
|
|
6512
|
+
const encrypted = await encryptWidgetSource(source);
|
|
6513
|
+
await node_fs.promises.writeFile(file, encrypted);
|
|
6514
|
+
api.logger.info(`已加密 ${node_path.basename(file)}`);
|
|
6515
|
+
}
|
|
6516
|
+
}
|
|
6517
|
+
async function processAfterBuild(api, stats, dtsPath, options = {}) {
|
|
6453
6518
|
const outputFiles = getOutputFiles(api, stats);
|
|
6454
6519
|
if (!outputFiles.length) return void api.logger.warn("未找到输出文件,跳过类型生成");
|
|
6455
6520
|
const typeDefFile = await setupTypeDefinitionFile(dtsPath);
|
|
@@ -6460,6 +6525,7 @@ async function processAfterBuild(api, stats, dtsPath) {
|
|
|
6460
6525
|
await generateWidgetTypes(outputFile);
|
|
6461
6526
|
}));
|
|
6462
6527
|
await typeDefFile.save();
|
|
6528
|
+
if (options.encrypt && !options.isWatch) await encryptOutputFiles(api, outputFiles);
|
|
6463
6529
|
}
|
|
6464
6530
|
function getOutputFiles(api, stats) {
|
|
6465
6531
|
const buildStats = stats?.toJson({
|
|
@@ -6483,7 +6549,7 @@ async function setupTypeDefinitionFile(dtsPath) {
|
|
|
6483
6549
|
typeDefFile.insertText(0, `/// <reference types='@rexnow/libs/env' />\n\n`);
|
|
6484
6550
|
return typeDefFile;
|
|
6485
6551
|
}
|
|
6486
|
-
const pluginRexWidget = ({ typesFilePath = "src/rex-widget-env.d.ts", devPort = 8000 } = {})=>({
|
|
6552
|
+
const pluginRexWidget = ({ typesFilePath = "src/rex-widget-env.d.ts", devPort = 8000, encrypt = false } = {})=>({
|
|
6487
6553
|
name: "plugin-rex-widget",
|
|
6488
6554
|
setup (api) {
|
|
6489
6555
|
const dtsPath = node_path.resolve(api.context.rootPath, typesFilePath);
|
|
@@ -6495,9 +6561,13 @@ const pluginRexWidget = ({ typesFilePath = "src/rex-widget-env.d.ts", devPort =
|
|
|
6495
6561
|
});
|
|
6496
6562
|
api.onAfterBuild(async ({ stats, isWatch, isFirstCompile })=>{
|
|
6497
6563
|
try {
|
|
6498
|
-
await processAfterBuild(api, stats, dtsPath
|
|
6564
|
+
await processAfterBuild(api, stats, dtsPath, {
|
|
6565
|
+
encrypt,
|
|
6566
|
+
isWatch
|
|
6567
|
+
});
|
|
6499
6568
|
} catch (error) {
|
|
6500
6569
|
api.logger.error("Rex Widget 插件处理失败", error);
|
|
6570
|
+
if (encrypt && !isWatch) throw error;
|
|
6501
6571
|
}
|
|
6502
6572
|
try {
|
|
6503
6573
|
if (isWatch && isFirstCompile) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexnow/rslib-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"provenance": true
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@rexnow/libs": "3.
|
|
20
|
+
"@rexnow/libs": "3.1.1",
|
|
21
21
|
"@rsbuild/core": "^2.2.5",
|
|
22
22
|
"@rslib/core": "^1.0.0",
|
|
23
23
|
"@rstest/adapter-rslib": "^0.11.12",
|