🚀. Socket Launch Week Day 3:Socket Firewall Now Blocks Malicious VS Code and Open VSX Extensions.Learn more
Sign In

@pisell/rsbuild-plugin-lowcode

Package Overview
Dependencies
Maintainers
8
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@pisell/rsbuild-plugin-lowcode - npm Package Compare versions

Comparing version
0.0.3
to
0.0.4
+56
src/inject/bootstrap.js
'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 };
+3
-0

@@ -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 @@ },

@@ -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/

'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/ 文件更新并重新编译