@pisell/rsbuild-plugin-lowcode
Advanced tools
| 'use strict'; | ||
| const logger = require('../logger'); | ||
| const { startInjectServer } = require('./server'); | ||
| const { writeInjectInfo } = require('./registry'); | ||
| const { openBrowser } = require('./utils'); | ||
| const DEFAULT_ALT_OPEN_URL = | ||
| 'https://lowcode-engine.cn/demo/demo-general/index.html?debug'; | ||
| function getUrlProtocol(url) { | ||
| try { | ||
| return new URL(url).protocol; | ||
| } catch (_) { | ||
| return ''; | ||
| } | ||
| } | ||
| async function startAltMode({ | ||
| pkg, | ||
| host, | ||
| port, | ||
| library, | ||
| protocol, | ||
| serverHttps, | ||
| altConfig = {}, | ||
| }) { | ||
| const openUrl = altConfig.openUrl || DEFAULT_ALT_OPEN_URL; | ||
| const injectPort = altConfig.port || 8899; | ||
| writeInjectInfo({ | ||
| pkg, | ||
| host, | ||
| port, | ||
| library, | ||
| }); | ||
| await startInjectServer(injectPort); | ||
| logger.hint('alt mode ', 'enabled'); | ||
| logger.hint('inject ', `http://0.0.0.0:${injectPort}/apis/injectInfo`); | ||
| logger.hint('view url ', `${protocol}://${host}:${port}/view.js`); | ||
| logger.hint('meta url ', `${protocol}://${host}:${port}/meta.js`); | ||
| if (getUrlProtocol(openUrl) === 'https:' && !serverHttps) { | ||
| logger.warn( | ||
| 'Alt HMR from an HTTPS designer requires `server.https: true`; HTTP dev servers cannot accept the browser\'s `wss://` reconnect fallback.', | ||
| ); | ||
| } | ||
| if (altConfig.openBrowser !== false) { | ||
| await openBrowser(openUrl); | ||
| } | ||
| } | ||
| module.exports = { startAltMode }; |
| 'use strict'; | ||
| /** | ||
| * inject/registry.js | ||
| * | ||
| * 管理 ~/.altrc/inject.json 文件,用于注册本地开发中的组件信息。 | ||
| * | ||
| * 功能: | ||
| * - 写入组件注册信息到 ~/.altrc/inject.json | ||
| * - 读取所有已注册的组件信息 | ||
| * - 支持多个项目同时运行 (通过端口区分) | ||
| */ | ||
| const os = require('os'); | ||
| const path = require('path'); | ||
| const http = require('http'); | ||
| const https = require('https'); | ||
| const fse = require('fs-extra'); | ||
| /** | ||
| * 获取 inject.json 文件路径 | ||
| */ | ||
| function getInjectFilePath() { | ||
| return path.join(os.homedir(), '.altrc', 'inject.json'); | ||
| } | ||
| /** | ||
| * 检查 URL 是否可访问 | ||
| * @param {string} url - 要检查的 URL | ||
| * @returns {Promise<boolean>} - 是否可访问 | ||
| */ | ||
| function checkUrlAccessible(url) { | ||
| return new Promise((resolve) => { | ||
| try { | ||
| const urlObj = new URL(url); | ||
| const client = urlObj.protocol === 'https:' ? https : http; | ||
| const req = client.request( | ||
| { | ||
| method: 'HEAD', | ||
| hostname: urlObj.hostname, | ||
| port: urlObj.port, | ||
| path: urlObj.pathname + urlObj.search, | ||
| timeout: 1000, | ||
| }, | ||
| (res) => { | ||
| resolve(res.statusCode >= 200 && res.statusCode < 400); | ||
| } | ||
| ); | ||
| req.on('error', () => resolve(false)); | ||
| req.on('timeout', () => { | ||
| req.destroy(); | ||
| resolve(false); | ||
| }); | ||
| req.end(); | ||
| } catch (e) { | ||
| resolve(false); | ||
| } | ||
| }); | ||
| } | ||
| /** | ||
| * 写入组件注册信息 | ||
| * @param {object} options | ||
| * @param {object} options.pkg - package.json 内容 | ||
| * @param {string} options.host - 主机地址 (127.0.0.1 或内网 IP) | ||
| * @param {number} options.port - dev server 端口 | ||
| * @param {string} options.library - UMD library 名称 | ||
| */ | ||
| function writeInjectInfo({ pkg, host, port, library }) { | ||
| const filePath = getInjectFilePath(); | ||
| fse.ensureFileSync(filePath); | ||
| let cache = {}; | ||
| try { | ||
| cache = JSON.parse(fse.readFileSync(filePath, 'utf-8')); | ||
| } catch (e) { | ||
| // 文件不存在或格式错误,使用空对象 | ||
| } | ||
| // 注册 view (组件实现) | ||
| cache[`${port}-view`] = { | ||
| packageName: pkg.name, | ||
| library, | ||
| type: 'view', | ||
| url: `http://${host}:${port}/view.js?name=${pkg.name}`, | ||
| }; | ||
| // 注册 meta (组件元数据) | ||
| cache[`${port}-meta`] = { | ||
| packageName: pkg.name, | ||
| // lowcode-plugin-inject 会按 packageName 聚合 view/meta 记录,并只保留一个 library 字段; | ||
| // 这里必须与 view 使用同一个 library,避免把组件实现误指向 `${library}Meta`。 | ||
| library, | ||
| type: 'meta', | ||
| url: `http://${host}:${port}/meta.js?name=${pkg.name}`, | ||
| }; | ||
| fse.writeFileSync(filePath, JSON.stringify(cache, null, 2)); | ||
| } | ||
| /** | ||
| * 读取所有已注册的组件信息,并检查 URL 可访问性 | ||
| * @returns {Promise<Array>} - 可访问的组件信息列表 | ||
| */ | ||
| async function readInjectInfo() { | ||
| const filePath = getInjectFilePath(); | ||
| try { | ||
| const data = JSON.parse(fse.readFileSync(filePath, 'utf-8')); | ||
| const items = Object.values(data); | ||
| // 检查每个 URL 是否可访问 | ||
| const checkResults = await Promise.all( | ||
| items.map(async (item) => { | ||
| const isAccessible = await checkUrlAccessible(item.url); | ||
| return isAccessible ? item : null; | ||
| }) | ||
| ); | ||
| return checkResults.filter(Boolean); | ||
| } catch (e) { | ||
| return []; | ||
| } | ||
| } | ||
| module.exports = { writeInjectInfo, readInjectInfo, getInjectFilePath }; |
| 'use strict'; | ||
| /** | ||
| * inject/server.js | ||
| * | ||
| * 提供 Inject Server (原生 http),用于低代码引擎调试模式下的物料注入。 | ||
| * | ||
| * 功能: | ||
| * - 启动 HTTP 服务器监听指定端口 (默认 8899) | ||
| * - 提供 JSONP 接口 /apis/injectInfo | ||
| * - 返回当前本地开发中的组件信息 | ||
| * - 支持 CORS | ||
| */ | ||
| const http = require('http'); | ||
| const { URL } = require('url'); | ||
| const net = require('net'); | ||
| const { readInjectInfo } = require('./registry'); | ||
| const logger = require('../logger'); | ||
| /** | ||
| * 检查端口是否被占用 | ||
| */ | ||
| async function checkPort(port, host = '0.0.0.0') { | ||
| return new Promise((resolve) => { | ||
| const server = net.createServer().listen(port, host); | ||
| server.on('listening', () => { | ||
| server.close(); | ||
| resolve(false); | ||
| }); | ||
| server.on('error', () => { | ||
| resolve(true); | ||
| }); | ||
| }); | ||
| } | ||
| /** | ||
| * 启动 Inject Server | ||
| * @param {number} port - 端口号 | ||
| * @returns {Promise<Server|null>} - 返回 server 实例或 null (端口已占用) | ||
| */ | ||
| async function startInjectServer(port = 8899) { | ||
| const isOccupied = await checkPort(port); | ||
| if (isOccupied) { | ||
| logger.info(`Inject server already running on port ${port}`); | ||
| return null; | ||
| } | ||
| const server = http.createServer(async (req, res) => { | ||
| // 设置 CORS 头 | ||
| res.setHeader('Access-Control-Allow-Origin', '*'); | ||
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With, sessionToken'); | ||
| res.setHeader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); | ||
| // 处理 OPTIONS 预检请求 | ||
| if (req.method === 'OPTIONS') { | ||
| res.writeHead(204); | ||
| res.end(); | ||
| return; | ||
| } | ||
| // 解析 URL | ||
| const parsedUrl = new URL(req.url, `http://${req.headers.host}`); | ||
| // 仅处理 /apis/injectInfo 路径 | ||
| if (parsedUrl.pathname === '/apis/injectInfo' && req.method === 'GET') { | ||
| try { | ||
| const callback = parsedUrl.searchParams.get('callback') || 'callback'; | ||
| const injectInfo = await readInjectInfo(); | ||
| // 返回 JSONP 格式 | ||
| const jsonpResponse = `;${callback}(${JSON.stringify({ | ||
| success: injectInfo.length > 0, | ||
| content: injectInfo, | ||
| })})`; | ||
| res.setHeader('Content-Type', 'text/javascript; charset=utf-8'); | ||
| res.writeHead(200); | ||
| res.end(jsonpResponse); | ||
| } catch (error) { | ||
| logger.error('Failed to read inject info:', error.message); | ||
| res.writeHead(500); | ||
| res.end('Internal Server Error'); | ||
| } | ||
| } else { | ||
| // 404 | ||
| res.writeHead(404); | ||
| res.end('Not Found'); | ||
| } | ||
| }); | ||
| return new Promise((resolve) => { | ||
| server.listen(port, '0.0.0.0', () => { | ||
| logger.success(`Inject server started on http://0.0.0.0:${port}`); | ||
| resolve(server); | ||
| }); | ||
| }); | ||
| } | ||
| module.exports = { startInjectServer }; |
| 'use strict'; | ||
| /** | ||
| * inject/utils.js | ||
| * | ||
| * Inject 模式的工具函数。 | ||
| * | ||
| * 功能: | ||
| * - 获取本机内网 IP 地址 | ||
| * - 打开浏览器 | ||
| */ | ||
| const os = require('os'); | ||
| const open = require('open'); | ||
| const logger = require('../logger'); | ||
| /** | ||
| * 获取本机内网 IP 地址 | ||
| * @returns {string} - 内网 IP,如果获取失败返回 127.0.0.1 | ||
| */ | ||
| function getPrivateIp() { | ||
| const interfaces = os.networkInterfaces(); | ||
| for (const name of Object.keys(interfaces)) { | ||
| for (const iface of interfaces[name]) { | ||
| // 跳过内部地址和非 IPv4 地址 | ||
| if (iface.family === 'IPv4' && !iface.internal) { | ||
| return iface.address; | ||
| } | ||
| } | ||
| } | ||
| return '127.0.0.1'; | ||
| } | ||
| /** | ||
| * 打开浏览器 | ||
| * @param {string} url - 要打开的 URL | ||
| */ | ||
| async function openBrowser(url) { | ||
| try { | ||
| await open(url); | ||
| logger.info(`Browser opened: ${url}`); | ||
| } catch (e) { | ||
| logger.error('Failed to open browser:', e.message); | ||
| } | ||
| } | ||
| module.exports = { getPrivateIp, openBrowser }; |
@@ -29,2 +29,5 @@ import { defineConfig } from '@rsbuild/core'; | ||
| }, | ||
| alt: { | ||
| enabled: true, | ||
| }, | ||
| builtinAssets: [ | ||
@@ -31,0 +34,0 @@ { |
+2
-1
| { | ||
| "name": "@pisell/rsbuild-plugin-lowcode", | ||
| "version": "0.0.3", | ||
| "version": "0.0.4", | ||
| "description": "Rsbuild plugin for low-code material development and build workflows.", | ||
@@ -35,2 +35,3 @@ "main": "index.js", | ||
| "lodash": "^4.17.21", | ||
| "open": "^8.4.0", | ||
| "picocolors": "^1.1.1" | ||
@@ -37,0 +38,0 @@ }, |
+45
-1
@@ -7,2 +7,3 @@ # @pisell/rsbuild-plugin-lowcode | ||
| - build 阶段多环境并行编译 | ||
| - **调试模式**:支持将本地组件注入到线上低代码引擎进行实时调试 | ||
@@ -39,5 +40,43 @@ ## 安装 | ||
| ## Alt 调试模式 | ||
| Alt 调试模式允许你在开发组件时,将本地正在开发的组件实时注入到线上低代码引擎中进行调试,无需发布即可测试组件效果。 | ||
| ### 配置 | ||
| ```js | ||
| export default defineConfig({ | ||
| server: { | ||
| port: 3000, // dev server 端口 | ||
| https: true, // 线上 HTTPS 设计器调试时建议开启,供 HMR 使用 wss:// | ||
| }, | ||
| plugins: [ | ||
| pluginLowcode({ | ||
| library: 'MyComponent', | ||
| // Alt 调试模式配置 | ||
| alt: { | ||
| enabled: true, // 启用 alt 调试模式 | ||
| port: 8899, // inject server 端口(默认 8899) | ||
| openBrowser: true, // 自动打开浏览器(默认 true) | ||
| openUrl: 'https://lowcode-engine.cn/demo/demo-general/index.html?debug', | ||
| usePrivateIp: false, // 使用内网 IP(默认 false,使用 127.0.0.1) | ||
| }, | ||
| }), | ||
| ], | ||
| }); | ||
| ``` | ||
| ### 配置项说明 | ||
| | 配置项 | 类型 | 默认值 | 说明 | | ||
| |--------|------|--------|------| | ||
| | `alt.enabled` | `boolean` | `false` | 是否启用 alt 调试模式 | | ||
| | `alt.port` | `number` | `8899` | inject server 监听端口 | | ||
| | `alt.openBrowser` | `boolean` | `true` | 是否自动打开浏览器 | | ||
| | `alt.openUrl` | `string` | `https://lowcode-engine.cn/demo/demo-general/index.html?debug` | 自动打开的 URL | | ||
| | `alt.usePrivateIp` | `boolean` | `false` | 是否使用内网 IP(用于跨设备调试) | | ||
| ## 目录结构 | ||
| ```text | ||
@@ -53,2 +92,7 @@ . | ||
| │ ├── assets.js | ||
| │ ├── inject/ # Alt 调试模式 | ||
| │ │ ├── bootstrap.js # Alt 模式启动编排 | ||
| │ │ ├── server.js # Inject server (原生 http) | ||
| │ │ ├── registry.js # 注册信息管理 | ||
| │ │ └── utils.js # 工具函数 | ||
| │ ├── utils/ | ||
@@ -55,0 +99,0 @@ │ ├── templates/ |
+50
-22
| 'use strict'; | ||
| /** | ||
| * rsbuild-plugin-lowcode/plugin.js | ||
| * | ||
| * 将原 lowcode/index.js 中的 build() / start() 编排逻辑 | ||
| * 改造为 rsbuild plugin 形式。 | ||
| * | ||
| * 生命周期职责: | ||
| * modifyRsbuildConfig — 确认 metaTypes/platforms、生成 .tmp/ 入口文件、 | ||
| * 注入 environments(build: 多环境并行;dev: 单一环境) | ||
| * onBeforeBuild — 清空输出目录(仅 build 模式) | ||
| * onAfterBuild — 生成 assets JSON 并拷贝(仅 build 模式) | ||
| * onAfterStartDevServer — 打印访问地址、启动 chokidar 监听(仅 dev 模式) | ||
| * | ||
| * 用法(直接集成到 rsbuild.config.ts): | ||
| * plugins: [pluginLowcode({ library: 'MyLib', engineScope: '@alilc' })] | ||
| * | ||
| * mode / port / https / alias 均从 rsbuild 自身配置中读取,无需在 pluginOptions 重复定义。 | ||
| */ | ||
| const path = require('path'); | ||
@@ -40,2 +21,3 @@ const fse = require('fs-extra'); | ||
| const { buildBuildEnvironments, buildDevEnvironment } = require('./environments'); | ||
| const { startAltMode } = require('./inject/bootstrap'); | ||
| const logger = require('./logger'); | ||
@@ -75,2 +57,13 @@ | ||
| function getAltHost(altConfig) { | ||
| if (!altConfig?.enabled) { | ||
| return undefined; | ||
| } | ||
| if (altConfig.usePrivateIp) { | ||
| const { getPrivateIp } = require('./inject/utils'); | ||
| return getPrivateIp(); | ||
| } | ||
| return '127.0.0.1'; | ||
| } | ||
| // ─── Plugin ─────────────────────────────────────────────────────────────────── | ||
@@ -90,2 +83,8 @@ | ||
| * @param {string[]} [pluginOptions.metaTypes] - meta 类型列表 | ||
| * @param {object} [pluginOptions.alt] - Alt 调试模式配置(仅 dev 模式生效) | ||
| * @param {boolean} [pluginOptions.alt.enabled] - 启用 alt 调试模式 | ||
| * @param {number} [pluginOptions.alt.port] - inject server 端口,默认 8899 | ||
| * @param {boolean} [pluginOptions.alt.openBrowser] - 自动打开浏览器,默认 true | ||
| * @param {string} [pluginOptions.alt.openUrl] - 自动打开的 URL | ||
| * @param {boolean} [pluginOptions.alt.usePrivateIp] - 使用内网 IP,默认 false (127.0.0.1) | ||
| */ | ||
@@ -131,2 +130,13 @@ function pluginLowcode(pluginOptions = {}) { | ||
| const serverHttps = !!(config.server?.https); | ||
| const altConfig = pluginOptions.alt || {}; | ||
| const altHost = getAltHost(altConfig); | ||
| const userDevClient = config.dev?.client || {}; | ||
| const altDevClient = isDev && altConfig.enabled | ||
| ? { | ||
| protocol: userDevClient.protocol || (serverHttps ? 'wss' : 'ws'), | ||
| host: userDevClient.host || altHost, | ||
| port: userDevClient.port || '<port>', | ||
| path: userDevClient.path || '/rsbuild-hmr', | ||
| } | ||
| : undefined; | ||
@@ -172,3 +182,3 @@ // 1. 确认实际存在的 metaTypes 和 render platforms | ||
| indexEntryPath, previewEntryPath, metaPathMap, | ||
| serverPort, serverHttps, | ||
| serverPort, serverHttps, altHost, | ||
| }; | ||
@@ -210,3 +220,6 @@ | ||
| tools: { postcss: buildPostcss() }, | ||
| dev: { progressBar: true }, | ||
| dev: { | ||
| progressBar: true, | ||
| ...(altDevClient && { client: altDevClient }), | ||
| }, | ||
| environments, | ||
@@ -278,3 +291,3 @@ ...(isDev && { | ||
| api.onAfterStartDevServer(async (serverInfo = {}) => { | ||
| const { rootDir, pkg, lib, serverPort, serverHttps } = state; | ||
| const { rootDir, pkg, lib, serverPort, serverHttps, altHost } = state; | ||
| const actualPort = serverInfo?.port || serverPort; | ||
@@ -286,2 +299,17 @@ const protocol = serverHttps ? 'https' : 'http'; | ||
| // Alt 调试模式 | ||
| const altConfig = pluginOptions.alt || {}; | ||
| if (altConfig.enabled) { | ||
| const host = altHost || '127.0.0.1'; | ||
| await startAltMode({ | ||
| pkg, | ||
| host, | ||
| port: actualPort, | ||
| library: lib, | ||
| protocol, | ||
| serverHttps, | ||
| altConfig, | ||
| }); | ||
| } | ||
| // 监听 lowcode/** 变化,重新生成入口文件 | ||
@@ -288,0 +316,0 @@ // rsbuild HMR 会自动检测 .tmp/ 文件更新并重新编译 |
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
125412
10.04%27
17.39%2925
12.2%102
75.86%8
14.29%14
7.69%11
57.14%+ Added
+ Added
+ Added
+ Added
+ Added