common: - 新增 autoInit.ts 自动初始化(对齐 Android ContentProvider 模式) - 新增 configCrypto.ts 内置配置文件解密 - XuqmSDK 新增 initWithConfigFile / setUserInfo / getUserInfo - 新增 crypto-types.d.ts Web Crypto 类型声明 update: - 重写 UpdateSDK:checkAppUpdate / checkPluginUpdate / checkAndCachePlugin - 移除 checkAndPromptAppUpdate(SDK 不做 UI) - 新增插件脚手架 create-plugin.mjs - 重命名 RnUpdateInfo → PluginUpdateInfo license: - crypto.ts 支持 XUQM-CONFIG-V1 + XUQM-LICENSE-V1 双格式 - 新增 decryptConfigFile 导出 docs: - 重写 README.md - 新增 docs/SDK-API参考.md - 新增 docs/插件脚手架.md - 新增 docs/配置文件规范.md
428 行
15 KiB
JavaScript
428 行
15 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* create-plugin.mjs — UpdateSDK 插件脚手架工具
|
||
*
|
||
* 用法:
|
||
* node scripts/create-plugin.mjs
|
||
* npx @xuqm/rn-update create-plugin
|
||
*
|
||
* 功能:
|
||
* 1. 交互式输入插件参数(moduleId / title / subtitle / accentColor)
|
||
* 2. 校验 moduleId 唯一性
|
||
* 3. 自动生成完整插件骨架(bundle.ts / Screen / plugin.json)
|
||
* 4. 自动注册到宿主(pluginCatalog / debugPlugins / build scripts / metro config)
|
||
*/
|
||
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import readline from 'readline';
|
||
|
||
// ─── 交互工具 ────────────────────────────────────────────────────────────────
|
||
|
||
function createRL() {
|
||
return readline.createInterface({
|
||
input: process.stdin,
|
||
output: process.stdout,
|
||
});
|
||
}
|
||
|
||
function ask(rl, question) {
|
||
return new Promise(resolve => {
|
||
rl.question(question, answer => resolve(answer.trim()));
|
||
});
|
||
}
|
||
|
||
// ─── 项目路径检测 ────────────────────────────────────────────────────────────
|
||
|
||
function findProjectRoot() {
|
||
let dir = process.cwd();
|
||
while (dir !== path.dirname(dir)) {
|
||
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
||
dir = path.dirname(dir);
|
||
}
|
||
console.error('❌ 未找到 package.json,请在 RN 项目根目录下运行此脚本。');
|
||
process.exit(1);
|
||
}
|
||
|
||
// ─── 唯一性校验 ──────────────────────────────────────────────────────────────
|
||
|
||
function getExistingPluginIds(root) {
|
||
const ids = new Set();
|
||
|
||
// 从 pluginCatalog.ts 读取
|
||
const catalogPath = path.join(root, 'src/app/pluginCatalog.ts');
|
||
if (fs.existsSync(catalogPath)) {
|
||
const content = fs.readFileSync(catalogPath, 'utf-8');
|
||
const matches = content.matchAll(/id:\s*'([^']+)'/g);
|
||
for (const m of matches) ids.add(m[1]);
|
||
}
|
||
|
||
// 从 plugins 目录读取
|
||
const pluginsDir = path.join(root, 'src/plugins');
|
||
if (fs.existsSync(pluginsDir)) {
|
||
for (const entry of fs.readdirSync(pluginsDir)) {
|
||
const full = path.join(pluginsDir, entry);
|
||
if (fs.statSync(full).isDirectory()) ids.add(entry);
|
||
}
|
||
}
|
||
|
||
return ids;
|
||
}
|
||
|
||
// ─── 文件生成 ────────────────────────────────────────────────────────────────
|
||
|
||
function pascalCase(str) {
|
||
return str
|
||
.replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
|
||
.replace(/^(\w)/, (_, c) => c.toUpperCase());
|
||
}
|
||
|
||
function generateBundleTs(moduleId, title, subtitle, accentColor) {
|
||
const screenName = `${pascalCase(moduleId)}Screen`;
|
||
return `import { UpdateSDK } from '@xuqm/rn-update';
|
||
import { registerPluginFromBridge } from '@plugins/runtimeBridge';
|
||
import { ${screenName} } from '@${moduleId}/${screenName}';
|
||
|
||
// 自动注册插件版本(bundle 加载时执行)
|
||
UpdateSDK.registerPlugin({ moduleId: '${moduleId}', version: '1.0.0' });
|
||
|
||
// 注册 UI 组件到宿主运行时
|
||
registerPluginFromBridge({
|
||
id: '${moduleId}',
|
||
title: '${title}',
|
||
subtitle: '${subtitle}',
|
||
accentColor: '${accentColor}',
|
||
Component: ${screenName},
|
||
});
|
||
`;
|
||
}
|
||
|
||
function generateScreenTs(moduleId) {
|
||
const screenName = `${pascalCase(moduleId)}Screen`;
|
||
return `import { StyleSheet, Text, View } from 'react-native';
|
||
|
||
export function ${screenName}() {
|
||
return (
|
||
<View style={styles.container}>
|
||
<Text style={styles.title}>${moduleId}</Text>
|
||
<Text style={styles.subtitle}>TODO: 实现${moduleId}功能</Text>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5F7FA' },
|
||
title: { fontSize: 24, fontWeight: '700', color: '#17171A' },
|
||
subtitle: { fontSize: 14, color: '#666666', marginTop: 8 },
|
||
});
|
||
`;
|
||
}
|
||
|
||
function generatePluginJson(moduleId) {
|
||
return JSON.stringify({ moduleId, version: '1.0.0' }, null, 2) + '\n';
|
||
}
|
||
|
||
// ─── 宿主文件更新 ────────────────────────────────────────────────────────────
|
||
|
||
function updatePluginCatalog(root, moduleId, title, accentColor) {
|
||
const filePath = path.join(root, 'src/app/pluginCatalog.ts');
|
||
if (!fs.existsSync(filePath)) {
|
||
console.warn('⚠️ src/app/pluginCatalog.ts 不存在,跳过');
|
||
return;
|
||
}
|
||
|
||
let content = fs.readFileSync(filePath, 'utf-8');
|
||
|
||
// 检查是否已存在
|
||
if (content.includes(`id: '${moduleId}'`)) {
|
||
console.log(` ℹ️ pluginCatalog 已包含 ${moduleId},跳过`);
|
||
return;
|
||
}
|
||
|
||
const newEntry = ` {
|
||
id: '${moduleId}',
|
||
title: '${title}',
|
||
summary: '${title}业务模块',
|
||
description: '点击后异步加载 ${moduleId} bundle。',
|
||
accentColor: '${accentColor}',
|
||
},`;
|
||
|
||
// 在最后一个 ] 之前插入
|
||
const lastBracket = content.lastIndexOf('];');
|
||
if (lastBracket === -1) {
|
||
console.warn('⚠️ pluginCatalog.ts 格式异常,跳过');
|
||
return;
|
||
}
|
||
|
||
content =
|
||
content.slice(0, lastBracket) + newEntry + '\n' + content.slice(lastBracket);
|
||
fs.writeFileSync(filePath, content, 'utf-8');
|
||
}
|
||
|
||
function updateDebugPlugins(root, moduleId) {
|
||
const filePath = path.join(root, 'src/bootstrap/debugPlugins.ts');
|
||
if (!fs.existsSync(filePath)) {
|
||
console.warn('⚠️ src/bootstrap/debugPlugins.ts 不存在,跳过');
|
||
return;
|
||
}
|
||
|
||
let content = fs.readFileSync(filePath, 'utf-8');
|
||
|
||
if (content.includes(`'${moduleId}'`)) {
|
||
console.log(` ℹ️ debugPlugins 已包含 ${moduleId},跳过`);
|
||
return;
|
||
}
|
||
|
||
const newLoader = `
|
||
registerDebugPluginLoader('${moduleId}', async () => {
|
||
require('../plugins/${moduleId}/bundle');
|
||
});`;
|
||
|
||
content = content.trimEnd() + newLoader + '\n';
|
||
fs.writeFileSync(filePath, content, 'utf-8');
|
||
}
|
||
|
||
function updatePackageJson(root, moduleId) {
|
||
const filePath = path.join(root, 'package.json');
|
||
const pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||
|
||
const androidKey = `build:android:${moduleId}`;
|
||
const iosKey = `build:ios:${moduleId}`;
|
||
|
||
if (pkg.scripts[androidKey]) {
|
||
console.log(` ℹ️ package.json 已包含 ${androidKey},跳过`);
|
||
return;
|
||
}
|
||
|
||
// 添加构建脚本
|
||
pkg.scripts[androidKey] =
|
||
`mkdir -p bundle/android/${moduleId} && react-native bundle --platform android --dev false --entry-file ./src/plugins/${moduleId}/bundle.ts --bundle-output ./bundle/android/${moduleId}/${moduleId}.android.bundle --assets-dest ./bundle/android/${moduleId} --config metro.split.config.js --reset-cache`;
|
||
pkg.scripts[iosKey] =
|
||
`mkdir -p bundle/ios/${moduleId} && react-native bundle --platform ios --dev false --entry-file ./src/plugins/${moduleId}/bundle.ts --bundle-output ./bundle/ios/${moduleId}/${moduleId}.ios.bundle --assets-dest ./bundle/ios/${moduleId} --config metro.split.config.js --reset-cache`;
|
||
|
||
// 更新 plugins 聚合脚本
|
||
for (const platform of ['android', 'ios']) {
|
||
const key = `build:${platform}:plugins`;
|
||
if (pkg.scripts[key] && !pkg.scripts[key].includes(`${platform}:${moduleId}`)) {
|
||
pkg.scripts[key] = pkg.scripts[key] + ` && yarn build:${platform}:${moduleId}`;
|
||
}
|
||
}
|
||
|
||
// 更新 embedded 脚本
|
||
for (const platform of ['android', 'ios']) {
|
||
const key = `build:${platform}:embedded`;
|
||
if (pkg.scripts[key] && !pkg.scripts[key].includes(`${platform}:${moduleId}`)) {
|
||
// 在 prepare-embedded-bundles 之前插入
|
||
pkg.scripts[key] = pkg.scripts[key].replace(
|
||
/ && node \.\/scripts\/prepare/,
|
||
` && yarn build:${platform}:${moduleId} && node ./scripts/prepare`,
|
||
);
|
||
}
|
||
}
|
||
|
||
fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
||
}
|
||
|
||
function updateMetroConfig(root, moduleId) {
|
||
const filePath = path.join(root, 'metro.split.config.js');
|
||
if (!fs.existsSync(filePath)) {
|
||
console.warn('⚠️ metro.split.config.js 不存在,跳过');
|
||
return;
|
||
}
|
||
|
||
let content = fs.readFileSync(filePath, 'utf-8');
|
||
|
||
if (content.includes(`'${moduleId}'`)) {
|
||
console.log(` ℹ️ metro.split.config 已包含 ${moduleId},跳过`);
|
||
return;
|
||
}
|
||
|
||
// 计算 offset:buz1=11M, buz2=12M, buzN=(10+N)M
|
||
const num = parseInt(moduleId.replace(/\D/g, ''), 10);
|
||
const offset = Number.isFinite(num) ? (10 + num) * 1_000_000 : 30_000_000;
|
||
|
||
// 在 inferOffset 函数中添加新插件的分支
|
||
const insertPoint = content.lastIndexOf('return MODULE_OFFSETS');
|
||
if (insertPoint === -1) {
|
||
console.warn('⚠️ metro.split.config.js 格式异常,跳过');
|
||
return;
|
||
}
|
||
|
||
const newBranch = ` if (modulePath.includes(\`\${path.sep}${moduleId}\${path.sep}\`)) {
|
||
return ${offset};
|
||
}
|
||
|
||
`;
|
||
content = content.slice(0, insertPoint) + newBranch + content.slice(insertPoint);
|
||
fs.writeFileSync(filePath, content, 'utf-8');
|
||
}
|
||
|
||
function updateBabelAlias(root, moduleId) {
|
||
const filePath = path.join(root, 'babel.config.js');
|
||
if (!fs.existsSync(filePath)) return;
|
||
|
||
let content = fs.readFileSync(filePath, 'utf-8');
|
||
if (content.includes(`'@${moduleId}'`)) {
|
||
console.log(` ℹ️ babel alias 已包含 @${moduleId},跳过`);
|
||
return;
|
||
}
|
||
|
||
// 在 alias 对象最后一个条目后添加
|
||
content = content.replace(
|
||
/('@utils':\s*'[^']+',?\s*)}/,
|
||
`$1'@${moduleId}': './src/plugins/${moduleId}',\n }}`,
|
||
);
|
||
fs.writeFileSync(filePath, content, 'utf-8');
|
||
}
|
||
|
||
function updateTsconfig(root, moduleId) {
|
||
const filePath = path.join(root, 'tsconfig.json');
|
||
if (!fs.existsSync(filePath)) return;
|
||
|
||
let content = fs.readFileSync(filePath, 'utf-8');
|
||
if (content.includes(`"@${moduleId}/*"`)) {
|
||
console.log(` ℹ️ tsconfig 已包含 @${moduleId},跳过`);
|
||
return;
|
||
}
|
||
|
||
content = content.replace(
|
||
/("@utils\/\*":\s*\["src\/utils\/\*"\],?)/,
|
||
`$1\n "@${moduleId}/*": ["src/plugins/${moduleId}/*"]`,
|
||
);
|
||
fs.writeFileSync(filePath, content, 'utf-8');
|
||
}
|
||
|
||
// ─── 主流程 ──────────────────────────────────────────────────────────────────
|
||
|
||
function printUsage() {
|
||
console.log('');
|
||
console.log('用法:');
|
||
console.log(' 交互模式: node scripts/create-plugin.mjs');
|
||
console.log(' 命令行: node scripts/create-plugin.mjs <moduleId> [title] [subtitle] [accentColor]');
|
||
console.log('');
|
||
console.log('示例:');
|
||
console.log(' node scripts/create-plugin.mjs buz4 "IM 消息" "即时通讯业务组件" "#E74C3C"');
|
||
console.log(' node scripts/create-plugin.mjs buz5');
|
||
console.log('');
|
||
}
|
||
|
||
async function main() {
|
||
console.log('');
|
||
console.log('╔══════════════════════════════════════════╗');
|
||
console.log('║ XuqmGroup UpdateSDK — 插件脚手架工具 ║');
|
||
console.log('╚══════════════════════════════════════════╝');
|
||
|
||
const root = findProjectRoot();
|
||
const args = process.argv.slice(2);
|
||
|
||
// ── 解析参数(支持 CLI 和交互两种模式)──
|
||
let moduleId, title, subtitle, accentColor;
|
||
|
||
if (args.length > 0) {
|
||
// CLI 模式
|
||
[moduleId, title, subtitle, accentColor] = args;
|
||
} else {
|
||
// 交互模式
|
||
const rl = createRL();
|
||
try {
|
||
moduleId = await ask(rl, '\n插件 ID(如 buz4): ');
|
||
title = await ask(rl, '插件标题(如 IM 消息): ');
|
||
subtitle = await ask(rl, '插件副标题(如 即时通讯业务组件): ');
|
||
accentColor = await ask(rl, '主题色(如 #0E84FA): ');
|
||
} finally {
|
||
rl.close();
|
||
}
|
||
}
|
||
|
||
// ── 校验 ──
|
||
if (!moduleId || !/^[a-z][a-z0-9]*$/.test(moduleId)) {
|
||
console.error('\n❌ moduleId 必须以小写字母开头,只包含小写字母和数字');
|
||
printUsage();
|
||
process.exit(1);
|
||
}
|
||
|
||
title = title || moduleId;
|
||
subtitle = subtitle || `${title}业务组件`;
|
||
accentColor = accentColor || '#0E84FA';
|
||
|
||
// 唯一性校验
|
||
const existing = getExistingPluginIds(root);
|
||
if (existing.has(moduleId)) {
|
||
console.error(`\n❌ moduleId "${moduleId}" 已存在,请使用其他 ID`);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log('');
|
||
console.log(`📦 创建插件: ${moduleId}`);
|
||
console.log(` 标题: ${title}`);
|
||
console.log(` 副标题: ${subtitle}`);
|
||
console.log(` 主题色: ${accentColor}`);
|
||
console.log('');
|
||
|
||
// ── 创建目录 ──
|
||
const pluginDir = path.join(root, 'src/plugins', moduleId);
|
||
fs.mkdirSync(pluginDir, { recursive: true });
|
||
|
||
// ── 生成文件 ──
|
||
const files = [
|
||
{
|
||
path: path.join(pluginDir, 'bundle.ts'),
|
||
content: generateBundleTs(moduleId, title, subtitle, accentColor),
|
||
desc: 'bundle 入口(自动注册 UpdateSDK + pluginRuntime)',
|
||
},
|
||
{
|
||
path: path.join(pluginDir, `${pascalCase(moduleId)}Screen.tsx`),
|
||
content: generateScreenTs(moduleId),
|
||
desc: 'Screen 骨架',
|
||
},
|
||
{
|
||
path: path.join(pluginDir, 'plugin.json'),
|
||
content: generatePluginJson(moduleId),
|
||
desc: '插件元数据',
|
||
},
|
||
];
|
||
|
||
for (const file of files) {
|
||
fs.writeFileSync(file.path, file.content, 'utf-8');
|
||
console.log(` ✅ ${path.relative(root, file.path)} — ${file.desc}`);
|
||
}
|
||
|
||
// ── 更新宿主配置 ──
|
||
console.log('');
|
||
console.log('🔗 更新宿主配置:');
|
||
|
||
updatePluginCatalog(root, moduleId, title, accentColor);
|
||
console.log(' ✅ pluginCatalog.ts');
|
||
|
||
updateDebugPlugins(root, moduleId);
|
||
console.log(' ✅ debugPlugins.ts');
|
||
|
||
updatePackageJson(root, moduleId);
|
||
console.log(' ✅ package.json(构建脚本)');
|
||
|
||
updateMetroConfig(root, moduleId);
|
||
console.log(' ✅ metro.split.config.js');
|
||
|
||
updateBabelAlias(root, moduleId);
|
||
console.log(' ✅ babel.config.js');
|
||
|
||
updateTsconfig(root, moduleId);
|
||
console.log(' ✅ tsconfig.json');
|
||
|
||
// ── 完成 ──
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════');
|
||
console.log(`✅ 插件 ${moduleId} 创建完成!`);
|
||
console.log('');
|
||
console.log('下一步:');
|
||
console.log(` 1. 编辑 src/plugins/${moduleId}/${pascalCase(moduleId)}Screen.tsx 实现业务 UI`);
|
||
console.log(` 2. 在 src/plugins/${moduleId}/ 下创建 services/、pages/ 等目录`);
|
||
console.log(` 3. yarn validate 确认无错误`);
|
||
console.log(` 4. yarn build:android:${moduleId} 构建 Android bundle`);
|
||
console.log('═══════════════════════════════════════════');
|
||
console.log('');
|
||
}
|
||
|
||
main();
|