@rexnow/rslib-plugin 3.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Baran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # @rexnow/rslib-plugin
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@rexnow/rslib-plugin)](https://www.npmjs.com/package/@rexnow/rslib-plugin)
4
+ ![NPM License](https://img.shields.io/npm/l/@rexnow/rslib-plugin)
5
+
6
+ > Rex Widget 专用的 Rslib 构建插件
7
+
8
+ ## 🚀 简介
9
+
10
+ `@rexnow/rslib-plugin` 是一个专为 Rex Widget 开发优化的 Rslib 插件。它提供了以下核心功能:
11
+
12
+ - 🔧 **自动类型生成**:根据 `WidgetMetadata` 自动生成 TypeScript 类型定义
13
+ - 📦 **构建优化**:清除 Rex Widget 不支持的导出声明
14
+ - 🛠️ **开发体验**:提供完整的类型支持和智能提示
15
+ - 🔄 **热更新**:开发模式下自动重新生成类型定义
16
+
17
+ ## 📦 安装
18
+
19
+ ```bash
20
+ npm install -D @rexnow/rslib-plugin
21
+ # 或
22
+ yarn add -D @rexnow/rslib-plugin
23
+ # 或
24
+ pnpm add -D @rexnow/rslib-plugin
25
+ ```
26
+
27
+ ## 🛠️ 使用方法
28
+
29
+ ### 基础配置
30
+
31
+ 在你的 `rslib.config.ts` 文件中添加插件:
32
+
33
+ ```ts
34
+ import { pluginRexWidget } from '@rexnow/rslib-plugin';
35
+ import { defineConfig } from '@rslib/core';
36
+
37
+ export default defineConfig({
38
+ plugins: [pluginRexWidget()],
39
+ lib: [
40
+ {
41
+ format: 'esm',
42
+ syntax: ['node 18'],
43
+ dts: true,
44
+ },
45
+ ],
46
+ });
47
+ ```
48
+
49
+ ### 自定义配置
50
+
51
+ ```ts
52
+ import { pluginRexWidget } from '@rexnow/rslib-plugin';
53
+ import { defineConfig } from '@rslib/core';
54
+
55
+ export default defineConfig({
56
+ plugins: [
57
+ pluginRexWidget({
58
+ typesFilePath: 'src/custom-types.d.ts', // 自定义类型文件路径
59
+ }),
60
+ ],
61
+ lib: [
62
+ {
63
+ format: 'esm',
64
+ syntax: ['node 18'],
65
+ dts: true,
66
+ },
67
+ ],
68
+ });
69
+ ```
70
+
71
+ ## 📋 配置选项
72
+
73
+ | 选项 | 类型 | 默认值 | 描述 |
74
+ |------|------|--------|------|
75
+ | `typesFilePath` | `string` | `'src/rex-widget-env.d.ts'` | 生成的类型定义文件路径 |
76
+
77
+ ## 💡 工作原理
78
+
79
+ ### 1. 解析 WidgetMetadata
80
+
81
+ 插件会解析你的代码中的 `WidgetMetadata` 对象:
82
+
83
+ ```ts
84
+ // src/index.ts
85
+ WidgetMetadata = {
86
+ id: 'my-widget',
87
+ title: 'My Widget',
88
+ modules: [
89
+ {
90
+ id: 'search-module',
91
+ title: 'Search Module',
92
+ functionName: 'searchContent',
93
+ description: 'Search for content',
94
+ params: [
95
+ {
96
+ name: 'query',
97
+ title: 'Search Query',
98
+ type: 'input',
99
+ },
100
+ {
101
+ name: 'category',
102
+ title: 'Category',
103
+ type: 'enumeration',
104
+ enumOptions: [
105
+ { title: 'Movies', value: 'movies' },
106
+ { title: 'TV Shows', value: 'tvshows' },
107
+ ],
108
+ },
109
+ ],
110
+ },
111
+ ],
112
+ };
113
+ ```
114
+
115
+ ### 2. 生成类型定义
116
+
117
+ 插件会根据 `WidgetMetadata` 自动生成相应的类型定义:
118
+
119
+ ```ts
120
+ // src/rex-widget-env.d.ts
121
+ /// <reference types='@rexnow/libs/env' />
122
+
123
+ //#region search-module
124
+ /**
125
+ * Params of Search Module
126
+ */
127
+ interface SearchContentParams {
128
+ /**
129
+ * Search Query
130
+ */
131
+ query: string;
132
+ /**
133
+ * Category
134
+ */
135
+ category: 'movies' | 'tvshows';
136
+ }
137
+
138
+ /**
139
+ * Search Module
140
+ * @description Search for content
141
+ * @param {SearchContentParams} params
142
+ * @returns {Promise<VideoItem[]>}
143
+ */
144
+ function searchContent(params: SearchContentParams): Promise<VideoItem[]>;
145
+
146
+ /**
147
+ * Search Module
148
+ */
149
+ type SearchContentType = typeof searchContent;
150
+ //#endregion search-module
151
+ ```
152
+
153
+ ### 3. 清除导出声明
154
+
155
+ 插件会自动清除构建输出中的导出声明,因为 Rex Widget 不支持脚本有导出声明。
156
+
157
+ ## 📚 完整示例
158
+
159
+ ### 项目结构
160
+
161
+ ```
162
+ my-widget/
163
+ ├── src/
164
+ │ ├── index.ts # 主要逻辑
165
+ │ └── rex-widget-env.d.ts # 自动生成的类型定义
166
+ ├── rslib.config.ts # 构建配置
167
+ ├── package.json
168
+ └── tsconfig.json
169
+ ```
170
+
171
+ ### 源代码示例
172
+
173
+ ```ts
174
+ // src/index.ts
175
+ WidgetMetadata = {
176
+ id: 'movie-search',
177
+ title: '电影搜索',
178
+ modules: [
179
+ {
180
+ id: 'search',
181
+ title: '搜索电影',
182
+ functionName: 'searchMovies',
183
+ description: '根据关键词搜索电影',
184
+ params: [
185
+ {
186
+ name: 'keyword',
187
+ title: '关键词',
188
+ type: 'input',
189
+ },
190
+ {
191
+ name: 'year',
192
+ title: '年份',
193
+ type: 'input',
194
+ },
195
+ {
196
+ name: 'genre',
197
+ title: '类型',
198
+ type: 'enumeration',
199
+ enumOptions: [
200
+ { title: '动作', value: 'action' },
201
+ { title: '喜剧', value: 'comedy' },
202
+ { title: '剧情', value: 'drama' },
203
+ ],
204
+ },
205
+ ],
206
+ },
207
+ ],
208
+ };
209
+
210
+ // 实现搜索函数
211
+ async function searchMovies(params: SearchMoviesParams): Promise<VideoItem[]> {
212
+ const { keyword, year, genre } = params;
213
+
214
+ // 使用 Widget API 进行搜索
215
+ const response = await Widget.http.get('https://api.example.com/search', {
216
+ params: { q: keyword, year, genre },
217
+ });
218
+
219
+ return response.data.map(item => ({
220
+ id: item.id,
221
+ title: item.title,
222
+ year: item.year,
223
+ // ... 其他字段
224
+ }));
225
+ }
226
+ ```
227
+
228
+ ### 生成的类型定义
229
+
230
+ ```ts
231
+ // src/rex-widget-env.d.ts (自动生成)
232
+ /// <reference types='@rexnow/libs/env' />
233
+
234
+ //#region search
235
+ /**
236
+ * Params of 搜索电影
237
+ */
238
+ interface SearchMoviesParams {
239
+ /**
240
+ * 关键词
241
+ */
242
+ keyword: string;
243
+ /**
244
+ * 年份
245
+ */
246
+ year: string;
247
+ /**
248
+ * 类型
249
+ */
250
+ genre: 'action' | 'comedy' | 'drama';
251
+ }
252
+
253
+ /**
254
+ * 搜索电影
255
+ * @description 根据关键词搜索电影
256
+ * @param {SearchMoviesParams} params
257
+ * @returns {Promise<VideoItem[]>}
258
+ */
259
+ function searchMovies(params: SearchMoviesParams): Promise<VideoItem[]>;
260
+
261
+ /**
262
+ * 搜索电影
263
+ */
264
+ type SearchMoviesType = typeof searchMovies;
265
+ //#endregion search
266
+ ```
267
+
268
+ ## 🎯 支持的参数类型
269
+
270
+ 插件支持以下参数类型的自动类型生成:
271
+
272
+ | 参数类型 | 描述 | 生成的 TypeScript 类型 |
273
+ |----------|------|----------------------|
274
+ | `input` | 输入框 | `string` |
275
+ | `enumeration` | 枚举选择 | `'option1' \| 'option2' \| ...` |
276
+ | `constant` | 常量值 | `'constantValue'` |
277
+
278
+ ## 🔧 开发模式
279
+
280
+ 在开发模式下,插件会监听文件变化并自动重新生成类型定义:
281
+
282
+ ```bash
283
+ # 启动开发模式
284
+ npm run dev
285
+ # 或
286
+ pnpm dev
287
+ ```
288
+
289
+ ## 🤝 最佳实践
290
+
291
+ 1. **保持 WidgetMetadata 结构清晰**:确保每个模块都有明确的 ID 和功能定义
292
+ 2. **使用描述性的函数名**:函数名应该清晰地表达其功能
293
+ 3. **提供完整的参数描述**:为每个参数提供有意义的标题和描述
294
+ 4. **合理组织模块**:将相关功能组织在一起,避免模块过于复杂
295
+
296
+ ## 📚 相关文档
297
+
298
+ - [@rexnow/libs](../libs/README.md) - 核心工具库
299
+ - [create-rex-widget](../create-rex-widget/README.md) - 脚手架工具
300
+ - [Rex Widget 开发指南](https://docs.forward-widget.com)
301
+
302
+ ## 🤝 贡献
303
+
304
+ 欢迎提交 Issue 和 Pull Request 来改善这个项目。
305
+
306
+ ## 📄 许可证
307
+
308
+ MIT License
@@ -0,0 +1,13 @@
1
+ /** @jsxImportSource hono/jsx */
2
+ import { Hono } from 'hono';
3
+ import type { HonoEnv } from './types.js';
4
+ interface FileInfo {
5
+ name: string;
6
+ size: number | null;
7
+ }
8
+ interface DirectoryProps {
9
+ fileInfos: FileInfo[];
10
+ }
11
+ export declare const Directory: ({ fileInfos }: DirectoryProps) => import("hono/jsx/jsx-dev-runtime").JSX.Element;
12
+ export declare const directoryRouter: Hono<HonoEnv, import("hono/types").BlankSchema, "/">;
13
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { RsbuildPluginAPI } from '@rsbuild/core';
2
+ export interface DevServerOptions {
3
+ api: RsbuildPluginAPI;
4
+ port: number;
5
+ }
6
+ export declare const createDevServer: (options: DevServerOptions) => Promise<import("@hono/node-server").ServerType>;
@@ -0,0 +1,6 @@
1
+ import type { Env } from 'hono';
2
+ export interface HonoEnv extends Env {
3
+ Variables: {
4
+ distPath: string;
5
+ };
6
+ }
@@ -0,0 +1,234 @@
1
+ import { networkInterfaces } from "node:os";
2
+ import { serve } from "@hono/node-server";
3
+ import { serveStatic } from "@hono/node-server/serve-static";
4
+ import { Hono } from "hono";
5
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
6
+ import { readdir, stat } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ const formatFileSize = (size)=>{
9
+ if (null === size) return '';
10
+ if (size < 1024) return `${size} B`;
11
+ if (size < 1048576) return `${(size / 1024).toFixed(1)} KB`;
12
+ if (size < 1073741824) return `${(size / 1048576).toFixed(1)} MB`;
13
+ return `${(size / 1073741824).toFixed(1)} GB`;
14
+ };
15
+ const FileItem = ({ fileInfo })=>{
16
+ const sizeText = formatFileSize(fileInfo.size);
17
+ return /*#__PURE__*/ jsx("li", {
18
+ class: "file-item",
19
+ children: /*#__PURE__*/ jsxs("a", {
20
+ href: `/${fileInfo.name}`,
21
+ class: "file-link",
22
+ children: [
23
+ /*#__PURE__*/ jsx("span", {
24
+ class: "file-name",
25
+ children: fileInfo.name
26
+ }),
27
+ /*#__PURE__*/ jsx("span", {
28
+ class: "file-size",
29
+ children: sizeText
30
+ })
31
+ ]
32
+ })
33
+ });
34
+ };
35
+ const Directory = ({ fileInfos })=>/*#__PURE__*/ jsxs("html", {
36
+ lang: "zh-CN",
37
+ children: [
38
+ /*#__PURE__*/ jsxs("head", {
39
+ children: [
40
+ /*#__PURE__*/ jsx("meta", {
41
+ charset: "utf-8"
42
+ }),
43
+ /*#__PURE__*/ jsx("title", {
44
+ children: "Rex Widget - 文件列表"
45
+ }),
46
+ /*#__PURE__*/ jsx("style", {
47
+ children: `
48
+ * { margin: 0; padding: 0; box-sizing: border-box; }
49
+ :root {
50
+ --bg-1: #000e1a;
51
+ --bg-2: #071221;
52
+ --bg-3: #00383d;
53
+ --card-bg: rgba(20, 20, 22, 0.55);
54
+ --card-border: rgba(255, 255, 255, 0.08);
55
+ --divider: rgba(255, 255, 255, 0.08);
56
+ --text-primary: #ffffff;
57
+ --text-secondary: rgba(255, 255, 255, 0.6);
58
+ --text-tertiary: rgba(255, 255, 255, 0.5);
59
+ --hover-bg: rgba(255, 255, 255, 0.06);
60
+ }
61
+ body {
62
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
63
+ background: linear-gradient(135deg, var(--bg-1) 0%, var(--bg-2) 50%, var(--bg-3) 100%);
64
+ min-height: 100vh;
65
+ color: var(--text-primary);
66
+ padding: calc(env(safe-area-inset-top, 0px) + 12px) 16px calc(env(safe-area-inset-bottom, 0px) + 16px);
67
+ }
68
+ .container {
69
+ max-width: 800px;
70
+ margin: 0 auto;
71
+ }
72
+ .header {
73
+ padding: 24px 0 12px;
74
+ }
75
+ h1 {
76
+ color: var(--text-primary);
77
+ font-size: 32px;
78
+ font-weight: 700;
79
+ margin-bottom: 6px;
80
+ text-align: left;
81
+ }
82
+ .content {
83
+ padding: 0;
84
+ }
85
+ .file-list {
86
+ list-style: none;
87
+ background: var(--card-bg);
88
+ border-radius: 18px;
89
+ overflow: hidden;
90
+ border: 1px solid var(--card-border);
91
+ backdrop-filter: blur(8px);
92
+ }
93
+ .file-item {
94
+ display: flex;
95
+ align-items: center;
96
+ padding: 18px 16px;
97
+ border-bottom: 1px solid var(--divider);
98
+ transition: background-color 0.2s ease;
99
+ }
100
+ .file-item:last-child {
101
+ border-bottom: none;
102
+ }
103
+ .file-item:hover {
104
+ background-color: var(--hover-bg);
105
+ }
106
+ .file-link {
107
+ text-decoration: none;
108
+ color: var(--text-primary);
109
+ display: flex;
110
+ align-items: center;
111
+ flex: 1;
112
+ font-size: 16px;
113
+ font-weight: 400;
114
+ }
115
+ .file-name {
116
+ flex: 1;
117
+ margin-right: 8px;
118
+ overflow: hidden;
119
+ text-overflow: ellipsis;
120
+ white-space: nowrap;
121
+ }
122
+ .file-size {
123
+ color: var(--text-tertiary);
124
+ font-size: 14px;
125
+ font-weight: 400;
126
+ min-width: 72px;
127
+ text-align: right;
128
+ margin-right: 8px;
129
+ }
130
+ .empty-message {
131
+ text-align: center;
132
+ color: var(--text-tertiary);
133
+ padding: 60px 20px;
134
+ font-size: 16px;
135
+ }
136
+ .section-title {
137
+ color: var(--text-secondary);
138
+ font-size: 13px;
139
+ font-weight: 400;
140
+ text-transform: uppercase;
141
+ letter-spacing: 0.5px;
142
+ margin: 30px 0 8px 0;
143
+ text-align: left;
144
+ }
145
+ `
146
+ })
147
+ ]
148
+ }),
149
+ /*#__PURE__*/ jsx("body", {
150
+ children: /*#__PURE__*/ jsxs("div", {
151
+ class: "container",
152
+ children: [
153
+ /*#__PURE__*/ jsx("div", {
154
+ class: "header",
155
+ children: /*#__PURE__*/ jsx("h1", {
156
+ children: "文件列表"
157
+ })
158
+ }),
159
+ /*#__PURE__*/ jsx("div", {
160
+ class: "content",
161
+ children: 0 === fileInfos.length ? /*#__PURE__*/ jsx("div", {
162
+ class: "empty-message",
163
+ children: "\uD83D\uDCED 此目录为空"
164
+ }) : /*#__PURE__*/ jsxs(Fragment, {
165
+ children: [
166
+ /*#__PURE__*/ jsx("div", {
167
+ class: "section-title",
168
+ children: "文件"
169
+ }),
170
+ /*#__PURE__*/ jsx("ul", {
171
+ class: "file-list",
172
+ children: fileInfos.map((fileInfo)=>/*#__PURE__*/ jsx(FileItem, {
173
+ fileInfo: fileInfo
174
+ }, fileInfo.name))
175
+ })
176
+ ]
177
+ })
178
+ })
179
+ ]
180
+ })
181
+ })
182
+ ]
183
+ });
184
+ const directoryRouter = new Hono();
185
+ directoryRouter.get('/', async (c)=>{
186
+ const distPath = c.get('distPath');
187
+ const files = await readdir(distPath);
188
+ const fileInfos = await Promise.all(files.map(async (file)=>{
189
+ const filePath = join(distPath, file);
190
+ const stats = await stat(filePath);
191
+ return {
192
+ name: file,
193
+ size: stats.isFile() ? stats.size : null
194
+ };
195
+ }));
196
+ return c.html(/*#__PURE__*/ jsx(Directory, {
197
+ fileInfos: fileInfos
198
+ }));
199
+ });
200
+ const getLocalIPs = ()=>{
201
+ const interfaces = networkInterfaces();
202
+ const ips = [];
203
+ for (const name of Object.keys(interfaces)){
204
+ const nets = interfaces[name];
205
+ if (nets) {
206
+ for (const net of nets)if ('IPv4' === net.family && !net.internal) ips.push(net.address);
207
+ }
208
+ }
209
+ return ips;
210
+ };
211
+ const createDevServer = async (options)=>{
212
+ const { api, port } = options;
213
+ const app = new Hono();
214
+ app.use('*', serveStatic({
215
+ root: api.context.distPath
216
+ }));
217
+ app.use('*', (c, next)=>{
218
+ c.set('distPath', api.context.distPath);
219
+ return next();
220
+ });
221
+ app.route('/', directoryRouter);
222
+ const server = serve({
223
+ fetch: app.fetch,
224
+ port
225
+ });
226
+ const localIPs = getLocalIPs();
227
+ api.logger.ready("Rex Widget 插件已启动,监听地址");
228
+ api.logger.info(` http://localhost:${port}`);
229
+ if (localIPs.length > 0) localIPs.forEach((ip)=>{
230
+ api.logger.info(` http://${ip}:${port}`);
231
+ });
232
+ return server;
233
+ };
234
+ export { createDevServer };
@@ -0,0 +1,5 @@
1
+ import type { SourceFile } from "ts-morph";
2
+ /**
3
+ * 生成弹幕模块接口
4
+ */
5
+ export declare function generateDanmuModuleInterfaces(nameSpaceName: string, sourceFile: SourceFile, module: WidgetModule): void;
@@ -0,0 +1,2 @@
1
+ import type { SourceFile } from "ts-morph";
2
+ export declare function generateStreamModuleInterface(nameSpaceName: string, sourceFile: SourceFile, module: WidgetModule): void;
@@ -0,0 +1,2 @@
1
+ import type { SourceFile } from "ts-morph";
2
+ export declare function generateSubtitleModuleInterface(nameSpaceName: string, sourceFile: SourceFile, module: WidgetModule): void;
@@ -0,0 +1,5 @@
1
+ import type { SourceFile } from 'ts-morph';
2
+ /**
3
+ * 生成 Video 模块接口
4
+ */
5
+ export declare function generateVideoModuleInterface(nameSpaceName: string, sourceFile: SourceFile, module: WidgetModule): void;
@@ -0,0 +1,15 @@
1
+ import type { RsbuildPlugin } from "@rsbuild/core";
2
+ interface RexWidgetPluginOptions {
3
+ /**
4
+ * 生成的 dts 文件路径
5
+ * @default `src/rex-widget-env.d.ts`
6
+ */
7
+ typesFilePath?: string;
8
+ /**
9
+ * 监听端口
10
+ * @default 8000
11
+ */
12
+ devPort?: number;
13
+ }
14
+ export declare const pluginRexWidget: ({ typesFilePath, devPort, }?: RexWidgetPluginOptions) => RsbuildPlugin;
15
+ export {};